@cyning/harness 2.11.1 → 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 +21 -0
- package/docs/USER_GUIDE_v1.0_zh.md +2 -1
- package/docs/methodology/graph/README.md +1 -1
- package/docs/spec/SPEC-invoke-hats-retention-gate_v1.md +59 -0
- package/harness/discipline-coverage.yaml +12 -7
- package/harness/invokes/TEMPLATE_invoke.md +3 -2
- package/harness/lifecycle.yaml +7 -1
- package/harness/prompts/10-task-requirements.md +3 -1
- package/harness/prompts/20-task-audit.md +3 -1
- package/harness/prompts/30-execute-code.md +2 -2
- package/harness/templates/TASK_TEMPLATE.md +2 -0
- package/lib/cli.js +29 -10
- package/lib/task-close.js +35 -7
- package/lib/task-lint.js +27 -0
- package/lib/task-meta.js +152 -0
- package/lib/verify.js +39 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
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
|
+
|
|
7
27
|
## [2.11.1] - 2026-07-25
|
|
8
28
|
|
|
9
29
|
### Fixed
|
|
@@ -15,6 +35,7 @@
|
|
|
15
35
|
|
|
16
36
|
- 摩擦报告:`Projects/docs/harness/guides/ANALYSIS_upgrade_wizard_friction_20260725_v1_zh.md`
|
|
17
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` 已推送
|
|
18
39
|
|
|
19
40
|
## [2.11.0] - 2026-07-25
|
|
20
41
|
|
|
@@ -187,7 +187,8 @@ Schema:[`schema/verify_result.v1.schema.json`](../schema/verify_result.v1.sche
|
|
|
187
187
|
| `npx @cyning/harness init` | 首次安装模板与 manifest(可选 `--with-scripts`) |
|
|
188
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
|
|
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) |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **对外名**:PIP(过程实例投影)。对内历史文件名可含 `HGM` / `hgm_*`(路径兼容,正文不用曾称括注)。
|
|
4
4
|
|
|
5
|
-
> **Track G · G1 / v2.0+**:PIP
|
|
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。
|
|
6
6
|
|
|
7
7
|
| 文档 | 状态 | 说明 |
|
|
8
8
|
| --- | --- | --- |
|
|
@@ -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.
|
|
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:
|
|
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:
|
|
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`
|
|
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
|
---
|
package/harness/lifecycle.yaml
CHANGED
|
@@ -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
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`
|
|
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 {
|
|
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);
|