From 06e035eadf0e1f6d0b6b92f99b16106b61f4b8ea Mon Sep 17 00:00:00 2001 From: dctouch Date: Sun, 24 May 2026 12:53:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BA=D0=B0=D0=B7=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=B8=D1=87=D0=B8=D0=BD=D1=8B=20=D0=B1?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20auto-c?= =?UTF-8?q?oder=20=D0=B2=20handoff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/domain_case_loop.py | 41 +++++++++++++++- scripts/stage_agent_loop.py | 2 + scripts/test_domain_case_loop_lead_handoff.py | 47 +++++++++++++++++++ scripts/test_stage_agent_loop.py | 2 + 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/scripts/domain_case_loop.py b/scripts/domain_case_loop.py index 2240658..0f6f400 100644 --- a/scripts/domain_case_loop.py +++ b/scripts/domain_case_loop.py @@ -5508,6 +5508,7 @@ def build_lead_coder_handoff( def build_lead_coder_handoff_markdown(handoff: dict[str, Any]) -> str: artifact_refs = handoff.get("artifact_refs") if isinstance(handoff.get("artifact_refs"), dict) else {} human_meaning = handoff.get("human_meaning") if isinstance(handoff.get("human_meaning"), dict) else {} + auto_coder_gate = handoff.get("auto_coder_gate") if isinstance(handoff.get("auto_coder_gate"), dict) else {} lines = [ "# Lead Codex repair handoff", "", @@ -5534,13 +5535,51 @@ def build_lead_coder_handoff_markdown(handoff: dict[str, Any]) -> str: f"- issue_codes: `{', '.join(normalize_string_list(handoff.get('issue_codes'))) or 'n/a'}`", f"- rerun_matrix: `{', '.join(normalize_string_list(handoff.get('rerun_matrix'))) or 'n/a'}`", "", + ] + if auto_coder_gate: + lines.extend( + [ + "## Auto-Coder Gate", + f"- allowed: `{bool(auto_coder_gate.get('allowed'))}`", + f"- reason: `{auto_coder_gate.get('reason') or 'n/a'}`", + f"- focus_id: `{auto_coder_gate.get('focus_id') or 'n/a'}`", + ] + ) + blocking_reasons = normalize_string_list(auto_coder_gate.get("blocking_reasons")) + lines.extend([f"- blocking_reason: `{item}`" for item in blocking_reasons[:12]] or ["- blocking_reason: `none`"]) + if len(blocking_reasons) > 12: + lines.append(f"- blocking_reason_extra_count: `{len(blocking_reasons) - 12}`") + issue_contracts = ( + auto_coder_gate.get("issue_catalog_contracts") + if isinstance(auto_coder_gate.get("issue_catalog_contracts"), dict) + else {} + ) + if issue_contracts: + lines.extend(["", "## Auto-Coder Catalog Contracts"]) + for issue_code, contract in sorted(issue_contracts.items()): + if not isinstance(contract, dict): + continue + lines.extend( + [ + f"- issue_code: `{issue_code}`", + f" expected_contract: `{contract.get('expected_answer_contract') or 'n/a'}`", + f" root_layers: `{', '.join(normalize_string_list(contract.get('root_layers'))) or 'n/a'}`", + f" allowed_patch_targets: `{', '.join(normalize_string_list(contract.get('allowed_patch_targets'))) or 'n/a'}`", + f" forbidden_patch_targets: `{', '.join(normalize_string_list(contract.get('forbidden_patch_targets'))) or 'n/a'}`", + f" rerun_matrix: `{', '.join(normalize_string_list(contract.get('rerun_matrix'))) or 'n/a'}`", + ] + ) + lines.append("") + lines.extend( + [ "## Human Meaning", f"- user_intent_summary: {human_meaning.get('user_intent_summary') or 'n/a'}", f"- expected_direct_answer: {human_meaning.get('expected_direct_answer') or 'n/a'}", f"- actual_direct_answer: {human_meaning.get('actual_direct_answer') or 'n/a'}", "", "## Primary Focus", - ] + ] + ) assigned_focus = handoff.get("assigned_primary_focus") if isinstance(handoff.get("assigned_primary_focus"), dict) else {} if assigned_focus: candidate_files = normalize_string_list(assigned_focus.get("candidate_files")) diff --git a/scripts/stage_agent_loop.py b/scripts/stage_agent_loop.py index 26a945d..9c37a42 100644 --- a/scripts/stage_agent_loop.py +++ b/scripts/stage_agent_loop.py @@ -860,6 +860,7 @@ def build_stage_summary( "last_deterministic_gate_ok": last_iteration.get("deterministic_gate_ok"), "last_deterministic_gate_reason": last_iteration.get("deterministic_gate_reason"), "latest_business_audit": repo_relative(Path(str(last_iteration.get("business_audit_path")))) if last_iteration.get("business_audit_path") else None, + "latest_auto_coder_gate": repo_relative(Path(str(last_iteration.get("auto_coder_gate_path")))) if last_iteration.get("auto_coder_gate_path") else None, "latest_lead_coder_handoff": repo_relative(Path(str(last_iteration.get("lead_coder_handoff_markdown_path") or loop_state.get("latest_lead_coder_handoff_markdown_path")))) if (last_iteration.get("lead_coder_handoff_markdown_path") or loop_state.get("latest_lead_coder_handoff_markdown_path")) else None, "latest_lead_coder_handoff_json": repo_relative(Path(str(last_iteration.get("lead_coder_handoff_path") or loop_state.get("latest_lead_coder_handoff_path")))) if (last_iteration.get("lead_coder_handoff_path") or loop_state.get("latest_lead_coder_handoff_path")) else None, "loop_accepted_gate": bool(last_iteration.get("accepted_gate")), @@ -970,6 +971,7 @@ def build_stage_handoff_markdown(summary: dict[str, Any]) -> str: f"- next_action: `{summary.get('next_action')}`", f"- loop_dir: `{summary.get('loop_dir')}`", f"- latest_business_audit: `{summary.get('latest_business_audit') or 'n/a'}`", + f"- latest_auto_coder_gate: `{summary.get('latest_auto_coder_gate') or 'n/a'}`", f"- latest_lead_coder_handoff: `{summary.get('latest_lead_coder_handoff') or 'n/a'}`", f"- stop_reason: {summary.get('stop_reason') or 'n/a'}", "", diff --git a/scripts/test_domain_case_loop_lead_handoff.py b/scripts/test_domain_case_loop_lead_handoff.py index 077fb36..0bfdb63 100644 --- a/scripts/test_domain_case_loop_lead_handoff.py +++ b/scripts/test_domain_case_loop_lead_handoff.py @@ -288,6 +288,53 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase): gate["blocking_reasons"], ) + def test_lead_handoff_markdown_surfaces_auto_coder_gate_blockers(self) -> None: + handoff = { + "repair_mode": "lead-handoff", + "loop_id": "demo_loop", + "iteration_id": "iteration_00", + "quality_score": 42, + "target_score": 88, + "loop_decision": "partial", + "deterministic_gate_ok": False, + "deterministic_gate_reason": "repair_targets_remaining=P0:1", + "artifact_refs": { + "business_audit": "artifacts/domain_runs/demo/business_audit.md", + "analyst_verdict": "artifacts/domain_runs/demo/analyst_verdict.json", + "repair_targets": "artifacts/domain_runs/demo/repair_targets.json", + "auto_coder_gate": "artifacts/domain_runs/demo/auto_coder_gate.json", + "pack_dir": "artifacts/domain_runs/demo/pack", + }, + "issue_codes": ["business_direct_answer_missing"], + "rerun_matrix": ["failed_scenario", "accepted_smoke_pack"], + "human_meaning": {"user_intent_summary": "User needs a direct answer."}, + "top_repair_targets": [], + "candidate_files": [], + "lead_instructions": [], + "auto_coder_gate": { + "allowed": False, + "reason": "target_missing_evidence_paths:pack:s01", + "focus_id": "answer_shape|composeStage", + "blocking_reasons": ["target_missing_evidence_paths:pack:s01"], + "issue_catalog_contracts": { + "business_direct_answer_missing": { + "expected_answer_contract": "direct_answer_surface_v1", + "root_layers": ["answer_surface"], + "allowed_patch_targets": ["llm_normalizer/backend/src/services/address_runtime/composeStage.ts"], + "forbidden_patch_targets": ["routing rewrites"], + "rerun_matrix": ["failed_scenario", "accepted_smoke_pack"], + } + }, + }, + } + + markdown = dcl.build_lead_coder_handoff_markdown(handoff) + + self.assertIn("## Auto-Coder Gate", markdown) + self.assertIn("target_missing_evidence_paths:pack:s01", markdown) + self.assertIn("## Auto-Coder Catalog Contracts", markdown) + self.assertIn("direct_answer_surface_v1", markdown) + def test_analyst_priority_targets_become_lead_repair_targets(self) -> None: repair_targets = { "pack_id": "demo_pack", diff --git a/scripts/test_stage_agent_loop.py b/scripts/test_stage_agent_loop.py index 72e3379..24cbcc8 100644 --- a/scripts/test_stage_agent_loop.py +++ b/scripts/test_stage_agent_loop.py @@ -227,6 +227,7 @@ class StageAgentLoopTests(unittest.TestCase): "accepted_gate": False, "deterministic_gate_ok": False, "business_audit_path": str(iteration_dir / "business_audit.md"), + "auto_coder_gate_path": str(iteration_dir / "auto_coder_gate.json"), "lead_coder_handoff_markdown_path": str(iteration_dir / "lead_coder_handoff.md"), "coder_status": "lead_handoff_required", } @@ -249,6 +250,7 @@ class StageAgentLoopTests(unittest.TestCase): self.assertEqual(summary["next_action"], "lead_coder_repair_required") self.assertIn("lead_coder_handoff", summary["latest_lead_coder_handoff"]) self.assertIn("business_audit", summary["latest_business_audit"]) + self.assertIn("auto_coder_gate", summary["latest_auto_coder_gate"]) def test_build_stage_summary_blocks_close_when_repair_lacks_validation(self) -> None: with tempfile.TemporaryDirectory() as tmp: