@hunter-harness/workflow-harness 0.2.83 → 0.2.85
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 +39 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/protocols.md +60 -0
- package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +314 -11
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +10 -0
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/claude-code/scripts/harness_review.py +43 -5
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +39 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/protocols.md +60 -0
- package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +314 -11
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +10 -0
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +43 -5
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +39 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/protocols.md +60 -0
- package/harness/bundles/general/codex/harness-run/reference.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +314 -11
- package/harness/bundles/general/codex/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/codex/scripts/harness_gate.py +10 -0
- package/harness/bundles/general/codex/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/codex/scripts/harness_review.py +43 -5
- package/harness/bundles/general/codex/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +39 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/protocols.md +60 -0
- package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +314 -11
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/cursor/scripts/harness_gate.py +10 -0
- package/harness/bundles/general/cursor/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/cursor/scripts/harness_review.py +43 -5
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +125 -10
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +39 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/protocols.md +60 -0
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +314 -11
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +10 -0
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/claude-code/scripts/harness_review.py +43 -5
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +125 -10
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +39 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/protocols.md +60 -0
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +314 -11
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +10 -0
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +43 -5
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +125 -10
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +39 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/protocols.md +60 -0
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +314 -11
- package/harness/bundles/java/codex/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/codex/scripts/harness_gate.py +10 -0
- package/harness/bundles/java/codex/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/codex/scripts/harness_review.py +43 -5
- package/harness/bundles/java/codex/scripts/harness_runtime.py +125 -10
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +39 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/protocols.md +60 -0
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +314 -11
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/cursor/scripts/harness_gate.py +10 -0
- package/harness/bundles/java/cursor/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/cursor/scripts/harness_review.py +43 -5
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +125 -10
- package/harness/manifests/general/claude-code.json +23 -23
- package/harness/manifests/general/codebuddy.json +23 -23
- package/harness/manifests/general/codex.json +23 -23
- package/harness/manifests/general/cursor.json +23 -23
- package/harness/manifests/java/claude-code.json +24 -24
- package/harness/manifests/java/codebuddy.json +24 -24
- package/harness/manifests/java/codex.json +24 -24
- package/harness/manifests/java/cursor.json +24 -24
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ name: harness-pull
|
|
|
3
3
|
description: 从 Hunter Platform 下拉配置/规则/架构/指令(及显式来源分支的分支文件恢复)。仅当用户显式调用
|
|
4
4
|
/harness-pull 或明确说'从平台拉取/恢复'时使用;不得自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=b0e6891811c856ce; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-pull — 从 Hunter Platform 下拉与恢复
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-push
|
|
|
3
3
|
description: 上传本地配置/规则/架构/指令(及显式归档)到 Hunter Platform。仅当用户显式调用 /harness-push
|
|
4
4
|
或明确说'上传到平台'时使用;不得因存在本地修改就自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=b0e6891811c856ce; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-push — 上传到 Hunter Platform
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -4,7 +4,7 @@ description: 6维度代码审查(架构/安全/规范/兼容/测试/性能)
|
|
|
4
4
|
.harness/context-index.json)和测试场景表,在隔离上下文运行。仅当用户显式调用 /harness-review 时使用;不得在
|
|
5
5
|
test 结束后自动接续执行。
|
|
6
6
|
---
|
|
7
|
-
<!-- generated by harness_deploy.py; core=
|
|
7
|
+
<!-- generated by harness_deploy.py; core=b0e6891811c856ce; overlay=none; agent=codebuddy; do not edit -->
|
|
8
8
|
# harness-review — 代码审查
|
|
9
9
|
|
|
10
10
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-run
|
|
|
3
3
|
description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。仅当用户显式调用
|
|
4
4
|
/harness-run 时使用;不得因用户提到编码/实现就自动触发,也不得被其他阶段 skill 自动接续。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=b0e6891811c856ce; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-run — 需求编码
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -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"`)
|
|
@@ -3,7 +3,7 @@ name: harness-submit
|
|
|
3
3
|
description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff 合并回主分支。仅当用户显式调用
|
|
4
4
|
/harness-submit(或 /harness-merge 重入合并段)时使用;用户口头说'提交/commit/push'时必须先确认,不得自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=b0e6891811c856ce; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-submit — 最终提交(含 worktree 合并)
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -4,7 +4,7 @@ description: Use when the user asks to synchronize, refresh, or validate Harness
|
|
|
4
4
|
metadata, adapters, remote knowledge ownership, instruction entrypoints,
|
|
5
5
|
config origins, or CodeGraph status.
|
|
6
6
|
---
|
|
7
|
-
<!-- generated by harness_deploy.py; core=
|
|
7
|
+
<!-- generated by harness_deploy.py; core=b0e6891811c856ce; overlay=none; agent=codebuddy; do not edit -->
|
|
8
8
|
# harness-sync
|
|
9
9
|
|
|
10
10
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-test
|
|
|
3
3
|
description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。仅当用户显式调用 /harness-test
|
|
4
4
|
时使用;不得在 run 结束后自动接续执行。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=b0e6891811c856ce; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-test — 测试执行
|
|
8
8
|
|
|
9
9
|
## 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
|
-
|
|
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(
|
|
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,13 @@ def check_status(
|
|
|
2407
2599
|
}
|
|
2408
2600
|
)
|
|
2409
2601
|
|
|
2602
|
+
checks["publication_content_scan"] = {
|
|
2603
|
+
"ok": True,
|
|
2604
|
+
"scan_performed": False,
|
|
2605
|
+
"findings": [],
|
|
2606
|
+
"message": "上传归档链路已停用内容敏感扫描;保留路径、凭据与 ZIP 安全校验。",
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2410
2609
|
sensitive_gate = validate_sensitive_evidence_publication_gate(
|
|
2411
2610
|
change_dir,
|
|
2412
2611
|
copy_root=change_dir,
|
|
@@ -7842,6 +8041,7 @@ def cmd_finalize(
|
|
|
7842
8041
|
source_sensitive_refresh = hruntime.refresh_sensitive_evidence_scan_receipt(
|
|
7843
8042
|
original_change_dir,
|
|
7844
8043
|
persist=False,
|
|
8044
|
+
exclude_dirs=hruntime.PUBLICATION_EXCLUDED_DIRS,
|
|
7845
8045
|
)
|
|
7846
8046
|
payload["steps"]["sensitive_evidence_source_refresh"] = source_sensitive_refresh
|
|
7847
8047
|
if not source_sensitive_refresh.get("ok"):
|
|
@@ -7981,7 +8181,8 @@ def cmd_finalize(
|
|
|
7981
8181
|
"stateDir": str(split_state_dir),
|
|
7982
8182
|
}
|
|
7983
8183
|
staged_sensitive_refresh = hruntime.refresh_sensitive_evidence_scan_receipt(
|
|
7984
|
-
work_dir
|
|
8184
|
+
work_dir,
|
|
8185
|
+
exclude_dirs=hruntime.PUBLICATION_EXCLUDED_DIRS,
|
|
7985
8186
|
)
|
|
7986
8187
|
payload["steps"][
|
|
7987
8188
|
"sensitive_evidence_staging_refresh"
|
|
@@ -8742,6 +8943,12 @@ def cmd_finalize(
|
|
|
8742
8943
|
payload["finalStatus"] = "OK"
|
|
8743
8944
|
payload["warnings"] = warnings
|
|
8744
8945
|
payload["summary_data"] = str(summary_path)
|
|
8946
|
+
# 结果落在归档里(移动之后的位置),调用方不必再靠 shell 重定向去接 stdout。
|
|
8947
|
+
persisted = persist_execute_result(archive_dir, payload)
|
|
8948
|
+
if not persisted.get("ok"):
|
|
8949
|
+
warnings.append(
|
|
8950
|
+
f"could not persist archive execute result: {persisted.get('error')}"
|
|
8951
|
+
)
|
|
8745
8952
|
return 0, payload
|
|
8746
8953
|
|
|
8747
8954
|
|
|
@@ -9190,6 +9397,9 @@ def resolve_hunter_cli_command(project_root: Path) -> list[str]:
|
|
|
9190
9397
|
"""Command prefix that runs the hunter-harness CLI, cheapest route first."""
|
|
9191
9398
|
node = shutil.which("node")
|
|
9192
9399
|
if node:
|
|
9400
|
+
current_entry = os.environ.get("HUNTER_HARNESS_CLI_ENTRY", "").strip()
|
|
9401
|
+
if current_entry and Path(current_entry).is_file():
|
|
9402
|
+
return [node, str(Path(current_entry).resolve())]
|
|
9193
9403
|
entry = _installed_cli_entry(project_root)
|
|
9194
9404
|
if entry is not None:
|
|
9195
9405
|
return [node, str(entry)]
|
|
@@ -9556,6 +9766,7 @@ def auto_push_archive_core(
|
|
|
9556
9766
|
change_key: str | None = None,
|
|
9557
9767
|
extra_entries: dict[str, tuple[str, str, bytes]] | None = None,
|
|
9558
9768
|
reuse_retained_package: bool = False,
|
|
9769
|
+
prepared_package: dict[str, Any] | None = None,
|
|
9559
9770
|
) -> dict[str, Any]:
|
|
9560
9771
|
"""Build and upload one deterministic core ZIP after finalize.
|
|
9561
9772
|
|
|
@@ -9568,11 +9779,13 @@ def auto_push_archive_core(
|
|
|
9568
9779
|
"""
|
|
9569
9780
|
project_root = project_root.resolve()
|
|
9570
9781
|
effective_change_key = change_key or archive_dir.name
|
|
9571
|
-
package =
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9782
|
+
package = prepared_package
|
|
9783
|
+
if package is None:
|
|
9784
|
+
package = (
|
|
9785
|
+
load_retained_package(project_root, effective_change_key)
|
|
9786
|
+
if reuse_retained_package
|
|
9787
|
+
else None
|
|
9788
|
+
)
|
|
9576
9789
|
if package is None:
|
|
9577
9790
|
try:
|
|
9578
9791
|
package = build_archive_package(
|
|
@@ -10120,9 +10333,45 @@ def cmd_certify_local_cli(args: argparse.Namespace) -> int:
|
|
|
10120
10333
|
return 0
|
|
10121
10334
|
|
|
10122
10335
|
|
|
10336
|
+
def _emit_with_optional_output(args: argparse.Namespace, payload: dict[str, Any]) -> int | None:
|
|
10337
|
+
"""Write the result to --output, refusing a path the archive will delete."""
|
|
10338
|
+
raw = getattr(args, "output", None)
|
|
10339
|
+
if not raw:
|
|
10340
|
+
return None
|
|
10341
|
+
resolved = resolve_execute_result_path(resolve_path(args.change_dir), str(raw))
|
|
10342
|
+
if not resolved.get("ok"):
|
|
10343
|
+
emit_json(resolved)
|
|
10344
|
+
return 2
|
|
10345
|
+
try:
|
|
10346
|
+
target = Path(resolved["path"])
|
|
10347
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
10348
|
+
target.write_text(
|
|
10349
|
+
json.dumps(payload, ensure_ascii=False, indent=2, default=str) + "\n",
|
|
10350
|
+
encoding="utf-8",
|
|
10351
|
+
newline="\n",
|
|
10352
|
+
)
|
|
10353
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
10354
|
+
emit_json({
|
|
10355
|
+
"ok": False,
|
|
10356
|
+
"reasonCode": "ARCHIVE_RESULT_WRITE_FAILED",
|
|
10357
|
+
"path": resolved["path"],
|
|
10358
|
+
"error": str(exc),
|
|
10359
|
+
})
|
|
10360
|
+
return 2
|
|
10361
|
+
payload["resultPath"] = resolved["path"]
|
|
10362
|
+
return None
|
|
10363
|
+
|
|
10364
|
+
|
|
10123
10365
|
def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
10124
10366
|
change_dir = resolve_path(args.change_dir)
|
|
10125
10367
|
archive_root = resolve_path(args.archive_root)
|
|
10368
|
+
# 先验路径再干活:跑完才发现输出落点会被移走,结果已经无法重建。
|
|
10369
|
+
raw_output = getattr(args, "output", None)
|
|
10370
|
+
if raw_output:
|
|
10371
|
+
resolved = resolve_execute_result_path(change_dir, str(raw_output))
|
|
10372
|
+
if not resolved.get("ok"):
|
|
10373
|
+
emit_json(resolved)
|
|
10374
|
+
return 2
|
|
10126
10375
|
durable_root = (
|
|
10127
10376
|
resolve_path(args.durable_root)
|
|
10128
10377
|
if getattr(args, "durable_root", None)
|
|
@@ -10141,6 +10390,9 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
|
10141
10390
|
closure_disposition=str(getattr(args, "closure", "completed")),
|
|
10142
10391
|
closure_reason=str(getattr(args, "closure_reason", "")),
|
|
10143
10392
|
)
|
|
10393
|
+
failure = _emit_with_optional_output(args, payload)
|
|
10394
|
+
if failure is not None:
|
|
10395
|
+
return failure
|
|
10144
10396
|
emit_json(payload)
|
|
10145
10397
|
return code
|
|
10146
10398
|
|
|
@@ -10151,6 +10403,14 @@ def cmd_execute_cli(args: argparse.Namespace) -> int:
|
|
|
10151
10403
|
if getattr(args, "durable_root", None)
|
|
10152
10404
|
else None
|
|
10153
10405
|
)
|
|
10406
|
+
raw_output = getattr(args, "output", None)
|
|
10407
|
+
if raw_output:
|
|
10408
|
+
resolved = resolve_execute_result_path(
|
|
10409
|
+
resolve_path(args.change_dir), str(raw_output)
|
|
10410
|
+
)
|
|
10411
|
+
if not resolved.get("ok"):
|
|
10412
|
+
emit_json(resolved)
|
|
10413
|
+
return 2
|
|
10154
10414
|
code, payload = execute_archive(
|
|
10155
10415
|
resolve_path(args.change_dir),
|
|
10156
10416
|
resolve_path(args.archive_root),
|
|
@@ -10164,6 +10424,9 @@ def cmd_execute_cli(args: argparse.Namespace) -> int:
|
|
|
10164
10424
|
closure_disposition=str(getattr(args, "closure", "completed")),
|
|
10165
10425
|
closure_reason=str(getattr(args, "closure_reason", "")),
|
|
10166
10426
|
)
|
|
10427
|
+
failure = _emit_with_optional_output(args, payload)
|
|
10428
|
+
if failure is not None:
|
|
10429
|
+
return failure
|
|
10167
10430
|
emit_json(payload)
|
|
10168
10431
|
return code
|
|
10169
10432
|
|
|
@@ -10369,6 +10632,28 @@ def resolve_archive_dir_for_change(
|
|
|
10369
10632
|
return archive_root / matches[-1], matches
|
|
10370
10633
|
|
|
10371
10634
|
|
|
10635
|
+
def resolve_latest_unpublished_archive(project_root: Path) -> tuple[str | None, list[str]]:
|
|
10636
|
+
"""Choose the newest sealed archive without a durable local receipt."""
|
|
10637
|
+
archive_root = project_root / ".harness" / "archive"
|
|
10638
|
+
if not archive_root.is_dir():
|
|
10639
|
+
return None, []
|
|
10640
|
+
candidates: list[tuple[str, str]] = []
|
|
10641
|
+
for entry in archive_root.iterdir():
|
|
10642
|
+
if not entry.is_dir():
|
|
10643
|
+
continue
|
|
10644
|
+
change = entry.name
|
|
10645
|
+
try:
|
|
10646
|
+
summary = read_json(entry / "reports" / "final" / "summary-data.json")
|
|
10647
|
+
if isinstance(summary, dict) and isinstance(summary.get("changeName"), str):
|
|
10648
|
+
change = str(summary["changeName"]).strip() or change
|
|
10649
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
10650
|
+
pass
|
|
10651
|
+
if read_durable_archive_receipt(project_root, change) is None:
|
|
10652
|
+
candidates.append((entry.name, change))
|
|
10653
|
+
candidates.sort(reverse=True)
|
|
10654
|
+
return (candidates[0][1] if candidates else None), [item[1] for item in candidates]
|
|
10655
|
+
|
|
10656
|
+
|
|
10372
10657
|
def _republish_knowledge_entry(archive_dir: Path) -> dict[str, tuple[str, str, bytes]]:
|
|
10373
10658
|
"""Knowledge candidates for archives sealed before the candidates step.
|
|
10374
10659
|
|
|
@@ -10439,12 +10724,26 @@ def cmd_republish(
|
|
|
10439
10724
|
"changeKey": change_key,
|
|
10440
10725
|
"dryRun": dry_run,
|
|
10441
10726
|
}
|
|
10727
|
+
root = resolve_republish_project_root(project_root, archive_dir)
|
|
10728
|
+
if change_key == "latest":
|
|
10729
|
+
selected, available = resolve_latest_unpublished_archive(root)
|
|
10730
|
+
if selected is None:
|
|
10731
|
+
payload.update({
|
|
10732
|
+
"ok": True,
|
|
10733
|
+
"reasonCode": "ARCHIVE_NO_UNPUBLISHED_ARCHIVE",
|
|
10734
|
+
"noChanges": True,
|
|
10735
|
+
"availableChanges": available,
|
|
10736
|
+
"projectRoot": str(root)
|
|
10737
|
+
})
|
|
10738
|
+
return 0, payload
|
|
10739
|
+
change_key = selected
|
|
10740
|
+
payload["changeKey"] = change_key
|
|
10741
|
+
payload["selectedChange"] = selected
|
|
10442
10742
|
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,159}", change_key) is None:
|
|
10443
10743
|
payload["error"] = "change key must be a portable path segment"
|
|
10444
10744
|
payload["reasonCode"] = "ARCHIVE_CHANGE_KEY_INVALID"
|
|
10445
10745
|
return 2, payload
|
|
10446
10746
|
|
|
10447
|
-
root = resolve_republish_project_root(project_root, archive_dir)
|
|
10448
10747
|
payload["projectRoot"] = str(root)
|
|
10449
10748
|
matches: list[str] = []
|
|
10450
10749
|
if archive_dir is None:
|
|
@@ -10553,7 +10852,7 @@ def cmd_republish(
|
|
|
10553
10852
|
# distinct name; it must never be mistaken for a pending upload.
|
|
10554
10853
|
preview_path = (
|
|
10555
10854
|
root / ".harness" / "state" / "local" / "archive-packages"
|
|
10556
|
-
/ f"{change_key}.preview.zip"
|
|
10855
|
+
/ f"{change_key}{'.preview.zip' if dry_run else '.zip'}"
|
|
10557
10856
|
)
|
|
10558
10857
|
try:
|
|
10559
10858
|
package = build_archive_package(
|
|
@@ -10569,7 +10868,8 @@ def cmd_republish(
|
|
|
10569
10868
|
return 1, payload
|
|
10570
10869
|
finally:
|
|
10571
10870
|
try:
|
|
10572
|
-
|
|
10871
|
+
if dry_run:
|
|
10872
|
+
preview_path.unlink(missing_ok=True)
|
|
10573
10873
|
except OSError:
|
|
10574
10874
|
pass
|
|
10575
10875
|
|
|
@@ -10639,6 +10939,7 @@ def cmd_republish(
|
|
|
10639
10939
|
archive_dir,
|
|
10640
10940
|
change_key=change_key,
|
|
10641
10941
|
extra_entries=extra_entries,
|
|
10942
|
+
prepared_package=package,
|
|
10642
10943
|
)
|
|
10643
10944
|
payload["push"] = result
|
|
10644
10945
|
payload["ok"] = bool(result.get("ok"))
|
|
@@ -10771,6 +11072,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
10771
11072
|
default="release-candidate",
|
|
10772
11073
|
help="release-candidate enforces candidate evidence; record-only archives facts without release eligibility",
|
|
10773
11074
|
)
|
|
11075
|
+
p_fin.add_argument("--output", default=None, help="把完整结果另存到该路径。**不得**指向 --change-dir 内部:归档会把该目录整个移走,写在里面的文件必然丢失。归档成功后结果也会自动落在 <archiveDir>/meta/archive-execute-result.json。")
|
|
10774
11076
|
p_fin.add_argument("--json", action="store_true", default=True)
|
|
10775
11077
|
p_fin.set_defaults(func=cmd_finalize_cli)
|
|
10776
11078
|
|
|
@@ -10795,6 +11097,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
10795
11097
|
choices=("release-candidate", "record-only"),
|
|
10796
11098
|
default="release-candidate",
|
|
10797
11099
|
)
|
|
11100
|
+
p_execute.add_argument("--output", default=None, help="把完整结果另存到该路径。**不得**指向 --change-dir 内部:归档会把该目录整个移走,写在里面的文件必然丢失。归档成功后结果也会自动落在 <archiveDir>/meta/archive-execute-result.json。")
|
|
10798
11101
|
p_execute.add_argument("--json", action="store_true", default=True)
|
|
10799
11102
|
p_execute.set_defaults(func=cmd_execute_cli)
|
|
10800
11103
|
|