@hunter-harness/workflow-harness 0.2.74 → 0.2.75

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 (146) 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 +26 -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 +2 -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 +2 -1
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +2 -1
  11. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -1
  12. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +2 -1
  14. package/harness/bundles/general/claude-code/scripts/harness_archive.py +67 -5
  15. package/harness/bundles/general/claude-code/scripts/harness_change.py +95 -0
  16. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +101 -1
  17. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  18. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +26 -1
  19. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  20. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  21. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  22. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -1
  23. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -1
  26. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +2 -1
  27. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -1
  28. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +2 -1
  30. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +67 -5
  31. package/harness/bundles/general/codebuddy/scripts/harness_change.py +95 -0
  32. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +101 -1
  33. package/harness/bundles/general/codex/.harness-build.json +1 -1
  34. package/harness/bundles/general/codex/harness-archive/SKILL.md +26 -1
  35. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  36. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  37. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  38. package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -1
  39. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  40. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  41. package/harness/bundles/general/codex/harness-review/SKILL.md +2 -1
  42. package/harness/bundles/general/codex/harness-run/SKILL.md +2 -1
  43. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -1
  44. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-test/SKILL.md +2 -1
  46. package/harness/bundles/general/codex/scripts/harness_archive.py +67 -5
  47. package/harness/bundles/general/codex/scripts/harness_change.py +95 -0
  48. package/harness/bundles/general/codex/scripts/harness_runtime.py +101 -1
  49. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  50. package/harness/bundles/general/cursor/harness-archive/SKILL.md +26 -1
  51. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  52. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  53. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  54. package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -1
  55. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  56. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  57. package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -1
  58. package/harness/bundles/general/cursor/harness-run/SKILL.md +2 -1
  59. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -1
  60. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  61. package/harness/bundles/general/cursor/harness-test/SKILL.md +2 -1
  62. package/harness/bundles/general/cursor/scripts/harness_archive.py +67 -5
  63. package/harness/bundles/general/cursor/scripts/harness_change.py +95 -0
  64. package/harness/bundles/general/cursor/scripts/harness_runtime.py +101 -1
  65. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  66. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +2 -1
  67. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +26 -1
  68. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  69. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  70. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  71. package/harness/bundles/java/claude-code/harness-package/SKILL.md +2 -1
  72. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -1
  73. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  74. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  75. package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -1
  76. package/harness/bundles/java/claude-code/harness-run/SKILL.md +2 -1
  77. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -1
  78. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  79. package/harness/bundles/java/claude-code/harness-test/SKILL.md +2 -1
  80. package/harness/bundles/java/claude-code/scripts/harness_archive.py +67 -5
  81. package/harness/bundles/java/claude-code/scripts/harness_change.py +95 -0
  82. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +101 -1
  83. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  84. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +2 -1
  85. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +26 -1
  86. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  87. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  88. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  89. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -1
  90. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -1
  91. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  92. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  93. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -1
  94. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +2 -1
  95. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -1
  96. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  97. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +2 -1
  98. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +67 -5
  99. package/harness/bundles/java/codebuddy/scripts/harness_change.py +95 -0
  100. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +101 -1
  101. package/harness/bundles/java/codex/.harness-build.json +1 -1
  102. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +2 -1
  103. package/harness/bundles/java/codex/harness-archive/SKILL.md +26 -1
  104. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  105. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  106. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  107. package/harness/bundles/java/codex/harness-package/SKILL.md +2 -1
  108. package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -1
  109. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  110. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  111. package/harness/bundles/java/codex/harness-review/SKILL.md +2 -1
  112. package/harness/bundles/java/codex/harness-run/SKILL.md +2 -1
  113. package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -1
  114. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  115. package/harness/bundles/java/codex/harness-test/SKILL.md +2 -1
  116. package/harness/bundles/java/codex/scripts/harness_archive.py +67 -5
  117. package/harness/bundles/java/codex/scripts/harness_change.py +95 -0
  118. package/harness/bundles/java/codex/scripts/harness_runtime.py +101 -1
  119. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  120. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +2 -1
  121. package/harness/bundles/java/cursor/harness-archive/SKILL.md +26 -1
  122. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  123. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  124. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  125. package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -1
  126. package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -1
  127. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  128. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  129. package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -1
  130. package/harness/bundles/java/cursor/harness-run/SKILL.md +2 -1
  131. package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -1
  132. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  133. package/harness/bundles/java/cursor/harness-test/SKILL.md +2 -1
  134. package/harness/bundles/java/cursor/scripts/harness_archive.py +67 -5
  135. package/harness/bundles/java/cursor/scripts/harness_change.py +95 -0
  136. package/harness/bundles/java/cursor/scripts/harness_runtime.py +101 -1
  137. package/harness/manifests/general/claude-code.json +17 -17
  138. package/harness/manifests/general/codebuddy.json +17 -17
  139. package/harness/manifests/general/codex.json +17 -17
  140. package/harness/manifests/general/cursor.json +17 -17
  141. package/harness/manifests/java/claude-code.json +19 -19
  142. package/harness/manifests/java/codebuddy.json +19 -19
  143. package/harness/manifests/java/codex.json +19 -19
  144. package/harness/manifests/java/cursor.json +19 -19
  145. package/hunter-workflow-family.json +3 -3
  146. package/package.json +1 -1
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "claude-code",
5
- "coreHash": "d0ad43fe132b6817"
5
+ "coreHash": "24b43afeb4dc4c52"
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=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-archive — 变更归档
26
26
 
27
27
  ## Purpose
@@ -56,6 +56,7 @@ disallowed-tools:
56
56
 
57
57
  ## 统一读取协议
58
58
 
59
+ 0. **脚本在 `<skills-root>/scripts/` 共享,不在每个 skill 子目录下** — 实际形态是 `.codebuddy/skills/scripts/harness_*.py`(`.claude`/`.cursor`/`.codex` 同理),**没有** `.../skills/harness-<phase>/scripts/`。plan、run/test、archive 三份执行日志里都先猜成后者、报 `No such file` 再靠 Search 找回来;照第一种写法直接用
59
60
  1. **`.harness/changes/<change-name>/` 是唯一真相源** — 所有输入从该目录读取,产物写入对应子目录
60
61
  2. **change-name 优先从 frontmatter 读取** — `plans/*-design.md`、`spec/*-design.md`、`plans/*-plan.md` 的 YAML `change-name`
61
62
  3. **frontmatter 缺失时兼容旧格式** — 从路径推断,标记 `🟡 legacy-plan`,不失败
@@ -143,6 +144,13 @@ disallowed-tools:
143
144
 
144
145
  `execute` 内部运行自动门禁。实际计划最后阶段已终态、边界快照存在且 `status.archivable=true` 时直接执行;未满足时返回阻断且不移动原目录。用户取消后不得重跑。
145
146
 
147
+ ### 二·A、两个常见阻断的正规出路(不要自己拼 python -c)
148
+
149
+ | 阻断 | 出路 |
150
+ |------|------|
151
+ | `SENSITIVE_EVIDENCE_UNQUARANTINED` | `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
+ | `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
+
146
154
  ### 三、文件移动只用内置工具或 PowerShell
147
155
 
148
156
  移动用 Read+Write+验证 或 PowerShell;**禁止 Bash mv/cp/rm**。移动失败时不删除原目录,报错退出让用户手动处理。
@@ -204,6 +212,23 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 su
204
212
  - `.harness/archive/YYYY-MM-DD-<change-name>/events.ndjson` — 结构化事件层(新流程推荐;旧 archive 可缺失)
205
213
  - `.harness/archive/YYYY-MM-DD-<change-name>/reports/final/summary-data.json` — 最终报告数据源
206
214
  - `.harness/archive/YYYY-MM-DD-<change-name>/evidence/archive-manifest-before.json` / `archive-manifest-after.json` — 归档前后 manifest/checksum
215
+ - `.harness/state/local/archive-packages/<change-name>.remote.json` — 远端上传回执(`archiveId`/`archiveStatus`/`knowledgeStatus`/`uploadStatus`/`fileCount`)
216
+
217
+ ### 结束报告必须回显两条上传结果 ⚠️
218
+
219
+ 归档会做**两次**独立上传,二者成败无关,必须分别如实回显:
220
+
221
+ | 上传 | 载什么 | 结果在哪 |
222
+ |------|--------|---------|
223
+ | `steps.archive_push` | 归档 ZIP(核心产物) | `archiveId`/`archiveStatus`/`uploadStatus`/`knowledgeStatus`,同时落盘 `state/local/archive-packages/<cn>.remote.json` |
224
+ | `steps.managed_snapshot_push` | **plan/spec 文档、`.harness/codebase` 架构地图、`.harness/rules`**(push 会 walk 每个归档目录的 `spec/` 与 `plans/`) | `ok`/`reasonCode`/`exitCode`/`cliCode`/`detail` |
225
+
226
+ ```
227
+ 远端归档:arc_3ff325bb…(archiveStatus=durable,uploadStatus=ready,knowledgeStatus=ready,8 文件)
228
+ 受管快照:❌ MANAGED_SNAPSHOT_UPLOAD_FAILED(exitCode=1,detail=…)→ plan/spec 与规则/架构地图未上平台,需 npx hunter-harness push --yes --non-interactive 重试
229
+ ```
230
+
231
+ **第二条尤其不能省**:它是 best-effort,失败不阻断归档,于是最容易被淹没成一句警告——而平台上"分支文件里没有 plan/spec"正是它失败的后果。成功时也要写明 `submitted` 数量;`ok=false` 时必须带上 `reasonCode` 与 `detail`,不得省略或写成"已完成"。
207
232
 
208
233
  ## 渐进披露
209
234
 
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; 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=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
11
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; 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=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
11
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; 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=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-plan — 需求规划
26
26
 
27
27
  ## Purpose
@@ -38,6 +38,7 @@ disallowed-tools:
38
38
 
39
39
  ## 统一读取协议
40
40
 
41
+ 0. **脚本在 `<skills-root>/scripts/` 共享,不在每个 skill 子目录下** — 实际形态是 `.codebuddy/skills/scripts/harness_*.py`(`.claude`/`.cursor`/`.codex` 同理),**没有** `.../skills/harness-<phase>/scripts/`。plan、run/test、archive 三份执行日志里都先猜成后者、报 `No such file` 再靠 Search 找回来;照第一种写法直接用
41
42
  1. **`.harness/changes/<change-name>/` 是唯一真相源** — 所有输入从该目录读取,产物写入对应子目录
42
43
  2. **change-name 优先从 frontmatter 读取** — `plans/*-design.md`、`spec/*-design.md`、`plans/*-plan.md` 的 YAML `change-name`
43
44
  3. **frontmatter 缺失时兼容旧格式** — 从路径推断,标记 `🟡 legacy-plan`,不失败
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; 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=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; 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=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; overlay=none; agent=claude-code; do not edit -->
26
26
  # harness-review — 代码审查
27
27
 
28
28
  ## Purpose
@@ -48,6 +48,7 @@ disallowed-tools:
48
48
 
49
49
  ## 统一读取协议
50
50
 
51
+ 0. **脚本在 `<skills-root>/scripts/` 共享,不在每个 skill 子目录下** — 实际形态是 `.codebuddy/skills/scripts/harness_*.py`(`.claude`/`.cursor`/`.codex` 同理),**没有** `.../skills/harness-<phase>/scripts/`。plan、run/test、archive 三份执行日志里都先猜成后者、报 `No such file` 再靠 Search 找回来;照第一种写法直接用
51
52
  1. **`.harness/changes/<change-name>/` 是唯一真相源** — 所有输入从该目录读取,产物写入对应子目录
52
53
  2. **change-name 优先从 frontmatter 读取** — `plans/*-design.md`、`spec/*-design.md`、`plans/*-plan.md` 的 YAML `change-name`
53
54
  3. **frontmatter 缺失时兼容旧格式** — 从路径推断,标记 `🟡 legacy-plan`,不失败
@@ -22,7 +22,7 @@ disallowed-tools:
22
22
  - Bash(curl *)
23
23
  - Bash(codegraph *)
24
24
  ---
25
- <!-- generated by harness_deploy.py; core=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; overlay=none; agent=claude-code; do not edit -->
26
26
  # harness-run — 需求编码
27
27
 
28
28
  ## Purpose
@@ -59,6 +59,7 @@ disallowed-tools:
59
59
 
60
60
  ## 统一读取协议
61
61
 
62
+ 0. **脚本在 `<skills-root>/scripts/` 共享,不在每个 skill 子目录下** — 实际形态是 `.codebuddy/skills/scripts/harness_*.py`(`.claude`/`.cursor`/`.codex` 同理),**没有** `.../skills/harness-<phase>/scripts/`。plan、run/test、archive 三份执行日志里都先猜成后者、报 `No such file` 再靠 Search 找回来;照第一种写法直接用
62
63
  1. **`.harness/changes/<change-name>/` 是唯一真相源** — 所有输入从该目录读取,产物写入对应子目录
63
64
  2. **change-name 优先从 frontmatter 读取** — `plans/*-design.md`、`spec/*-design.md`、`plans/*-plan.md` 的 YAML `change-name`
64
65
  3. **frontmatter 缺失时兼容旧格式** — 从路径推断,标记 `🟡 legacy-plan`,不失败
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-submit — 最终提交(含 worktree 合并)
26
26
 
27
27
  ## Purpose
@@ -47,6 +47,7 @@ disallowed-tools:
47
47
 
48
48
  ## 统一读取协议
49
49
 
50
+ 0. **脚本在 `<skills-root>/scripts/` 共享,不在每个 skill 子目录下** — 实际形态是 `.codebuddy/skills/scripts/harness_*.py`(`.claude`/`.cursor`/`.codex` 同理),**没有** `.../skills/harness-<phase>/scripts/`。plan、run/test、archive 三份执行日志里都先猜成后者、报 `No such file` 再靠 Search 找回来;照第一种写法直接用
50
51
  1. **`.harness/changes/<change-name>/` 是唯一真相源** — 所有输入从该目录读取,产物写入对应子目录
51
52
  2. **change-name 优先从 frontmatter 读取** — `plans/*-design.md`、`spec/*-design.md`、`plans/*-plan.md` 的 YAML `change-name`
52
53
  3. **frontmatter 缺失时兼容旧格式** — 从路径推断,标记 `🟡 legacy-plan`,不失败
@@ -19,7 +19,7 @@ disallowed-tools:
19
19
  - Bash(awk *)
20
20
  - Bash(curl *)
21
21
  ---
22
- <!-- generated by harness_deploy.py; core=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
22
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; 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=d0ad43fe132b6817; overlay=none; agent=claude-code; do not edit -->
26
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; overlay=none; agent=claude-code; do not edit -->
27
27
  # harness-test — 测试执行
28
28
 
29
29
  ## Purpose
@@ -51,6 +51,7 @@ disallowed-tools:
51
51
 
52
52
  ## 统一读取协议
