agentic-flow-kit 1.7.0 → 1.7.16

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 (162) hide show
  1. package/README.md +81 -9
  2. package/modules/gates/README.md +1 -0
  3. package/modules/gates/test-protect/README.md +23 -0
  4. package/modules/gates/test-protect/check-test-protect.mjs +64 -0
  5. package/modules/gates/test-protect/local-pre-commit.line +1 -0
  6. package/modules/hosts/README.md +57 -0
  7. package/modules/hosts/claude/commands/wf-build.md +5 -24
  8. package/modules/hosts/claude/commands/wf-deploy.md +1 -0
  9. package/modules/hosts/claude/commands/wf-design.md +7 -6
  10. package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
  11. package/modules/hosts/claude/commands/wf-new-task.md +42 -21
  12. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  13. package/modules/hosts/claude/commands/wf-review.md +1 -1
  14. package/modules/hosts/claude/commands/wf-sync-hosts.md +8 -16
  15. package/modules/hosts/claude/commands/wf-test.md +5 -4
  16. package/modules/hosts/codex/commands/wf-build.md +5 -24
  17. package/modules/hosts/codex/commands/wf-deploy.md +1 -0
  18. package/modules/hosts/codex/commands/wf-design.md +7 -6
  19. package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
  20. package/modules/hosts/codex/commands/wf-new-task.md +42 -21
  21. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  22. package/modules/hosts/codex/commands/wf-review.md +1 -1
  23. package/modules/hosts/codex/commands/wf-sync-hosts.md +8 -16
  24. package/modules/hosts/codex/commands/wf-test.md +5 -4
  25. package/modules/hosts/cursor/commands/wf-build.md +5 -24
  26. package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
  27. package/modules/hosts/cursor/commands/wf-design.md +7 -6
  28. package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
  29. package/modules/hosts/cursor/commands/wf-new-task.md +42 -21
  30. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  31. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  32. package/modules/hosts/cursor/commands/wf-sync-hosts.md +8 -16
  33. package/modules/hosts/cursor/commands/wf-test.md +5 -4
  34. package/modules/hosts/opencode/commands/wf-build.md +5 -24
  35. package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
  36. package/modules/hosts/opencode/commands/wf-design.md +7 -6
  37. package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
  38. package/modules/hosts/opencode/commands/wf-new-task.md +42 -21
  39. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  40. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  41. package/modules/hosts/opencode/commands/wf-sync-hosts.md +8 -16
  42. package/modules/hosts/opencode/commands/wf-test.md +5 -4
  43. package/modules/hosts/trae/commands/wf-build.md +5 -24
  44. package/modules/hosts/trae/commands/wf-deploy.md +1 -0
  45. package/modules/hosts/trae/commands/wf-design.md +7 -6
  46. package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
  47. package/modules/hosts/trae/commands/wf-new-task.md +42 -21
  48. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  49. package/modules/hosts/trae/commands/wf-review.md +1 -1
  50. package/modules/hosts/trae/commands/wf-sync-hosts.md +8 -16
  51. package/modules/hosts/trae/commands/wf-test.md +5 -4
  52. package/package.json +2 -2
  53. package/src/add-gate.mjs +25 -1
  54. package/src/add-gate.test.mjs +103 -0
  55. package/src/cli.mjs +59 -5
  56. package/src/doctor.mjs +136 -12
  57. package/src/executor.mjs +245 -0
  58. package/src/executor.test.mjs +292 -0
  59. package/src/gates.test.mjs +17 -0
  60. package/src/init.mjs +14 -1
  61. package/src/init.test.mjs +19 -0
  62. package/src/instance-ref.test.mjs +62 -0
  63. package/src/jscpd-drift.test.mjs +144 -0
  64. package/src/profiles.mjs +3 -0
  65. package/src/render.mjs +6 -1
  66. package/src/rule-budget.test.mjs +103 -0
  67. package/src/sync-hosts.mjs +41 -45
  68. package/src/sync-hosts.test.mjs +67 -56
  69. package/src/trae-hooks.test.mjs +15 -3
  70. package/src/validate.mjs +72 -0
  71. package/src/validate.test.mjs +95 -0
  72. package/templates/.gitattributes +8 -0
  73. package/templates/AGENTS.md +2 -1
  74. package/templates/_agents/commands/build.md +5 -24
  75. package/templates/_agents/commands/deploy.md +1 -0
  76. package/templates/_agents/commands/design.md +7 -6
  77. package/templates/_agents/commands/gate-checklist.md +1 -3
  78. package/templates/_agents/commands/new-task.md +42 -21
  79. package/templates/_agents/commands/plan.md +1 -1
  80. package/templates/_agents/commands/review.md +1 -1
  81. package/templates/_agents/commands/sync-hosts.md +8 -16
  82. package/templates/_agents/commands/test.md +5 -4
  83. package/templates/_agents/control-surfaces.txt +53 -0
  84. package/templates/_agents/engine-lock.json +246 -0
  85. package/templates/_agents/lane-surfaces.txt +1 -1
  86. package/templates/_agents/level-hints.txt +30 -0
  87. package/templates/_agents/scripts/agg-delegations.cjs +51 -6
  88. package/templates/_agents/scripts/agg-delegations.test.mjs +84 -4
  89. package/templates/_agents/scripts/check-engine-integrity.mjs +217 -0
  90. package/templates/_agents/scripts/check-engine-integrity.test.mjs +215 -0
  91. package/templates/_agents/scripts/check-lane-surface.mjs +78 -24
  92. package/templates/_agents/scripts/check-lane-surface.test.mjs +66 -0
  93. package/templates/_agents/scripts/check-ledger-invariant.mjs +112 -14
  94. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +5 -1
  95. package/templates/_agents/scripts/check-loop-rev.test.mjs +6 -4
  96. package/templates/_agents/scripts/check-loop.mjs +255 -45
  97. package/templates/_agents/scripts/check-loop.test.mjs +330 -15
  98. package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
  99. package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
  100. package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
  101. package/templates/_agents/scripts/confirm-doc.mjs +54 -12
  102. package/templates/_agents/scripts/confirm-doc.test.mjs +59 -0
  103. package/templates/_agents/scripts/doctor.test.mjs +294 -1
  104. package/templates/_agents/scripts/draft-sediment.mjs +178 -0
  105. package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
  106. package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
  107. package/templates/_agents/scripts/fill-intent.mjs +2 -2
  108. package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
  109. package/templates/_agents/scripts/fixture-scripts.test.mjs +107 -0
  110. package/templates/_agents/scripts/gate-checklist.mjs +6 -3
  111. package/templates/_agents/scripts/gate-checklist.test.mjs +2 -5
  112. package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
  113. package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
  114. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +10 -13
  115. package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -30
  116. package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
  117. package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
  118. package/templates/_agents/scripts/kb-search.mjs +87 -12
  119. package/templates/_agents/scripts/kb-search.test.mjs +83 -0
  120. package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
  121. package/templates/_agents/scripts/ledger-loader.mjs +108 -0
  122. package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
  123. package/templates/_agents/scripts/ledger-prune.mjs +96 -0
  124. package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
  125. package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
  126. package/templates/_agents/scripts/level-criteria.mjs +232 -0
  127. package/templates/_agents/scripts/level-criteria.test.mjs +279 -0
  128. package/templates/_agents/scripts/pipeline-run.mjs +5 -3
  129. package/templates/_agents/scripts/policy.mjs +47 -0
  130. package/templates/_agents/scripts/solidify-task.mjs +1 -1
  131. package/templates/_agents/scripts/stage-gates.mjs +52 -10
  132. package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
  133. package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
  134. package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
  135. package/templates/_agents/scripts/verify.mjs +1 -1
  136. package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
  137. package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
  138. package/templates/_agents/scripts/workflows-check.mjs +1 -1
  139. package/templates/_agents/scripts-test-exempt.txt +3 -1
  140. package/templates/_agents/skills/wiki/SKILL.md +5 -3
  141. package/templates/_agents/workflows/pipeline-closing.md +4 -5
  142. package/templates/_githooks/pre-commit +3 -34
  143. package/templates/_githooks/pre-push +3 -3
  144. package/templates/wiki/INDEX.md +44 -1
  145. package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
  146. package/templates/workflow/README.md +11 -0
  147. package/templates/workflow/delegations.md +5 -4
  148. package/templates/workflow/incidents/_TEMPLATE.md +2 -0
  149. package/templates/workflow/intents/_TEMPLATE.md +3 -0
  150. package/templates/workflow/plans/_TEMPLATE.md +4 -0
  151. package/templates/workflow/specs/_TEMPLATE.md +23 -0
  152. package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -91
  153. package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -91
  154. package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -91
  155. package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -91
  156. package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -92
  157. package/templates/_agents/commands/source-sync-check.md +0 -105
  158. package/templates/_agents/scripts/check-ledger.mjs +0 -109
  159. package/templates/_agents/scripts/check-ledger.test.mjs +0 -197
  160. package/templates/_agents/scripts/source-sync-check.mjs +0 -156
  161. package/templates/_agents/scripts/source-sync-check.test.mjs +0 -281
  162. package/templates/_agents/workflows/source-sync-repair.md +0 -30
@@ -24,6 +24,12 @@
24
24
  // 6. 子智能体角色契约 + OpenCode/Trae/ZCode Adapter 一致性(含旧委派残留/钉死模型) [warning]
25
25
  // 7. 阶段索引同步(AGENTS.md 与 new-task.md 须双向索引全部阶段指令) [warning]
26
26
  // 8. intent 验收标准对账(新建:done 未勾验/缺节=hard,勾选缺证据=warning;存量聚合 warning,含「存量对账豁免」声明者出账)
27
+ // + 场景覆盖子判据(2026-10-10 scenario-coverage,灰度 warning):同名 spec 有「验收场景」节时,
28
+ // done intent 验收标准须逐场景 ID 引用(场景:S1…),缺引用=warning;spec 无该节则不适用(零变化)
29
+ // + THEN 断言子判据(2026-10-11 then-assertion,灰度 warning):声明场景缺「- THEN」行=空壳场景,
30
+ // 聚合一条「场景无断言」warning(THEN 是机器对账锚;节不存在=零变化)
31
+ // + 引用断链子判据(2026-10-11 dangling-ref,灰度 warning):验收标准引用 spec 未声明的场景 ID
32
+ // (含 spec 无场景节)=「场景引用断链」warning——场景未覆盖的反向对账,共用同一次解析
27
33
  // **生效日锚=git 首次加入日期(2026-09-28 改;此前取文件名前 10 字符——命名规范强制的字段、
28
34
  // 写早零成本,一条 hard 门可被平凡绕过;incident 2026-09-28-check8-git-anchor 实证)**:
29
35
  // 非 git / 查不到加入记录 → 不可判定 → 走存量口径(不误报 hard)
@@ -31,6 +37,8 @@
31
37
  // 9. 文件名英文 kebab-case(非 ASCII 文件名=warning,2026-09-11 规则)
32
38
  // 10. 级别 vs 迁移文件一致性(L1/L2 入口文档加入提交触及迁移 SQL/Migrations=疑似判低,warning)
33
39
  // 11. 生成物漂移:workflow/INDEX.md(2026-09-21 检索层) + workflow/DASHBOARD.md(2026-10-08 扩覆盖面)=warning;
40
+ // wiki 侧(wiki/INDEX.md + 看板 DATA)由 doctor §6b 经 gen-wiki-board --check 校验,不在此重复
41
+ // (2026-10-10:wiki 生成区此前零机器校验,现与 workflow 侧对称为 doctor 单路);
34
42
  // 12. frontmatter「模块:」合法性(枚举非法 / 2026-09-22 起新建缺字段=warning;词表单源 .agents/workflow-modules.txt)
