@hunter-harness/workflow-harness 0.2.83 → 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 +1 -1
- 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 +10 -0
- 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 +1 -1
- 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 +10 -0
- 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 +1 -1
- 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 +10 -0
- 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 +1 -1
- 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 +10 -0
- 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 +1 -1
- 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 +10 -0
- 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 +1 -1
- 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 +10 -0
- 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 +1 -1
- 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 +10 -0
- 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 +1 -1
- 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 +10 -0
- 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
|
@@ -582,6 +582,9 @@ def launch_review_fixback(
|
|
|
582
582
|
"message": f"修复编码已启动,共 {len(batch.get('issues') or [])} 个代码修复项。",
|
|
583
583
|
"runId": effective_run_id,
|
|
584
584
|
"attempt": effective_attempt,
|
|
585
|
+
# close 的 --final-product-identity 必须与 GREEN 证据一致。回显这个值,
|
|
586
|
+
# 调用方就不用自己再推导一遍(推导方式不同就会撞身份不匹配)。
|
|
587
|
+
"resolvedProductIdentity": product_identity,
|
|
585
588
|
"preparationDurationMs": duration,
|
|
586
589
|
"context": prepared,
|
|
587
590
|
"transition": begun,
|
|
@@ -839,8 +842,12 @@ def evidence_contract(
|
|
|
839
842
|
"1. 修复前:先用 run-start 跑一次能复现该问题的命令,得到 status=FAIL 的会话(RED)",
|
|
840
843
|
"2. 实施修复",
|
|
841
844
|
"3. 修复后:用同一条命令再跑一次 run-start,得到 status=OK 的会话(GREEN)",
|
|
842
|
-
"4.
|
|
845
|
+
"4. 两条证据用 evidence-template 生成,再分别 register-evidence 注册",
|
|
843
846
|
"5. resolve-issue 引用这两个已注册的证据文件",
|
|
847
|
+
"6. 关批次前还需两张收据:affected(kind=verification,受影响链的托管会话)"
|
|
848
|
+
"与 review(kind=review,指向 review-findings.json)",
|
|
849
|
+
"7. close 引用这两张收据,--final-product-identity 取 launch-review 回显的 "
|
|
850
|
+
"resolvedProductIdentity",
|
|
844
851
|
],
|
|
845
852
|
"antiPattern": (
|
|
846
853
|
"不要先改完再把修改回退来凑 RED:那样 RED 证明的只是回退后的状态,"
|
|
@@ -865,6 +872,16 @@ def evidence_contract(
|
|
|
865
872
|
f"{change} --batch-id {batch} --issue-id <issueId> "
|
|
866
873
|
"--red-evidence <red.json> --green-evidence <green.json>"
|
|
867
874
|
),
|
|
875
|
+
"template": (
|
|
876
|
+
"python harness_fixback.py evidence-template --change-dir "
|
|
877
|
+
f"{change} --kind red|green|verification|review "
|
|
878
|
+
"--session <sessionId> --out <证据 JSON 路径>"
|
|
879
|
+
),
|
|
880
|
+
"close": (
|
|
881
|
+
"python harness_fixback.py close --change-dir "
|
|
882
|
+
f"{change} --batch-id {batch} --final-product-identity {product} "
|
|
883
|
+
"--affected-receipt <verification.json> --review-receipt <review.json>"
|
|
884
|
+
),
|
|
868
885
|
},
|
|
869
886
|
"evidenceFile": {
|
|
870
887
|
"schemaVersion": 2,
|
|
@@ -873,8 +890,33 @@ def evidence_contract(
|
|
|
873
890
|
"provenance": {
|
|
874
891
|
"type": "managed-run-session",
|
|
875
892
|
"note": "指向 run-start 产出的 runtime/run-sessions/<sessionId>/session.json",
|
|
893
|
+
"requiredFields": [
|
|
894
|
+
"sessionId",
|
|
895
|
+
"commandHash",
|
|
896
|
+
"resultDigest",
|
|
897
|
+
"runReceiptPath",
|
|
898
|
+
],
|
|
899
|
+
"hint": "这四个字段都从 session.json 直接读;用 evidence-template 生成即可,不必手抄。",
|
|
876
900
|
},
|
|
877
901
|
},
|
|
902
|
+
"reviewReceipt": {
|
|
903
|
+
"kind": "review",
|
|
904
|
+
"provenance": {
|
|
905
|
+
"type": "harness-review",
|
|
906
|
+
"reviewReportPath": "reports/review/review-findings.json",
|
|
907
|
+
"engine": "harness-review-6d",
|
|
908
|
+
},
|
|
909
|
+
"blocks": (
|
|
910
|
+
"只有 disposition 为 OPEN/未登记 的 RED/YELLOW 会挡住收据。"
|
|
911
|
+
"已判定 FIXED / NOT_APPLICABLE 的放行;ACCEPTED_RISK / DEFERRED 放行"
|
|
912
|
+
"并记入收据的 residualRisks。"
|
|
913
|
+
),
|
|
914
|
+
"antiPattern": (
|
|
915
|
+
"不要为了让 close 通过而清空或删改 review-findings.json——那是发现的真相源,"
|
|
916
|
+
"写空等于抹掉整轮审查的审计轨迹。处置结论写进 fixback-dispositions.json:"
|
|
917
|
+
"python harness_review.py write-dispositions --change-dir <dir> --input <json>"
|
|
918
|
+
),
|
|
919
|
+
},
|
|
878
920
|
}
|
|
879
921
|
|
|
880
922
|
|
|
@@ -905,6 +947,272 @@ def evidence_error_message(code: str, raw_path: str) -> str:
|
|
|
905
947
|
return base if hint is None else f"{base} — {hint}"
|
|
906
948
|
|
|
907
949
|
|
|
950
|
+
# 一条发现"还没被处置"才该挡住收据。OPEN 是显式待办,UNKNOWN 是根本没登记;
|
|
951
|
+
# 其余(FIXED / NOT_APPLICABLE / ACCEPTED_RISK / DEFERRED)都已有人做过判断。
|
|
952
|
+
_UNPROCESSED_DISPOSITIONS = {"OPEN", "UNKNOWN"}
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _load_dispositions(change_dir: Path) -> dict[str, str]:
|
|
956
|
+
"""读 fixback-dispositions.json;缺失或损坏都按"无处置"处理,不抛。"""
|
|
957
|
+
path = hr.dispositions_path(change_dir)
|
|
958
|
+
if not path.is_file():
|
|
959
|
+
return {}
|
|
960
|
+
try:
|
|
961
|
+
doc = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
962
|
+
except (OSError, json.JSONDecodeError):
|
|
963
|
+
return {}
|
|
964
|
+
if not isinstance(doc, dict):
|
|
965
|
+
return {}
|
|
966
|
+
mapping: dict[str, str] = {}
|
|
967
|
+
for item in doc.get("dispositions") or []:
|
|
968
|
+
if isinstance(item, dict):
|
|
969
|
+
finding_id = item.get("findingId")
|
|
970
|
+
if isinstance(finding_id, str) and finding_id.strip():
|
|
971
|
+
mapping[finding_id] = str(item.get("disposition") or "")
|
|
972
|
+
return mapping
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def classify_review_findings(
|
|
976
|
+
change_dir: Path, findings: Any
|
|
977
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
978
|
+
"""按 disposition 把 RED/YELLOW 分成"未处置(阻塞)"与"已接受的残留风险"。
|
|
979
|
+
|
|
980
|
+
此前这里只看 severity:只要 findings 里还有一条 RED/YELLOW,收据就永远注册
|
|
981
|
+
不了。唯一能过关的做法是把 review-findings.json 清空——2026-08-19 的执行记录
|
|
982
|
+
里就是这么干的,3 条原始发现加 2 条复审发现的审计轨迹被一次写空抹掉。
|
|
983
|
+
|
|
984
|
+
findings sidecar 是发现的真相源,dispositions sidecar 才是处置的真相源。
|
|
985
|
+
门禁该问的是"还有没有没人处理的问题",不是"还有没有问题"。
|
|
986
|
+
"""
|
|
987
|
+
dispositions = _load_dispositions(change_dir)
|
|
988
|
+
blocking: list[dict[str, Any]] = []
|
|
989
|
+
residual: list[dict[str, Any]] = []
|
|
990
|
+
for item in findings or []:
|
|
991
|
+
if not isinstance(item, dict) or item.get("severity") not in {"RED", "YELLOW"}:
|
|
992
|
+
continue
|
|
993
|
+
finding_id = str(item.get("id") or "")
|
|
994
|
+
disposition = dispositions.get(finding_id) or "UNKNOWN"
|
|
995
|
+
if disposition not in hr.DISPOSITIONS:
|
|
996
|
+
disposition = "UNKNOWN"
|
|
997
|
+
entry = {
|
|
998
|
+
"findingId": finding_id,
|
|
999
|
+
"severity": item.get("severity"),
|
|
1000
|
+
"title": item.get("title"),
|
|
1001
|
+
"disposition": disposition,
|
|
1002
|
+
}
|
|
1003
|
+
if disposition in _UNPROCESSED_DISPOSITIONS:
|
|
1004
|
+
blocking.append(entry)
|
|
1005
|
+
elif disposition in hr.CURRENT_RISK_DISPOSITIONS:
|
|
1006
|
+
residual.append(entry)
|
|
1007
|
+
return residual, blocking
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def _provenance_error(code: str, problems: list[dict[str, Any]], recovery: str) -> ValueError:
|
|
1011
|
+
"""把只有错误码的失败,补成"哪个字段、期望什么、实际什么、怎么修"。"""
|
|
1012
|
+
detail = "; ".join(
|
|
1013
|
+
f"{item['field']}: expected={item['expected']!r} actual={item['actual']!r}"
|
|
1014
|
+
for item in problems
|
|
1015
|
+
)
|
|
1016
|
+
return ValueError(f"{code}: {detail} — {recovery}")
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def resolve_product_identity(change_dir: Path) -> str:
|
|
1020
|
+
"""推导当前产品身份。
|
|
1021
|
+
|
|
1022
|
+
`--product-identity` 在 5 个子命令里必填,却在全仓文档里零命中;调用方只能去
|
|
1023
|
+
读源码猜,猜完还要在 close 时原样复述一遍。规范取值就是当前 HEAD——把它做成
|
|
1024
|
+
可推导的默认值,猜的环节就没有了。
|
|
1025
|
+
"""
|
|
1026
|
+
project_root = _project_root(change_dir)
|
|
1027
|
+
try:
|
|
1028
|
+
result = subprocess.run(
|
|
1029
|
+
["git", "rev-parse", "HEAD"],
|
|
1030
|
+
cwd=str(project_root),
|
|
1031
|
+
capture_output=True,
|
|
1032
|
+
text=True,
|
|
1033
|
+
encoding="utf-8",
|
|
1034
|
+
timeout=10,
|
|
1035
|
+
check=False,
|
|
1036
|
+
)
|
|
1037
|
+
except (OSError, subprocess.SubprocessError):
|
|
1038
|
+
result = None
|
|
1039
|
+
if result is not None and result.returncode == 0:
|
|
1040
|
+
head = (result.stdout or "").strip()
|
|
1041
|
+
if head:
|
|
1042
|
+
return head
|
|
1043
|
+
# 非 git 工作区(或 git 不可用)仍需一个稳定值,否则两次推导不一致会让
|
|
1044
|
+
# close 的身份比对无故失败。
|
|
1045
|
+
digest = hashlib.sha256(str(project_root).encode("utf-8")).hexdigest()
|
|
1046
|
+
return f"unversioned:{digest}"
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
_EVIDENCE_KINDS = ("red", "green", "verification", "review")
|
|
1050
|
+
_KIND_EXPECTED_SESSION_STATUS = {"red": "FAIL", "green": "OK", "verification": "OK"}
|
|
1051
|
+
_KIND_EVIDENCE_STATUS = {"red": "FAIL", "green": "OK", "verification": "OK", "review": "OK"}
|
|
1052
|
+
_KIND_DEFAULT_GATES = {"verification": ["affected"], "review": ["review"]}
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def evidence_template(
|
|
1056
|
+
change_dir: Path,
|
|
1057
|
+
*,
|
|
1058
|
+
kind: str,
|
|
1059
|
+
session_id: str | None = None,
|
|
1060
|
+
out: str | None = None,
|
|
1061
|
+
evidence_id: str | None = None,
|
|
1062
|
+
product_identity: str | None = None,
|
|
1063
|
+
passed_gates: list[str] | None = None,
|
|
1064
|
+
) -> dict[str, Any]:
|
|
1065
|
+
"""从托管会话/审查 sidecar 直接生成可注册的证据文件。
|
|
1066
|
+
|
|
1067
|
+
ledger 早就有 scenario-receipt-template,一次就能给对骨架;fixback 没有同类
|
|
1068
|
+
入口,于是每条证据都得手写 JSON,再靠 FIXBACK_*_PROVENANCE_INVALID 反推缺了
|
|
1069
|
+
哪个字段。sessionId / commandHash / resultDigest 三个字段全都能从 session.json
|
|
1070
|
+
直接读出来——让人手抄它们没有任何意义。
|
|
1071
|
+
"""
|
|
1072
|
+
if kind not in _EVIDENCE_KINDS:
|
|
1073
|
+
return {
|
|
1074
|
+
"ok": False,
|
|
1075
|
+
"code": "FIXBACK_EVIDENCE_KIND_INVALID",
|
|
1076
|
+
"kind": kind,
|
|
1077
|
+
"recoveryAction": f"--kind 必须是 {'/'.join(_EVIDENCE_KINDS)} 之一。",
|
|
1078
|
+
}
|
|
1079
|
+
state_root = _state_root(change_dir)
|
|
1080
|
+
identity = (product_identity or "").strip()
|
|
1081
|
+
gates = list(passed_gates) if passed_gates else list(_KIND_DEFAULT_GATES.get(kind, []))
|
|
1082
|
+
|
|
1083
|
+
if kind == "review":
|
|
1084
|
+
review_path = (state_root / "reports" / "review" / "review-findings.json").resolve()
|
|
1085
|
+
if not review_path.is_file():
|
|
1086
|
+
return {
|
|
1087
|
+
"ok": False,
|
|
1088
|
+
"code": "FIXBACK_REVIEW_FINDINGS_MISSING",
|
|
1089
|
+
"expectedPath": str(review_path),
|
|
1090
|
+
"recoveryAction": (
|
|
1091
|
+
"先用 harness_review.py write-findings 写入本轮审查发现,"
|
|
1092
|
+
"再生成 review 证据。"
|
|
1093
|
+
),
|
|
1094
|
+
}
|
|
1095
|
+
review = json.loads(review_path.read_text(encoding="utf-8-sig"))
|
|
1096
|
+
residual, blocking = classify_review_findings(change_dir, review.get("findings"))
|
|
1097
|
+
if blocking:
|
|
1098
|
+
return {
|
|
1099
|
+
"ok": False,
|
|
1100
|
+
"code": "FIXBACK_REVIEW_FINDINGS_UNDISPOSITIONED",
|
|
1101
|
+
"blocking": blocking,
|
|
1102
|
+
"recoveryAction": (
|
|
1103
|
+
"以下发现尚未处置:"
|
|
1104
|
+
+ ", ".join(str(item["findingId"]) for item in blocking)
|
|
1105
|
+
+ "。用 harness_review.py write-dispositions 标注 FIXED / "
|
|
1106
|
+
"ACCEPTED_RISK / DEFERRED / NOT_APPLICABLE 后重试;"
|
|
1107
|
+
"不要删改 review-findings.json——它是发现的真相源。"
|
|
1108
|
+
),
|
|
1109
|
+
}
|
|
1110
|
+
provenance = {
|
|
1111
|
+
"type": "harness-review",
|
|
1112
|
+
"reviewReportPath": str(review_path),
|
|
1113
|
+
"reviewRunId": review.get("runId"),
|
|
1114
|
+
"engine": "harness-review-6d",
|
|
1115
|
+
"sourceDigest": "sha256:" + hashlib.sha256(review_path.read_bytes()).hexdigest(),
|
|
1116
|
+
}
|
|
1117
|
+
resolved_id = evidence_id or f"review-{review.get('runId') or uuid.uuid4().hex}"
|
|
1118
|
+
if not identity:
|
|
1119
|
+
identity = resolve_product_identity(change_dir)
|
|
1120
|
+
extra: dict[str, Any] = {"residualRisks": residual}
|
|
1121
|
+
else:
|
|
1122
|
+
if not (session_id or "").strip():
|
|
1123
|
+
return {
|
|
1124
|
+
"ok": False,
|
|
1125
|
+
"code": "FIXBACK_SESSION_REQUIRED",
|
|
1126
|
+
"recoveryAction": (
|
|
1127
|
+
f"--kind {kind} 需要 --session <sessionId>;先用 "
|
|
1128
|
+
"harness_runtime.py run-start 采集托管会话,再用它的 sessionId。"
|
|
1129
|
+
),
|
|
1130
|
+
}
|
|
1131
|
+
run_path = (
|
|
1132
|
+
state_root / "runtime" / "run-sessions" / str(session_id) / "session.json"
|
|
1133
|
+
).resolve()
|
|
1134
|
+
if not run_path.is_file():
|
|
1135
|
+
return {
|
|
1136
|
+
"ok": False,
|
|
1137
|
+
"code": "FIXBACK_SESSION_MISSING",
|
|
1138
|
+
"expectedPath": str(run_path),
|
|
1139
|
+
"recoveryAction": (
|
|
1140
|
+
"该 sessionId 下没有 session.json。确认 run-start 的 "
|
|
1141
|
+
f"--state-root 指向 {state_root},并用 run-status --wait 等到终态。"
|
|
1142
|
+
),
|
|
1143
|
+
}
|
|
1144
|
+
run = json.loads(run_path.read_text(encoding="utf-8-sig"))
|
|
1145
|
+
expected_status = _KIND_EXPECTED_SESSION_STATUS[kind]
|
|
1146
|
+
actual_status = str(run.get("status") or "")
|
|
1147
|
+
if actual_status != expected_status:
|
|
1148
|
+
return {
|
|
1149
|
+
"ok": False,
|
|
1150
|
+
"code": "FIXBACK_SESSION_STATUS_MISMATCH",
|
|
1151
|
+
"kind": kind,
|
|
1152
|
+
"expectedStatus": expected_status,
|
|
1153
|
+
"actualStatus": actual_status,
|
|
1154
|
+
"recoveryAction": (
|
|
1155
|
+
f"kind={kind} 需要 status={expected_status} 的会话,实际是 "
|
|
1156
|
+
f"{actual_status or '(空)'}。RED 必须来自**修复前**能复现问题的失败"
|
|
1157
|
+
"会话(FAIL);不要先改完再回退代码去凑 RED——那证明的是回退后的"
|
|
1158
|
+
"状态,不是原始缺陷。"
|
|
1159
|
+
),
|
|
1160
|
+
}
|
|
1161
|
+
provenance = {
|
|
1162
|
+
"type": "managed-run-session",
|
|
1163
|
+
"runReceiptPath": str(run_path),
|
|
1164
|
+
"sessionId": run.get("sessionId"),
|
|
1165
|
+
"commandHash": run.get("commandHash"),
|
|
1166
|
+
"resultDigest": run.get("resultDigest"),
|
|
1167
|
+
}
|
|
1168
|
+
resolved_id = evidence_id or f"{kind}-{session_id}"
|
|
1169
|
+
if not identity:
|
|
1170
|
+
identity = str(run.get("productIdentity") or "").strip() or resolve_product_identity(
|
|
1171
|
+
change_dir
|
|
1172
|
+
)
|
|
1173
|
+
extra = {}
|
|
1174
|
+
|
|
1175
|
+
evidence = {
|
|
1176
|
+
"schemaVersion": 2,
|
|
1177
|
+
"kind": kind,
|
|
1178
|
+
"status": _KIND_EVIDENCE_STATUS[kind],
|
|
1179
|
+
"evidenceId": resolved_id,
|
|
1180
|
+
"productIdentity": identity,
|
|
1181
|
+
"passedGates": gates,
|
|
1182
|
+
"provenance": provenance,
|
|
1183
|
+
}
|
|
1184
|
+
target = Path(out) if out else Path("runtime") / f"fixback-{resolved_id}.json"
|
|
1185
|
+
path = target if target.is_absolute() else (change_dir / target)
|
|
1186
|
+
path = path.resolve()
|
|
1187
|
+
if not _is_within(path, [change_dir.resolve(), state_root, _project_root(change_dir)]):
|
|
1188
|
+
return {
|
|
1189
|
+
"ok": False,
|
|
1190
|
+
"code": "FIXBACK_EVIDENCE_OUTSIDE_PROJECT",
|
|
1191
|
+
"path": str(path),
|
|
1192
|
+
"recoveryAction": "--out 必须落在变更目录或项目内,证据才能随归档留痕。",
|
|
1193
|
+
}
|
|
1194
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1195
|
+
path.write_text(
|
|
1196
|
+
json.dumps(evidence, ensure_ascii=False, indent=2) + "\n",
|
|
1197
|
+
encoding="utf-8",
|
|
1198
|
+
newline="\n",
|
|
1199
|
+
)
|
|
1200
|
+
return {
|
|
1201
|
+
"ok": True,
|
|
1202
|
+
"code": "FIXBACK_EVIDENCE_TEMPLATE_WRITTEN",
|
|
1203
|
+
"path": str(path),
|
|
1204
|
+
"kind": kind,
|
|
1205
|
+
"evidenceId": resolved_id,
|
|
1206
|
+
"productIdentity": identity,
|
|
1207
|
+
"evidence": evidence,
|
|
1208
|
+
"nextAction": (
|
|
1209
|
+
"python harness_fixback.py register-evidence --change-dir "
|
|
1210
|
+
f"{change_dir} --evidence {path}"
|
|
1211
|
+
),
|
|
1212
|
+
**extra,
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
|
|
908
1216
|
def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
|
|
909
1217
|
"""Register evidence only after validating its authoritative provenance."""
|
|
910
1218
|
path = _resolve_evidence_path(change_dir, raw_path)
|
|
@@ -931,6 +1239,7 @@ def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
|
|
|
931
1239
|
):
|
|
932
1240
|
raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
|
|
933
1241
|
state_root = _state_root(change_dir)
|
|
1242
|
+
residual_risks: list[dict[str, Any]] = []
|
|
934
1243
|
if kind in {"red", "green", "verification"}:
|
|
935
1244
|
if provenance.get("type") != "managed-run-session":
|
|
936
1245
|
raise ValueError("FIXBACK_RUN_PROVENANCE_REQUIRED")
|
|
@@ -945,19 +1254,43 @@ def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
|
|
|
945
1254
|
except (OSError, json.JSONDecodeError) as exc:
|
|
946
1255
|
raise ValueError("FIXBACK_RUN_PROVENANCE_INVALID") from exc
|
|
947
1256
|
expected_status = "FAIL" if kind == "red" else "OK"
|
|
948
|
-
if (
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1257
|
+
if not isinstance(run, dict):
|
|
1258
|
+
raise _provenance_error(
|
|
1259
|
+
"FIXBACK_RUN_PROVENANCE_INVALID",
|
|
1260
|
+
[{"field": "session.json", "expected": "object", "actual": type(run).__name__}],
|
|
1261
|
+
"run-start 产出的 session.json 应当是一个 JSON 对象。",
|
|
1262
|
+
)
|
|
1263
|
+
# 逐字段比对而不是一个大 or:只回显错误码时,调用方唯一的出路是去读源码
|
|
1264
|
+
# 反推是哪一项不匹配(2026-08-19 的执行记录里就读了三遍)。
|
|
1265
|
+
checks = [
|
|
1266
|
+
("status", expected_status, run.get("status")),
|
|
1267
|
+
("testProcessStarted", True, run.get("testProcessStarted")),
|
|
1268
|
+
("session.productIdentity", product_identity, run.get("productIdentity")),
|
|
1269
|
+
("provenance.sessionId", run.get("sessionId"), provenance.get("sessionId")),
|
|
1270
|
+
("provenance.commandHash", run.get("commandHash"), provenance.get("commandHash")),
|
|
1271
|
+
("provenance.resultDigest", run.get("resultDigest"), provenance.get("resultDigest")),
|
|
1272
|
+
]
|
|
1273
|
+
problems = [
|
|
1274
|
+
{"field": field, "expected": expected, "actual": actual}
|
|
1275
|
+
for field, expected, actual in checks
|
|
1276
|
+
if expected != actual
|
|
1277
|
+
]
|
|
1278
|
+
for field in ("commandHash", "resultDigest"):
|
|
1279
|
+
value = str(run.get(field) or "")
|
|
1280
|
+
if not value.startswith("sha256:"):
|
|
1281
|
+
problems.append(
|
|
1282
|
+
{"field": f"session.{field}", "expected": "sha256:...", "actual": value}
|
|
1283
|
+
)
|
|
1284
|
+
if not str(run.get("endedAt") or "").strip():
|
|
1285
|
+
problems.append({"field": "session.endedAt", "expected": "非空时间戳", "actual": ""})
|
|
1286
|
+
if problems:
|
|
1287
|
+
raise _provenance_error(
|
|
1288
|
+
"FIXBACK_RUN_PROVENANCE_INVALID",
|
|
1289
|
+
problems,
|
|
1290
|
+
"不要手改证据文件去对齐;用 `harness_fixback.py evidence-template "
|
|
1291
|
+
f"--change-dir {change_dir} --kind {kind} --session <sessionId>` "
|
|
1292
|
+
"重新生成,它直接从 session.json 取这些字段。",
|
|
1293
|
+
)
|
|
961
1294
|
else:
|
|
962
1295
|
if provenance.get("type") != "harness-review":
|
|
963
1296
|
raise ValueError("FIXBACK_REVIEW_PROVENANCE_REQUIRED")
|
|
@@ -975,21 +1308,55 @@ def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
|
|
|
975
1308
|
raise ValueError("FIXBACK_REVIEW_PROVENANCE_INVALID") from exc
|
|
976
1309
|
source_digest = "sha256:" + hashlib.sha256(review_path.read_bytes()).hexdigest()
|
|
977
1310
|
findings = review.get("findings") if isinstance(review, dict) else None
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1311
|
+
problems: list[dict[str, Any]] = []
|
|
1312
|
+
if not isinstance(review, dict) or review.get("schemaVersion") != 1:
|
|
1313
|
+
problems.append({
|
|
1314
|
+
"field": "review-findings.schemaVersion",
|
|
1315
|
+
"expected": 1,
|
|
1316
|
+
"actual": review.get("schemaVersion") if isinstance(review, dict) else None,
|
|
1317
|
+
})
|
|
1318
|
+
if not isinstance(findings, list):
|
|
1319
|
+
problems.append({
|
|
1320
|
+
"field": "review-findings.findings",
|
|
1321
|
+
"expected": "list",
|
|
1322
|
+
"actual": type(findings).__name__,
|
|
1323
|
+
})
|
|
1324
|
+
for field, expected, actual in (
|
|
1325
|
+
(
|
|
1326
|
+
"provenance.reviewRunId",
|
|
1327
|
+
review.get("runId") if isinstance(review, dict) else None,
|
|
1328
|
+
provenance.get("reviewRunId"),
|
|
1329
|
+
),
|
|
1330
|
+
("provenance.engine", "harness-review-6d", provenance.get("engine")),
|
|
1331
|
+
("provenance.sourceDigest", source_digest, provenance.get("sourceDigest")),
|
|
991
1332
|
):
|
|
992
|
-
|
|
1333
|
+
if expected != actual:
|
|
1334
|
+
problems.append({"field": field, "expected": expected, "actual": actual})
|
|
1335
|
+
if problems:
|
|
1336
|
+
raise _provenance_error(
|
|
1337
|
+
"FIXBACK_REVIEW_PROVENANCE_INVALID",
|
|
1338
|
+
problems,
|
|
1339
|
+
"用 `harness_fixback.py evidence-template --change-dir "
|
|
1340
|
+
f"{change_dir} --kind review` 重新生成 review 证据。",
|
|
1341
|
+
)
|
|
1342
|
+
# 只有"没人处置过"的 RED/YELLOW 才该挡住收据;已判定 FIXED / ACCEPTED_RISK /
|
|
1343
|
+
# DEFERRED / NOT_APPLICABLE 的照常放行,findings sidecar 原样保留。
|
|
1344
|
+
residual_risks, blocking = classify_review_findings(change_dir, findings)
|
|
1345
|
+
if blocking:
|
|
1346
|
+
raise _provenance_error(
|
|
1347
|
+
"FIXBACK_REVIEW_PROVENANCE_INVALID",
|
|
1348
|
+
[
|
|
1349
|
+
{
|
|
1350
|
+
"field": f"finding[{item['findingId']}]",
|
|
1351
|
+
"expected": "已处置(FIXED / ACCEPTED_RISK / DEFERRED / NOT_APPLICABLE)",
|
|
1352
|
+
"actual": item["disposition"],
|
|
1353
|
+
}
|
|
1354
|
+
for item in blocking
|
|
1355
|
+
],
|
|
1356
|
+
"用 `harness_review.py write-dispositions` 为这些发现登记处置结论后重试。"
|
|
1357
|
+
"**不要**清空或删改 review-findings.json——它是发现的真相源,"
|
|
1358
|
+
"处置结论属于 fixback-dispositions.json。",
|
|
1359
|
+
)
|
|
993
1360
|
record = {
|
|
994
1361
|
"path": str(path),
|
|
995
1362
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
@@ -999,20 +1366,25 @@ def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
|
|
|
999
1366
|
"productIdentity": product_identity,
|
|
1000
1367
|
"passedGates": sorted(set(passed_gates)),
|
|
1001
1368
|
"provenance": provenance,
|
|
1369
|
+
# 已接受/已推迟的风险必须随收据留痕,否则"放行"会读成"没有问题"。
|
|
1370
|
+
"residualRisks": residual_risks,
|
|
1002
1371
|
"registeredAt": now_iso(),
|
|
1003
1372
|
}
|
|
1004
1373
|
ledger = _load_evidence_ledger(change_dir)
|
|
1005
1374
|
existing = ledger["evidence"].get(evidence_id)
|
|
1006
1375
|
if existing is not None:
|
|
1376
|
+
# residualRisks 是从 dispositions 派生的上下文,不属于证据自身的身份;
|
|
1377
|
+
# 把它算进比对会让"处置结论更新后重新注册"误报 ID 复用。
|
|
1378
|
+
derived = {"registeredAt", "residualRisks"}
|
|
1007
1379
|
comparable = {
|
|
1008
1380
|
key: value
|
|
1009
1381
|
for key, value in record.items()
|
|
1010
|
-
if key
|
|
1382
|
+
if key not in derived
|
|
1011
1383
|
}
|
|
1012
1384
|
existing_comparable = {
|
|
1013
1385
|
key: value
|
|
1014
1386
|
for key, value in existing.items()
|
|
1015
|
-
if key
|
|
1387
|
+
if key not in derived
|
|
1016
1388
|
} if isinstance(existing, dict) else {}
|
|
1017
1389
|
if existing_comparable != comparable:
|
|
1018
1390
|
raise ValueError(f"FIXBACK_EVIDENCE_ID_REUSED: {evidence_id}")
|
|
@@ -1402,14 +1774,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1402
1774
|
resume.add_argument("--json", action="store_true")
|
|
1403
1775
|
resume.add_argument("--change-dir", required=True)
|
|
1404
1776
|
resume.add_argument("--batch-id")
|
|
1405
|
-
resume.add_argument("--product-identity")
|
|
1777
|
+
resume.add_argument("--product-identity", help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1406
1778
|
resume.add_argument("--root-cause")
|
|
1407
1779
|
resume.add_argument("--run-id")
|
|
1408
1780
|
resume.add_argument("--attempt", type=int)
|
|
1409
1781
|
opened = sub.add_parser("open")
|
|
1410
1782
|
opened.add_argument("--change-dir", required=True)
|
|
1411
1783
|
opened.add_argument("--batch-id", required=True)
|
|
1412
|
-
opened.add_argument("--product-identity", required=True)
|
|
1784
|
+
opened.add_argument("--product-identity", required=True, help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1413
1785
|
opened.add_argument("--root-cause", required=True)
|
|
1414
1786
|
issue = sub.add_parser("add-issue")
|
|
1415
1787
|
issue.add_argument("--change-dir", required=True)
|
|
@@ -1427,7 +1799,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1427
1799
|
close = sub.add_parser("close")
|
|
1428
1800
|
close.add_argument("--change-dir", required=True)
|
|
1429
1801
|
close.add_argument("--batch-id", required=True)
|
|
1430
|
-
close.add_argument(
|
|
1802
|
+
close.add_argument(
|
|
1803
|
+
"--final-product-identity",
|
|
1804
|
+
required=True,
|
|
1805
|
+
help=(
|
|
1806
|
+
"关批次时的产品身份,必须与每条 GREEN 证据的 productIdentity 相同。"
|
|
1807
|
+
"取 launch-review 回显的 resolvedProductIdentity,不要另行推导。"
|
|
1808
|
+
),
|
|
1809
|
+
)
|
|
1431
1810
|
close.add_argument("--affected-receipt", required=True)
|
|
1432
1811
|
close.add_argument("--review-receipt", required=True)
|
|
1433
1812
|
status = sub.add_parser("status")
|
|
@@ -1435,17 +1814,37 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1435
1814
|
status.add_argument("--batch-id", required=True)
|
|
1436
1815
|
freeze = sub.add_parser("freeze-readiness")
|
|
1437
1816
|
freeze.add_argument("--change-dir", required=True)
|
|
1438
|
-
freeze.add_argument("--product-identity", required=True)
|
|
1817
|
+
freeze.add_argument("--product-identity", required=True, help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1439
1818
|
register = sub.add_parser("register-evidence")
|
|
1440
1819
|
register.add_argument("--change-dir", required=True)
|
|
1441
1820
|
register.add_argument("--evidence", required=True)
|
|
1821
|
+
template = sub.add_parser(
|
|
1822
|
+
"evidence-template",
|
|
1823
|
+
help="从托管会话/审查 sidecar 生成可直接注册的证据 JSON",
|
|
1824
|
+
)
|
|
1825
|
+
template.add_argument("--change-dir", required=True)
|
|
1826
|
+
template.add_argument(
|
|
1827
|
+
"--kind",
|
|
1828
|
+
required=True,
|
|
1829
|
+
choices=list(_EVIDENCE_KINDS),
|
|
1830
|
+
help="red=修复前失败会话;green=修复后成功会话;verification=受影响链;review=审查收据",
|
|
1831
|
+
)
|
|
1832
|
+
template.add_argument(
|
|
1833
|
+
"--session",
|
|
1834
|
+
help="run-start 产出的 sessionId(kind=red/green/verification 必填)",
|
|
1835
|
+
)
|
|
1836
|
+
template.add_argument("--out", help="输出路径,默认 runtime/fixback-<evidenceId>.json")
|
|
1837
|
+
template.add_argument("--evidence-id")
|
|
1838
|
+
template.add_argument("--product-identity", help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1839
|
+
template.add_argument("--passed-gate", action="append", default=[])
|
|
1840
|
+
template.add_argument("--json", action="store_true")
|
|
1442
1841
|
review_plan = sub.add_parser("review-plan")
|
|
1443
1842
|
review_plan.add_argument("--change-dir", required=True)
|
|
1444
1843
|
review_plan.add_argument("--json", action="store_true")
|
|
1445
1844
|
review_start = sub.add_parser("start-review")
|
|
1446
1845
|
review_start.add_argument("--change-dir", required=True)
|
|
1447
1846
|
review_start.add_argument("--batch-id", required=True)
|
|
1448
|
-
review_start.add_argument("--product-identity", required=True)
|
|
1847
|
+
review_start.add_argument("--product-identity", required=True, help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1449
1848
|
review_start.add_argument("--run-id", required=True)
|
|
1450
1849
|
review_start.add_argument("--attempt", type=int, required=True)
|
|
1451
1850
|
review_start.add_argument("--json", action="store_true")
|
|
@@ -1455,7 +1854,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1455
1854
|
launch_review.add_argument("--change-dir", required=True)
|
|
1456
1855
|
launch_review.add_argument("--executor", required=True)
|
|
1457
1856
|
launch_review.add_argument("--skills-root", required=True)
|
|
1458
|
-
launch_review.add_argument("--product-identity",
|
|
1857
|
+
launch_review.add_argument("--product-identity", help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1459
1858
|
launch_review.add_argument("--run-id")
|
|
1460
1859
|
launch_review.add_argument("--attempt", type=int, default=2)
|
|
1461
1860
|
launch_review.add_argument("--batch-id")
|
|
@@ -1489,7 +1888,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1489
1888
|
change_dir=change_dir,
|
|
1490
1889
|
executor=args.executor,
|
|
1491
1890
|
skills_root=Path(args.skills_root),
|
|
1492
|
-
|
|
1891
|
+
# 省略时推导当前 HEAD:这个值全仓文档零命中,硬性必填只会
|
|
1892
|
+
# 逼调用方去 grep 源码猜(2026-08-19 那轮猜了 8 次)。
|
|
1893
|
+
product_identity=(
|
|
1894
|
+
args.product_identity
|
|
1895
|
+
or resolve_product_identity(change_dir)
|
|
1896
|
+
),
|
|
1493
1897
|
run_id=args.run_id,
|
|
1494
1898
|
attempt=args.attempt,
|
|
1495
1899
|
batch_id=args.batch_id,
|
|
@@ -1543,6 +1947,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1543
1947
|
value = batch_status(change_dir, args.batch_id)
|
|
1544
1948
|
elif args.command == "register-evidence":
|
|
1545
1949
|
value = register_evidence(change_dir, args.evidence)
|
|
1950
|
+
elif args.command == "evidence-template":
|
|
1951
|
+
value = evidence_template(
|
|
1952
|
+
change_dir,
|
|
1953
|
+
kind=args.kind,
|
|
1954
|
+
session_id=args.session,
|
|
1955
|
+
out=args.out,
|
|
1956
|
+
evidence_id=args.evidence_id,
|
|
1957
|
+
product_identity=args.product_identity,
|
|
1958
|
+
passed_gates=args.passed_gate or None,
|
|
1959
|
+
)
|
|
1546
1960
|
else:
|
|
1547
1961
|
value = freeze_readiness(
|
|
1548
1962
|
change_dir,
|
|
@@ -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
|
|
|
@@ -3335,6 +3336,14 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3335
3336
|
phase=args.phase,
|
|
3336
3337
|
run_id=run_id,
|
|
3337
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
|
+
|
|
3338
3347
|
payload = {
|
|
3339
3348
|
"ok": True,
|
|
3340
3349
|
"code": close_code,
|
|
@@ -3353,6 +3362,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3353
3362
|
"contextHandoff": context_handoff,
|
|
3354
3363
|
"platformMonitor": platform_monitor,
|
|
3355
3364
|
"gateRecovery": recovery_result,
|
|
3365
|
+
"scratchSwept": scratch_swept,
|
|
3356
3366
|
}
|
|
3357
3367
|
emit(payload, as_json=as_json)
|
|
3358
3368
|
return 0
|