53
53
 
54
+ 0. **脚本在 `<skills-root>/scripts/` 共享,不在每个 skill 子目录下** — 实际形态是 `.codebuddy/skills/scripts/harness_*.py`(`.claude`/`.cursor`/`.codex` 同理),**没有** `.../skills/harness-<phase>/scripts/`。plan、run/test、archive 三份执行日志里都先猜成后者、报 `No such file` 再靠 Search 找回来;照第一种写法直接用
54
55
  1. **`.harness/changes/<change-name>/` 是唯一真相源** — 所有输入从该目录读取,产物写入对应子目录
55
56
  2. **change-name 优先从 frontmatter 读取** — `plans/*-design.md`、`spec/*-design.md`、`plans/*-plan.md` 的 YAML `change-name`
56
57
  3. **frontmatter 缺失时兼容旧格式** — 从路径推断,标记 `🟡 legacy-plan`,不失败
@@ -8552,8 +8552,17 @@ def cmd_finalize(
8552
8552
  payload["knowledgeMaintenance"] = _knowledge_maintenance_from_archive_push(
8553
8553
  push_result
8554
8554
  )
8555
- if push_result.get("archiveStatus") == "durable":
8556
- payload["archiveDurability"] = "ARCHIVED_REMOTE_DURABLE"
8555
+ remote_durability = remote_durable_archive_durability(
8556
+ payload.get("archiveDurability"), push_result
8557
+ )
8558
+ if remote_durability is not None:
8559
+ payload["archiveDurability"] = remote_durability
8560
+ # 报告必须跟着改口:上传发生在 summary 落盘之后,不回写就会出现
8561
+ # "回执 durable / 报告 local-only" 的自相矛盾
8562
+ if not persist_archive_durability(summary_path, remote_durability):
8563
+ warnings.append(
8564
+ "远端归档已持久化,但 summary-data.json 的 archiveDurability 回写失败"
8565
+ )
8557
8566
  if push_result.get("warning"):
8558
8567
  warnings.append(str(push_result["warning"]))
8559
8568
 
@@ -9121,6 +9130,50 @@ def build_archive_package(
9121
9130
  }
9122
9131
 
9123
9132
 
9133
+ def remote_durable_archive_durability(
9134
+ current: Any, push_result: Mapping[str, Any]
9135
+ ) -> dict[str, Any] | None:
9136
+ """远端上传成功后的耐久性投影;未 durable 时返回 None。
9137
+
9138
+ summary-data.json 在归档流程早期就以 ARCHIVED_LOCAL_ONLY 写盘,而 ZIP 上传在
9139
+ 那之后才发生。此前上传成功只改内存里的 payload、还把对象覆盖成裸字符串,落盘
9140
+ 报告从没跟着改——于是同一次归档,回执说 durable、报告说 local-only,平台和用户
9141
+ 读到的是后者。这里保持对象形状并带上回执标识,供调用方同时更新 payload 与文件。
9142
+ """
9143
+ if str(push_result.get("archiveStatus") or "") != "durable":
9144
+ return None
9145
+ projected = dict(current) if isinstance(current, dict) else {}
9146
+ projected.update({
9147
+ "status": "ARCHIVED_REMOTE_DURABLE",
9148
+ # 已远端持久化:不再声称"只存在于本地、可能随工作区丢失"
9149
+ "risk": None,
9150
+ "archiveId": push_result.get("archiveId"),
9151
+ "uploadStatus": push_result.get("uploadStatus"),
9152
+ "knowledgeStatus": push_result.get("knowledgeStatus"),
9153
+ })
9154
+ return projected
9155
+
9156
+
9157
+ def persist_archive_durability(summary_path: Path, durability: dict[str, Any]) -> bool:
9158
+ """把耐久性结论回写到已落盘的 summary,其余字段原样保留。
9159
+
9160
+ summary-data.json 本就被排除在归档校验和覆盖之外(它在覆盖快照之后才写),
9161
+ 所以这次回写不会破坏 manifest 对账。
9162
+ """
9163
+ try:
9164
+ summary = json.loads(Path(summary_path).read_text(encoding="utf-8-sig"))
9165
+ except (OSError, json.JSONDecodeError):
9166
+ return False
9167
+ if not isinstance(summary, dict):
9168
+ return False
9169
+ summary["archiveDurability"] = durability
9170
+ try:
9171
+ write_json(Path(summary_path), summary)
9172
+ except OSError:
9173
+ return False
9174
+ return True
9175
+
9176
+
9124
9177
  def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
9125
9178
  """Best-effort upload of rules and Codebase Map through the normal push protocol."""
9126
9179
  project_root = project_root.resolve()
@@ -9176,16 +9229,25 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
9176
9229
  except (json.JSONDecodeError, TypeError):
9177
9230
  output = None
9178
9231
  if completed.returncode != 0 or not isinstance(output, dict):
9179
- return {
9232
+ # 失败时丢掉 stdout/stderr 等于把线索一起丢了:plan/spec 与规则、架构地图
9233
+ # 全靠这条推送上平台,用户在平台上只会看到"东西没上去",却无从查为什么。
9234
+ # 保留有界的诊断片段(push 本身拒绝上传 secret,这里再截断兜底)。
9235
+ detail = (completed.stderr or completed.stdout or "").strip()
9236
+ failure: dict[str, Any] = {
9180
9237
  "ok": False,
9181
9238
  "skipped": False,
9182
9239
  "exitCode": completed.returncode,
9183
9240
  "reasonCode": "MANAGED_SNAPSHOT_UPLOAD_FAILED",
9241
+ "detail": detail[-2_048:],
9184
9242
  "warning": (
9185
- "项目规则与架构地图未能同步到平台;归档不受影响,"
9186
- "稍后可运行 npx hunter-harness push --yes --non-interactive 重试。"
9243
+ "项目规则与架构地图未能同步到平台(plan/spec 也随这条推送上传);"
9244
+ "归档不受影响,稍后可运行 npx hunter-harness push --yes --non-interactive 重试。"
9187
9245
  ),
9188
9246
  }
9247
+ cli_code = output.get("code") or output.get("reason_code") if isinstance(output, dict) else None
9248
+ if isinstance(cli_code, str) and cli_code:
9249
+ failure["cliCode"] = cli_code
9250
+ return failure
9189
9251
  summary = output.get("summary")
9190
9252
  submitted = summary.get("submitted", 0) if isinstance(summary, dict) else 0
9191
9253
  return {
@@ -1081,6 +1081,90 @@ def cmd_resolve(args: argparse.Namespace) -> int:
1081
1081
  )
1082
1082
 
1083
1083
 
1084
+ def declare_product_ownership(
1085
+ project_root: Path, change_id: str, *, product_paths: list[str]
1086
+ ) -> dict[str, Any]:
1087
+ """把 `ownership.productPaths` 写进 change 契约。
1088
+
1089
+ 这个字段此前没有任何写入方:plan 的 validate_product_ownership 只校验、缺失时
1090
+ 软放行,而归档的 compute_ownership_diff 会把全部改动判成 foreignPaths,
1091
+ filesChanged=0 直接触发 DIFF_ZERO_WITH_NONEMPTY_COMMIT。两端口径不一致,中间
1092
+ 没工具能补——只能手改契约。这里给出正规入口。
1093
+
1094
+ 规则与 plan 校验一致:只收精确文件或目录前缀,不接受通配。
1095
+ """
1096
+ resolved = resolve_change(project_root, change_id)
1097
+ if not resolved.get("ok"):
1098
+ return resolved
1099
+ change_dir = Path(resolved["changeDir"])
1100
+ normalized = sorted({
1101
+ str(item).strip().replace("\\", "/").removeprefix("./")
1102
+ for item in product_paths
1103
+ if isinstance(item, str) and item.strip()
1104
+ })
1105
+ if not normalized:
1106
+ return {
1107
+ "ok": False,
1108
+ "code": "PLAN_PRODUCT_PATHS_REQUIRED",
1109
+ "message": "至少声明一条 productPaths(精确文件或目录前缀)",
1110
+ }
1111
+ unsupported = sorted(p for p in normalized if any(ch in p for ch in "*?[]"))
1112
+ if unsupported:
1113
+ return {
1114
+ "ok": False,
1115
+ "code": "PLAN_PRODUCT_PATHS_GLOB_UNSUPPORTED",
1116
+ "message": "productPaths 只接受精确文件或目录前缀,不支持通配:" + ", ".join(unsupported),
1117
+ "unsupportedPaths": unsupported,
1118
+ }
1119
+
1120
+ context_path = change_dir / CHANGE_CONTEXT_REL
1121
+ try:
1122
+ context = _read_json(context_path)
1123
+ except (OSError, json.JSONDecodeError) as exc:
1124
+ return {"ok": False, "code": "CHANGE_CONTEXT_INVALID", "message": str(exc)}
1125
+ if not isinstance(context, dict):
1126
+ return {"ok": False, "code": "CHANGE_CONTEXT_INVALID", "message": "change-context.json 不是对象"}
1127
+
1128
+ ownership = context.get("ownership")
1129
+ ownership = dict(ownership) if isinstance(ownership, dict) else {}
1130
+ if ownership.get("productPaths") == normalized:
1131
+ return {
1132
+ "ok": True,
1133
+ "code": "PRODUCT_OWNERSHIP_DECLARED",
1134
+ "idempotent": True,
1135
+ "changeId": change_id,
1136
+ "productPaths": normalized,
1137
+ "path": str(context_path),
1138
+ }
1139
+ ownership["productPaths"] = normalized
1140
+ context["ownership"] = ownership
1141
+ _write_json(context_path, context)
1142
+ return {
1143
+ "ok": True,
1144
+ "code": "PRODUCT_OWNERSHIP_DECLARED",
1145
+ "idempotent": False,
1146
+ "changeId": change_id,
1147
+ "productPaths": normalized,
1148
+ "path": str(context_path),
1149
+ }
1150
+
1151
+
1152
+ def cmd_declare_ownership(args: argparse.Namespace) -> int:
1153
+ project = resolve_main_project_root()
1154
+ payload = declare_product_ownership(
1155
+ project, args.change, product_paths=list(args.product_path)
1156
+ )
1157
+ if payload.get("ok"):
1158
+ emit(payload, as_json=bool(args.json))
1159
+ return 0
1160
+ return emit_error(
1161
+ str(payload.get("code", "PRODUCT_OWNERSHIP_FAILED")),
1162
+ str(payload.get("message", "declare ownership failed")),
1163
+ as_json=bool(args.json),
1164
+ extra={k: v for k, v in payload.items() if k not in {"ok", "message"}},
1165
+ )
1166
+
1167
+
1084
1168
  def cmd_migrate(args: argparse.Namespace) -> int:
1085
1169
  project = resolve_main_project_root()
1086
1170
  if not args.change:
@@ -1451,6 +1535,17 @@ def build_parser() -> argparse.ArgumentParser:
1451
1535
  p_migrate.add_argument("--change", required=True)
1452
1536
  p_migrate.set_defaults(func=cmd_migrate)
1453
1537
 
1538
+ p_ownership = sub.add_parser(
1539
+ "declare-ownership", parents=[shared],
1540
+ help="declare ownership.productPaths so archive can project the real diff",
1541
+ )
1542
+ p_ownership.add_argument("--change", required=True)
1543
+ p_ownership.add_argument(
1544
+ "--product-path", required=True, action="append",
1545
+ help="exact file or directory prefix (no globs); repeatable",
1546
+ )
1547
+ p_ownership.set_defaults(func=cmd_declare_ownership)
1548
+
1454
1549
  p_claim = sub.add_parser("claim", parents=[shared])
1455
1550
  p_claim.add_argument("--change", required=True)
1456
1551
  p_claim.add_argument("--phase", required=True)
@@ -157,6 +157,35 @@ def _private_evidence_root(explicit: Path | None = None) -> Path:
157
157
  Path.home() / ".harness" / "private-evidence").expanduser().resolve()
158
158
 
159
159
 
160
+ def private_evidence_root_for(source: Path, explicit: Path | None = None) -> Path:
161
+ """私有隔离根:必须与源同一驱动器。
162
+
163
+ `os.replace` 跨驱动器会直接失败(Windows 上是 WinError 17)。默认根
164
+ `~/.harness/private-evidence` 在 Windows 落 C 盘,项目在别的盘时隔离必然失败
165
+ ——而隔离是归档的硬前置,等于整条归档路被堵死。同盘时沿用配置/默认值;不同盘
166
+ 时退到源所在驱动器根下的 `.harness-private-evidence`(天然在任何项目根之外)。
167
+ POSIX 上 drive 恒为空串,行为不变。
168
+ """
169
+ source = source.expanduser().resolve()
170
+ preferred = _private_evidence_root(explicit)
171
+ if preferred.drive.lower() == source.drive.lower():
172
+ return preferred
173
+ return (Path(source.anchor) / ".harness-private-evidence").resolve()
174
+
175
+
176
+ def _infer_project_root(change_root: Path) -> Path | None:
177
+ """change 目录形如 `<project>/.harness/changes/<name>`:取路径里 `.harness` 段的父目录。
178
+
179
+ 不做"向上找存在 .harness 的目录"式搜索——用户 HOME 下通常也有 `.harness`,
180
+ 那样会把 HOME 误判成项目根,把合法的临时隔离位置一并拒掉。
181
+ """
182
+ parts = change_root.parts
183
+ for index in range(len(parts) - 1, 0, -1):
184
+ if parts[index] == ".harness":
185
+ return Path(*parts[:index])
186
+ return None
187
+
188
+
160
189
  def _secure_private_path(path: Path, *, directory: bool) -> str:
161
190
  """Best-effort owner-only permissions, with an explicit ACL result on Windows."""
162
191
  mode = 0o700 if directory else 0o600
@@ -288,11 +317,12 @@ def quarantine_sensitive_evidence(
288
317
  change_root: Path,
289
318
  reason: str = "sensitive evidence",
290
319
  private_root: Path | None = None,
320
+ project_root: Path | None = None,
291
321
  ) -> dict[str, Any]:
292
322
  """Atomically move legacy plaintext evidence outside any publishable tree."""
293
323
  source = source.expanduser().resolve()
294
324
  change_root = change_root.expanduser().resolve()
295
- private = _private_evidence_root(private_root)
325
+ private = private_evidence_root_for(source, private_root)
296
326
  if not source.is_file():
297
327
  return {"ok": False, "reasonCode": "SENSITIVE_EVIDENCE_SOURCE_MISSING", "sourcePath": str(source)}
298
328
  if private == change_root or private.is_relative_to(change_root):
@@ -301,6 +331,25 @@ def quarantine_sensitive_evidence(
301
331
  "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
302
332
  "error": "private quarantine root must be outside the publishable change root",
303
333
  }
334
+ # 归档的密钥扫描门禁按**项目根**判定(SECRET_SCAN_PRIVATE_PATH_IN_COPY_ROOT)。
335
+ # 这里此前只看 change_root,比门禁宽——于是隔离"成功"了,门禁再拒一次,调用方
336
+ # 得多试一轮才找到合法位置。对齐到同一条边界,错就错在当场。
337
+ resolved_project = (
338
+ project_root.expanduser().resolve()
339
+ if project_root is not None
340
+ else _infer_project_root(change_root)
341
+ )
342
+ if resolved_project is not None and (
343
+ private == resolved_project or private.is_relative_to(resolved_project)
344
+ ):
345
+ return {
346
+ "ok": False,
347
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
348
+ "error": (
349
+ "private quarantine root must be outside the project root "
350
+ f"({resolved_project}); archive's secret scan rejects paths inside it"
351
+ ),
352
+ }
304
353
  try:
305
354
  raw_digest = "sha256:" + hashlib.sha256(source.read_bytes()).hexdigest()
306
355
  quarantine_dir = private / uuid.uuid4().hex
@@ -1850,6 +1899,37 @@ def doctor(project: Path, change_dir: Path, *, agent: str) -> dict[str, Any]:
1850
1899
  return {"ok": True, "action": "doctor", **payload}
1851
1900
 
1852
1901
 
1902
+ def cmd_quarantine_evidence(args: argparse.Namespace) -> dict[str, Any]:
1903
+ """隔离命令:把明文敏感证据原子移出可发布树,逐条返回结果。
1904
+
1905
+ 单条失败不吞掉其余条目——归档阻断项常常一次报多个文件,逐条报告才能一轮修完。
1906
+ """
1907
+ project = Path(args.project).expanduser().resolve()
1908
+ change_root = Path(args.change_dir).expanduser().resolve()
1909
+ private_root = Path(args.private_root) if args.private_root else None
1910
+ entries: list[dict[str, Any]] = []
1911
+ for raw in args.file:
1912
+ candidate = Path(raw)
1913
+ source = candidate if candidate.is_absolute() else change_root / candidate
1914
+ outcome = quarantine_sensitive_evidence(
1915
+ source,
1916
+ change_root=change_root,
1917
+ reason=args.reason,
1918
+ private_root=private_root,
1919
+ project_root=project,
1920
+ )
1921
+ entries.append({"file": raw, **outcome})
1922
+ failed = [item for item in entries if not item.get("ok")]
1923
+ return {
1924
+ "ok": not failed,
1925
+ "action": "quarantine-evidence",
1926
+ "code": "SENSITIVE_EVIDENCE_QUARANTINED" if not failed else "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
1927
+ "quarantined": len(entries) - len(failed),
1928
+ "failed": len(failed),
1929
+ "entries": entries,
1930
+ }
1931
+
1932
+
1853
1933
  def build_parser() -> argparse.ArgumentParser:
1854
1934
  parser = argparse.ArgumentParser(prog="harness_runtime.py")
1855
1935
  sub = parser.add_subparsers(dest="command", required=True)
@@ -1858,6 +1938,24 @@ def build_parser() -> argparse.ArgumentParser:
1858
1938
  p_doctor.add_argument("--change-dir", required=True)
1859
1939
  p_doctor.add_argument("--agent", choices=sorted(_ADAPTERS), required=True)
1860
1940
  p_doctor.add_argument("--json", action="store_true")
1941
+ # 归档的 SENSITIVE_EVIDENCE_UNQUARANTINED 是硬阻断,此前却没有任何命令行入口
1942
+ # ——调用方只能写 python -c + sys.path hack 去调内部函数。
1943
+ p_quarantine = sub.add_parser(
1944
+ "quarantine-evidence",
1945
+ help="move plaintext sensitive evidence outside the publishable tree",
1946
+ )
1947
+ p_quarantine.add_argument("--project", required=True)
1948
+ p_quarantine.add_argument("--change-dir", required=True)
1949
+ p_quarantine.add_argument(
1950
+ "--file", required=True, action="append",
1951
+ help="path to quarantine, relative to --change-dir or absolute; repeatable",
1952
+ )
1953
+ p_quarantine.add_argument("--reason", default="sensitive evidence")
1954
+ p_quarantine.add_argument(
1955
+ "--private-root", default=None,
1956
+ help="override the private root; must be outside the project root and on its drive",
1957
+ )
1958
+ p_quarantine.add_argument("--json", action="store_true")
1861
1959
  p_adapter = sub.add_parser("adapter")
1862
1960
  p_adapter.add_argument("--agent", choices=sorted(_ADAPTERS), required=True)
1863
1961
  p_adapter.add_argument("--change", required=True)
@@ -1899,6 +1997,8 @@ def main(argv: list[str] | None = None) -> int:
1899
1997
  try:
1900
1998
  if args.command == "doctor":
1901
1999
  result = doctor(Path(args.project), Path(args.change_dir), agent=args.agent)
2000
+ elif args.command == "quarantine-evidence":
2001
+ result = cmd_quarantine_evidence(args)
1902
2002
  elif args.command == "adapter":
1903
2003
  result = {"ok": True, "action": "adapter", **adapter_worktree(args.agent, args.change)}
1904
2004
  elif args.command == "_worker":
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "codebuddy",
5
- "coreHash": "d0ad43fe132b6817"
5
+ "coreHash": "24b43afeb4dc4c52"
6
6
  }