35
43
  // 13. 常驻面体积预算(超限=warning;判定单源 rule-budget.sh——经 sh 调用,
36
44
  // 无 sh 环境静默跳过:advisory 级且 pre-commit 侧在 git 钩子 sh 环境照常硬拦)
@@ -118,8 +126,10 @@ import { spawnSync } from 'node:child_process';
118
126
  import { createHash } from 'node:crypto';
119
127
  import { loadEnums } from './workflow-enums.mjs';
120
128
  import { runCheck16 } from './check-metric-claims.mjs';
121
- import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, scopeMatches } from './policy.mjs';
129
+ import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, ledgerDocChainActive, scopeMatches } from './policy.mjs';
122
130
  import { laneOfEntry } from './stage-gates.mjs';
131
+ import { readLedgerRows, readLedgerEntries, LEDGER_REL } from './ledger-loader.mjs';
132
+ import { prunedHashes as prunedHashesOf } from './ledger-prune.mjs';
123
133
  // MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
124
134
  // 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
125
135
  import { MARK_RE, approvedTraceHit } from './stage-gates.mjs';
@@ -156,7 +166,7 @@ function takeOpts() {
156
166
  return opts;
157
167
  }
158
168
 
159
- // ---- runCheckLoop:门禁主流程可 import 化(2026-10-08 checkloop-importable;papercuts 2026-10-04 isMain 行点名单独立项)----
169
+ // ---- runCheckLoop:门禁主流程可 import 化(2026-10-08 checkloop-importable;2026-10-04 isMain 行点名单独立项)----
160
170
  // import 本模块零副作用(isMain 主守卫,board-kb-p1 先例);opts = { root?, rev?, hardening? }——root 显式参数
161
171
  // 优先于 CHECK_LOOP_ROOT env 优先于 git toplevel(CLI 缺省行为逐字节不变);返回 { exitCode, blockers, warnings },
162
172
  // 输出打印照旧在本函数内(稳定输出契约:banner 后空行、条目 `- ` 前缀、HARD-BLOCK/WARN 两段式——消费者 .githooks/pre-push)。
