@hunter-harness/workflow-harness 0.2.66 → 0.2.69
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/contracts/workflow-policy.json +401 -88
- 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 +5 -5
- package/harness/bundles/general/claude-code/harness-plan/reference.md +35 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +73 -0
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +73 -0
- 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-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_events_sync.py +213 -4
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +119 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +401 -88
- 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 +5 -5
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +35 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +55 -0
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +55 -0
- 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-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_events_sync.py +213 -4
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +119 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +401 -88
- 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 +5 -5
- package/harness/bundles/general/codex/harness-plan/reference.md +35 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +55 -0
- package/harness/bundles/general/codex/harness-push/SKILL.md +55 -0
- 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-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_events_sync.py +213 -4
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +119 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +401 -88
- 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 +5 -5
- package/harness/bundles/general/cursor/harness-plan/reference.md +35 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +55 -0
- package/harness/bundles/general/cursor/harness-push/SKILL.md +55 -0
- 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-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_events_sync.py +213 -4
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +119 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +401 -88
- 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 +5 -5
- package/harness/bundles/java/claude-code/harness-plan/reference.md +35 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +73 -0
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +73 -0
- 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-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_events_sync.py +213 -4
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +119 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +401 -88
- 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 +5 -5
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +35 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +55 -0
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +55 -0
- 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-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_events_sync.py +213 -4
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +119 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +401 -88
- 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 +5 -5
- package/harness/bundles/java/codex/harness-plan/reference.md +35 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +55 -0
- package/harness/bundles/java/codex/harness-push/SKILL.md +55 -0
- 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-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_events_sync.py +213 -4
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +119 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +401 -88
- 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 +5 -5
- package/harness/bundles/java/cursor/harness-plan/reference.md +35 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +55 -0
- package/harness/bundles/java/cursor/harness-push/SKILL.md +55 -0
- 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-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_events_sync.py +213 -4
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +119 -0
- package/harness/manifests/general/claude-code.json +28 -18
- package/harness/manifests/general/codebuddy.json +28 -18
- package/harness/manifests/general/codex.json +28 -18
- package/harness/manifests/general/cursor.json +28 -18
- package/harness/manifests/java/claude-code.json +30 -20
- package/harness/manifests/java/codebuddy.json +30 -20
- package/harness/manifests/java/codex.json +30 -20
- package/harness/manifests/java/cursor.json +30 -20
- package/hunter-workflow-family.json +11 -7
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ name: harness-plan
|
|
|
3
3
|
description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。仅当用户显式调用 /harness-plan 或明确要求进入
|
|
4
4
|
Harness 规划阶段时使用;不得因用户描述需求就自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=e4b3e064899ba4af; overlay=none; agent=cursor; do not edit -->
|
|
7
7
|
# harness-plan — 需求规划
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -66,15 +66,15 @@ description: 将需求转化为设计文档+实施计划+完整测试场景表
|
|
|
66
66
|
| 阶段 | 动作 |
|
|
67
67
|
|------|------|
|
|
68
68
|
| 0 | 用当前解释器运行 `harness_runtime.py doctor`,后续消费绝对 argv;git status;脏工作区 → baseline 隔离 + `decision`,不询问 |
|
|
69
|
-
| 0.5 | 确定英文 `change-name` 后,根据需求生成一次简洁的中文展示标题(建议 6~24 个可见字符,保留必要产品名),执行 `harness_context.py prepare --phase plan --executor <tool> [--change <id>] --title "<中文标题>" --json`;英文名继续作为目录与稳定标识。立即运行一次 `harness_state.py capture --project . --change-dir "<executionRoot>" --json`,把此时 HEAD 固定为不可变 `changeBase`。以其唯一 change/executionRoot 初始化 plan-run-id 与 attempt(首次为 1
|
|
69
|
+
| 0.5 | 确定英文 `change-name` 后,根据需求生成一次简洁的中文展示标题(建议 6~24 个可见字符,保留必要产品名),执行 `harness_context.py prepare --phase plan --executor <tool> [--change <id>] --title "<中文标题>" --json`;英文名继续作为目录与稳定标识。立即运行一次 `harness_state.py capture --project . --change-dir "<executionRoot>" --json`,把此时 HEAD 固定为不可变 `changeBase`。以其唯一 change/executionRoot 初始化 plan-run-id 与 attempt(首次为 1);run-id 必须小写字母开头(v2 identity 规则,裸 UUID 有 10/16 概率数字开头被拒)——统一用 `plan_<uuid>` 形状(contracts `createPlanRunId()`),用同一身份追加 `phase.start`;从第一条知识查询起保留事件证据,并在 finalizer 中复用该身份 |
|
|
70
70
|
| 0.6 | 使用 `classify` 返回的默认阶段和项目能力生成 `plannedPhases`,向用户用中文说明可选阶段;确认后运行 `harness_context.py configure-plan --project . --change <id> --phases "plan,run,...,archive" --operator <tool> --reason "<中文原因>" --json`。无 Git 或不需要提交时不得加入 `submit`;快速迭代默认 `plan,run,archive`。省略项由脚本写入 `skippedPhases`,不得伪造阶段事件 |
|
|
71
71
|
| 1 | 直接执行一次 `npx hunter-harness knowledge query "<用户需求原文>" --limit 10 --json`。这是唯一执行入口,不扫描技能目录、不查找其他脚本;失败记 `issue` 并继续,不建立本地索引或离线回退 |
|
|
72
72
|
| 2 | 歧义优先检查 + 复杂度分级;先确认会改变实现方向的语义歧义 |
|
|
73
73
|
| 3 | 按复杂度执行有预算的代码探索;简单修复不得扩散到无关模块 |
|
|
74
74
|
| 4 | **设计审批包** blocking user confirmation;确认事件早于 approved 设计文档和 `meta/worktree.json` |
|
|
75
|
-
| 5–6 | plan + implementation-detail + test-scenarios → `plans
|
|
75
|
+
| 5–6 | plan + implementation-detail + test-scenarios → `plans/`;v2 路径时同步沉淀 `meta/plan-evidence-input.json`(自然输入,字段定稿时点见 `reference.md` 阶段 8 v2 表) |
|
|
76
76
|
| 7.5 | 仅 `--adversarial` 对抗评审 |
|
|
77
|
-
| 8 |
|
|
77
|
+
| 8 | **v2 路径(新 change 优先)**:`npx hunter-harness plan evidence-pack --input <meta/plan-evidence-input.json> --output <meta/plan-evidence.json>`,再 `npx hunter-harness plan finalize --input <meta/plan-evidence.json>`;exit 0 且 `code:"PLAN_FINALIZED"` 即发布完成(契约见 `reference.md` 阶段 8 v2 路径)。**legacy 路径**:自然输入不完整(如缺真实审批记录)时才在临时产物集上运行 `harness_plan_finalize.py finalize`,随后立即运行 `verify`;成功后把 finalizer 返回的绝对 `receiptPath` 原样传给 `harness_context.py close`,`--to-phase` 必须取 `plannedPhases` 中 plan 的真实后继,不得写死。写 append-only handoff receipt;不得手写占位路径;原子发布、派生清单计数对账、完整生命周期、render → `checklist.md` |
|
|
78
78
|
|
|
79
79
|
change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`)
|
|
80
80
|
|
|
@@ -100,7 +100,7 @@ change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`
|
|
|
100
100
|
| 产品边界 | `ownership.productPaths` 必须覆盖计划会修改的源文件、测试文件和构建入口;只写目录前缀或精确文件,禁止 `**` 通配。finalize 前对照任务表补齐,避免归档阶段才发现边界缺口 |
|
|
101
101
|
| 空目录 | 不得为“预留目录”生成 `.gitkeep`;只有产品明确需要跟踪空目录时才能创建,并在计划中说明业务原因 |
|
|
102
102
|
| 设计审批包 | 一次 blocking user confirmation 含 worktree(读 `harness.json` `defaultWorktree`) |
|
|
103
|
-
| 阶段 8 |
|
|
103
|
+
| 阶段 8 | 二选一且不得混用:**v2** = `hunter-harness plan finalize`(证据包 → 八 target + journal committed + plan-events.ndjson);**legacy** = 六项标准产物先进入 staging,仅 finalizer 校验成功后发布并写唯一 `phase.end`/log,随后 `verify` 确认 start/end、收据完整覆盖六项标准产物、哈希、全部任务表和非空场景清单一致。失败均不得手工补终态 |
|
|
104
104
|
| Plan 结束 | **禁止**询问执行模式;只提示 `/harness-run` |
|
|
105
105
|
| 知识查询 | 阶段 1 失败不得假装已读历史,也不得改用本地索引或其他执行入口 |
|
|
106
106
|
| 歧义优先检查 | 否定、对比、动作对象或范围存在多种合理解释时,最小取证后先给推荐理解并一次一问;确认前不深挖错误方向 |
|
|
@@ -299,7 +299,7 @@ status: approved
|
|
|
299
299
|
|
|
300
300
|
> 如果 frontmatter 缺失,后续 run/test/review/submit/archive 不得依赖模型猜测 change-name。
|
|
301
301
|
|
|
302
|
-
4. **初始化结构化事件**:确定 change-name 后,立即生成稳定的 `<plan-run-id
|
|
302
|
+
4. **初始化结构化事件**:确定 change-name 后,立即生成稳定的 `<plan-run-id>`(必须小写字母开头:v2 identity 规则,裸 UUID 有 10/16 概率数字开头被拒——统一用 `plan_<uuid>` 形状,contracts `createPlanRunId()`;同一次 plan 尝试内不得改变;首次 `<attempt>` 为 `1`),运行 `harness_events.py append --change-dir ... --phase plan --type phase.start --run-id <plan-run-id> --attempt <attempt>`。finalizer 必须复用完全相同的 `--run-id` / `--attempt`,否则 verify 会按生命周期身份 fail-closed。脚本负责建立父目录和 `events.ndjson`;执行日志在 `phase.end` 时由完整事件流渲染,任何阶段都不得直接用 Write/Edit 维护该投影。
|
|
303
303
|
|
|
304
304
|
5. **保存计划文件**:计划文件包含 YAML frontmatter(含 change-name),保存到:
|
|
305
305
|
- `.harness/changes/<change-name>/plans/<change-name>-plan.md`(简洁任务表)
|
|
@@ -345,6 +345,40 @@ status: approved
|
|
|
345
345
|
|
|
346
346
|
`plan-finalization.json.files` 必须完整列出 design、plan、implementation-detail、test-scenarios、gate-policy、worktree 六项标准输入。`verify` 对缺项、重复项、越界路径以及 symlink/junction/reparse point 一律 fail-closed;不得通过删减收据文件集后重算哈希来绕过完整性检查。
|
|
347
347
|
|
|
348
|
+
### 阶段 8 v2 路径(结构化证据包流程,新 change 优先)
|
|
349
|
+
|
|
350
|
+
阶段 8 v2 标准三步流(证据包自举,阶段 14 起可用):
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# 1) 规划自然产出 → 证据包(结构化身份/哈希全由冻结模块推导)
|
|
354
|
+
npx hunter-harness plan evidence-pack --input .harness/changes/<cn>/meta/plan-evidence-input.json \
|
|
355
|
+
--output .harness/changes/<cn>/meta/plan-evidence.json
|
|
356
|
+
# 2) 证据包 → 质量门 + 原子发布 + 事件 outbox
|
|
357
|
+
npx hunter-harness plan finalize --input .harness/changes/<cn>/meta/plan-evidence.json
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**自然输入文件**(`meta/plan-evidence-input.json`,权威定义 `packages/cli/src/commands/plan-evidence-pack.ts` 的 `EvidencePackInputFile`)由规划阶段逐步沉淀,各字段定稿时点不得倒置:
|
|
361
|
+
|
|
362
|
+
| 字段 | 内容 | 定稿阶段 |
|
|
363
|
+
|------|------|:---:|
|
|
364
|
+
| `change_key` / `risk_signals` | change 标识 + classify 风险信号(与 `harness_gate.py classify` 一致) | 0.5/0.6 |
|
|
365
|
+
| `intent` | source_input/goal/user_visible_outcome/in_scope/out_of_scope/constraints/acceptance_examples(2~5 条) | 2 |
|
|
366
|
+
| `evidence_sources` | 代码图/文件证据源(source_kind/source_id/version/content_hash + refs 列表) | 3 |
|
|
367
|
+
| `approval.content` | 审批包内容(recommended_design/invariants/failure_behaviors/risks/acceptance_examples(≥3) 等) | 4 |
|
|
368
|
+
| `approval.approver_id` / `decided_at` | **阶段 4 真实 blocking confirmation 的确认者与时间,不得伪造** | 4 |
|
|
369
|
+
| `structured_input.tasks` | task_id/objective/affected_paths/depends_on/owner_phase(refs 由命令接线,不写) | 5 |
|
|
370
|
+
| `structured_input.scenarios` | scenario_id/title/acceptance/coverage_dimension/execution_level/risk_level(八维度全覆盖,缺维度由命令记 not_applicable) | 7 |
|
|
371
|
+
| `structured_input.approved_scopes` | 批准边界文本列表(scope_ref 由命令按文本哈希派生) | 4 |
|
|
372
|
+
| `machine` | capabilities/worktree_policy | 6 |
|
|
373
|
+
| `context` | project_id/run_id/branch_name/attempt(复用 plan-run-id 与 attempt) | 0.5 |
|
|
374
|
+
| `expected_baseline` | 首次发布 `{state:"absent", manifest_hash:null, generation:0}` | 8 |
|
|
375
|
+
|
|
376
|
+
- **证据包**(`plan-evidence.json`)是命令推导的产物(trusted/publication/context/baseline),不得手改;任何字段变化必须改自然输入后重跑 evidence-pack。
|
|
377
|
+
- **成功语义**:finalize exit 0 且 `code:"PLAN_FINALIZED"`。落盘事实 = 八 target(plans/*.md ×4 + meta/*.json ×4)+ `meta/publication-journals/<op>.json`(状态 committed)+ `meta/plan-events.ndjson`(artifact_published/phase_ended)。确定性门失败 exit 1 且 `code:"PLAN_FINALIZE_DETERMINISTIC_FAILED"` 附 findings——此时必须回到对应阶段修正规划内容,**不得**手改证据包或 staged 内容绕过。
|
|
378
|
+
- **验证**:journal `state==="committed"` + 八 target 存在 + plan-events.ndjson 含两类终态事件;不得手工补写任何一项。
|
|
379
|
+
- **legacy 收据**:过渡期(阶段 14 前)v2 路径不写 `plan-finalization.json`;消费方若仍读 legacy receipt,由兼容投影单独提供,不得反向要求 v2 双写。
|
|
380
|
+
- **回退**:v2 自然输入不完整(如缺真实审批记录)时才允许走 Python finalizer(legacy 路径);同一次发布不得两条路径混用。
|
|
381
|
+
|
|
348
382
|
### Plan 结束行为规则
|
|
349
383
|
|
|
350
384
|
- **禁止询问执行模式**:Subagent-Driven / Inline Execution 属于 /harness-run 阶段
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-pull
|
|
3
|
+
description: 从 Hunter Platform 下拉配置/规则/架构/指令(及显式来源分支的分支文件恢复)。仅当用户显式调用
|
|
4
|
+
/harness-pull 或明确说'从平台拉取/恢复'时使用;不得自动触发。
|
|
5
|
+
---
|
|
6
|
+
<!-- generated by harness_deploy.py; core=e4b3e064899ba4af; overlay=none; agent=cursor; do not edit -->
|
|
7
|
+
# harness-pull — 从 Hunter Platform 下拉与恢复
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
从 Hunter Platform 下拉远端配置、规则、架构和指令,或按显式来源分支恢复分支受管文件。本 skill 只负责理解意图、展示预览和收集确认;冲突判断、哈希校验与本地事务由 CLI/core 完成。
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
仅当用户显式调用 `/harness-pull`(或明确说"从平台拉取/恢复")时执行。日常刷新元数据请走 `harness-sync`,不是本 skill。
|
|
16
|
+
|
|
17
|
+
## 前置条件
|
|
18
|
+
|
|
19
|
+
- 远端同步已配置:`HUNTER_REMOTE_SYNC_URL`、`HUNTER_REMOTE_SYNC_TOKEN`、`HUNTER_REMOTE_SYNC_ACTOR_ID`。缺失时 CLI 固定 fail closed(`PUSH_PULL_CLI_UNAVAILABLE`)。
|
|
20
|
+
- 分支文件恢复必须显式 `--scope branch_files --branch <来源分支>`;归档不出现在常规下拉范围(单个归档 ZIP 经 Platform 变更记录单独下载)。
|
|
21
|
+
|
|
22
|
+
## 交互流程
|
|
23
|
+
|
|
24
|
+
1. **选择范围**:常规范围 `config` / `rules` / `architecture` / `instructions`;分支恢复单独显式声明来源分支。
|
|
25
|
+
2. **展示预览**:来源版本、提交、缺失文件与冲突;默认只读预览,未经确认不写入本地。
|
|
26
|
+
3. **冲突决策**:本地有修改时逐项展示差异与决策(`keep-local` / `accept-remote` / `skip`);不得因关闭扫描直接覆盖本地有意修改。删除恢复只允许 `accept-remote` 并绑定 preview hash 与来源。
|
|
27
|
+
4. **报告结果**:应用、跳过、冲突遗留分列;冲突未决项保持本地原样。
|
|
28
|
+
|
|
29
|
+
## 命令
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
npx hunter-harness harness-pull --scope config,rules --json
|
|
33
|
+
npx hunter-harness harness-pull --scope branch_files --branch main --json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 关键规则
|
|
37
|
+
|
|
38
|
+
| 规则 | 要点 |
|
|
39
|
+
|------|------|
|
|
40
|
+
| 只读默认 | 未确认的预览不得写本地;`--dry-run` 不产生任何写入 |
|
|
41
|
+
| 本地修改保护 | 有本地修改时必须显示差异与决策,不静默覆盖 |
|
|
42
|
+
| 删除恢复 | 只接受 `accept-remote`,并绑定 preview hash、artifact、version 与来源分支 |
|
|
43
|
+
| 归档边界 | 归档不属于常规下拉;需要历史归档时提示用户走 Platform 变更记录下载 |
|
|
44
|
+
|
|
45
|
+
## P0 执行可信度规则
|
|
46
|
+
|
|
47
|
+
- 命令结果不得靠猜测;普通 Bash 被拒 → 立即改用等价 PowerShell 重试一次
|
|
48
|
+
- 仅 PowerShell 成功且有明确证据(构建/git/测试输出、文件存在、exit 0)时可标 ✅OK;否则 ❌FAIL 或 🟡WARN
|
|
49
|
+
- 禁止把 hook 拒绝、静态验证、无输出、用户跳过说成成功 → 详见 [[../protocols/powershell-protocol.md|powershell-protocol]]、[[../protocols/evidence-based-reporting-protocol.md|evidence-based-reporting-protocol]]
|
|
50
|
+
|
|
51
|
+
## 生成内容语言约定
|
|
52
|
+
|
|
53
|
+
- sync/ingest 等生成的文档、规则、知识条目、架构说明一律**优先使用中文**撰写(标识符、命令、代码、API 字段名保持原文)
|
|
54
|
+
- 面向平台展示的标题/摘要/正文默认中文;仅当用户明确要求或目标系统强制时才用英文
|
|
55
|
+
> 片段:p0-trust
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-push
|
|
3
|
+
description: 上传本地配置/规则/架构/指令(及显式归档)到 Hunter Platform。仅当用户显式调用 /harness-push
|
|
4
|
+
或明确说'上传到平台'时使用;不得因存在本地修改就自动触发。
|
|
5
|
+
---
|
|
6
|
+
<!-- generated by harness_deploy.py; core=e4b3e064899ba4af; overlay=none; agent=cursor; do not edit -->
|
|
7
|
+
# harness-push — 上传到 Hunter Platform
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
把本地受管内容(配置、规则、架构、指令,及显式选择的归档包)上传到 Hunter Platform。本 skill 只负责理解意图、展示预览和收集确认;三方 diff、敏感扫描、幂等与本地事务由 CLI/core 完成,不得绕过命令手工拼接上传。
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
仅当用户显式调用 `/harness-push`(或明确说"上传到 Hunter Platform")时执行。用户只是修改了配置/规则/指令时,**不得**自动建议上传。
|
|
16
|
+
|
|
17
|
+
## 前置条件
|
|
18
|
+
|
|
19
|
+
- 远端同步已配置:`HUNTER_REMOTE_SYNC_URL`、`HUNTER_REMOTE_SYNC_TOKEN`、`HUNTER_REMOTE_SYNC_ACTOR_ID`。缺失时 CLI 固定 fail closed(`PUSH_PULL_CLI_UNAVAILABLE`),**不得**改用旧 HTTP fallback 或手工 API 调用。
|
|
20
|
+
- 归档上传只选择已写入 outbox 的确定性包(阶段 06 产物);不存在可复用包时提示先完成本地归档,**不得**在 Push 内隐式补做 finalize/重建 ZIP。
|
|
21
|
+
|
|
22
|
+
## 交互流程
|
|
23
|
+
|
|
24
|
+
1. **选择范围**:`config` / `rules` / `architecture` / `instructions` / `all`,或显式归档 `--scope archive --change <change-key>`。归档必须显式,避免误传历史包或无法确定目标 Change。
|
|
25
|
+
2. **展示预览**:分支、提交、远端基线和实际变化文件;无变化时直接结束,不创建空版本。
|
|
26
|
+
3. **用户确认**:可覆盖风险(hard 敏感阻断除外)必须显式确认后才上传;确认只对本次 preview 有效。
|
|
27
|
+
4. **按内容分类报告**:成功、跳过、冲突和可重试项分开列出;冲突只投影机器决策,可附 `--resolve <path=keep-local|accept-remote|skip>` 逐项选择。
|
|
28
|
+
|
|
29
|
+
## 命令
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
npx hunter-harness harness-push --scope config,rules --json
|
|
33
|
+
npx hunter-harness harness-push --scope archive --change <change-key> --json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 关键规则
|
|
37
|
+
|
|
38
|
+
| 规则 | 要点 |
|
|
39
|
+
|------|------|
|
|
40
|
+
| 无变化不上传 | 预览无变化时返回 `no_changes`,不创建确认、不调用执行入口 |
|
|
41
|
+
| 敏感与凭据 | 不做内容遮盖;路径白名单、凭据文件排除、哈希校验保持启用;hard 敏感阻断不得生成确认 |
|
|
42
|
+
| 归档边界 | 归档不进普通范围;`--scope archive` 必须带 `--change`;只消费已有 outbox claim |
|
|
43
|
+
| 失败语义 | `PUSH_PULL_RECEIPT_INVALID`/远端未确认 = 未上传,不得宣称成功;可重试项标注后交还用户 |
|
|
44
|
+
|
|
45
|
+
## P0 执行可信度规则
|
|
46
|
+
|
|
47
|
+
- 命令结果不得靠猜测;普通 Bash 被拒 → 立即改用等价 PowerShell 重试一次
|
|
48
|
+
- 仅 PowerShell 成功且有明确证据(构建/git/测试输出、文件存在、exit 0)时可标 ✅OK;否则 ❌FAIL 或 🟡WARN
|
|
49
|
+
- 禁止把 hook 拒绝、静态验证、无输出、用户跳过说成成功 → 详见 [[../protocols/powershell-protocol.md|powershell-protocol]]、[[../protocols/evidence-based-reporting-protocol.md|evidence-based-reporting-protocol]]
|
|
50
|
+
|
|
51
|
+
## 生成内容语言约定
|
|
52
|
+
|
|
53
|
+
- sync/ingest 等生成的文档、规则、知识条目、架构说明一律**优先使用中文**撰写(标识符、命令、代码、API 字段名保持原文)
|
|
54
|
+
- 面向平台展示的标题/摘要/正文默认中文;仅当用户明确要求或目标系统强制时才用英文
|
|
55
|
+
> 片段:p0-trust
|
|
@@ -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=e4b3e064899ba4af; overlay=none; agent=cursor; 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=e4b3e064899ba4af; overlay=none; agent=cursor; do not edit -->
|
|
7
7
|
# harness-run — 需求编码
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -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=e4b3e064899ba4af; overlay=none; agent=cursor; 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=e4b3e064899ba4af; overlay=none; agent=cursor; 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=e4b3e064899ba4af; overlay=none; agent=cursor; do not edit -->
|
|
7
7
|
# harness-test — 测试执行
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -533,6 +533,187 @@ def run_id_for(change_dir: Path) -> str:
|
|
|
533
533
|
return "run_" + digest
|
|
534
534
|
|
|
535
535
|
|
|
536
|
+
PLAN_EVENTS_STREAM_NAME = "plan-events.ndjson"
|
|
537
|
+
PLAN_EVENTS_CURSOR_NAME = "plan-events-sync-cursor.json"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _plan_events_path(change_dir: Path) -> Path:
|
|
541
|
+
return _state_dir(change_dir) / "meta" / PLAN_EVENTS_STREAM_NAME
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _plan_events_cursor_path(change_dir: Path) -> Path:
|
|
545
|
+
return _state_dir(change_dir) / "meta" / PLAN_EVENTS_CURSOR_NAME
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _load_plan_cursor(change_dir: Path, *, initial_base: int) -> tuple[int, int]:
|
|
549
|
+
path = _plan_events_cursor_path(change_dir)
|
|
550
|
+
if not path.is_file():
|
|
551
|
+
return 0, initial_base
|
|
552
|
+
try:
|
|
553
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
554
|
+
except (OSError, json.JSONDecodeError):
|
|
555
|
+
return 0, initial_base
|
|
556
|
+
acked = data.get("acked_lines") if isinstance(data, dict) else None
|
|
557
|
+
base = data.get("producer_seq_base") if isinstance(data, dict) else None
|
|
558
|
+
if not isinstance(acked, int) or isinstance(acked, bool) or acked < 0:
|
|
559
|
+
acked = 0
|
|
560
|
+
if not isinstance(base, int) or isinstance(base, bool) or base < 0:
|
|
561
|
+
base = initial_base
|
|
562
|
+
return acked, base
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _save_plan_cursor(change_dir: Path, *, acked: int, base: int) -> None:
|
|
566
|
+
path = _plan_events_cursor_path(change_dir)
|
|
567
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
568
|
+
payload = {
|
|
569
|
+
"schema_version": 1,
|
|
570
|
+
"acked_lines": acked,
|
|
571
|
+
"producer_seq_base": base,
|
|
572
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
573
|
+
}
|
|
574
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
575
|
+
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
576
|
+
os.replace(tmp, path)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _sync_plan_events_stream(
|
|
580
|
+
change_dir: Path,
|
|
581
|
+
*,
|
|
582
|
+
endpoint: dict[str, str],
|
|
583
|
+
rid: str,
|
|
584
|
+
resolved_key: str,
|
|
585
|
+
display_title: str,
|
|
586
|
+
main_stream_total_lines: int,
|
|
587
|
+
) -> dict[str, Any]:
|
|
588
|
+
"""Upload the TS PlanEvent stream (meta/plan-events.ndjson) with its own ACK cursor.
|
|
589
|
+
|
|
590
|
+
producer_seq base starts after the Python stream's total line count, so the two
|
|
591
|
+
streams never collide in (run_id, producer_seq) space. 事件语义与转换边界见
|
|
592
|
+
12-M3 文档:TS 事件按 hunter-progress-sync/v1 线上格式投影,event_type 保留
|
|
593
|
+
TS 原值(artifact_published 等)。
|
|
594
|
+
"""
|
|
595
|
+
project_id = endpoint["project_id"]
|
|
596
|
+
plan_file = _plan_events_path(change_dir)
|
|
597
|
+
if not plan_file.is_file():
|
|
598
|
+
return {"ok": True, "uploaded": 0, "quarantined": 0, "reason": "no plan-events.ndjson"}
|
|
599
|
+
lines = _event_lines_snapshot(plan_file)
|
|
600
|
+
acked, base = _load_plan_cursor(change_dir, initial_base=main_stream_total_lines)
|
|
601
|
+
if acked > len(lines):
|
|
602
|
+
# 文件被截断重建:从头重放(event_id 服务端幂等去重)
|
|
603
|
+
acked = 0
|
|
604
|
+
pending = lines[acked:]
|
|
605
|
+
uploaded = 0
|
|
606
|
+
quarantined = 0
|
|
607
|
+
client_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
608
|
+
for chunk_start in range(0, len(pending), MAX_BATCH_SIZE):
|
|
609
|
+
raw_chunk = pending[chunk_start:chunk_start + MAX_BATCH_SIZE]
|
|
610
|
+
batch: list[dict[str, Any]] = []
|
|
611
|
+
line_by_event_id: dict[str, int] = {}
|
|
612
|
+
local_invalid: list[tuple[str, int, str]] = []
|
|
613
|
+
for offset, raw_line in enumerate(raw_chunk, start=1):
|
|
614
|
+
line_number = acked + chunk_start + offset
|
|
615
|
+
line = raw_line.strip()
|
|
616
|
+
if not line:
|
|
617
|
+
local_invalid.append((f"{resolved_key}:plan:{line_number}", line_number, "EMPTY_LINE"))
|
|
618
|
+
continue
|
|
619
|
+
try:
|
|
620
|
+
event = json.loads(line)
|
|
621
|
+
except json.JSONDecodeError:
|
|
622
|
+
local_invalid.append((f"{resolved_key}:plan:{line_number}", line_number, "INVALID_JSON"))
|
|
623
|
+
continue
|
|
624
|
+
if not isinstance(event, dict):
|
|
625
|
+
local_invalid.append((f"{resolved_key}:plan:{line_number}", line_number, "INVALID_EVENT"))
|
|
626
|
+
continue
|
|
627
|
+
event_id = str(event.get("event_id") or f"{resolved_key}:plan:{line_number}")
|
|
628
|
+
item: dict[str, Any] = {
|
|
629
|
+
"event_id": event_id,
|
|
630
|
+
"producer_seq": base + line_number,
|
|
631
|
+
"event_type": str(event.get("type") or "unknown"),
|
|
632
|
+
"occurred_at": str(event.get("occurred_at") or client_time),
|
|
633
|
+
"payload": sanitize(event),
|
|
634
|
+
}
|
|
635
|
+
if event.get("phase") is not None:
|
|
636
|
+
item["phase"] = event.get("phase")
|
|
637
|
+
batch.append(item)
|
|
638
|
+
line_by_event_id[event_id] = line_number
|
|
639
|
+
|
|
640
|
+
if batch:
|
|
641
|
+
try:
|
|
642
|
+
result = post_json(
|
|
643
|
+
endpoint,
|
|
644
|
+
f"/api/v1/projects/{project_id}/runs/events:batch",
|
|
645
|
+
{
|
|
646
|
+
"protocol_version": "hunter-progress-sync/v1",
|
|
647
|
+
"run_id": rid,
|
|
648
|
+
"change_key": resolved_key,
|
|
649
|
+
"title": display_title,
|
|
650
|
+
"events": batch,
|
|
651
|
+
},
|
|
652
|
+
)
|
|
653
|
+
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
|
|
654
|
+
return {
|
|
655
|
+
"ok": False,
|
|
656
|
+
"error": f"plan-events batch failed: {exc}",
|
|
657
|
+
"uploaded": uploaded,
|
|
658
|
+
"quarantined": quarantined,
|
|
659
|
+
}
|
|
660
|
+
items = result.get("items") if isinstance(result, dict) else None
|
|
661
|
+
if not isinstance(items, list) or len(items) != len(batch):
|
|
662
|
+
return {
|
|
663
|
+
"ok": False,
|
|
664
|
+
"error": "plan-events batch response did not acknowledge every event",
|
|
665
|
+
"uploaded": uploaded,
|
|
666
|
+
"quarantined": quarantined,
|
|
667
|
+
}
|
|
668
|
+
response_by_id = {
|
|
669
|
+
str(item.get("id") or item.get("event_id") or ""): item
|
|
670
|
+
for item in items if isinstance(item, dict)
|
|
671
|
+
}
|
|
672
|
+
for event in batch:
|
|
673
|
+
event_id = str(event["event_id"])
|
|
674
|
+
item = response_by_id.get(event_id)
|
|
675
|
+
status = str((item or {}).get("status") or "")
|
|
676
|
+
if status in ACCEPTED_STATUSES:
|
|
677
|
+
uploaded += 1
|
|
678
|
+
continue
|
|
679
|
+
if status in QUARANTINE_STATUSES:
|
|
680
|
+
append_quarantine(
|
|
681
|
+
change_dir,
|
|
682
|
+
event_id=event_id,
|
|
683
|
+
line_number=line_by_event_id[event_id],
|
|
684
|
+
error_code=str((item or {}).get("error_code") or status.upper()),
|
|
685
|
+
status=status,
|
|
686
|
+
)
|
|
687
|
+
quarantined += 1
|
|
688
|
+
continue
|
|
689
|
+
return {
|
|
690
|
+
"ok": False,
|
|
691
|
+
"error": f"plan event {event_id} was not durably acknowledged",
|
|
692
|
+
"uploaded": uploaded,
|
|
693
|
+
"quarantined": quarantined,
|
|
694
|
+
"server": result,
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
for event_id, line_number, error_code in local_invalid:
|
|
698
|
+
append_quarantine(
|
|
699
|
+
change_dir,
|
|
700
|
+
event_id=event_id,
|
|
701
|
+
line_number=line_number,
|
|
702
|
+
error_code=error_code,
|
|
703
|
+
status="local_invalid",
|
|
704
|
+
)
|
|
705
|
+
quarantined += 1
|
|
706
|
+
_save_plan_cursor(change_dir, acked=acked + chunk_start + len(raw_chunk), base=base)
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
"ok": True,
|
|
710
|
+
"uploaded": uploaded,
|
|
711
|
+
"quarantined": quarantined,
|
|
712
|
+
"acked_lines": len(lines),
|
|
713
|
+
"producer_seq_base": base,
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
|
|
536
717
|
def post_json(endpoint: dict[str, str], path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
537
718
|
url = endpoint["server_url"] + path
|
|
538
719
|
data = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
@@ -685,13 +866,26 @@ def sync_change(
|
|
|
685
866
|
_migrate_pending_legacy_stream(change_dir)
|
|
686
867
|
events_file = _state_dir(change_dir) / "events.ndjson"
|
|
687
868
|
if not events_file.is_file():
|
|
688
|
-
|
|
689
|
-
|
|
869
|
+
plan_result = _sync_plan_events_stream(
|
|
870
|
+
change_dir,
|
|
871
|
+
endpoint=endpoint,
|
|
872
|
+
rid=rid,
|
|
873
|
+
resolved_key=resolved_key,
|
|
874
|
+
display_title=display_title,
|
|
875
|
+
main_stream_total_lines=0,
|
|
876
|
+
)
|
|
877
|
+
result: dict[str, Any] = {
|
|
878
|
+
"ok": bool(plan_result.get("ok")),
|
|
690
879
|
"run_id": rid,
|
|
691
880
|
"change_key": resolved_key,
|
|
692
|
-
"uploaded": 0,
|
|
881
|
+
"uploaded": int(plan_result.get("uploaded") or 0),
|
|
882
|
+
"quarantined": int(plan_result.get("quarantined") or 0),
|
|
693
883
|
"reason": "no events.ndjson",
|
|
884
|
+
"plan_events": {key: value for key, value in plan_result.items() if key != "ok"},
|
|
694
885
|
}
|
|
886
|
+
if not plan_result.get("ok"):
|
|
887
|
+
result["error"] = plan_result.get("error")
|
|
888
|
+
return result
|
|
695
889
|
|
|
696
890
|
lines = _event_lines_snapshot(events_file)
|
|
697
891
|
cursor = load_cursor_state(
|
|
@@ -830,7 +1024,7 @@ def sync_change(
|
|
|
830
1024
|
producer_seq_base=producer_seq_base,
|
|
831
1025
|
)
|
|
832
1026
|
|
|
833
|
-
|
|
1027
|
+
main_result: dict[str, Any] = {
|
|
834
1028
|
"ok": True,
|
|
835
1029
|
"run_id": rid,
|
|
836
1030
|
"change_key": resolved_key,
|
|
@@ -841,6 +1035,21 @@ def sync_change(
|
|
|
841
1035
|
"producer_seq_base": producer_seq_base,
|
|
842
1036
|
"server": last_result,
|
|
843
1037
|
}
|
|
1038
|
+
plan_result = _sync_plan_events_stream(
|
|
1039
|
+
change_dir,
|
|
1040
|
+
endpoint=endpoint,
|
|
1041
|
+
rid=rid,
|
|
1042
|
+
resolved_key=resolved_key,
|
|
1043
|
+
display_title=display_title,
|
|
1044
|
+
main_stream_total_lines=len(lines),
|
|
1045
|
+
)
|
|
1046
|
+
main_result["uploaded"] += int(plan_result.get("uploaded") or 0)
|
|
1047
|
+
main_result["quarantined"] += int(plan_result.get("quarantined") or 0)
|
|
1048
|
+
main_result["plan_events"] = {key: value for key, value in plan_result.items() if key != "ok"}
|
|
1049
|
+
if not plan_result.get("ok"):
|
|
1050
|
+
main_result["ok"] = False
|
|
1051
|
+
main_result["error"] = plan_result.get("error")
|
|
1052
|
+
return main_result
|
|
844
1053
|
|
|
845
1054
|
|
|
846
1055
|
def auto_events_sync(
|
|
@@ -796,6 +796,121 @@ def _receipt_artifact_targets(
|
|
|
796
796
|
return targets, None
|
|
797
797
|
|
|
798
798
|
|
|
799
|
+
def _verify_plan_v2(change_dir: Path) -> dict[str, Any] | None:
|
|
800
|
+
"""v2 finalizer(TS)证据的只读验收。
|
|
801
|
+
|
|
802
|
+
v2 不产生 meta/plan-finalization.json;canonical 证据是
|
|
803
|
+
meta/plan-finalization-transactions/ + meta/publication-journals/。
|
|
804
|
+
本路径只做结构验收:最新事务终态、匹配 journal 的 committed/readback、
|
|
805
|
+
durable targets 存在且字节哈希与 binding 一致。内容质量由 TS 质量层裁决,
|
|
806
|
+
Python 不重复审判。无 v2 证据时返回 None(调用方回退 RECEIPT_MISSING)。
|
|
807
|
+
"""
|
|
808
|
+
transactions_dir = change_dir / "meta" / "plan-finalization-transactions"
|
|
809
|
+
if not transactions_dir.is_dir():
|
|
810
|
+
return None
|
|
811
|
+
transactions = sorted(
|
|
812
|
+
transactions_dir.glob("*.json"),
|
|
813
|
+
key=lambda path: path.stat().st_mtime,
|
|
814
|
+
)
|
|
815
|
+
if not transactions:
|
|
816
|
+
return None
|
|
817
|
+
latest: dict[str, Any] | None = None
|
|
818
|
+
for candidate in reversed(transactions):
|
|
819
|
+
try:
|
|
820
|
+
payload = json.loads(candidate.read_text(encoding="utf-8-sig"))
|
|
821
|
+
except (OSError, json.JSONDecodeError):
|
|
822
|
+
continue
|
|
823
|
+
if isinstance(payload, dict):
|
|
824
|
+
latest = payload
|
|
825
|
+
break
|
|
826
|
+
if latest is None:
|
|
827
|
+
return None
|
|
828
|
+
if latest.get("change_key") != change_dir.name:
|
|
829
|
+
return _result_error(
|
|
830
|
+
"RECEIPT_CHANGE_NAME_INVALID",
|
|
831
|
+
"v2 transaction change_key does not match the change directory",
|
|
832
|
+
)
|
|
833
|
+
if latest.get("status") != "publication_committed_event_complete":
|
|
834
|
+
return _result_error(
|
|
835
|
+
"RECEIPT_NOT_FINALIZED",
|
|
836
|
+
f"v2 transaction status is {latest.get('status')!r}, "
|
|
837
|
+
"expected 'publication_committed_event_complete'",
|
|
838
|
+
)
|
|
839
|
+
operation_id = str(latest.get("operation_id") or "")
|
|
840
|
+
journals_dir = change_dir / "meta" / "publication-journals"
|
|
841
|
+
journal: dict[str, Any] | None = None
|
|
842
|
+
if journals_dir.is_dir():
|
|
843
|
+
for candidate in sorted(journals_dir.glob("*.json")):
|
|
844
|
+
try:
|
|
845
|
+
payload = json.loads(candidate.read_text(encoding="utf-8-sig"))
|
|
846
|
+
except (OSError, json.JSONDecodeError):
|
|
847
|
+
continue
|
|
848
|
+
if isinstance(payload, dict) and payload.get("operation_id") == operation_id:
|
|
849
|
+
journal = payload
|
|
850
|
+
break
|
|
851
|
+
if journal is None:
|
|
852
|
+
return _result_error(
|
|
853
|
+
"PUBLICATION_JOURNAL_MISSING",
|
|
854
|
+
f"no publication journal matches v2 operation {operation_id!r}",
|
|
855
|
+
)
|
|
856
|
+
if journal.get("state") != "committed" or journal.get("readback") != "verified":
|
|
857
|
+
return _result_error(
|
|
858
|
+
"PUBLICATION_JOURNAL_NOT_COMMITTED",
|
|
859
|
+
"v2 publication journal is not committed with verified readback",
|
|
860
|
+
)
|
|
861
|
+
binding = journal.get("binding")
|
|
862
|
+
if not isinstance(binding, dict):
|
|
863
|
+
return _result_error("PUBLICATION_BINDING_INVALID", "v2 journal binding missing")
|
|
864
|
+
payload_hashes = binding.get("expected_payload_hashes")
|
|
865
|
+
ownership_paths = binding.get("ownership_paths")
|
|
866
|
+
if not isinstance(payload_hashes, dict) or not isinstance(ownership_paths, list):
|
|
867
|
+
return _result_error("PUBLICATION_BINDING_INVALID", "v2 binding fields malformed")
|
|
868
|
+
required = {
|
|
869
|
+
f"plans/{change_dir.name}-design.md",
|
|
870
|
+
f"plans/{change_dir.name}-plan.md",
|
|
871
|
+
f"plans/{change_dir.name}-implementation-detail.md",
|
|
872
|
+
f"plans/{change_dir.name}-test-scenarios.md",
|
|
873
|
+
"meta/gate-policy.json",
|
|
874
|
+
"meta/worktree.json",
|
|
875
|
+
}
|
|
876
|
+
missing_required = sorted(required - set(ownership_paths))
|
|
877
|
+
if missing_required:
|
|
878
|
+
return _result_error(
|
|
879
|
+
"RECEIPT_FILES_INCOMPLETE",
|
|
880
|
+
"v2 journal omits required artifacts: " + ", ".join(missing_required),
|
|
881
|
+
)
|
|
882
|
+
for rel, expected_hash in payload_hashes.items():
|
|
883
|
+
if not isinstance(expected_hash, str) or not expected_hash.startswith("sha256:"):
|
|
884
|
+
return _result_error("PUBLICATION_BINDING_INVALID", f"{rel}: malformed hash")
|
|
885
|
+
target = change_dir / rel
|
|
886
|
+
try:
|
|
887
|
+
resolved = target.resolve()
|
|
888
|
+
resolved.relative_to(change_dir.resolve())
|
|
889
|
+
except (OSError, ValueError):
|
|
890
|
+
return _result_error(
|
|
891
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
892
|
+
f"v2 artifact path resolves outside the change directory: {rel}",
|
|
893
|
+
)
|
|
894
|
+
if not target.is_file():
|
|
895
|
+
return _result_error("ARTIFACT_MISSING", f"published artifact missing: {rel}")
|
|
896
|
+
actual_hash = "sha256:" + hashlib.sha256(target.read_bytes()).hexdigest()
|
|
897
|
+
if actual_hash != expected_hash:
|
|
898
|
+
return _result_error(
|
|
899
|
+
"ARTIFACT_HASH_DRIFT",
|
|
900
|
+
f"{rel}: content hash {actual_hash} != v2 binding {expected_hash}",
|
|
901
|
+
)
|
|
902
|
+
return {
|
|
903
|
+
"ok": True,
|
|
904
|
+
"code": "PLAN_V2_VERIFIED",
|
|
905
|
+
"v2": True,
|
|
906
|
+
"operationId": operation_id,
|
|
907
|
+
"runId": latest.get("run_id"),
|
|
908
|
+
"attempt": latest.get("attempt"),
|
|
909
|
+
"artifactCount": len(payload_hashes),
|
|
910
|
+
"status": latest.get("status"),
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
|
|
799
914
|
def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
800
915
|
"""Read-only verification of a finalized plan (retro §5.8).
|
|
801
916
|
|
|
@@ -807,6 +922,10 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
807
922
|
change_dir = change_dir.resolve()
|
|
808
923
|
receipt_path = change_dir / "meta" / "plan-finalization.json"
|
|
809
924
|
if not receipt_path.is_file():
|
|
925
|
+
# v2 finalizer 证据路径(canonical 事实源是 transactions + journals)
|
|
926
|
+
v2_result = _verify_plan_v2(change_dir)
|
|
927
|
+
if v2_result is not None:
|
|
928
|
+
return v2_result
|
|
810
929
|
return _result_error("RECEIPT_MISSING", f"receipt not found: {receipt_path}")
|
|
811
930
|
|
|
812
931
|
try:
|