@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
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "claude-code",
5
- "coreHash": "611f91f44adcd1d1"
5
+ "coreHash": "2e7fe0fb0fef6467"
6
6
  }
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-archive — 变更归档
26
26
 
27
27
  ## Purpose
@@ -151,6 +151,37 @@ disallowed-tools:
151
151
  | `SENSITIVE_EVIDENCE_UNQUARANTINED`(默认只告警,`HUNTER_HARNESS_SENSITIVE_SCAN=block` 时才阻断) | `python <skills-root>/scripts/harness_runtime.py quarantine-evidence --project . --change-dir ".harness/changes/<cn>" --file "<相对 change-dir 的路径>" --reason "<为什么是敏感证据>" --json`(`--file` 可重复)。私有根默认已与项目同盘、且在项目根之外——**不要**手工指定项目内的路径,归档的密钥扫描会以 `SECRET_SCAN_PRIVATE_PATH_IN_COPY_ROOT` 拒绝 |
152
152
  | `DIFF_ZERO_WITH_NONEMPTY_COMMIT`(提交范围非空但 filesChanged=0) | 契约缺 `ownership.productPaths`,全部改动被判为 `foreignPaths`。用 `python <skills-root>/scripts/harness_change.py declare-ownership --change <cn> --product-path "<目录前缀或精确文件>" --json` 按计划的实际改动范围声明(可重复;只收精确路径,不支持通配)。**不要**手改 `change-context.json` |
153
153
 
154
+ ### 二·A·1、发布内容预检与服务端 422
155
+
156
+ `status`(`execute` 的预检)会用**与服务端同源的规则**(`packages/core` 的
157
+ `scanSensitiveFiles`)扫描**真正会进 ZIP 的那批文件**——`reports/final/summary-data.json`、
158
+ `spec/**.md`、`plans/**.md`、`candidates/knowledge.json`、`meta/archive-meta.md`、
159
+ `meta/change-context.json`。`runtime/` 是过程草稿,从不入包,也不进这道扫描。
160
+
161
+ 结果在 `checks.publication_content_scan`:
162
+
163
+ - `blocked=false` → 上传不会因内容被拒
164
+ - `blocked=true` → **服务端会以 HTTP 422 `archive contains sensitive content` 拒收**。
165
+ 归档不阻断(服务端策略不归本地裁决),但必须在此时处理,别等跑完全流程才发现
166
+ - `reasonCode=PUBLICATION_CONTENT_SCAN_UNAVAILABLE` → CLI 不可用,预检没跑;归档继续,
167
+ 上传仍可能 422
168
+
169
+ 每条 finding 都带 `rule_id / path / line / column / overridable / recovery_action`。
170
+
171
+ **处置:**
172
+
173
+ | 命中类型 | 出路 |
174
+ |---|---|
175
+ | `overridable=true`(medium/low,如 `HH_INTERNAL_ADDRESS` 内网地址、`HH_WINDOWS_ABSOLUTE_PATH`) | 若确属设计固有内容,在源文件该行附近加行内标注:`<!-- hunter-harness-ignore: <RULE_ID> reason=<简短理由> -->`,然后 `republish` 重建重传 |
176
+ | `overridable=false`(high,如私钥、真实 token) | **不提供豁免。** 必须真正脱敏后重新打包 |
177
+
178
+ > ⚠️ 行内标注是**申报**,不是绕过:它把"这是设计决策"写进文档本身,可审计、可追溯。
179
+ > **不要**为了过扫描去删改设计文档的事实内容——那是篡改证据。
180
+ >
181
+ > ⚠️ 已知限制:`hunter-harness-ignore` 在本地扫描器上确认生效;**服务端是否认这条标注尚未验证**
182
+ > (归档上传端点只收裸 ZIP,没有独立的豁免申报通道)。若加了标注仍被 422 拒收,
183
+ > 剩余动作是在平台侧对该规则/内容加白,本地无法自解。
184
+
154
185
  ### 二·B、归档补传(上传失败或历史归档缺条目)
155
186
 
156
187
  归档上传成功后 ZIP 与回执会被清理,旧版本产生的归档从未入 outbox——所以
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-codebase-map — 代码库地图
25
25
 
26
26
  ## Purpose
@@ -8,7 +8,7 @@ disallowed-tools:
8
8
  - Bash(git *)
9
9
  - Bash(curl *)
10
10
  ---
11
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
11
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
12
12
  # harness-knowledge-ingest
13
13
 
14
14
  知识 ingest 完全由 Hunter Platform 负责。归档完成时,客户端生成一个确定性 ZIP;
@@ -8,7 +8,7 @@ disallowed-tools:
8
8
  - Bash(git *)
9
9
  - Bash(curl *)
10
10
  ---
11
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
11
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
12
12
  # harness-knowledge-query
13
13
 
14
14
  项目知识以 Hunter Platform 的服务端索引为唯一真源。客户端只提交查询并消费结果:
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-plan — 需求规划
26
26
 
27
27
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-pull — 从 Hunter Platform 下拉与恢复
26
26
 
27
27
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-push — 上传到 Hunter Platform
26
26
 
27
27
  ## Purpose
@@ -22,7 +22,7 @@ disallowed-tools:
22
22
  - Bash(curl *)
23
23
  - Bash(codegraph *)
24
24
  ---
25
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
26
26
  # harness-review — 代码审查
27
27
 
28
28
  ## Purpose
@@ -22,7 +22,7 @@ disallowed-tools:
22
22
  - Bash(curl *)
23
23
  - Bash(codegraph *)
24
24
  ---
25
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
26
26
  # harness-run — 需求编码
27
27
 
28
28
  ## Purpose
@@ -92,6 +92,24 @@ disallowed-tools:
92
92
 
93
93
  **Fixback**:入口只用 `launch-review`,后续问题处理通过 `resolve-issue/close` 驱动,不得把修复说明当成新的普通 Run。只读取返回的受影响问题和文件;验证仅失效与 `changedFiles` 相交的目标,其他 Test/Review 证据继续复用。RED 优先;`manual`、`workflow` 或未选用的建议不进入代码批次,使用中文记录处理结论。
94
94
 
95
+ **构建/测试执行入口**:所有构建与测试命令(mvn / gradle / npm test / pytest 等)必须经 `harness_test_runner.py exec` 发起,**禁止**用 `powershell.exe -Command` 直接裸跑:
96
+
97
+ ```text
98
+ python <skills-root>/scripts/harness_test_runner.py exec --project . --timeout-seconds <预估上限> -- <构建命令及参数>
99
+ ```
100
+
101
+ 该入口持有项目级互斥锁并托管进程树。裸跑构建在宿主工具超时后会被推入后台,多个后台构建同时写同一个 `target/`、`build/` 或 `node_modules/.cache`,表现为编译产物被清空、日志文件被占用、"增量编译抖动"——这些都不是代码缺陷,是并发自相残杀,排查成本极高。`--timeout-seconds` 按最慢的一次全量构建估(默认 300 秒偏短),宁可给足也不要让命令被中途掐断。
102
+
103
+ 返回 `TEST_RUN_ALREADY_ACTIVE`(退出码 3)说明**已有构建在跑**:等它结束或用 `lock-status` 查明持有者,**不得**改用裸跑绕开锁,也不得另起一个并行构建。确认持有进程已消失时才用 `lock-reap` 回收。
104
+
105
+ **长阶段租约续期**:`gate begin` 的租约默认 TTL 3600 秒,而 run 阶段常常跑得更久。租约过期不会中断执行,只会让最后的 `gate close` 报 `LEASE_ABSENT`。凡预计超过 1 小时的阶段,每完成一个变更簇就用本阶段**原 run-id** 续租一次(同 run-id 重复 claim 即刷新,不会新开 attempt):
106
+
107
+ ```text
108
+ python <skills-root>/scripts/harness_change.py claim --change <id> --phase run --run-id <本阶段 run-id> --ttl-seconds 3600 --json
109
+ ```
110
+
111
+ 已经报了 `LEASE_ABSENT` 也按同一条命令恢复——错误响应的 `resumeRunId` 就是要用的 run-id;**不要**重跑 `gate begin`,那会新开 attempt 并丢失本轮 capsule。
112
+
95
113
  **执行器边界**:优先使用项目 build profile 和已有测试入口。禁止为了绕过 ESM、路径或参数问题临时生成 `.js`、`require` 脚本;需要文件式 runner 时使用项目已有入口,确需新增时遵循项目模块类型(例如 ESM 使用 `.mjs`)。runner 包装说明写入 `runnerCommand` 元数据,不得拼进账本的规范 `command`。
96
114
 
97
115
  **Foundation Gate**:若 `meta/implementation-checkpoints.json` 中 `foundation-gate` 为 pending,不得开始 plan 中任务 6+;由 `harness_gate.py` 硬阻断。
@@ -123,7 +141,9 @@ disallowed-tools:
123
141
  | **预存变更** | 保留 → baseline 隔离;存在则最终 ≥ 🟡WARN |
