diff --git a/docs/orchestration/domain_scenario_loop_repo_adapter.md b/docs/orchestration/domain_scenario_loop_repo_adapter.md index ee6f2f7..1a4c2f9 100644 --- a/docs/orchestration/domain_scenario_loop_repo_adapter.md +++ b/docs/orchestration/domain_scenario_loop_repo_adapter.md @@ -167,6 +167,8 @@ It stores the GUI review under `artifacts/domain_runs/stage_agent_loops/.json` as the cheap read-only checkpoint before continuing a stage. It prints the current next action, closing gate, latest GUI run, latest repair coder status, latest repair validation status, and cold-start continuation artifacts such as `domain_pack_loop.command.txt` without modifying artifacts. +The status payload also exposes `effective_stage_status`, `effective_stage_status_source`, and `latest_gui_review_superseded_by_validation`. These fields prevent stale GUI failures from looking current after a later accepted repair validation or domain-loop replay has already superseded them. Keep the historic `latest_gui_business_status` visible for audit, but use the effective status to decide whether the next action is repair, validation, or manual confirmation. + Use `python scripts/stage_agent_loop.py continue --manifest docs/orchestration/.json` as the safe one-command continuation layer. From a cold start it materializes `domain_pack_loop.command.txt` without launching the long live loop; after a GUI review it can prepare a repair iteration and materialize `run-repair --dry-run` automatically; it will not run the real coder pass unless `--execute-repair` is passed, and it waits for a `--run-id assistant-stage1-` when the next required step is post-repair rerun/ingest validation. It also writes `stage_repair_handoff.md/json` next to the stage summary. That handoff is the preferred input for the next coder pass: it lists primary repair targets and sample user-facing failures without forcing the coder to reread the entire GUI conversation first. diff --git a/scripts/stage_agent_loop.py b/scripts/stage_agent_loop.py index 4951b0c..d35d76b 100644 --- a/scripts/stage_agent_loop.py +++ b/scripts/stage_agent_loop.py @@ -2119,6 +2119,46 @@ def handle_ingest_domain_loop_validation(args: argparse.Namespace) -> int: return 0 +def stage_validation_passed(validation: dict[str, Any]) -> bool: + return bool(validation.get("accepted_after_repair")) and str(validation.get("validation_status") or "").strip() == "passed" + + +def build_effective_stage_status( + summary: dict[str, Any], + latest_gui_review: dict[str, Any], + latest_repair_validation: dict[str, Any], + latest_replay_validation: dict[str, Any], +) -> dict[str, Any]: + gui_status = str(latest_gui_review.get("overall_business_status") or "").strip() + gui_is_negative = bool(gui_status and gui_status not in {"pass", "accepted"}) + validation = latest_repair_validation if stage_validation_passed(latest_repair_validation) else {} + if not validation and stage_validation_passed(latest_replay_validation): + validation = latest_replay_validation + if validation: + return { + "status": "accepted_after_validation" if summary.get("accepted_gate") else "validated_after_repair", + "source": validation.get("validation_source") or "repair_validation", + "validation_run_id": validation.get("validation_run_id"), + "supersedes_latest_gui_review": gui_is_negative, + "superseded_gui_run_id": latest_gui_review.get("run_id") if gui_is_negative else None, + } + if gui_status: + return { + "status": gui_status, + "source": "latest_gui_review", + "validation_run_id": None, + "supersedes_latest_gui_review": False, + "superseded_gui_run_id": None, + } + return { + "status": summary.get("loop_final_status"), + "source": "stage_summary", + "validation_run_id": None, + "supersedes_latest_gui_review": False, + "superseded_gui_run_id": None, + } + + def ingest_replay_validation(args: argparse.Namespace) -> dict[str, Any]: stage_manifest_path = repo_path(args.manifest) stage_manifest = load_stage_manifest(stage_manifest_path) @@ -2307,6 +2347,12 @@ def build_stage_status(stage_manifest: dict[str, Any], stage_dir: Path) -> dict[ if isinstance(summary.get("stage_closing_gate"), dict) else {} ) + effective_status = build_effective_stage_status( + summary, + latest_gui_review, + latest_repair_validation, + latest_replay_validation, + ) return { "schema_version": "stage_agent_loop_status_v1", "stage_id": stage_manifest["stage_id"], @@ -2321,6 +2367,11 @@ def build_stage_status(stage_manifest: dict[str, Any], stage_dir: Path) -> dict[ "stage_closing_gate": stage_closing_gate or None, "next_action": next_action, "next_step_guidance": next_step_guidance, + "effective_stage_status": effective_status.get("status"), + "effective_stage_status_source": effective_status.get("source"), + "latest_gui_review_superseded_by_validation": effective_status.get("supersedes_latest_gui_review"), + "superseding_validation_run_id": effective_status.get("validation_run_id"), + "superseded_gui_run_id": effective_status.get("superseded_gui_run_id"), "latest_gui_run_id": latest_gui_review.get("run_id"), "latest_gui_business_status": latest_gui_review.get("overall_business_status"), "latest_business_audit": summary.get("latest_business_audit"), diff --git a/scripts/test_stage_agent_loop.py b/scripts/test_stage_agent_loop.py index b1a7a92..4438356 100644 --- a/scripts/test_stage_agent_loop.py +++ b/scripts/test_stage_agent_loop.py @@ -514,6 +514,11 @@ class StageAgentLoopTests(unittest.TestCase): self.assertTrue(summary["latest_repair_validation"]["accepted_after_repair"]) self.assertEqual(status["latest_validation_source"], "domain_truth_harness.run-live") self.assertEqual(status["latest_replay_validation_status"], "passed") + self.assertEqual(status["effective_stage_status"], "accepted_after_validation") + self.assertEqual(status["effective_stage_status_source"], "domain_truth_harness.run-live") + self.assertTrue(status["latest_gui_review_superseded_by_validation"]) + self.assertEqual(status["superseding_validation_run_id"], "accepted_replay") + self.assertEqual(status["superseded_gui_run_id"], "assistant-stage1-before") def test_domain_loop_validation_stage_summary_closes_after_accepted_pack_loop(self) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -1023,6 +1028,9 @@ class StageAgentLoopTests(unittest.TestCase): self.assertEqual(status["stage_closing_gate"]["status"], "blocked_pending_repair_validation") self.assertEqual(status["latest_repair_coder_status"], "patched") self.assertEqual(status["latest_validation_status"], "failed_p0") + self.assertEqual(status["effective_stage_status"], "fail") + self.assertEqual(status["effective_stage_status_source"], "latest_gui_review") + self.assertFalse(status["latest_gui_review_superseded_by_validation"]) def test_build_stage_status_derives_guidance_from_existing_next_action(self) -> None: with tempfile.TemporaryDirectory() as tmp: