Провязать 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.
|
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.
|
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]:
|
def unique_strings(values: list[str]) -> list[str]:
|
||||||
result: list[str] = []
|
result: list[str] = []
|
||||||
for value in values:
|
for value in values:
|
||||||
|
|
@ -1682,7 +1728,10 @@ def build_stage_repair_handoff(summary: dict[str, Any], review: dict[str, Any])
|
||||||
detector_results_summary = (
|
detector_results_summary = (
|
||||||
latest.get("detector_results_summary") if isinstance(latest.get("detector_results_summary"), dict) else {}
|
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 = (
|
assigned_focus = (
|
||||||
repair_contract.get("priority_foci", [None])[0]
|
repair_contract.get("priority_foci", [None])[0]
|
||||||
if isinstance(repair_contract.get("priority_foci"), list) and repair_contract.get("priority_foci")
|
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"),
|
"candidate_files": repair_contract.get("candidate_files"),
|
||||||
"repair_contract": repair_contract,
|
"repair_contract": repair_contract,
|
||||||
"auto_propose_gate": auto_propose_gate,
|
"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,
|
"primary_repair_targets": primary_targets,
|
||||||
"sample_findings": sample_findings,
|
"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 {}
|
auto_gate = handoff.get("auto_propose_gate") if isinstance(handoff.get("auto_propose_gate"), dict) else {}
|
||||||
if auto_gate:
|
if auto_gate:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
|
|
|
||||||
|
|
@ -761,6 +761,80 @@ class StageAgentLoopTests(unittest.TestCase):
|
||||||
self.assertTrue(handoff["auto_propose_gate"]["allowed"])
|
self.assertTrue(handoff["auto_propose_gate"]["allowed"])
|
||||||
self.assertEqual(handoff["auto_propose_gate"]["detector_results_summary"]["status"], "fail")
|
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:
|
def test_stage_repair_handoff_blocks_auto_propose_for_non_allowlisted_gui_issue(self) -> None:
|
||||||
summary = {
|
summary = {
|
||||||
"stage_id": "agent_loop",
|
"stage_id": "agent_loop",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue