diff --git a/docs/orchestration/domain_scenario_loop_repo_adapter.md b/docs/orchestration/domain_scenario_loop_repo_adapter.md index aa3f91d..98ecc96 100644 --- a/docs/orchestration/domain_scenario_loop_repo_adapter.md +++ b/docs/orchestration/domain_scenario_loop_repo_adapter.md @@ -95,6 +95,7 @@ python scripts/stage_agent_loop.py run --manifest docs/orchestration/.json --run-id assistant-stage1- python scripts/stage_agent_loop.py prepare-repair --manifest docs/orchestration/.json python scripts/stage_agent_loop.py run-repair --manifest docs/orchestration/.json --dry-run +python scripts/stage_agent_loop.py status --manifest docs/orchestration/.json python scripts/stage_agent_loop.py summarize --manifest docs/orchestration/.json ``` @@ -141,6 +142,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, and latest repair validation status without modifying artifacts. + 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. To prepare the next repair iteration from that handoff, run: diff --git a/scripts/stage_agent_loop.py b/scripts/stage_agent_loop.py index b8fb1c8..5492ba4 100644 --- a/scripts/stage_agent_loop.py +++ b/scripts/stage_agent_loop.py @@ -335,6 +335,10 @@ def build_next_step_guidance(next_action: str) -> dict[str, Any]: "inspect_gui_review_status": [ "inspect run_review.md and repair_targets.json manually", ], + "run_stage_loop_or_ingest_gui_run": [ + "python scripts/stage_agent_loop.py run --manifest --dry-run", + "python scripts/stage_agent_loop.py ingest-gui-run --manifest --run-id assistant-stage1-", + ], } return { "next_action": next_action, @@ -1098,6 +1102,58 @@ def handle_plan(args: argparse.Namespace) -> int: return 0 +def build_stage_status(stage_manifest: dict[str, Any], stage_dir: Path) -> dict[str, Any]: + summary_path = stage_dir / "stage_loop_summary.json" + summary = load_json_object(summary_path, "Existing stage summary") if summary_path.exists() else {} + latest_gui_review = summary.get("latest_gui_review") if isinstance(summary.get("latest_gui_review"), dict) else {} + latest_repair_execution = ( + summary.get("latest_repair_execution") + if isinstance(summary.get("latest_repair_execution"), dict) + else {} + ) + latest_repair_validation = ( + summary.get("latest_repair_validation") + if isinstance(summary.get("latest_repair_validation"), dict) + else {} + ) + stage_closing_gate = ( + summary.get("stage_closing_gate") + if isinstance(summary.get("stage_closing_gate"), dict) + else {} + ) + return { + "schema_version": "stage_agent_loop_status_v1", + "stage_id": stage_manifest["stage_id"], + "module_name": stage_manifest.get("module_name"), + "title": stage_manifest.get("title"), + "stage_dir": repo_relative(stage_dir), + "summary_exists": bool(summary), + "loop_final_status": summary.get("loop_final_status"), + "accepted_gate": summary.get("accepted_gate"), + "loop_accepted_gate": summary.get("loop_accepted_gate"), + "stage_closing_gate": stage_closing_gate or None, + "next_action": summary.get("next_action") or "run_stage_loop_or_ingest_gui_run", + "next_step_guidance": summary.get("next_step_guidance") or build_next_step_guidance("run_stage_loop_or_ingest_gui_run"), + "latest_gui_run_id": latest_gui_review.get("run_id"), + "latest_gui_business_status": latest_gui_review.get("overall_business_status"), + "latest_repair_coder_status": latest_repair_execution.get("coder_status"), + "latest_repair_dry_run": latest_repair_execution.get("dry_run"), + "latest_validation_run_id": latest_repair_validation.get("validation_run_id"), + "latest_validation_status": latest_repair_validation.get("validation_status"), + "accepted_after_repair": latest_repair_validation.get("accepted_after_repair"), + "summary_path": repo_relative(summary_path) if summary_path.exists() else None, + } + + +def handle_status(args: argparse.Namespace) -> int: + stage_manifest_path = repo_path(args.manifest) + stage_manifest = load_stage_manifest(stage_manifest_path) + stage_dir = stage_dir_for(repo_path(args.output_root), stage_manifest["stage_id"]) + payload = build_stage_status(stage_manifest, stage_dir) + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 0 + + def handle_summarize(args: argparse.Namespace) -> int: stage_manifest_path = repo_path(args.manifest) stage_manifest = load_stage_manifest(stage_manifest_path) @@ -1200,6 +1256,10 @@ def build_parser() -> argparse.ArgumentParser: summarize_parser.add_argument("--loop-dir") summarize_parser.set_defaults(func=handle_summarize) + status_parser = subparsers.add_parser("status", help="Print current stage summary, gates, and next action.") + add_common_args(status_parser) + status_parser.set_defaults(func=handle_status) + ingest_parser = subparsers.add_parser( "ingest-gui-run", help="Attach an existing assistant-stage1 GUI run review to the stage loop summary.", diff --git a/scripts/test_stage_agent_loop.py b/scripts/test_stage_agent_loop.py index 24371c5..3737cae 100644 --- a/scripts/test_stage_agent_loop.py +++ b/scripts/test_stage_agent_loop.py @@ -542,6 +542,56 @@ class StageAgentLoopTests(unittest.TestCase): self.assertEqual(stage_summary["next_action"], "execute_repair_without_dry_run_or_review_command") self.assertIn("run-repair", stage_summary["next_step_guidance"]["command_templates"][0]) + def test_build_stage_status_summarizes_current_gate_and_validation(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + stage_dir = root / "stage_runs" / "agent_loop" + write_json( + stage_dir / "stage_loop_summary.json", + { + "stage_id": "agent_loop", + "loop_final_status": "accepted", + "accepted_gate": False, + "loop_accepted_gate": True, + "next_action": "rerun_same_stage_or_gui_and_ingest_result", + "stage_closing_gate": { + "status": "blocked_pending_repair_validation", + "passed": False, + }, + "latest_gui_review": { + "run_id": "assistant-stage1-before", + "overall_business_status": "fail", + }, + "latest_repair_execution": { + "coder_status": "patched", + "dry_run": False, + }, + "latest_repair_validation": { + "validation_run_id": "assistant-stage1-rerun", + "validation_status": "failed_p0", + "accepted_after_repair": False, + }, + "next_step_guidance": { + "command_templates": ["rerun the same GUI/session or stage semantic pack"], + }, + }, + ) + + status = stage_loop.build_stage_status( + { + "stage_id": "agent_loop", + "module_name": "Agent Loop", + "title": "Agent Loop", + }, + stage_dir, + ) + + self.assertTrue(status["summary_exists"]) + self.assertEqual(status["next_action"], "rerun_same_stage_or_gui_and_ingest_result") + 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") + def test_resolve_stage_repair_iteration_auto_prepares_from_handoff(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp)