@hunter-harness/workflow-harness 0.2.73 → 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.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +26 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +2 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +3 -2
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +4 -2
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -2
- package/harness/bundles/general/claude-code/harness-test/checklist.md +2 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +149 -6
- package/harness/bundles/general/claude-code/scripts/harness_change.py +95 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +25 -4
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +45 -0
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +101 -1
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +26 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +3 -2
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +4 -2
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -2
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +2 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +149 -6
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +95 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +25 -4
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +45 -0
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +101 -1
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +26 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +3 -2
- package/harness/bundles/general/codex/harness-submit/SKILL.md +4 -2
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +3 -2
- package/harness/bundles/general/codex/harness-test/checklist.md +2 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +149 -6
- package/harness/bundles/general/codex/scripts/harness_change.py +95 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +25 -4
- package/harness/bundles/general/codex/scripts/harness_profile.py +45 -0
- package/harness/bundles/general/codex/scripts/harness_runtime.py +101 -1
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +26 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +3 -2
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +4 -2
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -2
- package/harness/bundles/general/cursor/harness-test/checklist.md +2 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +149 -6
- package/harness/bundles/general/cursor/scripts/harness_change.py +95 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +25 -4
- package/harness/bundles/general/cursor/scripts/harness_profile.py +45 -0
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +101 -1
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +26 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +3 -2
- package/harness/bundles/java/claude-code/harness-run/reference.md +348 -348
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +4 -2
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -2
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +149 -6
- package/harness/bundles/java/claude-code/scripts/harness_change.py +95 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +25 -4
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +45 -0
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +101 -1
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +26 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +3 -2
- package/harness/bundles/java/codebuddy/harness-run/reference.md +348 -348
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +4 -2
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -2
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +149 -6
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +95 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +25 -4
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +45 -0
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +101 -1
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +26 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +3 -2
- package/harness/bundles/java/codex/harness-run/reference.md +348 -348
- package/harness/bundles/java/codex/harness-submit/SKILL.md +4 -2
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +3 -2
- package/harness/bundles/java/codex/scripts/harness_archive.py +149 -6
- package/harness/bundles/java/codex/scripts/harness_change.py +95 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +25 -4
- package/harness/bundles/java/codex/scripts/harness_profile.py +45 -0
- package/harness/bundles/java/codex/scripts/harness_runtime.py +101 -1
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +26 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +3 -2
- package/harness/bundles/java/cursor/harness-run/reference.md +348 -348
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +4 -2
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -2
- package/harness/bundles/java/cursor/scripts/harness_archive.py +149 -6
- package/harness/bundles/java/cursor/scripts/harness_change.py +95 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +25 -4
- package/harness/bundles/java/cursor/scripts/harness_profile.py +45 -0
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +101 -1
- package/harness/manifests/general/claude-code.json +20 -20
- package/harness/manifests/general/codebuddy.json +20 -20
- package/harness/manifests/general/codex.json +20 -20
- package/harness/manifests/general/cursor.json +20 -20
- package/harness/manifests/java/claude-code.json +22 -22
- package/harness/manifests/java/codebuddy.json +22 -22
- package/harness/manifests/java/codex.json +22 -22
- package/harness/manifests/java/cursor.json +22 -22
- package/hunter-workflow-family.json +3 -3
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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`,不失败
|
|
@@ -81,7 +82,7 @@ disallowed-tools:
|
|
|
81
82
|
0. 加载上下文:普通 Run 先 `harness_context.py prepare --project . --change <id> --phase run --executor <tool> --json`(`--project` 必填,漏了直接 argparse 报错),再 **`harness_context.py begin --project . --change <id> --phase run --executor <tool> --json`** 校验交接,最后运行 **`harness_gate.py begin --phase run --change <id>`**。<br>**交接凭证不必手工补**:v2 计划的 `plan finalize` 不写 context 事务,`prepare` 会在检测到 committed 的 `meta/publication-journals/*.json` 时自动补录 `plan → run` 凭证(凭证带 `bootstrapSource=plan_publication_journal` 留痕)。仍报 `HANDOFF_REQUIRED`/`LEGACY_BOOTSTRAP_REQUIRED` 说明**没有**这份发布证据——回到 plan 阶段确认发布是否真的完成,**不得**自己拼 `classify + configure-plan + close` 造凭证。`harness_gate.py` 会从当前已安装适配器自动识别 skills root;只有执行复制到别处的脚本时才显式传 `--skills-root`。`--fixback` 不得拼装这些底层步骤,必须只调用一次 `harness_fixback.py launch-review --project . --change <id> --change-dir <change-dir> --executor <tool> --skills-root <skills-root> --product-identity <当前产品身份> --json`:该命令会先筛选结构化评审项,再原子选择 Fixback 分支、确认上下文、取得 Run 门禁并创建已填充批次。返回 `FIXBACK_NOTHING_TO_APPLY` 时直接报告“本轮评审没有需要执行的代码修复”并停止;返回阻塞码时按 `recoveryAction` 停止,不搜索实现、不试探其他参数、不创建空批次。禁止手写 `events.ndjson` / `phase.end`。已连接平台时 begin 会 best-effort 补传事件,失败只告警。
|
|
82
83
|
0.5. **测试基础设施探测**(先写 `CHECKING`,四项证据齐备后再结论)→ `reference.md` Step 0.5;测试基线已由上一步 gate begin 内部建立,不得再次执行 guard begin
|
|
83
84
|
1. **变更簇 TDD** — `protocols.md` `run-tdd-protocol`;批量 RED/GREEN;按需 `change-cluster-review-protocol`(高风险 + reviewer 预检可用)
|
|
84
|
-
2. 构建验证 + **仅**通过 `harness_ledger.py record` 写 ledger(禁止 Write/Edit `verification-ledger.json`);若本阶段创建/删除了清单、锁文件、源码根或改变技术栈,最后一次产品编辑后、写账本与关门前必须执行 `harness_preflight.py detect --project . --json` 刷新 build profile。`record --project . --profile-input <key>` 会对缺失/陈旧 profile 自动检测并从同一 target 推导 scope、coverage
|
|
85
|
+
2. 构建验证 + **仅**通过 `harness_ledger.py record` 写 ledger(禁止 Write/Edit `verification-ledger.json`);若本阶段创建/删除了清单、锁文件、源码根或改变技术栈,最后一次产品编辑后、写账本与关门前必须执行 `harness_preflight.py detect --project . --json` 刷新 build profile。`record --project . --profile-input <key>` 会对缺失/陈旧 profile 自动检测并从同一 target 推导 scope、coverage、规范命令和输入闭包;不得再手填另一套身份。detect 在嵌套/多组件仓库返回 `DETECTION_AMBIGUOUS` 时,响应里的 `profileTemplate` 就是可填骨架(已含 defaultsFingerprint、excludedRoots 默认集与 commands 条目形状),按 `hint` 填好写入 `.harness/config/build-profile.json` 即可,**不要去反读 `harness_profile.py` 源码凑结构**。`diff-hash --change-dir` 纳入 ignored tests → `reference.md` Step 2c
|
|
85
86
|
3. **场景覆盖检查**(场景表映射,禁止用用例数冒充场景数)
|
|
86
87
|
4. **关门检查**(10 项)→ 只执行一次 `harness_gate.py close`;`--to-phase` 必须取返回的 `nextPhases` 或 `plannedPhases` 中 run 的真实后继,禁止写死 test。该命令内部关闭 test guard、写 `phase.end`、释放租约、写 handoff 并补传事件;不得再单独调用 test-guard/context close。失败时按结构化 `recoveryAction` 原样重试,已完成步骤幂等复用。
|
|
87
88
|
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
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`,不失败
|
|
@@ -72,10 +73,11 @@ worktree 合并前必须运行 `harness_change.py integration-lock acquire --run
|
|
|
72
73
|
|
|
73
74
|
### 提交流程(步骤 0–7)
|
|
74
75
|
|
|
75
|
-
0. **启动准备** — `harness_context.py prepare --
|
|
76
|
+
0. **启动准备** — `harness_context.py prepare --project . --change <id> --phase submit --executor <tool> --json`(`--project` 必填)确定唯一变更与 executionRoot;`harness_context.py begin --project . --change <id> --phase submit --executor <tool> --json` 校验 review→submit receipt;**`harness_gate.py begin --phase submit --change <id>`**;读 ledger,以 `harness_ledger.py diff-hash --repo <executionRoot> --base <baseCommit> --change-dir ".harness/changes/<change-name>" --json` 计算 diffHash + post-test 7 类分类(`executionRoot` 必须直接取准备回执;禁止手写 ledger / 手工 phase.end)
|
|
76
77
|
1. **合并最新代码** — 主目录与 worktree 均**不在业务工作区 stash/pull**;远端同步由合并段 integration transaction 在隔离 integration worktree 内完成(见「worktree 合并流程」);**正常路径禁止 `git stash` / `stash pop`**
|
|
77
78
|
2. **最终验证** — ledger 复用优先;提交前只调用 `can-reuse --project . --profile-input unitTestFull --command <profile 规范命令>`。不得传入另一套 `--files`,不得把 runner 包装说明写进 `command`。`reuse=true` 时禁止重跑;只有真实输入、依赖、工具链或环境身份变化时才执行一次同一 profile 验证,并只登记一条结果。不要读取 ledger/archive 实现源码或临时编写散列脚本排查参数。
|
|
78
79
|
- 无远端 CI 且 gate-policy 未强制 remote provider 时,验证通过/复用后运行 `harness_archive.py certify-local --change-dir ... --project . --json`,从同一 ledger 生成 `local-reproducible` 产品候选收据;该命令不执行测试。
|
|
80
|
+
- **被与本变更无关的预存失败卡住时的正规出路**:用 `harness_preflight.py record-quirk --project . --action skip-not-block --pattern "<具体错误签名,≥8 字符>" --reason "<为什么与本变更无关>"` 把它声明进 build-profile 的 `knownPreexistingErrors`,certify-local 会在**该失败的 ledger 证据里确实出现该签名**时放行,并把 `{validation, pattern, reason}` 写进收据的 `verification.preexistingExemptions` 留痕。**不得**改 gate-policy 的 `candidateVerification.requiredValidations` 来绕过——那是把门禁本身拆掉。声明了但证据对不上、或签名短到能匹配一切,仍然阻断。
|
|
79
81
|
- gate-policy 要求 `remote-attested` 时不得降级成本地收据,等待远端 attestation。
|
|
80
82
|
3. **.gitignore + 精确暂存** ⚠️ — 检查 `.harness/` 在 `.gitignore`;**禁止 `git add -A`**。若存在 `evidence/test-tracking.json`,先执行 `python <skills-root>/scripts/harness_test_guard.py stage --project . --change-dir ".harness/changes/<change-name>" --json`;失败即硬停止。无 manifest 时不使用 `-f`。manifest 之外的文件按精确业务路径正常暂存,**禁止全局 force-add**。
|
|
81
83
|
4. **提交方式** — 主目录:blocking user confirmation 三选项(commit+push / 仅本地 / 取消);**worktree:固定仅本地 commit**
|
|
@@ -19,7 +19,7 @@ disallowed-tools:
|
|
|
19
19
|
- Bash(awk *)
|
|
20
20
|
- Bash(curl *)
|
|
21
21
|
---
|
|
22
|
-
<!-- generated by harness_deploy.py; core=
|
|
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=
|
|
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`,不失败
|
|
@@ -105,7 +106,7 @@ Runner 强制同项目单实例、低调度优先级、逐命令超时、正常
|
|
|
105
106
|
|
|
106
107
|
### Phase 0:环境准备(主会话执行,需要交互确认)
|
|
107
108
|
|
|
108
|
-
先 `harness_context.py prepare --
|
|
109
|
+
先 `harness_context.py prepare --project . --change <id> --phase test --executor <tool> --json`(`--project` 必填,漏了直接 argparse 报错),再 `harness_context.py begin --project . --change <id> --phase test --executor <tool> --json` 校验最新 run→test receipt 的 artifact/hash/HEAD;然后 **`harness_gate.py begin --phase test --change <id>`**(禁止手工 phase.start / 手写 ledger)。执行各项强制环境检查 + **命令执行模式 preflight (0.1)**;只有首选执行器不可用时,才执行 fallback 执行器探测。
|
|
109
110
|
|
|
110
111
|
验证写入**仅**允许 `harness_ledger.py record` / `can-reuse`;禁止 Write/Edit `verification-ledger.json`。测试跟踪:gate begin → 执行(可选 `harness_test_guard.py mark stale-test-repair`)→ 单次 `harness_gate.py close`。`--to-phase` 取实际阶段计划的后继;Fixback 返回 run,普通流程可直接进入 Review、Submit 或 Archive。Fixback 只失效与改动文件相交的验证目标。
|
|
111
112
|
|
|
@@ -273,6 +273,8 @@ powershell.exe -NoProfile -Command "try { (Invoke-WebRequest -Uri 'http://127.0.
|
|
|
273
273
|
- [ ] 复用 → 跳过重跑,标记"✅ 复用 harness-run 单元测试结果"
|
|
274
274
|
- [ ] 不复用 → 按 profile key resolve 重跑测试命令(`harness_profile.py resolve --key unitTest`,不复制示例 `-pl` 命令),结果写回 ledger 的 `unitTest` 项
|
|
275
275
|
- [ ] HTTP/API 契约结果写入 `apiTest`;真实浏览器/真实栈 Playwright 结果写入 `browserTest`,两者不得互相覆盖
|
|
276
|
+
- [ ] **本变更没有该维度场景时**(如全部为单元场景 → 无 API 场景):用 `harness_ledger.py record --verification apiTest --status NOT_RUN --applicability NOT_APPLICABLE --applicability-reason "<为什么不适用>"` 登记,**不传 `--files`**。门禁 close 要求 requiredValidations 每项都有 entry,但**绝不能拿无关文件(比如单元测试文件)凑 `--files`**——那会让 ledger 声称该维度的输入是那些文件,是假证据
|
|
277
|
+
- [ ] `can-reuse` 返回 `reuse:false` 时直接读默认输出里的 `reason`/`executionNeed`/`detail` 定位原因,不必再补跑 `--verbose`
|
|
276
278
|
- [ ] 复用判断前以 `harness_ledger.py diff-hash --repo . --base <baseCommit> --change-dir ".harness/changes/<change-name>" --json` 重算指纹;test-tracking manifest 无效或 hash 漂移即停止
|
|
277
279
|
- [ ] 测试失败若明确为陈旧测试,仅在当前代码/批准计划/可验证历史唯一确定新契约且只改测试时自动修复;否则记录 `BLOCKED_PREEXISTING`
|
|
278
280
|
- [ ] 自动修复后立即重跑该测试与目标测试,并以 `harness_test_guard.py record ... --reason stale-test-repair` 记录精确路径
|
|
@@ -991,6 +991,62 @@ def migrate_legacy_candidate_evidence(
|
|
|
991
991
|
return receipt
|
|
992
992
|
|
|
993
993
|
|
|
994
|
+
# 声明必须是具体的错误签名,不能是能匹配一切的短串。这个下限不是"安全边界"
|
|
995
|
+
# ——审计线索才是——但它挡住最省事的滥用写法。
|
|
996
|
+
_PREEXISTING_MIN_PATTERN = 8
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _known_preexisting_patterns(
|
|
1000
|
+
project_root: Path,
|
|
1001
|
+
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
|
1002
|
+
"""build-profile 里声明的预存失败签名,按"是否足够具体"分成两组。
|
|
1003
|
+
|
|
1004
|
+
`knownPreexistingErrors` 由 `harness_preflight.py record-quirk --action
|
|
1005
|
+
skip-not-block` 写入,此前没有任何消费方——于是预存失败在 certify-local 处
|
|
1006
|
+
成为死结:要么去 gate-policy 降门禁,要么顺手改范围外的产品 bug。
|
|
1007
|
+
"""
|
|
1008
|
+
path = project_root / ".harness" / "config" / "build-profile.json"
|
|
1009
|
+
if not path.is_file():
|
|
1010
|
+
return [], []
|
|
1011
|
+
try:
|
|
1012
|
+
profile = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
1013
|
+
except (OSError, ValueError):
|
|
1014
|
+
return [], []
|
|
1015
|
+
declared = profile.get("knownPreexistingErrors") if isinstance(profile, dict) else None
|
|
1016
|
+
if not isinstance(declared, list):
|
|
1017
|
+
return [], []
|
|
1018
|
+
usable: list[dict[str, str]] = []
|
|
1019
|
+
vague: list[dict[str, str]] = []
|
|
1020
|
+
for item in declared:
|
|
1021
|
+
if not isinstance(item, dict) or item.get("action") != "skip-not-block":
|
|
1022
|
+
continue
|
|
1023
|
+
pattern = str(item.get("pattern") or "").strip()
|
|
1024
|
+
reason = str(item.get("reason") or "").strip()
|
|
1025
|
+
if not pattern or not reason:
|
|
1026
|
+
continue
|
|
1027
|
+
record = {"pattern": pattern, "reason": reason}
|
|
1028
|
+
(usable if len(pattern) >= _PREEXISTING_MIN_PATTERN else vague).append(record)
|
|
1029
|
+
return usable, vague
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _match_preexisting(
|
|
1033
|
+
entry: dict[str, Any], patterns: list[dict[str, str]]
|
|
1034
|
+
) -> dict[str, str] | None:
|
|
1035
|
+
"""失败证据里是否确实出现了某个已声明签名。
|
|
1036
|
+
|
|
1037
|
+
只看"声明过"是不够的——那等于声明一次豁免一切。必须是这一条失败的证据里
|
|
1038
|
+
真的带着该签名,声明与现场才对得上。
|
|
1039
|
+
"""
|
|
1040
|
+
evidence = entry.get("evidence")
|
|
1041
|
+
haystack = evidence if isinstance(evidence, str) else json.dumps(
|
|
1042
|
+
evidence, ensure_ascii=False, sort_keys=True
|
|
1043
|
+
)
|
|
1044
|
+
for record in patterns:
|
|
1045
|
+
if record["pattern"] in haystack:
|
|
1046
|
+
return record
|
|
1047
|
+
return None
|
|
1048
|
+
|
|
1049
|
+
|
|
994
1050
|
def certify_local_candidate(
|
|
995
1051
|
change_dir: Path,
|
|
996
1052
|
*,
|
|
@@ -1029,14 +1085,28 @@ def certify_local_candidate(
|
|
|
1029
1085
|
required = ["unitTestFull"]
|
|
1030
1086
|
required = [str(item).strip() for item in required if str(item).strip()]
|
|
1031
1087
|
|
|
1088
|
+
usable_patterns, vague_patterns = _known_preexisting_patterns(project_root)
|
|
1089
|
+
|
|
1032
1090
|
selected: list[dict[str, Any]] = []
|
|
1091
|
+
exemptions: list[dict[str, Any]] = []
|
|
1033
1092
|
for name in required:
|
|
1034
1093
|
entry = validations.get(name)
|
|
1035
1094
|
if not isinstance(entry, dict):
|
|
1036
1095
|
raise ValueError(f"required validation missing: {name}")
|
|
1037
1096
|
missing: list[str] = []
|
|
1097
|
+
exempted = None
|
|
1038
1098
|
if str(entry.get("status") or "").upper() != "OK":
|
|
1039
|
-
|
|
1099
|
+
exempted = _match_preexisting(entry, usable_patterns)
|
|
1100
|
+
if exempted is None:
|
|
1101
|
+
vague = _match_preexisting(entry, vague_patterns)
|
|
1102
|
+
if vague is not None:
|
|
1103
|
+
raise ValueError(
|
|
1104
|
+
f"required validation {name} failed and the declared "
|
|
1105
|
+
f"knownPreexistingErrors pattern {vague['pattern']!r} is too "
|
|
1106
|
+
"generic to identify it; declare the specific error signature "
|
|
1107
|
+
f"(at least {_PREEXISTING_MIN_PATTERN} characters)"
|
|
1108
|
+
)
|
|
1109
|
+
missing.append("status=OK")
|
|
1040
1110
|
for field in (
|
|
1041
1111
|
"command",
|
|
1042
1112
|
"evidence",
|
|
@@ -1048,6 +1118,15 @@ def certify_local_candidate(
|
|
|
1048
1118
|
raise ValueError(
|
|
1049
1119
|
f"required validation {name} is incomplete: {', '.join(missing)}"
|
|
1050
1120
|
)
|
|
1121
|
+
if exempted is not None:
|
|
1122
|
+
exemptions.append(
|
|
1123
|
+
{
|
|
1124
|
+
"validation": name,
|
|
1125
|
+
"status": str(entry.get("status") or "").upper(),
|
|
1126
|
+
"pattern": exempted["pattern"],
|
|
1127
|
+
"reason": exempted["reason"],
|
|
1128
|
+
}
|
|
1129
|
+
)
|
|
1051
1130
|
selected.append(
|
|
1052
1131
|
{
|
|
1053
1132
|
"name": name,
|
|
@@ -1160,6 +1239,8 @@ def certify_local_candidate(
|
|
|
1160
1239
|
{str(item["inputsHash"]) for item in selected}
|
|
1161
1240
|
),
|
|
1162
1241
|
"logHashes": sorted({str(item["logHash"]) for item in selected}),
|
|
1242
|
+
# 豁免必须随收据一起留痕,否则事后无法把它与干净通过区分开
|
|
1243
|
+
"preexistingExemptions": exemptions,
|
|
1163
1244
|
},
|
|
1164
1245
|
}
|
|
1165
1246
|
if rebound_from_commit is not None:
|
|
@@ -8471,8 +8552,17 @@ def cmd_finalize(
|
|
|
8471
8552
|
payload["knowledgeMaintenance"] = _knowledge_maintenance_from_archive_push(
|
|
8472
8553
|
push_result
|
|
8473
8554
|
)
|
|
8474
|
-
|
|
8475
|
-
payload
|
|
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
|
+
)
|
|
8476
8566
|
if push_result.get("warning"):
|
|
8477
8567
|
warnings.append(str(push_result["warning"]))
|
|
8478
8568
|
|
|
@@ -9040,6 +9130,50 @@ def build_archive_package(
|
|
|
9040
9130
|
}
|
|
9041
9131
|
|
|
9042
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
|
+
|
|
9043
9177
|
def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
9044
9178
|
"""Best-effort upload of rules and Codebase Map through the normal push protocol."""
|
|
9045
9179
|
project_root = project_root.resolve()
|
|
@@ -9095,16 +9229,25 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
|
9095
9229
|
except (json.JSONDecodeError, TypeError):
|
|
9096
9230
|
output = None
|
|
9097
9231
|
if completed.returncode != 0 or not isinstance(output, dict):
|
|
9098
|
-
|
|
9232
|
+
# 失败时丢掉 stdout/stderr 等于把线索一起丢了:plan/spec 与规则、架构地图
|
|
9233
|
+
# 全靠这条推送上平台,用户在平台上只会看到"东西没上去",却无从查为什么。
|
|
9234
|
+
# 保留有界的诊断片段(push 本身拒绝上传 secret,这里再截断兜底)。
|
|
9235
|
+
detail = (completed.stderr or completed.stdout or "").strip()
|
|
9236
|
+
failure: dict[str, Any] = {
|
|
9099
9237
|
"ok": False,
|
|
9100
9238
|
"skipped": False,
|
|
9101
9239
|
"exitCode": completed.returncode,
|
|
9102
9240
|
"reasonCode": "MANAGED_SNAPSHOT_UPLOAD_FAILED",
|
|
9241
|
+
"detail": detail[-2_048:],
|
|
9103
9242
|
"warning": (
|
|
9104
|
-
"
|
|
9105
|
-
"
|
|
9243
|
+
"项目规则与架构地图未能同步到平台(plan/spec 也随这条推送上传);"
|
|
9244
|
+
"归档不受影响,稍后可运行 npx hunter-harness push --yes --non-interactive 重试。"
|
|
9106
9245
|
),
|
|
9107
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
|
|
9108
9251
|
summary = output.get("summary")
|
|
9109
9252
|
submitted = summary.get("submitted", 0) if isinstance(summary, dict) else 0
|
|
9110
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)
|
|
@@ -304,12 +304,23 @@ def _compact_record_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
304
304
|
|
|
305
305
|
|
|
306
306
|
def _compact_can_reuse_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
307
|
-
"""C5: can-reuse compact — ok/reuse/code
|
|
308
|
-
|
|
307
|
+
"""C5: can-reuse compact — ok/reuse/code;拒绝复用时附带可执行的原因。
|
|
308
|
+
|
|
309
|
+
只回 ok/reuse/code 时,调用方拿到裸 `{"reuse": false}` 无从判断该怎么办,
|
|
310
|
+
只能再跑一次 `--verbose`——原因(profile 缺 verificationInputs、证据不完整等)
|
|
311
|
+
是单行短文本,值得留在默认输出里。允许复用时保持原样精简。
|
|
312
|
+
"""
|
|
313
|
+
compact = {
|
|
309
314
|
"ok": payload.get("ok", True),
|
|
310
315
|
"reuse": payload.get("reuse"),
|
|
311
316
|
"code": payload.get("code"),
|
|
312
317
|
}
|
|
318
|
+
if payload.get("reuse") is not True:
|
|
319
|
+
for key in ("reason", "executionNeed", "detail"):
|
|
320
|
+
value = payload.get(key)
|
|
321
|
+
if value not in (None, "", [], {}):
|
|
322
|
+
compact[key] = value
|
|
323
|
+
return compact
|
|
313
324
|
|
|
314
325
|
|
|
315
326
|
def emit_compact_or_verbose(
|
|
@@ -2581,9 +2592,19 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2581
2592
|
error_code="PROFILE_INPUT_FILES_CONFLICT",
|
|
2582
2593
|
)
|
|
2583
2594
|
files = profile_files
|
|
2584
|
-
|
|
2595
|
+
# NOT_APPLICABLE 的 validation 没有输入文件可言。门禁 close 又要求
|
|
2596
|
+
# requiredValidations 每项都有 entry——两条规则叠加会逼调用方拿无关文件凑数,
|
|
2597
|
+
# ledger 从此声称该 validation 的输入是那些文件,是假证据。
|
|
2598
|
+
not_applicable = str(getattr(args, "applicability", "") or "") == "NOT_APPLICABLE"
|
|
2599
|
+
if not files and not not_applicable:
|
|
2600
|
+
return emit_error(
|
|
2601
|
+
"record requires --files or a non-empty --profile-input file set; "
|
|
2602
|
+
"unless --applicability NOT_APPLICABLE (then --applicability-reason is required)",
|
|
2603
|
+
as_json=as_json,
|
|
2604
|
+
)
|
|
2605
|
+
if not_applicable and not _nonempty_str(getattr(args, "applicability_reason", None)):
|
|
2585
2606
|
return emit_error(
|
|
2586
|
-
"
|
|
2607
|
+
"NOT_APPLICABLE requires --applicability-reason",
|
|
2587
2608
|
as_json=as_json,
|
|
2588
2609
|
)
|
|
2589
2610
|
|