@hunter-harness/workflow-harness 0.2.53 → 0.2.55
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 +1 -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 +4 -3
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -4
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/claude-code/harness-run/reference.md +2 -2
- 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 +5 -3
- package/harness/bundles/general/claude-code/harness-test/reference.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +46 -7
- package/harness/bundles/general/claude-code/scripts/harness_context.py +123 -22
- package/harness/bundles/general/claude-code/scripts/harness_events.py +40 -1
- package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +236 -10
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +165 -1
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +2 -0
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +2 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -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 +4 -3
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -4
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -2
- 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 +5 -3
- package/harness/bundles/general/codebuddy/harness-test/reference.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +46 -7
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +123 -22
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +40 -1
- package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +236 -10
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +165 -1
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +2 -0
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +2 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -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 +4 -3
- package/harness/bundles/general/codex/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-run/reference.md +2 -2
- 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 +5 -3
- package/harness/bundles/general/codex/harness-test/reference.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +46 -7
- package/harness/bundles/general/codex/scripts/harness_context.py +123 -22
- package/harness/bundles/general/codex/scripts/harness_events.py +40 -1
- package/harness/bundles/general/codex/scripts/harness_events_sync.py +236 -10
- package/harness/bundles/general/codex/scripts/harness_gate.py +165 -1
- package/harness/bundles/general/codex/scripts/harness_ledger.py +2 -0
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +2 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -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 +4 -3
- package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-run/reference.md +2 -2
- 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 +5 -3
- package/harness/bundles/general/cursor/harness-test/reference.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +46 -7
- package/harness/bundles/general/cursor/scripts/harness_context.py +123 -22
- package/harness/bundles/general/cursor/scripts/harness_events.py +40 -1
- package/harness/bundles/general/cursor/scripts/harness_events_sync.py +236 -10
- package/harness/bundles/general/cursor/scripts/harness_gate.py +165 -1
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +2 -0
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +2 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +37 -44
- 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 +1 -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 +4 -3
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -4
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +46 -7
- package/harness/bundles/java/claude-code/scripts/harness_context.py +123 -22
- package/harness/bundles/java/claude-code/scripts/harness_events.py +40 -1
- package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +236 -10
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +165 -1
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +2 -0
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +2 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +37 -44
- 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 +1 -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 +4 -3
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -4
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +46 -7
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +123 -22
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +40 -1
- package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +236 -10
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +165 -1
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +2 -0
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +2 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +37 -44
- 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 +1 -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 +4 -3
- package/harness/bundles/java/codex/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/java/codex/scripts/harness_archive.py +46 -7
- package/harness/bundles/java/codex/scripts/harness_context.py +123 -22
- package/harness/bundles/java/codex/scripts/harness_events.py +40 -1
- package/harness/bundles/java/codex/scripts/harness_events_sync.py +236 -10
- package/harness/bundles/java/codex/scripts/harness_gate.py +165 -1
- package/harness/bundles/java/codex/scripts/harness_ledger.py +2 -0
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +2 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +37 -44
- 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 +1 -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 +4 -3
- package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/java/cursor/scripts/harness_archive.py +46 -7
- package/harness/bundles/java/cursor/scripts/harness_context.py +123 -22
- package/harness/bundles/java/cursor/scripts/harness_events.py +40 -1
- package/harness/bundles/java/cursor/scripts/harness_events_sync.py +236 -10
- package/harness/bundles/java/cursor/scripts/harness_gate.py +165 -1
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +2 -0
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +2 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +37 -44
- package/harness/manifests/general/claude-code.json +24 -24
- package/harness/manifests/general/codebuddy.json +24 -24
- package/harness/manifests/general/codex.json +24 -24
- package/harness/manifests/general/cursor.json +24 -24
- 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 -4
- 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=e9b3973b8ba91be9; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-archive — 变更归档
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -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=e9b3973b8ba91be9; 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=e9b3973b8ba91be9; 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=e9b3973b8ba91be9; 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=e9b3973b8ba91be9; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-plan — 需求规划
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -85,14 +85,14 @@ disallowed-tools:
|
|
|
85
85
|
| 阶段 | 动作 |
|
|
86
86
|
|------|------|
|
|
87
87
|
| 0 | 用当前解释器运行 `harness_runtime.py doctor`,后续消费绝对 argv;git status;脏工作区 → baseline 隔离 + `decision`,不询问 |
|
|
88
|
-
| 0.5 |
|
|
88
|
+
| 0.5 | 确定英文 `change-name` 后,根据需求生成一次简洁的中文展示标题(建议 6~24 个可见字符,保留必要产品名),执行 `harness_context.py prepare --phase plan --executor <tool> [--change <id>] --title "<中文标题>" --json`;英文名继续作为目录与稳定标识。以其唯一 change/executionRoot 初始化 plan-run-id 与 attempt(首次为 1),用同一身份追加 `phase.start`;从第一条知识查询起保留事件证据,并在 finalizer 中复用该身份 |
|
|
89
89
|
| 1 | `harness-knowledge-query` 单次远端 query(失败记 `issue`,不建立本地索引或离线回退) |
|
|
90
90
|
| 2 | 歧义优先检查 + 复杂度分级;先确认会改变实现方向的语义歧义 |
|
|
91
91
|
| 3 | 按复杂度执行有预算的代码探索;简单修复不得扩散到无关模块 |
|
|
92
92
|
| 4 | **设计审批包** blocking user confirmation;确认事件早于 approved 设计文档和 `meta/worktree.json` |
|
|
93
93
|
| 5–6 | plan + implementation-detail + test-scenarios → `plans/` |
|
|
94
94
|
| 7.5 | 仅 `--adversarial` 对抗评审 |
|
|
95
|
-
| 8 | 在临时产物集上运行 `harness_plan_finalize.py finalize`,随后立即运行 `verify
|
|
95
|
+
| 8 | 在临时产物集上运行 `harness_plan_finalize.py finalize`,随后立即运行 `verify`;成功后把 finalizer 返回的绝对 `receiptPath` 原样传给 `harness_context.py close --from-phase plan --to-phase run --executor <tool> --artifact "<receiptPath>" --json`(也可使用返回的稳定 `artifactRef=meta/plan-finalization.json`),写 append-only handoff receipt;不得手写占位路径;原子发布、派生清单计数对账、完整生命周期、render → `checklist.md` |
|
|
96
96
|
|
|
97
97
|
change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`)
|
|
98
98
|
|
|
@@ -113,6 +113,7 @@ change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`
|
|
|
113
113
|
| 规则 | 要点 |
|
|
114
114
|
|------|------|
|
|
115
115
|
| 产物路径 | 只写 `.harness/changes/<cn>/`;禁止 superpowers 输入 |
|
|
116
|
+
| Change 标题 | 首次 Plan 同时确定英文 `change-name` 与中文展示标题;英文名保持目录和机器标识不变,中文标题由 `prepare --title` 持久化,后续阶段只复用、不重新生成 |
|
|
116
117
|
| 设计审批包 | 一次 blocking user confirmation 含 worktree(读 `harness.json` `defaultWorktree`) |
|
|
117
118
|
| 阶段 8 | spec/plan/detail/scenarios/gate-policy/worktree 六项标准产物先进入 staging;仅 finalizer 校验成功后发布并写唯一 `phase.end`/log;随后 `verify` 必须确认 start/end、收据完整覆盖六项标准产物、哈希、全部任务表和非空场景清单一致,失败不得手工补终态 |
|
|
118
119
|
| Plan 结束 | **禁止**询问执行模式;只提示 `/harness-run` |
|
|
@@ -211,13 +211,14 @@ source: harness-plan
|
|
|
211
211
|
|
|
212
212
|
- [ ] 所有待发布产物先写入 staging,不直接覆盖正式 change 目录
|
|
213
213
|
- [ ] 执行 `harness_plan_finalize.py finalize --change-dir ... --staging-dir ... --change ... --run-id <plan-run-id> --attempt <attempt> --json`;身份必须与本次 `phase.start` 完全相同
|
|
214
|
-
- [ ] finalizer 返回 `ok=true`
|
|
214
|
+
- [ ] finalizer 返回 `ok=true`、`artifactsHash`、绝对 `receiptPath` 与稳定 `artifactRef=meta/plan-finalization.json`;重复执行返回 `idempotent=true`
|
|
215
215
|
- [ ] 紧接着执行 `harness_plan_finalize.py verify --change-dir ... --json`
|
|
216
216
|
- [ ] verify 返回 `phaseStartCount=1`、`phaseEndCount=1`、`phaseEndStatus=OK`、`receiptConsistent=true`
|
|
217
217
|
- [ ] 收据 `files` 完整包含 design / plan / implementation-detail / test-scenarios / gate-policy / worktree 六项标准产物;不得省略、重复或经 symlink/junction/reparse point 引用
|
|
218
218
|
- [ ] verify 的 `taskCount` 等于全部任务表行数,`scenarioCount` 等于 Markdown 中全部唯一场景 ID 数;任一为 0 或不一致即 ❌FAIL
|
|
219
219
|
- [ ] finalizer 失败时正式目录无半发布产物、无成功 `phase.end`、无伪造 execution log
|
|
220
220
|
- [ ] 禁止在 finalizer 之前手工追加成功 `phase.end`
|
|
221
|
+
- [ ] context close 的 `--artifact` 只传 finalizer 原样返回的 `receiptPath` 或 `artifactRef`,不得猜测、拼接或使用 `<plan-finalization>` 占位文本
|
|
221
222
|
|
|
222
223
|
| 文件 | 必须存在 | 检查结果 |
|
|
223
224
|
|------|:---:|:---:|
|
|
@@ -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=e9b3973b8ba91be9; overlay=none; agent=claude-code; do not edit -->
|
|
26
26
|
# harness-review — 代码审查
|
|
27
27
|
|
|
28
28
|
## Purpose
|
|
@@ -63,14 +63,16 @@ disallowed-tools:
|
|
|
63
63
|
|
|
64
64
|
## Workflow
|
|
65
65
|
|
|
66
|
-
先用 `harness_context.py prepare --phase review --executor <tool> [--change <id>] --json` 与 `harness_context.py begin --phase review --change <id> --executor <tool> --json` 解析唯一 active change 并校验 test→review receipt;多个 active change 未显式选择时返回 `ACTIVE_CHANGE_AMBIGUOUS`,禁止再按 Glob/mtime 猜测。阶段门禁使用 `harness_gate.py begin --phase review --change <id> --task <n> --skills-root <skills-root> --executor-tool <tool> --json
|
|
66
|
+
先用 `harness_context.py prepare --phase review --executor <tool> [--change <id>] --json` 与 `harness_context.py begin --phase review --change <id> --executor <tool> --json` 解析唯一 active change 并校验 test→review receipt;多个 active change 未显式选择时返回 `ACTIVE_CHANGE_AMBIGUOUS`,禁止再按 Glob/mtime 猜测。阶段门禁使用 `harness_gate.py begin --phase review --change <id> --task <n> --skills-root <skills-root> --executor-tool <tool> --json`;结束时只执行一次 `harness_gate.py close --phase review --change <id> --status <OK|WARN|FAIL> --run-id <begin-run-id> --to-phase submit|run --executor <tool> --json`,由 gate 完成阶段事件、租约、上下文交接与远端补传。不得手工追加 `phase.start/phase.end` 或再次调用 context close;任一 close 失败不得宣称审查完成。
|
|
67
67
|
|
|
68
68
|
0. **启动准备** — 确定变更名(Glob `.harness/changes/*/plans/*-plan.md`,排除 `.harness/archive/*/`,读 frontmatter 提取 change-name);**append `phase.start` 事件**(不得等审查完成才补)
|
|
69
69
|
1. **读取 worktree 状态(门禁检查)** — 读 `.harness/changes/<change-name>/meta/worktree.json`:`requested=true` 但 worktree 不存在 → 停止并提示先修复 `harness-run`,不得静默回主目录(否则 git diff 为空);`requested=true` 且 worktree 已创建 → spawned agent 用该 worktree 路径执行 `git diff`(确保审查 worktree 变更而非主目录);`requested=false` → 审查主目录变更
|
|
70
70
|
<!-- @section-id review.delegate -->
|
|
71
|
-
### 2.
|
|
71
|
+
### 2. 审查执行(隔离 reviewer 优先)
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
review 默认优先在独立上下文执行,避免主会话已形成的实现结论影响审查。阶段开始后只做一次能力判定:固定 reviewer 可用时执行 `python <skills-root>/scripts/harness_preflight.py check-agents --skills-root <skills-root> --agent harness-reviewer --json`;宿主提供通用隔离任务能力时可直接委派一次只读审查。委派只读取 diff、规则和测试证据,主会话负责最终核验与落盘。
|
|
74
|
+
|
|
75
|
+
无隔离能力、定义损坏、spawn 失败、空返回或只有元数据时,不重试,立即由主会话按同一 6 维度检查清单完成审查。无论是否委派,都必须追加一条 `decision` 事件:`executor_agent=harness-reviewer` 表示已委派;`decision` 写执行方式,`reason` 写选择或回退原因,稳定原因码使用 `REVIEW_DELEGATED`、`REVIEW_INLINE_UNAVAILABLE`、`REVIEW_INLINE_SPAWN_FAILED` 或 `REVIEW_INLINE_INVALID_RESULT`。正常 inline 是可接受结果,不显示成流程故障;`fallbackPolicy=inline-no-retry`。
|
|
74
76
|
|
|
75
77
|
3. **持久化报告与事实 sidecar(强制,主会话)** — Agent 返回后主会话 Write 到 `reports/review/review-report-*.md`,并把同一批发现写成临时 JSON 后调用 `python <skills-root>/scripts/harness_review.py write-findings --change-dir <change-dir> --input <findings.json>`。权威计数来自 `reports/review/review-findings.json`,不是 Markdown。任一写入缺失 → 🟡WARN,不得宣称 review 完成。
|
|
76
78
|
4. **生成修复反馈(原生协议)** — 若报告存在 RED/YELLOW 问题,执行 `protocols.md` 的 `review-fixback-protocol`,将问题转化为结构化 fixback 清单并落盘到 `.harness/changes/<change-name>/reports/review/fixback-YYYYMMDD-HHmm.md`;随后把 disposition 临时 JSON 交给 `python <skills-root>/scripts/harness_review.py write-dispositions --change-dir <change-dir> --input <dispositions.json>`,权威状态写入 `reports/review/fixback-dispositions.json`。若无 RED/YELLOW,也必须写空 findings sidecar,并记录 `review-fixback-protocol: skipped(no findings)`。不调用 Superpowers `receiving-code-review`,也不记录外部 skill 降级。
|
|
@@ -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=e9b3973b8ba91be9; overlay=none; agent=claude-code; do not edit -->
|
|
26
26
|
# harness-run — 需求编码
|
|
27
27
|
|
|
28
28
|
## Purpose
|
|
@@ -79,11 +79,11 @@ disallowed-tools:
|
|
|
79
79
|
## Workflow 概要
|
|
80
80
|
|
|
81
81
|
0. 加载上下文:先 `harness_context.py prepare --phase run --executor <tool> [--change <id>] --json`,以返回的唯一 change/executionRoot/transition receipt 为准(多 active 缺参 → `ACTIVE_CHANGE_AMBIGUOUS`,禁止按 mtime 猜测);再 **`harness_context.py begin --phase run --change <id> --executor <tool> --json`** 校验 Plan artifact/hash/HEAD,读 spec/plan/detail/scenarios/ledger/run-task-status/worktree;`--fixback` 读 fixback → **`harness_gate.py begin --phase run --change <id>`**(claim + phase.start + identity;禁止手工 Write `events.ndjson` / 手工 `phase.end`)。**C3 钩子**:`harness_gate.py begin` 在已连接平台时会 best-effort 调用 events-sync(注册 run / `running`);失败只告警,不阻断 begin。
|
|
82
|
-
0.5. **测试基础设施探测**(先写 `CHECKING`,四项证据齐备后再结论)→ `reference.md` Step 0.5
|
|
82
|
+
0.5. **测试基础设施探测**(先写 `CHECKING`,四项证据齐备后再结论)→ `reference.md` Step 0.5;测试基线已由上一步 gate begin 内部建立,不得再次执行 guard begin
|
|
83
83
|
1. **变更簇 TDD** — `protocols.md` `run-tdd-protocol`;批量 RED/GREEN;按需 `change-cluster-review-protocol`(高风险 + reviewer 预检可用)
|
|
84
84
|
2. 构建验证 + **仅**通过 `harness_ledger.py record` 写 ledger(禁止 Write/Edit `verification-ledger.json`);`diff-hash --change-dir` 纳入 ignored tests → `reference.md` Step 2c
|
|
85
85
|
3. **场景覆盖检查**(场景表映射,禁止用用例数冒充场景数)
|
|
86
|
-
4. **关门检查**(10 项)→
|
|
86
|
+
4. **关门检查**(10 项)→ 只执行一次 **`harness_gate.py close --phase run --change <id> --status <OK|WARN|FAIL> --to-phase test --executor <tool> --json`**。该命令内部依次关闭 test guard、写 `phase.end`、释放阶段租约、写 append-only handoff,并立即补传远端事件;不得再单独调用 test-guard/context close。若返回 `PHASE_HANDOFF_PENDING`,修正明确指出的输入后原样重试同一命令,已完成步骤会幂等复用。
|
|
87
87
|
|
|
88
88
|
**Fixback**:`--fixback` 或用户要求时读 `reports/review/fixback-*.md`;RED 优先;未选用则记 `fixback: advisory-not-applied`。
|
|
89
89
|
|
|
@@ -112,7 +112,7 @@ disallowed-tools:
|
|
|
112
112
|
| **RED/GREEN** | RED 须有效;静态验证 ≠ 测试通过;greenfield 大重写豁免见 reference |
|
|
113
113
|
| **Mapper/DB** | 纯 Mock 不得宣称 DB 验证通过;迁移脚本**永不自动执行** |
|
|
114
114
|
| **探测/ledger** | 基础设施先探测;每次构建/测试经 `harness_ledger.py record`;禁止手写 ledger JSON;用 canonical `diff-hash --change-dir` |
|
|
115
|
-
| **Gate/Guard** | 跨 Agent/阶段先用 `harness_context.py prepare/begin
|
|
115
|
+
| **Gate/Guard** | 跨 Agent/阶段先用 `harness_context.py prepare/begin`;阶段门禁与上下文交接统一用 `harness_gate.py begin/close`;gate 内部负责 guard begin/close,模型只在需要记录测试来源或修复时调用 guard 的 `record/stage/mark` |
|
|
116
116
|
| **预存变更** | 保留 → baseline 隔离;存在则最终 ≥ 🟡WARN |
|
|
117
117
|
| **关门/状态** | 10 项关门检查;持久化 run-task-status;P0 静态-only 不得建议 submit |
|
|
118
118
|
| **Worktree** | `requested=true` 时代码只写 worktree |
|
|
@@ -540,7 +540,7 @@ python <skills-root>/scripts/harness_ledger.py record --change-dir ".harness/cha
|
|
|
540
540
|
>
|
|
541
541
|
> **Ledger v3**:`record` 自动解析并强制顶层身份(`schemaVersion=3/repositoryId/changeName/baseCommit/currentHead/diffHash/ownershipHash`),缺字段非零退出、不写账本;已有 v1/v2 ledger 在写前透明迁移,`migrationHistory` 回执记录原/目标 schema 与迁移前后 evidence identity。无法迁移时返回 `LEDGER_MIGRATION_REQUIRED` 和确定性 `rerecordCommand`,不得半升级。v2 契约下 `--metrics-json` 必须通过 typed schema(unit=`total/passed/failed`,apiContract=`scenariosTotal/passed/failed`,browserE2E=`total/passed/failed`,dbCompatibility=`applicability(+reason)`)。详见 `../protocols/ledger-protocol.md` 第十节。
|
|
542
542
|
|
|
543
|
-
|
|
543
|
+
阶段边界与测试跟踪统一由 `harness_gate.py begin/close` 协调;底层 guard begin/close 不再由模型重复调用。close 失败不得用自然语言覆盖。
|
|
544
544
|
|
|
545
545
|
## 步骤 3:场景覆盖检查
|
|
546
546
|
|
|
@@ -800,7 +800,7 @@ powershell.exe -Command "git -C '<project-path>' diff --check"
|
|
|
800
800
|
```powershell
|
|
801
801
|
# gate begin/close(--task 仅在该 change 启用 checkpoint 时必需;close 不需要 --skills-root)
|
|
802
802
|
python <skills-root>/scripts/harness_gate.py begin --change <cn> --phase run --skills-root <skills-root> [--task N]
|
|
803
|
-
python <skills-root>/scripts/harness_gate.py close --change <cn> --phase run --status OK [--task N]
|
|
803
|
+
python <skills-root>/scripts/harness_gate.py close --change <cn> --phase run --status OK --to-phase test --executor <tool> [--task N]
|
|
804
804
|
|
|
805
805
|
# ledger 记录(status: ok|fail|not_run —— 没有 PASS)
|
|
806
806
|
python <skills-root>/scripts/harness_ledger.py record --change-dir <dir> --verification unitTestFull --status ok --command "<完整命令>" --exit-code 0 --duration-ms 120000 --evidence "Tests run: 155, Failures: 0, Errors: 0, Skipped: 0" --coverage full --files "packages/core/src/index.ts"
|
|
@@ -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=e9b3973b8ba91be9; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-submit — 最终提交(含 worktree 合并)
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -19,7 +19,7 @@ disallowed-tools:
|
|
|
19
19
|
- Bash(awk *)
|
|
20
20
|
- Bash(curl *)
|
|
21
21
|
---
|
|
22
|
-
<!-- generated by harness_deploy.py; core=
|
|
22
|
+
<!-- generated by harness_deploy.py; core=e9b3973b8ba91be9; 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=e9b3973b8ba91be9; overlay=none; agent=claude-code; do not edit -->
|
|
27
27
|
# harness-test — 测试执行
|
|
28
28
|
|
|
29
29
|
## Purpose
|
|
@@ -66,7 +66,9 @@ disallowed-tools:
|
|
|
66
66
|
|
|
67
67
|
## Workflow
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
测试跟踪由 `harness_gate.py begin/close` 统一协调 test guard;不得额外手工执行 guard close。任一失败都必须使用 gate 返回的结构化状态,不得以自然语言覆盖失败状态。
|
|
70
|
+
|
|
71
|
+
兼容实现中的底层命令名是 `harness_test_guard.py begin` 与 `harness_test_guard.py close`,两者只允许由 gate 内部调用;写在这里用于能力审计,不构成模型执行步骤。
|
|
70
72
|
|
|
71
73
|
并行服务测试先运行 `harness_change.py lease-port --change <id> --run-id <run-id> --range <start-end> --json`,再把返回端口传给 `harness_service.py ensure --leased-port <port> --lease-owner <run-id>`。`serviceStart` 的 command/health/overlay 可用 `{leasedPort}` 占位符;用户自启进程仍只进入 Service Gate,禁止 kill。测试清理的 `finally` 中运行 `harness_change.py release-port --change <id> --run-id <run-id> --json`,避免租约池耗尽。
|
|
72
74
|
|
|
@@ -105,7 +107,7 @@ Runner 强制同项目单实例、低调度优先级、逐命令超时、正常
|
|
|
105
107
|
|
|
106
108
|
先 `harness_context.py prepare --phase test --executor <tool> [--change <id>] --json`,再 `harness_context.py begin --phase test --change <id> --executor <tool> --json` 校验最新 run→test receipt 的 artifact/hash/HEAD;然后 **`harness_gate.py begin --phase test --change <id>`**(禁止手工 phase.start / 手写 ledger)。执行各项强制环境检查 + **命令执行模式 preflight (0.1)**;只有首选执行器不可用时,才执行 fallback 执行器探测。
|
|
107
109
|
|
|
108
|
-
验证写入**仅**允许 `harness_ledger.py record` / `can-reuse`;禁止 Write/Edit `verification-ledger.json
|
|
110
|
+
验证写入**仅**允许 `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 --phase test --status ... --to-phase review --executor <tool> --json`。fixback 时由 `harness_fixback.py` 合批记录 RED/GREEN 和 affected verification,再把同一 close 命令的 `--to-phase` 改为 `run`,由 context 失效受影响 target。
|
|
109
111
|
|
|
110
112
|
- **Read `checklist.md`** — 各项检查详情 + 0.1 preflight + Playwright 探测 + 避坑规则指引
|
|
111
113
|
- **失败处理**:任一项检查失败 → 终止流程并报告原因,用户确认修复后才能继续
|
|
@@ -740,7 +740,7 @@ TEST_<change-name>_<timestamp>_<short-random>
|
|
|
740
740
|
```powershell
|
|
741
741
|
# gate begin/close(phase=test;--task 仅 checkpoint 启用时必需;close 不需要 --skills-root)
|
|
742
742
|
python <skills-root>/scripts/harness_gate.py begin --change <cn> --phase test --skills-root <skills-root> [--task N]
|
|
743
|
-
python <skills-root>/scripts/harness_gate.py close --change <cn> --phase test --status OK [--task N]
|
|
743
|
+
python <skills-root>/scripts/harness_gate.py close --change <cn> --phase test --status OK --to-phase review --executor <tool> [--task N]
|
|
744
744
|
|
|
745
745
|
# ledger 记录 / 复用(--profile-input = verification key,不是文件路径)
|
|
746
746
|
python <skills-root>/scripts/harness_ledger.py record --change-dir <dir> --verification unitTestFull --status ok --command "<完整命令>" --exit-code 0 --duration-ms 120000 --evidence "Tests run: N, Failures: 0" --coverage full --files "packages/core/src/index.ts"
|
|
@@ -17,6 +17,7 @@ import argparse
|
|
|
17
17
|
import datetime as dt
|
|
18
18
|
import hashlib
|
|
19
19
|
import io
|
|
20
|
+
import ipaddress
|
|
20
21
|
import json
|
|
21
22
|
import os
|
|
22
23
|
import re
|
|
@@ -31,6 +32,7 @@ import zipfile
|
|
|
31
32
|
from collections.abc import Mapping
|
|
32
33
|
from pathlib import Path
|
|
33
34
|
from typing import Any
|
|
35
|
+
from urllib.parse import urlsplit
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
if hasattr(sys.stdout, "reconfigure"):
|
|
@@ -192,9 +194,9 @@ def append_event(
|
|
|
192
194
|
for key, value in fields.items():
|
|
193
195
|
if value is not None:
|
|
194
196
|
event[key] = value
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
all_events =
|
|
197
|
+
result = he.append_with_auto_seal(path, event, existing_events=existing)
|
|
198
|
+
event = result["event"]
|
|
199
|
+
all_events = he.load_events(path)
|
|
198
200
|
he.write_execution_log(change_dir, he.render_execution_log(all_events))
|
|
199
201
|
return event
|
|
200
202
|
|
|
@@ -3053,6 +3055,25 @@ def build_verification_projection(
|
|
|
3053
3055
|
"apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
|
|
3054
3056
|
}
|
|
3055
3057
|
db_compatibility = _ledger_db_compat(effective_ledger)
|
|
3058
|
+
if db_compatibility["status"] == "NOT_RUN" and change_dir is not None:
|
|
3059
|
+
gate_policy_path = change_dir / "meta" / "gate-policy.json"
|
|
3060
|
+
try:
|
|
3061
|
+
gate_policy = read_json(gate_policy_path) if gate_policy_path.is_file() else None
|
|
3062
|
+
except (OSError, json.JSONDecodeError):
|
|
3063
|
+
gate_policy = None
|
|
3064
|
+
if isinstance(gate_policy, dict):
|
|
3065
|
+
capabilities = gate_policy.get("capabilities")
|
|
3066
|
+
required = gate_policy.get("requiredValidations")
|
|
3067
|
+
if (
|
|
3068
|
+
isinstance(capabilities, list)
|
|
3069
|
+
and "database" not in capabilities
|
|
3070
|
+
and (not isinstance(required, list) or "dbCompatibility" not in required)
|
|
3071
|
+
):
|
|
3072
|
+
db_compatibility = {
|
|
3073
|
+
"status": "NOT_APPLICABLE",
|
|
3074
|
+
"reason": "项目能力画像未声明数据库能力",
|
|
3075
|
+
"source": "capability-profile",
|
|
3076
|
+
}
|
|
3056
3077
|
projection["dbCompatibility"] = db_compatibility["status"]
|
|
3057
3078
|
projection["dbCompatibilityEvidence"] = db_compatibility
|
|
3058
3079
|
api_contract = validations.get("apiContract")
|
|
@@ -8377,6 +8398,27 @@ def _read_optional_text(path: Path) -> str:
|
|
|
8377
8398
|
return ""
|
|
8378
8399
|
|
|
8379
8400
|
|
|
8401
|
+
def _archive_server_url_allowed(value: str | None) -> bool:
|
|
8402
|
+
"""Require TLS remotely while allowing explicit loopback HTTP for local testing."""
|
|
8403
|
+
if value is None or not value.strip() or re.search(r"\s", value):
|
|
8404
|
+
return False
|
|
8405
|
+
try:
|
|
8406
|
+
parsed = urlsplit(value)
|
|
8407
|
+
host = (parsed.hostname or "").lower()
|
|
8408
|
+
except ValueError:
|
|
8409
|
+
return False
|
|
8410
|
+
if parsed.scheme.lower() == "https":
|
|
8411
|
+
return bool(host)
|
|
8412
|
+
if parsed.scheme.lower() != "http":
|
|
8413
|
+
return False
|
|
8414
|
+
if host == "localhost":
|
|
8415
|
+
return True
|
|
8416
|
+
try:
|
|
8417
|
+
return ipaddress.ip_address(host).is_loopback
|
|
8418
|
+
except ValueError:
|
|
8419
|
+
return False
|
|
8420
|
+
|
|
8421
|
+
|
|
8380
8422
|
def _resolve_archive_remote_credentials(
|
|
8381
8423
|
project_root: Path,
|
|
8382
8424
|
environment: Mapping[str, str],
|
|
@@ -8403,10 +8445,7 @@ def _resolve_archive_remote_credentials(
|
|
|
8403
8445
|
else ""
|
|
8404
8446
|
)
|
|
8405
8447
|
token_available = bool(env_token or local_token)
|
|
8406
|
-
url_available =
|
|
8407
|
-
server_url
|
|
8408
|
-
and re.fullmatch(r"https://[^\s]+", server_url, flags=re.IGNORECASE)
|
|
8409
|
-
)
|
|
8448
|
+
url_available = _archive_server_url_allowed(server_url)
|
|
8410
8449
|
return {
|
|
8411
8450
|
"configured": bool(url_available and token_available),
|
|
8412
8451
|
"serverUrl": server_url if url_available else None,
|
|
@@ -23,6 +23,8 @@ PHASE_GRAPH = {
|
|
|
23
23
|
"submit": (),
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
DISPLAY_TITLE_MAX_LENGTH = 80
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
def _now() -> dt.datetime:
|
|
28
30
|
return dt.datetime.now(dt.timezone.utc)
|
|
@@ -35,6 +37,41 @@ def _read_json(path: Path) -> dict[str, Any]:
|
|
|
35
37
|
return data
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
def _normalize_display_title(value: Any) -> str | None:
|
|
41
|
+
if value is None:
|
|
42
|
+
return None
|
|
43
|
+
if not isinstance(value, str):
|
|
44
|
+
raise ValueError("display title must be text")
|
|
45
|
+
title = value.strip()
|
|
46
|
+
if not title:
|
|
47
|
+
raise ValueError("display title must not be empty")
|
|
48
|
+
if len(title) > DISPLAY_TITLE_MAX_LENGTH:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"display title exceeds {DISPLAY_TITLE_MAX_LENGTH} characters"
|
|
51
|
+
)
|
|
52
|
+
if any(ord(character) < 32 or ord(character) == 127 for character in title):
|
|
53
|
+
raise ValueError("display title contains control characters")
|
|
54
|
+
return title
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ensure_display_title(contract_root: Path, value: Any) -> str | None:
|
|
58
|
+
path = contract_root / "meta" / "change-title.json"
|
|
59
|
+
if path.is_file():
|
|
60
|
+
payload = _read_json(path)
|
|
61
|
+
return _normalize_display_title(payload.get("displayTitle"))
|
|
62
|
+
title = _normalize_display_title(value)
|
|
63
|
+
if title is None:
|
|
64
|
+
return None
|
|
65
|
+
_write_json_atomic(
|
|
66
|
+
path,
|
|
67
|
+
{
|
|
68
|
+
"schemaVersion": 1,
|
|
69
|
+
"displayTitle": title,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
return title
|
|
73
|
+
|
|
74
|
+
|
|
38
75
|
def _write_json_atomic(path: Path, payload: dict[str, Any]) -> None:
|
|
39
76
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
77
|
fd, raw = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
|
@@ -243,6 +280,7 @@ def prepare_context(
|
|
|
243
280
|
phase: str,
|
|
244
281
|
executor: str,
|
|
245
282
|
change: str | None = None,
|
|
283
|
+
display_title: str | None = None,
|
|
246
284
|
ttl_seconds: int = 3600,
|
|
247
285
|
) -> dict[str, Any]:
|
|
248
286
|
project = Path(project).resolve()
|
|
@@ -265,6 +303,16 @@ def prepare_context(
|
|
|
265
303
|
contract_root, contract, state_root = _contract(project, change)
|
|
266
304
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
267
305
|
return {"ok": False, "code": str(exc).split(":", 1)[0], "error": str(exc)}
|
|
306
|
+
try:
|
|
307
|
+
resolved_display_title = _ensure_display_title(
|
|
308
|
+
contract_root, display_title
|
|
309
|
+
)
|
|
310
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
311
|
+
return {
|
|
312
|
+
"ok": False,
|
|
313
|
+
"code": "DISPLAY_TITLE_INVALID",
|
|
314
|
+
"error": str(exc),
|
|
315
|
+
}
|
|
268
316
|
lifecycle = contract.get("lifecycle")
|
|
269
317
|
if (
|
|
270
318
|
isinstance(lifecycle, dict)
|
|
@@ -343,6 +391,7 @@ def prepare_context(
|
|
|
343
391
|
"executionRoot": str(execution_root),
|
|
344
392
|
"preparedAt": now.isoformat(),
|
|
345
393
|
"receiptHash": latest.get("receiptHash") if latest else None,
|
|
394
|
+
"displayTitle": resolved_display_title,
|
|
346
395
|
},
|
|
347
396
|
)
|
|
348
397
|
next_phases = (
|
|
@@ -363,6 +412,7 @@ def prepare_context(
|
|
|
363
412
|
"latestTransition": latest,
|
|
364
413
|
"lease": lease,
|
|
365
414
|
"recovery": recovery,
|
|
415
|
+
"displayTitle": resolved_display_title,
|
|
366
416
|
}
|
|
367
417
|
|
|
368
418
|
|
|
@@ -424,7 +474,7 @@ def close_transition(
|
|
|
424
474
|
*,
|
|
425
475
|
from_phase: str,
|
|
426
476
|
to_phase: str,
|
|
427
|
-
executor: str,
|
|
477
|
+
executor: str | None,
|
|
428
478
|
artifacts: list[str] | None = None,
|
|
429
479
|
status: str = "OK",
|
|
430
480
|
) -> dict[str, Any]:
|
|
@@ -441,13 +491,81 @@ def close_transition(
|
|
|
441
491
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
442
492
|
return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
|
|
443
493
|
paths = _paths(state_root)
|
|
494
|
+
|
|
495
|
+
artifact_entries: list[dict[str, Any]] = []
|
|
496
|
+
for raw in artifacts or []:
|
|
497
|
+
requested = Path(raw)
|
|
498
|
+
candidates = (
|
|
499
|
+
[requested]
|
|
500
|
+
if requested.is_absolute()
|
|
501
|
+
else [project / requested, contract_root / requested]
|
|
502
|
+
)
|
|
503
|
+
valid: list[Path] = []
|
|
504
|
+
for candidate in candidates:
|
|
505
|
+
try:
|
|
506
|
+
resolved_candidate = candidate.resolve()
|
|
507
|
+
except OSError:
|
|
508
|
+
continue
|
|
509
|
+
if resolved_candidate in valid:
|
|
510
|
+
continue
|
|
511
|
+
if resolved_candidate.is_relative_to(project) and resolved_candidate.is_file():
|
|
512
|
+
valid.append(resolved_candidate)
|
|
513
|
+
if len(valid) > 1:
|
|
514
|
+
return {
|
|
515
|
+
"ok": False,
|
|
516
|
+
"code": "TRANSITION_ARTIFACT_AMBIGUOUS",
|
|
517
|
+
"path": raw,
|
|
518
|
+
"candidates": [item.relative_to(project).as_posix() for item in valid],
|
|
519
|
+
"message": "relative artifact exists under both project and change roots",
|
|
520
|
+
}
|
|
521
|
+
if not valid:
|
|
522
|
+
return {
|
|
523
|
+
"ok": False,
|
|
524
|
+
"code": "TRANSITION_ARTIFACT_INVALID",
|
|
525
|
+
"path": raw,
|
|
526
|
+
"acceptedBases": [str(project), str(contract_root)],
|
|
527
|
+
"examples": [
|
|
528
|
+
str(contract_root / "meta" / "plan-finalization.json"),
|
|
529
|
+
f".harness/changes/{change}/meta/plan-finalization.json",
|
|
530
|
+
"meta/plan-finalization.json",
|
|
531
|
+
],
|
|
532
|
+
}
|
|
533
|
+
resolved_artifact = valid[0]
|
|
534
|
+
artifact_entries.append(
|
|
535
|
+
{
|
|
536
|
+
"path": resolved_artifact.relative_to(project).as_posix(),
|
|
537
|
+
"sha256": _sha256(resolved_artifact),
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
transitions = _read_ndjson(paths["transitions"])
|
|
444
542
|
if not paths["lease"].is_file():
|
|
543
|
+
latest = transitions[-1] if transitions else None
|
|
544
|
+
if (
|
|
545
|
+
isinstance(latest, dict)
|
|
546
|
+
and latest.get("fromPhase") == from_phase
|
|
547
|
+
and latest.get("toPhase") == to_phase
|
|
548
|
+
and latest.get("status") == status
|
|
549
|
+
and latest.get("artifacts") == artifact_entries
|
|
550
|
+
and (executor is None or latest.get("executor") == executor)
|
|
551
|
+
):
|
|
552
|
+
return {
|
|
553
|
+
"ok": True,
|
|
554
|
+
"code": "TRANSITION_ALREADY_CLOSED",
|
|
555
|
+
"idempotent": True,
|
|
556
|
+
"receipt": latest,
|
|
557
|
+
"path": str(paths["transitions"]),
|
|
558
|
+
"invalidation": None,
|
|
559
|
+
}
|
|
445
560
|
return {"ok": False, "code": "CONTEXT_LEASE_REQUIRED"}
|
|
446
561
|
try:
|
|
447
562
|
lease = _read_json(paths["lease"])
|
|
448
563
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
449
564
|
return {"ok": False, "code": "CONTEXT_LEASE_INVALID"}
|
|
450
|
-
|
|
565
|
+
effective_executor = executor or str(lease.get("owner") or "")
|
|
566
|
+
if not effective_executor:
|
|
567
|
+
return {"ok": False, "code": "CONTEXT_EXECUTOR_REQUIRED"}
|
|
568
|
+
if lease.get("owner") != effective_executor or lease.get("phase") != from_phase:
|
|
451
569
|
return {
|
|
452
570
|
"ok": False,
|
|
453
571
|
"code": "CONTEXT_LEASE_MISMATCH",
|
|
@@ -458,25 +576,6 @@ def close_transition(
|
|
|
458
576
|
if expiry is None or _now() >= expiry:
|
|
459
577
|
return {"ok": False, "code": "CONTEXT_LEASE_EXPIRED"}
|
|
460
578
|
|
|
461
|
-
artifact_entries: list[dict[str, Any]] = []
|
|
462
|
-
for raw in artifacts or []:
|
|
463
|
-
path = Path(raw)
|
|
464
|
-
if not path.is_absolute():
|
|
465
|
-
path = contract_root / path
|
|
466
|
-
resolved = path.resolve()
|
|
467
|
-
if not resolved.is_relative_to(project) or not resolved.is_file():
|
|
468
|
-
return {
|
|
469
|
-
"ok": False,
|
|
470
|
-
"code": "TRANSITION_ARTIFACT_INVALID",
|
|
471
|
-
"path": str(path),
|
|
472
|
-
}
|
|
473
|
-
artifact_entries.append(
|
|
474
|
-
{
|
|
475
|
-
"path": resolved.relative_to(project).as_posix(),
|
|
476
|
-
"sha256": _sha256(resolved),
|
|
477
|
-
}
|
|
478
|
-
)
|
|
479
|
-
transitions = _read_ndjson(paths["transitions"])
|
|
480
579
|
previous_hash = transitions[-1].get("receiptHash") if transitions else None
|
|
481
580
|
attempt = (
|
|
482
581
|
1
|
|
@@ -493,7 +592,7 @@ def close_transition(
|
|
|
493
592
|
"fromPhase": from_phase,
|
|
494
593
|
"toPhase": to_phase,
|
|
495
594
|
"status": status,
|
|
496
|
-
"executor":
|
|
595
|
+
"executor": effective_executor,
|
|
497
596
|
"productCommit": _head(project),
|
|
498
597
|
"artifacts": artifact_entries,
|
|
499
598
|
"attempt": attempt,
|
|
@@ -657,6 +756,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
657
756
|
prepare.add_argument("--change")
|
|
658
757
|
prepare.add_argument("--phase", required=True)
|
|
659
758
|
prepare.add_argument("--executor", required=True)
|
|
759
|
+
prepare.add_argument("--title")
|
|
660
760
|
prepare.add_argument("--ttl-seconds", type=int, default=3600)
|
|
661
761
|
close = sub.add_parser("close")
|
|
662
762
|
close.add_argument("--json", action="store_true")
|
|
@@ -688,6 +788,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
688
788
|
phase=args.phase,
|
|
689
789
|
executor=args.executor,
|
|
690
790
|
change=args.change,
|
|
791
|
+
display_title=args.title,
|
|
691
792
|
ttl_seconds=args.ttl_seconds,
|
|
692
793
|
)
|
|
693
794
|
elif args.command == "close":
|