@hunter-harness/workflow-harness 0.2.82 → 0.2.84
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 +32 -1
- 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-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +22 -2
- package/harness/bundles/general/claude-code/harness-run/protocols.md +60 -0
- package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +273 -3
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +88 -3
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/claude-code/scripts/harness_review.py +43 -5
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +32 -1
- 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-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +22 -2
- package/harness/bundles/general/codebuddy/harness-run/protocols.md +60 -0
- package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +273 -3
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +88 -3
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +43 -5
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +32 -1
- 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-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +22 -2
- package/harness/bundles/general/codex/harness-run/protocols.md +60 -0
- package/harness/bundles/general/codex/harness-run/reference.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +273 -3
- package/harness/bundles/general/codex/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/codex/scripts/harness_gate.py +88 -3
- package/harness/bundles/general/codex/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/codex/scripts/harness_review.py +43 -5
- package/harness/bundles/general/codex/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +32 -1
- 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-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +22 -2
- package/harness/bundles/general/cursor/harness-run/protocols.md +60 -0
- package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +273 -3
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/cursor/scripts/harness_gate.py +88 -3
- package/harness/bundles/general/cursor/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/cursor/scripts/harness_review.py +43 -5
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +125 -10
- 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 +32 -1
- 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-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +22 -2
- package/harness/bundles/java/claude-code/harness-run/protocols.md +60 -0
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +273 -3
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +88 -3
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/claude-code/scripts/harness_review.py +43 -5
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +125 -10
- 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 +32 -1
- 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-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +22 -2
- package/harness/bundles/java/codebuddy/harness-run/protocols.md +60 -0
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +273 -3
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +88 -3
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +43 -5
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +125 -10
- 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 +32 -1
- 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-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +22 -2
- package/harness/bundles/java/codex/harness-run/protocols.md +60 -0
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +273 -3
- package/harness/bundles/java/codex/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/codex/scripts/harness_gate.py +88 -3
- package/harness/bundles/java/codex/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/codex/scripts/harness_review.py +43 -5
- package/harness/bundles/java/codex/scripts/harness_runtime.py +125 -10
- 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 +32 -1
- 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-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +22 -2
- package/harness/bundles/java/cursor/harness-run/protocols.md +60 -0
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_archive.py +273 -3
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/cursor/scripts/harness_gate.py +88 -3
- package/harness/bundles/java/cursor/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/cursor/scripts/harness_review.py +43 -5
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +125 -10
- package/harness/manifests/general/claude-code.json +23 -23
- package/harness/manifests/general/codebuddy.json +23 -23
- package/harness/manifests/general/codex.json +23 -23
- package/harness/manifests/general/cursor.json +23 -23
- package/harness/manifests/java/claude-code.json +24 -24
- package/harness/manifests/java/codebuddy.json +24 -24
- package/harness/manifests/java/codex.json +24 -24
- package/harness/manifests/java/cursor.json +24 -24
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ import harness_ledger as hl # noqa: E402
|
|
|
37
37
|
import harness_paths as hp # noqa: E402
|
|
38
38
|
import harness_plan_finalize as hpf # noqa: E402
|
|
39
39
|
import harness_review as hr # noqa: E402
|
|
40
|
+
import harness_runtime as hruntime # noqa: E402
|
|
40
41
|
import harness_workflow_policy as hwp # noqa: E402
|
|
41
42
|
import harness_test_guard as htg # noqa: E402
|
|
42
43
|
|
|
@@ -1906,6 +1907,31 @@ def _latest_phase_end(
|
|
|
1906
1907
|
return None
|
|
1907
1908
|
|
|
1908
1909
|
|
|
1910
|
+
def _latest_open_run_id(change_dir: Path, phase: str) -> str | None:
|
|
1911
|
+
"""Return the newest ``phase.start`` run_id that has no matching ``phase.end``.
|
|
1912
|
+
|
|
1913
|
+
A long phase can outlive its lease TTL; close then reports ``LEASE_ABSENT``
|
|
1914
|
+
with no way back. Resuming needs the *original* run_id, so surface it here
|
|
1915
|
+
instead of making the caller reconstruct it from ``events.ndjson``.
|
|
1916
|
+
"""
|
|
1917
|
+
started: list[str] = []
|
|
1918
|
+
ended: set[str] = set()
|
|
1919
|
+
for event in he.load_events(he.events_path(change_dir)):
|
|
1920
|
+
if event.get("phase") != phase:
|
|
1921
|
+
continue
|
|
1922
|
+
run_id = str(event.get("run_id") or "")
|
|
1923
|
+
if not run_id:
|
|
1924
|
+
continue
|
|
1925
|
+
if event.get("type") == "phase.start":
|
|
1926
|
+
started.append(run_id)
|
|
1927
|
+
elif event.get("type") == "phase.end":
|
|
1928
|
+
ended.add(run_id)
|
|
1929
|
+
for run_id in reversed(started):
|
|
1930
|
+
if run_id not in ended:
|
|
1931
|
+
return run_id
|
|
1932
|
+
return None
|
|
1933
|
+
|
|
1934
|
+
|
|
1909
1935
|
def _terminal_matches_context_session(
|
|
1910
1936
|
project: Path,
|
|
1911
1937
|
change_id: str,
|
|
@@ -2631,11 +2657,42 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
2631
2657
|
ttl_seconds=int(args.ttl_seconds),
|
|
2632
2658
|
)
|
|
2633
2659
|
if not claim.get("ok"):
|
|
2660
|
+
claim_code = str(claim.get("code", "LEASE_CONFLICT"))
|
|
2661
|
+
claim_extra = {
|
|
2662
|
+
k: v for k, v in claim.items() if k not in {"ok", "message", "code"}
|
|
2663
|
+
}
|
|
2664
|
+
holder = claim.get("holder")
|
|
2665
|
+
if claim_code == "LEASE_CONFLICT" and isinstance(holder, dict):
|
|
2666
|
+
holder_phase = str(holder.get("phase") or "")
|
|
2667
|
+
holder_run_id = str(holder.get("runId") or "")
|
|
2668
|
+
# A lease whose own phase already recorded phase.end is leftover state:
|
|
2669
|
+
# only close releases the lease, so any begin-without-close strands it
|
|
2670
|
+
# for the full TTL and blocks the next phase.
|
|
2671
|
+
holder_closed = (
|
|
2672
|
+
bool(holder_phase)
|
|
2673
|
+
and bool(holder_run_id)
|
|
2674
|
+
and _latest_phase_end(change_dir, holder_phase, holder_run_id) is not None
|
|
2675
|
+
)
|
|
2676
|
+
claim_extra["holderPhaseClosed"] = holder_closed
|
|
2677
|
+
if holder_closed:
|
|
2678
|
+
claim_extra["recoveryAction"] = (
|
|
2679
|
+
f"持有租约的 {holder_phase} 阶段(run-id {holder_run_id})已写入 phase.end,"
|
|
2680
|
+
"这是残留租约,释放后重试 begin 是安全的:"
|
|
2681
|
+
"python <skills-root>/scripts/harness_change.py release "
|
|
2682
|
+
f"--change {resolved['changeId']} --phase {holder_phase} "
|
|
2683
|
+
f"--run-id {holder_run_id} --json"
|
|
2684
|
+
)
|
|
2685
|
+
else:
|
|
2686
|
+
claim_extra["recoveryAction"] = (
|
|
2687
|
+
f"{holder_phase or '另一个'} 阶段(run-id {holder_run_id or '未知'})"
|
|
2688
|
+
"仍在进行中且尚未写入 phase.end。先让该阶段正常关门;"
|
|
2689
|
+
"确认其执行进程已不存在时才可释放租约,不要直接 --steal。"
|
|
2690
|
+
)
|
|
2634
2691
|
return emit_error(
|
|
2635
|
-
|
|
2692
|
+
claim_code,
|
|
2636
2693
|
str(claim.get("message", "lease claim failed")),
|
|
2637
2694
|
as_json=as_json,
|
|
2638
|
-
extra=
|
|
2695
|
+
extra=claim_extra,
|
|
2639
2696
|
)
|
|
2640
2697
|
|
|
2641
2698
|
guard_result: dict[str, Any] | None = None
|
|
@@ -2843,7 +2900,26 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
2843
2900
|
as_json=as_json,
|
|
2844
2901
|
)
|
|
2845
2902
|
return 0
|
|
2846
|
-
|
|
2903
|
+
resume_run_id = explicit_run_id or _latest_open_run_id(change_dir, args.phase)
|
|
2904
|
+
return emit_error(
|
|
2905
|
+
"LEASE_ABSENT",
|
|
2906
|
+
"no active lease for phase close",
|
|
2907
|
+
as_json=as_json,
|
|
2908
|
+
extra={
|
|
2909
|
+
"retryable": True,
|
|
2910
|
+
"phase": args.phase,
|
|
2911
|
+
"changeId": resolved["changeId"],
|
|
2912
|
+
"resumeRunId": resume_run_id,
|
|
2913
|
+
"recoveryAction": (
|
|
2914
|
+
"阶段租约已过期(TTL 由 begin 的 --ttl-seconds 决定,默认 3600 秒)或已被释放。"
|
|
2915
|
+
"用本阶段原 run-id 重新取得租约后原样重试 close:"
|
|
2916
|
+
"python <skills-root>/scripts/harness_change.py claim "
|
|
2917
|
+
f"--change {resolved['changeId']} --phase {args.phase} "
|
|
2918
|
+
f"--run-id {resume_run_id or '<原 run-id>'} --ttl-seconds 3600 --json"
|
|
2919
|
+
"。不要重跑 gate begin——那会新开一次 attempt 并丢失本轮 capsule。"
|
|
2920
|
+
),
|
|
2921
|
+
},
|
|
2922
|
+
)
|
|
2847
2923
|
run_id = explicit_run_id or str(current_lease.get("runId") or "")
|
|
2848
2924
|
if str(current_lease.get("runId")) != run_id or str(current_lease.get("phase")) != args.phase:
|
|
2849
2925
|
return emit_error(
|
|
@@ -3260,6 +3336,14 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3260
3336
|
phase=args.phase,
|
|
3261
3337
|
run_id=run_id,
|
|
3262
3338
|
)
|
|
3339
|
+
# 关门时清掉本阶段的过程草稿(diff、临时输入、命令输出重定向)。没人清它们,
|
|
3340
|
+
# 而且它们会被别的门禁读到——2026-08-19 那次,review 自己生成的两个 diff
|
|
3341
|
+
# 直接把归档挡住了。白名单式删除,证据/报告/运行态一律不碰;失败只记不阻断。
|
|
3342
|
+
try:
|
|
3343
|
+
scratch_swept = hruntime.sweep_scratch(change_dir)
|
|
3344
|
+
except OSError as exc:
|
|
3345
|
+
scratch_swept = {"ok": False, "code": "SCRATCH_SWEEP_FAILED", "error": str(exc)}
|
|
3346
|
+
|
|
3263
3347
|
payload = {
|
|
3264
3348
|
"ok": True,
|
|
3265
3349
|
"code": close_code,
|
|
@@ -3278,6 +3362,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3278
3362
|
"contextHandoff": context_handoff,
|
|
3279
3363
|
"platformMonitor": platform_monitor,
|
|
3280
3364
|
"gateRecovery": recovery_result,
|
|
3365
|
+
"scratchSwept": scratch_swept,
|
|
3281
3366
|
}
|
|
3282
3367
|
emit(payload, as_json=as_json)
|
|
3283
3368
|
return 0
|
|
@@ -378,6 +378,121 @@ def _normalize_verify_command(
|
|
|
378
378
|
return result
|
|
379
379
|
|
|
380
380
|
|
|
381
|
+
# 合并态验证的兜底优先级:先跑覆盖面最广的,再退到窄的。profile 里这些命令
|
|
382
|
+
# 本来就是 ledger 一直在用的规范命令,没有理由让调用方在合并时另行手敲一遍。
|
|
383
|
+
MERGE_VERIFICATION_FALLBACK_ORDER = ("unitTestFull", "unitTest", "apiTest", "compile")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def verify_plan_recovery_action() -> str:
|
|
387
|
+
"""把 VERIFY_PLAN_MISSING 补成一句能照做的话。
|
|
388
|
+
|
|
389
|
+
此前它只说 "requires at least one executable command"。于是调用方要么去读
|
|
390
|
+
实现,要么随手塞一条命令让门过去——两条路都走过,后者更糟:它让"验证通过"
|
|
391
|
+
这句话失去含义。
|
|
392
|
+
"""
|
|
393
|
+
return (
|
|
394
|
+
"合并态验证没有可执行计划。两条正规出路:"
|
|
395
|
+
"(1) 在 .harness/config/build-profile.json 增加 mergeVerification.requiredOnMerge "
|
|
396
|
+
"声明合并时必跑的命令(推荐,一次配置长期生效);"
|
|
397
|
+
"(2) 本次用 --command 传入。注意 --command 可重复,每次只接**一条**命令,"
|
|
398
|
+
"且按 argv 直接执行(不经过 shell)——`&&`、`|`、`cd x && y` 都不成立,"
|
|
399
|
+
"需要拆成多个 --command。"
|
|
400
|
+
"示例:--command \"npm test\" --command \"npm run lint\"。"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _fallback_merge_commands(profile: Any) -> list[dict[str, Any]]:
|
|
405
|
+
"""从 profile 已有的验证目标推导合并态计划。"""
|
|
406
|
+
commands = profile.get("commands") if isinstance(profile, dict) else None
|
|
407
|
+
if not isinstance(commands, dict):
|
|
408
|
+
return []
|
|
409
|
+
for name in MERGE_VERIFICATION_FALLBACK_ORDER:
|
|
410
|
+
entry = commands.get(name)
|
|
411
|
+
if not isinstance(entry, dict):
|
|
412
|
+
continue
|
|
413
|
+
argv = entry.get("argvTemplate")
|
|
414
|
+
if isinstance(argv, list) and argv and all(
|
|
415
|
+
isinstance(item, str) and item for item in argv
|
|
416
|
+
):
|
|
417
|
+
return [{
|
|
418
|
+
"command": [str(item) for item in argv],
|
|
419
|
+
"kind": "heavyweight",
|
|
420
|
+
"verification": name,
|
|
421
|
+
"source": "profile-fallback",
|
|
422
|
+
"reuseContext": {},
|
|
423
|
+
}]
|
|
424
|
+
raw = entry.get("command")
|
|
425
|
+
if isinstance(raw, str) and raw.strip():
|
|
426
|
+
return [{
|
|
427
|
+
"command": _split_verify_command(raw.strip()),
|
|
428
|
+
"kind": "heavyweight",
|
|
429
|
+
"verification": name,
|
|
430
|
+
"source": "profile-fallback",
|
|
431
|
+
"reuseContext": {},
|
|
432
|
+
}]
|
|
433
|
+
return []
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def merge_verification_plan(profile: Any) -> list[dict[str, Any]]:
|
|
437
|
+
"""声明优先,推导兜底;两者都没有才返回空计划。"""
|
|
438
|
+
merge = profile.get("mergeVerification") if isinstance(profile, dict) else None
|
|
439
|
+
required = merge.get("requiredOnMerge") if isinstance(merge, dict) else None
|
|
440
|
+
if isinstance(required, list) and required:
|
|
441
|
+
plan: list[dict[str, Any]] = []
|
|
442
|
+
for index, item in enumerate(required):
|
|
443
|
+
if isinstance(item, str) and item.strip():
|
|
444
|
+
plan.append({
|
|
445
|
+
"command": _split_verify_command(item.strip()),
|
|
446
|
+
"kind": "smoke",
|
|
447
|
+
"verification": None,
|
|
448
|
+
"source": "profile",
|
|
449
|
+
"reuseContext": {},
|
|
450
|
+
})
|
|
451
|
+
continue
|
|
452
|
+
if not isinstance(item, dict):
|
|
453
|
+
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
454
|
+
raw = item.get("command")
|
|
455
|
+
if isinstance(raw, list) and raw and all(
|
|
456
|
+
isinstance(value, str) and value for value in raw
|
|
457
|
+
):
|
|
458
|
+
command = [str(value) for value in raw]
|
|
459
|
+
elif isinstance(raw, str) and raw.strip():
|
|
460
|
+
command = _split_verify_command(raw.strip())
|
|
461
|
+
else:
|
|
462
|
+
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
463
|
+
kind = str(item.get("kind") or "smoke")
|
|
464
|
+
plan.append({
|
|
465
|
+
"command": command,
|
|
466
|
+
"kind": kind,
|
|
467
|
+
"verification": item.get("verification"),
|
|
468
|
+
"source": "profile",
|
|
469
|
+
"reuseContext": {
|
|
470
|
+
field: str(item.get(field)).strip()
|
|
471
|
+
for field in (
|
|
472
|
+
"environmentHash",
|
|
473
|
+
"toolchainHash",
|
|
474
|
+
"lockHash",
|
|
475
|
+
"dbSchemaHash",
|
|
476
|
+
)
|
|
477
|
+
if str(item.get(field) or "").strip()
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
return plan
|
|
481
|
+
return _fallback_merge_commands(profile)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def verification_depth(plan: Sequence[dict[str, Any]]) -> str:
|
|
485
|
+
"""一条临时探测命令通过,不该和跑完整条链读起来一样。
|
|
486
|
+
|
|
487
|
+
2026-08-19 那次合并,verify 只跑了一个测试文件就 DONE,报告里看不出任何差别。
|
|
488
|
+
真正的全链验证是人工在事务外补跑的——账面与事实脱节,正是这种静默造成的。
|
|
489
|
+
"""
|
|
490
|
+
entries = [item for item in plan if isinstance(item, dict)]
|
|
491
|
+
if len(entries) == 1 and entries[0].get("source") not in {"profile", "profile-fallback"}:
|
|
492
|
+
return "thin"
|
|
493
|
+
return "declared"
|
|
494
|
+
|
|
495
|
+
|
|
381
496
|
class IntegrationError(Exception):
|
|
382
497
|
code = "INTEGRATION_ERROR"
|
|
383
498
|
|
|
@@ -1330,47 +1445,16 @@ class IntegrationTransaction:
|
|
|
1330
1445
|
return commands
|
|
1331
1446
|
|
|
1332
1447
|
def _required_on_merge_plan(self) -> list[dict[str, Any]]:
|
|
1333
|
-
"""
|
|
1448
|
+
"""合并态验证计划:profile 声明优先,缺声明时从既有验证目标推导。
|
|
1449
|
+
|
|
1450
|
+
此前只读 mergeVerification.requiredOnMerge,没配就是空计划 → VERIFY_PLAN_MISSING。
|
|
1451
|
+
但 profile 里本来就有 ledger 一直在用的规范命令;让调用方在合并时另行手敲一遍,
|
|
1452
|
+
换来的通常是一条随手写的探测命令。
|
|
1453
|
+
"""
|
|
1334
1454
|
import harness_profile
|
|
1335
1455
|
|
|
1336
1456
|
profile = harness_profile.load_profile(self.project_root)
|
|
1337
|
-
|
|
1338
|
-
required = merge.get("requiredOnMerge") if isinstance(merge, dict) else []
|
|
1339
|
-
if not isinstance(required, list):
|
|
1340
|
-
return []
|
|
1341
|
-
plan: list[dict[str, Any]] = []
|
|
1342
|
-
for index, item in enumerate(required):
|
|
1343
|
-
if isinstance(item, str) and item.strip():
|
|
1344
|
-
plan.append({"command": _split_verify_command(item.strip()), "kind": "smoke"})
|
|
1345
|
-
continue
|
|
1346
|
-
if not isinstance(item, dict):
|
|
1347
|
-
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
1348
|
-
raw = item.get("command")
|
|
1349
|
-
if isinstance(raw, list) and raw and all(isinstance(value, str) and value for value in raw):
|
|
1350
|
-
command = [str(value) for value in raw]
|
|
1351
|
-
elif isinstance(raw, str) and raw.strip():
|
|
1352
|
-
command = _split_verify_command(raw.strip())
|
|
1353
|
-
else:
|
|
1354
|
-
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
1355
|
-
kind, verification = _merge_command_kind(item)
|
|
1356
|
-
plan.append(
|
|
1357
|
-
{
|
|
1358
|
-
"command": command,
|
|
1359
|
-
"kind": kind,
|
|
1360
|
-
"verification": verification,
|
|
1361
|
-
"reuseContext": {
|
|
1362
|
-
field: str(item.get(field)).strip()
|
|
1363
|
-
for field in (
|
|
1364
|
-
"environmentHash",
|
|
1365
|
-
"toolchainHash",
|
|
1366
|
-
"lockHash",
|
|
1367
|
-
"dbSchemaHash",
|
|
1368
|
-
)
|
|
1369
|
-
if str(item.get(field) or "").strip()
|
|
1370
|
-
},
|
|
1371
|
-
}
|
|
1372
|
-
)
|
|
1373
|
-
return plan
|
|
1457
|
+
return merge_verification_plan(profile)
|
|
1374
1458
|
|
|
1375
1459
|
def verify(
|
|
1376
1460
|
self,
|
|
@@ -1384,7 +1468,12 @@ class IntegrationTransaction:
|
|
|
1384
1468
|
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
1385
1469
|
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
1386
1470
|
plan = [
|
|
1387
|
-
{
|
|
1471
|
+
{
|
|
1472
|
+
"command": list(command),
|
|
1473
|
+
"kind": "smoke",
|
|
1474
|
+
"verification": None,
|
|
1475
|
+
"source": "cli",
|
|
1476
|
+
}
|
|
1388
1477
|
for command in commands
|
|
1389
1478
|
]
|
|
1390
1479
|
plan.extend(profile_plan)
|
|
@@ -1399,7 +1488,21 @@ class IntegrationTransaction:
|
|
|
1399
1488
|
):
|
|
1400
1489
|
raise VerifyPlanMissingError(
|
|
1401
1490
|
"integration verification requires at least one executable command"
|
|
1491
|
+
" — " + verify_plan_recovery_action()
|
|
1402
1492
|
)
|
|
1493
|
+
# 计划先落盘:verify 失败时也要看得见"打算跑什么",
|
|
1494
|
+
# 而不是只剩一个 FAILED 状态。
|
|
1495
|
+
journal["verifyPlan"] = [
|
|
1496
|
+
{
|
|
1497
|
+
"command": list(item["command"]),
|
|
1498
|
+
"kind": item.get("kind"),
|
|
1499
|
+
"verification": item.get("verification"),
|
|
1500
|
+
"source": item.get("source"),
|
|
1501
|
+
}
|
|
1502
|
+
for item in plan
|
|
1503
|
+
]
|
|
1504
|
+
journal["verificationDepth"] = verification_depth(plan)
|
|
1505
|
+
self._save(journal)
|
|
1403
1506
|
evidence_change = self._bound_evidence_change(journal)
|
|
1404
1507
|
intg_root = Path(journal["integrationRoot"]).resolve()
|
|
1405
1508
|
results: list[dict[str, Any]] = []
|
|
@@ -2253,6 +2356,20 @@ class IntegrationTransaction:
|
|
|
2253
2356
|
"steps": journal["steps"],
|
|
2254
2357
|
"mergeCommit": journal.get("mergeCommit"),
|
|
2255
2358
|
"pushedHead": journal.get("pushedHead"),
|
|
2359
|
+
# "verify = DONE" 单独一句什么也没说明。把计划、来源和退出码一起回显,
|
|
2360
|
+
# 调用方才看得出这次合并到底验证了什么。
|
|
2361
|
+
"verifyPlan": journal.get("verifyPlan") or [],
|
|
2362
|
+
"verificationDepth": journal.get("verificationDepth"),
|
|
2363
|
+
"verifyResults": [
|
|
2364
|
+
{
|
|
2365
|
+
"command": item.get("command"),
|
|
2366
|
+
"exitCode": item.get("exitCode"),
|
|
2367
|
+
"reused": bool(item.get("reused")),
|
|
2368
|
+
"durationMs": item.get("durationMs"),
|
|
2369
|
+
}
|
|
2370
|
+
for item in (journal.get("verifyResults") or [])
|
|
2371
|
+
if isinstance(item, dict)
|
|
2372
|
+
],
|
|
2256
2373
|
}
|
|
2257
2374
|
|
|
2258
2375
|
|
|
@@ -515,13 +515,42 @@ def cmd_validate_codegraph_identity(args: argparse.Namespace) -> int:
|
|
|
515
515
|
return _emit(payload, as_json=True)
|
|
516
516
|
|
|
517
517
|
|
|
518
|
+
def _input_document(args: argparse.Namespace) -> Any:
|
|
519
|
+
"""--input <file> 或 --stdin,二选一。
|
|
520
|
+
|
|
521
|
+
为了把一段 JSON 交给命令而先在 runtime/ 落一个临时文件,既多一次往返,
|
|
522
|
+
也给 runtime/ 又添一件没人清的草稿。
|
|
523
|
+
"""
|
|
524
|
+
use_stdin = bool(getattr(args, "stdin", False))
|
|
525
|
+
raw_input_path = getattr(args, "input", None)
|
|
526
|
+
if use_stdin and raw_input_path:
|
|
527
|
+
raise ValueError("--input 与 --stdin 只能二选一")
|
|
528
|
+
if use_stdin:
|
|
529
|
+
text = getattr(args, "_stdin_text", None)
|
|
530
|
+
if text is None:
|
|
531
|
+
text = sys.stdin.read()
|
|
532
|
+
return json.loads(text)
|
|
533
|
+
if not raw_input_path:
|
|
534
|
+
raise ValueError("需要 --input <file> 或 --stdin")
|
|
535
|
+
return _read_json(Path(raw_input_path))
|
|
536
|
+
|
|
537
|
+
|
|
518
538
|
def cmd_write_findings(args: argparse.Namespace) -> int:
|
|
519
|
-
|
|
539
|
+
try:
|
|
540
|
+
doc = _input_document(args)
|
|
541
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
542
|
+
return _emit({"ok": False, "code": "FINDINGS_INPUT_INVALID", "error": str(exc)}, as_json=True)
|
|
520
543
|
return _emit(write_findings(Path(args.change_dir), doc), as_json=True)
|
|
521
544
|
|
|
522
545
|
|
|
523
546
|
def cmd_write_dispositions(args: argparse.Namespace) -> int:
|
|
524
|
-
|
|
547
|
+
try:
|
|
548
|
+
doc = _input_document(args)
|
|
549
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
550
|
+
return _emit(
|
|
551
|
+
{"ok": False, "code": "DISPOSITIONS_INPUT_INVALID", "error": str(exc)},
|
|
552
|
+
as_json=True,
|
|
553
|
+
)
|
|
525
554
|
return _emit(write_dispositions(Path(args.change_dir), doc), as_json=True)
|
|
526
555
|
|
|
527
556
|
|
|
@@ -547,12 +576,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
547
576
|
|
|
548
577
|
p_findings = sub.add_parser("write-findings")
|
|
549
578
|
p_findings.add_argument("--change-dir", required=True)
|
|
550
|
-
p_findings.add_argument("--input",
|
|
579
|
+
p_findings.add_argument("--input", help="findings JSON 文件路径")
|
|
580
|
+
p_findings.add_argument(
|
|
581
|
+
"--stdin", action="store_true", help="从标准输入读 JSON,免落临时文件"
|
|
582
|
+
)
|
|
551
583
|
p_findings.set_defaults(func=cmd_write_findings)
|
|
552
584
|
|
|
553
585
|
p_dispositions = sub.add_parser("write-dispositions")
|
|
554
586
|
p_dispositions.add_argument("--change-dir", required=True)
|
|
555
|
-
p_dispositions.add_argument("--input",
|
|
587
|
+
p_dispositions.add_argument("--input", help="dispositions JSON 文件路径")
|
|
588
|
+
p_dispositions.add_argument(
|
|
589
|
+
"--stdin", action="store_true", help="从标准输入读 JSON,免落临时文件"
|
|
590
|
+
)
|
|
556
591
|
p_dispositions.set_defaults(func=cmd_write_dispositions)
|
|
557
592
|
|
|
558
593
|
p_status = sub.add_parser("status")
|
|
@@ -562,9 +597,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
562
597
|
return parser
|
|
563
598
|
|
|
564
599
|
|
|
565
|
-
def main(argv: list[str] | None = None) -> int:
|
|
600
|
+
def main(argv: list[str] | None = None, *, stdin_text: str | None = None) -> int:
|
|
566
601
|
parser = build_parser()
|
|
567
602
|
args = parser.parse_args(argv)
|
|
603
|
+
if stdin_text is not None:
|
|
604
|
+
# 测试注入用;正式路径仍然读真正的 stdin。
|
|
605
|
+
args._stdin_text = stdin_text
|
|
568
606
|
return int(args.func(args))
|
|
569
607
|
|
|
570
608
|
|