@cyning/harness 2.4.0 → 2.5.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.5.0] - 2026-07-24
8
+
9
+ ### Changed
10
+
11
+ - **⚠ 行为变更 · reviews 留档闸(G2)**:`verify --task` 新增 blocking 条件——`docs/harness/reviews/task_<base>_audit_R<n>_*.md`(n≥1)不存在时 `VERIFY: BLOCKED · missing R<n> review` exit 2(即使人工闸表全 approved)。职责切分:机器只查**存在性**,审查结论仍由维护者签 `HG-AUDIT-R1` 覆盖。**豁免**:`--allow-no-review`(warn 放行 · 留痕)。
12
+ - `task close` 新增**检查 6**:归档时 R<n> 审查文应存在(纸链完整);同 `--allow-no-review` 豁免。
13
+ - `findReviewPath` 升级 `findReview`:glob `_audit_R1_` → `_audit_R` 多轮取最新;**匹配两侧剥离版本后缀**(合法变体 `task_x_v1.md` ↔ `task_x_audit_R1_*.md` · R1 审查实测案例);`review_path` 字段兼容。
14
+ - verify `--json` handoff 新增 `review_found` / `review_latest` / `review_rounds`;`may_start_30` 纳入审查文条件。
15
+
16
+ ### Notes
17
+
18
+ - 背景:机械化率审计 G2(rethink 矩阵 P0 最后一块);22/20 帽自此脱离「全帽零机械」。
19
+ - dogfood:工作区 approved 任务全过(含版本后缀变体 ops_clarify · 恰好是 R1 审查抓出的案例)。
20
+ - minor · verify 行为变更(本版核心 · 见上)· `npm test` 118 全绿 · SPEC `docs/spec/SPEC-reviews-retention-gate_v1.md`
21
+
7
22
  ## [2.4.0] - 2026-07-24
8
23
 
9
24
  ### Changed
@@ -29,6 +44,7 @@
29
44
  - **升级指引**:业务仓 `upgrade` 后若出现 `warn: obsolete`,按提示人工删除旧帽文件即可(sync 不会自动删)。
30
45
  - 背景:三方对比(后端仓=V1 手工快照 · 包内=半途状态 · V2 真值仅在工作区)· task `cyning-harness-hat-v2-split-sync`。
31
46
  - minor · 无 CLI breaking · `npm test` 111 全绿
47
+ - **已发布**:`@cyning/harness@2.4.0`(npm `latest` · 2026-07-24)· tag `v2.4.0` 已推送
32
48
 
33
49
  ## [2.3.0] - 2026-07-24
34
50
 
@@ -35,7 +35,7 @@ description: 机械化率统计矩阵 + 缺口聚类 + 候选闸优先级排序
35
35
  | 优先级 | 缺口 | 风险 | 成本 | 候选闸形态 | 挂点 |
36
36
  |---|---|---|---|---|---|
37
37
  | ~~P0~~ ✅ | **G1 · task md 结构 lint** | 高(下游全部闸的输入假设) | 低(解析与 M5 共享) | **已落地 v2.3.0**:`harness task lint`(E1–E7/W1–W3)· 不接入 verify(dogfood 后另议) | 10 产出时 |
38
- | **P0** | **G2 · reviews 留档闸** | 高(与 invoke 失守同构,且是 HG-AUDIT-R1 的签署依据) | 低(glob 命名模式) | `verify` 增检查:`reviews/task_<slug>_audit_R1_*.md` 存在(may_start_30 加条件);可选 `close` 增第 6 | verify(30 前)· close(归档前) |
38
+ | ~~P0~~ | **G2 · reviews 留档闸** | 高(与 invoke 失守同构,且是 HG-AUDIT-R1 的签署依据) | 低(glob 命名模式) | **已落地 v2.5.0**:verify blocking(`missing R<n> review`)+ close 检查 6 · 豁免 `--allow-no-review` · 匹配两侧剥离版本后缀 | verify(30 前)· close(归档前) |
39
39
  | ~~P1~~ ✅(task 部分) | **G3 · 文本纪律 grep 闸** | 中 | 极低(正则) | **已落地 v2.3.0**(E6 绝对路径);invoke 预写 approved grep 缓做(SPEC R2:误报不可控) | 同 G1 |