@@ -3,7 +3,7 @@ name: harness-archive
3
3
  description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到 .harness/archive/,含归档元数据。仅当用户显式调用
4
4
  /harness-archive 时使用;不得被其他阶段 skill 自动接续触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d0ad43fe132b6817; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -38,6 +38,7 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
38
38
 
39
39
  ## 统一读取协议
40
40
 
41
+ 0. **脚本在 `<skills-root>/scripts/` 共享,不在每个 skill 子目录下** — 实际形态是 `.codebuddy/skills/scripts/harness_*.py`(`.claude`/`.cursor`/`.codex` 同理),**没有** `.../skills/harness-<phase>/scripts/`。plan、run/test、archive 三份执行日志里都先猜成后者、报 `No such file` 再靠 Search 找回来;照第一种写法直接用
41
42
  1. **`.harness/changes/<change-name>/` 是唯一真相源** — 所有输入从该目录读取,产物写入对应子目录
42
43
  2. **change-name 优先从 frontmatter 读取** — `plans/*-design.md`、`spec/*-design.md`、`plans/*-plan.md` 的 YAML `change-name`
43
44
  3. **frontmatter 缺失时兼容旧格式** — 从路径推断,标记 `🟡 legacy-plan`,不失败
@@ -125,6 +126,13 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
125
126
 
