agentic-flow-kit 1.3.0 → 1.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.
Files changed (139) hide show
  1. package/README.md +10 -4
  2. package/modules/hosts/claude/agents/implementer.md +5 -6
  3. package/modules/hosts/claude/agents/independent-reviewer.md +6 -6
  4. package/modules/hosts/claude/agents/ui-verifier.md +5 -5
  5. package/modules/hosts/claude/commands/wf-build.md +21 -21
  6. package/modules/hosts/claude/commands/wf-deploy.md +2 -2
  7. package/modules/hosts/claude/commands/wf-design.md +8 -8
  8. package/modules/hosts/claude/commands/wf-gate-checklist.md +8 -8
  9. package/modules/hosts/claude/commands/wf-maintain.md +12 -12
  10. package/modules/hosts/claude/commands/wf-new-task.md +9 -9
  11. package/modules/hosts/claude/commands/wf-pipeline-run.md +3 -3
  12. package/modules/hosts/claude/commands/wf-plan.md +10 -11
  13. package/modules/hosts/claude/commands/wf-review.md +4 -4
  14. package/modules/hosts/claude/commands/wf-source-sync-check.md +10 -10
  15. package/modules/hosts/claude/commands/wf-sync-hosts.md +9 -9
  16. package/modules/hosts/claude/commands/wf-test.md +6 -7
  17. package/modules/hosts/codex/agents/implementer.md +5 -6
  18. package/modules/hosts/codex/agents/independent-reviewer.md +6 -6
  19. package/modules/hosts/codex/agents/ui-verifier.md +5 -5
  20. package/modules/hosts/codex/commands/wf-build.md +21 -21
  21. package/modules/hosts/codex/commands/wf-deploy.md +2 -2
  22. package/modules/hosts/codex/commands/wf-design.md +8 -8
  23. package/modules/hosts/codex/commands/wf-gate-checklist.md +8 -8
  24. package/modules/hosts/codex/commands/wf-maintain.md +12 -12
  25. package/modules/hosts/codex/commands/wf-new-task.md +9 -9
  26. package/modules/hosts/codex/commands/wf-pipeline-run.md +3 -3
  27. package/modules/hosts/codex/commands/wf-plan.md +10 -11
  28. package/modules/hosts/codex/commands/wf-review.md +4 -4
  29. package/modules/hosts/codex/commands/wf-source-sync-check.md +10 -10
  30. package/modules/hosts/codex/commands/wf-sync-hosts.md +9 -9
  31. package/modules/hosts/codex/commands/wf-test.md +6 -7
  32. package/modules/hosts/cursor/agents/implementer.md +5 -6
  33. package/modules/hosts/cursor/agents/independent-reviewer.md +6 -6
  34. package/modules/hosts/cursor/agents/ui-verifier.md +5 -5
  35. package/modules/hosts/cursor/commands/wf-build.md +21 -21
  36. package/modules/hosts/cursor/commands/wf-deploy.md +2 -2
  37. package/modules/hosts/cursor/commands/wf-design.md +8 -8
  38. package/modules/hosts/cursor/commands/wf-gate-checklist.md +8 -8
  39. package/modules/hosts/cursor/commands/wf-maintain.md +12 -12
  40. package/modules/hosts/cursor/commands/wf-new-task.md +9 -9
  41. package/modules/hosts/cursor/commands/wf-pipeline-run.md +3 -3
  42. package/modules/hosts/cursor/commands/wf-plan.md +10 -11
  43. package/modules/hosts/cursor/commands/wf-review.md +4 -4
  44. package/modules/hosts/cursor/commands/wf-source-sync-check.md +10 -10
  45. package/modules/hosts/cursor/commands/wf-sync-hosts.md +9 -9
  46. package/modules/hosts/cursor/commands/wf-test.md +6 -7
  47. package/modules/hosts/omp/agents/implementer.md +5 -6
  48. package/modules/hosts/omp/agents/independent-reviewer.md +6 -6
  49. package/modules/hosts/omp/agents/ui-verifier.md +5 -5
  50. package/modules/hosts/opencode/agents/implementer.md +5 -6
  51. package/modules/hosts/opencode/agents/independent-reviewer.md +6 -6
  52. package/modules/hosts/opencode/agents/ui-verifier.md +5 -5
  53. package/modules/hosts/opencode/commands/wf-build.md +21 -21
  54. package/modules/hosts/opencode/commands/wf-deploy.md +2 -2
  55. package/modules/hosts/opencode/commands/wf-design.md +8 -8
  56. package/modules/hosts/opencode/commands/wf-gate-checklist.md +8 -8
  57. package/modules/hosts/opencode/commands/wf-maintain.md +12 -12
  58. package/modules/hosts/opencode/commands/wf-new-task.md +9 -9
  59. package/modules/hosts/opencode/commands/wf-pipeline-run.md +3 -3
  60. package/modules/hosts/opencode/commands/wf-plan.md +10 -11
  61. package/modules/hosts/opencode/commands/wf-review.md +4 -4
  62. package/modules/hosts/opencode/commands/wf-source-sync-check.md +10 -10
  63. package/modules/hosts/opencode/commands/wf-sync-hosts.md +9 -9
  64. package/modules/hosts/opencode/commands/wf-test.md +6 -7
  65. package/modules/hosts/trae/agents/implementer.md +5 -6
  66. package/modules/hosts/trae/agents/independent-reviewer.md +6 -6
  67. package/modules/hosts/trae/agents/ui-verifier.md +5 -5
  68. package/modules/hosts/trae/commands/wf-build.md +21 -21
  69. package/modules/hosts/trae/commands/wf-deploy.md +2 -2
  70. package/modules/hosts/trae/commands/wf-design.md +8 -8
  71. package/modules/hosts/trae/commands/wf-gate-checklist.md +8 -8
  72. package/modules/hosts/trae/commands/wf-maintain.md +12 -12
  73. package/modules/hosts/trae/commands/wf-new-task.md +9 -9
  74. package/modules/hosts/trae/commands/wf-pipeline-run.md +3 -3
  75. package/modules/hosts/trae/commands/wf-plan.md +10 -11
  76. package/modules/hosts/trae/commands/wf-review.md +4 -4
  77. package/modules/hosts/trae/commands/wf-source-sync-check.md +10 -10
  78. package/modules/hosts/trae/commands/wf-sync-hosts.md +9 -9
  79. package/modules/hosts/trae/commands/wf-test.md +6 -7
  80. package/modules/hosts/zcode/agents/implementer.md +5 -6
  81. package/modules/hosts/zcode/agents/independent-reviewer.md +6 -6
  82. package/modules/hosts/zcode/agents/ui-verifier.md +5 -5
  83. package/package.json +7 -2
  84. package/src/doctor.mjs +1 -1
  85. package/src/init.test.mjs +1 -1
  86. package/src/run-tests.mjs +15 -1
  87. package/src/sync-hosts.test.mjs +4 -4
  88. package/src/sync.test.mjs +1 -1
  89. package/templates/_agents/commands/build.md +21 -21
  90. package/templates/_agents/commands/deploy.md +2 -2
  91. package/templates/_agents/commands/design.md +8 -8
  92. package/templates/_agents/commands/gate-checklist.md +8 -8
  93. package/templates/_agents/commands/maintain.md +12 -12
  94. package/templates/_agents/commands/new-task.md +9 -9
  95. package/templates/_agents/commands/pipeline-run.md +3 -3
  96. package/templates/_agents/commands/plan.md +10 -11
  97. package/templates/_agents/commands/review.md +4 -4
  98. package/templates/_agents/commands/source-sync-check.md +10 -10
  99. package/templates/_agents/commands/sync-hosts.md +9 -9
  100. package/templates/_agents/commands/test.md +6 -7
  101. package/templates/_agents/roles/implementer.md +5 -6
  102. package/templates/_agents/roles/independent-reviewer.md +6 -6
  103. package/templates/_agents/roles/ui-verifier.md +5 -5
  104. package/templates/_agents/scripts/agg-delegations.cjs +41 -13
  105. package/templates/_agents/scripts/agg-delegations.test.mjs +78 -3
  106. package/templates/_agents/scripts/agg-gate-stats.mjs +113 -0
  107. package/templates/_agents/scripts/agg-gate-stats.test.mjs +95 -0
  108. package/templates/_agents/scripts/check-hygiene.mjs +167 -0
  109. package/templates/_agents/scripts/check-hygiene.test.mjs +182 -0
  110. package/templates/_agents/scripts/check-ledger-invariant.mjs +0 -1
  111. package/templates/_agents/scripts/check-loop.mjs +112 -117
  112. package/templates/_agents/scripts/check-loop.test.mjs +115 -1
  113. package/templates/_agents/scripts/commit-check-trigger.test.mjs +0 -1
  114. package/templates/_agents/scripts/confirm-doc.mjs +27 -7
  115. package/templates/_agents/scripts/confirm-doc.test.mjs +35 -0
  116. package/templates/_agents/scripts/ensure-board.mjs +2 -2
  117. package/templates/_agents/scripts/fill-intent.mjs +2 -2
  118. package/templates/_agents/scripts/fill-plan.mjs +1 -2
  119. package/templates/_agents/scripts/gate-seg.mjs +54 -0
  120. package/templates/_agents/scripts/gate-seg.test.mjs +112 -0
  121. package/templates/_agents/scripts/gen-wiki-board.mjs +1 -1
  122. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +76 -9
  123. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +64 -3
  124. package/templates/_agents/scripts/gen-workflow-index.test.mjs +1 -1
  125. package/templates/_agents/scripts/gen-workflow-metrics.mjs +1 -1
  126. package/templates/_agents/scripts/kb-search.mjs +1 -2
  127. package/templates/_agents/scripts/kb-search.test.mjs +1 -1
  128. package/templates/_agents/scripts/pipeline-run-docs.mjs +76 -0
  129. package/templates/_agents/scripts/pipeline-run-run.mjs +63 -0
  130. package/templates/_agents/scripts/pipeline-run.mjs +8 -128
  131. package/templates/_agents/scripts/policy.mjs +37 -3
  132. package/templates/_agents/scripts/source-sync-check.mjs +2 -2
  133. package/templates/_agents/scripts/source-sync-check.test.mjs +1 -1
  134. package/templates/_agents/scripts/verify-wiki-consistency.mjs +0 -1
  135. package/templates/_agents/scripts/verify.mjs +27 -2
  136. package/templates/_agents/scripts/verify.test.mjs +53 -0
  137. package/templates/_agents/scripts/workflow-board-server.test.mjs +0 -1
  138. package/templates/_agents/scripts/workflow-enums.test.mjs +0 -1
  139. package/templates/_agents/scripts-test-exempt.txt +4 -1
