Провязать detector results в stage repair handoff
This commit is contained in:
parent
d24f8476b4
commit
92e075157f
|
|
@ -171,7 +171,7 @@ The status payload also exposes `effective_stage_status`, `effective_stage_statu
|
|||
|
||||
Use `python scripts/stage_agent_loop.py continue --manifest docs/orchestration/<stage_loop>.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-<id>` 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.
|
||||
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, detector result signals, and sample user-facing failures without forcing the coder to reread the entire GUI conversation first.
|
||||
|
||||
For live stage-pack failures, prefer `lead_coder_handoff.md` over immediately preparing a coder pass. The intent is: strong business audit first, Lead Codex code repair second, same replay/GUI validation third.
|
||||
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,52 @@ def load_gui_review_detector_gate(review_dir: Path) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def load_detector_results_for_stage_handoff(latest_gui_review: dict[str, Any]) -> dict[str, Any]:
|
||||
raw_path = str(latest_gui_review.get("detector_results_json") or "").strip()
|
||||
if not raw_path:
|
||||
return {}
|
||||
path = repo_path(raw_path)
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
return load_json_object(path, "Stage GUI detector_results.json")
|
||||
except (OSError, json.JSONDecodeError, RuntimeError):
|
||||
return {}
|
||||
|
||||
|
||||
def build_detector_result_signals(
|
||||
detector_results: dict[str, Any],
|
||||
*,
|
||||
issue_codes: list[str],
|
||||
limit: int = 8,
|
||||
) -> list[dict[str, Any]]:
|
||||
results = detector_results.get("results") if isinstance(detector_results.get("results"), list) else []
|
||||
wanted_issue_codes = set(issue_codes)
|
||||
signals: list[dict[str, Any]] = []
|
||||
for item in results:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
status = str(item.get("status") or "").strip()
|
||||
if status not in {"fail", "review"}:
|
||||
continue
|
||||
item_issue_codes = dcl.normalize_string_list(item.get("issue_codes"))
|
||||
if wanted_issue_codes and not wanted_issue_codes.intersection(item_issue_codes):
|
||||
continue
|
||||
evidence = item.get("evidence") if isinstance(item.get("evidence"), list) else []
|
||||
signals.append(
|
||||
{
|
||||
"detector": item.get("detector"),
|
||||
"status": status,
|
||||
"issue_codes": item_issue_codes,
|
||||
"message": item.get("message"),
|
||||
"evidence_preview": evidence[:3],
|
||||
}
|
||||
)
|
||||
if len(signals) >= limit:
|
||||
break
|
||||
return signals
|
||||
|
||||
|
||||
def unique_strings(values: list[str]) -> list[str]:
|
||||
result: list[str] = []
|
||||
for value in values:
|
||||
|
|
@ -1682,7 +1728,10 @@ def build_stage_repair_handoff(summary: dict[str, Any], review: dict[str, Any])
|
|||
detector_results_summary = (
|
||||
latest.get("detector_results_summary") if isinstance(latest.get("detector_results_summary"), dict) else {}
|
||||
)
|
||||
detector_results_for_gate = {"summary": detector_results_summary, "results": []}
|
||||
detector_results = load_detector_results_for_stage_handoff(latest)
|
||||
if detector_results:
|
||||
detector_results_summary = dcl.summarize_detector_results(detector_results)
|
||||
detector_results_for_gate = detector_results if detector_results else {"summary": detector_results_summary, "results": []}
|
||||
assigned_focus = (
|
||||
repair_contract.get("priority_foci", [None])[0]
|
||||
if isinstance(repair_contract.get("priority_foci"), list) and repair_contract.get("priority_foci")
|
||||
|
|
@ -1714,6 +1763,10 @@ def build_stage_repair_handoff(summary: dict[str, Any], review: dict[str, Any])
|
|||
"candidate_files": repair_contract.get("candidate_files"),
|
||||
"repair_contract": repair_contract,
|
||||
"auto_propose_gate": auto_propose_gate,
|
||||
"detector_result_signals": build_detector_result_signals(
|
||||
detector_results_for_gate,
|
||||
issue_codes=dcl.normalize_string_list(repair_contract.get("issue_codes")),
|
||||
),
|
||||
"primary_repair_targets": primary_targets,
|
||||
"sample_findings": sample_findings,
|
||||
}
|
||||
|
|
@ -1748,6 +1801,22 @@ def build_stage_repair_handoff_markdown(handoff: dict[str, Any]) -> str:
|
|||
"",
|
||||
]
|
||||
)
|
||||
detector_signals = (
|
||||
handoff.get("detector_result_signals") if isinstance(handoff.get("detector_result_signals"), list) else []
|
||||
)
|
||||
if detector_signals:
|
||||
lines.extend(["## Detector Result Signals"])
|
||||
for signal in detector_signals:
|
||||
if not isinstance(signal, dict):
|
||||
continue
|
||||
lines.extend(
|
||||
[
|
||||
f"- `{signal.get('status')}` `{signal.get('detector')}` "
|
||||
f"`{', '.join(dcl.normalize_string_list(signal.get('issue_codes'))) or 'n/a'}`",
|
||||
f" message: {str(signal.get('message') or '').strip()}",
|
||||
]
|
||||
)
|
||||
lines.append("")
|
||||
auto_gate = handoff.get("auto_propose_gate") if isinstance(handoff.get("auto_propose_gate"), dict) else {}
|
||||
if auto_gate:
|
||||
lines.extend(
|
||||
|
|
|
|||
|
|
@ -761,6 +761,80 @@ class StageAgentLoopTests(unittest.TestCase):
|
|||
self.assertTrue(handoff["auto_propose_gate"]["allowed"])
|
||||
self.assertEqual(handoff["auto_propose_gate"]["detector_results_summary"]["status"], "fail")
|
||||
|
||||
def test_stage_repair_handoff_loads_detector_result_signals_from_json(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
detector_results_path = Path(tmp) / "detector_results.json"
|
||||
write_json(
|
||||
detector_results_path,
|
||||
{
|
||||
"schema_version": "agent_detector_results_v1",
|
||||
"created_at": "2026-06-03T00:00:00+00:00",
|
||||
"artifact_dir": str(Path(tmp)),
|
||||
"registry_path": "docs/orchestration/detector_registry.json",
|
||||
"selected_detectors": ["business_direct_answer_missing_signal"],
|
||||
"artifact_counts": {},
|
||||
"summary": {
|
||||
"status": "fail",
|
||||
"detector_count": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"review": 0,
|
||||
"skipped": 0,
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"detector": "business_direct_answer_missing_signal",
|
||||
"kind": "semantic",
|
||||
"automation_level": "deterministic",
|
||||
"status": "fail",
|
||||
"issue_codes": ["business_direct_answer_missing"],
|
||||
"message": "Direct answer is missing on the first line.",
|
||||
"evidence": [{"turn_index": 19, "first_line": "Коротко: общий обзор"}],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
summary = {
|
||||
"stage_id": "agent_loop",
|
||||
"next_action": "continue_repair_from_gui_review_p0",
|
||||
"latest_gui_review": {
|
||||
"run_id": "assistant-stage1-test",
|
||||
"overall_business_status": "fail",
|
||||
"p0_findings": 1,
|
||||
"p1_findings": 0,
|
||||
"question_quality_score": 100,
|
||||
"review_json": "artifacts/domain_runs/gui_run_reviews/assistant-stage1-test/run_review.json",
|
||||
"review_markdown": "artifacts/domain_runs/gui_run_reviews/assistant-stage1-test/run_review.md",
|
||||
"repair_targets_json": "artifacts/domain_runs/gui_run_reviews/assistant-stage1-test/repair_targets.json",
|
||||
"conversation_pairs_json": "artifacts/domain_runs/gui_run_reviews/assistant-stage1-test/conversation_pairs.json",
|
||||
"detector_results_json": str(detector_results_path),
|
||||
"detector_results_status": "fail",
|
||||
"detector_results_summary": {"status": "fail", "detector_count": 1, "fail": 1},
|
||||
},
|
||||
}
|
||||
review = {
|
||||
"run_id": "assistant-stage1-test",
|
||||
"repair_targets": [
|
||||
{
|
||||
"problem_layer": "answer_shape_mismatch",
|
||||
"issue_code": "business_direct_answer_missing",
|
||||
"severity": "P0",
|
||||
"occurrences": 1,
|
||||
}
|
||||
],
|
||||
"findings": [],
|
||||
}
|
||||
|
||||
handoff = stage_loop.build_stage_repair_handoff(summary, review)
|
||||
|
||||
self.assertTrue(handoff["auto_propose_gate"]["allowed"])
|
||||
self.assertEqual(
|
||||
handoff["auto_propose_gate"]["detector_results_summary"]["failed_detectors"],
|
||||
["business_direct_answer_missing_signal"],
|
||||
)
|
||||
self.assertEqual(handoff["detector_result_signals"][0]["detector"], "business_direct_answer_missing_signal")
|
||||
self.assertIn("Direct answer is missing", handoff["detector_result_signals"][0]["message"])
|
||||
|
||||
def test_stage_repair_handoff_blocks_auto_propose_for_non_allowlisted_gui_issue(self) -> None:
|
||||
summary = {
|
||||
"stage_id": "agent_loop",
|
||||
|
|
|
|||
Loading…
Reference in New Issue