@cyning/harness 2.7.0 → 2.8.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,21 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.8.0] - 2026-07-25
8
+
9
+ ### Added
10
+
11
+ - **SPEC 审查文留档闸(N3 · G2 姊妹)**:`verify --spec FILE` 检查 `docs/harness/reviews/` 下 SPEC 审查文存在性(推荐 `spec_<slug>_audit_R*` · 兼容 `*_ACCEPT_*` / `task_*_spec_ACCEPT_*`)。缺失 → `VERIFY: BLOCKED · missing SPEC R<n> review` exit 2。
12
+ - 豁免:`--allow-no-spec-review`;元信息 `track: bugfix` / `skip_spec_audit: true`。
13
+ - `findSpecReview`(支持 `--workspace-root` 分仓);handoff:`may_start_00` · `spec_review_found` · `spec_review_latest`。
14
+ - `lifecycle.yaml` 转移 `to_00` + guard `spec_reviews_retention`(只登记)。
15
+
16
+ ### Notes
17
+
18
+ - 与 `--task` 互斥;**不**改 task 侧 verify/close。
19
+ - SPEC:`docs/spec/SPEC-spec-reviews-retention-gate_v1.md`
20
+ - minor · 行为新增(新模式)
21
+
7
22
  ## [2.7.0] - 2026-07-24
8
23
 
9
24
  ### Added
@@ -17,6 +32,7 @@
17
32
  - 背景:Post-G4 方案 N1+N2 · rethink 方向二骨架;dogfood active lint FAIL 率高故本波不做 block。
18
33
  - SPEC:`docs/spec/SPEC-lifecycle-and-verify-lint_v1.md`
19
34
  - minor · `npm test` 含 lifecycle + verify-lint-warn
35
+ - **已发布**:`@cyning/harness@2.7.0`(npm `latest` · 2026-07-24)· tag `v2.7.0`
20
36
 
21
37
  ## [2.6.0] - 2026-07-24
22
38
 
@@ -52,6 +52,11 @@ npx @cyning/harness verify --target /path/to/your-repo \
52
52
  # --task 另跑 task lint(v2.7+ · E 级仅 WARN,不挡 may_start_30)
53
53
  # 抑制 lint WARN:加 --allow-lint-fail
54
54
 
55
+ # SPEC→00 前:审查文存在性(v2.8+ · 与 --task 互斥)
56
+ npx @cyning/harness verify --spec docs/spec/SPEC-xxx_v1.md \
57
+ --workspace-root /path/to/Projects
58
+ # 豁免:--allow-no-spec-review · 或 SPEC track=bugfix / skip_spec_audit
59
+
55
60
  # 只读生命周期登记(方向二 · 非引擎)
56
61
  npx @cyning/harness lifecycle show
57
62
  npx @cyning/harness lifecycle show --json
@@ -115,6 +115,11 @@ npx @cyning/harness@1.0.1 init --preset harness-only --ide cursor,agents --yes
115
115
  npx @cyning/harness verify --target . --task docs/tasks/active/task_xxx.md
116
116
  # lint FAIL 仅 WARN(v2.7+ · 不挡 30);抑制:--allow-lint-fail
117
117
 
118
+ # SPEC→00:审查文存在性(v2.8+ · 与 --task 互斥)
119
+ npx @cyning/harness verify --spec docs/spec/SPEC-xxx_v1.md \
120
+ --workspace-root /path/to/Projects
121
+ # 豁免:--allow-no-spec-review
122
+
118
123
  # Agent handoff(v2.0.2+):JSON 路由 + 下一帽提示
119
124
  npx @cyning/harness verify --target . \
120
125
  --task docs/tasks/active/task_xxx.md \