40
40
  | **P1** | **G4 · 思考轮结构检查** | 中(阶段 C 纪律,漏槽=思考轮形同虚设) | 中(格式变体多,须宽容:只查槽位存在与控制表字段,不查内容质量) | 并入 `task lint`:R0–R5 槽 + `actual_last_round`/`early_stop`/`residual_risks` 字段存在性 | 10 产出时 |
41
41
  | **P2** | **G7 · 执行证据** | 高但难(「真跑过」本质要 runner 见证) | 高 | `harness run -- <验证命令>`:包装执行、留 exit code + 输出摘要到 `.cyning-harness/runs/`,40 回填引用 run id | 方向二状态机内再做 |
@@ -31,6 +31,7 @@ description: 索引与背景 · 2026-07 机械化率审计思考系列 · 何时
31
31
  | 2026-07-24 | 矩阵聚合 + 缺口聚类 | 03 |
32
32
  | 2026-07-24 | 结论与立项建议 | 04 |
33
33
  | 2026-07-24 | **第一波落地**:G1+G3(task 部分)→ `task lint` v2.3.0(03 已勾 ✅);dogfood 数据:17 个 active task 中 15 个存在真实结构缺口 | 03 更新 |
34
+ | 2026-07-24 | **第二波落地**:G2 → verify/close reviews 闸 v2.5.0(03 已勾 ✅ · **P0 队列清零**);期间帽定义 V2 拆分收编(v2.4.0) | 03 更新 |
34
35
 
35
36
  **机制编号约定**(02/03 引用):M1 gate-check.sh · M2 verify · M3 audit · M4 task check · M5 task close · M6 task lint-done · M7 graph yaml · M8 graph HGM · M9 harness-sync · M10 package-scripts
36
37
 