@@ -30,7 +30,7 @@
30
30
  // 证据可写在 [x] 行的续行(仓库通写法「(证据:…)」另起一行;全/半角冒号皆认)
31
31
  // 9. 文件名英文 kebab-case(非 ASCII 文件名=warning,2026-09-11 规则)
32
32
  // 10. 级别 vs 迁移文件一致性(L1/L2 入口文档加入提交触及迁移 SQL/Migrations=疑似判低,warning)
33
- // 11. workflow/INDEX.md 漂移(活跃层索引与磁盘不一致=warning,2026-09-21 检索层;调生成器 --check,口径单一)
33
+ // 11. 生成物漂移:workflow/INDEX.md(2026-09-21 检索层) + workflow/DASHBOARD.md(2026-10-08 扩覆盖面)=warning;
34
34
  // 12. frontmatter「模块:」合法性(枚举非法 / 2026-09-22 起新建缺字段=warning;词表单源 .agents/workflow-modules.txt)
35
35
  // 13. 常驻面体积预算(超限=warning;判定单源 rule-budget.sh——经 sh 调用,
36
36
  // 无 sh 环境静默跳过:advisory 级且 pre-commit 侧在 git 钩子 sh 环境照常硬拦)
@@ -109,8 +109,6 @@ import os from 'node:os';
109
109
  import path from 'node:path';
110
110
  import { spawnSync } from 'node:child_process';
111
111
  import { createHash } from 'node:crypto';
112
- import { createRequire } from 'node:module';
113
- import { fileURLToPath } from 'node:url';
114
112
  import { loadEnums } from './workflow-enums.mjs';
115
113
  import { runCheck16 } from './check-metric-claims.mjs';
116
114
  import { auditEnabled, loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
@@ -118,25 +116,31 @@ import { laneOfEntry } from './stage-gates.mjs';
118
116
  // MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
119
117
  // 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
120
118
  import { MARK_RE, approvedTraceHit } from './stage-gates.mjs';
119
+ // 卫生类检查已拆出(2026-10-08 selfmeasure-and-modularize):检查 2/9/11/12/13 迁入 check-hygiene.mjs,
120
+ // 本文件只组装 ctx 并消费返回的文案数组(先例 = check-metric-claims.mjs 承载检查 16)。
121
+ // 调用点位置即输出顺序位:须留在检查 10 与 12 之间以保持 warnings 行序(稳定输出契约),见该模块内注释。
122
+ import { runCheckHygiene } from './check-hygiene.mjs';
123
+ import { gateSeg, finishSegs, makeCollector } from './gate-seg.mjs';
121
124
 
122
- const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
123
125
  // CHECK_LOOP_GIT:测试注入钩子(指向不存在/不可执行路径可稳定触发 spawn 异常);未设置时与原行为逐字节一致(2026-10-05-gitout-fail-open)
124
126
  const GIT = process.env.CHECK_LOOP_GIT || (process.platform === 'win32' ? 'git.exe' : 'git');
125
127
 
126
128
  function takeOpts() {
127
129
  const args = process.argv.slice(2);
128
- const opts = { rev: null, hardening: false };
130
+ const opts = { rev: null, hardening: false, gateStats: false };
129
131
  for (let i = 0; i < args.length; i++) {
130
132
  if (args[i] === '--rev') {
131
133
  opts.rev = args[++i] ?? '';
132
134
  if (!opts.rev) {
133
- console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening]');
135
+ console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening] [--gate-stats]');
134
136
  process.exit(1);
135
137
  }
136
138
  } else if (args[i] === '--hardening') {
137
139
  opts.hardening = true; // 加固门(2026-09-30 hybrid-governance-explore-hardening):pre-push 对 main 目标传入
140
+ } else if (args[i] === '--gate-stats') {
141
+ opts.gateStats = true; // 门禁 ROI 落盘(2026-10-08 gate-roi-metrics):默认路径输出逐字节不变
138
142
  } else {
139
- console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening]');
143
+ console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening] [--gate-stats]');
140
144
  process.exit(1);
141
145
  }
142
146
  }