126
127
  `execute` 内部运行自动门禁。实际计划最后阶段已终态、边界快照存在且 `status.archivable=true` 时直接执行;未满足时返回阻断且不移动原目录。用户取消后不得重跑。
127
128
 
129
+ ### 二·A、两个常见阻断的正规出路(不要自己拼 python -c)
130
+
131
+ | 阻断 | 出路 |
132
+ |------|------|
133
+ | `SENSITIVE_EVIDENCE_UNQUARANTINED` | `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` 拒绝 |
134
+ | `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` |
135
+
128
136
  ### 三、文件移动只用内置工具或 PowerShell
129
137
 
130
138
  移动用 Read+Write+验证 或 PowerShell;**禁止 Bash mv/cp/rm**。移动失败时不删除原目录,报错退出让用户手动处理。
@@ -186,6 +194,23 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 su
186
194
  - `.harness/archive/YYYY-MM-DD-<change-name>/events.ndjson` — 结构化事件层(新流程推荐;旧 archive 可缺失)
187
195
  - `.harness/archive/YYYY-MM-DD-<change-name>/reports/final/summary-data.json` — 最终报告数据源
188
196
  - `.harness/archive/YYYY-MM-DD-<change-name>/evidence/archive-manifest-before.json` / `archive-manifest-after.json` — 归档前后 manifest/checksum
