@hunter-harness/workflow-harness 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +5 -2
- package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -3
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +19 -4
- package/harness/bundles/general/claude-code/harness-run/checklist.md +7 -3
- package/harness/bundles/general/claude-code/harness-run/reference.md +4 -10
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +9 -4
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +18 -3
- package/harness/bundles/general/claude-code/harness-test/checklist.md +4 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +5 -5
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +190 -32
- package/harness/bundles/general/claude-code/scripts/harness_events.py +134 -40
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/claude-code/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +31 -3
- package/harness/bundles/general/claude-code/scripts/harness_state.py +148 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +5 -2
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -3
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +19 -4
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +7 -3
- package/harness/bundles/general/codebuddy/harness-run/reference.md +4 -10
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +9 -4
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +18 -3
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +4 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +5 -5
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +190 -32
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +134 -40
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +31 -3
- package/harness/bundles/general/codebuddy/scripts/harness_state.py +148 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +5 -2
- package/harness/bundles/general/codex/harness-archive/reference.md +1 -1
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/codex/harness-review/SKILL.md +5 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +19 -4
- package/harness/bundles/general/codex/harness-run/checklist.md +7 -3
- package/harness/bundles/general/codex/harness-run/reference.md +4 -10
- package/harness/bundles/general/codex/harness-submit/SKILL.md +9 -4
- package/harness/bundles/general/codex/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/codex/harness-test/SKILL.md +18 -3
- package/harness/bundles/general/codex/harness-test/checklist.md +4 -0
- package/harness/bundles/general/codex/harness-test/reference.md +5 -5
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/codex/scripts/harness_archive.py +190 -32
- package/harness/bundles/general/codex/scripts/harness_events.py +134 -40
- package/harness/bundles/general/codex/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/codex/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/codex/scripts/harness_profile.py +31 -3
- package/harness/bundles/general/codex/scripts/harness_state.py +148 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +5 -2
- package/harness/bundles/general/cursor/harness-archive/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +19 -4
- package/harness/bundles/general/cursor/harness-run/checklist.md +7 -3
- package/harness/bundles/general/cursor/harness-run/reference.md +4 -10
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +9 -4
- package/harness/bundles/general/cursor/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/cursor/harness-test/SKILL.md +18 -3
- package/harness/bundles/general/cursor/harness-test/checklist.md +4 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +5 -5
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/cursor/scripts/harness_archive.py +190 -32
- package/harness/bundles/general/cursor/scripts/harness_events.py +134 -40
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/cursor/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/cursor/scripts/harness_profile.py +31 -3
- package/harness/bundles/general/cursor/scripts/harness_state.py +148 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +5 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +5 -2
- package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +5 -2
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -3
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +22 -5
- package/harness/bundles/java/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +9 -4
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +20 -3
- package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +190 -32
- package/harness/bundles/java/claude-code/scripts/harness_events.py +134 -40
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/claude-code/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +31 -3
- package/harness/bundles/java/claude-code/scripts/harness_state.py +148 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +5 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +5 -2
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +5 -2
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -3
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +22 -5
- package/harness/bundles/java/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +9 -4
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +20 -3
- package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +190 -32
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +134 -40
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +31 -3
- package/harness/bundles/java/codebuddy/scripts/harness_state.py +148 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +5 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +5 -2
- package/harness/bundles/java/codex/harness-archive/reference.md +1 -1
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/java/codex/harness-package/SKILL.md +5 -2
- package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/codex/harness-review/SKILL.md +5 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +22 -5
- package/harness/bundles/java/codex/harness-run/reference.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +9 -4
- package/harness/bundles/java/codex/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/codex/harness-test/SKILL.md +20 -3
- package/harness/bundles/java/codex/harness-test/reference.md +1 -1
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/codex/scripts/harness_archive.py +190 -32
- package/harness/bundles/java/codex/scripts/harness_events.py +134 -40
- package/harness/bundles/java/codex/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/codex/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/codex/scripts/harness_profile.py +31 -3
- package/harness/bundles/java/codex/scripts/harness_state.py +148 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +5 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +5 -2
- package/harness/bundles/java/cursor/harness-archive/reference.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/java/cursor/harness-package/SKILL.md +5 -2
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +22 -5
- package/harness/bundles/java/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +9 -4
- package/harness/bundles/java/cursor/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/cursor/harness-test/SKILL.md +20 -3
- package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/cursor/scripts/harness_archive.py +190 -32
- package/harness/bundles/java/cursor/scripts/harness_events.py +134 -40
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/cursor/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/cursor/scripts/harness_profile.py +31 -3
- package/harness/bundles/java/cursor/scripts/harness_state.py +148 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +502 -0
- package/harness/manifests/general/claude-code.json +33 -29
- package/harness/manifests/general/codebuddy.json +33 -29
- package/harness/manifests/general/codex.json +33 -29
- package/harness/manifests/general/cursor.json +33 -29
- package/harness/manifests/java/claude-code.json +33 -29
- package/harness/manifests/java/codebuddy.json +33 -29
- package/harness/manifests/java/codex.json +33 -29
- package/harness/manifests/java/cursor.json +33 -29
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -622,13 +622,16 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
622
622
|
status = str(unit.get("status") or "").upper()
|
|
623
623
|
evidence = unit.get("evidence") or {}
|
|
624
624
|
if isinstance(evidence, dict):
|
|
625
|
-
run = evidence.get(
|
|
625
|
+
run = evidence.get(
|
|
626
|
+
"run",
|
|
627
|
+
evidence.get("testsRun", unit.get("run", unit.get("testsRun", 0))),
|
|
628
|
+
)
|
|
626
629
|
failures = evidence.get("failures", unit.get("failures", 0))
|
|
627
630
|
errors = evidence.get("errors", unit.get("errors", 0))
|
|
628
631
|
skipped = evidence.get("skipped", unit.get("skipped", 0))
|
|
629
632
|
pass_rate = evidence.get("passRate") or unit.get("passRate")
|
|
630
633
|
else:
|
|
631
|
-
run = unit.get("run", 0)
|
|
634
|
+
run = unit.get("run", unit.get("testsRun", 0))
|
|
632
635
|
failures = unit.get("failures", 0)
|
|
633
636
|
errors = unit.get("errors", 0)
|
|
634
637
|
skipped = unit.get("skipped", 0)
|
|
@@ -886,16 +889,21 @@ def _durations_from_event_phases(event_summary: dict[str, Any]) -> dict[str, Any
|
|
|
886
889
|
minutes = round((dur or 0) / 60000, 2) if dur is not None else 0
|
|
887
890
|
if dur:
|
|
888
891
|
total_ms += int(dur)
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
}
|
|
898
|
-
|
|
892
|
+
attempts = info.get("attempts") if isinstance(info.get("attempts"), list) else []
|
|
893
|
+
result = str(info.get("status") or "UNKNOWN").upper()
|
|
894
|
+
if result in {"PASS", "PASSED", "SUCCESS"}:
|
|
895
|
+
result = "OK"
|
|
896
|
+
elif result in {"FAILED", "ERROR"}:
|
|
897
|
+
result = "FAIL"
|
|
898
|
+
stages.append({
|
|
899
|
+
"stage": str(name),
|
|
900
|
+
"skill": f"harness-{name}",
|
|
901
|
+
"startedAt": info.get("started_at") or NOT_AVAILABLE,
|
|
902
|
+
"endedAt": info.get("ended_at") or NOT_AVAILABLE,
|
|
903
|
+
"minutes": minutes,
|
|
904
|
+
"result": result,
|
|
905
|
+
"attempts": attempts,
|
|
906
|
+
})
|
|
899
907
|
total_min = round(total_ms / 60000, 2)
|
|
900
908
|
return {
|
|
901
909
|
"totalLabel": f"约 {int(round(total_min))} 分",
|
|
@@ -917,11 +925,31 @@ def _stage_status_from_sources(
|
|
|
917
925
|
"submit": "OK",
|
|
918
926
|
"archive": "OK",
|
|
919
927
|
}
|
|
928
|
+
for event in events:
|
|
929
|
+
if event.get("type") != "phase.end":
|
|
930
|
+
continue
|
|
931
|
+
phase = str(event.get("phase") or "").lower()
|
|
932
|
+
raw = str(event.get("status") or "").upper()
|
|
933
|
+
if phase not in status or not raw:
|
|
934
|
+
continue
|
|
935
|
+
if raw in {"PASS", "PASSED", "SUCCESS"}:
|
|
936
|
+
raw = "OK"
|
|
937
|
+
elif raw in {"FAILED", "ERROR"}:
|
|
938
|
+
raw = "FAIL"
|
|
939
|
+
elif raw in {"SKIP", "SKIPPED"}:
|
|
940
|
+
raw = "USER_SKIPPED"
|
|
941
|
+
status[phase] = raw
|
|
920
942
|
# Issues in events can downgrade
|
|
921
943
|
for e in events:
|
|
922
944
|
if e.get("type") != "issue":
|
|
923
945
|
continue
|
|
924
946
|
sev = str(e.get("severity") or "").lower()
|
|
947
|
+
issue_text = " ".join(str(e.get(key) or "") for key in ("message", "note", "code")).lower()
|
|
948
|
+
if not sev:
|
|
949
|
+
if any(token in issue_text for token in ("fail", "error", "blocked", "失败", "阻塞")):
|
|
950
|
+
sev = "error"
|
|
951
|
+
elif any(token in issue_text for token in ("warn", "skip", "风险", "警告")):
|
|
952
|
+
sev = "warn"
|
|
925
953
|
phase = str(e.get("phase") or "").lower()
|
|
926
954
|
if phase in status and sev in {"error", "fail", "failed", "critical"}:
|
|
927
955
|
status[phase] = "FAIL"
|
|
@@ -930,14 +958,19 @@ def _stage_status_from_sources(
|
|
|
930
958
|
|
|
931
959
|
api = _ledger_api_tests(ledger)
|
|
932
960
|
db = _ledger_db_compat(ledger)
|
|
933
|
-
|
|
961
|
+
api_status = str(api.get("status") or "").upper()
|
|
962
|
+
if api_status in {"FAIL", "FAILED", "ERROR"} or int(api.get("failed") or 0) > 0:
|
|
963
|
+
status["test"] = "FAIL"
|
|
964
|
+
elif api_status in {"BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA"}:
|
|
965
|
+
status["test"] = api_status
|
|
966
|
+
elif api_status == "USER_SKIPPED":
|
|
934
967
|
status["test"] = "USER_SKIPPED"
|
|
935
968
|
elif db == "BLOCKED_BY_DBA":
|
|
936
969
|
status["test"] = "BLOCKED_BY_DBA"
|
|
937
970
|
elif api.get("status") == "NOT_RUN" and not find_test_reports(change_dir):
|
|
938
971
|
unit = _ledger_unit_tests(ledger)
|
|
939
972
|
if unit.get("source") == "not-run" and unit.get("run", 0) == 0:
|
|
940
|
-
status["test"] = "
|
|
973
|
+
status["test"] = "NOT_RUN"
|
|
941
974
|
|
|
942
975
|
if not find_review_reports(change_dir):
|
|
943
976
|
log_text = load_execution_log(change_dir)
|
|
@@ -954,8 +987,6 @@ def _compute_final_status(
|
|
|
954
987
|
api = verification.get("apiTests") or {}
|
|
955
988
|
db = str(verification.get("dbCompatibility") or "")
|
|
956
989
|
api_status = str(api.get("status") or "")
|
|
957
|
-
if api_status == "USER_SKIPPED" or db == "BLOCKED_BY_DBA":
|
|
958
|
-
return "CONDITIONAL_OK"
|
|
959
990
|
for v in stage_status.values():
|
|
960
991
|
if v == "FAIL":
|
|
961
992
|
return "FAIL"
|
|
@@ -964,8 +995,14 @@ def _compute_final_status(
|
|
|
964
995
|
return "FAIL"
|
|
965
996
|
if int(api.get("failed") or 0) > 0:
|
|
966
997
|
return "FAIL"
|
|
998
|
+
conditional = {
|
|
999
|
+
"USER_SKIPPED", "BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA",
|
|
1000
|
+
"NOT_RUN", "PARTIAL",
|
|
1001
|
+
}
|
|
1002
|
+
if api_status in conditional or db in conditional:
|
|
1003
|
+
return "CONDITIONAL_OK"
|
|
967
1004
|
for v in stage_status.values():
|
|
968
|
-
if v in {"WARN",
|
|
1005
|
+
if v in {"WARN", *conditional}:
|
|
969
1006
|
return "WARN" if v == "WARN" else "CONDITIONAL_OK"
|
|
970
1007
|
return "OK"
|
|
971
1008
|
|
|
@@ -984,6 +1021,8 @@ def _changed_files_from_git(
|
|
|
984
1021
|
changed: list[dict[str, Any]] = []
|
|
985
1022
|
if not base or not head:
|
|
986
1023
|
return diff_stat, changed
|
|
1024
|
+
# The ledger bounds the complete task. Using only ``head^..head`` would
|
|
1025
|
+
# silently omit earlier checkpoint commits from the same change.
|
|
987
1026
|
rng = f"{base}..{head}"
|
|
988
1027
|
code, out, _ = git_run(project, "diff", "--numstat", rng)
|
|
989
1028
|
if code != 0 or not out:
|
|
@@ -1040,6 +1079,97 @@ def _review_summary(change_dir: Path, existing: dict[str, Any] | None) -> dict[s
|
|
|
1040
1079
|
return base
|
|
1041
1080
|
|
|
1042
1081
|
|
|
1082
|
+
def _final_commit_from_sources(
|
|
1083
|
+
ledger: dict[str, Any] | None,
|
|
1084
|
+
events: list[dict[str, Any]],
|
|
1085
|
+
existing: dict[str, Any] | None,
|
|
1086
|
+
project: Path,
|
|
1087
|
+
) -> str:
|
|
1088
|
+
if ledger:
|
|
1089
|
+
for key in (
|
|
1090
|
+
"mergeFinalHash", "finalCommit", "finalHash", "changeCommit", "headCommit",
|
|
1091
|
+
):
|
|
1092
|
+
value = str(ledger.get(key) or "").strip()
|
|
1093
|
+
if value:
|
|
1094
|
+
return value
|
|
1095
|
+
commit_pattern = re.compile(r"\b[0-9a-f]{7,40}\b", re.IGNORECASE)
|
|
1096
|
+
for event in reversed(events):
|
|
1097
|
+
if str(event.get("phase") or "").lower() not in {"submit", "merge", "archive"}:
|
|
1098
|
+
continue
|
|
1099
|
+
text = " ".join(str(event.get(key) or "") for key in ("note", "message", "command"))
|
|
1100
|
+
match = commit_pattern.search(text)
|
|
1101
|
+
if match:
|
|
1102
|
+
return match.group(0)
|
|
1103
|
+
if existing and existing.get("finalCommit"):
|
|
1104
|
+
return str(existing["finalCommit"])
|
|
1105
|
+
code, head, _ = git_run(project, "rev-parse", "HEAD")
|
|
1106
|
+
return head if code == 0 and head else ""
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _business_goal_from_sources(change_dir: Path, events: list[dict[str, Any]]) -> str:
|
|
1110
|
+
plans_root = change_dir / "plans"
|
|
1111
|
+
primary = sorted(plans_root.glob("*-plan.md"))
|
|
1112
|
+
secondary = [
|
|
1113
|
+
path for path in sorted(plans_root.glob("*.md"))
|
|
1114
|
+
if path not in primary
|
|
1115
|
+
and "implementation-detail" not in path.name
|
|
1116
|
+
and "test-scenarios" not in path.name
|
|
1117
|
+
]
|
|
1118
|
+
for plan in [*primary, *secondary]:
|
|
1119
|
+
try:
|
|
1120
|
+
text = plan.read_text(encoding="utf-8-sig")
|
|
1121
|
+
except OSError:
|
|
1122
|
+
continue
|
|
1123
|
+
body = re.sub(r"\A---\s*\n.*?\n---\s*\n", "", text, count=1, flags=re.DOTALL)
|
|
1124
|
+
goal = re.search(r"(?im)^\s*(?:goal|目标|业务目标|需求)\s*[::]\s*(.+)$", body)
|
|
1125
|
+
if goal:
|
|
1126
|
+
return goal.group(1).strip()
|
|
1127
|
+
scope = re.search(r"(?im)^\s*>?\s*(?:变更范围|目标)\s*[::]\s*(.+)$", body)
|
|
1128
|
+
if scope:
|
|
1129
|
+
return scope.group(1).strip()
|
|
1130
|
+
first_task = re.search(r"(?m)^\s*\|\s*1\s*\|\s*([^|]+?)\s*\|", body)
|
|
1131
|
+
if first_task:
|
|
1132
|
+
return first_task.group(1).strip()
|
|
1133
|
+
for line in body.splitlines():
|
|
1134
|
+
clean = line.strip().lstrip("#").strip()
|
|
1135
|
+
if clean and not clean.startswith(("---", ">", "|")) and len(clean) > 8:
|
|
1136
|
+
return clean
|
|
1137
|
+
for event in events:
|
|
1138
|
+
if event.get("type") == "decision":
|
|
1139
|
+
value = str(event.get("decision") or event.get("note") or "").strip()
|
|
1140
|
+
if value:
|
|
1141
|
+
return re.sub(r"^(?:需求收敛|目标)\s*[::]\s*", "", value)
|
|
1142
|
+
return ""
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _timeline_from_events(event_summary: dict[str, Any], events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
1146
|
+
timeline: list[dict[str, Any]] = []
|
|
1147
|
+
for phase, info in (event_summary.get("phases") or {}).items():
|
|
1148
|
+
attempts = info.get("attempts") if isinstance(info.get("attempts"), list) else []
|
|
1149
|
+
for attempt in attempts:
|
|
1150
|
+
timeline.append({
|
|
1151
|
+
"phase": phase,
|
|
1152
|
+
"attempt": attempt.get("attempt"),
|
|
1153
|
+
"startedAt": attempt.get("started_at"),
|
|
1154
|
+
"endedAt": attempt.get("ended_at"),
|
|
1155
|
+
"durationMs": attempt.get("duration_ms"),
|
|
1156
|
+
"status": attempt.get("status") or "UNKNOWN",
|
|
1157
|
+
"executorTool": attempt.get("executor_tool"),
|
|
1158
|
+
"executorAgent": attempt.get("executor_agent"),
|
|
1159
|
+
"handoffFromTool": attempt.get("handoff_from_tool"),
|
|
1160
|
+
})
|
|
1161
|
+
for event in events:
|
|
1162
|
+
if event.get("type") not in {"decision", "issue"}:
|
|
1163
|
+
continue
|
|
1164
|
+
timeline.append({
|
|
1165
|
+
"phase": event.get("phase"),
|
|
1166
|
+
"timestamp": event.get("timestamp"),
|
|
1167
|
+
"type": event.get("type"),
|
|
1168
|
+
"summary": event.get("decision") or event.get("message") or event.get("note") or "",
|
|
1169
|
+
})
|
|
1170
|
+
return timeline
|
|
1171
|
+
|
|
1172
|
+
|
|
1043
1173
|
def collect_summary_data(
|
|
1044
1174
|
change_dir: Path,
|
|
1045
1175
|
*,
|
|
@@ -1118,18 +1248,14 @@ def collect_summary_data(
|
|
|
1118
1248
|
if existing and existing.get("businessGoal"):
|
|
1119
1249
|
data["businessGoal"] = existing["businessGoal"]
|
|
1120
1250
|
else:
|
|
1121
|
-
|
|
1251
|
+
inferred_goal = _business_goal_from_sources(change_dir, events)
|
|
1252
|
+
data["businessGoal"] = inferred_goal or (NOT_AVAILABLE if for_replay else "")
|
|
1122
1253
|
|
|
1123
1254
|
# commits
|
|
1124
1255
|
project = find_project_root(change_dir)
|
|
1125
1256
|
if not data.get("finalCommit") or str(data.get("finalCommit")).startswith("<"):
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
data["finalCommit"] = head
|
|
1129
|
-
elif existing and existing.get("finalCommit"):
|
|
1130
|
-
data["finalCommit"] = existing["finalCommit"]
|
|
1131
|
-
else:
|
|
1132
|
-
data["finalCommit"] = NOT_AVAILABLE if for_replay else ""
|
|
1257
|
+
final_commit = _final_commit_from_sources(ledger, events, existing, project)
|
|
1258
|
+
data["finalCommit"] = final_commit or (NOT_AVAILABLE if for_replay else "")
|
|
1133
1259
|
|
|
1134
1260
|
if not data.get("baseCommit") or str(data.get("baseCommit")).startswith("<"):
|
|
1135
1261
|
if ledger and ledger.get("baseCommit"):
|
|
@@ -1235,18 +1361,43 @@ def collect_summary_data(
|
|
|
1235
1361
|
if not isinstance(data.get("artifacts"), list):
|
|
1236
1362
|
data["artifacts"] = []
|
|
1237
1363
|
if not for_replay:
|
|
1238
|
-
|
|
1239
|
-
pass
|
|
1364
|
+
data["artifacts"] = _artifacts_from_events(events)
|
|
1240
1365
|
|
|
1241
1366
|
data["reviewSummary"] = _review_summary(change_dir, existing if for_replay else None)
|
|
1242
|
-
|
|
1367
|
+
if not for_replay:
|
|
1368
|
+
data["timeline"] = _timeline_from_events(event_summary, events)
|
|
1369
|
+
else:
|
|
1370
|
+
data.setdefault("timeline", [])
|
|
1243
1371
|
data.setdefault("uncommittedTestEvidence", [])
|
|
1244
1372
|
|
|
1245
|
-
#
|
|
1373
|
+
# Derive risks/actions from evidence. These fields are facts, not model prose.
|
|
1246
1374
|
if not for_replay:
|
|
1247
|
-
data["maintenanceNotes"] = [
|
|
1248
|
-
|
|
1375
|
+
data["maintenanceNotes"] = [
|
|
1376
|
+
str(event.get("note") or event.get("message") or "")
|
|
1377
|
+
for event in events
|
|
1378
|
+
if event.get("type") == "decision" and (event.get("note") or event.get("message"))
|
|
1379
|
+
]
|
|
1380
|
+
data["knownRisks"] = [
|
|
1381
|
+
{
|
|
1382
|
+
"phase": event.get("phase"),
|
|
1383
|
+
"severity": event.get("severity") or "unknown",
|
|
1384
|
+
"message": event.get("message") or event.get("note") or event.get("code") or "",
|
|
1385
|
+
}
|
|
1386
|
+
for event in events
|
|
1387
|
+
if event.get("type") == "issue"
|
|
1388
|
+
and not (
|
|
1389
|
+
event.get("phase") == "archive"
|
|
1390
|
+
and event.get("code") == "missing-command"
|
|
1391
|
+
)
|
|
1392
|
+
]
|
|
1249
1393
|
data["manualActions"] = []
|
|
1394
|
+
for name, value in (data.get("stageStatus") or {}).items():
|
|
1395
|
+
if value in {"BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA", "NOT_RUN", "USER_SKIPPED"}:
|
|
1396
|
+
data["manualActions"].append({
|
|
1397
|
+
"stage": name,
|
|
1398
|
+
"status": value,
|
|
1399
|
+
"action": "补充或确认该阶段的真实验证证据",
|
|
1400
|
+
})
|
|
1250
1401
|
else:
|
|
1251
1402
|
data.setdefault("maintenanceNotes", [])
|
|
1252
1403
|
data.setdefault("knownRisks", [])
|
|
@@ -1978,6 +2129,7 @@ def cmd_finalize(
|
|
|
1978
2129
|
_restore_target,
|
|
1979
2130
|
phase="archive",
|
|
1980
2131
|
type_="phase.end",
|
|
2132
|
+
status="FAIL",
|
|
1981
2133
|
note="finalize aborted; render failed; original preserved",
|
|
1982
2134
|
)
|
|
1983
2135
|
except OSError:
|
|
@@ -2117,6 +2269,7 @@ def cmd_finalize(
|
|
|
2117
2269
|
_safe_append_restored,
|
|
2118
2270
|
phase="archive",
|
|
2119
2271
|
type_="phase.end",
|
|
2272
|
+
status="FAIL",
|
|
2120
2273
|
note="finalize aborted; original preserved",
|
|
2121
2274
|
)
|
|
2122
2275
|
except OSError:
|
|
@@ -2168,7 +2321,12 @@ def cmd_finalize(
|
|
|
2168
2321
|
if service_result.get("warning"):
|
|
2169
2322
|
warnings.append(str(service_result["warning"]))
|
|
2170
2323
|
|
|
2171
|
-
_safe_append(
|
|
2324
|
+
_safe_append(
|
|
2325
|
+
phase="archive",
|
|
2326
|
+
type_="phase.end",
|
|
2327
|
+
status="WARN" if warnings else "OK",
|
|
2328
|
+
note="finalize complete",
|
|
2329
|
+
)
|
|
2172
2330
|
|
|
2173
2331
|
payload["ok"] = True
|
|
2174
2332
|
payload["warnings"] = warnings
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""Harness events.ndjson writer and execution-log renderer (D2).
|
|
3
3
|
|
|
4
4
|
Subcommands:
|
|
5
|
-
append — append one schema_version
|
|
5
|
+
append — append one schema_version 3 event, then auto-render execution-log.md
|
|
6
6
|
render — full re-render of logs/execution-log.md from events.ndjson
|
|
7
7
|
summary — phase durations, event counts, and issue list (JSON)
|
|
8
8
|
|
|
@@ -42,7 +42,7 @@ EVENT_TYPES = frozenset(
|
|
|
42
42
|
}
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
SCHEMA_VERSION =
|
|
45
|
+
SCHEMA_VERSION = 3
|
|
46
46
|
HEADER_LINE = (
|
|
47
47
|
"本文件由 harness_events.py 自动渲染,请勿手工编辑;事实源为 events.ndjson"
|
|
48
48
|
)
|
|
@@ -62,6 +62,13 @@ OPTIONAL_FIELDS = (
|
|
|
62
62
|
"message",
|
|
63
63
|
"decision",
|
|
64
64
|
"reason",
|
|
65
|
+
"run_id",
|
|
66
|
+
"attempt",
|
|
67
|
+
"executor_tool",
|
|
68
|
+
"executor_agent",
|
|
69
|
+
"executor_model",
|
|
70
|
+
"handoff_from_tool",
|
|
71
|
+
"handoff_reason",
|
|
65
72
|
)
|
|
66
73
|
|
|
67
74
|
|
|
@@ -120,7 +127,7 @@ def duration_ms_between(start: Any, end: Any) -> int | None:
|
|
|
120
127
|
|
|
121
128
|
|
|
122
129
|
def normalize_event(raw: dict[str, Any]) -> dict[str, Any]:
|
|
123
|
-
"""Normalize schema_version 1/2 events for rendering and summary."""
|
|
130
|
+
"""Normalize schema_version 1/2/3 events for rendering and summary."""
|
|
124
131
|
event = dict(raw)
|
|
125
132
|
version = event.get("schema_version", 1)
|
|
126
133
|
try:
|
|
@@ -273,6 +280,15 @@ def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dic
|
|
|
273
280
|
if value is None:
|
|
274
281
|
continue
|
|
275
282
|
event[field] = value
|
|
283
|
+
environment_defaults = {
|
|
284
|
+
"run_id": "HUNTER_HARNESS_RUN_ID",
|
|
285
|
+
"executor_tool": "HUNTER_HARNESS_TOOL",
|
|
286
|
+
"executor_agent": "HUNTER_HARNESS_AGENT",
|
|
287
|
+
"executor_model": "HUNTER_HARNESS_MODEL",
|
|
288
|
+
}
|
|
289
|
+
for field, env_name in environment_defaults.items():
|
|
290
|
+
if field not in event and os.environ.get(env_name):
|
|
291
|
+
event[field] = os.environ[env_name]
|
|
276
292
|
if "note" not in event:
|
|
277
293
|
event["note"] = ""
|
|
278
294
|
return event
|
|
@@ -333,6 +349,46 @@ def group_events_by_phase(events: list[dict[str, Any]]) -> list[tuple[str, list[
|
|
|
333
349
|
return [(phase, buckets[phase]) for phase in order]
|
|
334
350
|
|
|
335
351
|
|
|
352
|
+
def split_phase_attempts(phase_events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
353
|
+
"""Split repeated starts into attempts and flag legacy events written after end."""
|
|
354
|
+
attempts: list[dict[str, Any]] = []
|
|
355
|
+
current: dict[str, Any] | None = None
|
|
356
|
+
next_attempt = 1
|
|
357
|
+
for event in phase_events:
|
|
358
|
+
if event.get("type") == "phase.start":
|
|
359
|
+
if current is not None:
|
|
360
|
+
current["warnings"].append("new phase.start before prior phase.end")
|
|
361
|
+
attempts.append(current)
|
|
362
|
+
raw_attempt = event.get("attempt")
|
|
363
|
+
attempt = raw_attempt if isinstance(raw_attempt, int) and raw_attempt > 0 else next_attempt
|
|
364
|
+
next_attempt = max(next_attempt, attempt + 1)
|
|
365
|
+
current = {"attempt": attempt, "events": [event], "warnings": []}
|
|
366
|
+
continue
|
|
367
|
+
if current is None:
|
|
368
|
+
if attempts:
|
|
369
|
+
prior_type = attempts[-1]["events"][-1].get("type")
|
|
370
|
+
attempts[-1]["events"].append(event)
|
|
371
|
+
if prior_type == "phase.end":
|
|
372
|
+
attempts[-1]["warnings"].append("event recorded after phase.end")
|
|
373
|
+
continue
|
|
374
|
+
raw_attempt = event.get("attempt")
|
|
375
|
+
attempt = raw_attempt if isinstance(raw_attempt, int) and raw_attempt > 0 else next_attempt
|
|
376
|
+
next_attempt = max(next_attempt, attempt + 1)
|
|
377
|
+
current = {
|
|
378
|
+
"attempt": attempt,
|
|
379
|
+
"events": [event],
|
|
380
|
+
"warnings": ["missing phase.start"],
|
|
381
|
+
}
|
|
382
|
+
else:
|
|
383
|
+
current["events"].append(event)
|
|
384
|
+
if event.get("type") == "phase.end":
|
|
385
|
+
attempts.append(current)
|
|
386
|
+
current = None
|
|
387
|
+
if current is not None:
|
|
388
|
+
attempts.append(current)
|
|
389
|
+
return attempts
|
|
390
|
+
|
|
391
|
+
|
|
336
392
|
def phase_start_time(phase_events: list[dict[str, Any]]) -> str:
|
|
337
393
|
for event in phase_events:
|
|
338
394
|
if event.get("type") == "phase.start":
|
|
@@ -352,7 +408,9 @@ def phase_duration_ms(phase_events: list[dict[str, Any]]) -> int | None:
|
|
|
352
408
|
elif etype == "phase.end":
|
|
353
409
|
end_ts = event.get("timestamp")
|
|
354
410
|
if start_ts and end_ts:
|
|
355
|
-
|
|
411
|
+
stamps = [e.get("timestamp") for e in phase_events if e.get("timestamp")]
|
|
412
|
+
effective_end = stamps[-1] if stamps else end_ts
|
|
413
|
+
return duration_ms_between(start_ts, effective_end)
|
|
356
414
|
# Fallback: first to last timestamp in the phase bucket.
|
|
357
415
|
stamps = [e.get("timestamp") for e in phase_events if e.get("timestamp")]
|
|
358
416
|
if len(stamps) >= 2:
|
|
@@ -400,7 +458,15 @@ def render_event_line(event: dict[str, Any]) -> list[str]:
|
|
|
400
458
|
if etype == "phase.start":
|
|
401
459
|
note = str(event.get("note") or "").strip()
|
|
402
460
|
suffix = f" — {note}" if note else ""
|
|
403
|
-
|
|
461
|
+
lines = [f"- phase.start @ {event.get('timestamp', '')}{suffix}"]
|
|
462
|
+
tool = str(event.get("executor_tool") or "").strip()
|
|
463
|
+
agent = str(event.get("executor_agent") or "").strip()
|
|
464
|
+
handoff = str(event.get("handoff_from_tool") or "").strip()
|
|
465
|
+
if handoff and tool:
|
|
466
|
+
lines.append(f"- 工具交接: {handoff} → {tool}")
|
|
467
|
+
elif tool:
|
|
468
|
+
lines.append(f"- 执行来源: {tool}" + (f" / {agent}" if agent else ""))
|
|
469
|
+
return lines
|
|
404
470
|
if etype == "phase.end":
|
|
405
471
|
note = str(event.get("note") or "").strip()
|
|
406
472
|
suffix = f" — {note}" if note else ""
|
|
@@ -445,29 +511,34 @@ def render_execution_log(events: list[dict[str, Any]]) -> str:
|
|
|
445
511
|
return "\n".join(lines)
|
|
446
512
|
|
|
447
513
|
for phase, phase_events in group_events_by_phase(events):
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
514
|
+
attempts = split_phase_attempts(phase_events)
|
|
515
|
+
for attempt_record in attempts:
|
|
516
|
+
attempt_events = attempt_record["events"]
|
|
517
|
+
start = phase_start_time(attempt_events) or "—"
|
|
518
|
+
duration = phase_duration_ms(attempt_events)
|
|
519
|
+
label = f"{phase}(尝试 {attempt_record['attempt']})" if len(attempts) > 1 else phase
|
|
520
|
+
lines.append(f"## {label} — {start}")
|
|
521
|
+
lines.append("")
|
|
522
|
+
if duration is not None:
|
|
523
|
+
lines.append(f"- 阶段耗时: {format_duration(duration)}")
|
|
524
|
+
for warning in attempt_record["warnings"]:
|
|
525
|
+
lines.append(f"- 生命周期警告: {warning}")
|
|
526
|
+
buffer: list[dict[str, Any]] = []
|
|
527
|
+
|
|
528
|
+
def flush_commands() -> None:
|
|
529
|
+
nonlocal buffer
|
|
530
|
+
if buffer:
|
|
531
|
+
lines.extend(render_command_block(buffer))
|
|
532
|
+
buffer = []
|
|
533
|
+
|
|
534
|
+
for event in attempt_events:
|
|
535
|
+
if event.get("type") == "command":
|
|
536
|
+
buffer.append(event)
|
|
537
|
+
continue
|
|
538
|
+
flush_commands()
|
|
539
|
+
lines.extend(render_event_line(event))
|
|
467
540
|
flush_commands()
|
|
468
|
-
lines.
|
|
469
|
-
flush_commands()
|
|
470
|
-
lines.append("")
|
|
541
|
+
lines.append("")
|
|
471
542
|
|
|
472
543
|
return "\n".join(lines).rstrip() + "\n"
|
|
473
544
|
|
|
@@ -485,21 +556,37 @@ def build_summary(change_dir: Path, events: list[dict[str, Any]]) -> dict[str, A
|
|
|
485
556
|
issues: list[dict[str, Any]] = []
|
|
486
557
|
|
|
487
558
|
for phase, phase_events in group_events_by_phase(events):
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
559
|
+
attempt_records: list[dict[str, Any]] = []
|
|
560
|
+
for record in split_phase_attempts(phase_events):
|
|
561
|
+
attempt_events = record["events"]
|
|
562
|
+
starts = [e for e in attempt_events if e.get("type") == "phase.start"]
|
|
563
|
+
ends = [e for e in attempt_events if e.get("type") == "phase.end"]
|
|
564
|
+
start_ts = starts[0].get("timestamp") if starts else None
|
|
565
|
+
end_ts = ends[-1].get("timestamp") if ends else None
|
|
566
|
+
provenance = starts[0] if starts else (attempt_events[0] if attempt_events else {})
|
|
567
|
+
attempt_records.append({
|
|
568
|
+
"attempt": record["attempt"],
|
|
569
|
+
"event_count": len(attempt_events),
|
|
570
|
+
"started_at": start_ts,
|
|
571
|
+
"ended_at": end_ts,
|
|
572
|
+
"duration_ms": phase_duration_ms(attempt_events),
|
|
573
|
+
"status": ends[-1].get("status") if ends else None,
|
|
574
|
+
"run_id": provenance.get("run_id"),
|
|
575
|
+
"executor_tool": provenance.get("executor_tool"),
|
|
576
|
+
"executor_agent": provenance.get("executor_agent"),
|
|
577
|
+
"handoff_from_tool": provenance.get("handoff_from_tool"),
|
|
578
|
+
"warnings": record["warnings"],
|
|
579
|
+
})
|
|
580
|
+
durations = [a["duration_ms"] for a in attempt_records if a["duration_ms"] is not None]
|
|
581
|
+
first = attempt_records[0] if attempt_records else {}
|
|
582
|
+
last = attempt_records[-1] if attempt_records else {}
|
|
498
583
|
phases[phase] = {
|
|
499
584
|
"event_count": len(phase_events),
|
|
500
|
-
"started_at":
|
|
501
|
-
"ended_at":
|
|
502
|
-
"duration_ms":
|
|
585
|
+
"started_at": first.get("started_at"),
|
|
586
|
+
"ended_at": last.get("ended_at"),
|
|
587
|
+
"duration_ms": sum(durations) if durations else None,
|
|
588
|
+
"status": last.get("status"),
|
|
589
|
+
"attempts": attempt_records,
|
|
503
590
|
}
|
|
504
591
|
|
|
505
592
|
for event in events:
|
|
@@ -649,6 +736,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
649
736
|
p_append.add_argument("--message", default=None)
|
|
650
737
|
p_append.add_argument("--decision", default=None)
|
|
651
738
|
p_append.add_argument("--reason", default=None)
|
|
739
|
+
p_append.add_argument("--run-id", default=None)
|
|
740
|
+
p_append.add_argument("--attempt", type=int, default=None)
|
|
741
|
+
p_append.add_argument("--executor-tool", default=None)
|
|
742
|
+
p_append.add_argument("--executor-agent", default=None)
|
|
743
|
+
p_append.add_argument("--executor-model", default=None)
|
|
744
|
+
p_append.add_argument("--handoff-from-tool", default=None)
|
|
745
|
+
p_append.add_argument("--handoff-reason", default=None)
|
|
652
746
|
p_append.set_defaults(func=cmd_append)
|
|
653
747
|
|
|
654
748
|
p_render = sub.add_parser(
|