Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions argus_skill/life/supervisor/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
STALL_ESCALATION_AFTER_NO_PROGRESS_MISSIONS = 3
REPLAN_FILTER_REJECTION_LIMIT = 3
MANAGER_FEEDBACK_REPLAN_LIMIT = 3
MANAGER_FEEDBACK_INSTRUCTION_VERSION = 2
FULL_PAPER_GATE_DESCRIPTION = (
"the L2 reviewer's full pipeline checklist (research → submission)"
)
Expand Down Expand Up @@ -84,5 +85,6 @@ def consecutive_replan_escalation_threshold() -> int:
"STALL_ESCALATION_AFTER_NO_PROGRESS_MISSIONS",
"REPLAN_FILTER_REJECTION_LIMIT",
"MANAGER_FEEDBACK_REPLAN_LIMIT",
"MANAGER_FEEDBACK_INSTRUCTION_VERSION",
"FULL_PAPER_GATE_DESCRIPTION",
]
2 changes: 1 addition & 1 deletion argus_skill/life/supervisor/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,7 +993,7 @@ def _maybe_skip_inapplicable_final_submission_item(
"""
if self._planner_scope_from_item(item) != _PLANNER_SCOPE_FINAL_SUBMISSION:
return None
if self._effective_final_certification_gate(self._artifact_root()):
if self._final_submission_scope_is_applicable(self._artifact_root()):
return None

reason = (
Expand Down
69 changes: 61 additions & 8 deletions argus_skill/life/supervisor/_planning_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from ...core.planner_verdict import PlannerVerdictStatus
from ..memory import BacklogItem
from ._constants import (
MANAGER_FEEDBACK_INSTRUCTION_VERSION,
MANAGER_FEEDBACK_REPLAN_LIMIT,
PLAN_AWAITING,
PLAN_RETRY,
PLANNER_SCOPE_BOUNDED,
Expand All @@ -24,6 +26,7 @@
VERIFICATION_PROBE_COOLDOWN_SECONDS,
)
from ._helpers import _operator_only_external_blocker_wait_reason_for_project
from ._planning_cycle_helpers import _research_target_certification_required

log = logging.getLogger(__name__)

Expand All @@ -43,14 +46,16 @@ def _emit_planner_verdict(

def _planner_task_tags(self, task: Any) -> list[str]:
scope = self._normalize_planner_scope(getattr(task, "scope", ""))
if scope == PLANNER_SCOPE_FINAL_SUBMISSION and not self._effective_final_certification_gate(
self._artifact_root()
if (
scope == PLANNER_SCOPE_FINAL_SUBMISSION
and not self._final_submission_scope_is_applicable(
self._artifact_root()
)
):
# ``final_submission`` is a paper-only transport scope. A Planner
# may still choose it for another vertical's terminal review task,
# but persisting that tag makes ``tick()`` retire the task as stale
# and re-plan it forever. Normalize at the enqueue boundary; the
# old skip path remains as migration support for persisted rows.
# ``final_submission`` is reserved for an active authoritative
# completion gate. Persisting it elsewhere makes ``tick()`` retire
# the task as stale and re-plan it forever. Normalize at the enqueue
# boundary; the old skip path remains for persisted rows.
scope = PLANNER_SCOPE_BOUNDED
tags = ["planner", f"scope:{scope}"]
if scope == PLANNER_SCOPE_BOUNDED:
Expand Down Expand Up @@ -374,6 +379,18 @@ def _effective_final_certification_gate(self, workdir: object) -> bool:
vertical, project_root=workdir
).completion_gate == "certified"

def _final_submission_scope_is_applicable(self, workdir: object) -> bool:
"""Keep final-review transport for either supported completion gate.

A finite campaign may disable the legacy paper gate while still having
a persisted research-quality target. That target has the same need for
authoritative final Reviewer evidence, so its certification task must
not be normalized to ``bounded``.
"""
return self._effective_final_certification_gate(
workdir
) or _research_target_certification_required(workdir)

def _final_submission_signature(self) -> str:
from ..terminal_state import build_project_state_signature

Expand Down Expand Up @@ -777,6 +794,7 @@ def _persist_manager_planner_feedback(
return self._write_manager_planner_feedback(
{
"version": 1,
"instruction_version": MANAGER_FEEDBACK_INSTRUCTION_VERSION,
"active": True,
"objective_fingerprint": self._planner_waiting_objective_fingerprint(),
"stage": stage,
Expand All @@ -789,6 +807,38 @@ def _persist_manager_planner_feedback(
}
)

def _migrate_manager_planner_feedback_instruction(
self,
state: dict[str, Any],
) -> dict[str, Any]:
"""Give persisted feedback one retry when its routing contract changes."""
diagnostic = str(state.get("diagnostic") or "")
try:
instruction_version = int(state.get("instruction_version") or 1)
except (TypeError, ValueError):
instruction_version = 1
if (
diagnostic != "research_target_incomplete"
or instruction_version >= MANAGER_FEEDBACK_INSTRUCTION_VERSION
):
return state

migrated = dict(state)
migrated["instruction_version"] = MANAGER_FEEDBACK_INSTRUCTION_VERSION
migrated["attempts"] = min(
max(1, int(state.get("attempts") or 1)),
max(1, MANAGER_FEEDBACK_REPLAN_LIMIT - 1),
)
migrated["updated_at"] = time.time()
if not self._write_manager_planner_feedback(migrated):
return state
self._reset_idle_backoff()
self._emit_status(
"migrated Manager→Planner research certification routing; "
"allowing one bounded retry"
)
return migrated

def _clear_manager_planner_feedback(self) -> None:
state = self._load_manager_planner_feedback()
if state is None:
Expand All @@ -807,7 +857,10 @@ def _manager_planner_feedback_runtime_note(self) -> str:
"next executable certification task with "
"`TASK_SCOPE=final_submission`, so its successful Reviewer verdict can "
"be recorded as project-final evidence."
if diagnostic == "final_certification_missing"
if diagnostic in {
"final_certification_missing",
"research_target_incomplete",
}
else (
"You decide which tasks, if any, are appropriate; the harness does "
"not prescribe a repair or delivery task."
Expand Down
4 changes: 3 additions & 1 deletion argus_skill/life/supervisor/_planning_cycle_enqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,9 @@ def _pc_build_pending_items(self, state: _PlanCycleState) -> Any | None:
)
if (
canonical_scope == PLANNER_SCOPE_FINAL_SUBMISSION
and not self._effective_final_certification_gate(self._artifact_root())
and not self._final_submission_scope_is_applicable(
self._artifact_root()
)
):
canonical_scope = PLANNER_SCOPE_BOUNDED
canonical_acceptance = str(
Expand Down
22 changes: 22 additions & 0 deletions argus_skill/life/supervisor/_planning_cycle_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,28 @@ def _research_project_done_issue(
return f"missing_{target_level}_reviewer_certification"


def _research_target_certification_required(project_root: object) -> bool:
"""Whether the persisted research target needs final Reviewer evidence."""
from ...core.research_contract import (
research_target_contract,
resolve_research_target_level,
)
from ...skills.vertical_select import resolve_checklist_vertical
from ...verticals._base import load_vertical_contract

vertical = resolve_checklist_vertical(project_root)
if vertical is None:
return False
contract = research_target_contract(
supported_levels=load_vertical_contract(
vertical,
project_root=project_root,
).research_target_levels,
selected_level=resolve_research_target_level(project_root),
)
return contract.required and contract.selected_level is not None


def _staged_goal_completion_issue(project_root: object) -> str:
"""Require the ordinary Reviewer/Manager final-stage certificate."""
from ...skills.stage_machine import current_stage
Expand Down
3 changes: 3 additions & 0 deletions argus_skill/life/supervisor/_planning_cycle_intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ def _pc_intake_gate(self, state: _PlanCycleState) -> Any | None:
if revision_request is None:
feedback = self._load_manager_planner_feedback()
if feedback is not None:
feedback = self._migrate_manager_planner_feedback_instruction(
feedback
)
recorded_signature = str(
feedback.get("evidence_signature") or ""
)
Expand Down
12 changes: 12 additions & 0 deletions argus_skill/manager/_vertical_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,17 @@ def _commit_vertical_decision_locked(
restore_paths = [pipeline_state]
if adapted:
restore_paths.extend((domain_path, index_path))
refresh_research_target_epoch = bool(
force_stage_reset
or adapted
or (
old_vertical
and vertical_select.vertical_reached_own_terminal_stage(
self.project_root,
old_vertical,
)
)
)
with _restore_files_on_error(restore_paths):
if adapted:
revise_data_domain_stages(
Expand All @@ -558,6 +569,7 @@ def _commit_vertical_decision_locked(
research_target_level=decision.research_target_level or None,
workflow_mode=decision.workflow_mode,
target_venue=decision.target_venue or None,
refresh_research_target_epoch=refresh_research_target_epoch,
)
vertical_select.reset_stage_for_new_intent(
self.project_root,
Expand Down
4 changes: 2 additions & 2 deletions argus_skill/release_manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"package_version": "0.1.1",
"release_id": "0.1.1+e49bd9f10086a3df",
"release_id": "0.1.1+3832b4a22f9a3a53",
"schema_version": 1,
"source_digest": "e49bd9f10086a3df711c1be3594b60b70d593a0430c71f0e8f3488d29fe7644c"
"source_digest": "3832b4a22f9a3a538ed021364ae82755fe67aee3d76cb175ee67dcbe040a3933"
}
25 changes: 24 additions & 1 deletion argus_skill/skills/vertical_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ def persist_vertical(
research_target_level: str | None = None,
workflow_mode: str | None = None,
target_venue: str | None = None,
refresh_research_target_epoch: bool = False,
) -> None:
"""Persist the chosen ``vertical`` into ``research/PIPELINE_STATE.json``.

Expand All @@ -498,6 +499,7 @@ def persist_vertical(
persisting the Manager's decision is load-bearing, not best-effort.
A target-capable vertical may carry ``research_target_level``; vertical, target,
and target revision timestamp are then committed by the same atomic replace.
A confirmed replacement intent can explicitly start a new evidence epoch.

STAGE AUTHORITY — the harness must NOT control ``current_stage``; only the
reviewer agent moves it (advance via its verdict, or roll back via
Expand Down Expand Up @@ -530,6 +532,7 @@ def persist_vertical(
f"PIPELINE_STATE.json at {path} is not a JSON object"
)

previous_vertical = str(payload.get("vertical") or "").strip().lower()
payload["vertical"] = vert
if domain is not None:
from ..domains import require_domain
Expand Down Expand Up @@ -571,8 +574,28 @@ def persist_vertical(
raise ValueError(
f"invalid research target level: {research_target_level!r}"
)
previous_target = normalize_research_target_level(
payload.get("research_target_level")
)
try:
previous_target_set_at = float(
payload.get("research_target_set_at") or 0.0
)
except (TypeError, ValueError):
previous_target_set_at = 0.0
payload["research_target_level"] = normalized_target
payload["research_target_set_at"] = time.time()
# Reclassification commonly reasserts the same Manager decision after
# every mission. Refreshing this timestamp would make certification
# evidence from the just-finished mission look stale and can wedge the
# completion gate forever. Start a new evidence epoch only for a
# replacement intent, a changed vertical/target, or legacy state.
if (
refresh_research_target_epoch
or previous_vertical != vert
or previous_target != normalized_target
or previous_target_set_at <= 0.0
):
payload["research_target_set_at"] = time.time()
else:
from ..verticals._base import load_vertical, vertical_research_target_levels

Expand Down
4 changes: 2 additions & 2 deletions frontend/core/src/release.generated.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Generated by argus_skill.release_tools.generate_manifest. Do not edit.
export const RELEASE_ID = "0.1.1+e49bd9f10086a3df";
export const RELEASE_SOURCE_DIGEST = "e49bd9f10086a3df711c1be3594b60b70d593a0430c71f0e8f3488d29fe7644c";
export const RELEASE_ID = "0.1.1+3832b4a22f9a3a53";
export const RELEASE_SOURCE_DIGEST = "3832b4a22f9a3a538ed021364ae82755fe67aee3d76cb175ee67dcbe040a3933";
Loading