@@ -0,0 +1,141 @@
1
+ # SPEC:reviews 留档闸(verify + close 的 R&lt;n&gt; 审查文存在性检查)(v1)
2
+
3
+ > **状态**:`done`(10-spec R0–R5 已回填 · **维护者已签收 2026-07-24** · → 00 起草 task)
4
+ > **track**:`feature`
5
+ > **关联图谱**:无(纯 Harness 工具链)
6
+ > **上游思考**:[`docs/rethink/2026-07-mechanization-rate/`](../rethink/2026-07-mechanization-rate/)(缺口 G2 · P0 最后一块)
7
+ > **下游**:SPEC 签收 → 00 起草 task → 10-task
8
+
9
+ ---
10
+
11
+ ## Harness 元信息
12
+
13
+ | 字段 | 值 |
14
+ |------|-----|
15
+ | **spec_slug** | `reviews-retention-gate` |
16
+ | **test_strategy** | `required` |
17
+ | **test_strategy_note** | verify/close 双挂点的 block/pass/豁免路径均可 fixture 驱动 |
18
+ | **entry_invoke_10_spec** | `Projects/docs/harness/invokes/by-task/cyning-harness-reviews-retention-gate/invoke_20260724_10_spec_reviews_retention_gate.md` |
19
+ | **entry_invoke_00_draft** | 工作区 `docs/harness/prompts/PROMPT_00_draft_spec_or_task_v1_zh.md` |
20
+
21
+ ---
22
+
23
+ ## 1. 背景与目标
24
+
25
+ 机械化率审计(rethink 02 C1–C4):**20-task-audit(原 22)全帽零机械**——「必须落盘 `reviews/task_<slug>_audit_R<n>_<date>.md`」是纯 Prompt 纪律,与 invoke 失守同构。`FRAGMENT_30_gate_verify` 闸扫描表里本有一行「reviews:`task_*_audit_R1_*.md` 存在且 R1 通过?」,但它是**手工填表项**,无任何代码消费。
26
+
27
+ **关键事实**:`HG-AUDIT-R1` 是 30 的硬闸且由人签,但**签署依据(R1 审查文)的存在性本身没有闸**——人可以对着一份不存在的审查文签 approved,机械层面无法发现。
28
+
29
+ **目标**:把「R&lt;n&gt; 审查文存在」从填表项升级为机械闸,双挂点:
30
+
31
+ 1. **verify(30 前)**:`may_start_30` 增加前置条件——`docs/harness/reviews/task_<base>_audit_R<n>_*.md`(n≥1)至少一份存在;
32
+ 2. **close(归档前)**:新增第 6 项检查——归档时 R&lt;n&gt; 审查文应存在(纸链完整性)。
33
+
34
+ **职责切分(核心设计)**:机器只查**存在性**(形式);审查**结论是否通过**由维护者签 `HG-AUDIT-R1` 覆盖(实质)。闸不做内容判定。
35
+
36
+ ## 2. 范围
37
+
38
+ - **D1 · verify 挂点(`lib/verify.js` + `lib/task-meta.js`)**:
39
+ - `findReviewPath` 扩展:glob 从 `_audit_R1_` 放宽为 `_audit_R`(取最新轮),返回 `{found, latest, rounds[]}`
40
+ - `verifyTarget`:taskFile 模式下审查文缺失 → `VERIFY: BLOCKED · missing R<n> review` · exit 2
41
+ - 豁免:`--allow-no-review`(warn 放行 · 留痕);handoff JSON 增加 `review_found` / `review_latest`(`review_path` 保留兼容)
42
+ - **D2 · close 挂点(`lib/task-close.js`)**:新增检查 6——R&lt;n&gt; 审查文存在;缺失 → `CLOSE: BLOCKED`;同一 `--allow-no-review` 豁免(warn)
43
+ - **D3 · 测试**:verify 三态(存在 pass / 缺失 block / 豁免 warn)+ close 检查 6 三态;`npm test` 全绿(含既有回归)
44
+ - **D4 · dogfood**:工作区 active tasks 跑 verify 报告「approved 但无审查文」分布(只报告)
45
+ - **D5 · 文档**:`FRAGMENT_30_gate_verify_v1_zh.md` 该行标注「v2.5+ verify 机械强制存在性」;CHANGELOG + 版本 **v2.5.0**
46
+
47
+ ## 3. 非范围
48
+
49
+ - 审查文**内容**判定(pass/fail 结论、核对项质量)——永远人/20 帽职责
50
+ - SPEC 审查文(20-spec-audit · HG-SPEC-SIGNOFF)留档闸——下一轮
51
+ - `task lint-done` 的 reviews 集合 diff(done↔reviews)——有需求再议
52
+ - 存量 done 任务的 reviews 补录;R 轮次与 task 回填一致性的交叉校验
53
+
54
+ ---
55
+
56
+ ## 4. 验收标准
57
+
58
+ - [ ] verify:task 有 `reviews/task_<base>_audit_R<n>_*.md`(n≥1)→ 行为不变;**缺失 → `VERIFY: BLOCKED · missing R<n> review`** exit 2(即使闸表全 approved)
59
+ - [ ] verify:`--allow-no-review` → warn + 放行;`--json` handoff 含 `review_found` / `review_latest`
60
+ - [ ] verify:多轮审查文(R1+R2)取最新轮为 `review_latest`
61
+ - [ ] close:检查 6 缺失审查文 → exit 2 不 mv;`--allow-no-review` warn 放行
62
+ - [ ] `npm test` 全绿(新增用例覆盖上述三态 · 既有 111 例回归)
63
+ - [ ] dogfood:工作区 active tasks「approved 无审查文」分布落自检结论
64
+ - [ ] FRAGMENT_30 + CHANGELOG(v2.5.0 · 行为变更醒目说明 + 豁免指引)
65
+
66
+ ---
67
+
68
+ ## 5. failure_paths
69
+
70
+ | 触发条件 | 系统行为 | 可重试 |
71
+ |----------|----------|--------|
72
+ | 30 前审查文缺失 | `VERIFY: BLOCKED · missing R<n> review` · exit 2 | 20 补审落盘后重跑;或维护者授意 `--allow-no-review` |
73
+ | 归档时审查文缺失 | `CLOSE: BLOCKED` · 不 mv | 补审 / 豁免后重跑 |
74
+ | 存量 approved 无审查文任务被挡 | 属**预期拦截**(闸的目的);豁免阀 `--allow-no-review` 留痕放行 | 同上 |
75
+ | 审查文命名不规范(无 `_audit_R` 段) | 视为缺失(形式宽容止于轮次段,文件名主体必须匹配 task basename) | 重命名后重跑 |
76
+ | 业务仓未升级 2.5.0 | 旧 verify 无此检查 · 行为不变 | upgrade |
77
+
78
+ ---
79
+
80
+ ## 6. 依赖与引用
81
+
82
+ - 复用:`lib/task-meta.js` `findReviewPath`(现 glob `_audit_R1_` · 已服务 verify --json 的 `review_path` 信息位)
83
+ - 挂点:`lib/verify.js` `verifyTarget`(taskFile 分支 · gate-check 之后);`lib/task-close.js` 检查表第 6 项
84
+ - 纪律原文:`harness/prompts/20-task-audit.md`(必须落盘 reviews)· `harness/prompts/FRAGMENT_30_gate_verify_v1_zh.md`(reviews 填表行)
85
+ - 缺口出处:rethink 02(C1–C4)· 03(G2 行)
86
+
87
+ ---
88
+
89
+ ## 7. 思考轮(10-spec 回填 · R0–R5)
90
+
91
+ ### R0 · 读入与约束
92
+
93
+ 读入:rethink 02/03(G2=P0 队列最后一块);`findReviewPath` 现实现(只 glob R1 · 只作信息位);FRAGMENT_30 的 reviews 填表行(纪律原文);v2.3 教训(lint 不接入 verify = 不误伤存量)。约束:verify 新增 blocking 条件是**行为变更**,豁免阀与 CHANGELOG 醒目说明是硬要求。
94
+
95
+ ### R1 · 范围 / 非范围 / 场景
96
+
97
+ 场景:① 30 前 verify 挡「闸表 approved 但审查文不存在」(主);② 归档时 close 保证纸链完整;③ 存量任务被挡时维护者有豁免阀。**职责切分**是本 SPEC 最重要的边界:机器查存在性(形式),人签覆盖结论(实质)——不做审查文内容判定,那是 20 帽与维护者的职责,机械判定会重蹈 slug 式误报。双挂点同波做:机制共享(同一个 finder),分开做反而要两次发版说明。
98
+
99
+ ### R2 · 方案对比
100
+
101
+ | 决策点 | 选项 | 裁定 | 理由 |
102
+ |---|---|---|---|
103
+ | 严格度 | block / warn 起步 | **block + `--allow-no-review`** | 这是 verify 的语义本职(「30 可开工?」应含签署依据存在);warn 起步等于又把纪律留在自觉层。豁免阀保灵活 |
104
+ | 挂点 | verify only / close only / 双挂点 | **双挂点同波** | finder 共享 · 一份发版说明;close 保证归档纸链(done 任务的未来审计入口) |
105
+ | 轮次语义 | 只认 R1 / 最新 R&lt;n&gt; / 要求结论 pass | **存在 ≥1 份 R&lt;n&gt;,信息位报最新轮** | R1 有阻塞 → 回填 → R2 是合法流程;结论 pass 由 HG-AUDIT-R1 人签覆盖,机器不判内容 |
106
+ | 与 `--allow-unchecked` 合并豁免阀 | 合并 / 独立 flag | **独立 `--allow-no-review`** | 语义不同(勾选豁免 vs 审查文豁免),合并会模糊审计痕迹 |
107
+ | verify 无 --task 全量模式 | 也查 reviews / 只查闸表 | **本波只查闸表(不变)** | 全量模式的存量面太大(dogfood 前先探);taskFile 模式是 30 开工的正式路径 |
108
+
109
+ ### R3 · 边界 / 失败语义 / 安全
110
+
111
+ - **存量误伤面**:受影响 = 「HG-AUDIT-R1 approved 且无 R&lt;n&gt; 审查文」的在途 task。工作区近三波 task 全有 R1 文(实测);业务仓手工快照可能有缺口 → 豁免阀 + dogfood 报告(D4)先行量化。
112
+ - **文件名形式宽容边界**:宽容到「轮次数字任意」,不宽容到「文件名主体必须 = task basename」——否则张冠李戴的审查文也能过闸,闸就失去意义。
113
+ - **兼容**:`review_path` 字段保留(handoff 既有消费者);新增字段不删旧字段。
114
+ - **安全**:verify/close 均为只读检查(close 的 mv 语义不变);无 git 动作。
115
+
116
+ ### R4 · 验收 / 可测性 / test_strategy
117
+
118
+ `test_strategy: required`。fixture:verify 三态 + 多轮取最新 + close 检查 6 三态;复用 verify.test.js / task-close.test.js 的 mkdtemp 模式。dogfood 命令与分布落自检结论(同 v2.3 模式)。
119
+
120
+ ### R5 · SPEC 签收就绪 · 是否可交 00 出 task
121
+
122
+ SPEC 自足:双挂点行为、豁免阀、职责切分、存量缓解均有明确规则。**可交 00 起草 task**。图谱:纯工具链无需 bootstrap。版本 v2.5.0(行为变更但属闸语义本职 · CHANGELOG 醒目 + 豁免指引)。落地后 rethink 矩阵 ❌ 8 → 6(C1/C2 族 ✅),22/20 帽脱离「全帽零机械」。
123
+
124
+ ### 思考轮控制
125
+
126
+ | 字段 | 值 |
127
+ |------|-----|
128
+ | `actual_last_round` | `R5` |
129
+ | `early_stop` | `no` |
130
+ | `early_stop_reason` | — |
131
+ | `residual_risks` | ① 存量仓 verify 行为变更可能挡在途 task(豁免阀 + CHANGELOG 醒目 + dogfood 量化缓解);② 「审查文命名不规范视为缺失」可能对历史命名变体过严(宽容边界已声明 · 误报可重命名或豁免);③ verify 无 --task 全量模式本波不查 reviews,遗留到 dogfood 后决策 |
132
+ | `round_extension_note` | — |
133
+
134
+ ---
135
+
136
+ ## 修订记录
137
+
138
+ | 日期 | 摘要 |
139
+ |------|------|
140
+ | 2026-07-24 | 10-spec R0–R5 同会话回填(维护者委派「开始」)· 基于 rethink G2 + findReviewPath 资产核查 |
141
+ | 2026-07-24 | **维护者签收** · 进入 00 起草 task |
@@ -14,7 +14,7 @@
14
14
  | HG-AUDIT-R1 | {读表} | {若有} | {Y/N} | Y | … |