@@ -148,6 +152,29 @@ function takeOpts() {
148
152
  // 优先于 CHECK_LOOP_ROOT env 优先于 git toplevel(CLI 缺省行为逐字节不变);返回 { exitCode, blockers, warnings },
149
153
  // 输出打印照旧在本函数内(稳定输出契约:banner 后空行、条目 `- ` 前缀、HARD-BLOCK/WARN 两段式——消费者 .githooks/pre-push)。
150
154
  // opts.rev 的 worktree 清理挂 process.on('exit')——进程内多次调用累计监听器(测试场景 ≤3 次,可接受)。
155
+ // ---- 门禁 ROI 插桩件已抽至 gate-seg.mjs(2026-10-08 gate-roi-metrics)----
156
+ // 抽因:check-loop.mjs 已 import check-hygiene.mjs,后者若再 import check-loop.mjs 即循环依赖——
157
+ // 而两者都要插桩,判据单源必须只有一处。gateSeg / finishSegs / makeCollector 见该件;
158
+ // 本文件在每段开头调 markGate(id,label),统计由数组长度差推出,不动任何 push 调用点。
159
+
160
+ // writeGateStats:门禁 ROI 落盘(2026-10-08 gate-roi-metrics)——append 一行到
161
+ // <root>/.agents/cache/gate-stats.jsonl(该目录已 gitignore,属运行时数据、不入 git、不进 kit.json
162
+ // managed 台账——刻意避开 papercuts 2026-10-05 探针临时件被 sync 收编的事故面)。
163
+ // IO 失败只出账不阻断(沿 verify.mjs 凭证落账同款容错——度量失败不该让门禁变成红灯)。
164
+ export function writeGateStats(root, segs) {
165
+ try {
166
+ const dir = path.join(root, '.agents', 'cache');
167
+ fs.mkdirSync(dir, { recursive: true });
168
+ const total = segs.reduce((a, s) => a + s.ms, 0);
169
+ fs.appendFileSync(path.join(dir, 'gate-stats.jsonl'),
170
+ `${JSON.stringify({ ts: new Date().toISOString(), root, segs, totalMs: total })}\n`);
171
+ const hits = segs.filter((s) => s.warns || s.blocks).length;
172
+ console.error(`[gate-stats] ${segs.length} 段已记账(命中 ${hits} 段)→ .agents/cache/gate-stats.jsonl`);
173
+ } catch (e) {
174
+ console.error(`[gate-stats] ⚠️ 落盘失败(${e.code || e.message})——不阻断门禁`);
175
+ }
176
+ }
177
+
151
178
  export function runCheckLoop(opts = {}) {
152
179
  const REV_ARG = opts.rev ?? null;
153
180
  const HARDENING = !!opts.hardening;
@@ -293,6 +320,10 @@ export function runCheckLoop(opts = {}) {
293
320
  const DOC_DIRS = ['intents', 'specs', 'plans', 'incidents'];
294
321
  const blockers = [];
295
322
  const warnings = [];
323
+ // 门禁 ROI 收集器(2026-10-08 gate-roi-metrics):引用同一对数组 + 段游标,不复制任何数据。
324
+ // 默认路径不打印不写盘(见 gate-seg.mjs 注释的输出契约硬约束)。
325
+ const gateStats = makeCollector(warnings, blockers);
326
+ const markGate = (id, label) => gateSeg(gateStats, id, label); // 局部别名,避免遮蔽同名导出
296
327
  // audit:false(init 新装)只保留 blockers。缺省与 audit:true 保持全量警告。
297
328
  // 硬规则:配对 / 验收勾验的阻断 / 确认留痕 / 发版草稿。卫生项走 warnings。
298
329
  const kitPolicy = loadKitPolicy(ROOT);
@@ -318,6 +349,7 @@ export function runCheckLoop(opts = {}) {
318
349
  };
319
350
 
320
351
  // --- 1. intent/spec/plan 同名配对 + 状态确认 + L3 确认三件 + 风险泳道一致性 [hard-block] ---
352
+ markGate('1', '意图/spec/plan 配对');
321
353
  for (const intent of docFiles('intents')) {
322
354
  const base = path.basename(intent);
323
355
  const st = fmGet(intent, '状态');
@@ -383,51 +415,17 @@ export function runCheckLoop(opts = {}) {
383
415
  }
384
416
  }
385
417
 
386
- // --- 2. 模板字段占位符残留 [warning](排除运行时文件名格式与协议样板,口径沿 sh 版)---
387
- {
388
- const phRe = /YYYY-MM-DD|<主题>|<日期 主题>|L0 \/ L1 \/ L2 \/ L3|draft \/ approved \/ done|open \/ fixed \/ closed/;
389
- const boilerRe = /\.\.\/specs\/[A-Za-z0-9-]*\.md|写明如何满足|防复发验证|_YYYY-MM-DD\.|format\('YYYY-MM-DD'\)|value-format="YYYY-MM-DD"|确认结果:approved(YYYY-MM-DD 用户对话内确认);done(YYYY-MM-DD 关单,随入口文档置终态)/;
390
- // plan 确认节样板豁免(2026-10-03 plan-confirm-boiler):plan 模板「确认与复核」节自带
391
- // 「确认结果:approved(YYYY-MM-DD 用户对话内确认);done(YYYY-MM-DD 关单,随入口文档置终态)」
392
- // 样板句,未回填不等于漏填占位(确认由 confirm-doc 机器兜底)——精确豁免整句,真实占位(如
393
- // `日期: YYYY-MM-DD`)照拦。
394
- // 命名约定豁免:`<主题>` 与 `.md` 同行 = 在描述文件命名规则(如 `.agents/workflows/<主题>.md`、
395
- // 「复制本模板为 YYYY-MM-DD-<主题>.md」),非未填占位符;真未填的占位(标题 `# INTENT — <主题>`、
396
- // `日期: YYYY-MM-DD`)不含 .md,仍照拦(2026-09-25-wf-runtime incident 记录的误报口径)
397
- const isNamingConv = (line) => line.includes('<主题>') && /\.md/.test(line);
398
- // 样例引用豁免(2026-10-02 caliber-convergence 假阳性消除,先例=检查 18 行尾归一):命中位于行内代码
399
- // (反引号包裹)或围栏代码块内 = 引用样例而非未填占位符,不报;正文裸占位符照报——真占位符均为
400
- // 裸文本(模板正文无反引号包裹),检测面不缩。
401
- // 日期显示格式豁免(2026-10-06 check2-datetime-literal-exempt):`YYYY-MM-DD HH:mm(:ss)` 是功能
402
- // 自实现的显示格式描述(文档叙述「匹配时间列显示为 YYYY-MM-DD HH:mm」属常态——cancel-export
403
- // intent/plan 实证 2 条 advisory、2026-10-06 loop-audit intent 现场复现),非未填占位——行内剔除
404
- // 后再判;真占位(`日期: YYYY-MM-DD`,无时间后缀)不含该形态仍拦。只豁免实证形态,变体(斜杠日期
405
- // /十二小时制等)出现假阳性再扩。
406
- const stripSamples = (lines) => {
407
- let inFence = false;
408
- return lines.map((line) => {
409
- if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; return ''; }
410
- if (inFence) return '';
411
- return line.replace(/`[^`]*`/g, '').replace(/YYYY-MM-DD\s+HH:mm(:ss)?/g, '');
412
- });
413
- };
414
- const groups = new Map(); // file -> [ "行号:内容" ](首现序)
415
- for (const sub of DOC_DIRS) {
416
- for (const f of docFiles(sub)) {
417
- const lines = linesOf(f) || [];
418
- const hits = [];
419
- stripSamples(lines).forEach((line, i) => {
420
- if (phRe.test(line) && !boilerRe.test(line) && !isNamingConv(line)) hits.push(`${i + 1}:${lines[i]}`);
421
- });
422
- if (hits.length) groups.set(f, hits);
423
- }
424
- }
425
- for (const [f, hits] of groups) {
426
- warnings.push(`- [WARN 模板未填] ${path.relative(ROOT, f).split(path.sep).join('/')} 含模板占位符:\n${hits.join('\n')}`);
427
- }
428
- }
429
-
418
+ // --- 卫生类检查 2 / 9 / 11 / 12 / 13(2026-10-08 selfmeasure-and-modularize 拆出)---
419
+ // ⚠️ 调用点位置即输出顺序位:原实现分处五处,逐条顺序 2 → 9 → 11 → 12 → 13,而 warnings 按插入序输出、
420
+ // 行序属稳定输出契约(pre-push / pre-commit 消费者 + doctor 按 WARN 行计数)。拆成一次调用后本块统一落在
421
+ // 原检查 2 的位置,**块内顺序由 check-hygiene.mjs 保证**(与其内注释互引)。新增检查请按原编号插入
422
+ // 到正确位置,勿一律追加末尾。
423
+ // ctx 只传既有 helper、模块内零复刻:docFiles 的 tracked 过滤、fmGet 的 frontmatter 受限子集、
424
+ // linesOf 的读异常响亮出账各自承载不可漂移语义,复刻即造口径分叉。audit:false 下 warnings.push 是
425
+ // no-op,卫生项因此不出账(硬规则语义保持)。
426
+ warnings.push(...runCheckHygiene({ root: ROOT, WF, DOC_DIRS, docFiles, linesOf, fmGet, isTracked, readdirOrNull, kitPolicy, gateStats }));
430
427
  // --- 3. incidents 复盘三件套 + 状态严格枚举 + 新 intent 回路(回路断档 = hard)---
428
+ markGate('3', 'incidents 复盘三件套');
431
429
  for (const inc of docFiles('incidents')) {
432
430
  const name = path.basename(inc);
433
431
  const lines = linesOf(inc) || [];
@@ -472,6 +470,7 @@ export function runCheckLoop(opts = {}) {
472
470
  }
473
471
 
474
472
  // --- 4. 引用有效性 [warning](.agents/ 路径须存在;skills 特例允许用户级 ~/.agents/skills)---
473
+ markGate('4', '引用有效性');
475
474
  {
476
475
  // 引用可带 {a,b,c} 花括号展开(仓库通写法 `fill-{intent,spec,plan}.mjs`)或 `*` 通配(`fill-*.mjs`)——
477
476
  // 花括号展开逐路查存在,任一路存在即有效;`*` 按目录枚举 glob 匹配,命中即有效;全不中才算断档
@@ -535,6 +534,7 @@ export function runCheckLoop(opts = {}) {
535
534
  }
536
535
 
537
536
  // --- 5. 子智能体角色契约 + 宿主 Adapter 一致性 + 旧委派残留 + 钉死模型 [warning] ---
537
+ markGate('5', '角色契约与宿主 Adapter');
538
538
  {
539
539
  const cmdFiles = () => (readdirOrNull(path.join(ROOT, '.agents', 'commands')) || [])
540
540
  .filter((f) => f.endsWith('.md')).map((f) => `.agents/commands/${f}`);
@@ -593,6 +593,7 @@ export function runCheckLoop(opts = {}) {
593
593
  }
594
594
 
595
595
  // --- 6. 阶段索引同步 [warning] ---
596
+ markGate('6', '阶段索引同步');
596
597
  for (const cmd of ['plan', 'design', 'build', 'test', 'deploy', 'maintain', 'review']) {
597
598
  for (const doc of ['AGENTS.md', '.agents/commands/new-task.md']) {
598
599
  // existsSync 守卫(2026-10-06-check19-entry-enoent 同类):两文件可合法缺失(下方 text 判空本就
@@ -714,7 +715,7 @@ export function runCheckLoop(opts = {}) {
714
715
  if (/^##\s+(改动面|任务拆解|改动方案)/.test(line)) { inDeclaredSec = true; continue; }
715
716
  if (inDeclaredSec && /^##\s+/.test(line)) { inDeclaredSec = false; continue; }
716
717
  if (inDeclaredSec) {
717
- const fileMatch = line.match(/([a-zA-Z0-9._\/-]+\.[a-zA-Z0-9]+)/);
718
+ const fileMatch = line.match(/([a-zA-Z0-9._/-]+\.[a-zA-Z0-9]+)/);
718
719
  if (fileMatch) declaredFiles.push(fileMatch[1]);
719
720
  }
720
721
  }
@@ -744,6 +745,7 @@ export function runCheckLoop(opts = {}) {
744
745
  }
745
746
 
746
747
  // --- 8. intent 验收标准对账(done 须逐条勾验并补证据;新建 hard,存量聚合 warning)---
748
+ markGate('8', '验收标准对账');
747
749
  // 生效日锚 2026-09-28 改「git 首次加入日期」(此前取文件名前 10 字符——命名规范强制的字段、
748
750
  // 写早零成本,见 incidents/2026-09-28-check8-git-anchor)。非 git → 不可判定 → 走存量口径(不误报 hard)。
749
751
  {
@@ -752,15 +754,55 @@ export function runCheckLoop(opts = {}) {
752
754
  // 测试绿凭证状态(2026-10-07 verify-evidence):台账读一次循环外缓存;policy 缺 verifySince
753
755
  //(v1-v4)→ ctxBase null → 对账整体跳过(向后兼容)
754
756
  const verifySinceOf = kitPolicy.verifySince;
755
- const verifyFresh = (() => {
757
+ // 台账内容循环外读一次并缓存(doc 过滤是纯内存行匹配,不新增子进程——门禁成本零增加)。
758
+ const verifyLines = (() => {
756
759
  try {
757
760
  const vp = path.join(ROOT, '.agents', 'verifications.jsonl');
758
- return fs.existsSync(vp) ? hasFreshVerifyLine(fs.readFileSync(vp, 'utf8').split(/\r?\n/)) : false;
759
- } catch { return false; }
761
+ return fs.existsSync(vp) ? fs.readFileSync(vp, 'utf8').split(/\r?\n/) : null;
762
+ } catch { return null; }
760
763
  })();
764
+ // 全局绿行(v5 语义):仅供 v1-v5 装户走原路径,本仓 v6+ 不再消费它。
765
+ const verifyFreshGlobal = verifyLines ? hasFreshVerifyLine(verifyLines) : false;
766
+ // 2026-10-08 verify-doc-binding:v6+(verifyDocSince)按本 doc 精确匹配;缺键维持全局布尔。
767
+ const verifyDocBound = typeof kitPolicy.verifyDocSince === 'string';
768
+ // 2026-10-09 verifydoc-anchor:v6 精确匹配的**追溯边界**——只对「确认门 done ts ≥ verifyDocSince」的单生效。
769
+ // 背景(用户 2026-10-09 拍板 D):4 单(verify-evidence / workflow-dashboard / selfmeasure / gate-roi-metrics)
770
+ // 的 done ts 均 < v6 上线日(2026-10-08 17:37),它们关单时既无 --doc 机制、也无从补证——被精确匹配追责
771
+ // 会产 9 条不可消退 advisory(补跑=自证,回溯不了关单当时全绿,与门禁本意相悖)。退回全局布尔 = v5 语义
772
+ // (不误报、不伪造),与检查 15/18 生效日锚同构。done ts 台账读一次循环外缓存,零新增子进程。
773
+ const verifyDocSinceTs = verifyDocBound ? kitPolicy.verifyDocSince : null;
774
+ const doneTsOf = (() => {
775
+ const map = new Map();
776
+ try {
777
+ const lp = path.join(ROOT, '.agents', 'confirmations.jsonl');
778
+ if (fs.existsSync(lp)) {
779
+ for (const line of fs.readFileSync(lp, 'utf8').split(/\r?\n/)) {
780
+ if (!line.trim()) continue;
781
+ try {
782
+ const e = JSON.parse(line);
783
+ // append-only 取末次 done 跳转(同一 doc 多次 done 取最后一条 ts)
784
+ if (e && e.doc && ['done', 'closed'].includes(e.stage)) map.set(e.doc, e.ts || '');
785
+ } catch { /* 坏行跳过(同检查 15 口径) */ }
786
+ }
787
+ }
788
+ } catch { /* 台账不可读 → 无锚 → 全走全局布尔(fail-open,不误报 hard) */ }
789
+ return map;
790
+ })();
791
+ // 该 doc 的确认门 done 是否落在 v6 追溯窗口内;无台账行 / 台账不可读 → false(存量豁免,退回全局布尔)
792
+ // 2026-10-09 verifydoc-anchor:按 ISO 时刻比较(不再是日期粒度)——机制上线当日的更早时刻 done 的单
793
+ // 仍在窗口外(日期粒度会把同日的早时刻误纳入,见 policy.mjs verifyDocSince 注释)。
794
+ const verifyDocInWindow = (rel) => {
795
+ if (!verifyDocSinceTs) return false;
796
+ const ts = doneTsOf.get(rel);
797
+ return !!ts && ts >= verifyDocSinceTs;
798
+ };
799
+ const verifyFreshFor = (rel) => (verifyLines
800
+ ? hasFreshVerifyLine(verifyLines, Date.now(), verifyDocInWindow(rel) ? rel : null)
801
+ : false);
761
802
  for (const intent of docFiles('intents')) {
762
803
  if (fmGet(intent, '状态') !== 'done') continue;
763
804
  const base = path.basename(intent);
805
+ const rel = path.relative(ROOT, intent).split(path.sep).join('/'); // 与 verify.mjs --doc 落账口径同源
764
806
  const lines = linesOf(intent) || [];
765
807
  // 节扫描:hs=有验收节;insec 至下一个二级标题;uc=未勾项;ne=勾选缺证据;ex=存量豁免声明
766
808
  // 证据可写在 [x] 行的续行(仓库通写法「(证据:…)」另起一行)——pendX 记「待证据项」,
@@ -811,12 +853,14 @@ export function runCheckLoop(opts = {}) {
811
853
  // verify 对账 ctx:intent 首次加入 git ≥ verifySince(v5 锚)才参与;不可判定 → null 走 text 豁免
812
854
  const iadd = addedDateOf(intent);
813
855
  const vctx = typeof verifySinceOf === 'string' && iadd !== '' && iadd >= verifySinceOf
814
- ? { verifyRelevant: true, hasFresh: verifyFresh } : null;
856
+ ? { verifyRelevant: true, hasFresh: verifyDocBound ? verifyFreshFor(rel) : verifyFreshGlobal } : null;
815
857
  const results = evidenceItems.map((ev) => ({ t: verifyEvidenceTruth(ev, planBase, ROOT, vctx) }));
816
858
  const hasImplEvidence = results.some((r) => r.t.ok && (r.t.type === 'sha' || r.t.type === 'process'));
817
859
  for (const r of results) {
818
860
  if (r.t.ok && r.t.type === 'verify-missing') {
819
- warnings.push(`- [WARN 测试绿缺凭证] ${base} 验收证据声明「测试绿」但 24h 内无 verify 凭证(verifications.jsonl)——跑 node .agents/scripts/verify.mjs 后重跑(advisory,灰度第一档)`);
861
+ // ⚠️ 文案锚点硬约束(既有 check-loop.test.mjs:2743 锚定「测试绿缺凭证」四字连续,零改动前提):
862
+ // doc 与修复命令只能追加在整句之后,不得插进这四个字中间。
863
+ warnings.push(`- [WARN 测试绿缺凭证] ${base} 验收证据声明「测试绿」但 24h 内无 verify 凭证(verifications.jsonl${verifyDocBound ? `;本单须带 --doc 落账` : ''})——跑 node .agents/scripts/verify.mjs${verifyDocBound ? ` --doc ${rel}` : ''} 后重跑(advisory,灰度第一档)`);
820
864
  continue;
821
865
  }
822
866
  if (r.t.ok) continue;
@@ -851,20 +895,8 @@ export function runCheckLoop(opts = {}) {
851
895
  }
852
896
  }
853
897
 
854
- // --- 9. 文件名英文 kebab-case [warning] ---
855
- for (const sub of DOC_DIRS) {
856
- const dir = path.join(ROOT, WF, sub);
857
- for (const f of readdirOrNull(dir) || []) {
858
- if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
859
- const abs = path.join(dir, f);
860
- if (!isTracked(abs)) continue;
861
- if (!/^[\x20-\x7e]*$/.test(f)) {
862
- warnings.push(`- [WARN 文件名非英文] 文件名含非 ASCII 字符,应为英文 kebab-case:${WF}/${sub}/${f}`);
863
- }
864
- }
865
- }
866
-
867
898
  // --- 10. 级别 vs 迁移文件一致性 [warning](启发式:入口文档加入提交触及迁移 SQL/Migrations → 疑似判低;非 git 跳过)---
899
+ markGate('10', '级别 vs 迁移文件');
868
900
  if (gitOut(['rev-parse', '--git-dir']) !== null) {
869
901
  const log = gitOut(['log', '--diff-filter=A', '--format=@%H', '--name-only']);
870
902
  if (log !== null) {
@@ -893,54 +925,8 @@ export function runCheckLoop(opts = {}) {
893
925
  }
894
926
  }
895
927
 
896
- // --- 11. workflow/INDEX.md 漂移 [warning](调生成器 --check,口径单一不复刻渲染)---
897
- {
898
- const gen = path.join(ROOT, '.agents', 'scripts', 'gen-workflow-index.mjs');
899
- if (fs.existsSync(gen)) {
900
- const r = spawnSync(process.execPath, [gen, '--check'], { cwd: ROOT, encoding: 'utf8' });
901
- if (r.status !== 0) {
902
- const first = `${r.stdout || ''}${r.stderr || ''}`.split('\n')[0];
903
- warnings.push(`- [WARN 索引漂移] workflow/INDEX.md 与磁盘不一致——跑 node .agents/scripts/gen-workflow-index.mjs 重新生成(${first})`);
904
- }
905
- }
906
- }
907
-
908
- // --- 12. frontmatter「模块:」合法性 [warning](词表单源;缺字段只对 2026-09-22 起新建提示)---
909
- {
910
- const modsFile = path.join(ROOT, '.agents', 'workflow-modules.txt');
911
- if (fs.existsSync(modsFile)) {
912
- const mods = linesOf(modsFile).filter((l) => l.trim() && !l.startsWith('#')).map((l) => l.trim());
913
- const vocab = new Set(mods);
914
- for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
915
- for (const doc of docFiles(sub)) {
916
- const base = path.basename(doc);
917
- const mod = fmGet(doc, '模块');
918
- let d = fmGet(doc, '日期') || fmGet(doc, '发现');
919
- if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
920
- if (mod) {
921
- if (!vocab.has(mod)) warnings.push(`- [WARN 模块元数据] ${base}「模块: ${mod}」不在词表(见 .agents/workflow-modules.txt)`);
922
- } else if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= kitPolicy.moduleSince) {
923
- warnings.push(`- [WARN 模块元数据] ${base} 缺「模块:」字段(${kitPolicy.moduleSince} 起新建文档必填)`);
924
- }
925
- }
926
- }
927
- }
928
- }
929
-
930
- // --- 13. 常驻面体积预算 [warning](判定单源 rule-budget.sh;无 sh 环境静默跳过——advisory,pre-commit 硬拦兜底)---
931
- {
932
- const rb = path.join(ROOT, '.agents', 'scripts', 'rule-budget.sh');
933
- const budgets = path.join(ROOT, '.agents', 'rule-budgets.txt');
934
- if (fs.existsSync(rb) && fs.existsSync(budgets)) {
935
- const r = spawnSync('sh', [rb, '--all'], { cwd: ROOT, encoding: 'utf8' });
936
- if (!r.error && r.status !== 0) {
937
- const head = `${r.stdout || ''}${r.stderr || ''}`.split('\n').slice(0, 5).map((l) => ' ' + l).join('\n');
938
- warnings.push(`- [WARN 常驻面超限] 常驻面体积超预算(先删除或下沉被取代条目再增——一进一出):\n${head}`);
939
- }
940
- }
941
- }
942
-
943
928
  // --- 14. 新 done 的 spec/plan 须有确认留痕:git 历史「状态: approved」 或 confirmations.jsonl 台账 approved 行 [warning](恒 advisory;非 git 跳过)---
929
+ markGate('14', '确认留痕(approved 快照)');
944
930
  // 2026-10-08 papercuts-cleanup-batch 修 3:两跳确认同批提交时 git 历史不出现行首「状态: approved」曾误报,
945
931
  // 而台账已证明两跳均走(2026-09-28-adopter-derivers 实证)——判据改 OR 并集(git 判据保留不弱化,
946
932
  // 台账为第二通道;确认事件以台账为准)。台账伪造面由检查 15 指纹 hard 门把关,本修仅消 advisory 误报。
@@ -982,6 +968,7 @@ export function runCheckLoop(opts = {}) {
982
968
  // --- 15. 确认指纹对账 [hard-block](2026-09-26 confirm-gate-machine;2026-09-27 confirm-gate-delegated 两形态;
983
969
  // 生效日锚 2026-09-28 改台账 ts——此前锚取自文档自报「日期/发现」,新档只要把日期写早即整段跳过判定,
984
970
  // incident 2026-09-28-confirm-gate-effective-date-anchor 实证)---
971
+ markGate('15', '确认指纹对账');
985
972
  // 确认两形态(confirm-doc.mjs):TTY 亲手键入「可以」/ --delegated 对话委托代录(用户对话内明确放行后
986
973
  // AI 代录,台账行如实记 source=chat-delegated + quote 原话,永不伪装 TTY)。intents/specs/plans 凡
987
974
  // approved/done 须有 confirm-doc 产生的 frontmatter 确认指纹 + 台账(.agents/confirmations.jsonl)
@@ -1099,6 +1086,7 @@ export function runCheckLoop(opts = {}) {
1099
1086
  }
1100
1087
 
1101
1088
  // --- 16. 量化断言指标签名对账 [warning](2026-09-28 claim-exceeds-fix;登记表单源 .agents/metric-claims.txt)---
1089
+ markGate('16', '量化断言签名对账');
1102
1090
  // 判据要点:活跃态文档(draft/approved/open)中的 `{{指标名}}`(小写点分)签名须替换为实时值,留签名=未回填=
1103
1091
  // warning;只查显式签名、不全文扫数字(历史叙述假阳性恒 0);未登记签名 / 取数器缺失 → fail-loud 出账;
1104
1092
  // 登记表缺失 → 静默跳过(未启用该检查的装户零噪声);装户自有指标走 .agents/metric-derivers.cjs(CJS 契约,
@@ -1109,6 +1097,7 @@ export function runCheckLoop(opts = {}) {
1109
1097
  runCheck16({ ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrNull, warnings });
1110
1098
 
1111
1099
  // --- 17. 发版提交树上仍未收口的 intent/spec/plan [hard-block] ---
1100
+ markGate('17', '发版树未收口文档');
1112
1101
  // 最近一次 package.json version 发生变化的提交里,当时状态已是 draft 或 approved 的
1113
1102
  // intent/spec/plan,被扫描的树上仍是 draft 或 approved 则阻断。
1114
1103
  // 当时已是 draft:不看版本锚。当时已是 approved:只在发版版本大于
@@ -1148,6 +1137,7 @@ export function runCheckLoop(opts = {}) {
1148
1137
  }
1149
1138
 
1150
1139
  // --- 18. 委派台账对账 [warning] ---
1140
+ markGate('18', '委派台账对账');
1151
1141
  // L2/L3 的 intent、spec、plan 状态为 done,或同级别 incident 状态为 fixed 或 closed 时,
1152
1142
  // 「委派结果」表与「自做任务结果」表均没有日期不早于文档日期、且含该文件名(带/不带 .md 均匹配)的一行,则警告。
1153
1143
  // 缺级别、缺日期、以及其他状态不警告。audit:false 时 warnings.push 已被换成空函数。
@@ -1177,6 +1167,7 @@ export function runCheckLoop(opts = {}) {
1177
1167
  }
1178
1168
 
1179
1169
  // --- 19. 逐阶段审计 [warning](2026-09-30 stage-gate-machine;口径与 stage-gates.mjs / fill-* / confirm-doc 前置门互引) ---
1170
+ markGate('19', '逐阶段审计');
1180
1171
  // 判据 A(在途扫描):同名 spec/plan 为 draft 且其日期 ≥ stageGateSince 时——入口未确认(intent:状态非
1181
1172
  // approved/done、或缺台账行且非存量;incident:「时间线」小节缺行首「用户确认」条目)→ warning;
1182
1173
  // 入口缺合法「级别」→ warning(与起草门 fail-closed 同口径,2026-09-30 复核 P2-7);
@@ -1352,6 +1343,7 @@ export function runCheckLoop(opts = {}) {
1352
1343
  }
1353
1344
 
1354
1345
  // --- 20. 引擎脚本测试覆盖 [warning](2026-10-01 gate-script-test-coverage;仅包源环境) ---
1346
+ markGate('20', '引擎脚本测试覆盖');
1355
1347
  // 包源开发纪律:templates/_agents/scripts/ 下每个非 *.test.mjs 引擎脚本须有同名兄弟 .test.mjs,
1356
1348
  // 或在 .agents/scripts-test-exempt.txt 登记豁免(managed 下发)。装户(无 templates/)整体跳过。
1357
1349
  {
@@ -1375,6 +1367,9 @@ export function runCheckLoop(opts = {}) {
1375
1367
  }
1376
1368
 
1377
1369
  // --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
1370
+ // 门禁 ROI 收口(2026-10-08 gate-roi-metrics):末段结算(末段之后无下次 gateSeg 调用)+ 可选落盘。
1371
+ // ⚠️ 结算在输出**之前**:否则检查 20 之后的段不计入。落盘只在 --gate-stats 时发生。
1372
+ if (opts.gateStats) writeGateStats(ROOT, finishSegs(gateStats));
1378
1373
  if (blockers.length) {
1379
1374
  process.stderr.write(`闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n\n${blockers.join('\n')}\n\n`);
1380
1375
  if (warnings.length) process.stderr.write(`WARN(advisory,不阻断):\n\n${warnings.join('\n')}\n`);
@@ -12,7 +12,7 @@ import { spawnSync } from 'node:child_process';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { computeFingerprint } from './confirm-doc.mjs';
14
14
  import { POLICIES, loadKitPolicy } from './policy.mjs';
15
- import { entryConfirmed, MARK_RE, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
15
+ import { entryConfirmed, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
16
16
 
17
17
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
18
18
  const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
@@ -423,6 +423,37 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
423
423
  rmfix(T);
424
424
  }
425
425
 
426
+ // ---- 场景 27b:DASHBOARD 漂移门(2026-10-08 selfmeasure-and-modularize 检查 11 扩覆盖面)----
427
+ // 三态:陈旧盘面 → WARN;盘面缺失(装户从未生成过看板)→ 不报(防常驻噪声);一致 → 不报。
428
+ {
429
+ const mkDashFix = () => {
430
+ const T = mkfix();
431
+ fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
432
+ for (const f of ['gen-workflow-index.mjs', 'gen-workflow-dashboard.mjs', 'agg-delegations.cjs', 'workflow-enums.mjs']) {
433
+ fs.copyFileSync(path.join(SCRIPT_DIR, f), path.join(T, '.agents', 'scripts', f));
434
+ }
435
+ fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
436
+ w(T, 'workflow/intents/2026-09-22-dash.md', INTENT('dash', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
437
+ w(T, 'workflow/plans/2026-09-22-dash.md', PLAN('dash', '状态: approved\n级别: L1\n模块: pipeline'));
438
+ spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
439
+ return T;
440
+ };
441
+ // ① DASHBOARD.md 从未生成(装户形态)→ 不得出 WARN
442
+ let T = mkDashFix();
443
+ let r = run(T);
444
+ check('观测面:装户未生成过 DASHBOARD.md → 不出仪表盘漂移 WARN(防常驻噪声)',
445
+ r.status === 0 && !outOf(r).includes('WARN 仪表盘漂移'), outOf(r));
446
+ rmfix(T);
447
+ // ② 生成后改事实 → 漂移 WARN,且 exit 0(advisory 不阻断)
448
+ T = mkDashFix();
449
+ spawnSync(process.execPath, ['.agents/scripts/gen-workflow-dashboard.mjs'], { cwd: T });
450
+ w(T, 'workflow/intents/2026-09-22-dash.md', INTENT('dash', '状态: done\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
451
+ r = run(T);
452
+ check('观测面:DASHBOARD 漂移 → WARN 仪表盘漂移 且 exit 0',
453
+ r.status === 0 && outOf(r).includes('WARN 仪表盘漂移') && outOf(r).includes('gen-workflow-dashboard.mjs'), outOf(r));
454
+ rmfix(T);
455
+ }
456
+
426
457
  // ---- 场景 28/29:常驻面预算——合规无告警;AGENTS.md 超限 → WARN 含一进一出 ----
427
458
  {
428
459
  const T = mkfix();
@@ -2721,6 +2752,54 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
2721
2752
  rmfix(T2);
2722
2753
  }
2723
2754
 
2755
+ // ---- 检查 8 凭证按 doc 绑定(2026-10-08 verify-doc-binding):v6 收紧 / v5 不变 ----
2756
+ // 漏洞本体:凭证行不带 doc 字段 + 判定只看「全仓任意一份 24h 内绿行」→ 跑一次 verify 就能给全仓
2757
+ // 任意一单的「测试绿」声明背书。本组三条:① v6 + 只有他单绿行 → 仍出 WARN(漏洞已堵);
2758
+ // ② v6 + 本单绿行 → 豁免零 WARN;③ **v5 + 只有他单绿行 → 无 WARN**(兼容反例,收紧只对 v6+ 生效)。
2759
+ {
2760
+ const mkVeDoc = (pv, docField) => {
2761
+ const T = mkfix();
2762
+ gitInit(T);
2763
+ w(T, '.agents/kit.json', JSON.stringify({ policyVersion: pv }));
2764
+ const green = { ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' };
2765
+ if (docField) green.doc = docField;
2766
+ w(T, '.agents/verifications.jsonl', `${JSON.stringify(green)}\n`);
2767
+ const iBody = '# INTENT — ve\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n';
2768
+ const pBody = '# PLAN — ve\n';
2769
+ const fpI = computeFingerprint(`---\n状态: approved\n级别: L1\n日期: 2026-10-07\n---\n${iBody}`);
2770
+ const fpP = computeFingerprint(`---\n状态: approved\n级别: L1\n---\n${pBody}`);
2771
+ w(T, 'workflow/intents/2026-10-07-ve.md', `---\n状态: done\n级别: L1\n日期: 2026-10-07\n确认指纹: ${fpI.slice(0, 16)}\n---\n${iBody}`);
2772
+ w(T, 'workflow/plans/2026-10-07-ve.md', `---\n状态: done\n级别: L1\n确认指纹: ${fpP.slice(0, 16)}\n---\n${pBody}`);
2773
+ const now = new Date().toISOString();
2774
+ writeLedger(T, [
2775
+ { ts: now, doc: 'workflow/intents/2026-10-07-ve.md', stage: 'done', fingerprint: fpI, prev: 'approved' },
2776
+ { ts: now, doc: 'workflow/plans/2026-10-07-ve.md', stage: 'done', fingerprint: fpP, prev: 'approved' },
2777
+ ]);
2778
+ gitCommitAll(T, 'feat: ve doc-binding fixture');
2779
+ return T;
2780
+ };
2781
+ const MINE = 'workflow/intents/2026-10-07-ve.md';
2782
+ const OTHER = 'workflow/intents/2026-10-07-someone-else.md';
2783
+
2784
+ const D1 = mkVeDoc(6, OTHER);
2785
+ let r = run(D1);
2786
+ check('检查8 凭证绑定:v6 + 只有**他单**绿行 → 仍出 WARN(跨单背书漏洞已堵)',
2787
+ r.status === 0 && outOf(r).includes('测试绿缺凭证') && !outOf(r).includes('证据豁免 verify'), `exit=${r.status}\n${outOf(r)}`);
2788
+ rmfix(D1);
2789
+
2790
+ const D2 = mkVeDoc(6, MINE);
2791
+ r = run(D2);
2792
+ check('检查8 凭证绑定:v6 + 本单绿行 → verify 豁免零 WARN(绑定成立)',
2793
+ r.status === 0 && outOf(r).includes('证据豁免 verify') && !outOf(r).includes('测试绿缺凭证'), `exit=${r.status}\n${outOf(r)}`);
2794
+ rmfix(D2);
2795
+
2796
+ const D3 = mkVeDoc(5, OTHER);
2797
+ r = run(D3);
2798
+ check('检查8 凭证绑定:v5 + 只有他单绿行 → **无 WARN**(兼容反例:收紧只对 v6+ 生效)',
2799
+ r.status === 0 && !outOf(r).includes('测试绿缺凭证') && outOf(r).includes('证据豁免 verify'), `exit=${r.status}\n${outOf(r)}`);
2800
+ rmfix(D3);
2801
+ }
2802
+
2724
2803
  // ---- 检查 18 表头签名识别(2026-10-08 papercuts-cleanup-batch 修 2):节标题漂移不再静默停解析 ----
2725
2804
  // 修前按 `## 委派结果|自做任务结果` 精确节标题切段,装户把标题写成「委派结果表」即静默停解析该表
2726
2805
  // → L2/L3 done 回流无告警;修后按表头签名(与 agg-delegations.cjs splitTables 同口径)识别。
@@ -2788,5 +2867,40 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
2788
2867
  rmfix(T);
2789
2868
  }
2790
2869
 
2870
+ // ---- 门禁 ROI 插桩(2026-10-08 gate-roi-metrics)----
2871
+ // 断言两件事:① 默认路径不落盘、stderr 无 gate-stats 行(输出契约零污染);
2872
+ // ② --gate-stats 落一行 jsonl 且段覆盖全部检查(判据不漏插)。
2873
+ // 既有 223 条断言零改动本身即最强钉子——任何 push 到 warnings 的插桩副作用都会把它们打红。
2874
+ {
2875
+ const T = mkfix();
2876
+ const statsRel = path.join('.agents', 'cache', 'gate-stats.jsonl');
2877
+ const plain = run(T);
2878
+ check('门禁ROI:默认路径不落 gate-stats.jsonl(统计默认静默)',
2879
+ !fs.existsSync(path.join(T, statsRel)), outOf(plain));
2880
+ check('门禁ROI:默认路径 stderr 无 [gate-stats] 摘要行(稳定输出契约零污染)',
2881
+ !outOf(plain).includes('[gate-stats]'), outOf(plain));
2882
+ const before = fs.existsSync(path.join(T, statsRel)) ? fs.readFileSync(path.join(T, statsRel), 'utf8') : '';
2883
+
2884
+ const st = run(T, { args: ['--gate-stats'] });
2885
+ check('门禁ROI:--gate-stats → 落盘 + stderr 摘要行', fs.existsSync(path.join(T, statsRel)) && outOf(st).includes('[gate-stats]'), outOf(st));
2886
+ const lines = fs.readFileSync(path.join(T, statsRel), 'utf8').split('\n').filter(Boolean);
2887
+ check('门禁ROI:--gate-stats 只 append 一行(每跑一次门禁 = 一条记录)',
2888
+ lines.length === 1 && !before, `lines=${lines.length} before=${before.length}`);
2889
+ let row = null;
2890
+ try { row = JSON.parse(lines[0]); } catch { /* 留给下一条断言报错 */ }
2891
+ const ids = row && Array.isArray(row.segs) ? row.segs.map((s) => String(s.id)) : [];
2892
+ const uniq = new Set(ids);
2893
+ check('门禁ROI:段 id 唯一且覆盖 19 个插桩点(14 主流程 + 5 hygiene 模块),无重复插桩',
2894
+ ids.length === 19 && uniq.size === 19, `ids=${ids.join(',')}`);
2895
+ const expected = ['1','2','3','4','5','6','8','9','10','11','12','13','14','15','16','17','18','19','20'];
2896
+ check('门禁ROI:段 id 集合 == 现有检查编号(不新增/不遗漏检查项编号)',
2897
+ expected.every((e) => uniq.has(e)) && uniq.size === expected.length, `got=${[...uniq].sort().join(',')}`);
2898
+ check('门禁ROI:每段都有 label + 非负 ms(成本度量的地基)',
2899
+ row && row.segs.every((s) => s.label && Number(s.ms) >= 0 && Number.isInteger(Number(s.ms))), JSON.stringify(row && row.segs.slice(0, 3)));
2900
+ check('门禁ROI:行顶层含 ts / root / totalMs(聚合器的窗口过滤依赖 ts)',
2901
+ row && typeof row.ts === 'string' && typeof row.root === 'string' && Number(row.totalMs) >= 0, JSON.stringify({ ts: row && row.ts, totalMs: row && row.totalMs }));
2902
+ rmfix(T);
2903
+ }
2904
+
2791
2905
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
2792
2906
  process.exit(fail ? 1 : 0);
@@ -9,7 +9,6 @@ import { fileURLToPath } from 'node:url';
9
9
 
10
10
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
11
11
  const HOOK = path.join(SCRIPT_DIR, '..', 'hooks', 'commit-check.cjs');
12
- const MARK = 'TRIGGER_TEST_BUILD_RAN';
13
12
 
14
13
  let pass = 0, fail = 0;
15
14
  const check = (desc, cond, detail = '') => {
@@ -38,22 +38,33 @@ import fs from 'node:fs';
38
38
  import path from 'node:path';
39
39
  import readline from 'node:readline';
40
40
  import { createHash, randomBytes } from 'node:crypto';
41
- import { fileURLToPath } from 'node:url';
42
41
  import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
43
42
  import { loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
44
43
 
45
- // hasFreshVerify(root):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
44
+ // hasFreshVerify(root, doc):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
46
45
  // policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
47
- function hasFreshVerify(root) {
46
+ // doc 绑定(2026-10-08 verify-doc-binding):非空时只认 e.doc === doc 的绿行——不带 doc 的绿行
47
+ // (CI 场景)与**别的单**的绿行都不给本单背证。调用方负责「仅 v6+ 才传 doc」,本函数不做版本判断。
48
+ function hasFreshVerify(root, doc = null) {
48
49
  try {
49
50
  const p = path.join(root, '.agents', 'verifications.jsonl');
50
51
  if (!fs.existsSync(p)) return false;
51
- return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/));
52
+ return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/), Date.now(), doc);
52
53
  } catch {
53
54
  return false;
54
55
  }
55
56
  }
56
57
 
58
+ // docRelOf:把用户传入的 doc 路径归一成「相对仓库根的 posix 相对路径」——与 verify.mjs --doc 的落账口径
59
+ // 必须逐字一致(一个转得动、另一个转不动 = 凭证永远匹配不上)。win32 反斜杠 / posix / 绝对路径三者同值。
60
+ function docRelOf(root, doc) {
61
+ try {
62
+ return path.relative(root, path.resolve(root, doc)).split(path.sep).join('/');
63
+ } catch {
64
+ return doc;
65
+ }
66
+ }
67
+
57
68
  // computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
58
69
  export function computeFingerprint(text) {
59
70
  const norm = String(text).replace(/\r\n/g, '\n').split('\n')
@@ -319,10 +330,19 @@ if (isMain) {
319
330
  // 测试绿凭证前置(2026-10-07 verify-evidence,advisory 不拦):→done 且文档含「验收标准」节且
320
331
  // policy v5(verifySince)→ 24h 窗口内须有 verifications.jsonl 绿行。只读判定,任何读取异常降级
321
332
  // 静默(fail-open——advisory 层不因台账缺失/坏行阻断关单);升 hard 走后续 policyVersion 演进。
333
+ // 2026-10-08 verify-doc-binding:v6+(verifyDocSince)时按本 doc 精确匹配绿行——他单的绿行不再
334
+ // 给本单背书;v1-v5 维持 v5 全局行为逐字不变。
335
+ // ⚠️ 文案锚点硬约束(既有 confirm-doc.test.mjs 断言锚定子串,零改动前提):
336
+ // 「无测试绿凭证」六字须**连续**,doc 与命令只能追加在其后——不得写成「无本单(x)的测试绿凭证」。
322
337
  if (target === 'done' && typeof loadKitPolicy(root).verifySince === 'string'
323
- && /^##\s*[^#]*验收标准/m.test(text) && !hasFreshVerify(root)) {
324
- console.error(`⚠️ ${doc} 置 done:24h 内无测试绿凭证(verifications.jsonl)——关单勾验的「测试绿」声明缺机器事实`);
325
- console.error(' 跑 node .agents/scripts/verify.mjs(全绿自动落账)后重新关单;本次不拦截(advisory,灰度第一档)');
338
+ && /^##\s*[^#]*验收标准/m.test(text)) {
339
+ const pol = loadKitPolicy(root);
340
+ const rel = docRelOf(root, doc);
341
+ const bound = typeof pol.verifyDocSince === 'string' ? rel : null;
342
+ if (!hasFreshVerify(root, bound)) {
343
+ console.error(`⚠️ ${doc} 置 done:24h 内无测试绿凭证(${bound ? `本单 ${rel} 须带 --doc 落账` : '凭证未按单绑定'})——关单勾验的「测试绿」声明缺机器事实`);
344
+ console.error(` 跑 node .agents/scripts/verify.mjs${bound ? ` --doc ${rel}` : ''} 后重新关单;本次不拦截(advisory,灰度第一档)`);
345
+ }
326
346
  }
327
347
  const fp = computeFingerprint(text);
328
348
  // AI 自治放行分支(2026-09-30 ai-autonomy-trust):