@@ -172,7 +177,7 @@ Schema:[`schema/verify_result.v1.schema.json`](../schema/verify_result.v1.sche
172
177
  | `npx @cyning/harness init` | 首次安装模板与 manifest(可选 `--with-scripts`) |
173
178
  | `npx @cyning/harness upgrade` | 同步产品包更新(可加 `--gate-check` 先 audit) |
174
179
  | `npx @cyning/harness check` | 检查是否有新版本 |
175
- | `npx @cyning/harness verify` | 30 前聚合:gate-check + audit D5 + reviews +(`--task`)lint WARN + S5 · 可选 `--graph` / `--json` / `--allow-lint-fail` |
180
+ | `npx @cyning/harness verify` | `--task`:30 前聚合;`--spec`:SPEC→00 审查文闸(v2.8+ · 互斥)· `--allow-no-spec-review` |
176
181
  | `npx @cyning/harness lifecycle show` | 只读展示 `harness/lifecycle.yaml`(状态/转移/守卫 · v2.7+ · 非引擎) |
177
182
  | `npx @cyning/harness gate-check` | 仅人工闸(`--graph` / `--json`) |
178
183
  | `npx @cyning/harness audit` | ICVO 机械审计(D3/D5/S5) |
@@ -0,0 +1,192 @@
1
+ # SPEC:SPEC 审查文留档闸(20-spec-audit / HG-SPEC-SIGNOFF 存在性检查)(v1)
2
+
3
+ > **状态**:`signed`(维护者签收 2026-07-25 · 对话「签收」)
4
+ > **track**:`feature`
5
+ > **关联图谱**:无(纯 Harness 工具链)
6
+ > **上游**:[`PLAN_post_g4_next_mechanization_v1_zh.md`](../../../docs/harness/guides/PLAN_post_g4_next_mechanization_v1_zh.md) · N3 · G2 姊妹
7
+ > **前置**:G2 `@cyning/harness@2.5.0`(`findReview`)· N1 `@2.7.0`(`lifecycle.yaml`)
8
+ > **下游**:00 已起草 task → 20-task-audit → HG-AUDIT-R1 → 30(目标版本 **v2.8.0**)
9
+
10
+ ---
11
+
12
+ ## Harness 元信息
13
+
14
+ | 字段 | 值 |
15
+ |------|-----|
16
+ | **spec_slug** | `spec-reviews-retention-gate` |
17
+ | **test_strategy** | `required` |
18
+ | **test_strategy_note** | `findSpecReview` 命名变体 + `verify --spec` 三态(pass/block/豁免)+ bugfix 豁免;既有 `verify --task` 回归不改 |
19
+ | **entry_invoke_10_spec** | `Projects/docs/harness/invokes/by-task/cyning-harness-spec-reviews-retention-gate/invoke_20260725_10_spec_spec_reviews_retention_gate.md` |
20
+ | **entry_invoke_00_draft** | 工作区 `docs/harness/prompts/PROMPT_00_draft_spec_or_task_v1_zh.md` |
21
+
22
+ ---
23
+
24
+ ## 1. 背景与目标
25
+
26
+ G2(v2.5.0)已把 **task** 侧「R&lt;n&gt; 审查文存在」机械化为 `verify --task` / `task close` 检查 6。功能轨上游对称缺口仍在:
27
+
28
+ - [`20-spec-audit.md`](../../harness/prompts/20-spec-audit.md) 要求落盘 `reviews/spec_<slug>_audit_R<n>_*.md`(或既有 `*_spec_ACCEPT_R*` 惯例);
29
+ - **`HG-SPEC-SIGNOFF` 可由对话「签收」完成,签署依据(SPEC 审查文)存在性零机械**——与 G2 补闸前的 task 侧同构。
30
+
31
+ **dogfood(2026-07-25)**:产品仓 `docs/spec/SPEC-*.md` **5** 份近期 SPEC(含 G1–G4 / N1+N2)**均无**对应 `spec_*_audit_R*` / `*_spec_ACCEPT_R*` 文;工作区仅有少量历史 `spec_*_ACCEPT_*` / `*_spec_ACCEPT_*`。说明当前主流是「对话签收 + 跳过 20-spec-audit 落盘」。
32
+
33
+ **目标**:补上 SPEC→task 纸链的对称闸——机器只查**审查文存在性**;审查结论仍由人签 `HG-SPEC-SIGNOFF` 覆盖。
34
+
35
+ **职责切分(同 G2)**:机器 = 形式(文件在);人 = 实质(通过/条件通过)。
36
+
37
+ ---
38
+
39
+ ## 2. 范围
40
+
41
+ ### D1 · `findSpecReview`(`lib/task-meta.js` 或邻近模块)
42
+
43
+ - 输入:`specFile` + `target`(reviews 根所在仓)+ 可选 `workspaceRoot`(SPEC 在产品仓、reviews 在工作区时)
44
+ - 匹配(任一命中即 `found`;取最新轮):
45
+ 1. **推荐**:`spec_<slug>_audit_R<n>_*.md`
46
+ 2. **兼容**:`spec_<slug>_ACCEPT_R<n>_*.md`
47
+ 3. **兼容**:`task_<slug>_spec_ACCEPT_R<n>_*.md`(工作区历史惯例)
48
+ - slug 来源:优先 SPEC 表 `spec_slug`;否则自文件名剥离 `SPEC-` / `_v\d+` / 扩展名后 `normalizeSlug`
49
+ - 文件名与 slug:**两侧**下划线/连字符等价;版本后缀 `_v\d+` 双侧剥离(复用 G2 R1-B1 思路)
50
+ - 返回:`{ found, latest, rounds[], matched_pattern? }`
51
+
52
+ ### D2 · CLI 挂点:`verify --spec FILE`
53
+
54
+ - **新模式**(与 `--task` 互斥):`npx @cyning/harness verify --spec PATH [--target PATH] [--workspace-root PATH] [--json] [--allow-no-spec-review]`
55
+ - 行为:
56
+ - 审查文存在 → `VERIFY: PASS`(或等价摘要)· exit 0
57
+ - 缺失 → `VERIFY: BLOCKED · missing SPEC R<n> review` · exit 2
58
+ - `--allow-no-spec-review` → warn 放行 · 留痕
59
+ - `--json`:`may_start_00`(或 `spec_review_ok`)· `spec_review_found` · `spec_review_latest` · `blocked_reason`
60
+ - **不**改动既有 `verify --task` / 无参全量模式语义(N4 仍另议)
61
+ - **不**把本闸挂进 `verify --task`(挂点错误:30 查的是 task 审查文,不是 SPEC)
62
+
63
+ ### D3 · bugfix / 跳过 10-spec 豁免
64
+
65
+ 满足任一则 **不**要求 SPEC 审查文(exit 0 + 可选 info 行):
66
+
67
+ - SPEC / 元信息显式:`track: bugfix` 或 `skip_spec_audit: true`(字段名以实现为准 · 文档冻结)
68
+ - CLI:`--allow-no-spec-review`(通用泄压,含历史对话签收存量)
69
+
70
+ ### D4 · lifecycle 登记
71
+
72
+ - `harness/lifecycle.yaml` 新增转移(建议 id:`to_00` 或 `spec_signoff`):
73
+ - from:spec draft / signed 相关状态(可用注释说明别名;本波允许最小 states 扩展或仅在 transitions 注释)
74
+ - to:可起草 task
75
+ - guard:`spec_reviews_retention` · severity=`block` · `allow_flag: --allow-no-spec-review`
76
+ - **只登记 · 不实现引擎**(同 N1)
77
+
78
+ ### D5 · 文档 / 版本
79
+
80
+ - `20-spec-audit.md` / ONBOARDING 或 USER_GUIDE:注明 v2.8+ `verify --spec` 机械强制存在性
81
+ - CHANGELOG **v2.8.0**(行为新增 · minor;醒目说明 + 豁免指引)
82
+ - PLAN / rethink 矩阵回链本 SPEC
83
+
84
+ ### D6 · 测试
85
+
86
+ - 命名三变体命中;slug 连字符/下划线;版本后缀
87
+ - verify `--spec` 三态 + bugfix 豁免 + 与 `--task` 互斥用法
88
+ - 既有 verify/close/lifecycle 回归绿
89
+
90
+ ---
91
+
92
+ ## 3. 非范围
93
+
94
+ - 审查文**内容**判定(pass / conditional_pass / fail)——人/20-spec-audit 职责
95
+ - 强制给**存量** 5 份已对话签收的产品 SPEC 补审(可用豁免;另开 hygiene 可选)
96
+ - 把 SPEC 审查闸挂进 `verify --task` 或 `task close`
97
+ - N4:verify 无 `--task` 全量模式纳入 task reviews
98
+ - 自动代签 `HG-SPEC-SIGNOFF`;状态机引擎执行 `to_00`
99
+ - 统一历史所有命名为单一范式(本波只兼容读取)
100
+
101
+ ---
102
+
103
+ ## 4. 验收标准
104
+
105
+ - [ ] `findSpecReview`:推荐名 / `ACCEPT` / `task_*_spec_ACCEPT_*` 均可 `found`;多轮取最新
106
+ - [ ] `verify --spec`:有文 → PASS;无文 → BLOCKED exit 2;`--allow-no-spec-review` → warn 放行
107
+ - [ ] bugfix / `skip_spec_audit` → 不要求审查文
108
+ - [ ] `--json` 含 `spec_review_found`(或等价)且 **不**破坏 `--task` handoff 字段
109
+ - [ ] lifecycle.yaml 已登记 `to_00`(或等价)+ `spec_reviews_retention` guard
110
+ - [ ] `npm test` 全绿;CHANGELOG v2.8.0;文档可链到
111
+ - [ ] dogfood:对本 SPEC 走通「20-spec-audit 落盘 → `verify --spec` PASS」(实现波自检)
112
+
113
+ ---
114
+
115
+ ## 5. failure_paths
116
+
117
+ | 触发条件 | 系统行为 | 可重试 |
118
+ |----------|----------|--------|
119
+ | 00 前无 SPEC 审查文 | `VERIFY: BLOCKED · missing SPEC R<n> review` | 跑 20-spec-audit 落盘;或 `--allow-no-spec-review` |
120
+ | SPEC 在产品仓、reviews 在工作区 | 未传 `--workspace-root` / 错 `--target` → 误报缺失 | 按文档传参后重跑 |
121
+ | 命名不在兼容列表 | 视为缺失 | 按推荐名重命名或补一份推荐名 |
122
+ | bugfix 轨被误挡 | 应走 D3 豁免;若未标 track → 用 allow 旗 | 补元信息 |
123
+ | 业务仓未升级 2.8.0 | 无 `--spec` 模式 · 行为不变 | upgrade |
124
+
125
+ ---
126
+
127
+ ## 6. 依赖与引用
128
+
129
+ - G2:`findReview` / `--allow-no-review` 模式;本波 **独立** flag `--allow-no-spec-review`(审计痕迹不混)
130
+ - N1:`lifecycle.yaml` 挂点登记
131
+ - 纪律:`harness/prompts/20-spec-audit.md` · `10-spec-requirements.md`(bugfix 跳过)
132
+ - PLAN N3;G2 SPEC 非范围「SPEC 审查文——下一轮」本 SPEC 关闭该句
133
+
134
+ ---
135
+
136
+ ## 7. 思考轮(10-spec 回填 · R0–R5)
137
+
138
+ ### R0 · 读入与约束
139
+
140
+ 读入:PLAN N3;G2 SPEC(非范围点名本缺口);`20-spec-audit` 命名双惯例;`findReview` 实现;lifecycle.yaml 仅有 task 转移;dogfood:5/5 产品 SPEC 无 spec 审查文。约束:挂点时刻须晚于审查文应存在时刻;不误伤 bugfix;不把闸错挂到 `--task`。
141
+
142
+ ### R1 · 范围 / 非范围 / 场景
143
+
144
+ **场景**:① 维护者/Agent 在 00 起草 task 前跑 `verify --spec`,挡「对话签了但无审查文」;② 20-spec-audit 多轮 R1→R2 取最新;③ 历史 `*_ACCEPT_*` 命名仍认;④ bugfix 无 10-spec 不挡。
145
+ **同构 G2、异挂点**:共享「存在性闸」模式,但消费者是 **SPEC→00**,不是 **task→30**。
146
+ **非范围**:内容判定、存量强制补审、挂进 task verify/close——避免双重计费与挂点错位。
147
+
148
+ ### R2 · 方案对比
149
+
150
+ | 决策点 | 选项 | 裁定 | 理由 |
151
+ |---|---|---|---|
152
+ | 严格度 | warn 起步 / block+allow | **block + `--allow-no-spec-review`** | 与 G2 同构;warn 等于再留自觉层。dogfood 存量用豁免,不降级整闸 |
153
+ | CLI 形态 | 独立 `spec check` / 并入 `verify --spec` | **`verify --spec`** | 与 G2 叙事一致(「verify = 下一转移前置」);少一个动词 |
154
+ | 挂点 | `--task` 附带查 SPEC / 仅 `--spec` / close | **仅 `--spec`** | `--task` 时 SPEC 链路可能已结束;close 查 SPEC 无审计价值 |
155
+ | 命名 | 只认推荐 / 兼容 ACCEPT | **推荐 + 两兼容** | 20-spec-audit 已写双惯例;工作区有 ACCEPT 实档 |
156
+ | reviews 根 | 仅 SPEC 所在仓 / 可 workspace-root | **支持 `--workspace-root`(或 `--target` 指 reviews 仓)** | 产品 SPEC × 工作区 reviews 是本仓常态 |
157
+ | 豁免旗 | 复用 `--allow-no-review` / 独立 | **独立 `--allow-no-spec-review`** | 与 task 审查豁免分迹 |
158
+ | 版本 | 2.7.x patch / 2.8.0 | **2.8.0** | 新 blocking 模式 · minor |
159
+
160
+ ### R3 · 边界 / 失败语义 / 安全
161
+
162
+ - **存量**:5 份已对话签收 SPEC 不自动被扫;仅在有人执行 `verify --spec` 时暴露 → 豁免或补 20-spec-audit。
163
+ - **误报**:形式宽容止于「slug 匹配 + 兼容模式名」;不接受任意含 spec 字样的文件。
164
+ - **安全**:只读检查;不写盘、不代签人闸。
165
+ - **兼容**:无 `--spec` 的旧调用链行为不变。
166
+
167
+ ### R4 · 验收 / 可测性 / test_strategy
168
+
169
+ `test_strategy: required`。fixture:三命名 · 互斥参数 · block/pass/allow · bugfix 跳过;回归 verify-task / lifecycle show。实现波 dogfood:本 SPEC 的 20-spec-audit 文 + `verify --spec` PASS。
170
+
171
+ ### R5 · SPEC 签收就绪 · 是否可交 00 出 task
172
+
173
+ 自足:挂点、命名、豁免、lifecycle 登记、版本均已裁定。**可交 00**:建议单 task `cyning-harness-spec-reviews-retention-gate`。图谱无需 bootstrap。
174
+
175
+ ### 思考轮控制
176
+
177
+ | 字段 | 值 |
178
+ |------|-----|
179
+ | `actual_last_round` | `R5` |
180
+ | `early_stop` | `no` |
181
+ | `early_stop_reason` | — |
182
+ | `residual_risks` | ① 对话签收文化可能继续依赖豁免旗——须文档把「00 前 verify --spec」写成默认;② SPEC 路径与 reviews 仓分裂时 Agent 漏传 workspace-root → 误 BLOCKED;③ 是否在 00 帽 Prompt 增加「开工前 GATE_VERIFY_SPEC」句,属文档同步,实现时对照 10-spec/00 模板 |
183
+ | `round_extension_note` | — |
184
+
185
+ ---
186
+
187
+ ## 修订记录
188
+
189
+ | 日期 | 摘要 |
190
+ |------|------|
191
+ | 2026-07-25 | 10-spec R0–R5(维护者点菜「继续 N3」)· dogfood 5/5 无 spec 审查文 |
192
+ | 2026-07-25 | 维护者签收(对话「签收」)· → 00 起草 task |
@@ -8,7 +8,9 @@ version: "1"
8
8
 
9
9
  states:
10
10
  - id: draft
11
- note: "含 pending / active 等词表别名 · 见 KNOWN_STATUS_TOKENS"
11
+ note: "含 pending / active 等词表别名 · 见 KNOWN_STATUS_TOKENS;SPEC 侧亦用 draft"
12
+ - id: signed
13
+ note: "SPEC 人签 HG-SPEC-SIGNOFF 后 · 可起草 task"
12
14
  - id: in_progress
13
15
  - id: done
14
16
  note: "close 可接受 completed 别名"
@@ -16,6 +18,18 @@ states:
16
18
  note: "task close 归档后 · 物理上在 done/ 目录"
17
19
 
18
20
  transitions:
21
+ - id: to_00
22
+ from: ["draft", "signed"]
23
+ to: signed
24
+ hat: "00"
25
+ description: "SPEC 签收后起草 task(verify --spec 聚合 · v2.8+)"
26
+ guards:
27
+ - id: spec_reviews_retention
28
+ command_or_check: "verify --spec · findSpecReview(v2.8+)"
29
+ severity: block
30
+ allow_flag: "--allow-no-spec-review"
31
+ note: "bugfix / skip_spec_audit 元信息豁免 · 不挂 verify --task"
32
+
19
33
  - id: to_30
20
34
  from: ["draft", "in_progress"]
21
35
  to: in_progress
@@ -17,6 +17,7 @@
17
17
  - 判定:`pass` | `conditional_pass` | `fail` · 建议 **HG-SPEC-SIGNOFF**(**不**代签)
18
18
  - **轻量路径**:10-spec 思考轮已充分时,单轮 R1 即可
19
19
  - 通过 → 建议 00 起草 task;**不**附 30 Prompt
20
+ - **v2.8+ 机械闸**:00 前跑 `npx @cyning/harness verify --spec <SPEC路径> [--workspace-root <Projects>]`——机器只查审查文**存在性**;缺失 exit 2。豁免:`--allow-no-spec-review` · bugfix / `skip_spec_audit`
20
21
 
21
22
  ## 禁止什么
22
23
 
package/lib/cli.js CHANGED
@@ -41,7 +41,7 @@ function usage(version = 'unknown') {
41
41
  npx @cyning/harness check [--target PATH]
42
42
  npx @cyning/harness audit [--target PATH] [--task FILE]
43
43
  npx @cyning/harness gate-check [--target PATH] [--task FILE] [--graph] [--json]
44
- npx @cyning/harness verify [--target PATH] [--task FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH]
44
+ npx @cyning/harness verify [--target PATH] [--task FILE | --spec FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH]
45
45
  npx @cyning/harness lifecycle show [--json]
46
46
  npx @cyning/harness sync index [--target PATH]
47
47
  npx @cyning/harness task check --file PATH [--no-circular] [--registry DIR]...
@@ -62,7 +62,7 @@ function usage(version = 'unknown') {
62
62
  check 读 manifest.json · 对比当前包版本是否有可升级版本
63
63
  audit ICVO 机械审计:复用 gate-check · D5 测试声明检查 · S5 git-clean 提醒
64
64
  gate-check 调用 wizard/gate-check.sh(业务仓无需 clone 即可用)
65
- verify 30 前聚合:gate-check + audit D5 + reviews + task lint WARN(v2.7+)+ S5 warn + 可选 --graph
65
+ verify 30 前聚合(--task)或 SPEC→00 审查文闸(--spec · v2.8+)
66
66
  lifecycle 只读展示 harness/lifecycle.yaml(状态/转移/守卫 · v2.7+)
67
67
  sync index 生成 .cyning-harness/invoke_index.json
68
68
  task check 校验 task.harness.v1.json sidecar · --no-circular 检测 depends_on 环
@@ -390,16 +390,21 @@ async function cmdGateCheck(args) {
390
390
 
391
391
  async function cmdVerify(args) {
392
392
  // D3 · implemented in lib/verify.js
393
- const { verifyTarget, buildVerifyHandoff, formatAgentHint } = await import('./verify.js');
393
+ const {
394
+ verifyTarget,
395
+ verifySpecTarget,
396
+ buildVerifyHandoff,
397
+ buildSpecVerifyHandoff,
398
+ formatAgentHint,
399
+ } = await import('./verify.js');
394
400
  if (args.includes('--help') || args.includes('-h')) {
395
- console.log(`用法: npx @cyning/harness verify [--target PATH] [--task FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH] [--allow-no-review] [--allow-lint-fail]
396
-
397
- 无 --task 时:扫描 docs/tasks/active/task_*.md 并逐项列出闸表(同 gate-check);本波不跑 task lint。
398
- 任一 task 阻塞则 VERIFY: BLOCKED · exit 2;全部可 30 VERIFY: PASS
399
- --task:另跑 task lint;E 级仅 WARN(v2.7+ · 不改 may_start_30);--allow-lint-fail 抑制 WARN
400
- --json:输出 Agent handoff JSON(may_start_30 · entry_invoke_30 · review_path · lint 等)。
401
- --agent-hint:人类可读 handoff 摘要(可与 --json 同用)。
402
- --workspace-root:解析 task 中 Projects/ 前缀的 entry_invoke 路径。
401
+ 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]
402
+
403
+ 无 --task/--spec:扫描 docs/tasks/active/task_*.md 并逐项列出闸表(同 gate-check);不跑 task lint。
404
+ --task:30 前聚合(gate + audit + reviews + lint WARN)· --allow-no-review / --allow-lint-fail
405
+ --spec:SPEC→00 前查审查文存在性(v2.8+ · --task 互斥)· --allow-no-spec-review
406
+ 推荐:--spec <产品仓 SPEC> --workspace-root <含 docs/harness/reviews 的工作区根>
407
+ --json / --agent-hint:Agent handoff
403
408
  `);
404
409
  return;
405
410
  }
@@ -409,20 +414,24 @@ async function cmdVerify(args) {
409
414
  rest = r1;
410
415
  const { value: taskFile, rest: r2 } = takeOption(rest, '--task');
411
416
  rest = r2;
412
- const { value: workspaceRoot, rest: r3 } = takeOption(rest, '--workspace-root');
417
+ const { value: specFile, rest: r3 } = takeOption(rest, '--spec');
413
418
  rest = r3;
419
+ const { value: workspaceRoot, rest: r4 } = takeOption(rest, '--workspace-root');
420
+ rest = r4;
414
421
  const graph = rest.includes('--graph');
415
422
  const json = rest.includes('--json');
416
423
  const agentHint = rest.includes('--agent-hint');
417
424
  const allowNoReview = rest.includes('--allow-no-review');
418
425
  const allowLintFail = rest.includes('--allow-lint-fail');
426
+ const allowNoSpecReview = rest.includes('--allow-no-spec-review');
419
427
  rest = rest.filter(
420
428
  (a) =>
421
429
  a !== '--graph' &&
422
430
  a !== '--json' &&
423
431
  a !== '--agent-hint' &&
424
432
  a !== '--allow-no-review' &&
425
- a !== '--allow-lint-fail',
433
+ a !== '--allow-lint-fail' &&
434
+ a !== '--allow-no-spec-review',
426
435
  );
427
436
 
428
437
  if (rest.length > 0) {
@@ -431,8 +440,37 @@ async function cmdVerify(args) {
431
440
  throw err;
432
441
  }
433
442
 
443
+ if (taskFile && specFile) {
444
+ const err = new Error('verify:--task 与 --spec 互斥');
445
+ err.exitCode = 1;
446
+ throw err;
447
+ }
448
+
434
449
  const target = resolveTarget(process.cwd(), targetArg);
435
- const result = verifyTarget(target, { taskFile, graph, allowNoReview, allowLintFail });
450
+ const ws = workspaceRoot ? path.resolve(workspaceRoot) : undefined;
451
+
452
+ let result;
453
+ let handoff;
454
+ let label;
455
+
456
+ if (specFile) {
457
+ result = verifySpecTarget(target, { specFile, workspaceRoot: ws, allowNoSpecReview });
458
+ handoff = buildSpecVerifyHandoff(target, {
459
+ specFile,
460
+ workspaceRoot: ws,
461
+ allowNoSpecReview,
462
+ });
463
+ label = path.basename(specFile);
464
+ } else {
465
+ result = verifyTarget(target, { taskFile, graph, allowNoReview, allowLintFail });
466
+ handoff = buildVerifyHandoff(target, {
467
+ taskFile,
468
+ workspaceRoot: ws,
469
+ allowNoReview,
470
+ allowLintFail,
471
+ });
472
+ label = taskFile ? path.basename(taskFile) : null;
473
+ }
436
474
 
437
475
  // forward gate-check/audit/graph stdout if any(--json 时仍透传机械闸表)
438
476
  if (result.stdout && !json) {
@@ -442,13 +480,6 @@ async function cmdVerify(args) {
442
480
  process.stderr.write(result.stdout);
443
481
  }
444
482
 
445
- const handoff = buildVerifyHandoff(target, {
446
- taskFile,
447
- workspaceRoot: workspaceRoot ? path.resolve(workspaceRoot) : undefined,
448
- allowNoReview,
449
- allowLintFail,
450
- });
451
-
452
483
  if (json) {
453
484
  console.log(JSON.stringify(handoff, null, 2));
454
485
  }
@@ -456,15 +487,13 @@ async function cmdVerify(args) {
456
487
  console.log(formatAgentHint(handoff));
457
488
  }
458
489
 
490
+ const summary = label
491
+ ? `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''} · ${label}`
492
+ : `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''}`;
493
+
459
494
  if (!json && !agentHint) {
460
- const summary = taskFile
461
- ? `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''} · ${path.basename(taskFile)}`
462
- : `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''}`;
463
495
  console.log(summary);
464
496
  } else if (json || agentHint) {
465
- const summary = taskFile
466
- ? `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''} · ${path.basename(taskFile)}`
467
- : `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''}`;
468
497
  console.error(summary);
469
498
  }
470
499
 
package/lib/task-meta.js CHANGED
@@ -157,6 +157,107 @@ export function findReviewPath(target, taskFile) {
157
157
  return findReview(target, taskFile).latest;
158
158
  }
159
159
 
160
+ /** 剥离文件名末尾 `_v\d+` */
161
+ function stripVerSuffix(s) {
162
+ return String(s).replace(/_v\d+$/, '');
163
+ }
164
+
165
+ /**
166
+ * 自 SPEC 路径/正文解析 slug(优先表 `spec_slug`)。
167
+ */
168
+ export function extractSpecSlug(specFile, content) {
169
+ const meta = content ? parseHarnessMeta(content) : {};
170
+ if (meta.spec_slug) return meta.spec_slug;
171
+
172
+ let base = path.basename(specFile, '.md');
173
+ base = base.replace(/^SPEC[-_]/i, '');
174
+ base = stripVerSuffix(base);
175
+ return base;
176
+ }
177
+
178
+ /**
179
+ * bugfix / 显式跳过 10-spec 审计:不要求 SPEC 审查文。
180
+ * 只认元信息表 / 文首 track 行——避免命中正文里对规则的说明文字。
181
+ */
182
+ export function shouldSkipSpecAudit(content) {
183
+ if (!content) return false;
184
+ const meta = parseHarnessMeta(content);
185
+ if (String(meta.skip_spec_audit || '').toLowerCase() === 'true') return true;
186
+ if (String(meta.track || '').toLowerCase() === 'bugfix') return true;
187
+ if (/^>\s*\*\*track\*\*[::]\s*`?bugfix\b/im.test(content)) return true;
188
+ return false;
189
+ }
190
+
191
+ /**
192
+ * 查找 SPEC 的 R<n> 审查文(verify --spec · N3)。
193
+ * 兼容命名:
194
+ * 1. spec_<slug>_audit_R<n>_*.md(推荐)
195
+ * 2. spec_<slug>_ACCEPT_R<n>_*.md
196
+ * 3. task_<slug>_spec_ACCEPT_R<n>_*.md
197
+ * 在 target 与可选 workspaceRoot 的 docs/harness/reviews/ 下搜索。
198
+ */
199
+ export function findSpecReview(specFile, options = {}) {
200
+ const { target = process.cwd(), workspaceRoot } = options;
201
+ const empty = { found: false, latest: null, rounds: [], matched_pattern: null };
202
+
203
+ const absSpec = path.isAbsolute(specFile)
204
+ ? specFile
205
+ : path.resolve(target, specFile);
206
+ if (!fs.existsSync(absSpec)) return empty;
207
+
208
+ const content = fs.readFileSync(absSpec, 'utf8');
209
+ const slugNorm = normalizeSlug(stripVerSuffix(extractSpecSlug(absSpec, content)));
210
+
211
+ const roots = [];
212
+ const pushRoot = (root) => {
213
+ if (!root) return;
214
+ const abs = path.resolve(root);
215
+ if (!roots.includes(abs)) roots.push(abs);
216
+ };
217
+ pushRoot(target);
218
+ pushRoot(workspaceRoot);
219
+
220
+ const PATTERNS = [
221
+ { id: 'spec_audit', re: /^spec_(.+?)_audit_R(\d+)_.*\.md$/i },
222
+ { id: 'spec_ACCEPT', re: /^spec_(.+?)_ACCEPT_R(\d+)_.*\.md$/i },
223
+ { id: 'task_spec_ACCEPT', re: /^task_(.+?)_spec_ACCEPT_R(\d+)_.*\.md$/i },
224
+ ];
225
+
226
+ const matches = [];
227
+ for (const root of roots) {
228
+ const reviewsDir = path.join(root, 'docs/harness/reviews');
229
+ if (!fs.existsSync(reviewsDir)) continue;
230
+ for (const name of fs.readdirSync(reviewsDir)) {
231
+ for (const pat of PATTERNS) {
232
+ const m = name.match(pat.re);
233
+ if (!m) continue;
234
+ if (normalizeSlug(stripVerSuffix(m[1])) !== slugNorm) continue;
235
+ matches.push({
236
+ name,
237
+ round: parseInt(m[2], 10),
238
+ mtime: fs.statSync(path.join(reviewsDir, name)).mtimeMs,
239
+ pattern: pat.id,
240
+ root,
241
+ rel: path.join('docs/harness/reviews', name).replace(/\\/g, '/'),
242
+ });
243
+ break;
244
+ }
245
+ }
246
+ }
247
+
248
+ if (matches.length === 0) return empty;
249
+
250
+ matches.sort((a, b) => b.round - a.round || b.mtime - a.mtime);
251
+ const latest = matches[0];
252
+ return {
253
+ found: true,
254
+ latest: latest.rel,
255
+ rounds: [...new Set(matches.map((x) => x.round))].sort((a, b) => a - b),
256
+ matched_pattern: latest.pattern,
257
+ reviews_root: latest.root,
258
+ };
259
+ }
260
+
160
261
  /**
161
262
  * 聚合单 task 的 Agent handoff 结果。
162
263
  */
package/lib/verify.js CHANGED
@@ -1,8 +1,16 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { resolveHarnessRootForTarget, wizardScript } from './paths.js';
4
5
  import { auditTarget } from './audit.js';
5
- import { buildTaskHandoff, findReview, listActiveTasks } from './task-meta.js';
6
+ import {
7
+ buildTaskHandoff,
8
+ extractSpecSlug,
9
+ findReview,
10
+ findSpecReview,
11
+ listActiveTasks,
12
+ shouldSkipSpecAudit,
13
+ } from './task-meta.js';
6
14
  import { lintTaskFile } from './task-lint.js';
7
15
 
8
16
  /**
@@ -89,6 +97,96 @@ function runTaskLintForVerify(target, taskFile) {
89
97
  }
90
98
  }
91
99
 
100
+ /**
101
+ * SPEC→00 前验证(N3 · v2.8+):仅查 SPEC 审查文存在性。
102
+ * 不跑 gate-check / audit D5 / task lint(与 --task 模式分离)。
103
+ */
104
+ export function verifySpecTarget(target, options = {}) {
105
+ const { specFile, workspaceRoot, allowNoSpecReview = false } = options;
106
+ let stdout = '';
107
+
108
+ if (!specFile) {
109
+ return {
110
+ ok: false,
111
+ exitCode: 1,
112
+ reason: 'verify --spec 须指定 SPEC 文件路径',
113
+ stdout,
114
+ };
115
+ }
116
+
117
+ const absSpec = path.isAbsolute(specFile)
118
+ ? specFile
119
+ : path.resolve(target, specFile);
120
+ if (!fs.existsSync(absSpec)) {
121
+ return {
122
+ ok: false,
123
+ exitCode: 1,
124
+ reason: `SPEC 文件不存在: ${specFile}`,
125
+ stdout,
126
+ };
127
+ }
128
+
129
+ const content = fs.readFileSync(absSpec, 'utf8');
130
+ if (shouldSkipSpecAudit(content)) {
131
+ stdout += `INFO: skip SPEC review gate(bugfix / skip_spec_audit)\n`;
132
+ return { ok: true, exitCode: 0, stdout, skipped: true };
133
+ }
134
+
135
+ const review = findSpecReview(absSpec, { target, workspaceRoot });
136
+ if (!review.found) {
137
+ if (allowNoSpecReview) {
138
+ stdout += `WARN: missing SPEC R<n> review(--allow-no-spec-review 豁免 · 留痕)\n`;
139
+ return { ok: true, exitCode: 0, stdout, skipped: false };
140
+ }
141
+ return {
142
+ ok: false,
143
+ exitCode: 2,
144
+ reason:
145
+ 'missing SPEC R<n> review(docs/harness/reviews/ 无 spec_*_audit_R / *_ACCEPT_R · 20-spec-audit 或 --allow-no-spec-review)',
146
+ stdout,
147
+ };
148
+ }
149
+
150
+ return { ok: true, exitCode: 0, stdout, skipped: false };
151
+ }
152
+
153
+ /** SPEC 模式 handoff(verify --spec --json) */
154
+ export function buildSpecVerifyHandoff(target, options = {}) {
155
+ const { specFile, workspaceRoot, allowNoSpecReview = false } = options;
156
+ const absSpec = path.isAbsolute(specFile)
157
+ ? specFile
158
+ : path.resolve(target, specFile);
159
+ const content = fs.existsSync(absSpec) ? fs.readFileSync(absSpec, 'utf8') : '';
160
+ const skipped = shouldSkipSpecAudit(content);
161
+ const review = skipped
162
+ ? { found: false, latest: null, rounds: [], matched_pattern: null }
163
+ : findSpecReview(absSpec, { target, workspaceRoot });
164
+
165
+ const reviewOk = skipped || review.found || allowNoSpecReview;
166
+ let blocked_reason = null;
167
+ if (!reviewOk) {
168
+ blocked_reason = 'missing SPEC R<n> review';
169
+ }
170
+
171
+ return {
172
+ schema_version: '1',
173
+ mode: 'spec',
174
+ verify_ok: reviewOk,
175
+ may_start_00: reviewOk,
176
+ spec: path.basename(specFile),
177
+ spec_path: specFile.replace(/\\/g, '/'),
178
+ spec_slug: content ? extractSpecSlug(absSpec, content) : null,
179
+ spec_review_found: Boolean(review.found),
180
+ spec_review_latest: review.latest,
181
+ spec_review_rounds: review.rounds ?? [],
182
+ spec_review_skipped: skipped,
183
+ spec_review_suppressed: Boolean(allowNoSpecReview && !review.found && !skipped),
184
+ matched_pattern: review.matched_pattern ?? null,
185
+ blocked_reason,
186
+ next_hat: reviewOk ? '00' : null,
187
+ };
188
+ }
189
+
92
190
  /** 规范化 lint 结果供 handoff */
93
191
  export function lintSnapshotForHandoff(target, taskFile, { allowLintFail = false } = {}) {
94
192
  if (!taskFile) return null;
@@ -258,6 +356,16 @@ export function buildVerifyHandoff(target, options = {}) {
258
356
  export function formatAgentHint(payload) {
259
357
  const lines = ['=== Harness verify · agent-hint ==='];
260
358
 
359
+ if (payload.mode === 'spec') {
360
+ lines.push(`spec: ${payload.spec}`);
361
+ lines.push(` may_start_00: ${payload.may_start_00}`);
362
+ if (payload.blocked_reason) lines.push(` blocked: ${payload.blocked_reason}`);
363
+ if (payload.spec_review_latest) lines.push(` review: ${payload.spec_review_latest}`);
364
+ if (payload.spec_review_skipped) lines.push(` review: skipped`);
365
+ if (payload.next_hat) lines.push(` next_hat: ${payload.next_hat}`);
366
+ return lines.join('\n').trimEnd();
367
+ }
368
+
261
369
  const items = payload.tasks ?? [payload];
262
370
  for (const item of items) {
263
371
  lines.push(`task: ${item.task}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyning/harness",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "cyning-harness discipline package · init / upgrade / check CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",