15
15
  | HG-GRAPH-MODULES | {01_struct或—} | — | — | … | … |
16
16
 
17
- reviews:task_*_audit_R1_*.md 存在且 R1 通过? {是/否}
17
+ reviews:task_*_audit_R1_*.md 存在且 R1 通过? {是/否}(**v2.5+**:`verify` 对 R<n> 审查文**存在性**机械强制 · 缺失即 `VERIFY: BLOCKED · missing R<n> review`;结论通过与否仍由维护者签 `HG-AUDIT-R1` 覆盖)
18
18
 
19
19
  冲突规则:task表 pending 且用户/invoke 称 approved → **STOP** · 以 task 表为准
20
20
 
package/lib/cli.js CHANGED
@@ -407,7 +407,8 @@ async function cmdVerify(args) {
407
407
  const graph = rest.includes('--graph');
408
408
  const json = rest.includes('--json');
409
409
  const agentHint = rest.includes('--agent-hint');
410
- rest = rest.filter((a) => a !== '--graph' && a !== '--json' && a !== '--agent-hint');
410
+ const allowNoReview = rest.includes('--allow-no-review');
411
+ rest = rest.filter((a) => a !== '--graph' && a !== '--json' && a !== '--agent-hint' && a !== '--allow-no-review');
411
412
 
412
413
  if (rest.length > 0) {
413
414
  const err = new Error(`verify 未知参数: ${rest.join(' ')}`);
@@ -416,7 +417,7 @@ async function cmdVerify(args) {
416
417
  }
417
418
 
418
419
  const target = resolveTarget(process.cwd(), targetArg);
419
- const result = verifyTarget(target, { taskFile, graph });
420
+ const result = verifyTarget(target, { taskFile, graph, allowNoReview });
420
421
 
421
422
  // forward gate-check/audit/graph stdout if any(--json 时仍透传机械闸表)
422
423
  if (result.stdout && !json) {
@@ -429,6 +430,7 @@ async function cmdVerify(args) {
429
430
  const handoff = buildVerifyHandoff(target, {
430
431
  taskFile,
431
432
  workspaceRoot: workspaceRoot ? path.resolve(workspaceRoot) : undefined,
433
+ allowNoReview,
432
434
  });
433
435
 
434
436
  if (json) {
@@ -536,7 +538,8 @@ async function cmdTaskClose(args) {
536
538
 
537
539
  const yes = args.includes('--yes');
538
540
  const allowUnchecked = args.includes('--allow-unchecked');
539
- let rest = args.filter((a) => a !== '--yes' && a !== '--allow-unchecked');
541
+ const allowNoReview = args.includes('--allow-no-review');
542
+ let rest = args.filter((a) => a !== '--yes' && a !== '--allow-unchecked' && a !== '--allow-no-review');
540
543
  const { value: fileArg, rest: r1 } = takeOption(rest, '--file');
541
544
  rest = r1;
542
545
  const { value: targetArg, rest: r2 } = takeOption(rest, '--target');
@@ -557,6 +560,7 @@ async function cmdTaskClose(args) {
557
560
  yes,
558
561
  target: targetArg,
559
562
  allowUnchecked,
563
+ allowNoReview,
560
564
  cwd: process.cwd(),
561
565
  });
562
566
 
package/lib/task-close.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  UNCHECKED_RE,
8
8
  extractSection,
9
9
  extractTaskSlug,
10
+ findReview,
10
11
  normalizeSlug,
11
12
  parseHarnessMeta,
12
13
  } from './task-meta.js';
@@ -21,6 +22,7 @@ export function closeTaskFile(filePath, options = {}) {
21
22
  yes = false,
22
23
  target: targetArg,
23
24
  allowUnchecked = false,
25
+ allowNoReview = false,
24
26
  cwd = process.cwd(),
25
27
  } = options;
26
28
 
@@ -121,6 +123,20 @@ export function closeTaskFile(filePath, options = {}) {
121
123
  blockers.push(`状态非 done/completed(当前: ${status})`);
122
124
  }
123
125
 
126
+ // 检查 6 · R<n> 审查文留档(G2 · v2.5+)
127
+ if (repoRoot) {
128
+ const review = findReview(repoRoot, abs);
129
+ if (!review.found) {
130
+ if (allowNoReview) {
131
+ warnings.push('warn: missing R<n> review(--allow-no-review 豁免 · 留痕)');
132
+ } else {
133
+ blockers.push(
134
+ 'missing R<n> review(docs/harness/reviews/ 无该 task 的 _audit_R 文件 · 或 --allow-no-review)',
135
+ );
136
+ }
137
+ }
138
+ }
139
+
124
140
  // 归档目标
125
141
  let dest = null;
126
142
  if (targetArg) {
package/lib/task-meta.js CHANGED
@@ -119,29 +119,42 @@ export function resolveInvokePath(rawPath, { target, workspaceRoot } = {}) {
119
119
  }
120
120
 
121
121
  /**
122
- * 查找最新 R1 审查文(相对 target)。
122
+ * 查找 task R<n> 审查文(verify/close 共用)。
123
+ * 匹配规则(G2 · R1-B1):task basename 与审查文文件名均剥离 `_v\d+$` 后前缀比较;
124
+ * 文件名须含 `_audit_R<n>_` 段。返回 {found, latest, rounds}。
123
125
  */
124
- export function findReviewPath(target, taskFile) {
126
+ export function findReview(target, taskFile) {
125
127
  const reviewsDir = path.join(target, 'docs/harness/reviews');
126
- if (!fs.existsSync(reviewsDir)) return null;
127
-
128
- const taskBase = path.basename(taskFile, '.md');
129
- const prefix = `${taskBase}_audit_R1_`;
128
+ const empty = { found: false, latest: null, rounds: [] };
129
+ if (!fs.existsSync(reviewsDir)) return empty;
130
130
 
131
- let best = null;
132
- let bestMtime = 0;
131
+ const stripVer = (s) => s.replace(/_v\d+$/, '');
132
+ const base = stripVer(path.basename(taskFile, '.md'));
133
+ const REVIEW_RE = /^(task_.+?)_audit_R(\d+)_.*\.md$/;
133
134
 
135
+ const matches = [];
134
136
  for (const name of fs.readdirSync(reviewsDir)) {
135
- if (!name.startsWith(prefix) || !name.endsWith('.md')) continue;
136
- const full = path.join(reviewsDir, name);
137
- const mtime = fs.statSync(full).mtimeMs;
138
- if (mtime > bestMtime) {
139
- bestMtime = mtime;
140
- best = path.relative(target, full).replace(/\\/g, '/');
141
- }
137
+ const m = name.match(REVIEW_RE);
138
+ if (!m) continue;
139
+ if (stripVer(m[1]) !== base) continue;
140
+ matches.push({ name, round: parseInt(m[2], 10), mtime: fs.statSync(path.join(reviewsDir, name)).mtimeMs });
142
141
  }
142
+ if (matches.length === 0) return empty;
143
+
144
+ matches.sort((a, b) => b.round - a.round || b.mtime - a.mtime);
145
+ const latest = matches[0];
146
+ return {
147
+ found: true,
148
+ latest: path.join('docs/harness/reviews', latest.name).replace(/\\/g, '/'),
149
+ rounds: [...new Set(matches.map((x) => x.round))].sort((a, b) => a - b),
150
+ };
151
+ }
143
152
 
144
- return best;
153
+ /**
154
+ * 查找最新 R1+ 审查文(相对 target)· 兼容旧签名:返回路径或 null。
155
+ */
156
+ export function findReviewPath(target, taskFile) {
157
+ return findReview(target, taskFile).latest;
145
158
  }
146
159
 
147
160
  /**
@@ -166,7 +179,7 @@ export function buildTaskHandoff(target, taskFile, options = {}) {
166
179
  const entry20 = resolveInvokePath(entry20Raw, { target, workspaceRoot });
167
180
  warnings.push(...entry30.warnings, ...entry20.warnings);
168
181
 
169
- const reviewPath = findReviewPath(target, taskFile);
182
+ const review = findReview(target, taskFile);
170
183
 
171
184
  return {
172
185
  task: path.basename(taskFile),
@@ -174,7 +187,10 @@ export function buildTaskHandoff(target, taskFile, options = {}) {
174
187
  task_slug: meta.task_slug ?? null,
175
188
  may_start_30: gateEval.may_start_30,
176
189
  blocked_reason: gateEval.blocked_reason,
177
- review_path: reviewPath,
190
+ review_path: review.latest,
191
+ review_found: review.found,
192
+ review_latest: review.latest,
193
+ review_rounds: review.rounds,
178
194
  entry_invoke_20: entry20Raw,
179
195
  entry_invoke_20_resolved: entry20.resolved,
180
196
  entry_invoke_30: entry30Raw,
package/lib/verify.js CHANGED
@@ -3,13 +3,13 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { resolveHarnessRootForTarget, wizardScript } from './paths.js';
5
5
  import { auditTarget } from './audit.js';
6
- import { buildTaskHandoff, listActiveTasks } from './task-meta.js';
6
+ import { buildTaskHandoff, findReview, listActiveTasks } from './task-meta.js';
7
7
 
8
8
  /**
9
- * 30 前聚合验证:gate-check + audit D5(仅 --task)+ S5 git-clean warn + 可选 --graph
9
+ * 30 前聚合验证:gate-check + audit D5(仅 --task)+ reviews 留档闸(仅 --task)+ S5 git-clean warn + 可选 --graph
10
10
  */
11
11
  export function verifyTarget(target, options = {}) {
12
- const { taskFile, graph } = options;
12
+ const { taskFile, graph, allowNoReview = false } = options;
13
13
  const harnessRoot = resolveHarnessRootForTarget(target);
14
14
 
15
15
  // 1. gate-check(含 --graph)
@@ -34,9 +34,24 @@ export function verifyTarget(target, options = {}) {
34
34
  stdout: gateResult.stdout,
35
35
  };
36
36
  }
37
+
38
+ // 3. reviews 留档闸(G2 · v2.5+):R<n> 审查文存在性(存在性=机器 · 结论=人签)
39
+ const review = findReview(target, taskFile);
40
+ if (!review.found) {
41
+ if (allowNoReview) {
42
+ gateResult.stdout += `WARN: missing R<n> review(--allow-no-review 豁免 · 留痕)\n`;
43
+ } else {
44
+ return {
45
+ ok: false,
46
+ exitCode: 2,
47
+ reason: 'missing R<n> review(docs/harness/reviews/ 无该 task 的 _audit_R 文件 · 20 补审或 --allow-no-review)',
48
+ stdout: gateResult.stdout,
49
+ };
50
+ }
51
+ }
37
52
  }
38
53
 
39
- // 3. S5 git-clean:warn 不挡
54
+ // 4. S5 git-clean:warn 不挡
40
55
  const gitResult = runGitCleanCheck(target);
41
56
  if (gitResult.dirty) {
42
57
  // 把 warn 拼到 stdout 里让 CLI 透传
@@ -156,15 +171,20 @@ function runGitCleanCheck(target) {
156
171
  * 构建 verify --json / --agent-hint 的 Agent handoff 载荷。
157
172
  */
158
173
  export function buildVerifyHandoff(target, options = {}) {
159
- const { taskFile, workspaceRoot } = options;
174
+ const { taskFile, workspaceRoot, allowNoReview = false } = options;
160
175
  const handoffOpts = { workspaceRoot };
161
176
 
162
177
  if (taskFile) {
163
178
  const handoff = buildTaskHandoff(target, taskFile, handoffOpts);
179
+ const reviewOk = handoff.review_found || allowNoReview;
180
+ const verifyOk = handoff.may_start_30 && reviewOk;
164
181
  return {
165
182
  schema_version: '1',
166
- verify_ok: handoff.may_start_30,
183
+ verify_ok: verifyOk,
167
184
  ...handoff,
185
+ may_start_30: verifyOk,
186
+ blocked_reason: handoff.blocked_reason ?? (reviewOk ? null : 'missing R<n> review'),
187
+ next_hat: verifyOk ? '30' : null,
168
188
  };
169
189
  }
170
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyning/harness",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "cyning-harness discipline package · init / upgrade / check CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",