@@ -168,7 +178,7 @@ function takeOpts() {
168
178
 
169
179
  // writeGateStats:门禁 ROI 落盘(2026-10-08 gate-roi-metrics)——append 一行到
170
180
  // <root>/.agents/cache/gate-stats.jsonl(该目录已 gitignore,属运行时数据、不入 git、不进 kit.json
171
- // managed 台账——刻意避开 papercuts 2026-10-05 探针临时件被 sync 收编的事故面)。
181
+ // managed 台账——刻意避开 2026-10-05 探针临时件被 sync 收编的事故面)。
172
182
  // IO 失败只出账不阻断(沿 verify.mjs 凭证落账同款容错——度量失败不该让门禁变成红灯)。
173
183
  export function writeGateStats(root, segs) {
174
184
  try {
@@ -325,6 +335,18 @@ export function runCheckLoop(opts = {}) {
325
335
  return '';
326
336
  }
327
337
 
338
+ // 落点空壳告警(2026-10-11 landing-reality,沉淀追踪第三级,incident/intent 两侧共用):
339
+ // 「沉淀: <路径>」存在 ≠ 有效——文件存在但 sedimentLandingIsShell 为真(无来源 frontmatter
340
+ // 且非占位正文不足 3 行)→ WARN 落点空壳(advisory,灰度第一档)。调用方须先出过「沉淀断档」
341
+ // 分支(文件不存在),故此处 readFileSync 异常 = 真读取故障,按空壳出账并在文案注明读取失败。
342
+ const warnSedimentShell = (single, sed) => {
343
+ let text = null;
344
+ try { text = fs.readFileSync(path.resolve(ROOT, sed), 'utf8'); } catch { text = null; }
345
+ if (!sedimentLandingIsShell(text)) return;
346
+ const readFail = text === null ? ';落点文件读取失败' : '';
347
+ warnings.push(`- [WARN 落点空壳] ${single} frontmatter「沉淀: ${sed}」指向的文件存在但无有效内容(无来源 frontmatter 且非占位正文不足 3 行${readFail})——沉淀须真实整理而非空壳占位(advisory,灰度第一档)`);
348
+ };
349
+
328
350
  const WF = 'workflow';
329
351
  const DOC_DIRS = ['intents', 'specs', 'plans', 'incidents'];
330
352
  const blockers = [];
@@ -459,9 +481,25 @@ export function runCheckLoop(opts = {}) {
459
481
  }
460
482
 
461
483
  // 三件套:正文行锚定 /^1\. /^2\. /^3\. (sh awk 同款)
484
+ let trioOk = true;
462
485
  for (const n of [1, 2, 3]) {
463
486
  if (!lines.some((l) => l.startsWith(`${n}. `))) {
464
487
  warnings.push(`- [WARN 三件套不全] ${name} 缺复盘三件套之 ${n}`);
488
+ trioOk = false;
489
+ }
490
+ }
491
+ // 沉淀追踪(incident 侧,与检查 8 intent 侧对称):closed 且三件套齐全 = 最高价值沉淀源。
492
+ // frontmatter「沉淀: <相对仓根路径>」登记去向;缺登记 → WARN 未沉淀(advisory,灰度第一档);
493
+ // 登记了但文件不存在 → WARN 沉淀断档;存在但空壳(无来源且非占位正文不足 3 行)→ WARN 落点空壳。
494
+ // 同「沉淀: 无」显式豁免口径。
495
+ if (incSt === 'closed' && trioOk) {
496
+ const sed = fmGet(inc, '沉淀');
497
+ if (!sed) {
498
+ warnings.push(`- [WARN 未沉淀] ${name} 复盘三件套齐全(closed)但未登记沉淀去向——整理到 wiki 后在 frontmatter 记「沉淀: wiki/<主题>/<文件>」,或显式写「沉淀: 无」(advisory,灰度第一档;沉淀是人工判断,机器只提醒不强制)`);
499
+ } else if (sed !== '无' && !fs.existsSync(path.resolve(ROOT, sed))) {
500
+ warnings.push(`- [WARN 沉淀断档] ${name} frontmatter「沉淀: ${sed}」指向的文件不存在——沉淀文件移动/改名后须同步更新本字段(advisory,灰度第一档)`);
501
+ } else if (sed !== '无') {
502
+ warnSedimentShell(name, sed); // 落点真实性(P1-3):存在但空壳 → WARN 落点空壳
465
503
  }
466
504
  }
467
505
  const hasParent = lines.some((l) => l.includes('是否需要新 intent'));
@@ -742,7 +780,7 @@ export function runCheckLoop(opts = {}) {
742
780
  // --- 8. intent 验收标准对账(done 须逐条勾验并补证据;新建 hard,存量聚合 warning)---
743
781
  markGate('8', '验收标准对账');
744
782
  // 生效日锚 2026-09-28 改「git 首次加入日期」(此前取文件名前 10 字符——命名规范强制的字段、
745
- // 写早零成本,见 incidents/2026-09-28-check8-git-anchor)。非 git → 不可判定 → 走存量口径(不误报 hard)。
783
+ // 写早零成本)。非 git → 不可判定 → 走存量口径(不误报 hard)。
746
784
  {
747
785
  const accCutoff = '2026-09-12';
748
786
  let legacyUnaccounted = 0;
@@ -769,16 +807,10 @@ export function runCheckLoop(opts = {}) {
769
807
  const doneTsOf = (() => {
770
808
  const map = new Map();
771
809
  try {
772
- const lp = path.join(ROOT, '.agents', 'confirmations.jsonl');
773
- if (fs.existsSync(lp)) {
774
- for (const line of fs.readFileSync(lp, 'utf8').split(/\r?\n/)) {
775
- if (!line.trim()) continue;
776
- try {
777
- const e = JSON.parse(line);
778
- // append-only 取末次 done 跳转(同一 doc 多次 done 取最后一条 ts)
779
- if (e && e.doc && ['done', 'closed'].includes(e.stage)) map.set(e.doc, e.ts || '');
780
- } catch { /* 坏行跳过(同检查 15 口径) */ }
781
- }
810
+ // 读法收敛 ledger-loader(2026-10-12 ledger-loader-p1):末次 done 跳转取自物理读序,
811
+ // loader 保持读序返回故「取末次」语义与逐处直读一致(分片按文件名序排在主台账之后=较新)
812
+ for (const e of readLedgerRows(ROOT)) {
813
+ if (e && e.doc && ['done', 'closed'].includes(e.stage)) map.set(e.doc, e.ts || '');
782
814
  }
783
815
  } catch { /* 台账不可读 → 无锚 → 全走全局布尔(fail-open,不误报 hard) */ }
784
816
  return map;
@@ -877,6 +909,65 @@ export function runCheckLoop(opts = {}) {
877
909
  else if (!ex) legacyUnaccounted++;
878
910
  continue;
879
911
  }
912
+ // 场景覆盖子判据(2026-10-10 scenario-coverage,灰度第一档 = warning):
913
+ // 同名 spec 写了「## 验收场景」节(GIVEN/WHEN/THEN,每条带 ID:S1、S2…)时,
914
+ // done intent 的验收标准须**逐场景 ID 引用**(`场景:S1`)。缺引用只出 warning——
915
+ // 有意不进 hard:新判据先灰度观察存量装户告警噪音,沿检查 8 自身 / 检查 15 / 检查 18
916
+ // 「先 WARN、装户吃过警告后升 FAIL」的既有路径;spec 无该节 = 原判据零变化。
917
+ // ID 提取单源 = 本块(spec 侧),引用匹配只认验收标准节内的 `场景:S<n>` 行。
918
+ // 引用断链子判据(2026-10-11 dangling-ref,P2-1,灰度第一档 = warning):
919
+ // 上判据的**反向对账**——验收标准引用了 spec 未声明的场景 ID = 悬空引用(改 ID 漏改 / 复制粘贴
920
+ // 残留的机器信号);spec 无场景节但引用了 S<n> 同样算断链(引用了不存在的场景)。与「场景未覆盖」
921
+ // 共用同一次 ID 解析与引用遍历(covered 一份两用),不重复解析。
922
+ // THEN 断言子判据(2026-10-11 then-assertion,P2-2,灰度第一档 = warning):
923
+ // 声明了场景却无 THEN 断言行 = 空壳场景——THEN 是可观测结果的机器对账锚(GIVEN/WHEN 可省),
924
+ // 聚合一条不逐场景刷屏;验收场景节本身可选,节不存在 → 零变化。
925
+ const specPath = path.join(ROOT, WF, 'specs', base);
926
+ if (fs.existsSync(specPath)) {
927
+ const sids = scenarioIdsOf(specPath);
928
+ // 引用遍历单次(覆盖/断链两方向共用):不锁进 sids.length 分支——spec 无场景节时引用同样悬空
929
+ const covered = new Set();
930
+ for (const line of lines) {
931
+ const m = line.match(/场景[::]\s*S(\d+)/);
932
+ if (m) covered.add(`S${m[1]}`);
933
+ }
934
+ if (sids.length) {
935
+ const missing = sids.filter((id) => !covered.has(id));
936
+ if (missing.length) {
937
+ warnings.push(`- [WARN 场景未覆盖] ${base} 同名 spec 有验收场景但验收标准未逐场景引用:缺 ${missing.join('、')}——在「## 验收标准」按「- [x] 场景:S<n> <判据>(证据:…)」补齐(advisory,灰度第一档;不写场景节的 spec 不适用)`);
938
+ }
939
+ // THEN 对账需 spec 全文(scenarioIdsOf 只回 ID);读取失败 = 不可判定 → 跳过不误报(fail-open)
940
+ let thenMap = null;
941
+ try { thenMap = extractThenByScenario(fs.readFileSync(specPath, 'utf8')); } catch { /* 极窄竞态,不判空壳 */ }
942
+ if (thenMap) {
943
+ const noThen = sids.filter((id) => !(thenMap.get(id) || []).length);
944
+ if (noThen.length) {
945
+ warnings.push(`- [WARN 场景无断言] ${base} 同名 spec 场景缺 THEN 断言:${noThen.join('、')}——每场景至少一行「- THEN <可观测结果>」(GIVEN/WHEN 可省;advisory,灰度第一档)`);
946
+ }
947
+ }
948
+ }
949
+ const dangling = [...covered].filter((id) => !sids.includes(id));
950
+ if (dangling.length) {
951
+ warnings.push(`- [WARN 场景引用断链] ${base} 验收标准引用了 spec 未声明的场景 ID:${dangling.join('、')}——修正 ID 或在 spec「## 验收场景」补该场景(advisory,灰度第一档)`);
952
+ }
953
+ }
954
+ // 沉淀追踪子判据(2026-10-10 sediment-tracking,灰度第一档 = warning):
955
+ // intent 高价值节(影响面 / 触达红线)是 wiki 沉淀的主源——frontmatter「沉淀: <相对仓根路径>」
956
+ // 登记去向(如 wiki/项目规范/xxx.md),「沉淀: 无」= 显式声明不沉淀。缺登记且内容高价值
957
+ // → WARN 未沉淀(advisory:沉淀是人工判断,机器只提醒不强制,不进 hard);登记了但文件
958
+ // 不存在 → WARN 沉淀断档(路径漂移);存在但空壳(无来源且非占位正文不足 3 行)→ WARN 落点
959
+ // 空壳(2026-10-11 landing-reality,P1-3 落点真实性)。高价值判据 = 两节之一有非占位正文内容。
960
+ // incident 侧对称(检查 3 内):closed 且复盘三件套齐全 = 最高价值沉淀源,同款提醒。
961
+ // 有意不加 policyVersion 锚:字段缺失 = 未登记,无字段语义零变化(沿场景覆盖子判据同款口径)。
962
+ const sed = fmGet(intent, '沉淀');
963
+ const highValue = sectionHasBody(lines, '影响面') || sectionHasBody(lines, '触达红线');
964
+ if (highValue && !sed) {
965
+ warnings.push(`- [WARN 未沉淀] ${base} 含高价值沉淀内容(影响面/触达红线)但未登记沉淀去向——整理到 wiki 后在 frontmatter 记「沉淀: wiki/<主题>/<文件>」,或显式写「沉淀: 无」(advisory,灰度第一档;沉淀是人工判断,机器只提醒不强制)`);
966
+ } else if (sed && sed !== '无' && !fs.existsSync(path.resolve(ROOT, sed))) {
967
+ warnings.push(`- [WARN 沉淀断档] ${base} frontmatter「沉淀: ${sed}」指向的文件不存在——沉淀文件移动/改名后须同步更新本字段(advisory,灰度第一档)`);
968
+ } else if (sed && sed !== '无') {
969
+ warnSedimentShell(base, sed); // 落点真实性(P1-3):存在但空壳 → WARN 落点空壳
970
+ }
880
971
  if (!uc && !ne) continue;
881
972
  if (isNew) {
882
973
  if (uc) blockers.push(`- [验收未对账] done intent 验收标准有未勾验项:${base}(逐条勾验并补证据:commit/用例/冒烟输出)`);
@@ -929,15 +1020,8 @@ export function runCheckLoop(opts = {}) {
929
1020
  // 台账 approved 文档集合:单次读取(坏行容忍跳过,与检查 15 同口径);缺失视为空集(git 判据仍守门)
930
1021
  const ledgerApproved = new Set();
931
1022
  try {
932
- const lp = path.join(ROOT, '.agents', 'confirmations.jsonl');
933
- if (fs.existsSync(lp)) {
934
- for (const line of fs.readFileSync(lp, 'utf8').split(/\r?\n/)) {
935
- if (!line.trim()) continue;
936
- try {
937
- const e = JSON.parse(line);
938
- if (e && typeof e.doc === 'string' && e.stage === 'approved') ledgerApproved.add(e.doc);
939
- } catch { /* 坏行容忍跳过 */ }
940
- }
1023
+ for (const e of readLedgerRows(ROOT)) {
1024
+ if (typeof e.doc === 'string' && e.stage === 'approved') ledgerApproved.add(e.doc);
941
1025
  }
942
1026
  } catch { /* 台账不可读视为无 approved 行 */ }
943
1027
  for (const sub of ['specs', 'plans']) {
@@ -976,18 +1060,26 @@ export function runCheckLoop(opts = {}) {
976
1060
  // 台账坏行容忍跳过(审计件,jsonl 追加式);frontmatter 存 16 位、台账存 64 位,按前 16 位配对。
977
1061
  {
978
1062
  const EFFECTIVE = kitPolicy.confirmDocsEffective;
979
- const ledgerPath = path.join(ROOT, '.agents', 'confirmations.jsonl');
1063
+ // 取数走 ledger-loader 的**带来源行号视图**(2026-10-12 ledger-shard-write):主台账 + 分片
1064
+ // `.agents/confirmations.d/*.jsonl`。分片里的行同样受链校验——否则 `--shard` 写入就等于给台账开了
1065
+ // 一个防伪盲区(门禁看得见分片行、却不验它们的链)。
1066
+ // **此处曾刻意不走 loader**(单源化时留的例外,因当时要物理行号对齐);引入分片后该理由已不成立
1067
+ // ——单一行号跨来源无法定位,故改用带 rel 的定位。第二处例外仍是 check-ledger-invariant.mjs
1068
+ // (它审的是**盘面字节**的 append-only 前缀不变量,不是台账语义,见该文件头部)。
980
1069
  const ledger = [];
981
- const ledgerBadLines = []; // 坏行(JSON.parse 失败)的物理行号(复核 P2-1:报文行号须对齐物理行)
982
- if (fs.existsSync(ledgerPath)) {
983
- for (const [lineNo, line] of (linesOf(ledgerPath) || []).entries()) {
984
- if (!line.trim()) continue;
985
- try { ledger.push(JSON.parse(line)); } catch { ledgerBadLines.push(lineNo + 1); }
986
- }
1070
+ const ledgerRefs = []; // 与 ledger 同下标:{rel, no}——断裂报文的定位来源
1071
+ for (const e of readLedgerEntries(ROOT)) {
1072
+ if (e.row === null) continue; // 坏行跳过(容错口径同前;它占物理行号但不入 ledger 下标)
1073
+ ledger.push(e.row);
1074
+ ledgerRefs.push({ rel: e.rel, no: e.no });
987
1075
  }
988
- // 台账哈希链校验(2026-10-09 engine-quality-round2 D;写入侧=confirm-doc appendLedger,哈希单源
989
- // policy.ledgerChainHash——判定零复刻)。判据:连续带 hash 行段内,① prevHash 须等于前一带 hash
990
- // 行的 hash(无 hash 行后链段重启 '',与写入侧读末行口径一致);② hash 须可重算一致。
1076
+ // 台账哈希链校验(2026-10-09 engine-quality-round2 D;2026-10-12 ledger-doc-chain 改链域;
1077
+ // 写入侧=confirm-doc appendLedger,哈希单源 policy.ledgerChainHash——判定零复刻)。
1078
+ // **链域已从「文件物理序」改为「按 doc 的逻辑序」**(锚后生效,理由与不追溯口径见 POLICIES v8):
1079
+ // 原判据 ① prevHash = 前一带 hash 行(物理)的 hash;新判据 ① = **同行 doc** 最后一个带 hash 行的
1080
+ // hash。两者都要求 ② hash 可重算一致。无 hash 行仍按「链段重启」处理。
1081
+ // 并发 append / git 并集合并 / 分片归并这三种**正常协作**都会打断物理链——门禁若继续据此 hard 拦,
1082
+ // 必然被 --no-verify 绕过,治理失效;而 doc 链对就地改行 / 删行 / 插行三类篡改的检出能力不变。
991
1083
  // **坏行口径(复核 P2-1 勘误,此前注释误称「与写入侧一致」)**:坏行被本解析整体剔除、不重置游标
992
1084
  // ——坏行后写入的链行(写入侧读末行遇坏行 prevHash='')验链必失配出账。这是**预期 fail-loud**
993
1085
  // (坏行本身即台账完整性事故,应人工查验),非口径一致场景。
@@ -998,19 +1090,36 @@ export function runCheckLoop(opts = {}) {
998
1090
  let prevHash = '';
999
1091
  let brokenAt = 0;
1000
1092
  let brokenCount = 0;
1093
+ // 链域分界(2026-10-12 ledger-doc-chain):doc 链锚存在且合法时,**按行分域**验链——
1094
+ // 锚前行(ts < 锚)维持物理链口径(存量零变化);锚后行(ts ≥ 锚)按「同行 doc 的最后一个
1095
+ // 带 hash 行」接续,并同样要求 hash 可重算一致。锚缺键(v1-v7)→ 全表走物理链(= 旧行为)。
1096
+ // byDoc 对**两种域的行都更新**:锚后行需要接到锚前该 doc 的最后一行,跨域才能连续。
1097
+ const docChainOn = ledgerDocChainActive(kitPolicy);
1098
+ const docChainSince = docChainOn ? kitPolicy.ledgerDocChainSince : '';
1099
+ const byDoc = new Map();
1100
+ const prunedHashSet = prunedHashesOf(ROOT); // 特许修剪凭据(无 manifest 时为空集,零 IO 成本)
1001
1101
  ledger.forEach((row, idx) => {
1002
1102
  if (!row || typeof row.hash !== 'string') { prevHash = ''; return; } // 存量行/无链字段 → 链段重启
1003
1103
  const { hash, prevHash: p, ...rest } = row;
1004
- const ok = (p ?? '') === prevHash && ledgerChainHash(rest, p ?? '') === hash;
1104
+ const inDocChain = docChainOn && typeof row.ts === 'string' && row.ts >= docChainSince;
1105
+ const expected = inDocChain ? (byDoc.get(row.doc) ?? '') : prevHash;
1106
+ // 特许修剪链段重启(2026-10-12 ledger-prune-manifest):删掉某 doc 的链行后,其后继行仍带被删行
1107
+ // 的 prevHash,而该行已不在表内 → 必然失配。本行的 prevHash 若出现在 manifest 声明的被删哈希里,
1108
+ // 即视为**有凭据的合法重启点**而非篡改(凭据 = .agents/ledger-prune-manifest.json,入库可对质)。
1109
+ const prunedRestart = (p ?? '') !== expected && prunedHashSet.has(p ?? '');
1110
+ const ok = ((p ?? '') === expected || prunedRestart) && ledgerChainHash(rest, p ?? '') === hash;
1005
1111
  if (!ok) {
1006
- if (!brokenAt) brokenAt = idx + 1;
1112
+ if (!brokenAt) brokenAt = idx + 1; // 1-based 下标,供 ledgerRefs 定位
1007
1113
  brokenCount++;
1008
1114
  }
1009
1115
  prevHash = hash;
1116
+ if (typeof row.doc === 'string') byDoc.set(row.doc, hash);
1010
1117
  });
1011
1118
  if (brokenAt) {
1012
- const physical = brokenAt + ledgerBadLines.filter((n) => n <= brokenAt).length; // 物理行号(复核 P2-1:剔除坏行会令数组下标漂移)
1013
- blockers.push(`- [台账链断裂] confirmations.jsonl 自第 ${physical} 行(物理)起哈希链校验失败(共 ${brokenCount} 行不一致——append-only 台账出现就地改/删痕迹;prevHash 接续或内容重算不符。整文件重写不可机器防,对质走台账自身 git 历史)`);
1119
+ // 定位用 ledgerRefs(来源 + 行号):跨来源后单一行号无法定位;坏行占位不计入 ledger 下标
1120
+ const at = ledgerRefs[brokenAt - 1] || { rel: LEDGER_REL, no: brokenAt };
1121
+ const domain = docChainOn ? '锚前物理链 / 锚后 doc 链' : '物理链';
1122
+ blockers.push(`- [台账链断裂] 自 ${at.rel}:${at.no} 起哈希链校验失败(${domain},共 ${brokenCount} 行不一致——append-only 台账出现就地改/删痕迹;prevHash 接续或内容重算不符。整文件重写不可机器防,对质走台账自身 git 历史)`);
1014
1123
  }
1015
1124
  }
1016
1125
  // scope 子项(2026-10-09 审查 A1 干净版):受管台账行须带**机器派生**的改动面 scope{files,sha256}。
@@ -1137,7 +1246,7 @@ export function runCheckLoop(opts = {}) {
1137
1246
  if (rows.every((r) => r.brief === true)) continue;
1138
1247
  const of = Math.max(...rows.map((r) => (typeof r.of === 'number' ? r.of : 0)));
1139
1248
  const docs = rows.map((r) => String(r.doc).replace(/^workflow\//, '')).join('、');
1140
- warnings.push(`- [WARN 确认并录] 单次调用落账 ${rows.length} 份(batch ${b},of=${of}):${docs}——同一次 --delegated 调用放行多份,塌掉「逐件确认」门(build.md);口径见 workflow/papercuts.md 2026-09-28 与 incidents/2026-09-28-batch-ledger-audit.md`);
1249
+ warnings.push(`- [WARN 确认并录] 单次调用落账 ${rows.length} 份(batch ${b},of=${of}):${docs}——同一次 --delegated 调用放行多份,塌掉「逐件确认」门(build.md;口径见根 AGENTS.md「门禁与提交」确认门节)`);
1141
1250
  }
1142
1251
  }
1143
1252
  }
@@ -1240,13 +1349,7 @@ export function runCheckLoop(opts = {}) {
1240
1349
  if (typeof since === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(since)) {
1241
1350
  const ledgerRows = [];
1242
1351
  {
1243
- const lp = path.join(ROOT, '.agents', 'confirmations.jsonl');
1244
- if (fs.existsSync(lp)) {
1245
- for (const line of linesOf(lp) || []) {
1246
- if (!line.trim()) continue;
1247
- try { ledgerRows.push(JSON.parse(line)); } catch { /* 坏行跳过(同检查 15 口径) */ }
1248
- }
1249
- }
1352
+ for (const e of readLedgerRows(ROOT)) ledgerRows.push(e);
1250
1353
  }
1251
1354
  const hasRow = (rel) => ledgerRows.some((e) => e && e.doc === rel && (e.stage === 'approved' || e.stage === 'done'));
1252
1355
  const docConfirmed = (rel, abs) => {
@@ -1556,3 +1659,110 @@ function readdirOrNull(dir) {
1556
1659
  function isDirectoryOrNull(p) {
1557
1660
  try { return fs.statSync(p).isDirectory(); } catch { return false; }
1558
1661
  }
1662
+
1663
+ // 场景 ID 提取(2026-10-10 scenario-coverage,检查 8 场景覆盖子判据的 spec 侧单源):
1664
+ // 认「## 验收场景」节内的场景声明——三级标题 `### S1 <名>` 或列表项 `- S1 <名>`,
1665
+ // 提取 S<n> 形 ID(按出现序去重)。节外不认(防「## 功能行为」里偶然出现的 S1 干扰)。
1666
+ // 无该节 / 无场景声明 → 空数组(调用方按「不适用」跳过,原判据零变化)。
1667
+ // 独立 export 供单元测试直测(纯函数:只吃文件路径、只读)。
1668
+ export function scenarioIdsOf(specPath) {
1669
+ let lines;
1670
+ try { lines = fs.readFileSync(specPath, 'utf8').split(/\r?\n/); } catch { return []; }
1671
+ const ids = [];
1672
+ let inSec = false;
1673
+ for (const line of lines) {
1674
+ if (/^\s*##\s+[^#]*验收场景/.test(line)) { inSec = true; continue; }
1675
+ if (inSec && /^\s*##\s/.test(line)) break; // 下一二级标题 = 节末
1676
+ if (!inSec) continue;
1677
+ const m = line.match(/^\s*(?:###|-)\s*\**S(\d+)\b/i);
1678
+ if (m && !ids.includes(`S${m[1]}`)) ids.push(`S${m[1]}`);
1679
+ }
1680
+ return ids;
1681
+ }
1682
+
1683
+ // THEN 断言抽取(2026-10-11 then-assertion,检查 8「场景无断言」子判据的 spec 侧单源):
1684
+ // 输入 spec 全文,定位「## 验收场景」节(节定位正则与 scenarioIdsOf 同款——口径单源,改写法时两处同步),
1685
+ // 按场景块分组(`### S1 <名>` 三级标题或 `- S1 <名>` 列表项,声明正则同 scenarioIdsOf),抽块内 THEN 断言行。
1686
+ // 行匹配口径:剥缩进后以 `- THEN` 开头(模板规定形式);`THEN:` / `THEN `(无连字符)也认——
1687
+ // 防「写了断言但少了 `- ` 前缀」的写法漂移把断言误判为空;只认行首 THEN,GIVEN/WHEN 行中段的
1688
+ // "THEN" 字样不误抽。返回 Map<S-ID, string[]>(声明即建键,无 THEN = 空数组——空壳判定交给调用方);
1689
+ // 节不存在 / 非字符串输入 → 空 Map(验收场景节本身可选,调用方零变化)。
1690
+ // 独立 export 供单元测试直测(纯函数:只吃文本、无 IO)。
1691
+ export function extractThenByScenario(text) {
1692
+ const byScen = new Map();
1693
+ if (typeof text !== 'string') return byScen; // 读取失败(调用方传 null)→ 空 Map,调用方按不可判定跳过
1694
+ const lines = text.split(/\r?\n/);
1695
+ let cur = null; // 当前场景块 ID(null = 尚未进入任何场景块,节头说明行不算断言)
1696
+ let inSec = false;
1697
+ for (const line of lines) {
1698
+ if (/^\s*##\s+[^#]*验收场景/.test(line)) { inSec = true; continue; }
1699
+ if (inSec && /^\s*##\s/.test(line)) break; // 下一二级标题 = 节末
1700
+ if (!inSec) continue;
1701
+ const decl = line.match(/^\s*(?:###|-)\s*\**S(\d+)\b/i);
1702
+ if (decl) {
1703
+ const id = `S${decl[1]}`;
1704
+ if (!byScen.has(id)) byScen.set(id, []);
1705
+ cur = id;
1706
+ continue;
1707
+ }
1708
+ if (cur === null) continue;
1709
+ const s = line.trim();
1710
+ if (/^-\s*THEN\b/i.test(s) || /^THEN\b/i.test(s)) byScen.get(cur).push(s);
1711
+ }
1712
+ return byScen;
1713
+ }
1714
+
1715
+ // 节体非空判定(2026-10-10 sediment-tracking,检查 8 沉淀追踪子判据的「高价值」信号):
1716
+ // 认 `## <标题前缀>` 节内**标题之后**存在非空非占位行(占位 = `<…>` 单行包揽 / HTML 注释 / 引用)。
1717
+ // 只有光标题(如模板未填的 `## 影响面`)= 未沉淀价值,不出告警——防模板未填段被误判为高价值。
1718
+ // 前缀匹配(startsWith):兼容「## 影响面」「## 影响面与边界」等变体写法。
1719
+ // 独立 export 供单元测试直测(纯函数:只吃行数组、无 IO)。
1720
+ export function sectionHasBody(lines, headingPrefix) {
1721
+ let inSec = false;
1722
+ for (const line of lines) {
1723
+ if (/^\s*##\s/.test(line)) {
1724
+ const t = line.replace(/^\s*##\s*/, '').trim();
1725
+ inSec = t.startsWith(headingPrefix);
1726
+ continue;
1727
+ }
1728
+ if (!inSec) continue;
1729
+ const s = line.trim();
1730
+ if (!s) continue;
1731
+ if (/^<.*>$/.test(s) || s.startsWith('<!--') || s.startsWith('>')) continue; // 占位 / 注释 / 引用
1732
+ return true;
1733
+ }
1734
+ return false;
1735
+ }
1736
+
1737
+ // 沉淀落点空壳判定(2026-10-11 landing-reality,「存在 ≠ 有效」的落点真实性一环):
1738
+ // 落点文件可能是只有标题没有内容的空壳。满足任一即**非**空壳(有效沉淀):
1739
+ // ① frontmatter 含「来源:」键(人工已登记来源 = 真实整理的机器信号;frontmatter 口径同 fmGet 受限子集);
1740
+ // ② frontmatter / 标题行 / 占位行剔除后的非空正文 ≥ 3 行(占位口径逐字对齐 sectionHasBody:
1741
+ // `<…>` 单行包揽 / HTML 注释 / 引用块)——标题行不算正文,防「只有标题」凑行数绕过。
1742
+ // text = 落点文件全文;非字符串(调用方读取失败传 null)按空壳处理,调用方在告警文案注明读取失败。
1743
+ // 独立 export 供单元测试直测(纯函数:只吃文本、无 IO)。
1744
+ export function sedimentLandingIsShell(text) {
1745
+ if (typeof text !== 'string') return true; // 读取失败 → 按空壳
1746
+ const lines = text.split(/\r?\n/);
1747
+ const isDelim = (l) => /^---\s*$/.test(l); // 同 fmGet:首行 --- 进入、下一 --- 闭合
1748
+ let inFm = isDelim(lines[0] || '');
1749
+ if (inFm) {
1750
+ for (let i = 1; i < lines.length; i++) {
1751
+ if (isDelim(lines[i])) break; // 下一 --- = frontmatter 闭合
1752
+ if (lines[i].startsWith('来源:')) return false; // ① 有来源登记 = 有效
1753
+ }
1754
+ }
1755
+ let body = 0;
1756
+ for (let i = inFm ? 1 : 0; i < lines.length; i++) {
1757
+ if (inFm) {
1758
+ if (isDelim(lines[i])) inFm = false; // frontmatter 闭合,其后才开始数正文
1759
+ continue;
1760
+ }
1761
+ const s = lines[i].trim();
1762
+ if (!s) continue;
1763
+ if (s.startsWith('#')) continue; // 标题行 = 结构不算正文(空壳 = 只有标题没有内容)
1764
+ if (/^<.*>$/.test(s) || s.startsWith('<!--') || s.startsWith('>')) continue; // 占位 / 注释 / 引用(同 sectionHasBody)
1765
+ if (++body >= 3) return false; // ② 非占位正文 ≥ 3 行 = 有效
1766
+ }
1767
+ return true;
1768
+ }