197
+ - `.harness/state/local/archive-packages/<change-name>.remote.json` — 远端上传回执(`archiveId`/`archiveStatus`/`knowledgeStatus`/`uploadStatus`/`fileCount`)
198
+
199
+ ### 结束报告必须回显两条上传结果 ⚠️
200
+
201
+ 归档会做**两次**独立上传,二者成败无关,必须分别如实回显:
202
+
203
+ | 上传 | 载什么 | 结果在哪 |
204
+ |------|--------|---------|
205
+ | `steps.archive_push` | 归档 ZIP(核心产物) | `archiveId`/`archiveStatus`/`uploadStatus`/`knowledgeStatus`,同时落盘 `state/local/archive-packages/<cn>.remote.json` |
206
+ | `steps.managed_snapshot_push` | **plan/spec 文档、`.harness/codebase` 架构地图、`.harness/rules`**(push 会 walk 每个归档目录的 `spec/` 与 `plans/`) | `ok`/`reasonCode`/`exitCode`/`cliCode`/`detail` |
207
+
208
+ ```
209
+ 远端归档:arc_3ff325bb…(archiveStatus=durable,uploadStatus=ready,knowledgeStatus=ready,8 文件)
210
+ 受管快照:❌ MANAGED_SNAPSHOT_UPLOAD_FAILED(exitCode=1,detail=…)→ plan/spec 与规则/架构地图未上平台,需 npx hunter-harness push --yes --non-interactive 重试
211
+ ```
212
+
213
+ **第二条尤其不能省**:它是 best-effort,失败不阻断归档,于是最容易被淹没成一句警告——而平台上"分支文件里没有 plan/spec"正是它失败的后果。成功时也要写明 `submitted` 数量;`ok=false` 时必须带上 `reasonCode` 与 `detail`,不得省略或写成"已完成"。
189
214
 
190
215
  ## 渐进披露
191
216
 
@@ -3,7 +3,7 @@ name: harness-codebase-map
3
3
  description: 分析现有代码库并生成 .harness/codebase/map/
4
4
  下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d0ad43fe132b6817; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=24b43afeb4dc4c52; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-codebase-map — 代码库地图
8
8
 
9
9
  ## Purpose