@hunter-harness/workflow-harness 0.2.17 → 0.2.19
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 +7 -1
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/reference.md +5 -5
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +11 -2
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +24 -8
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +199 -30
- package/harness/bundles/general/claude-code/scripts/harness_events.py +5 -18
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +153 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +15 -2
- package/harness/bundles/general/claude-code/scripts/harness_paths.py +170 -1
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +1 -0
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +4 -4
- package/harness/bundles/general/claude-code/scripts/harness_service.py +1 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +7 -1
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +5 -5
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +11 -2
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +24 -8
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +199 -30
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +5 -18
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +153 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +15 -2
- package/harness/bundles/general/codebuddy/scripts/harness_paths.py +170 -1
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +1 -0
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +4 -4
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +1 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +7 -1
- package/harness/bundles/general/codex/harness-archive/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/reference.md +5 -5
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-run/reference.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +11 -2
- package/harness/bundles/general/codex/harness-submit/checklist.md +24 -8
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +199 -30
- package/harness/bundles/general/codex/scripts/harness_events.py +5 -18
- package/harness/bundles/general/codex/scripts/harness_integration.py +153 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +15 -2
- package/harness/bundles/general/codex/scripts/harness_paths.py +170 -1
- package/harness/bundles/general/codex/scripts/harness_profile.py +1 -0
- package/harness/bundles/general/codex/scripts/harness_runtime.py +4 -4
- package/harness/bundles/general/codex/scripts/harness_service.py +1 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +7 -1
- package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/reference.md +5 -5
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +11 -2
- package/harness/bundles/general/cursor/harness-submit/checklist.md +24 -8
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +199 -30
- package/harness/bundles/general/cursor/scripts/harness_events.py +5 -18
- package/harness/bundles/general/cursor/scripts/harness_integration.py +153 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +15 -2
- package/harness/bundles/general/cursor/scripts/harness_paths.py +170 -1
- package/harness/bundles/general/cursor/scripts/harness_profile.py +1 -0
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +4 -4
- package/harness/bundles/general/cursor/scripts/harness_service.py +1 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +7 -1
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -0
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/reference.md +5 -5
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/checklist.md +2 -2
- package/harness/bundles/java/claude-code/harness-run/reference.md +352 -352
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +11 -2
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +24 -8
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +199 -30
- package/harness/bundles/java/claude-code/scripts/harness_events.py +5 -18
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +153 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +15 -2
- package/harness/bundles/java/claude-code/scripts/harness_paths.py +170 -1
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +1 -0
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +4 -4
- package/harness/bundles/java/claude-code/scripts/harness_service.py +1 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +7 -1
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -0
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +5 -5
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/checklist.md +2 -2
- package/harness/bundles/java/codebuddy/harness-run/reference.md +352 -352
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +11 -2
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +24 -8
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +199 -30
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +5 -18
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +153 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +15 -2
- package/harness/bundles/java/codebuddy/scripts/harness_paths.py +170 -1
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +1 -0
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +4 -4
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +1 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +7 -1
- package/harness/bundles/java/codex/harness-archive/checklist.md +6 -0
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/reference.md +5 -5
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/checklist.md +2 -2
- package/harness/bundles/java/codex/harness-run/reference.md +352 -352
- package/harness/bundles/java/codex/harness-submit/SKILL.md +11 -2
- package/harness/bundles/java/codex/harness-submit/checklist.md +24 -8
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +199 -30
- package/harness/bundles/java/codex/scripts/harness_events.py +5 -18
- package/harness/bundles/java/codex/scripts/harness_integration.py +153 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +15 -2
- package/harness/bundles/java/codex/scripts/harness_paths.py +170 -1
- package/harness/bundles/java/codex/scripts/harness_profile.py +1 -0
- package/harness/bundles/java/codex/scripts/harness_runtime.py +4 -4
- package/harness/bundles/java/codex/scripts/harness_service.py +1 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +7 -1
- package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -0
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/reference.md +5 -5
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/checklist.md +2 -2
- package/harness/bundles/java/cursor/harness-run/reference.md +352 -352
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +11 -2
- package/harness/bundles/java/cursor/harness-submit/checklist.md +24 -8
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +199 -30
- package/harness/bundles/java/cursor/scripts/harness_events.py +5 -18
- package/harness/bundles/java/cursor/scripts/harness_integration.py +153 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +15 -2
- package/harness/bundles/java/cursor/scripts/harness_paths.py +170 -1
- package/harness/bundles/java/cursor/scripts/harness_profile.py +1 -0
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +4 -4
- package/harness/bundles/java/cursor/scripts/harness_service.py +1 -0
- package/harness/manifests/general/claude-code.json +26 -26
- package/harness/manifests/general/codebuddy.json +26 -26
- package/harness/manifests/general/codex.json +26 -26
- package/harness/manifests/general/cursor.json +26 -26
- package/harness/manifests/java/claude-code.json +29 -29
- package/harness/manifests/java/codebuddy.json +29 -29
- package/harness/manifests/java/codex.json +29 -29
- package/harness/manifests/java/cursor.json +29 -29
- package/hunter-workflow-family.json +1 -1
- package/package.json +1 -1
|
@@ -520,6 +520,41 @@ def check_status(
|
|
|
520
520
|
}
|
|
521
521
|
)
|
|
522
522
|
|
|
523
|
+
# --- H-4 formal-layer minimum set ---
|
|
524
|
+
plans_dir = change_dir / "plans"
|
|
525
|
+
plan_files = (
|
|
526
|
+
sorted(plans_dir.glob("*-plan.md")) if plans_dir.is_dir() else []
|
|
527
|
+
)
|
|
528
|
+
checks["plan_files"] = [str(p.relative_to(change_dir)) for p in plan_files]
|
|
529
|
+
if not plan_files:
|
|
530
|
+
blockers.append(
|
|
531
|
+
{
|
|
532
|
+
"code": "missing-plan",
|
|
533
|
+
"message": "plans/*-plan.md is required before archive",
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
events_path = change_dir / "events.ndjson"
|
|
538
|
+
events_present = events_path.is_file() and events_path.stat().st_size > 0
|
|
539
|
+
checks["events_ndjson"] = events_present
|
|
540
|
+
if not events_present:
|
|
541
|
+
blockers.append(
|
|
542
|
+
{
|
|
543
|
+
"code": "missing-events",
|
|
544
|
+
"message": "events.ndjson is required and must be non-empty",
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
ledger = load_ledger(change_dir)
|
|
549
|
+
checks["verification_ledger"] = ledger is not None
|
|
550
|
+
if ledger is None:
|
|
551
|
+
blockers.append(
|
|
552
|
+
{
|
|
553
|
+
"code": "missing-verification-ledger",
|
|
554
|
+
"message": "evidence/verification-ledger.json is required before archive",
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
|
|
523
558
|
# --- commit pushed ---
|
|
524
559
|
code, out, err = git_run(project, "rev-parse", "--is-inside-work-tree")
|
|
525
560
|
if code != 0:
|
|
@@ -559,7 +594,8 @@ def check_status(
|
|
|
559
594
|
|
|
560
595
|
expected_hash: str | None = None
|
|
561
596
|
hash_source: str | None = None
|
|
562
|
-
ledger
|
|
597
|
+
if ledger is None:
|
|
598
|
+
ledger = load_ledger(change_dir)
|
|
563
599
|
if worktree_requested(change_dir) and ledger:
|
|
564
600
|
merge_hash = ledger.get("mergeFinalHash") or (
|
|
565
601
|
ledger.get("merge") or {}
|
|
@@ -625,13 +661,25 @@ def check_status(
|
|
|
625
661
|
# --- test / review reports ---
|
|
626
662
|
test_reports = find_test_reports(change_dir)
|
|
627
663
|
review_reports = find_review_reports(change_dir)
|
|
628
|
-
events_path = change_dir / "events.ndjson"
|
|
629
664
|
events = he.load_events(events_path) if events_path.is_file() else []
|
|
630
665
|
review_ran = review_evidence_present(change_dir, events)
|
|
631
666
|
|
|
632
667
|
checks["test_reports"] = [str(p.relative_to(change_dir)) for p in test_reports]
|
|
633
668
|
checks["review_reports"] = [str(p.relative_to(change_dir)) for p in review_reports]
|
|
634
669
|
|
|
670
|
+
# H-4 min blockers are plan/events/ledger only. Missing test/review evidence
|
|
671
|
+
# remains a warning so archive-before-review stays possible (advisory).
|
|
672
|
+
if not test_reports and not review_reports and not review_ran:
|
|
673
|
+
warnings.append(
|
|
674
|
+
{
|
|
675
|
+
"code": "missing-test-or-review-report",
|
|
676
|
+
"message": (
|
|
677
|
+
"no test report or review evidence yet; prefer completing "
|
|
678
|
+
"test/review before archive (not a hard blocker)"
|
|
679
|
+
),
|
|
680
|
+
}
|
|
681
|
+
)
|
|
682
|
+
|
|
635
683
|
if test_reports:
|
|
636
684
|
checks["test_report_status"] = "present"
|
|
637
685
|
else:
|
|
@@ -842,15 +890,18 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
842
890
|
elif str(unit.get("reused") or "").lower() in {"true", "1"} or "REUSED" in status:
|
|
843
891
|
source = "committed"
|
|
844
892
|
|
|
845
|
-
if pass_rate is None
|
|
893
|
+
if pass_rate is None:
|
|
846
894
|
passed = (
|
|
847
895
|
passed_count
|
|
848
896
|
if passed_count is not None
|
|
849
|
-
else max(run - failures - errors - skipped, 0)
|
|
897
|
+
else max(int(run or 0) - int(failures or 0) - int(errors or 0) - int(skipped or 0), 0)
|
|
850
898
|
)
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
899
|
+
# H-13: passRate denominator excludes skipped.
|
|
900
|
+
denom = int(passed) + int(failures or 0) + int(errors or 0)
|
|
901
|
+
if denom > 0:
|
|
902
|
+
pass_rate = f"{passed / denom:.0%}"
|
|
903
|
+
else:
|
|
904
|
+
pass_rate = NOT_AVAILABLE
|
|
854
905
|
|
|
855
906
|
result = {
|
|
856
907
|
"run": int(run or 0),
|
|
@@ -1332,7 +1383,19 @@ def _stage_status_from_sources(
|
|
|
1332
1383
|
elif raw in {"SKIP", "SKIPPED"}:
|
|
1333
1384
|
raw = "USER_SKIPPED"
|
|
1334
1385
|
status[phase] = raw
|
|
1335
|
-
# Issues in events can downgrade
|
|
1386
|
+
# Issues in events can downgrade. H-14: informational / hygiene / agent-preflight
|
|
1387
|
+
# notes must not downgrade an already-OK phase.end.
|
|
1388
|
+
_informational_markers = (
|
|
1389
|
+
"[archive-hygiene]",
|
|
1390
|
+
"informational",
|
|
1391
|
+
"custom_agents_unsupported",
|
|
1392
|
+
"definition_not_found_host_capable",
|
|
1393
|
+
"知识查询",
|
|
1394
|
+
"knowledge query",
|
|
1395
|
+
"harness-explorer",
|
|
1396
|
+
"harness-evaluator",
|
|
1397
|
+
"委派",
|
|
1398
|
+
)
|
|
1336
1399
|
for e in he.current_issues(events):
|
|
1337
1400
|
sev = str(e.get("severity") or "").lower()
|
|
1338
1401
|
issue_text = " ".join(str(e.get(key) or "") for key in ("message", "note", "code")).lower()
|
|
@@ -1341,6 +1404,10 @@ def _stage_status_from_sources(
|
|
|
1341
1404
|
sev = "error"
|
|
1342
1405
|
elif any(token in issue_text for token in ("warn", "skip", "风险", "警告")):
|
|
1343
1406
|
sev = "warn"
|
|
1407
|
+
if sev in {"warn", "warning"} and any(m in issue_text for m in _informational_markers):
|
|
1408
|
+
continue
|
|
1409
|
+
if sev in {"", "info", "note", "informational"}:
|
|
1410
|
+
continue
|
|
1344
1411
|
phase = str(e.get("phase") or "").lower()
|
|
1345
1412
|
if phase in status and sev in {"error", "fail", "failed", "critical"}:
|
|
1346
1413
|
status[phase] = "FAIL"
|
|
@@ -1651,6 +1718,51 @@ def _review_summary(
|
|
|
1651
1718
|
return base
|
|
1652
1719
|
|
|
1653
1720
|
|
|
1721
|
+
def _resolve_base_commit(
|
|
1722
|
+
ledger: dict[str, Any] | None,
|
|
1723
|
+
change_dir: Path,
|
|
1724
|
+
project: Path,
|
|
1725
|
+
final: str | None,
|
|
1726
|
+
) -> str:
|
|
1727
|
+
"""Resolve baseCommit: ledger → latest phase-context → merge first parent → merge-base."""
|
|
1728
|
+
if ledger:
|
|
1729
|
+
base = str(ledger.get("baseCommit") or "").strip()
|
|
1730
|
+
if base and base != NOT_AVAILABLE:
|
|
1731
|
+
return base
|
|
1732
|
+
|
|
1733
|
+
ctx_dir = change_dir / "runtime" / "phase-context"
|
|
1734
|
+
if ctx_dir.is_dir():
|
|
1735
|
+
candidates = sorted(
|
|
1736
|
+
(p for p in ctx_dir.glob("*.json") if p.is_file()),
|
|
1737
|
+
key=lambda p: p.stat().st_mtime,
|
|
1738
|
+
reverse=True,
|
|
1739
|
+
)
|
|
1740
|
+
for path in candidates:
|
|
1741
|
+
try:
|
|
1742
|
+
payload = read_json(path)
|
|
1743
|
+
except (OSError, json.JSONDecodeError, TypeError):
|
|
1744
|
+
continue
|
|
1745
|
+
if not isinstance(payload, dict):
|
|
1746
|
+
continue
|
|
1747
|
+
base = str(payload.get("baseCommit") or "").strip()
|
|
1748
|
+
if base and base != NOT_AVAILABLE:
|
|
1749
|
+
return base
|
|
1750
|
+
|
|
1751
|
+
final_commit = str(final or "").strip()
|
|
1752
|
+
if final_commit and final_commit != NOT_AVAILABLE:
|
|
1753
|
+
code, parents_line, _ = git_run(
|
|
1754
|
+
project, "rev-list", "--parents", "-n", "1", final_commit
|
|
1755
|
+
)
|
|
1756
|
+
if code == 0 and parents_line:
|
|
1757
|
+
parts = parents_line.split()
|
|
1758
|
+
if len(parts) >= 2:
|
|
1759
|
+
return parts[1]
|
|
1760
|
+
code, merge_base, _ = git_run(project, "merge-base", final_commit, "HEAD")
|
|
1761
|
+
if code == 0 and merge_base and merge_base != final_commit:
|
|
1762
|
+
return merge_base
|
|
1763
|
+
return ""
|
|
1764
|
+
|
|
1765
|
+
|
|
1654
1766
|
def _final_commit_from_sources(
|
|
1655
1767
|
ledger: dict[str, Any] | None,
|
|
1656
1768
|
events: list[dict[str, Any]],
|
|
@@ -1864,12 +1976,21 @@ def collect_summary_data(
|
|
|
1864
1976
|
data["finalCommit"] = final_commit or (NOT_AVAILABLE if for_replay else "")
|
|
1865
1977
|
|
|
1866
1978
|
if not data.get("baseCommit") or str(data.get("baseCommit")).startswith("<"):
|
|
1867
|
-
if
|
|
1868
|
-
data["baseCommit"] = ledger["baseCommit"]
|
|
1869
|
-
elif existing and existing.get("baseCommit"):
|
|
1979
|
+
if for_replay and existing and existing.get("baseCommit"):
|
|
1870
1980
|
data["baseCommit"] = existing["baseCommit"]
|
|
1871
1981
|
else:
|
|
1872
|
-
|
|
1982
|
+
resolved_base = _resolve_base_commit(
|
|
1983
|
+
ledger,
|
|
1984
|
+
change_dir,
|
|
1985
|
+
project,
|
|
1986
|
+
str(data.get("finalCommit") or "") or None,
|
|
1987
|
+
)
|
|
1988
|
+
if resolved_base:
|
|
1989
|
+
data["baseCommit"] = resolved_base
|
|
1990
|
+
elif existing and existing.get("baseCommit"):
|
|
1991
|
+
data["baseCommit"] = existing["baseCommit"]
|
|
1992
|
+
else:
|
|
1993
|
+
data["baseCommit"] = NOT_AVAILABLE if for_replay else ""
|
|
1873
1994
|
|
|
1874
1995
|
if not data.get("finalCommitBranch") or str(data.get("finalCommitBranch")).startswith("<"):
|
|
1875
1996
|
code, branch, _ = git_run(project, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
|
@@ -2002,6 +2123,16 @@ def collect_summary_data(
|
|
|
2002
2123
|
}
|
|
2003
2124
|
data["changedFiles"] = []
|
|
2004
2125
|
|
|
2126
|
+
# H-11: surface identity + diff facts for adequacy (top-level + gitFacts).
|
|
2127
|
+
diff_for_facts = data.get("diffStat") if isinstance(data.get("diffStat"), dict) else {}
|
|
2128
|
+
data["gitFacts"] = {
|
|
2129
|
+
"baseCommit": data.get("baseCommit") or "",
|
|
2130
|
+
"finalCommit": data.get("finalCommit") or "",
|
|
2131
|
+
"filesChanged": int(diff_for_facts.get("filesChanged") or 0),
|
|
2132
|
+
"insertions": int(diff_for_facts.get("insertions") or 0),
|
|
2133
|
+
"deletions": int(diff_for_facts.get("deletions") or 0),
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2005
2136
|
data.setdefault(
|
|
2006
2137
|
"ownershipDiff",
|
|
2007
2138
|
{
|
|
@@ -2837,18 +2968,52 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
2837
2968
|
"""
|
|
2838
2969
|
issues: list[dict[str, str]] = []
|
|
2839
2970
|
|
|
2971
|
+
# H-12: prefer top-level baseCommit + diffStat; gitFacts is a mirror.
|
|
2972
|
+
base = str(summary.get("baseCommit") or "").strip()
|
|
2973
|
+
final = str(summary.get("finalCommit") or "").strip()
|
|
2974
|
+
merge_final = str(summary.get("mergeFinalHash") or "").strip()
|
|
2975
|
+
diff_stat = summary.get("diffStat") if isinstance(summary.get("diffStat"), dict) else {}
|
|
2976
|
+
files_changed = int(diff_stat.get("filesChanged") or 0)
|
|
2977
|
+
|
|
2840
2978
|
git_facts = summary.get("gitFacts") or {}
|
|
2841
2979
|
if isinstance(git_facts, dict):
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
if
|
|
2847
|
-
|
|
2848
|
-
"
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2980
|
+
if not base or base == NOT_AVAILABLE:
|
|
2981
|
+
base = str(git_facts.get("baseCommit") or "").strip() or base
|
|
2982
|
+
if not final or final == NOT_AVAILABLE:
|
|
2983
|
+
final = str(git_facts.get("finalCommit") or "").strip() or final
|
|
2984
|
+
if "filesChanged" in git_facts and files_changed == 0:
|
|
2985
|
+
try:
|
|
2986
|
+
files_changed = int(git_facts.get("filesChanged") or 0)
|
|
2987
|
+
except (TypeError, ValueError):
|
|
2988
|
+
files_changed = 0
|
|
2989
|
+
|
|
2990
|
+
base_missing = not base or base == NOT_AVAILABLE
|
|
2991
|
+
final_present = bool(
|
|
2992
|
+
(final and final != NOT_AVAILABLE) or (merge_final and merge_final != NOT_AVAILABLE)
|
|
2993
|
+
)
|
|
2994
|
+
if final_present and base_missing:
|
|
2995
|
+
issues.append({
|
|
2996
|
+
"code": "IDENTITY_BASE_MISSING",
|
|
2997
|
+
"severity": "error",
|
|
2998
|
+
"message": (
|
|
2999
|
+
"final/merge identity present but baseCommit is empty; "
|
|
3000
|
+
"refuse CONDITIONAL_OK+ without a resolved base"
|
|
3001
|
+
),
|
|
3002
|
+
})
|
|
3003
|
+
|
|
3004
|
+
if (
|
|
3005
|
+
base
|
|
3006
|
+
and final
|
|
3007
|
+
and base != NOT_AVAILABLE
|
|
3008
|
+
and final != NOT_AVAILABLE
|
|
3009
|
+
and base != final
|
|
3010
|
+
and files_changed == 0
|
|
3011
|
+
):
|
|
3012
|
+
issues.append({
|
|
3013
|
+
"code": "DIFF_ZERO_WITH_NONEMPTY_COMMIT",
|
|
3014
|
+
"severity": "error",
|
|
3015
|
+
"message": f"final commit {final} differs from base {base} but filesChanged=0",
|
|
3016
|
+
})
|
|
2852
3017
|
|
|
2853
3018
|
# Typed metrics missing despite test report artifacts present.
|
|
2854
3019
|
verification = summary.get("verification") or {}
|
|
@@ -2886,11 +3051,11 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
2886
3051
|
|
|
2887
3052
|
|
|
2888
3053
|
def artifact_preflight(change_dir: Path) -> dict[str, Any]:
|
|
2889
|
-
"""Classify artifact events before destructive finalize (retro §5.31).
|
|
3054
|
+
"""Classify artifact events before destructive finalize (retro §5.31 / H-8).
|
|
2890
3055
|
|
|
2891
|
-
Returns per-artifact classification:
|
|
2892
|
-
canonicalizable (same-change repo-relative path
|
|
2893
|
-
|
|
3056
|
+
Returns per-artifact classification: blocking (missing / escaping /
|
|
3057
|
+
cross-change path), canonicalizable (same-change repo-relative path),
|
|
3058
|
+
or file-backed (change-relative path). Pathless legacy rows fail closed.
|
|
2894
3059
|
"""
|
|
2895
3060
|
change_dir = change_dir.resolve()
|
|
2896
3061
|
events_p = change_dir / "events.ndjson"
|
|
@@ -2910,12 +3075,16 @@ def artifact_preflight(change_dir: Path) -> dict[str, Any]:
|
|
|
2910
3075
|
kind = str(event.get("kind") or "").strip()
|
|
2911
3076
|
event_id = str(event.get("id") or "")
|
|
2912
3077
|
if not path:
|
|
2913
|
-
|
|
3078
|
+
item = {
|
|
2914
3079
|
"eventId": event_id,
|
|
2915
|
-
"category": "
|
|
2916
|
-
"
|
|
3080
|
+
"category": "blocking",
|
|
3081
|
+
"path": "",
|
|
3082
|
+
"kind": kind or "",
|
|
3083
|
+
"reason": "artifact path missing",
|
|
2917
3084
|
"note": str(event.get("note") or "")[:80],
|
|
2918
|
-
}
|
|
3085
|
+
}
|
|
3086
|
+
items.append(item)
|
|
3087
|
+
blocking.append(item)
|
|
2919
3088
|
continue
|
|
2920
3089
|
# Check for escaping/cross-change paths.
|
|
2921
3090
|
parts = path.replace("\\", "/").split("/")
|
|
@@ -679,29 +679,16 @@ def validate_append_event(args: argparse.Namespace) -> tuple[str, str] | None:
|
|
|
679
679
|
"CORRECTION_VALUE_INVALID_JSON",
|
|
680
680
|
f"CORRECTION_VALUE_INVALID_JSON: {exc}",
|
|
681
681
|
)
|
|
682
|
-
# Retro
|
|
683
|
-
#
|
|
684
|
-
# (preview/summary) must not masquerade as a file artifact. Legacy kind
|
|
685
|
-
# values (ledger, report, etc.) are treated as file-backed aliases.
|
|
682
|
+
# Retro 2026-07-21 H-8: artifact events always require a non-empty path.
|
|
683
|
+
# Knowledge/exploration notes must use issue/decision, not pathless artifacts.
|
|
686
684
|
if event_type == "artifact":
|
|
687
|
-
kind = str(getattr(args, "kind", "") or "").strip()
|
|
688
685
|
path = str(getattr(args, "path", "") or "").strip()
|
|
689
|
-
|
|
690
|
-
if kind == "file-backed" and not path:
|
|
691
|
-
return (
|
|
692
|
-
"ARTIFACT_PATH_REQUIRED",
|
|
693
|
-
"ARTIFACT_PATH_REQUIRED: file-backed artifact requires --path",
|
|
694
|
-
)
|
|
695
|
-
# If kind is explicitly informational, path is optional. If kind is
|
|
696
|
-
# absent and path is absent, reject (can't infer file-backed without path).
|
|
697
|
-
if not kind and not path:
|
|
686
|
+
if not path:
|
|
698
687
|
return (
|
|
699
688
|
"ARTIFACT_PATH_REQUIRED",
|
|
700
|
-
"ARTIFACT_PATH_REQUIRED: artifact
|
|
701
|
-
"use
|
|
689
|
+
"ARTIFACT_PATH_REQUIRED: artifact requires --path "
|
|
690
|
+
"(use issue/decision for informational notes)",
|
|
702
691
|
)
|
|
703
|
-
# If no kind is provided, infer from path presence: a path implies
|
|
704
|
-
# file-backed, absence implies informational (legacy compatibility).
|
|
705
692
|
return None
|
|
706
693
|
|
|
707
694
|
|
|
@@ -109,6 +109,10 @@ class CleanupResidualError(IntegrationError):
|
|
|
109
109
|
)
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
class AbandonRefusedError(IntegrationError):
|
|
113
|
+
code = "ABANDON_REFUSED"
|
|
114
|
+
|
|
115
|
+
|
|
112
116
|
class JournalConflictError(IntegrationError):
|
|
113
117
|
code = "JOURNAL_CONFLICT"
|
|
114
118
|
|
|
@@ -740,6 +744,8 @@ class IntegrationTransaction:
|
|
|
740
744
|
str(intg_root),
|
|
741
745
|
self.temp_branch,
|
|
742
746
|
)
|
|
747
|
+
# H-5: LF-stable hashing in the integration worktree.
|
|
748
|
+
self.runner.run(intg_root, "config", "core.autocrlf", "false")
|
|
743
749
|
|
|
744
750
|
return self._run_step("prepare", action)
|
|
745
751
|
|
|
@@ -1195,6 +1201,148 @@ class IntegrationTransaction:
|
|
|
1195
1201
|
harness_change.integration_lock_release(self.project_root, run_id=self.run_id)
|
|
1196
1202
|
return journal
|
|
1197
1203
|
|
|
1204
|
+
# ------------------------------------------------------------ abandon
|
|
1205
|
+
|
|
1206
|
+
def _remote_contains_merge(self, journal: dict[str, Any]) -> bool:
|
|
1207
|
+
merge_commit = str(journal.get("mergeCommit") or "")
|
|
1208
|
+
if not merge_commit:
|
|
1209
|
+
return False
|
|
1210
|
+
remote_line = self.runner.text(
|
|
1211
|
+
self.project_root, "ls-remote", "origin", self.target_branch
|
|
1212
|
+
)
|
|
1213
|
+
observed_remote = (
|
|
1214
|
+
remote_line.split()[0] if remote_line and remote_line.split() else ""
|
|
1215
|
+
)
|
|
1216
|
+
return bool(observed_remote and observed_remote == merge_commit)
|
|
1217
|
+
|
|
1218
|
+
def _force_remove_integration_root(
|
|
1219
|
+
self, journal: dict[str, Any], intg_root: Path
|
|
1220
|
+
) -> dict[str, Any]:
|
|
1221
|
+
"""Best-effort remove of the integration worktree for abandon only."""
|
|
1222
|
+
allowed = Path(str(journal.get("allowedCleanupRoot") or "")).resolve()
|
|
1223
|
+
resolved = harness_paths.assert_path_within(intg_root, self.temp_root)
|
|
1224
|
+
if not allowed or resolved != allowed:
|
|
1225
|
+
raise CleanupRefusedError(
|
|
1226
|
+
f"refusing to abandon non-transaction path: {resolved}"
|
|
1227
|
+
)
|
|
1228
|
+
self.runner.run(
|
|
1229
|
+
self.project_root,
|
|
1230
|
+
"worktree",
|
|
1231
|
+
"remove",
|
|
1232
|
+
"--force",
|
|
1233
|
+
str(resolved),
|
|
1234
|
+
check=False,
|
|
1235
|
+
)
|
|
1236
|
+
residual: list[str] = []
|
|
1237
|
+
if resolved.exists():
|
|
1238
|
+
for name in HEAVY_WORKTREE_ROOTS:
|
|
1239
|
+
heavy = resolved / name
|
|
1240
|
+
if heavy.exists():
|
|
1241
|
+
harness_paths.assert_path_within(heavy, resolved)
|
|
1242
|
+
try:
|
|
1243
|
+
if heavy.is_dir() and not heavy.is_symlink():
|
|
1244
|
+
shutil.rmtree(heavy)
|
|
1245
|
+
else:
|
|
1246
|
+
heavy.unlink()
|
|
1247
|
+
except OSError:
|
|
1248
|
+
residual.append(name)
|
|
1249
|
+
if resolved.exists():
|
|
1250
|
+
try:
|
|
1251
|
+
shutil.rmtree(resolved)
|
|
1252
|
+
except OSError:
|
|
1253
|
+
residual.append(str(resolved))
|
|
1254
|
+
listed = self.runner.text(self.project_root, "worktree", "list", "--porcelain")
|
|
1255
|
+
still_registered = str(resolved) in {
|
|
1256
|
+
str(Path(line.removeprefix("worktree ")).resolve())
|
|
1257
|
+
for line in (listed or "").splitlines()
|
|
1258
|
+
if line.startswith("worktree ")
|
|
1259
|
+
}
|
|
1260
|
+
ok = not still_registered and not residual and not resolved.exists()
|
|
1261
|
+
return {
|
|
1262
|
+
"ok": ok,
|
|
1263
|
+
"code": "CLEANUP_COMPLETE" if ok else "CLEANUP_RESIDUAL",
|
|
1264
|
+
"target": str(resolved),
|
|
1265
|
+
"worktreeRegistered": still_registered,
|
|
1266
|
+
"diskPathPresent": resolved.exists(),
|
|
1267
|
+
"residualPaths": residual,
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
def abandon(self) -> dict[str, Any]:
|
|
1271
|
+
"""Official recovery for a failed integration transaction (H-6).
|
|
1272
|
+
|
|
1273
|
+
Allowed only when push is not DONE and the remote does not already
|
|
1274
|
+
contain ``mergeCommit``. Removes the integration worktree, temp branch,
|
|
1275
|
+
protection refs, and integration lock. Never deletes the feature
|
|
1276
|
+
worktree or feature branch.
|
|
1277
|
+
"""
|
|
1278
|
+
try:
|
|
1279
|
+
journal = self._load()
|
|
1280
|
+
except FileNotFoundError as exc:
|
|
1281
|
+
raise AbandonRefusedError(
|
|
1282
|
+
f"no journal for transaction {self.transaction_id}"
|
|
1283
|
+
) from exc
|
|
1284
|
+
|
|
1285
|
+
push_status = str(self._step(journal, "push").get("status") or "")
|
|
1286
|
+
push_intent = journal.get("pushIntent") or {}
|
|
1287
|
+
push_confirmed = (
|
|
1288
|
+
push_status == "DONE"
|
|
1289
|
+
or (
|
|
1290
|
+
isinstance(push_intent, dict)
|
|
1291
|
+
and push_intent.get("status") == "REMOTE_CONFIRMED"
|
|
1292
|
+
)
|
|
1293
|
+
or bool(journal.get("pushedHead"))
|
|
1294
|
+
or bool(journal.get("mergeFinalHash"))
|
|
1295
|
+
)
|
|
1296
|
+
remote_has_merge = self._remote_contains_merge(journal)
|
|
1297
|
+
if push_confirmed or remote_has_merge:
|
|
1298
|
+
raise AbandonRefusedError(
|
|
1299
|
+
"refuse abandon: push succeeded or remote already contains "
|
|
1300
|
+
"the merge commit; use cleanup instead"
|
|
1301
|
+
)
|
|
1302
|
+
|
|
1303
|
+
intg_root = Path(str(journal.get("integrationRoot") or ""))
|
|
1304
|
+
cleanup_result: dict[str, Any] = {
|
|
1305
|
+
"ok": True,
|
|
1306
|
+
"code": "ALREADY_ABSENT",
|
|
1307
|
+
"target": str(intg_root) if intg_root else "",
|
|
1308
|
+
"worktreeRegistered": False,
|
|
1309
|
+
"diskPathPresent": False,
|
|
1310
|
+
"residualPaths": [],
|
|
1311
|
+
}
|
|
1312
|
+
prepare_done = self._step(journal, "prepare").get("status") == "DONE"
|
|
1313
|
+
if prepare_done and str(journal.get("integrationRoot") or "").strip():
|
|
1314
|
+
try:
|
|
1315
|
+
cleanup_result = self.cleanup_target(intg_root)
|
|
1316
|
+
except CleanupRefusedError:
|
|
1317
|
+
cleanup_result = self._force_remove_integration_root(journal, intg_root)
|
|
1318
|
+
|
|
1319
|
+
self.runner.run(
|
|
1320
|
+
self.project_root, "branch", "-D", self.temp_branch, check=False
|
|
1321
|
+
)
|
|
1322
|
+
refs = journal.get("protectionRefs") or {}
|
|
1323
|
+
if isinstance(refs, dict):
|
|
1324
|
+
for ref in (refs.get("base"), refs.get("head")):
|
|
1325
|
+
if ref:
|
|
1326
|
+
self.runner.run(
|
|
1327
|
+
self.project_root, "update-ref", "-d", str(ref), check=False
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
harness_change.integration_lock_release(self.project_root, run_id=self.run_id)
|
|
1331
|
+
|
|
1332
|
+
journal = self._load()
|
|
1333
|
+
journal["abandonedAt"] = now_iso()
|
|
1334
|
+
journal["abandonResult"] = cleanup_result
|
|
1335
|
+
self._save(journal)
|
|
1336
|
+
|
|
1337
|
+
return {
|
|
1338
|
+
"ok": True,
|
|
1339
|
+
"code": "ABANDON_COMPLETE",
|
|
1340
|
+
"transactionId": self.transaction_id,
|
|
1341
|
+
"cleanup": cleanup_result,
|
|
1342
|
+
"featureBranchRetained": True,
|
|
1343
|
+
"featureWorktreeRetained": True,
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1198
1346
|
# ------------------------------------------------------------ recover
|
|
1199
1347
|
|
|
1200
1348
|
def recover(
|
|
@@ -1282,6 +1430,10 @@ def cmd_cleanup(args: argparse.Namespace) -> int:
|
|
|
1282
1430
|
return _emit(_txn_from_args(args).cleanup())
|
|
1283
1431
|
|
|
1284
1432
|
|
|
1433
|
+
def cmd_abandon(args: argparse.Namespace) -> int:
|
|
1434
|
+
return _emit(_txn_from_args(args).abandon())
|
|
1435
|
+
|
|
1436
|
+
|
|
1285
1437
|
def cmd_recover(args: argparse.Namespace) -> int:
|
|
1286
1438
|
return _emit(_txn_from_args(args).recover())
|
|
1287
1439
|
|
|
@@ -1333,6 +1485,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1333
1485
|
add("merge", cmd_merge)
|
|
1334
1486
|
add("push", cmd_push)
|
|
1335
1487
|
add("cleanup", cmd_cleanup)
|
|
1488
|
+
add("abandon", cmd_abandon)
|
|
1336
1489
|
add("recover", cmd_recover)
|
|
1337
1490
|
add("status", cmd_status)
|
|
1338
1491
|
|
|
@@ -131,8 +131,19 @@ def emit_compact_or_verbose(
|
|
|
131
131
|
emit_json(out, as_json=as_json)
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
def emit_error(
|
|
135
|
-
|
|
134
|
+
def emit_error(
|
|
135
|
+
message: str,
|
|
136
|
+
*,
|
|
137
|
+
as_json: bool,
|
|
138
|
+
code: int = 1,
|
|
139
|
+
error_code: str | None = None,
|
|
140
|
+
extra: dict[str, Any] | None = None,
|
|
141
|
+
) -> int:
|
|
142
|
+
payload: dict[str, Any] = {"ok": False, "error": message}
|
|
143
|
+
if error_code:
|
|
144
|
+
payload["code"] = error_code
|
|
145
|
+
if extra:
|
|
146
|
+
payload.update(extra)
|
|
136
147
|
if as_json:
|
|
137
148
|
sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
138
149
|
else:
|
|
@@ -1520,6 +1531,8 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1520
1531
|
return emit_error(
|
|
1521
1532
|
"ledger identity incomplete: " + ", ".join(missing),
|
|
1522
1533
|
as_json=as_json,
|
|
1534
|
+
error_code="LEDGER_IDENTITY_INVALID",
|
|
1535
|
+
extra={"missing": missing},
|
|
1523
1536
|
)
|
|
1524
1537
|
write_ledger(out_path, ledger)
|
|
1525
1538
|
except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
|