diff --git a/AGENTS.md b/AGENTS.md index 9c23690..e721523 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,11 @@ - Use an integer `0%` to `100%` scale and keep the estimate architecture-aware, based on implemented runtime wiring, tests, replay coverage, and remaining integration risk. - Do not inflate progress because unit tests are green; semantic replay and real runtime wiring still count as unfinished work when they are pending. +## next_stage_plan_rule +- After applying fixes or completing a development stage, always provide `Дальше по плану:` in the close-out. +- The plan must list the next concrete module/stage action, the expected validation path, and whether the next step needs commit/push/manual replay. +- If the next plan is unclear or stale, the first next action must be to resync docs, graphify, and current stage artifacts before continuing implementation. + ## graphify This project has a graphify knowledge graph at graphify-out/. diff --git a/docs/orchestration/domain_scenario_loop_repo_adapter.md b/docs/orchestration/domain_scenario_loop_repo_adapter.md index d705c4e..431fdfe 100644 --- a/docs/orchestration/domain_scenario_loop_repo_adapter.md +++ b/docs/orchestration/domain_scenario_loop_repo_adapter.md @@ -93,6 +93,7 @@ Canonical commands: python scripts/stage_agent_loop.py plan --manifest docs/orchestration/.json python scripts/stage_agent_loop.py run --manifest docs/orchestration/.json python scripts/stage_agent_loop.py ingest-gui-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 summarize --manifest docs/orchestration/.json ``` @@ -139,6 +140,14 @@ It stores the GUI review under `artifacts/domain_runs/stage_agent_loops/.json +``` + +This writes `repair_iterations//repair_iteration_plan.json`, `repair_prompt.md`, and `repair_checklist.md`. The plan enriches GUI repair targets with candidate runtime files and rerun instructions, so the next coder pass can start from a bounded business defect instead of a full transcript archaeology dig. + ## Placeholder contract Scenario questions can reference earlier step outputs with placeholders such as: diff --git a/scripts/stage_agent_loop.py b/scripts/stage_agent_loop.py index e4eae0e..07f3264 100644 --- a/scripts/stage_agent_loop.py +++ b/scripts/stage_agent_loop.py @@ -10,6 +10,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any +import domain_case_loop as dcl import review_assistant_stage1_run as gui_review @@ -451,6 +452,142 @@ def save_stage_repair_handoff(stage_dir: Path, handoff: dict[str, Any]) -> None: write_text(stage_dir / "stage_repair_handoff.md", build_stage_repair_handoff_markdown(handoff)) +def enrich_repair_target_for_coder(target: dict[str, Any]) -> dict[str, Any]: + problem_layer = str(target.get("problem_layer") or "other").strip() or "other" + candidate_files = list(dcl.REPAIR_TARGET_FILE_HINTS.get(problem_layer) or dcl.REPAIR_TARGET_FILE_HINTS["other"]) + return { + **target, + "candidate_files": candidate_files, + } + + +def build_stage_repair_iteration_plan( + *, + stage_manifest: dict[str, Any], + stage_dir: Path, + handoff: dict[str, Any], + iteration_id: str, +) -> dict[str, Any]: + primary_targets = handoff.get("primary_repair_targets") if isinstance(handoff.get("primary_repair_targets"), list) else [] + enriched_targets = [ + enrich_repair_target_for_coder(target) + for target in primary_targets + if isinstance(target, dict) + ] + sample_findings = handoff.get("sample_findings") if isinstance(handoff.get("sample_findings"), list) else [] + candidate_files = list( + dict.fromkeys( + path + for target in enriched_targets + for path in target.get("candidate_files", []) + if isinstance(path, str) and path.strip() + ) + ) + return { + "schema_version": "stage_gui_repair_iteration_plan_v1", + "stage_id": stage_manifest["stage_id"], + "module_name": stage_manifest.get("module_name"), + "title": stage_manifest.get("title"), + "iteration_id": iteration_id, + "created_at": now_iso(), + "stage_dir": repo_relative(stage_dir), + "source_handoff": repo_relative(stage_dir / "stage_repair_handoff.json"), + "source_review_markdown": handoff.get("review_markdown"), + "source_repair_targets_json": handoff.get("repair_targets_json"), + "run_id": handoff.get("run_id"), + "next_action": handoff.get("next_action"), + "overall_business_status": handoff.get("overall_business_status"), + "p0_findings": handoff.get("p0_findings"), + "p1_findings": handoff.get("p1_findings"), + "question_quality_score": handoff.get("question_quality_score"), + "primary_repair_targets": enriched_targets, + "candidate_files": candidate_files, + "sample_findings": [finding for finding in sample_findings if isinstance(finding, dict)][:8], + "acceptance_rerun": { + "after_patch": [ + "run focused unit tests for touched backend/runtime code", + "run graphify rebuild after code changes", + "rerun the same GUI/user session or equivalent stage pack", + "ingest the new assistant-stage1 run id with stage_agent_loop.py ingest-gui-run", + "accept only when P0 is zero and P1 noise is either cleared or explicitly bounded", + ], + "ingest_command_template": ( + "python scripts/stage_agent_loop.py ingest-gui-run " + "--manifest --run-id assistant-stage1-" + ), + }, + } + + +def build_stage_repair_prompt(plan: dict[str, Any]) -> str: + return ( + "You are repairing the NDC_1C assistant from a stage-level GUI semantic review.\n\n" + "Use the repo rules from `AGENTS.md` and `.codex/skills/domain-case-loop/SKILL.md`.\n\n" + "Goal:\n" + "- Make the smallest safe runtime/domain patch that clears the primary repair targets.\n" + "- Preserve successful flows and avoid architecture drift.\n" + "- User-facing business meaning is the acceptance surface; route ids and tests are supporting evidence only.\n\n" + "Hard rules:\n" + "- Do not fabricate 1C data.\n" + "- Do not hide unsupported facts behind confident prose.\n" + "- Keep direct answers first for direct business questions.\n" + "- Keep methodology/evidence after the answer, not above it.\n" + "- Do not touch unrelated files.\n" + "- After code changes, run relevant tests and graphify rebuild.\n\n" + "Repair iteration plan JSON:\n" + "```json\n" + f"{json.dumps(plan, ensure_ascii=False, indent=2)}\n" + "```\n\n" + "Required outputs for the coding pass:\n" + "- A minimal patch in the working tree.\n" + "- A short patch summary.\n" + "- Verification commands and results.\n" + "- The next GUI/stage rerun command to validate the same semantic defect class.\n" + ) + + +def build_stage_repair_checklist(plan: dict[str, Any]) -> str: + lines = [ + "# Stage Repair Checklist", + "", + f"- stage_id: `{plan.get('stage_id')}`", + f"- iteration_id: `{plan.get('iteration_id')}`", + f"- source_run_id: `{plan.get('run_id')}`", + f"- next_action: `{plan.get('next_action')}`", + "", + "## Candidate Files", + ] + files = plan.get("candidate_files") if isinstance(plan.get("candidate_files"), list) else [] + lines.extend([f"- `{path}`" for path in files] if files else ["- none"]) + lines.extend(["", "## Primary Targets"]) + targets = plan.get("primary_repair_targets") if isinstance(plan.get("primary_repair_targets"), list) else [] + if not targets: + lines.append("- no repair targets") + else: + for target in targets: + if not isinstance(target, dict): + continue + lines.append( + f"- `{target.get('severity')}` `{target.get('problem_layer')}` / `{target.get('issue_code')}`: " + f"{target.get('occurrences')} occurrence(s)" + ) + lines.extend(["", "## Acceptance"]) + acceptance = plan.get("acceptance_rerun") if isinstance(plan.get("acceptance_rerun"), dict) else {} + for item in acceptance.get("after_patch", []) if isinstance(acceptance.get("after_patch"), list) else []: + lines.append(f"- {item}") + lines.append(f"- ingest template: `{acceptance.get('ingest_command_template') or 'n/a'}`") + return "\n".join(lines).strip() + "\n" + + +def save_stage_repair_iteration(stage_dir: Path, plan: dict[str, Any]) -> Path: + iteration_id = str(plan.get("iteration_id") or "repair_iteration").strip() + iteration_dir = stage_dir / "repair_iterations" / slugify(iteration_id) + write_json(iteration_dir / "repair_iteration_plan.json", plan) + write_text(iteration_dir / "repair_prompt.md", build_stage_repair_prompt(plan)) + write_text(iteration_dir / "repair_checklist.md", build_stage_repair_checklist(plan)) + return iteration_dir + + def handle_ingest_gui_run(args: argparse.Namespace) -> int: stage_manifest_path = repo_path(args.manifest) stage_manifest = load_stage_manifest(stage_manifest_path) @@ -485,6 +622,34 @@ def handle_ingest_gui_run(args: argparse.Namespace) -> int: return 0 +def handle_prepare_repair(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"]) + handoff_path = repo_path(args.handoff) if args.handoff else stage_dir / "stage_repair_handoff.json" + handoff = load_json_object(handoff_path, "Stage repair handoff") + iteration_id = str(args.iteration_id or f"repair_{slugify(str(handoff.get('run_id') or 'gui_run'))}").strip() + plan = build_stage_repair_iteration_plan( + stage_manifest=stage_manifest, + stage_dir=stage_dir, + handoff=handoff, + iteration_id=iteration_id, + ) + iteration_dir = save_stage_repair_iteration(stage_dir, plan) + payload = { + "schema_version": "stage_gui_repair_prepare_result_v1", + "stage_id": stage_manifest["stage_id"], + "iteration_id": iteration_id, + "iteration_dir": repo_relative(iteration_dir), + "repair_plan": repo_relative(iteration_dir / "repair_iteration_plan.json"), + "repair_prompt": repo_relative(iteration_dir / "repair_prompt.md"), + "repair_checklist": repo_relative(iteration_dir / "repair_checklist.md"), + "candidate_files": plan.get("candidate_files") or [], + } + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 0 + + def handle_plan(args: argparse.Namespace) -> int: stage_manifest_path = repo_path(args.manifest) stage_manifest = load_stage_manifest(stage_manifest_path) @@ -611,6 +776,15 @@ def build_parser() -> argparse.ArgumentParser: ingest_parser.add_argument("--reports-dir", default=str(gui_review.DEFAULT_REPORTS_DIR)) ingest_parser.add_argument("--review-output-dir") ingest_parser.set_defaults(func=handle_ingest_gui_run) + + repair_parser = subparsers.add_parser( + "prepare-repair", + help="Build coder-ready repair iteration artifacts from stage_repair_handoff.json.", + ) + add_common_args(repair_parser) + repair_parser.add_argument("--handoff") + repair_parser.add_argument("--iteration-id") + repair_parser.set_defaults(func=handle_prepare_repair) return parser diff --git a/scripts/test_stage_agent_loop.py b/scripts/test_stage_agent_loop.py index b18a9ab..78bc175 100644 --- a/scripts/test_stage_agent_loop.py +++ b/scripts/test_stage_agent_loop.py @@ -244,6 +244,109 @@ class StageAgentLoopTests(unittest.TestCase): self.assertEqual(handoff["primary_repair_targets"][0]["issue_code"], "business_direct_answer_missing") self.assertEqual(handoff["sample_findings"][0]["turn_index"], 19) + def test_repair_iteration_plan_adds_candidate_files(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + stage_dir = Path(tmp) / "stage" + handoff = { + "run_id": "assistant-stage1-test", + "next_action": "continue_repair_from_gui_review_p0", + "overall_business_status": "fail", + "p0_findings": 1, + "p1_findings": 0, + "question_quality_score": 100, + "review_markdown": "review.md", + "repair_targets_json": "repair_targets.json", + "primary_repair_targets": [ + { + "problem_layer": "answer_shape_mismatch", + "issue_code": "business_direct_answer_missing", + "severity": "P0", + "occurrences": 2, + } + ], + "sample_findings": [ + { + "turn_index": 19, + "severity": "P0", + "issue_codes": ["business_direct_answer_missing"], + "question": "какой у нас самый доходный год", + "assistant_first_line": "Коротко: Ограниченный бизнес-обзор...", + } + ], + } + + plan = stage_loop.build_stage_repair_iteration_plan( + stage_manifest={ + "stage_id": "agent_loop", + "module_name": "Agent Loop", + "title": "Agent Loop", + }, + stage_dir=stage_dir, + handoff=handoff, + iteration_id="repair_001", + ) + + self.assertEqual(plan["iteration_id"], "repair_001") + self.assertIn("llm_normalizer/backend/src/services/address_runtime/composeStage.ts", plan["candidate_files"]) + self.assertEqual(plan["primary_repair_targets"][0]["candidate_files"][0], "llm_normalizer/backend/src/services/address_runtime/composeStage.ts") + self.assertIn("ingest-gui-run", plan["acceptance_rerun"]["ingest_command_template"]) + + def test_handle_prepare_repair_materializes_prompt_and_checklist(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + manifest_path = root / "stage.json" + output_root = root / "stage_runs" + stage_dir = output_root / "agent_loop" + write_json( + manifest_path, + { + "stage_id": "agent_loop", + "module_name": "Agent Loop", + "title": "Agent Loop", + "pack_manifest": "docs/orchestration/demo_pack.json", + }, + ) + write_json( + stage_dir / "stage_repair_handoff.json", + { + "run_id": "assistant-stage1-test", + "next_action": "continue_repair_from_gui_review_p0", + "overall_business_status": "fail", + "p0_findings": 1, + "p1_findings": 0, + "question_quality_score": 100, + "review_markdown": "review.md", + "repair_targets_json": "repair_targets.json", + "primary_repair_targets": [ + { + "problem_layer": "business_utility_gap", + "issue_code": "business_answer_too_verbose", + "severity": "P1", + "occurrences": 4, + } + ], + "sample_findings": [], + }, + ) + + exit_code = stage_loop.handle_prepare_repair( + stage_args( + manifest=str(manifest_path), + output_root=str(output_root), + handoff=None, + iteration_id="repair_001", + ) + ) + iteration_dir = stage_dir / "repair_iterations" / "repair_001" + plan_exists = (iteration_dir / "repair_iteration_plan.json").exists() + prompt = (iteration_dir / "repair_prompt.md").read_text(encoding="utf-8") + checklist_exists = (iteration_dir / "repair_checklist.md").exists() + + self.assertEqual(exit_code, 0) + self.assertTrue(plan_exists) + self.assertTrue(checklist_exists) + self.assertIn("business_answer_too_verbose", prompt) + def test_handle_ingest_gui_run_materializes_stage_review(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp)