@cyning/harness 2.11.0 → 2.12.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.12.0] - 2026-07-26
8
+
9
+ ### Added
10
+
11
+ - **多帽 invoke 留档硬闸**:task 元信息 `required_invoke_hats` / `invoke_retention_profile`(`default`=`10,30,40` · `minimal`=`30` · `full`=`00,10,20,30,40,CLOSE`)。
12
+ - `task close` 检查 1 按集合覆盖校验;文件名 `30_40` 可同时满足 30 与 40;`--allow-invoke-gap` 豁免留痕。
13
+ - `verify --task` 对 invoke hats 缺口 **WARN**(不挡 30);`task lint` **W6** 提醒未设字段。
14
+ - `lib/task-meta.js`:`resolveRequiredInvokeHats` · `scanInvokeHats` · `evaluateInvokeHatsRetention` · `extractHatsFromInvokeFilename`。
15
+
16
+ ### Changed
17
+
18
+ - `lifecycle.yaml` close 守卫登记 `invoke_hats_retention`;`discipline-coverage.yaml` `as_of` → **2.12.0** · gaps `INVOKE-HATS`。
19
+ - TASK_TEMPLATE / TEMPLATE_invoke / 10·20·30 帽文同步字段与纪律。
20
+
21
+ ### Notes
22
+
23
+ - SPEC:`docs/spec/SPEC-invoke-hats-retention-gate_v1.md`
24
+ - **破坏性(close)**:无字段时按 default 要求 10+30+40;存量仅 30 的 active 须补档、改 `minimal` 或 `--allow-invoke-gap`
25
+ - minor · 建议业务仓 upgrade 后按 RUNBOOK 勾选 invoke 集合
26
+
27
+ ## [2.11.1] - 2026-07-25
28
+
29
+ ### Fixed
30
+
31
+ - **F1 · `upgrade` 自绊 S5(ops-desk-web dogfood)**:`local.json` 改为 **sync apply 成功后** 写入(compare-before-write)。避免 npx 缓存路径变化时先脏工作区再被 git-clean 中止。
32
+ - **F2 · manifest 静默丢字段**:`write_manifest_upgrade` 重写前对 schema 外字段与 `ide` 裁剪打 **WARN** 清单(知情);**仍不**合并保留(`additionalProperties: false`)。
33
+
34
+ ### Notes
35
+
36
+ - 摩擦报告:`Projects/docs/harness/guides/ANALYSIS_upgrade_wizard_friction_20260725_v1_zh.md`
37
+ - patch · 2.3+ manifest 仅五字段:`version` / `preset` / `ide` / `from_version` / `upgraded_at`(`name` / `tech_graph_dir` / `tasks_dir` / `hooks` 等已废弃)
38
+ - **已发布**:`@cyning/harness@2.11.1`(npm `latest` · 2026-07-26)· tag `v2.11.1` 已推送
39
+
7
40
  ## [2.11.0] - 2026-07-25
8
41
 
9
42
  ### Added
@@ -19,6 +52,7 @@
19
52
  - SPEC:`docs/spec/SPEC-lifecycle-guard-expand_v1.md` · `docs/spec/SPEC-discipline-show_v1.md`
20
53
  - minor · `lifecycle show` / verify 硬闸语义不变
21
54
  - `discipline-coverage.yaml` `as_of_package_version` → **2.11.0**
55
+ - **已发布**:`@cyning/harness@2.11.0`(npm `latest` · 2026-07-25)· tag `v2.11.0` 已推送
22
56
 
23
57
  ## [2.10.0] - 2026-07-25
24
58
 
@@ -68,5 +68,5 @@ CLI/Worker 与模板 **同一仓库** 演进,避免多仓割裂。
68
68
  Track / Hat / Sync / SDD 公理与扩展规则见 [`methodology/product/DESIGN_ONTOLOGY_v1_zh.md`](./methodology/product/DESIGN_ONTOLOGY_v1_zh.md)(**v1.3**)。
69
69
  **帽链流程(人读)**:[`methodology/product/SDD_HAT_FLOW_v2_zh.md`](./methodology/product/SDD_HAT_FLOW_v2_zh.md)
70
70
  **方法论总指引**:[`methodology/README.md`](./methodology/README.md) · 关系图 [`methodology/DOCUMENT_MAP_v1_zh.md`](./methodology/DOCUMENT_MAP_v1_zh.md)
71
- 远期 **过程实例图 + 事件历史** 见 [`methodology/graph/HARNESS_GRAPH_MODEL_design_v0_zh.md`](./methodology/graph/HARNESS_GRAPH_MODEL_design_v0_zh.md)(HGM · **v2.0+** 提案 · v1.0 后)。
71
+ 远期 **过程实例图 + 事件历史** 见 [`methodology/graph/HARNESS_GRAPH_MODEL_design_v0_zh.md`](./methodology/graph/HARNESS_GRAPH_MODEL_design_v0_zh.md)(**PIP(过程实例投影)** · **v2.0+** · v1.0 后)。
72
72
  与 **战略本体**(工作区 `docs/harness/guides/STRATEGY_ONTOLOGY_*`)分层:本文件描述 **产品如何实现**,战略文档描述 **对外演进**。
@@ -26,6 +26,10 @@ npx @cyning/harness check # 仅检查是否有新版本
26
26
  写入 `.cyning-harness/manifest.json`(钉版本 · preset · ide)与 `profile.json`。
27
27
  Schema:[`schema/manifest.v1.schema.json`](../schema/manifest.v1.schema.json)
28
28
 
29
+ **manifest(2.3+)仅五字段**:`version` / `preset` / `ide` / `from_version` / `upgraded_at`(`additionalProperties: false`)。
30
+ 旧字段 `name` / `harness_version` / `tech_graph_dir` / `tasks_dir` / `hooks` 已废弃且无消费方;`upgrade` 会整体重写并(v2.11.1+)对将被移除的字段打 WARN,**不会**合并保留。
31
+ `upgrade` 的 `local.json` 簿记在 sync apply **成功之后**写入(v2.11.1+),避免自绊 S5 git-clean。
32
+
29
33
  **维护者 / 离线 · clone 路径:**
30
34
 
31
35
  ```bash
@@ -185,9 +185,10 @@ Schema:[`schema/verify_result.v1.schema.json`](../schema/verify_result.v1.sche
185
185
  | 命令 | 用途 |
186
186
  | --- | --- |
187
187
  | `npx @cyning/harness init` | 首次安装模板与 manifest(可选 `--with-scripts`) |
188
- | `npx @cyning/harness upgrade` | 同步产品包更新(可加 `--gate-check` audit) |
188
+ | `npx @cyning/harness upgrade` | 同步产品包更新(可加 `--gate-check`);v2.11.1+:`local.json` apply 后写入;manifest 重写前 WARN 非标准字段(2.3+ 仅五字段) |
189
189
  | `npx @cyning/harness check` | 检查是否有新版本 |
190
- | `npx @cyning/harness verify` | 全量:双路径 + reviews(v2.9+);`--task`:30 前聚合;`--spec`:SPEC→00(v2.8+ · 互斥) |
190
+ | `npx @cyning/harness verify` | 全量:双路径 + reviews(v2.9+);`--task`:30 前聚合 + invoke hats WARN(v2.12+);`--spec`:SPEC→00(v2.8+ · 互斥) |
191
+ | `npx @cyning/harness task close` | 受闸归档;v2.12+ 按 `required_invoke_hats` / `invoke_retention_profile` 校验多帽 invoke(缺省 `10,30,40`;`--allow-invoke-gap`) |
191
192
  | `npx @cyning/harness lifecycle show` | 只读展示 `harness/lifecycle.yaml`(登记 · v2.7+) |
192
193
  | `npx @cyning/harness lifecycle dry-run` | 转移资格判定(引擎消费 yaml · v2.10+ · 旁路 · 非 G7) |
193
194
  | `npx @cyning/harness discipline show` | 只读展示 `harness/discipline-coverage.yaml`(v2.11+ · 非 audit UI) |
@@ -284,9 +285,9 @@ npm run harness -- check --target /tmp/foo
284
285
  | --- | --- | --- | --- |
285
286
  | **MD 人类轨** | `docs/_tech_graph/*.md` | 代码审阅、README 引用、Mermaid 渲染 | v1.0+ |
286
287
  | **YAML 编辑源** | `docs/_tech_graph/*.graph.yaml` | 结构化编辑、diff、CI 校验 | **v1.1+** |
287
- | **HGM 过程轨** | `.cyning-harness/events/*.jsonl` | Task / Gate / Review 实例与事件史 | **v2.0+** |
288
+ | **PIP(过程实例投影)过程轨** | `.cyning-harness/events/*.jsonl` | Task / Gate / Review 实例与事件史 | **v2.0+** |
288
289
 
289
- **原则**:YAML 为 Inform 编辑源,HGM 为过程事件图;YAML **不替代** task/review 真值,HGM **不替代** YAML/MD Inform 正文。
290
+ **原则**:YAML 为 Inform 编辑源,PIP(过程实例投影)为过程事件图;YAML **不替代** task/review 真值,PIP(过程实例投影)**不替代** YAML/MD Inform 正文。
290
291
 
291
292
  ### 10.2 最小工作流
292
293
 
@@ -327,9 +328,9 @@ npx @cyning/harness graph yaml check --all --input docs/_tech_graph
327
328
  | bench `100` | [SDD-Compliance](../examples/compliance_bench/README.md) 五场景合规率 · 见上文 §6.1 |
328
329
  | Extended 帽 | 00/50/链式 PROMPT 不在 Starter 默认包 · 见 [`harness/prompts/README.md`](../harness/prompts/README.md) |
329
330
  | Inform-YAML | **v1.1+** · 可选编辑源 · 须 `graph yaml check` 与 `graph.json` 一致 |
330
- | HGM / 图数据库 | **v2.0+ 已实现** `graph ingest|snapshot|axioms` · 本地 JSONL + snapshot;Neo4j / 远端同步 **仍提案** |
331
+ | PIP(过程实例投影)/ 图数据库 | **v2.0+ 已实现** `graph ingest|snapshot|axioms` · 本地 JSONL + snapshot;Neo4j / 远端同步 **仍提案** |
331
332
  | Agent-shell | 研究轨 #9,非 npm 功能 |
332
- | rejected→draft | **v2.0.1+** bench S5 + HGM axioms 事件流精确匹配 |
333
+ | rejected→draft | **v2.0.1+** bench S5 + PIP(过程实例投影)axioms 事件流精确匹配 |
333
334
 
334
335
  ---
335
336
 
@@ -350,21 +351,21 @@ A:不会(S2 域)。若 prompts 模板有更新,apply 会更新 **模板
350
351
  **Q:如何编辑 Inform 架构图?**
351
352
  A:v1.1+ 可选 `docs/_tech_graph/*.graph.yaml` 作为编辑源,运行 `npx @cyning/harness graph yaml compile|check` 生成 MD / 校验 graph.json。
352
353
 
353
- **Q:HGM Inform-YAML 是什么关系?**
354
- A:Inform-YAML 是 **架构图谱** 的编辑源;HGM(v2.0+)是 **过程协作** 的事件图。二者并列,HGM 通过 `InformArtifact` 节点引用 Inform 产物,但不覆盖其正文。
354
+ **Q:PIP(过程实例投影)与 Inform-YAML 是什么关系?**
355
+ A:Inform-YAML 是 **架构图谱** 的编辑源;PIP(过程实例投影,v2.0+)是 **过程协作** 的事件图。二者并列,PIP(过程实例投影)通过 `InformArtifact` 节点引用 Inform 产物,但不覆盖其正文。
355
356
 
356
357
  **Q:如何贡献或报 issue?**
357
358
  A:GitHub [Cyning12/cyning-harness](https://github.com/Cyning12/cyning-harness) · MIT。
358
359
 
359
360
  ---
360
361
 
361
- ## 13. HGM 过程轨(v2.0+)
362
+ ## 13. PIP(过程实例投影)过程轨(v2.0+)
362
363
 
363
- Harness Graph Model(HGM)把 task、gate、review、invoke、sync 等过程实例变成 **append-only 事件流** 与 **可重放图快照**,用于机械检查 SDD 公理。
364
+ PIP(过程实例投影;Process Instance Projection)把 task、gate、review、invoke、sync 等过程实例变成 **append-only 事件流** 与 **可重放图快照**,用于机械检查 SDD 公理。
364
365
 
365
366
  ### 13.1 事件文件
366
367
 
367
- HGM 事件写入 `.cyning-harness/events/YYYY-MM.jsonl`,修正 = 追加 `CorrectionEvent`,**禁止删改历史**。
368
+ PIP(过程实例投影) 事件写入 `.cyning-harness/events/YYYY-MM.jsonl`,修正 = 追加 `CorrectionEvent`,**禁止删改历史**。
368
369
 
369
370
  ### 13.2 CLI
370
371
 
@@ -397,7 +398,7 @@ chmod +x .git/hooks/pre-commit
397
398
 
398
399
  - `InformArtifact` 节点 ID:`inform:{repo_rel_path}`
399
400
  - Task → InformArtifact 边:`MUST_READ`
400
- - HGM **不存储** Inform 正文,只存路径指针与 schema 版本
401
+ - PIP(过程实例投影)**不存储** Inform 正文,只存路径指针与 schema 版本
401
402
 
402
403
  ### 13.5 局限
403
404
 
@@ -432,4 +433,4 @@ chmod +x .git/hooks/pre-commit
432
433
  | 2026-06-16 | v1.0.1:verify / gate-check / sync index CLI · `--with-scripts` · QUICKREF |
433
434
  | 2026-06-17 | v1.1.0:新增 §10 Inform-YAML · `graph yaml compile|check` · 三轨边界说明 |
434
435
  | 2026-06-17 | v2.0.1:D4-a axioms · rejected→draft 精确化 · S5 bench · optional pre-commit hook |
435
- | 2026-06-17 | v2.0.0:新增 §13 HGM 过程轨 · `graph ingest|snapshot|axioms` · InformArtifact 与 MUST_READ 边 |
436
+ | 2026-06-17 | v2.0.0:新增 §13 PIP(过程实例投影)过程轨 · `graph ingest|snapshot|axioms` · InformArtifact 与 MUST_READ 边 |
@@ -1,4 +1,6 @@
1
- # Harness Graph Model (HGM) · 设计草案 v0
1
+ # PIP(过程实例投影;Process Instance Projection)· 设计草案 v0
2
+
3
+ > 文件名历史:`HARNESS_GRAPH_MODEL_*`(路径兼容)。
2
4
 
3
5
 
4
6
  | 项 | 内容 |
@@ -20,7 +22,7 @@
20
22
  ### 0.1 一句话
21
23
 
22
24
  ```text
23
- Harness Graph Model (HGM)
25
+ PIP(过程实例投影)
24
26
  = OOP 的结构化对象(本体 Class 的实例)
25
27
  + 显式带类型的边(ObjectProperty · HatRef · 基数)
26
28
  + 不可变事件历史(append-only · 可重放投影)
@@ -1,6 +1,8 @@
1
- # graph · Harness Graph Model (HGM)
1
+ # graph · PIP(过程实例投影;Process Instance Projection)
2
2
 
3
- > **Track G · G1 / v2.0+**:HGM 为 **v1.0 公理稳定后的可选增强**(proposal · 未实现)· 详见 [`../ROADMAP_v1_zh.md`](../ROADMAP_v1_zh.md) §2.0 · §2.2。
3
+ > **对外名**:PIP(过程实例投影)。对内历史文件名可含 `HGM` / `hgm_*`(路径兼容,正文不用曾称括注)。
4
+
5
+ > **Track G · G1 / v2.0+**:PIP(过程实例投影)本地 JSONL + `graph ingest|snapshot|axioms` **已实现**;Neo4j / 远端同步仍提案 · 详见 [`../ROADMAP_v1_zh.md`](../ROADMAP_v1_zh.md) §2.0 · §2.2。
4
6
 
5
7
  | 文档 | 状态 | 说明 |
6
8
  | --- | --- | --- |
@@ -10,7 +12,7 @@
10
12
  | [`HARNESS_GRAPH_MODEL_dialogue_archive_v1_zh.md`](./HARNESS_GRAPH_MODEL_dialogue_archive_v1_zh.md) | archive | 对话参考 · **非真值** |
11
13
 
12
14
  ```text
13
- HGM = 结构化对象 + 显式带类型的边 + 不可变事件历史 + 可推理的公理
15
+ PIP(过程实例投影) = 结构化对象 + 显式带类型的边 + 不可变事件历史 + 可推理的公理
14
16
  ```
15
17
 
16
18
  **勿混淆**:
@@ -19,6 +21,6 @@ HGM = 结构化对象 + 显式带类型的边 + 不可变事件历史 + 可推
19
21
  | --- | --- |
20
22
  | **GraphTrack** | `docs/_tech_graph/` · Inform 架构地图 |
21
23
  | **gate-check --graph** | Inform 模块依赖图(Q3 · v1.0) |
22
- | **HGM** | Process 实例图 · Task / Gate / Review 时序 |
24
+ | **PIP(过程实例投影)** | Process 实例图 · Task / Gate / Review 时序 |
23
25
 
24
26
  **依赖**:[`../product/DESIGN_ONTOLOGY_v1_zh.md`](../product/DESIGN_ONTOLOGY_v1_zh.md) v1.2
@@ -0,0 +1,59 @@
1
+ # SPEC:多帽 invoke 留档硬闸(可配置)(v1)
2
+
3
+ > **状态**:`signed`
4
+ > **track**:`feature`
5
+ > **关联图谱**:无(纯 Harness 工具链)
6
+ > **下游**:30 实现 → 40 → close
7
+
8
+ ---
9
+
10
+ ## Harness 元信息
11
+
12
+ | 字段 | 值 |
13
+ |------|-----|
14
+ | **spec_slug** | `invoke-hats-retention-gate` |
15
+ | **test_strategy** | `required` |
16
+ | **test_strategy_note** | close/lint/verify 集合校验与 profile 展开须可失败测试驱动 |
17
+ | **skip_spec_audit** | `true` |
18
+
19
+ ---
20
+
21
+ ## 1. 背景与目标
22
+
23
+ close 检查 1 仅要求 `by-task/<slug>/` ≥1 个 `.md`,业务仓理性只交 30。
24
+ 目标:按 task 元信息 `required_invoke_hats` / `invoke_retention_profile` 做**集合覆盖**硬闸;默认 `10,30,40`;40 允许与 30 合并文件名。
25
+
26
+ ## 2. 范围
27
+
28
+ - `lib/task-meta.js`:profile 预置、解析、文件名 hat 扫描、缺口计算
29
+ - `lib/task-close.js`:检查 1 升级;`--allow-invoke-gap`
30
+ - `lib/task-lint.js`:缺字段 WARN
31
+ - `lib/verify.js`:缺口 WARN(不挡 30)
32
+ - lifecycle / discipline / 模板 / CHANGELOG · **v2.12.0**
33
+
34
+ ## 3. 非范围
35
+
36
+ - 强制 CLOSE invoke 文件(关账= close 命令)
37
+ - 默认要求 20 invoke(reviews 已硬闸)
38
+ - 同步 Ink 全套 TEMPLATE-*-invoke.md
39
+ - 历史 done invoke 补录
40
+
41
+ ## 4. 验收标准
42
+
43
+ - [x] 无字段 → 默认要求 `10,30,40`;仅有 30 → CLOSE BLOCKED
44
+ - [x] `invoke_retention_profile: minimal` → 仅需 30
45
+ - [x] `required_invoke_hats` 优先于 profile
46
+ - [x] `invoke_*_30_40_*.md` 同时满足 30 与 40
47
+ - [x] `--allow-invoke-gap` 豁免并留痕
48
+ - [x] verify --task 对缺口 WARN、不挡 may_start_30
49
+ - [x] `npm test` 全绿
50
+
51
+ > **复检**(2026-07-26 · cyning-harness `task/invoke-hats-retention-gate`):`npm test` **187/187** 绿;行为与上表一致。非阻塞备注见对话复检报告。
52
+
53
+ ## 5. 预置
54
+
55
+ | profile | hats |
56
+ |---------|------|
57
+ | default | 10,30,40 |
58
+ | minimal | 30 |
59
+ | full | 00,10,20,30,40,CLOSE |
@@ -6,7 +6,7 @@
6
6
  # 禁止:把本文件当「每周全量重盘」流程;不做 audit --discipline UI(本波)
7
7
 
8
8
  version: "1"
9
- as_of_package_version: "2.11.0"
9
+ as_of_package_version: "2.12.0"
10
10
  scope: "Starter · harness/prompts (10/22/30/40 + FRAGMENT)+ TEMPLATE_invoke"
11
11
  source_audit: "rethink/2026-07-mechanization-rate · 02 转录 + 03 聚合 · 状态已按 G1–G4/N1–N4 刷新(非第二轮通读盘点)"
12
12
 
@@ -61,6 +61,11 @@ gaps:
61
61
  status: closed
62
62
  closed_in: "2.10.0"
63
63
  note: "Epic T2 · 旁路报告"
64
+ - id: INVOKE-HATS
65
+ title: 多帽 invoke 留档集合闸
66
+ status: closed
67
+ closed_in: "2.12.0"
68
+ note: "close evaluateInvokeHatsRetention · default 10,30,40"
64
69
 
65
70
  statements:
66
71
  - id: A1
@@ -105,11 +110,11 @@ statements:
105
110
  mechanism_quality: null
106
111
  - id: A6
107
112
  source: 30-execute-code.md
108
- summary: invoke 快照落盘 by-task/<slug>/
113
+ summary: invoke 快照落盘 by-task/<slug>/(按 required_invoke_hats 集合)
109
114
  status: mechanical
110
- mechanism: "M5 close + M6 lint-done"
115
+ mechanism: "M5 close evaluateInvokeHatsRetention + M6 lint-done"
111
116
  gap: null
112
- notes: null
117
+ notes: "v2.12.0 默认 10,30,40;profile minimal/full;--allow-invoke-gap"
113
118
  mechanism_quality: null
114
119
  - id: A7
115
120
  source: 30-execute-code.md
@@ -265,11 +270,11 @@ statements:
265
270
  mechanism_quality: null
266
271
  - id: F1
267
272
  source: TEMPLATE_invoke.md
268
- summary: 落盘路径 by-task/<task_slug>/
273
+ summary: 落盘路径 by-task/<task_slug>/ 且帽子集合覆盖
269
274
  status: mechanical
270
- mechanism: "M5/M6"
275
+ mechanism: "M5 evaluateInvokeHatsRetention / M6"
271
276
  gap: null
272
- notes: null
277
+ notes: "v2.12.0 文件名 hat token · 合并 30_40 可双计"
273
278
  mechanism_quality: null
274
279
  - id: F2
275
280
  source: TEMPLATE_invoke.md
@@ -9,7 +9,7 @@
9
9
 
10
10
  | 字段 | 值 |
11
11
  |------|-----|
12
- | hat_id | 10 / 22 / 30 / 40 / 50 / 00 / CLOSE |
12
+ | hat_id | 10 / 22 / 30 / 40 / 50 / 00 / CLOSE(合并可写 `30(含 40)`) |
13
13
  | task_slug | `<slug>` |
14
14
  | freeze_id | (可选) |
15
15
  | task_paths | `docs/tasks/active/task_….md` |
@@ -44,7 +44,8 @@
44
44
  | 同帽追问 | **不** 新增 invoke;沿用本节路径 |
45
45
  | 打回重开 | 新文件或 `_r2` 后缀 |
46
46
  | 与 reviews 分工 | reviews = 结论;invoke = **当时指令** |
47
- | close 机械闸 | `harness task close` 归档前校验 `by-task/<task_slug>/` 存在且含 ≥1 `.md`(v2.2+) |
47
+ | close 机械闸 | `harness task close` task 元信息 `required_invoke_hats` / `invoke_retention_profile` 校验帽集合覆盖(v2.12+;缺省 `10,30,40`;文件名含 `30_40` 可双计;`--allow-invoke-gap` 豁免) |
48
+ | 命名 | `invoke_YYYYMMDD_<hat>_<slug>.md` 或 `invoke_YYYYMMDD_30_40_<slug>.md` |
48
49
  | commit | 落盘 + task 回填后再 commit(见 HANDOFF) |
49
50
 
50
51
  ---
@@ -66,8 +66,14 @@ transitions:
66
66
  description: "task close · active→done 归档"
67
67
  guards:
68
68
  - id: close_invoke
69
- command_or_check: "task close 检查1 · invoke 快照存在"
69
+ command_or_check: "task close 检查1 · invoke hats 集合覆盖(required_invoke_hats / profile · v2.12+)"
70
70
  severity: block
71
+ allow_flag: "--allow-invoke-gap"
72
+ note: "缺省 default=10,30,40;minimal=30;full 含 CLOSE;40 可与 30 合并文件名"
73
+ - id: invoke_hats_retention
74
+ command_or_check: "evaluateInvokeHatsRetention · 同 close_invoke(登记别名)"
75
+ severity: block
76
+ allow_flag: "--allow-invoke-gap"
71
77
  - id: close_self_check
72
78
  command_or_check: "task close 检查2 · 自检结论已回填"
73
79
  severity: block
@@ -15,6 +15,7 @@
15
15
  - 补齐 **`failure_paths`**(触发 → 行为 → 可重试 → 用户可见)
16
16
  - 写清 **非范围**、**依赖**(相对路径链接)
17
17
  - 建议 `test_strategy` + `code_quality_bar`
18
+ - **选定 invoke 留档集合**(v2.12+):写 `invoke_retention_profile`(`default`=`10,30,40` / `minimal`=`30` / `full`)或显式 `required_invoke_hats`;并落盘本帽 `invoke_*_10_*.md`
18
19
  - 承接 **20-task-audit 审查**:按 `docs/harness/reviews/*_audit_*.md` 回填 task
19
20
 
20
21
  ## 禁止什么
@@ -48,7 +49,8 @@
48
49
  |------|------|
49
50
  | 2026-07-24 | V2 拆分收编:自 `10-requirements.md` 改名为 `10-task-requirements.md`(来源:工作区 `10-task-requirements.md` · 2026-06-12 v2) |
50
51
  | 2026-07-24 | G4:思考轮摘要节补 v2.6+ `task lint` 条件触发结构检查 |
52
+ | 2026-07-26 | v2.12:起草时选定 `required_invoke_hats` / `invoke_retention_profile` |
51
53
 
52
54
  ## 给 Cursor
53
55
 
54
- `Harness`、`10-task`、`验收`、`failure_paths`、`test_strategy`、`task lint`、`拒开工`
56
+ `Harness`、`10-task`、`验收`、`failure_paths`、`test_strategy`、`task lint`、`required_invoke_hats`、`拒开工`
@@ -62,7 +62,8 @@
62
62
 
63
63
  ## 交接物
64
64
 
65
- - 审查 md 路径 + invoke 快照(可选)
65
+ - 审查 md 路径(**必须** · `docs/harness/reviews/`)
66
+ - invoke 快照:仅当 task `required_invoke_hats` / profile **含 20** 时必须落盘;默认 default 集合不含 20(与 reviews 硬闸分工)
66
67
  - 按 HANDOFF 分仓 commit
67
68
 
68
69
  ## 修订记录
@@ -70,6 +71,7 @@
70
71
  | 日期 | 摘要 |
71
72
  |------|------|
72
73
  | 2026-07-24 | V2 拆分收编:自 `22-task-audit.md` 改名为 `20-task-audit.md`(来源:工作区 `20-task-audit.md` · 2026-06-21 v3 + 原包内人工闸联动节保留) |
74
+ | 2026-07-26 | v2.12:澄清 20 invoke 仅在 required 集合含 20 时强制;默认靠 reviews |
73
75
 
74
76
  ## 给 Cursor
75
77
 
@@ -20,8 +20,8 @@
20
20
  - 读 task **必读列表** + `AGENTS.md` + `_tech_graph/` + L2(涉码)
21
21
  - `test_strategy: required` → **先** 可失败测试再改实现
22
22
  - 闸扫描通过后:运行 task **验证命令**;回填 `### 自检结论(执行者)`
23
- - invoke 快照落盘 `docs/harness/invokes/by-task/<task_slug>/`
24
- - 归档(active→done)**只能**走 `npx @cyning/harness task close --file <task> --yes`:`CLOSE: PASS` 后方可归档;机械校验 invoke 留档 / 自检结论 / 验收勾选 / slug / 状态,任一不过 **不执行** mv(v2.2+)
23
+ - invoke 快照落盘 `docs/harness/invokes/by-task/<task_slug>/`(须覆盖 task 要求的 hats;40 可与 30 合并为 `invoke_*_30_40_*.md`)
24
+ - 归档(active→done)**只能**走 `npx @cyning/harness task close --file <task> --yes`:`CLOSE: PASS` 后方可归档;机械校验 **invoke hats 集合** / 自检结论 / 验收勾选 / slug / 状态 / R1 review,任一不过 **不执行** mv(v2.2+ · hats 集合 v2.12+)
25
25
 
26
26
  ## 禁止什么
27
27
 
@@ -19,6 +19,8 @@
19
19
  | **chain_prompt** | `docs/harness/prompts/PROMPT_cursor_task_chain_serial_v1.md`(嵌入后路径) |
20
20
  | **semi_auto** | **`false`**(禁止作总闸) |
21
21
  | **audit_profile** | `full` / `post_close` / `human_only` |
22
+ | **invoke_retention_profile** | `default` / `minimal` / `full`(10 起草时选定) |
23
+ | **required_invoke_hats** | `10,30,40`(显式列表优先于 profile;缺省=default) |
22
24
  | **git_branch** | `task/<slug>` |
23
25
  | **worktree_root** | (并行时填独立 worktree 目录) |
24
26
  | **experience_capture** | `required` / `recommended` / `not_applicable` |
package/lib/cli.js CHANGED
@@ -404,11 +404,11 @@ async function cmdVerify(args) {
404
404
  formatAgentHint,
405
405
  } = await import('./verify.js');
406
406
  if (args.includes('--help') || args.includes('-h')) {
407
- console.log(`用法: npx @cyning/harness verify [--target PATH] [--task FILE | --spec FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH] [--allow-no-review] [--allow-lint-fail] [--allow-no-spec-review]
407
+ console.log(`用法: npx @cyning/harness verify [--target PATH] [--task FILE | --spec FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH] [--allow-no-review] [--allow-lint-fail] [--allow-no-spec-review] [--allow-invoke-gap]
408
408
 
409
409
  无 --task/--spec:扫描 docs/tasks/active ∪ docs/harness/tasks/active(同 basename 优先 Starter);
410
410
  闸表 + 全量 reviews 存在性(v2.9+)· 不跑 task lint / D5 · --allow-no-review。
411
- --task:30 前聚合(gate + audit + reviews + lint WARN)· --allow-no-review / --allow-lint-fail。
411
+ --task:30 前聚合(gate + audit + reviews + lint WARN + invoke hats WARN)· --allow-no-review / --allow-lint-fail / --allow-invoke-gap
412
412
  --spec:SPEC→00 前查审查文存在性(v2.8+ · 与 --task 互斥)· --allow-no-spec-review。
413
413
  推荐:--spec <产品仓 SPEC> --workspace-root <含 docs/harness/reviews 的工作区根>
414
414
  --json / --agent-hint:Agent handoff。
@@ -431,6 +431,7 @@ async function cmdVerify(args) {
431
431
  const allowNoReview = rest.includes('--allow-no-review');
432
432
  const allowLintFail = rest.includes('--allow-lint-fail');
433
433
  const allowNoSpecReview = rest.includes('--allow-no-spec-review');
434
+ const allowInvokeGap = rest.includes('--allow-invoke-gap');
434
435
  rest = rest.filter(
435
436
  (a) =>
436
437
  a !== '--graph' &&
@@ -438,7 +439,8 @@ async function cmdVerify(args) {
438
439
  a !== '--agent-hint' &&
439
440
  a !== '--allow-no-review' &&
440
441
  a !== '--allow-lint-fail' &&
441
- a !== '--allow-no-spec-review',
442
+ a !== '--allow-no-spec-review' &&
443
+ a !== '--allow-invoke-gap',
442
444
  );
443
445
 
444
446
  if (rest.length > 0) {
@@ -469,7 +471,13 @@ async function cmdVerify(args) {
469
471
  });
470
472
  label = path.basename(specFile);
471
473
  } else {
472
- result = verifyTarget(target, { taskFile, graph, allowNoReview, allowLintFail });
474
+ result = verifyTarget(target, {
475
+ taskFile,
476
+ graph,
477
+ allowNoReview,
478
+ allowLintFail,
479
+ allowInvokeGap,
480
+ });
473
481
  handoff = buildVerifyHandoff(target, {
474
482
  taskFile,
475
483
  workspaceRoot: ws,
@@ -709,13 +717,16 @@ async function cmdTask(args) {
709
717
 
710
718
  async function cmdTaskClose(args) {
711
719
  if (args.includes('--help') || args.includes('-h')) {
712
- console.log(`用法: npx @cyning/harness task close --file PATH [--target PATH] [--yes] [--allow-unchecked]
720
+ console.log(`用法: npx @cyning/harness task close --file PATH [--target PATH] [--yes] [--allow-unchecked] [--allow-no-review] [--allow-invoke-gap]
713
721
 
714
- 受闸归档(方案 A · invoke 留档机械闸):
715
- 5 项机械校验:invoke 留档 · 自检结论回填 · 验收标准勾选 · task_slug 一致 · 状态 done/completed
722
+ 受闸归档(方案 A · invoke 留档机械闸 · v2.12+ 多帽集合):
723
+ 机械校验:invoke hats 覆盖(required_invoke_hats / profile)· 自检结论 · 验收勾选 · slug · 状态 · R1 review
716
724
  默认 dry-run(只检不 mv);--yes 才执行 mv 到 active/ 同级 done/(basename 保留)
717
- --target PATH 显式覆盖归档目标完整路径
718
- --allow-unchecked 验收标准未勾选项 warn 放行
725
+ --target PATH 显式覆盖归档目标完整路径
726
+ --allow-unchecked 验收标准未勾选项 warn 放行
727
+ --allow-no-review 缺 R<n> 审查文 warn 放行
728
+ --allow-invoke-gap 缺 required invoke hats warn 放行(v2.12+)
729
+ 缺省 required hats:10,30,40(可用 invoke_retention_profile=minimal|full 或显式列表覆盖)
719
730
  任一校验失败 → exit 2 + CLOSE: BLOCKED · 不执行 mv。末行协议:CLOSE: PASS · <slug>。
720
731
  `);
721
732
  return;
@@ -724,7 +735,14 @@ async function cmdTaskClose(args) {
724
735
  const yes = args.includes('--yes');
725
736
  const allowUnchecked = args.includes('--allow-unchecked');
726
737
  const allowNoReview = args.includes('--allow-no-review');
727
- let rest = args.filter((a) => a !== '--yes' && a !== '--allow-unchecked' && a !== '--allow-no-review');
738
+ const allowInvokeGap = args.includes('--allow-invoke-gap');
739
+ let rest = args.filter(
740
+ (a) =>
741
+ a !== '--yes' &&
742
+ a !== '--allow-unchecked' &&
743
+ a !== '--allow-no-review' &&
744
+ a !== '--allow-invoke-gap',
745
+ );
728
746
  const { value: fileArg, rest: r1 } = takeOption(rest, '--file');
729
747
  rest = r1;
730
748
  const { value: targetArg, rest: r2 } = takeOption(rest, '--target');
@@ -746,6 +764,7 @@ async function cmdTaskClose(args) {
746
764
  target: targetArg,
747
765
  allowUnchecked,
748
766
  allowNoReview,
767
+ allowInvokeGap,
749
768
  cwd: process.cwd(),
750
769
  });
751
770
 
package/lib/task-close.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  PLACEHOLDER_RE,
6
6
  STATUS_RE,
7
7
  UNCHECKED_RE,
8
+ evaluateInvokeHatsRetention,
8
9
  extractSection,
9
10
  extractTaskSlug,
10
11
  findReview,
@@ -23,6 +24,7 @@ export function closeTaskFile(filePath, options = {}) {
23
24
  target: targetArg,
24
25
  allowUnchecked = false,
25
26
  allowNoReview = false,
27
+ allowInvokeGap = false,
26
28
  cwd = process.cwd(),
27
29
  } = options;
28
30
 
@@ -60,7 +62,8 @@ export function closeTaskFile(filePath, options = {}) {
60
62
  const inActive = path.basename(activeDir) === 'active';
61
63
  const repoRoot = findRepoRoot(activeDir);
62
64
 
63
- // 检查 1 · invoke 留档
65
+ // 检查 1 · invoke 留档(v2.12+:按 required_invoke_hats / profile 集合覆盖)
66
+ let invokeHatsEval = null;
64
67
  if (!repoRoot) {
65
68
  blockers.push(`无法定位 repo root(${activeDir} 无 docs 祖先)`);
66
69
  } else if (slug) {
@@ -69,12 +72,29 @@ export function closeTaskFile(filePath, options = {}) {
69
72
  'docs/harness/invokes/by-task',
70
73
  slug,
71
74
  );
72
- const hasMd =
73
- fs.existsSync(invokeDir) &&
74
- fs.readdirSync(invokeDir).some((f) => f.endsWith('.md'));
75
- if (!hasMd) {
76
- blockers.push(
77
- `missing invoke snapshots: ${path.relative(repoRoot, invokeDir)} 不存在或无 .md`,
75
+ invokeHatsEval = evaluateInvokeHatsRetention(meta, invokeDir);
76
+ const relDir = path.relative(repoRoot, invokeDir).replace(/\\/g, '/');
77
+ if (invokeHatsEval.missingDir || invokeHatsEval.files.length === 0) {
78
+ const msg = `missing invoke snapshots: ${relDir} 不存在或无 .md(要求 hats: ${invokeHatsEval.required.join(',')})`;
79
+ if (allowInvokeGap) {
80
+ warnings.push(`warn: ${msg}(--allow-invoke-gap 豁免 · 留痕)`);
81
+ } else {
82
+ blockers.push(msg);
83
+ }
84
+ } else if (invokeHatsEval.missing.length > 0) {
85
+ const msg =
86
+ `missing invoke hats: ${invokeHatsEval.missing.join(',')} ` +
87
+ `(要求 ${invokeHatsEval.required.join(',')} · 已有 ${invokeHatsEval.covered.join(',') || '∅'} · ` +
88
+ `source=${invokeHatsEval.resolution.source}` +
89
+ `${invokeHatsEval.resolution.unknown_profile ? ` · unknown_profile=${invokeHatsEval.resolution.unknown_profile}` : ''})`;
90
+ if (allowInvokeGap) {
91
+ warnings.push(`warn: ${msg}(--allow-invoke-gap 豁免 · 留痕)`);
92
+ } else {
93
+ blockers.push(msg);
94
+ }
95
+ } else if (invokeHatsEval.resolution.unknown_profile) {
96
+ warnings.push(
97
+ `warn: unknown invoke_retention_profile '${invokeHatsEval.resolution.unknown_profile}' · 已回退 default`,
78
98
  );
79
99
  }
80
100
  }
@@ -167,6 +187,14 @@ export function closeTaskFile(filePath, options = {}) {
167
187
  dest,
168
188
  dryRun: !yes,
169
189
  moved,
190
+ invoke_hats: invokeHatsEval
191
+ ? {
192
+ required: invokeHatsEval.required,
193
+ covered: invokeHatsEval.covered,
194
+ missing: invokeHatsEval.missing,
195
+ source: invokeHatsEval.resolution.source,
196
+ }
197
+ : null,
170
198
  };
171
199
  }
172
200
 
package/lib/task-lint.js CHANGED
@@ -193,6 +193,33 @@ export function lintTaskFile(filePath, options = {}) {
193
193
  warnings.push(warn('W2', '缺 ### 人工闸 节(轻量 task 可忽略本提醒)'));
194
194
  }
195
195
 
196
+ // W6 · invoke 留档集合字段(v2.12+ · 缺则 close 按 default=10,30,40)
197
+ if (
198
+ meta.task_slug &&
199
+ !meta.required_invoke_hats &&
200
+ !meta.invoke_retention_profile
201
+ ) {
202
+ warnings.push(
203
+ warn(
204
+ 'W6',
205
+ '未设 required_invoke_hats / invoke_retention_profile · close 将按 default=10,30,40(紧急 fix 可写 minimal)',
206
+ ),
207
+ );
208
+ } else if (
209
+ meta.invoke_retention_profile &&
210
+ !['default', 'minimal', 'full'].includes(
211
+ String(meta.invoke_retention_profile).trim().toLowerCase(),
212
+ ) &&
213
+ !meta.required_invoke_hats
214
+ ) {
215
+ warnings.push(
216
+ warn(
217
+ 'W6',
218
+ `未知 invoke_retention_profile '${meta.invoke_retention_profile}' · 将回退 default`,
219
+ ),
220
+ );
221
+ }
222
+
196
223
  // G4 · 思考轮结构(条件触发)
197
224
  lintThinkingRounds(content, lines, errors, warnings);
198
225
 
package/lib/task-meta.js CHANGED
@@ -396,6 +396,158 @@ export function normalizeSlug(slug) {
396
396
  return String(slug).replace(/_/g, '-');
397
397
  }
398
398
 
399
+ /**
400
+ * invoke 留档 profile → 帽集合(v2.12+)。
401
+ * default=10,30,40 · minimal=30 · full=00,10,20,30,40,CLOSE
402
+ */
403
+ export const INVOKE_RETENTION_PROFILES = Object.freeze({
404
+ default: Object.freeze(['10', '30', '40']),
405
+ minimal: Object.freeze(['30']),
406
+ full: Object.freeze(['00', '10', '20', '30', '40', 'CLOSE']),
407
+ });
408
+
409
+ /** 已知 hat token(文件名扫描白名单) */
410
+ export const KNOWN_INVOKE_HAT_TOKENS = Object.freeze(
411
+ new Set([
412
+ '00',
413
+ '10',
414
+ '20',
415
+ '22',
416
+ '30',
417
+ '40',
418
+ '50',
419
+ 'CLOSE',
420
+ 'close',
421
+ ]),
422
+ );
423
+
424
+ /**
425
+ * 解析 required_invoke_hats / invoke_retention_profile。
426
+ * 显式列表优先;皆无 → default。
427
+ * @returns {{ hats: string[], profile: string|null, source: 'explicit'|'profile'|'default', unknown_profile?: string }}
428
+ */
429
+ export function resolveRequiredInvokeHats(meta = {}) {
430
+ const explicit = meta.required_invoke_hats;
431
+ if (explicit != null && String(explicit).trim() !== '') {
432
+ const hats = parseHatList(explicit);
433
+ return { hats, profile: meta.invoke_retention_profile ?? null, source: 'explicit' };
434
+ }
435
+
436
+ const profileRaw = meta.invoke_retention_profile;
437
+ if (profileRaw != null && String(profileRaw).trim() !== '') {
438
+ const key = String(profileRaw).trim().toLowerCase();
439
+ const preset = INVOKE_RETENTION_PROFILES[key];
440
+ if (preset) {
441
+ return { hats: [...preset], profile: key, source: 'profile' };
442
+ }
443
+ return {
444
+ hats: [...INVOKE_RETENTION_PROFILES.default],
445
+ profile: key,
446
+ source: 'default',
447
+ unknown_profile: key,
448
+ };
449
+ }
450
+
451
+ return {
452
+ hats: [...INVOKE_RETENTION_PROFILES.default],
453
+ profile: 'default',
454
+ source: 'default',
455
+ };
456
+ }
457
+
458
+ /**
459
+ * 从 invoke 文件名提取 hat token 集合。
460
+ * 兼容:invoke_YYYYMMDD_30_slug.md · invoke_YYYYMMDD_30_40_slug.md · invoke_*_CLOSE_*.md
461
+ * 亦认 22→20(V2 改名等价,计为 20)。
462
+ */
463
+ export function extractHatsFromInvokeFilename(fileName) {
464
+ const base = path.basename(fileName, '.md');
465
+ const hats = new Set();
466
+
467
+ if (/CLOSE/i.test(base)) {
468
+ hats.add('CLOSE');
469
+ }
470
+
471
+ // 去 invoke_ 前缀与可选日期段
472
+ let rest = base.replace(/^invoke[_-]?/i, '');
473
+ rest = rest.replace(/^\d{8}[_-]?/, '');
474
+ rest = rest.replace(/^\d{4}-\d{2}-\d{2}[_-]?/, '');
475
+
476
+ const parts = rest.split(/[_-]+/).filter(Boolean);
477
+ for (const p of parts) {
478
+ const upper = p.toUpperCase();
479
+ if (upper === 'CLOSE') {
480
+ hats.add('CLOSE');
481
+ continue;
482
+ }
483
+ if (KNOWN_INVOKE_HAT_TOKENS.has(p)) {
484
+ hats.add(normalizeHatToken(p));
485
+ }
486
+ }
487
+
488
+ // V2:22 与 20 等价(审 task)
489
+ if (hats.has('22')) {
490
+ hats.add('20');
491
+ }
492
+
493
+ return hats;
494
+ }
495
+
496
+ /**
497
+ * 扫描 by-task/<slug>/ 目录,聚合已覆盖 hat。
498
+ * @returns {{ covered: Set<string>, files: string[], missingDir: boolean }}
499
+ */
500
+ export function scanInvokeHats(invokeDir) {
501
+ if (!invokeDir || !fs.existsSync(invokeDir)) {
502
+ return { covered: new Set(), files: [], missingDir: true };
503
+ }
504
+ const files = fs
505
+ .readdirSync(invokeDir)
506
+ .filter((f) => f.endsWith('.md'));
507
+ const covered = new Set();
508
+ for (const f of files) {
509
+ for (const h of extractHatsFromInvokeFilename(f)) {
510
+ covered.add(h);
511
+ }
512
+ }
513
+ return { covered, files, missingDir: false };
514
+ }
515
+
516
+ /**
517
+ * 计算 required vs covered 缺口。
518
+ * @returns {{ required: string[], covered: string[], missing: string[], ok: boolean, resolution }}
519
+ */
520
+ export function evaluateInvokeHatsRetention(meta, invokeDir) {
521
+ const resolution = resolveRequiredInvokeHats(meta);
522
+ const { covered, files, missingDir } = scanInvokeHats(invokeDir);
523
+ const required = resolution.hats.map(normalizeHatToken);
524
+ const missing = required.filter((h) => !covered.has(h));
525
+ return {
526
+ required,
527
+ covered: [...covered].sort(),
528
+ missing,
529
+ ok: missing.length === 0 && !missingDir && files.length > 0,
530
+ missingDir,
531
+ files,
532
+ resolution,
533
+ };
534
+ }
535
+
536
+ function parseHatList(raw) {
537
+ return String(raw)
538
+ .split(/[,,\s]+/)
539
+ .map((s) => s.trim())
540
+ .filter(Boolean)
541
+ .map(normalizeHatToken);
542
+ }
543
+
544
+ function normalizeHatToken(token) {
545
+ const t = String(token).trim();
546
+ if (/^close$/i.test(t)) return 'CLOSE';
547
+ if (t === '22') return '22';
548
+ return t;
549
+ }
550
+
399
551
  function normalizeCell(cell) {
400
552
  return cell.replace(/[`\\*]/g, '').trim();
401
553
  }
package/lib/verify.js CHANGED
@@ -5,10 +5,12 @@ import { resolveHarnessRootForTarget, wizardScript } from './paths.js';
5
5
  import { auditTarget } from './audit.js';
6
6
  import {
7
7
  buildTaskHandoff,
8
+ evaluateInvokeHatsRetention,
8
9
  extractSpecSlug,
9
10
  findReview,
10
11
  findSpecReview,
11
12
  listActiveTasks,
13
+ parseHarnessMeta,
12
14
  shouldSkipSpecAudit,
13
15
  } from './task-meta.js';
14
16
  import { lintTaskFile } from './task-lint.js';
@@ -16,10 +18,17 @@ import { lintTaskFile } from './task-lint.js';
16
18
  /**
17
19
  * 30 前聚合验证:gate-check + audit D5(仅 --task)+ reviews 留档闸
18
20
  *(--task 与全量 · N4/v2.9+)+ task lint WARN(仅 --task · v2.7+)
21
+ * + invoke hats 缺口 WARN(仅 --task · v2.12+ · 不挡 30)
19
22
  * + S5 git-clean warn + 可选 --graph
20
23
  */
21
24
  export function verifyTarget(target, options = {}) {
22
- const { taskFile, graph, allowNoReview = false, allowLintFail = false } = options;
25
+ const {
26
+ taskFile,
27
+ graph,
28
+ allowNoReview = false,
29
+ allowLintFail = false,
30
+ allowInvokeGap = false,
31
+ } = options;
23
32
  const harnessRoot = resolveHarnessRootForTarget(target);
24
33
 
25
34
  // 1. gate-check(含 --graph)
@@ -70,6 +79,35 @@ export function verifyTarget(target, options = {}) {
70
79
  gateResult.stdout += `WARN: task lint FAIL · ${rules || 'errors'}(不挡 may_start_30 · 修结构或 --allow-lint-fail)\n`;
71
80
  }
72
81
  }
82
+
83
+ // 3d. invoke hats 缺口(v2.12+):WARN 不挡 30 · close 才 block
84
+ const absTask = path.isAbsolute(taskFile)
85
+ ? taskFile
86
+ : path.join(target, taskFile);
87
+ if (fs.existsSync(absTask)) {
88
+ const content = fs.readFileSync(absTask, 'utf8');
89
+ const meta = parseHarnessMeta(content);
90
+ const slug = meta.task_slug;
91
+ if (slug) {
92
+ const invokeDir = path.join(
93
+ target,
94
+ 'docs/harness/invokes/by-task',
95
+ slug,
96
+ );
97
+ const hatsEval = evaluateInvokeHatsRetention(meta, invokeDir);
98
+ if (hatsEval.missing.length > 0 || hatsEval.missingDir || hatsEval.files.length === 0) {
99
+ const detail =
100
+ hatsEval.missingDir || hatsEval.files.length === 0
101
+ ? `目录缺失/空 · 要求 ${hatsEval.required.join(',')}`
102
+ : `缺 ${hatsEval.missing.join(',')}(要求 ${hatsEval.required.join(',')} · 已有 ${hatsEval.covered.join(',') || '∅'})`;
103
+ if (allowInvokeGap) {
104
+ gateResult.stdout += `WARN: invoke hats gap suppressed(--allow-invoke-gap · 留痕)· ${detail}\n`;
105
+ } else {
106
+ gateResult.stdout += `WARN: invoke hats gap · ${detail}(不挡 may_start_30 · close 前须补齐或 --allow-invoke-gap)\n`;
107
+ }
108
+ }
109
+ }
110
+ }
73
111
  } else {
74
112
  // 3c. 全量 reviews 闸(N4 · v2.9+):不跑 lint / D5
75
113
  const taskFiles = listActiveTasks(target);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyning/harness",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "cyning-harness discipline package · init / upgrade / check CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -198,6 +198,23 @@ profile_ide_cursor_rel() {
198
198
  echo "$rel"
199
199
  }
200
200
 
201
+ # 写入 .cyning-harness/local.json(compare-before-write · 内容不变则跳过)
202
+ # 供 upgrade post-apply 簿记;避免在 sync apply 前改已跟踪文件触发 S5
203
+ write_local_json_if_changed() {
204
+ local target="$1" root="$2"
205
+ local dir="$target/.cyning-harness"
206
+ local path="$dir/local.json"
207
+ local payload
208
+ payload="$(printf '%s\n' "{\"cyning_harness_root\":\"$root\"}")"
209
+ mkdir -p "$dir"
210
+ if [[ -f "$path" ]] && [[ "$(cat "$path")" == "$payload" ]]; then
211
+ echo "local.json 未变(跳过写入)→ cyning_harness_root=$root"
212
+ return 0
213
+ fi
214
+ printf '%s\n' "$payload" > "$path"
215
+ echo "已更新 local.json → cyning_harness_root=$root"
216
+ }
217
+
201
218
  # 从 CLAUDE.md / AGENTS.md 剥离 Harness marker 块;若仅剩空文件则删除
202
219
  # S5 · sync/apply 前检测 Git 工作区干净(v0.3+)
203
220
  # 非 git 仓跳过;--force 或 HARNESS_SYNC_FORCE=1 跳过
@@ -77,6 +77,38 @@ EOF
77
77
  echo "已写入 manifest: $mf"
78
78
  }
79
79
 
80
+ # upgrade 重写前:对 schema 外字段 / ide 裁剪打 warning(知情而非静默)
81
+ # 标准五字段:version / preset / ide / from_version / upgraded_at(additionalProperties: false)
82
+ warn_manifest_upgrade_drops() {
83
+ local mf="$1" new_ide_json="$2"
84
+ [[ -f "$mf" ]] || return 0
85
+ if ! command -v node >/dev/null 2>&1; then
86
+ echo "WARN: 未找到 node,跳过 manifest 字段变更预检" >&2
87
+ return 0
88
+ fi
89
+ node -e '
90
+ const fs = require("fs");
91
+ const mf = process.argv[1];
92
+ const newIde = JSON.parse(process.argv[2]);
93
+ const schema = new Set(["version", "preset", "ide", "from_version", "upgraded_at"]);
94
+ let old;
95
+ try { old = JSON.parse(fs.readFileSync(mf, "utf8")); } catch { process.exit(0); }
96
+ const extra = Object.keys(old).filter((k) => !schema.has(k));
97
+ if (extra.length) {
98
+ console.log(
99
+ "WARN: manifest 以下非标准字段将被移除(2.3+ 仅五字段 · 见 ONBOARDING): " +
100
+ extra.join(", "),
101
+ );
102
+ }
103
+ const oldIde = old.ide;
104
+ const oldS = JSON.stringify(oldIde ?? null);
105
+ const newS = JSON.stringify(newIde);
106
+ if (oldS !== newS) {
107
+ console.log("WARN: manifest.ide 将从 profile 重算: " + oldS + " → " + newS);
108
+ }
109
+ ' "$mf" "$new_ide_json"
110
+ }
111
+
80
112
  # upgrade apply 后更新 manifest
81
113
  write_manifest_upgrade() {
82
114
  local target="$1" version="$2"
@@ -96,6 +128,8 @@ write_manifest_upgrade() {
96
128
  preset="$(grep '"preset"' "$mf" | head -1 | sed -E 's/.*:[[:space:]]*"([^"]+)".*/\1/' || echo "$preset")"
97
129
  fi
98
130
 
131
+ warn_manifest_upgrade_drops "$mf" "$ide_json"
132
+
99
133
  mkdir -p "$target/.cyning-harness"
100
134
  cat > "$mf" <<EOF
101
135
  {
package/wizard/upgrade.sh CHANGED
@@ -101,10 +101,8 @@ echo "preset: $(profile_preset_name "$PROFILE_FILE")"
101
101
  echo "当前 IDE: $(profile_ide_summary "$PROFILE_FILE")"
102
102
  echo ""
103
103
 
104
- # 始终刷新 local.json,避免路径漂移(如 Desktop Projects)
105
- printf '%s\n' "{\"cyning_harness_root\":\"$CYNING_HARNESS\"}" > "$TARGET/.cyning-harness/local.json"
106
- echo "已更新 local.json → cyning_harness_root=$CYNING_HARNESS"
107
- echo ""
104
+ # 注意:local.json / manifest 簿记放在 sync apply **成功之后**(F1)。
105
+ # 勿在 apply 前改已跟踪的 .cyning-harness/local.json,否则 S5 git-clean 自绊。
108
106
 
109
107
  if [[ -z "$IDE_LIST" ]] && [[ "$ASSUME_YES" -eq 0 ]]; then
110
108
  if prompt_yes_no "是否更新 IDE 勾选(cursor / claude / agents)?" "n"; then
@@ -140,14 +138,16 @@ if [[ "$DO_APPLY" -eq 1 ]]; then
140
138
  SYNC_ARGS=(apply --target "$TARGET")
141
139
  [[ "$SYNC_FORCE" -eq 1 ]] && SYNC_ARGS+=(--force)
142
140
  "$SCRIPT_DIR/harness-sync.sh" "${SYNC_ARGS[@]}"
141
+ # post-sync 簿记:apply 失败则不落(set -e 已中止)
142
+ write_local_json_if_changed "$TARGET" "$CYNING_HARNESS"
143
143
  write_manifest_upgrade "$TARGET" "$VERSION"
144
144
  cp "$CYNING_HARNESS/harness/templates/QUICKREF_v1_zh.md" "$TARGET/.cyning-harness/QUICKREF.md"
145
- echo ""
146
- echo "升级完成。"
147
- echo "常用命令:"
148
- echo " npx @cyning/harness verify --target $TARGET"
149
- echo " npx @cyning/harness gate-check --target $TARGET"
150
- echo " npx @cyning/harness upgrade --target $TARGET --yes"
145
+ echo ""
146
+ echo "升级完成。"
147
+ echo "常用命令:"
148
+ echo " npx @cyning/harness verify --target $TARGET"
149
+ echo " npx @cyning/harness gate-check --target $TARGET"
150
+ echo " npx @cyning/harness upgrade --target $TARGET --yes"
151
151
  else
152
152
  echo "已取消 apply(仅预览)。"
153
153
  exit 0