124
142
  | **关门/状态** | 10 项关门检查;持久化 run-task-status;仅 run-owned P0 静态-only 导致 WARN;test-owned 待办正常移交 |
125
143
  | **Worktree** | `requested=true` 时代码只写 worktree |
126
- | **PowerShell** | 所有 git/构建经 `powershell.exe -NoProfile -Command` |
144
+ | **构建/测试** | 一律经 `harness_test_runner.py exec`;禁止裸跑 mvn/gradle/npm test;`TEST_RUN_ALREADY_ACTIVE` 表示已有构建在跑,等待而非另起 |
145
+ | **租约** | 阶段超 1 小时按变更簇用原 run-id 续租;`LEASE_ABSENT` 用 `harness_change.py claim` 恢复,不重跑 begin |
146
+ | **PowerShell** | 所有 git 经 `powershell.exe -NoProfile -Command` |
127
147
 
128
148
  ### 陈旧测试安全修复与精确跟踪
129
149
 
@@ -79,3 +79,63 @@ REFACTOR 只允许不改变行为的整理。若重构改变行为,必须回
79
79
  ```
80
80
 
81
81
  RED 问题必须在当前 run 中处理或明确记录为未处理风险;YELLOW 问题可交给后续 `/harness-review`。
82
+
83
+ ## fixback 证据契约
84
+
85
+ > `launch-review` 的返回体里带 `evidenceContract` 字段,内容与本节一致。这里写一份是因为
86
+ > 光靠运行时返回,调用方往往已经动手改代码了才看到。
87
+
88
+ ### 命令链
89
+
90
+ ```text
91
+ run-start(修复前,采 RED)→ 改代码 → run-start(修复后,采 GREEN)
92
+ → evidence-template ×2 → register-evidence ×2 → resolve-issue
93
+ → run-start(受影响链)→ evidence-template --kind verification
94
+ → evidence-template --kind review → close
95
+ ```
96
+
97
+ ### 别手写证据 JSON
98
+
99
+ 证据文件的 `provenance` 需要 `sessionId` / `commandHash` / `resultDigest` / `runReceiptPath`
100
+ 四个字段,全部能从 `session.json` 直接读出来。用模板生成,不要手抄:
101
+
102
+ ```text
103
+ python <skills-root>/scripts/harness_fixback.py evidence-template \
104
+ --change-dir <change-dir> --kind red|green|verification|review \
105
+ --session <sessionId> --out <证据 JSON 路径> --json
106
+ ```
107
+
108
+ 输出即可直接喂给 `register-evidence`。撞 `FIXBACK_RUN_PROVENANCE_INVALID` 时,错误体会点名
109
+ **哪个字段、期望什么、实际什么**——按它改,不要去读实现反推。
110
+
111
+ ### `--product-identity`
112
+
113
+ 产品身份指纹,把 RED/GREEN 绑定到同一产品状态。规范取值是**当前 git HEAD**。省略即自动推导,
114
+ `launch-review` 会在返回体的 `resolvedProductIdentity` 里回显;`close --final-product-identity`
115
+ **原样引用那个值**,不要另行推导——推导方式不同就会撞 `FIXBACK_GREEN_IDENTITY_MISMATCH`。
116
+
117
+ ### RED 必须在修复之前
118
+
119
+ RED 证明问题真实存在,GREEN 证明修复真的生效。先改完再把改动回退来凑 RED,证明的只是回退后的
120
+ 状态,不是原始缺陷,中途还会留下脏工作树。
121
+
122
+ ### close 的两张收据与 review-findings.json
123
+
124
+ `close` 要 `--affected-receipt`(`kind=verification`,受影响链的托管会话)和 `--review-receipt`
125
+ (`kind=review`,指向 `reports/review/review-findings.json`)。
126
+
127
+ review 收据的放行规则按 **disposition**,不按 severity:
128
+
129
+ | disposition | 是否阻塞 | 说明 |
130
+ |---|---|---|
131
+ | `OPEN` / 未登记 | ❌ 阻塞 | 还没人处置,必须先给结论 |
132
+ | `FIXED` / `NOT_APPLICABLE` | ✅ 放行 | 已闭环 |
133
+ | `ACCEPTED_RISK` / `DEFERRED` | ✅ 放行 | 记入收据 `residualRisks` 留痕 |
134
+
135
+ > ⚠️ **不要为了让 close 通过而清空或删改 `review-findings.json`。** 它是发现的真相源,写空等于
136
+ > 抹掉整轮审查的审计轨迹。处置结论属于另一个 sidecar:
137
+ >
138
+ > ```text
139
+ > python <skills-root>/scripts/harness_review.py write-dispositions \
140
+ > --change-dir <change-dir> --input <dispositions.json>
141
+ > ```
@@ -94,7 +94,7 @@ requested=true + path missing
94
94
  6. **读取测试场景表**:`.harness/changes/<change-name>/plans/<change-name>-test-scenarios.md` → 获取测试真相源
95
95
  7. **读取验证账本**:通过 state layout resolver 定位 `evidence/verification-ledger.json`(如存在)→ 复用已有 compile/unitTest 结果
96
96
  8. **读取任务状态**:`.harness/changes/<change-name>/evidence/run-task-status.md`(如存在)→ 恢复上次运行状态
97
- 9. **读取 review fixback**:用户传入 `--fixback` 或要求修复 review 问题时,读取最新 `.harness/changes/<change-name>/reports/review/fixback-*.md`,并用 `harness_fixback.py` 将相关 RED/YELLOW 条目合并为一个批次;每条问题保留 RED/GREEN 证据,批次关闭后只触发一次 affected verification 与一次 review
97
+ 9. **读取 review fixback**:用户传入 `--fixback` 或要求修复 review 问题时,读取最新 `.harness/changes/<change-name>/reports/review/fixback-*.md`,并用 `harness_fixback.py` 将相关 RED/YELLOW 条目合并为一个批次;每条问题保留 RED/GREEN 证据,批次关闭后只触发一次 affected verification 与一次 review。证据契约见下方[fixback 证据契约](#fixback-证据契约)——**动手前先读**,它决定了 RED 必须在修复之前采集
98
98
  10. 确认 `项目规则(见 .harness/context-index.json)/` 规则已加载
99
99
  11. **执行测试基础设施探测**(见下方"步骤 0.5")
100
100
  12. 确认构建环境正常(构建命令按技术栈,见项目 CLAUDE.md 或 `.harness/config/harness-build-config.md`;如 Java 的 `powershell.exe -Command "mvn compile -pl <module> -o -q"`)
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-submit — 最终提交(含 worktree 合并)
26
26
 
27
27
  ## Purpose
@@ -19,7 +19,7 @@ disallowed-tools:
19
19
  - Bash(awk *)
20
20
  - Bash(curl *)
21
21
  ---
22
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
22
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
23
23
  # harness-sync
24
24
 
25
25
  ## Purpose
@@ -23,7 +23,7 @@ disallowed-tools:
23
23
  - Bash(node *)
24
24
  - Bash(codegraph *)
25
25
  ---
26
- <!-- generated by harness_deploy.py; core=611f91f44adcd1d1; overlay=none; agent=claude-code; do not edit -->
26
+ <!-- generated by harness_deploy.py; core=2e7fe0fb0fef6467; overlay=none; agent=claude-code; do not edit -->
27
27
  # harness-test — 测试执行
28
28
 
29
29
  ## Purpose
@@ -2167,6 +2167,185 @@ def _advisory_sensitive_gate(result: dict[str, Any], policy: str) -> dict[str, A
2167
2167
  }
2168
2168
 
2169
2169
 
2170
+ ARCHIVE_EXECUTE_RESULT_REL = Path("meta") / "archive-execute-result.json"
2171
+
2172
+
2173
+ def resolve_execute_result_path(change_dir: Path, raw_path: str) -> dict[str, Any]:
2174
+ """校验 `--output`:execute 会移走 change 目录,写进去的结果必然丢失。
2175
+
2176
+ 这个错误只能在**动手前**报。等 execute 跑完再发现输出文件不见了,steps 结果
2177
+ 已经随目录被移走且无法重建——调用方只剩考古一条路。
2178
+ """
2179
+ change_dir = change_dir.expanduser().resolve()
2180
+ target = Path(raw_path).expanduser()
2181
+ target = target if target.is_absolute() else (Path.cwd() / target)
2182
+ target = target.resolve()
2183
+ try:
2184
+ target.relative_to(change_dir)
2185
+ except ValueError:
2186
+ return {"ok": True, "path": str(target)}
2187
+ suggestion = change_dir.parent / f"{change_dir.name}-archive-result.json"
2188
+ return {
2189
+ "ok": False,
2190
+ "reasonCode": "ARCHIVE_RESULT_PATH_VOLATILE",
2191
+ "path": str(target),
2192
+ "changeDir": str(change_dir),
2193
+ "recoveryAction": (
2194
+ f"归档会把 {change_dir} 整个移进 .harness/archive/,写在它内部的输出文件"
2195
+ f"会随之消失。把 --output 指到目录之外,例如 {suggestion};"
2196
+ "归档成功后结果也会自动落在 <archiveDir>/meta/archive-execute-result.json。"
2197
+ ),
2198
+ }
2199
+
2200
+
2201
+ def persist_execute_result(archive_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
2202
+ """把 execute 的完整结果写进归档目录——移动之后的位置,不会再被搬走。
2203
+
2204
+ 调用方此前只能靠 shell 重定向抓 stdout,而唯一"顺手"的落点(change 目录)正是
2205
+ 要被移走的那个。结果落盘在归档里,并把路径回显进 payload,重定向就不再必要。
2206
+ """
2207
+ archive_dir = archive_dir.expanduser().resolve()
2208
+ path = archive_dir / ARCHIVE_EXECUTE_RESULT_REL
2209
+ try:
2210
+ path.parent.mkdir(parents=True, exist_ok=True)
2211
+ path.write_text(
2212
+ json.dumps(payload, ensure_ascii=False, indent=2, default=str) + "\n",
2213
+ encoding="utf-8",
2214
+ newline="\n",
2215
+ )
2216
+ except (OSError, TypeError, ValueError) as exc:
2217
+ return {"ok": False, "reasonCode": "ARCHIVE_RESULT_WRITE_FAILED", "error": str(exc)}
2218
+ payload["resultPath"] = str(path)
2219
+ return {"ok": True, "path": str(path)}
2220
+
2221
+
2222
+ def publication_member_paths(source_dir: Path) -> list[str]:
2223
+ """归档包会包含哪些文件——由 `_archive_core_file_specs` 单一定义,不另起一套。
2224
+
2225
+ 包成员在 execute 之前就已确定:`reports/final/summary-data.json`、`spec/**.md`、
2226
+ `plans/**.md`、`candidates/knowledge.json`、`meta/archive-meta.md`、
2227
+ `meta/change-context.json`。其中 summary-data.json / archive-meta.md 由 execute
2228
+ 生成,预检时尚不存在,会被自动跳过——它们是机器产物,承载不了人写的敏感串。
2229
+ """
2230
+ project_root = find_project_root(source_dir)
2231
+ try:
2232
+ _, _, specs = _archive_core_file_specs(project_root, source_dir)
2233
+ except (OSError, ValueError):
2234
+ return []
2235
+ return sorted(package_path for _, package_path, _, _ in specs)
2236
+
2237
+
2238
+ def precheck_publication_content(
2239
+ source_dir: Path,
2240
+ *,
2241
+ timeout_seconds: int = 60,
2242
+ ) -> dict[str, Any]:
2243
+ """用**发布同款规则**扫描**会被发布的那批文件**,在归档开始时就给出结论。
2244
+
2245
+ 此前这两件事都不成立:本地跑的是 harness_runtime 自建的赋值式正则,扫的是整棵
2246
+ change 树(含从不入包的 runtime/);服务端跑的是 packages/core 的
2247
+ `scanSensitiveFiles`,扫的是 ZIP 里那几个文件。范围与规则双双错位,于是本地在
2248
+ 草稿上误报阻塞,真正会被拒的设计文档一个字没查——直到 upload 收到 422。
2249
+
2250
+ 这里改为调用 CLI 的 `scan-sensitive`(直接复用 core 的导出),保证与服务端同源。
2251
+ CLI 不可用时降级为警告:预检本身不该变成新的卡点。
2252
+ """
2253
+ members = publication_member_paths(source_dir)
2254
+ if not members:
2255
+ return {
2256
+ "ok": True,
2257
+ "reasonCode": "PUBLICATION_CONTENT_SCAN_EMPTY",
2258
+ "blocked": False,
2259
+ "findings": [],
2260
+ "members": [],
2261
+ }
2262
+ project_root = find_project_root(source_dir)
2263
+ try:
2264
+ prefix = resolve_hunter_cli_command(project_root)
2265
+ except (FileNotFoundError, OSError) as exc:
2266
+ return _publication_scan_unavailable(members, str(exc))
2267
+ command = [*prefix, "scan-sensitive", "--root", str(source_dir), "--json"]
2268
+ for member in members:
2269
+ command.extend(["--file", member])
2270
+ try:
2271
+ completed = subprocess.run(
2272
+ command,
2273
+ capture_output=True,
2274
+ text=True,
2275
+ encoding="utf-8",
2276
+ errors="replace",
2277
+ timeout=timeout_seconds,
2278
+ check=False,
2279
+ )
2280
+ except (OSError, subprocess.SubprocessError) as exc:
2281
+ return _publication_scan_unavailable(members, str(exc))
2282
+ try:
2283
+ report = json.loads(completed.stdout or "{}")
2284
+ except json.JSONDecodeError as exc:
2285
+ return _publication_scan_unavailable(
2286
+ members, f"scan-sensitive output was not JSON: {exc}"
2287
+ )
2288
+ if not isinstance(report, dict) or "findings" not in report:
2289
+ return _publication_scan_unavailable(
2290
+ members, "scan-sensitive output missing findings"
2291
+ )
2292
+ findings = [item for item in report.get("findings") or [] if isinstance(item, dict)]
2293
+ blocked = [item for item in findings if item.get("disposition") == "blocked"]
2294
+ hard = [item for item in blocked if item.get("overridable") is False]
2295
+ return {
2296
+ "ok": True,
2297
+ "reasonCode": (
2298
+ "PUBLICATION_CONTENT_SCAN_FLAGGED" if blocked
2299
+ else "PUBLICATION_CONTENT_SCAN_CLEAN"
2300
+ ),
2301
+ "scannerVersion": report.get("scanner_version"),
2302
+ "members": members,
2303
+ "blocked": bool(blocked),
2304
+ "hardBlocked": bool(hard),
2305
+ "findings": findings,
2306
+ "nextAction": _publication_scan_next_action(blocked, hard),
2307
+ }
2308
+
2309
+
2310
+ def _publication_scan_unavailable(members: list[str], error: str) -> dict[str, Any]:
2311
+ return {
2312
+ "ok": False,
2313
+ "reasonCode": "PUBLICATION_CONTENT_SCAN_UNAVAILABLE",
2314
+ "blocked": False,
2315
+ "findings": [],
2316
+ "members": members,
2317
+ "error": error,
2318
+ "message": (
2319
+ "无法运行发布内容预检(hunter-harness CLI 不可用)。归档继续,但上传时"
2320
+ "仍可能被服务端以 422 拒收;装好 CLI 后可用 "
2321
+ "`hunter-harness scan-sensitive` 单独复查。"
2322
+ ),
2323
+ }
2324
+
2325
+
2326
+ def _publication_scan_next_action(
2327
+ blocked: list[dict[str, Any]], hard: list[dict[str, Any]]
2328
+ ) -> str:
2329
+ if not blocked:
2330
+ return "发布内容预检通过。"
2331
+ if hard:
2332
+ names = ", ".join(
2333
+ f"{item.get('path')}:{item.get('line')} {item.get('rule_id')}"
2334
+ for item in hard
2335
+ )
2336
+ return (
2337
+ f"以下命中不可豁免(high):{names}。必须真正脱敏后重新打包,"
2338
+ "不要用行内标注绕过。"
2339
+ )
2340
+ lines = "; ".join(
2341
+ str(item.get("recovery_action") or "") for item in blocked
2342
+ )
2343
+ return (
2344
+ "服务端会以同款规则拒收这些内容(HTTP 422)。若确属设计固有内容,"
2345
+ f"按下列写法加行内标注后重新打包:{lines}"
2346
+ )
2347
+
2348
+
2170
2349
  def validate_sensitive_evidence_publication_gate(
2171
2350
  change_dir: Path,
2172
2351
  *,
@@ -2194,7 +2373,11 @@ def validate_sensitive_evidence_publication_gate(
2194
2373
  "receiptPath": str(receipt_path),
2195
2374
  "unresolvedFailures": [],
2196
2375
  }
2197
- candidates = hruntime.sensitive_evidence_candidates(change_dir)
2376
+ # 只扫会被发布出去的字节。runtime/ 是各阶段草稿(review 的 diff、临时校验脚本、
2377
+ # 命令输出),从不进归档包——拿它挡发布,挡的是永远不会离开本机的内容。
2378
+ candidates = hruntime.sensitive_evidence_candidates(
2379
+ change_dir, exclude_dirs=hruntime.PUBLICATION_EXCLUDED_DIRS
2380
+ )
2198
2381
  if candidates:
2199
2382
  return _advisory_sensitive_gate(
2200
2383
  {
@@ -2265,8 +2448,17 @@ def validate_sensitive_evidence_publication_gate(
2265
2448
  issues.append({"code": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED", "count": len(unresolved)})
2266
2449
  elif unresolved not in (None, []):
2267
2450
  issues.append({"code": "SECRET_SCAN_UNRESOLVED_INVALID"})
2451
+ # 用收据自己声明的排除项复算,两侧口径才一致;写死一套常量会让旧收据
2452
+ # 在升级后集体报 digest 漂移。
2453
+ receipt_excluded = receipt.get("publicationExcludedDirs")
2454
+ if not isinstance(receipt_excluded, list) or any(
2455
+ not isinstance(item, str) for item in receipt_excluded
2456
+ ):
2457
+ receipt_excluded = []
2268
2458
  try:
2269
- actual_digest = hruntime.publishable_tree_digest(change_dir)
2459
+ actual_digest = hruntime.publishable_tree_digest(
2460
+ change_dir, exclude_dirs=tuple(receipt_excluded)
2461
+ )
2270
2462
  except OSError as exc:
2271
2463
  actual_digest = None
2272
2464
  issues.append({"code": "SECRET_SCAN_TREE_UNREADABLE", "error": str(exc)})
@@ -2407,6 +2599,24 @@ def check_status(
2407
2599
  }
2408
2600
  )
2409
2601
 
2602
+ # 发布内容预检:与服务端同源的规则,扫的是真正会进 ZIP 的那批文件。
2603
+ # 服务端策略不归本地裁决,所以只预警不阻断——但必须在归档开始时就说出来,
2604
+ # 而不是等跑完全流程由 upload 返回 422。
2605
+ content_scan = precheck_publication_content(change_dir)
2606
+ checks["publication_content_scan"] = content_scan
2607
+ if content_scan.get("blocked"):
2608
+ warnings.append({
2609
+ "code": "PUBLICATION_CONTENT_SCAN_FLAGGED",
2610
+ "message": str(content_scan.get("nextAction") or ""),
2611
+ })
2612
+ elif not content_scan.get("ok"):
2613
+ warnings.append({
2614
+ "code": str(
2615
+ content_scan.get("reasonCode") or "PUBLICATION_CONTENT_SCAN_UNAVAILABLE"
2616
+ ),
2617
+ "message": str(content_scan.get("message") or ""),
2618
+ })
2619
+
2410
2620
  sensitive_gate = validate_sensitive_evidence_publication_gate(
2411
2621
  change_dir,
2412
2622
  copy_root=change_dir,
@@ -7842,6 +8052,7 @@ def cmd_finalize(
7842
8052
  source_sensitive_refresh = hruntime.refresh_sensitive_evidence_scan_receipt(
7843
8053
  original_change_dir,
7844
8054
  persist=False,
8055
+ exclude_dirs=hruntime.PUBLICATION_EXCLUDED_DIRS,
7845
8056
  )
7846
8057
  payload["steps"]["sensitive_evidence_source_refresh"] = source_sensitive_refresh
7847
8058
  if not source_sensitive_refresh.get("ok"):
@@ -7981,7 +8192,8 @@ def cmd_finalize(
7981
8192
  "stateDir": str(split_state_dir),
7982
8193
  }
7983
8194
  staged_sensitive_refresh = hruntime.refresh_sensitive_evidence_scan_receipt(
7984
- work_dir
8195
+ work_dir,
8196
+ exclude_dirs=hruntime.PUBLICATION_EXCLUDED_DIRS,
7985
8197
  )
7986
8198
  payload["steps"][
7987
8199
  "sensitive_evidence_staging_refresh"
@@ -8742,6 +8954,12 @@ def cmd_finalize(
8742
8954
  payload["finalStatus"] = "OK"
8743
8955
  payload["warnings"] = warnings
8744
8956
  payload["summary_data"] = str(summary_path)
8957
+ # 结果落在归档里(移动之后的位置),调用方不必再靠 shell 重定向去接 stdout。
8958
+ persisted = persist_execute_result(archive_dir, payload)
8959
+ if not persisted.get("ok"):
8960
+ warnings.append(
8961
+ f"could not persist archive execute result: {persisted.get('error')}"
8962
+ )
8745
8963
  return 0, payload
8746
8964
 
8747
8965
 
@@ -10120,9 +10338,45 @@ def cmd_certify_local_cli(args: argparse.Namespace) -> int:
10120
10338
  return 0
10121
10339
 
10122
10340
 
10341
+ def _emit_with_optional_output(args: argparse.Namespace, payload: dict[str, Any]) -> int | None:
10342
+ """Write the result to --output, refusing a path the archive will delete."""
10343
+ raw = getattr(args, "output", None)
10344
+ if not raw:
10345
+ return None
10346
+ resolved = resolve_execute_result_path(resolve_path(args.change_dir), str(raw))
10347
+ if not resolved.get("ok"):
10348
+ emit_json(resolved)
10349
+ return 2
10350
+ try:
10351
+ target = Path(resolved["path"])
10352
+ target.parent.mkdir(parents=True, exist_ok=True)
10353
+ target.write_text(
10354
+ json.dumps(payload, ensure_ascii=False, indent=2, default=str) + "\n",
10355
+ encoding="utf-8",
10356
+ newline="\n",
10357
+ )
10358
+ except (OSError, TypeError, ValueError) as exc:
10359
+ emit_json({
10360
+ "ok": False,
10361
+ "reasonCode": "ARCHIVE_RESULT_WRITE_FAILED",
10362
+ "path": resolved["path"],
10363
+ "error": str(exc),
10364
+ })
10365
+ return 2
10366
+ payload["resultPath"] = resolved["path"]
10367
+ return None
10368
+
10369
+
10123
10370
  def cmd_finalize_cli(args: argparse.Namespace) -> int:
10124
10371
  change_dir = resolve_path(args.change_dir)
10125
10372
  archive_root = resolve_path(args.archive_root)
10373
+ # 先验路径再干活:跑完才发现输出落点会被移走,结果已经无法重建。
10374
+ raw_output = getattr(args, "output", None)
10375
+ if raw_output:
10376
+ resolved = resolve_execute_result_path(change_dir, str(raw_output))
10377
+ if not resolved.get("ok"):
10378
+ emit_json(resolved)
10379
+ return 2
10126
10380
  durable_root = (
10127
10381
  resolve_path(args.durable_root)
10128
10382
  if getattr(args, "durable_root", None)
@@ -10141,6 +10395,9 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
10141
10395
  closure_disposition=str(getattr(args, "closure", "completed")),
10142
10396
  closure_reason=str(getattr(args, "closure_reason", "")),
10143
10397
  )
10398
+ failure = _emit_with_optional_output(args, payload)
10399
+ if failure is not None:
10400
+ return failure
10144
10401
  emit_json(payload)
10145
10402
  return code
10146
10403
 
@@ -10151,6 +10408,14 @@ def cmd_execute_cli(args: argparse.Namespace) -> int:
10151
10408
  if getattr(args, "durable_root", None)
10152
10409
  else None
10153
10410
  )
10411
+ raw_output = getattr(args, "output", None)
10412
+ if raw_output:
10413
+ resolved = resolve_execute_result_path(
10414
+ resolve_path(args.change_dir), str(raw_output)
10415
+ )
10416
+ if not resolved.get("ok"):
10417
+ emit_json(resolved)
10418
+ return 2
10154
10419
  code, payload = execute_archive(
10155
10420
  resolve_path(args.change_dir),
10156
10421
  resolve_path(args.archive_root),
@@ -10164,6 +10429,9 @@ def cmd_execute_cli(args: argparse.Namespace) -> int:
10164
10429
  closure_disposition=str(getattr(args, "closure", "completed")),
10165
10430
  closure_reason=str(getattr(args, "closure_reason", "")),
10166
10431
  )
10432
+ failure = _emit_with_optional_output(args, payload)
10433
+ if failure is not None:
10434
+ return failure
10167
10435
  emit_json(payload)
10168
10436
  return code
10169
10437
 
@@ -10771,6 +11039,7 @@ def build_parser() -> argparse.ArgumentParser:
10771
11039
  default="release-candidate",
10772
11040
  help="release-candidate enforces candidate evidence; record-only archives facts without release eligibility",
10773
11041
  )
11042
+ p_fin.add_argument("--output", default=None, help="把完整结果另存到该路径。**不得**指向 --change-dir 内部:归档会把该目录整个移走,写在里面的文件必然丢失。归档成功后结果也会自动落在 <archiveDir>/meta/archive-execute-result.json。")
10774
11043
  p_fin.add_argument("--json", action="store_true", default=True)
10775
11044
  p_fin.set_defaults(func=cmd_finalize_cli)
10776
11045
 
@@ -10795,6 +11064,7 @@ def build_parser() -> argparse.ArgumentParser:
10795
11064
  choices=("release-candidate", "record-only"),
10796
11065
  default="release-candidate",
10797
11066
  )
11067
+ p_execute.add_argument("--output", default=None, help="把完整结果另存到该路径。**不得**指向 --change-dir 内部:归档会把该目录整个移走,写在里面的文件必然丢失。归档成功后结果也会自动落在 <archiveDir>/meta/archive-execute-result.json。")
10798
11068
  p_execute.add_argument("--json", action="store_true", default=True)
10799
11069
  p_execute.set_defaults(func=cmd_execute_cli)
10800
11070