@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.
Files changed (182) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +32 -1
  3. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +22 -2
  11. package/harness/bundles/general/claude-code/harness-run/protocols.md +60 -0
  12. package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/scripts/harness_archive.py +273 -3
  17. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +451 -37
  18. package/harness/bundles/general/claude-code/scripts/harness_gate.py +88 -3
  19. package/harness/bundles/general/claude-code/scripts/harness_integration.py +156 -39
  20. package/harness/bundles/general/claude-code/scripts/harness_review.py +43 -5
  21. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +125 -10
  22. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  23. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +32 -1
  24. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +22 -2
  32. package/harness/bundles/general/codebuddy/harness-run/protocols.md +60 -0
  33. package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +273 -3
  38. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +451 -37
  39. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +88 -3
  40. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +156 -39
  41. package/harness/bundles/general/codebuddy/scripts/harness_review.py +43 -5
  42. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +125 -10
  43. package/harness/bundles/general/codex/.harness-build.json +1 -1
  44. package/harness/bundles/general/codex/harness-archive/SKILL.md +32 -1
  45. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-run/SKILL.md +22 -2
  53. package/harness/bundles/general/codex/harness-run/protocols.md +60 -0
  54. package/harness/bundles/general/codex/harness-run/reference.md +1 -1
  55. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  56. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/scripts/harness_archive.py +273 -3
  59. package/harness/bundles/general/codex/scripts/harness_fixback.py +451 -37
  60. package/harness/bundles/general/codex/scripts/harness_gate.py +88 -3
  61. package/harness/bundles/general/codex/scripts/harness_integration.py +156 -39
  62. package/harness/bundles/general/codex/scripts/harness_review.py +43 -5
  63. package/harness/bundles/general/codex/scripts/harness_runtime.py +125 -10
  64. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  65. package/harness/bundles/general/cursor/harness-archive/SKILL.md +32 -1
  66. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  67. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  68. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  73. package/harness/bundles/general/cursor/harness-run/SKILL.md +22 -2
  74. package/harness/bundles/general/cursor/harness-run/protocols.md +60 -0
  75. package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
  76. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  77. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  79. package/harness/bundles/general/cursor/scripts/harness_archive.py +273 -3
  80. package/harness/bundles/general/cursor/scripts/harness_fixback.py +451 -37
  81. package/harness/bundles/general/cursor/scripts/harness_gate.py +88 -3
  82. package/harness/bundles/general/cursor/scripts/harness_integration.py +156 -39
  83. package/harness/bundles/general/cursor/scripts/harness_review.py +43 -5
  84. package/harness/bundles/general/cursor/scripts/harness_runtime.py +125 -10
  85. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  86. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  87. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +32 -1
  88. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  89. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  90. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  92. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  93. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  94. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  96. package/harness/bundles/java/claude-code/harness-run/SKILL.md +22 -2
  97. package/harness/bundles/java/claude-code/harness-run/protocols.md +60 -0
  98. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  99. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  100. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  101. package/harness/bundles/java/claude-code/scripts/harness_archive.py +273 -3
  102. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +451 -37
  103. package/harness/bundles/java/claude-code/scripts/harness_gate.py +88 -3
  104. package/harness/bundles/java/claude-code/scripts/harness_integration.py +156 -39
  105. package/harness/bundles/java/claude-code/scripts/harness_review.py +43 -5
  106. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +125 -10
  107. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  108. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  109. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +32 -1
  110. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  111. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  112. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  113. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  114. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  116. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  117. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  118. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +22 -2
  119. package/harness/bundles/java/codebuddy/harness-run/protocols.md +60 -0
  120. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  121. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  122. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  123. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +273 -3
  124. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +451 -37
  125. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +88 -3
  126. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +156 -39
  127. package/harness/bundles/java/codebuddy/scripts/harness_review.py +43 -5
  128. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +125 -10
  129. package/harness/bundles/java/codex/.harness-build.json +1 -1
  130. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  131. package/harness/bundles/java/codex/harness-archive/SKILL.md +32 -1
  132. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  133. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  134. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  135. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  136. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  137. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  138. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  139. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  140. package/harness/bundles/java/codex/harness-run/SKILL.md +22 -2
  141. package/harness/bundles/java/codex/harness-run/protocols.md +60 -0
  142. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  143. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  144. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  145. package/harness/bundles/java/codex/scripts/harness_archive.py +273 -3
  146. package/harness/bundles/java/codex/scripts/harness_fixback.py +451 -37
  147. package/harness/bundles/java/codex/scripts/harness_gate.py +88 -3
  148. package/harness/bundles/java/codex/scripts/harness_integration.py +156 -39
  149. package/harness/bundles/java/codex/scripts/harness_review.py +43 -5
  150. package/harness/bundles/java/codex/scripts/harness_runtime.py +125 -10
  151. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  152. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  153. package/harness/bundles/java/cursor/harness-archive/SKILL.md +32 -1
  154. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  155. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  156. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  157. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  158. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  159. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  160. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  161. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  162. package/harness/bundles/java/cursor/harness-run/SKILL.md +22 -2
  163. package/harness/bundles/java/cursor/harness-run/protocols.md +60 -0
  164. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  165. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  166. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  167. package/harness/bundles/java/cursor/scripts/harness_archive.py +273 -3
  168. package/harness/bundles/java/cursor/scripts/harness_fixback.py +451 -37
  169. package/harness/bundles/java/cursor/scripts/harness_gate.py +88 -3
  170. package/harness/bundles/java/cursor/scripts/harness_integration.py +156 -39
  171. package/harness/bundles/java/cursor/scripts/harness_review.py +43 -5
  172. package/harness/bundles/java/cursor/scripts/harness_runtime.py +125 -10
  173. package/harness/manifests/general/claude-code.json +23 -23
  174. package/harness/manifests/general/codebuddy.json +23 -23
  175. package/harness/manifests/general/codex.json +23 -23
  176. package/harness/manifests/general/cursor.json +23 -23
  177. package/harness/manifests/java/claude-code.json +24 -24
  178. package/harness/manifests/java/codebuddy.json +24 -24
  179. package/harness/manifests/java/codex.json +24 -24
  180. package/harness/manifests/java/cursor.json +24 -24
  181. package/hunter-workflow-family.json +5 -5
  182. 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. 两条证据各写一个 JSON 文件,分别 register-evidence 注册",
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
- not isinstance(run, dict)
950
- or run.get("status") != expected_status
951
- or run.get("testProcessStarted") is not True
952
- or run.get("productIdentity") != product_identity
953
- or provenance.get("sessionId") != run.get("sessionId")
954
- or provenance.get("commandHash") != run.get("commandHash")
955
- or provenance.get("resultDigest") != run.get("resultDigest")
956
- or not str(run.get("commandHash") or "").startswith("sha256:")
957
- or not str(run.get("resultDigest") or "").startswith("sha256:")
958
- or not str(run.get("endedAt") or "").strip()
959
- ):
960
- raise ValueError("FIXBACK_RUN_PROVENANCE_INVALID")
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
- unresolved = [
979
- item
980
- for item in findings or []
981
- if isinstance(item, dict) and item.get("severity") in {"RED", "YELLOW"}
982
- ]
983
- if (
984
- not isinstance(review, dict)
985
- or review.get("schemaVersion") != 1
986
- or not isinstance(findings, list)
987
- or review.get("runId") != provenance.get("reviewRunId")
988
- or provenance.get("engine") != "harness-review-6d"
989
- or provenance.get("sourceDigest") != source_digest
990
- or unresolved
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
- raise ValueError("FIXBACK_REVIEW_PROVENANCE_INVALID")
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 != "registeredAt"
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 != "registeredAt"
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("--final-product-identity", required=True)
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", required=True)
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
- product_identity=args.product_identity,
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,