agentic-flow-kit 0.5.0 → 0.6.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 (82) hide show
  1. package/README.md +2 -0
  2. package/modules/gates/dotnet-ca/check-architecture.sh +23 -5
  3. package/modules/hosts/omp/agents/implementer.md +2 -0
  4. package/modules/hosts/omp/agents/independent-reviewer.md +2 -0
  5. package/modules/hosts/omp/agents/ui-verifier.md +2 -0
  6. package/modules/hosts/opencode/agents/implementer.md +2 -0
  7. package/modules/hosts/opencode/agents/independent-reviewer.md +2 -0
  8. package/modules/hosts/opencode/agents/ui-verifier.md +2 -0
  9. package/modules/hosts/opencode/commands/build.md +20 -1
  10. package/modules/hosts/opencode/commands/design.md +2 -0
  11. package/modules/hosts/opencode/commands/gate-checklist.md +77 -0
  12. package/modules/hosts/opencode/commands/maintain.md +2 -3
  13. package/modules/hosts/opencode/commands/plan.md +4 -1
  14. package/modules/hosts/opencode/commands/review.md +1 -0
  15. package/modules/hosts/opencode/commands/source-sync-check.md +89 -0
  16. package/modules/hosts/opencode/commands/sync-hosts.md +66 -0
  17. package/modules/hosts/opencode/commands/test.md +4 -2
  18. package/modules/hosts/trae/agents/implementer.md +2 -0
  19. package/modules/hosts/trae/agents/independent-reviewer.md +2 -0
  20. package/modules/hosts/trae/agents/ui-verifier.md +2 -0
  21. package/modules/hosts/trae/commands/wf-build.md +20 -1
  22. package/modules/hosts/trae/commands/wf-design.md +2 -0
  23. package/modules/hosts/trae/commands/wf-gate-checklist.md +78 -0
  24. package/modules/hosts/trae/commands/wf-maintain.md +2 -3
  25. package/modules/hosts/trae/commands/wf-plan.md +4 -1
  26. package/modules/hosts/trae/commands/wf-review.md +1 -0
  27. package/modules/hosts/trae/commands/wf-source-sync-check.md +90 -0
  28. package/modules/hosts/trae/commands/wf-sync-hosts.md +67 -0
  29. package/modules/hosts/trae/commands/wf-test.md +4 -2
  30. package/modules/hosts/trae/hooks/post-edit-check.cjs +3 -2
  31. package/modules/hosts/trae/hooks/pre-shell-check.cjs +40 -9
  32. package/modules/hosts/trae/hooks.json +1 -1
  33. package/modules/hosts/zcode/agents/implementer.md +2 -0
  34. package/modules/hosts/zcode/agents/independent-reviewer.md +2 -0
  35. package/modules/hosts/zcode/agents/ui-verifier.md +2 -0
  36. package/package.json +2 -2
  37. package/src/add-gate.mjs +5 -3
  38. package/src/doctor.mjs +121 -18
  39. package/src/gate-dotnet-ca.test.mjs +124 -0
  40. package/src/init.mjs +79 -17
  41. package/src/init.test.mjs +60 -1
  42. package/src/render.mjs +64 -4
  43. package/src/run-tests.mjs +3 -0
  44. package/src/sync-hosts.mjs +3 -2
  45. package/src/sync.mjs +64 -20
  46. package/src/sync.test.mjs +42 -0
  47. package/src/trae-hooks.test.mjs +115 -0
  48. package/templates/AGENTS.md +1 -0
  49. package/templates/_agents/board/index.html +32 -3
  50. package/templates/_agents/commands/build.md +5 -4
  51. package/templates/_agents/commands/design.md +1 -0
  52. package/templates/_agents/commands/maintain.md +1 -2
  53. package/templates/_agents/commands/plan.md +2 -0
  54. package/templates/_agents/commands/test.md +2 -1
  55. package/templates/_agents/hooks/check-pairing-incremental.sh +20 -7
  56. package/templates/_agents/roles/implementer.md +2 -0
  57. package/templates/_agents/roles/independent-reviewer.md +2 -0
  58. package/templates/_agents/roles/ui-verifier.md +2 -0
  59. package/templates/_agents/scripts/agg-delegations.cjs +39 -30
  60. package/templates/_agents/scripts/agg-delegations.test.mjs +32 -0
  61. package/templates/_agents/scripts/check-loop.mjs +262 -16
  62. package/templates/_agents/scripts/check-loop.test.mjs +621 -0
  63. package/templates/_agents/scripts/confirm-doc.mjs +228 -0
  64. package/templates/_agents/scripts/confirm-doc.test.mjs +321 -0
  65. package/templates/_agents/scripts/doctor.test.mjs +74 -1
  66. package/templates/_agents/scripts/ensure-board.mjs +29 -2
  67. package/templates/_agents/scripts/gate-checklist.mjs +2 -0
  68. package/templates/_agents/scripts/gate-checklist.test.mjs +3 -3
  69. package/templates/_agents/scripts/gen-wiki-board.mjs +38 -24
  70. package/templates/_agents/scripts/gen-wiki-board.test.mjs +18 -0
  71. package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -3
  72. package/templates/_agents/scripts/kb-search.mjs +22 -8
  73. package/templates/_agents/scripts/kb-search.test.mjs +38 -8
  74. package/templates/_agents/scripts/source-sync-check.mjs +17 -3
  75. package/templates/_agents/scripts/source-sync-check.test.mjs +96 -8
  76. package/templates/_agents/scripts/verify-wiki-consistency.mjs +46 -23
  77. package/templates/_agents/scripts/workflow-board-server.mjs +64 -9
  78. package/templates/_agents/scripts/workflow-board-server.test.mjs +136 -0
  79. package/templates/_agents/scripts/workflow-enums.mjs +4 -2
  80. package/templates/_agents/workflow-enums.txt +3 -3
  81. package/templates/_agents/workflows/_TEMPLATE.md +1 -0
  82. package/templates/_githooks/pre-commit +16 -1
@@ -1,16 +1,20 @@
1
1
  #!/bin/sh
2
- # 闭环配对(增量版):本次暂存的 intents/ incidents/ 入口文档必须已有同名 plan
2
+ # 闭环配对(增量版):本次暂存的 intents/ incidents/ 入口文档必须已有同名 plan(L2/L3 另须同名 spec)
3
3
  # 目的:把 check-loop.sh「配对断裂」hard-block 的发现时机从 pre-push 提前到 commit
4
4
  # (papercut 2026-09-15:maintain.md 只写「incident≡intent 免另立 intent」,未把配 plan 写成显式步骤,
5
- # 按文档字面执行三次 commit 全绿、push 才爆红,只能后补)
6
- # 口径与 check-loop.sh 对齐:入口文档(intents/ incidents/ 直下 .md)↔ workflow/plans/ 同名;
7
- # 同 commit 携带 plan 合法(检查的是工作区文件存在,staged 新建 plan 同样命中)。
8
- # 已知边界:legacy 豁免读的是工作区文件而非暂存 blob——add 后又改工作区时可能误豁免/误拦(逐文件取 blob 的 fork 成本大于收益,接受)。
5
+ # 按文档字面执行三次 commit 全绿、push 才爆红,只能后补;
6
+ # 2026-09-27 audit-gate-hardening 补齐 spec 档——此前只查 plan,L2/L3 缺 spec 仍要拖到 push 才爆红)
7
+ # 口径与 check-loop.sh 对齐:入口文档(intents/ incidents/ 直下 .md)↔ workflow/plans/ 同名,
8
+ # L2/L3 级别另须 workflow/specs/ 同名(check-loop 检查 1 同款分档);
9
+ # 同 commit 携带 plan/spec 合法(检查的是工作区文件存在,staged 新建同样命中)。
10
+ # 已知边界:legacy 豁免与级别读取均基于工作区文件而非暂存 blob——add 后又改工作区时可能误豁免/误拦(逐文件取 blob 的 fork 成本大于收益,接受)。
9
11
 
10
12
  cd "$(git rev-parse --show-toplevel)" || exit 1
11
13
 
12
14
  fail=0
13
- for f in $(git diff --cached --name-only --diff-filter=ACMR | grep -E '^workflow/(intents|incidents)/[^/]+\.md$' | grep -v '_TEMPLATE\.md$'); do
15
+ # -c core.quotepath=off:quotepath 默认 true 时非 ASCII 路径被引号转义("workflow/\346..."),
16
+ # ^workflow/ 前缀匹配失配 → 门禁静默跳过——教训与修法同 check-wiki-ledger.sh(2026-09-27 gate-coverage)
17
+ for f in $(git -c core.quotepath=off diff --cached --name-only --diff-filter=ACMR | grep -E '^workflow/(intents|incidents)/[^/]+\.md$' | grep -v '_TEMPLATE\.md$'); do
14
18
  # 闭环引擎启用前的回填件带「流程: legacy」豁免配对检查(与 check-loop 同口径)
15
19
  if grep -q '^流程: legacy' "$f"; then
16
20
  continue
@@ -22,9 +26,18 @@ for f in $(git diff --cached --name-only --diff-filter=ACMR | grep -E '^workflow
22
26
  echo " L1 极简 plan 只需「改动面 + 验证方式」两节(复制 workflow/plans/_TEMPLATE.md)"
23
27
  fail=1
24
28
  fi
29
+ lvl=$(sed -n 's/^级别:[[:space:]]*//p' "$f" | head -1)
30
+ if [ "$lvl" = "L2" ] || [ "$lvl" = "L3" ]; then
31
+ if [ ! -f "workflow/specs/$base.md" ]; then
32
+ echo "闭环配对(增量)— BLOCK:"
33
+ echo " 入口缺同名 spec: $f(级别 $lvl 须三件套,应在 workflow/specs/$base.md)"
34
+ echo " 与 pre-push check-loop 检查 1 同口径——先起草 spec 走 design.md,再提交"
35
+ fail=1
36
+ fi
37
+ fi
25
38
  done
26
39
 
27
40
  if [ $fail -ne 0 ]; then
28
- echo "pre-commit: 入口文档缺同名 plan,提交已阻止(与 pre-push check-loop 同口径,发现时机提前到 commit)" >&2
41
+ echo "pre-commit: 入口文档配对不全(plan / L2-L3 spec),提交已阻止(与 pre-push check-loop 同口径,发现时机提前到 commit)" >&2
29
42
  fi
30
43
  exit $fail
@@ -1,5 +1,7 @@
1
1
  # Implementer
2
2
 
3
+ > 公共角色契约(本文件):`.agents/roles/implementer.md`。宿主薄适配(`.opencode/` / `.trae/` / `.zcode/` 下 `agents/implementer.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
4
+
3
5
  ## 职责
4
6
 
5
7
  执行已经由主智能体和用户确认、范围明确的实现任务。适用范围仅限:
@@ -1,5 +1,7 @@
1
1
  # Independent Reviewer
2
2
 
3
+ > 公共角色契约(本文件):`.agents/roles/independent-reviewer.md`。宿主薄适配(`.opencode/` / `.trae/` / `.zcode/` 下 `agents/independent-reviewer.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
4
+
3
5
  ## 职责
4
6
 
5
7
  在独立上下文中对 spec、plan 或代码 diff 做只读复核,发现原作者可能遗漏的正确性、契约、架构、数据安全和回归风险。
@@ -1,5 +1,7 @@
1
1
  # UI Verifier
2
2
 
3
+ > 公共角色契约(本文件):`.agents/roles/ui-verifier.md`。宿主薄适配(`.opencode/` / `.trae/` / `.zcode/` 下 `agents/ui-verifier.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
4
+
3
5
  ## 职责
4
6
 
5
7
  对已经实现的 UI 改动执行浏览器实测并收集可复核证据。只验证,不修复。
@@ -4,7 +4,7 @@
4
4
  * 输出各月指标(一次通过率 / 平均返工次数 / 主兜底占比)、并发扩容门判定、可粘贴的月度快照行。
5
5
  * 结果取值:一次通过 | 返工×N | 主兜底 | 返工待修(未闭环,不计入率)。
6
6
  * 用法:node .agents/scripts/agg-delegations.cjs [--month=YYYY-MM](默认输出全部月份 + 全量累计)
7
- * 门槛定义见 workflow/delegations.md §并发扩容门槛,两处须同步改。
7
+ * 门槛定义见 workflow/delegations.md §并发扩容门槛(doc 为权威,脚本向文档对齐——2026-09-27 board-kb-p1)。
8
8
  */
9
9
  'use strict';
10
10
  const fs = require('fs');
@@ -88,8 +88,11 @@ function countIncidents() {
88
88
  return byMonth;
89
89
  }
90
90
 
91
- // 门槛(与 workflow/delegations.md §并发扩容门槛 同步)
92
- const GATE = { minSample: 20, minPassRate: 0.9, maxAvgRework: 0.3, minDelegated: 5, maxFallbackRate: 0.1 };
91
+ // 门槛(2026-09-27 board-kb-p1 对齐 workflow/delegations.md §并发扩容门槛声明——doc 为权威):
92
+ // 月度口径项:一次完成率 ≥90% | 月度返工次数 = 0 | 无主兜底信号(=0)(旧「平均返工≤0.3 / 兜底≤10%」为宽松分叉,退役)
93
+ // 样本护栏项(脚本侧统计可信度门槛,非口径分叉):月样本量 ≥20 | 委派样本 ≥5
94
+ // 连续性:扩容需「本月 + 上一自然月」连续两月月度口径全达标(delegations.md「连续两个月」)
95
+ const GATE = { minSample: 20, minPassRate: 0.9, minDelegated: 5 };
93
96
 
94
97
  function metrics(rows) {
95
98
  // 未知结果(结果列拼错/漏填)单列计数,不混进「待修」——待修=未闭环,未知=记录本身有问题(2026-09-24 口径分离)
@@ -126,29 +129,26 @@ function fmtNum(x) {
126
129
  return x == null ? '—' : x.toFixed(2);
127
130
  }
128
131
 
129
- function gate(m, incidents) {
132
+ // gateMonth:单月门槛判定(pure,供测试)——口径项 + 样本护栏项分开列
133
+ function gateMonth(m) {
130
134
  const items = [];
131
- items.push({ no: 1, ok: m.total >= GATE.minSample, desc: `样本量 ${m.total}/${GATE.minSample}` });
132
- items.push({
133
- no: 2,
134
- ok: m.passRate != null && m.passRate >= GATE.minPassRate,
135
- desc: `一次通过率 ${fmtRate(m.passRate)}≥90%`,
136
- });
137
- items.push({
138
- no: 3,
139
- ok: m.avgRework != null && m.avgRework <= GATE.maxAvgRework,
140
- desc: `平均返工 ${fmtNum(m.avgRework)}≤0.3`,
141
- });
142
- const delegOk = m.delegatedValid >= GATE.minDelegated && m.fallbackRate != null && m.fallbackRate <= GATE.maxFallbackRate;
143
- items.push({
144
- no: 4,
145
- ok: delegOk,
146
- desc: `委派样本 ${m.delegatedValid}≥5 且主兜底 ${fmtRate(m.fallbackRate)}≤10%`,
147
- });
148
- items.push({ no: 5, ok: null, desc: '门禁不失守(人工对照当月 incident 定性)' });
149
- const hardOk = items.filter((i) => i.ok !== null).every((i) => i.ok);
150
- const bad = items.filter((i) => i.ok === false).map((i) => i.no).join('+');
151
- return { items, verdict: hardOk ? '✅(待人工确认门5)' : `❌ 未达标项:${bad}` };
135
+ items.push({ no: 1, ok: m.total >= GATE.minSample, desc: `样本量 ${m.total}/${GATE.minSample}`, kind: '样本护栏' });
136
+ items.push({ no: 2, ok: m.passRate != null && m.passRate >= GATE.minPassRate, desc: `一次完成率 ${fmtRate(m.passRate)}≥90%`, kind: '口径' });
137
+ items.push({ no: 3, ok: m.reworkSum === 0, desc: `月度返工次数 ${m.reworkSum}=0`, kind: '口径' });
138
+ items.push({ no: 4, ok: m.fallback === 0, desc: `主兜底 ${m.fallback}=0(无主兜底信号)`, kind: '口径' });
139
+ items.push({ no: 5, ok: m.delegatedValid >= GATE.minDelegated, desc: `委派样本 ${m.delegatedValid}≥5`, kind: '样本护栏' });
140
+ items.push({ no: 6, ok: null, desc: '门禁不失守(人工对照当月 incident 定性)', kind: '人工' });
141
+ return { items, monthOk: items.filter((i) => i.ok !== null).every((i) => i.ok) };
142
+ }
143
+
144
+ // expansionVerdict:连续两个月扩容判定(pure,供测试)——本月与上一自然月 monthOk 均真才「可扩容」;
145
+ // prevMissing = 无上月数据(首月/断档);items 供未达标项列出
146
+ function expansionVerdict(monthOk, prevOk, prevMissing, items) {
147
+ if (monthOk && prevOk) return '✅ 连续两月达标(可扩容,待人工确认门6)';
148
+ if (monthOk && prevMissing) return '⚠️ 本月达标(连续性 1/2——上月无数据)';
149
+ if (monthOk) return '⚠️ 本月达标(连续性 1/2——上月未达标)';
150
+ const bad = (items || []).filter((i) => i.ok === false).map((i) => i.no).join('+');
151
+ return `❌ 未达标(连续性中断):${bad || '月度口径项未全过'}`;
152
152
  }
153
153
 
154
154
  function main() {
@@ -165,14 +165,18 @@ function main() {
165
165
  console.log(`# 量化证据聚合(${argMonth || '全部月份'}) 生成于 ${new Date().toISOString().slice(0, 10)}\n`);
166
166
 
167
167
  const snapshotRows = [];
168
+ const monthResults = new Map(); // ym → { monthOk }(连续两月判定用)
168
169
  for (const ym of months) {
169
170
  const m = metrics(byMonth[ym]);
170
- const g = gate(m, incidents[ym] || 0);
171
+ const { items, monthOk } = gateMonth(m);
172
+ monthResults.set(ym, monthOk);
173
+ const prevYm = (() => { const [y, mo] = ym.split('-').map(Number); const d = new Date(Date.UTC(y, mo - 2, 1)); return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`; })();
174
+ const verdict = expansionVerdict(monthOk, monthResults.get(prevYm), !months.includes(prevYm), items);
171
175
  console.log(`## ${ym}`);
172
176
  console.log(` 有效任务 ${m.total}(待修 ${m.pending}${m.unknown ? `、未知结果 ${m.unknown}` : ''})|一次通过 ${m.pass}(${fmtRate(m.passRate)})|返工总次数 ${m.reworkSum}(平均 ${fmtNum(m.avgRework)})|主兜底 ${m.fallback}/${m.delegatedValid}(${fmtRate(m.fallbackRate)})|incident ${incidents[ym] || 0} 起`);
173
- for (const i of g.items) console.log(` 门${i.no} ${i.ok === null ? '[人工]' : i.ok ? '✅' : '❌'} ${i.desc}`);
174
- console.log(` 扩容门判定:${g.verdict}\n`);
175
- snapshotRows.push(`| ${ym} | ${m.total} | ${fmtRate(m.passRate)} | ${fmtNum(m.avgRework)} | ${fmtRate(m.fallbackRate)} | ${incidents[ym] || 0} | ${g.verdict} | 样本含待修${m.pending}${m.unknown ? `、未知${m.unknown}` : ''} |`);
177
+ for (const i of items) console.log(` 门${i.no}[${i.kind}] ${i.ok === null ? '[人工]' : i.ok ? '✅' : '❌'} ${i.desc}`);
178
+ console.log(` 扩容门判定:${verdict}\n`);
179
+ snapshotRows.push(`| ${ym} | ${m.total} | ${fmtRate(m.passRate)} | ${fmtNum(m.avgRework)} | ${fmtRate(m.fallbackRate)} | ${incidents[ym] || 0} | ${verdict} | 样本含待修${m.pending}${m.unknown ? `、未知${m.unknown}` : ''} |`);
176
180
  }
177
181
 
178
182
  if (!argMonth && rows.length) {
@@ -189,4 +193,9 @@ function main() {
189
193
  }
190
194
  }
191
195
 
192
- main();
196
+ // isMain 守卫 + 纯函数导出(board-kb-p1:gateMonth / expansionVerdict 供测试 import,import 不执行 main)
197
+ if (require.main === module) {
198
+ main();
199
+ } else {
200
+ module.exports = { parseResult, metrics, gateMonth, expansionVerdict, GATE };
201
+ }
@@ -87,6 +87,38 @@ const runAgg = (root) => spawnSync(process.execPath, [path.join(root, '.agents',
87
87
  fs.rmSync(root, { recursive: true, force: true });
88
88
  }
89
89
 
90
+ // ---- 场景 4-5:扩容门口径(2026-09-27 board-kb-p1 对齐 delegations.md:返工 0 / 无主兜底 / 连续两月)----
91
+ // 纯函数经 module.exports require(isMain 守卫后 import 不执行 main)
92
+ import { createRequire } from 'node:module';
93
+ const requireCjs = createRequire(import.meta.url);
94
+ const { metrics, gateMonth, expansionVerdict } = requireCjs(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
95
+
96
+ // 场景 4:审查反例——30 任务、2 主兜底、平均返工 0.25(旧口径四门全过,声明口径下必须 ❌)
97
+ {
98
+ const rows = [
99
+ ...Array.from({ length: 22 }, (_, i) => ({ scope: '委派', result: '一次通过' })),
100
+ ...Array.from({ length: 6 }, (_, i) => ({ scope: '委派', result: '返工×1' })),
101
+ { scope: '委派', result: '主兜底' }, { scope: '委派', result: '主兜底' },
102
+ ];
103
+ const m = metrics(rows);
104
+ const g = gateMonth(m);
105
+ check('场景 4:反例(2 主兜底 + 平均返工 0.25)门3 返工=0 ❌', g.items.find((i) => i.no === 3).ok === false, JSON.stringify(g.items));
106
+ check('场景 4:反例门4 主兜底=0 ❌ 且 monthOk=false(声明口径下不达标)',
107
+ g.items.find((i) => i.no === 4).ok === false && g.monthOk === false, `monthOk=${g.monthOk}`);
108
+ const good = metrics([{ scope: '自做', result: '一次通过' }, { scope: '委派', result: '一次通过' }]);
109
+ const g2 = gateMonth(good);
110
+ check('场景 4:返工 0 + 无主兜底月 → 口径项全 ✅(样本护栏项单独列出)',
111
+ g2.items.filter((i) => i.kind === '口径').every((i) => i.ok === true) && g2.items.find((i) => i.no === 1).ok === false,
112
+ JSON.stringify(g2.items));
113
+ }
114
+ // 场景 5:连续两月判定四态
115
+ {
116
+ check('场景 5:本月+上月均达标 → ✅ 可扩容', expansionVerdict(true, true, false, []).includes('✅'));
117
+ check('场景 5:本月达标、上月无数据 → ⚠️ 1/2', expansionVerdict(true, undefined, true, []).includes('⚠️'));
118
+ check('场景 5:本月达标、上月未达标 → ⚠️ 1/2', expansionVerdict(true, false, false, []).includes('⚠️'));
119
+ check('场景 5:本月未达标 → ❌ 且列出未达标项号', expansionVerdict(false, true, false, [{ no: 3, ok: false }, { no: 4, ok: false }]).match(/❌.*3\+4/));
120
+ }
121
+
90
122
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
91
123
  if (fail) {
92
124
  console.log(`\n${USAGE}`);
@@ -14,13 +14,18 @@
14
14
  //
15
15
  // 检查项清单(编号/标题/severity 逐条沿 sh 版头部——gate-checklist 配对登记表按 id 消费,不得增删改号):
16
16
  // 1. 入口文档/spec/plan 同名配对(L1 必须有 plan;L2/L3 必须有 spec+plan) [hard-block]
17
- // 2. 模板字段占位符残留(YYYY-MM-DD / <主题> 等未替换) [warning]
17
+ // 2. 模板字段占位符残留(YYYY-MM-DD / <主题> 等未替换;<主题> 与 .md 同行 = 命名约定描述,豁免) [warning]
18
18
  // 3. incidents 复盘三件套完整性 + 状态严格枚举 + 新 intent 回路(回路断档=hard,其他=warning)
19
- // 4. 引用有效性(文档/指令中引用的 .agents/ 路径必须存在) [warning]
19
+ // 4. 引用有效性(文档/指令中引用的 .agents/ 路径必须存在;支持 fill-{a,b,c}.mjs 花括号展开与 fill-*.mjs 通配;
20
+ // workflow 文档仅扫活跃态——终态件的引用是历史叙述,不扫,2026-09-27 audit-gate-hardening) [warning]
20
21
  // 5. intent/spec/plan 状态字段 + L3 独立复核 [warning]
21
22
  // 6. 子智能体角色契约 + OpenCode/Trae/ZCode Adapter 一致性(含旧委派残留/钉死模型) [warning]
22
23
  // 7. 阶段索引同步(AGENTS.md 与 new-task.md 须双向索引全部阶段指令) [warning]
23
- // 8. intent 验收标准对账(2026-09-12 起新建:done 未勾验/缺节=hard,勾选缺证据=warning;存量聚合 warning,含「存量对账豁免」声明者出账)
24
+ // 8. intent 验收标准对账(新建:done 未勾验/缺节=hard,勾选缺证据=warning;存量聚合 warning,含「存量对账豁免」声明者出账)
25
+ // **生效日锚=git 首次加入日期(2026-09-28 改;此前取文件名前 10 字符——命名规范强制的字段、
26
+ // 写早零成本,一条 hard 门可被平凡绕过;incident 2026-09-28-check8-git-anchor 实证)**:
27
+ // 非 git / 查不到加入记录 → 不可判定 → 走存量口径(不误报 hard)
28
+ // 证据可写在 [x] 行的续行(仓库通写法「(证据:…)」另起一行;全/半角冒号皆认)
24
29
  // 9. 文件名英文 kebab-case(非 ASCII 文件名=warning,2026-09-11 规则)
25
30
  // 10. 级别 vs 迁移文件一致性(L1/L2 入口文档加入提交触及迁移 SQL/Migrations=疑似判低,warning)
26
31
  // 11. workflow/INDEX.md 漂移(活跃层索引与磁盘不一致=warning,2026-09-21 检索层;调生成器 --check,口径单一)
@@ -28,6 +33,36 @@
28
33
  // 13. 常驻面体积预算(超限=warning;判定单源 rule-budget.sh——经 sh 调用,
29
34
  // 无 sh 环境静默跳过:advisory 级且 pre-commit 侧在 git 钩子 sh 环境照常硬拦)
30
35
  // 14. 新 done 的 spec/plan 须在 git 历史里出现过 `状态: approved`(确认环节留痕,2026-09-22;恒 advisory 永不升级 hard)
36
+ // 15. 确认指纹对账(2026-09-27 起:approved/done 须 confirm-doc.mjs 确认指纹+台账配对,缺=hard-block;
37
+ // 两形态——TTY 亲手 / --delegated 对话委托代录,台账 source 如实区分,配对判据与 source 无关)
38
+ // **受管准入 = 两条件取或(2026-09-28 改锚;此前仅「自报日期 ≥ 生效日」单条件——
39
+ // 新档只要把日期写早即整段跳过判定,incident 2026-09-28-confirm-gate-effective-date-anchor 实证)**:
40
+ // ① 该 doc 在台账中有合法跳转行(stage∈approved/done/fixed/closed/superseded/cancelled,append-only
41
+ // 取末次)——「确认确实发生过」的机器事实,作者不可手填;
42
+ // ② 自报日期(日期/发现/文件名兜底)≥ 生效日——覆盖「新档完全没跑 confirm-doc」的漏网面
43
+ // (此类文档无台账行,仅靠 ① 会误判为存量豁免)。
44
+ // 两条件皆不满足 = 存量豁免(生效日前既有、从未走过确认门,不追溯)。
45
+ // 改锚收益:把「确认过却把日期写早以逃掉对账」这条攻破路径堵死(条件 ① 独立于自报日期成立)。
46
+ // **注意条件②的日期阈值仍分档、且不等价**:docs 2026-09-27 / incidents 2026-09-28——本仓现役
47
+ // incident 的「发现」均 ≤2026-09-27,故该档差异当前不显;新 incident 若把「发现」写成 09-27 且
48
+ // 无台账行,会因 incidents 阈值(09-28)被豁免,而同日期文档在 docs 档则受管。
49
+ // (复核 P2-1 更正:此前本注释误称「incidents 常量退役 / 两档等价」,与代码 :eff 实际保留分档不符;
50
+ // 台账锚两档共用为真,条件②阈值分档亦为真,二者不是一回事——README 遣词为准确口径)
51
+ // + incidents 覆盖(2026-09-27 gate-coverage:fixed/closed 须配对;open=起草态不加门;
52
+ // 不支持 open→closed 单跳——confirm-doc 状态机强制经 fixed)
53
+ // + 放弃态覆盖(2026-09-27 closing-coverage:superseded/cancelled 入配对集,confirm-doc --to 唯一通道)
54
+ // + 四终态内容绑定(2026-09-27 audit-gate-hardening;生效锚=台账行 ts≥2026-09-28——同日 p2-batch
55
+ // 自文档自报日期改锚:旧日期文档晚关单也绑定,3 份失配存量 ts 均 09-27 天然豁免):
56
+ // 按 prev 复原跳转前文本重算 sha256 与台账全量比对,不符=hard「确认内容漂移」;状态行保分隔符换值
57
+ // (非规范格式不误伤);台账行缺 prev 降级 warning;关单编辑顺序新约定:勾验/回填先于关单确认,
58
+ // confirm-doc 是最后一次写入)
59
+ // + 并录批次审计子检查(2026-09-28 batch-ledger-audit 改判据;**读调用事实,不猜时间戳模式**):
60
+ // 判据 = 台账行按 batch 分组、组内 of>1(delegated 行) → warning「确认并录」;batch/seq/of 由
61
+ // confirm-doc 写入时记录(那才是"本次调用落几份"确定已知的时刻)。旧判据「同 quote + 相邻 ts<2s」
62
+ // 整段退役——已实证四类失效:假阳性(合规逐件复用同句必然误报)/假阴性(换 quote 即零告警,
63
+ // 等于引导伪装)/噪声不可消退(不按日期门豁免,退役前本仓 22 条 advisory 中 14 条为该告警)/
64
+ // quote 字段职责冲突(对质凭据 vs 并录指纹,现归还单一职责)。无 batch 的历史行静默跳过
65
+ // (无判定依据的行不产出不可消除噪声——沿 audit-gate-hardening P3 教训)
31
66
  //
32
67
  // 注:清单条目 5(状态字段+L3 复核)与 1(配对)在同一遍 intents/specs/plans 循环里实现(沿 sh 版代码结构);
33
68
  // 条目 6 的旧委派残留/钉死模型子项在「角色契约与 Adapter」代码段实现。
@@ -44,6 +79,7 @@
44
79
  import fs from 'node:fs';
45
80
  import path from 'node:path';
46
81
  import { spawnSync } from 'node:child_process';
82
+ import { createHash } from 'node:crypto';
47
83
  import { fileURLToPath } from 'node:url';
48
84
  import { loadEnums } from './workflow-enums.mjs';
49
85
 
@@ -190,12 +226,16 @@ for (const plan of docFiles('plans')) {
190
226
  {
191
227
  const phRe = /YYYY-MM-DD|<主题>|<日期 主题>|L0 \/ L1 \/ L2 \/ L3|draft \/ approved \/ done|open \/ fixed \/ closed/;
192
228
  const boilerRe = /\.\.\/specs\/[A-Za-z0-9-]*\.md|写明如何满足|防复发验证|_YYYY-MM-DD\.|format\('YYYY-MM-DD'\)|value-format="YYYY-MM-DD"/;
229
+ // 命名约定豁免:`<主题>` 与 `.md` 同行 = 在描述文件命名规则(如 `.agents/workflows/<主题>.md`、
230
+ // 「复制本模板为 YYYY-MM-DD-<主题>.md」),非未填占位符;真未填的占位(标题 `# INTENT — <主题>`、
231
+ // `日期: YYYY-MM-DD`)不含 .md,仍照拦(2026-09-25-wf-runtime incident 记录的误报口径)
232
+ const isNamingConv = (line) => line.includes('<主题>') && /\.md/.test(line);
193
233
  const groups = new Map(); // file -> [ "行号:内容" ](首现序)
194
234
  for (const sub of DOC_DIRS) {
195
235
  for (const f of docFiles(sub)) {
196
236
  const hits = [];
197
237
  (linesOf(f) || []).forEach((line, i) => {
198
- if (phRe.test(line) && !boilerRe.test(line)) hits.push(`${i + 1}:${line}`);
238
+ if (phRe.test(line) && !boilerRe.test(line) && !isNamingConv(line)) hits.push(`${i + 1}:${line}`);
199
239
  });
200
240
  if (hits.length) groups.set(f, hits);
201
241
  }
@@ -251,7 +291,23 @@ for (const inc of docFiles('incidents')) {
251
291
 
252
292
  // --- 4. 引用有效性 [warning](.agents/ 路径须存在;skills 特例允许用户级 ~/.agents/skills)---
253
293
  {
254
- const refRe = /\.agents\/(commands|hooks|scripts|skills|roles)\/[A-Za-z0-9_][A-Za-z0-9_./-]*/g;
294
+ // 引用可带 {a,b,c} 花括号展开(仓库通写法 `fill-{intent,spec,plan}.mjs`)或 `*` 通配(`fill-*.mjs`)——
295
+ // 花括号展开逐路查存在,任一路存在即有效;`*` 按目录枚举 glob 匹配,命中即有效;全不中才算断档
296
+ // (2026-09-26 审查修复:正则字符类不含 {} *,曾把 `fill-{intent,spec,plan}.mjs` / `fill-*.mjs`
297
+ // 截断成 `fill-` 误报引用断档)
298
+ const refRe = /\.agents\/(commands|hooks|scripts|skills|roles)\/[A-Za-z0-9_][A-Za-z0-9_./{},,*-]*/g;
299
+ const expandRef = (ref) => {
300
+ const m = ref.match(/^([^{]*)\{([^}]*)\}(.*)$/);
301
+ if (!m) return [ref];
302
+ return m[2].split(/[,,]/).map((v) => m[1] + v.trim() + m[3]);
303
+ };
304
+ // globExists:`*` 通配引用按目录枚举匹配(仅文件名层,`*` 不跨 /;目录不存在 = false)
305
+ const globExists = (ref) => {
306
+ const re = new RegExp('^' + path.basename(ref).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/\\\\]*') + '$');
307
+ try { return fs.readdirSync(path.dirname(path.join(ROOT, ref))).some((f) => re.test(f)); } catch { return false; }
308
+ };
309
+ const refExists = (r) => (r.includes('*') ? globExists(r) : fs.existsSync(path.join(ROOT, r)));
310
+ const HOME_DIR = process.env.HOME || process.env.USERPROFILE || '';
255
311
  const files = [];
256
312
  if (fs.existsSync(path.join(ROOT, 'AGENTS.md'))) files.push('AGENTS.md');
257
313
  for (const e of readdirOrNull(ROOT) || []) {
@@ -263,8 +319,13 @@ for (const inc of docFiles('incidents')) {
263
319
  }
264
320
  for (const sub of DOC_DIRS) {
265
321
  const dir = path.join(ROOT, WF, sub);
322
+ // 仅扫活跃态(draft/approved/open)——终态件的引用是历史叙述(提及已改名/已废文件属常态),
323
+ // 扫了只会产生不可消除的 advisory 噪声,真漏点被淹没(2026-09-27 audit-gate-hardening)
324
+ const activeSet = sub === 'incidents' ? ENUMS['incident.status.active'] : ENUMS['doc.status.active'];
266
325
  for (const f of readdirOrNull(dir) || []) {
267
- if (f.endsWith('.md') && (f === '_TEMPLATE.md' || /^\d/.test(f))) files.push(`${WF}/${sub}/${f}`);
326
+ if (!f.endsWith('.md') || !(f === '_TEMPLATE.md' || /^\d/.test(f))) continue;
327
+ if (f !== '_TEMPLATE.md' && !inSet(fmGet(path.join(dir, f), '状态'), activeSet)) continue;
328
+ files.push(`${WF}/${sub}/${f}`);
268
329
  }
269
330
  }
270
331
  const cmdDir = path.join(ROOT, '.agents', 'commands');
@@ -280,8 +341,9 @@ for (const inc of docFiles('incidents')) {
280
341
  const ref = m[0];
281
342
  if (seen.has(ref)) continue;
282
343
  seen.add(ref);
283
- if (fs.existsSync(path.join(ROOT, ref))) continue;
284
- if (ref.startsWith('.agents/skills/') && fs.existsSync(path.join(process.env.HOME || process.env.USERPROFILE || '', ref))) continue;
344
+ const refs = expandRef(ref);
345
+ if (refs.some(refExists)) continue;
346
+ if (refs.some((r) => r.startsWith('.agents/skills/') && !r.includes('*') && fs.existsSync(path.join(HOME_DIR, r)))) continue;
285
347
  warnings.push(`- [WARN 引用断档] ${rel.split(path.sep).join('/')} 引用不存在的文件:${ref}`);
286
348
  }
287
349
  }
@@ -351,7 +413,41 @@ for (const cmd of ['plan', 'design', 'build', 'test', 'deploy', 'maintain', 'rev
351
413
  }
352
414
  }
353
415
 
354
- // --- 8. intent 验收标准对账(done 须逐条勾验并补证据;2026-09-12 起新建 hard,存量聚合 warning)---
416
+ // --- 共享:文件「首次加入 git」的日期索引(2026-09-28 check8-git-anchor)---
417
+ // 用途:检查 8 的生效日锚从**可手填的文件名前缀**(项目命名规范本身即 `YYYY-MM-DD-<主题>`,作者
418
+ // 日常手写该前缀,「写早」零成本、顺手即发生——一条 hard 门可被平凡绕过)改为**首次加入 git 的日期**。
419
+ // **诚实边界(复核 P1 更正)**:该锚取 `git log --diff-filter=A --format=@%aI`,即 **author date
420
+ // (作者自报时间戳)**,**不是** commit 时刻——`git commit --date=<过去>` 一条参数即可伪造,故
421
+ // 「锚不可手填」的说法**不成立**。本改动的确切收益是:关掉「改文件名(日常必写、零成本)」这条通道,
422
+ // 把伪造成本抬到「主动加参数伪造时间戳」——后者与「伪造台账」同属**本地信任边界内不可机器防**的范畴
423
+ // (事后对质靠 git 历史与台账)。此处如实声明,不宣称「通道已关闭」。
424
+ // 口径:git 按时间新→旧遍历,同一文件首遇即最早那次加入(与检查 10 的 addpath 同款判据)。
425
+ // 非 git / 命令失败 → null,调用方按既有「非 git 跳过」语义退化(不 fail-loud)。
426
+ // 注:检查 10 需要的是「文件→加入 commit」+「commit→文件清单」两份映射(判断同提交是否触及迁移 SQL),
427
+ // 与本索引「文件→日期」不同构,故**不合并**——避免把两处判据耦死。
428
+ const addedDates = (() => {
429
+ if (gitOut(['rev-parse', '--git-dir']) === null) return null;
430
+ const log = gitOut(['log', '--diff-filter=A', '--format=@%aI', '--name-only']);
431
+ if (log === null) return null;
432
+ const m = new Map();
433
+ let cur = null;
434
+ for (const line of log.split('\n')) {
435
+ if (line.startsWith('@')) { cur = line.slice(1); continue; }
436
+ if (line.trim() && cur && !m.has(line.trim())) m.set(line.trim(), cur); // 新→旧序首遇 = 最早
437
+ }
438
+ return m;
439
+ })();
440
+ // 某文档「首次加入日期」(YYYY-MM-DD);不可判定(非 git / 查不到)返回 ''
441
+ const addedDateOf = (absDoc) => {
442
+ if (!addedDates) return '';
443
+ const rel = path.relative(ROOT, absDoc).split(path.sep).join('/');
444
+ const iso = addedDates.get(rel);
445
+ return iso ? iso.slice(0, 10) : '';
446
+ };
447
+
448
+ // --- 8. intent 验收标准对账(done 须逐条勾验并补证据;新建 hard,存量聚合 warning)---
449
+ // 生效日锚 2026-09-28 改「git 首次加入日期」(此前取文件名前 10 字符——命名规范强制的字段、
450
+ // 写早零成本,见 incidents/2026-09-28-check8-git-anchor)。非 git → 不可判定 → 走存量口径(不误报 hard)。
355
451
  {
356
452
  const accCutoff = '2026-09-12';
357
453
  let legacyUnaccounted = 0;
@@ -360,17 +456,29 @@ for (const cmd of ['plan', 'design', 'build', 'test', 'deploy', 'maintain', 'rev
360
456
  const base = path.basename(intent);
361
457
  const lines = linesOf(intent) || [];
362
458
  // 节扫描:hs=有验收节;insec 至下一个二级标题;uc=未勾项;ne=勾选缺证据;ex=存量豁免声明
459
+ // 证据可写在 [x] 行的续行(仓库通写法「(证据:…)」另起一行)——pendX 记「待证据项」,
460
+ // 遇证据行(全角「:」/半角「:」皆认)清零;遇下一条目 / 下一小节 / 节末仍无证据 → ne
363
461
  let hs = false, uc = false, ne = false;
364
462
  let insec = false, ex = false;
463
+ let pendX = false; // 上一 [x] 项尚无证据,证据可能在紧随的续行
464
+ const evRe = /证据[::]/;
465
+ const closeItem = () => { if (pendX) { ne = true; pendX = false; } };
365
466
  for (const line of lines) {
366
467
  if (/存量对账豁免(/.test(line)) ex = true;
367
- if (/^\s*##\s+[^#]*验收标准/.test(line)) { hs = true; insec = true; continue; }
368
- if (insec && /^\s*##\s/.test(line)) insec = false;
369
- if (insec && /^\s*- \[ \]/.test(line)) uc = true;
370
- if (insec && /^\s*- \[x\]/.test(line) && !/证据:/.test(line)) ne = true;
468
+ if (/^\s*##\s+[^#]*验收标准/.test(line)) { hs = true; insec = true; closeItem(); continue; }
469
+ if (insec && /^\s*##\s/.test(line)) { insec = false; closeItem(); continue; }
470
+ if (!insec) continue;
471
+ if (/^\s*- \[ \]/.test(line)) { closeItem(); uc = true; continue; }
472
+ if (/^\s*- \[x\]/.test(line)) { closeItem(); pendX = !evRe.test(line); continue; }
473
+ if (pendX && evRe.test(line)) pendX = false; // 续行补上证据
371
474
  }
372
- const filedate = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
373
- const isNew = filedate !== '' && filedate >= accCutoff;
475
+ closeItem(); // 节末(或全文末)仍无证据 → 计缺证据
476
+ // 生效日锚 = git 首次加入日期(2026-09-28 改;此前取文件名前 10 字符)。**准确收益**:关掉
477
+ // 「改文件名日期(日常必写、零成本)即整段跳过 hard 门」这条通道;锚取 author date(%aI),
478
+ // 刻意伪造(`git commit --date=`)仍可绕过——与伪造台账同属本地信任边界内,不宣称"通道已关闭"。
479
+ // 非 git / 查不到 → '' → isNew=false → 走存量口径(与既有语义一致:不可判定时不误报 hard)。
480
+ const adddate = addedDateOf(intent);
481
+ const isNew = adddate !== '' && adddate >= accCutoff;
374
482
  if (!hs) {
375
483
  if (isNew) blockers.push(`- [验收未对账] done intent 缺「## 验收标准」节(勾验无从核对):${base}`);
376
484
  else if (!ex) legacyUnaccounted++;
@@ -385,7 +493,7 @@ for (const cmd of ['plan', 'design', 'build', 'test', 'deploy', 'maintain', 'rev
385
493
  }
386
494
  }
387
495
  if (legacyUnaccounted > 0) {
388
- warnings.push(`- [WARN 验收对账存量] ${legacyUnaccounted} 个存量 done intent 验收标准未对账(2026-09-12 前创建,豁免 hard,不回填)`);
496
+ warnings.push(`- [WARN 验收对账存量] ${legacyUnaccounted} 个存量 done intent 验收标准未对账(生效日锚之前加入仓库,豁免 hard,不回填)`);
389
497
  }
390
498
  }
391
499
 
@@ -498,6 +606,121 @@ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '-
498
606
  }
499
607
  }
500
608
 
609
+ // --- 15. 确认指纹对账 [hard-block](2026-09-26 confirm-gate-machine;2026-09-27 confirm-gate-delegated 两形态;
610
+ // 生效日锚 2026-09-28 改台账 ts——此前锚取自文档自报「日期/发现」,新档只要把日期写早即整段跳过判定,
611
+ // incident 2026-09-28-confirm-gate-effective-date-anchor 实证)---
612
+ // 确认两形态(confirm-doc.mjs):TTY 亲手键入「可以」/ --delegated 对话委托代录(用户对话内明确放行后
613
+ // AI 代录,台账行如实记 source=chat-delegated + quote 原话,永不伪装 TTY)。intents/specs/plans 凡
614
+ // approved/done 须有 confirm-doc 产生的 frontmatter 确认指纹 + 台账(.agents/confirmations.jsonl)
615
+ // 配对行——配对判据 doc/stage/fingerprint 三键,与 source 无关。
616
+ // **生效日锚 = 台账 ts(2026-09-28 起)**:受管准入 = 该 doc 在台账中有合法跳转行(stage ∈ approved/done/
617
+ // fixed/closed/superseded/cancelled,append-only 取末次);无台账行 = 存量豁免(不追溯)。自报日期
618
+ // (日期/发现/文件名)不再参与门禁判定——它可被作者手填,把「门是否生效」的决定权交给被约束对象本身
619
+ // 是自举漏洞;台账行是「确认确实发生过」的唯一机器事实。docs 与 incidents 共用同一锚(原 incidents
620
+ // 独立生效日 2026-09-28 常量退役:台账起算日 = docs 生效日,两档等价)。
621
+ // 台账坏行容忍跳过(审计件,jsonl 追加式);frontmatter 存 16 位、台账存 64 位,按前 16 位配对。
622
+ {
623
+ const EFFECTIVE = '2026-09-27';
624
+ const ledgerPath = path.join(ROOT, '.agents', 'confirmations.jsonl');
625
+ const ledger = [];
626
+ if (fs.existsSync(ledgerPath)) {
627
+ for (const line of linesOf(ledgerPath) || []) {
628
+ if (!line.trim()) continue;
629
+ try { ledger.push(JSON.parse(line)); } catch { /* 坏行跳过 */ }
630
+ }
631
+ }
632
+ // incidents 侧覆盖(2026-09-27 gate-coverage):fixed/closed 为已确认态(open = 起草态不加门——
633
+ // maintain.md 创建即对话确认的既有口径)
634
+ // 放弃态覆盖(2026-09-27 closing-coverage):superseded/cancelled 入配对集——confirm-doc --to 是唯一
635
+ // 合法产生通道(cancelled 自 draft/approved/open/fixed;superseded 自 approved/done/fixed/closed),
636
+ // 此前手改 frontmatter 即出账的口子收死
637
+ const CONFIRMED_BY_SUB = {
638
+ intents: ['approved', 'done', 'superseded', 'cancelled'],
639
+ specs: ['approved', 'done', 'superseded', 'cancelled'],
640
+ plans: ['approved', 'done', 'superseded', 'cancelled'],
641
+ incidents: ['fixed', 'closed', 'superseded', 'cancelled'],
642
+ };
643
+ // 合法跳转 stage 集(台账行「确认确实发生过」的判据;revert-draft / revert-open 类回退注记行不算)
644
+ const VALID_STAGES = new Set(['approved', 'done', 'fixed', 'closed', 'superseded', 'cancelled']);
645
+ for (const [sub, confirmedSet] of Object.entries(CONFIRMED_BY_SUB)) {
646
+ for (const doc of docFiles(sub)) {
647
+ const st = fmGet(doc, '状态');
648
+ if (!confirmedSet.includes(st)) continue;
649
+ const base = path.basename(doc);
650
+ const rel = path.relative(ROOT, doc).split(path.sep).join('/');
651
+ // 受管准入 = 两条件取或(2026-09-28 改锚):
652
+ // ① 台账中有该 doc 的合法跳转行——「确认确实发生过」的机器事实;它独立于自报日期成立,
653
+ // 故堵死「确认过却把日期写早以逃掉对账」这条攻破路径(本 incident 的核心缺陷);
654
+ // ② 自报日期 ≥ 生效日——覆盖「新档完全没跑 confirm-doc」的漏网面(此类无台账行,
655
+ // 仅靠 ① 会被误判为存量豁免而静默放行)。
656
+ // 两条件皆不满足 = 存量豁免(生效日前既有、从未走确认门,不追溯)。
657
+ const docEntries = ledger.filter((e) => e && e.doc === rel && VALID_STAGES.has(e.stage));
658
+ const eff = sub === 'incidents' ? '2026-09-28' : EFFECTIVE;
659
+ let d = fmGet(doc, '日期') || fmGet(doc, '发现');
660
+ if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
661
+ const dateManaged = /^\d{4}-\d{2}-\d{2}$/.test(d) && d >= eff;
662
+ if (!docEntries.length && !dateManaged) continue; // 存量豁免
663
+ const fp = fmGet(doc, '确认指纹');
664
+ const ok = !!fp && docEntries.some((e) => e.stage === st
665
+ && typeof e.fingerprint === 'string' && e.fingerprint.startsWith(fp));
666
+ if (!ok) {
667
+ blockers.push(`- [确认未对账] ${base} 状态 ${st} 无用户确认记录——AI 不得代确认,用户在终端跑 node .agents/scripts/confirm-doc.mjs ${rel} 后重试`);
668
+ }
669
+ // 内容绑定(2026-09-27 audit-gate-hardening;生效锚 2026-09-27 gate-hardening-p2-batch 自文档自报日期
670
+ // 改为台账 done 行 ts):done 文档当前内容须与台账 done 行确认时的内容一致——按 prev 复原跳转前文本
671
+ // 重算比对,防「确认后篡改」(改验收标准/正文均触发)。复原口径 = confirm-doc computeFingerprint 的
672
+ // 逆推:CRLF 归一 → frontmatter 首个「状态:」行保分隔符换值为台账 prev(双空格等非规范分隔符不误伤,
673
+ // 与前向按原行字面计算对称)→ 剔「确认指纹:」行。两条降级路径:台账行缺 prev(schema 演进前存量,
674
+ // 无以复原)→ warning 不拦;entry.ts 早于 2026-09-28(UTC 字符串比较——该次确认在旧关单顺序时代完成)→
675
+ // 豁免绑定(配对判定已过,3 份失配存量即此列)。关单编辑顺序新约定不变:confirm-doc 是最后一次写入,
676
+ // 此后修订走 superseded 或新 intent。
677
+ if (ok && ['done', 'closed', 'superseded', 'cancelled'].includes(st)) {
678
+ const doneEntries = ledger.filter((e) => e && e.doc === rel && e.stage === st && typeof e.fingerprint === 'string');
679
+ const entry = doneEntries[doneEntries.length - 1]; // append-only 台账,末次生效(重确认场景)
680
+ if (!entry || !entry.prev) {
681
+ warnings.push(`- [WARN 绑定降级] ${base} 台账 stage=${st} 行缺 prev 字段(schema 演进前行),内容绑定跳过——仅配对判定`);
682
+ } else if (!(typeof entry.ts === 'string' && entry.ts >= '2026-09-28')) {
683
+ // 该 done 确认发生在生效锚前(旧关单顺序时代)——豁免内容绑定,配对判定照常
684
+ } else if (bindingSha256(linesOf(doc) || [], entry.prev) !== entry.fingerprint) {
685
+ blockers.push(`- [确认内容漂移] ${base} ${st} 后内容与确认台账不符——已关单文档不得直接改(关单编辑先于关单确认);确需修订走 superseded 或新 intent 引用`);
686
+ }
687
+ }
688
+ }
689
+ }
690
+ // 并录批次审计(2026-09-28 batch-ledger-audit 改判据;原 2026-09-27 confirm-gate-one-per-call 引入):
691
+ // **读调用事实,不猜时间戳模式**。判据 = 台账行按 `batch` 分组,组内 `of > 1` → warning「确认并录」。
692
+ // batch/seq/of 由 confirm-doc 在**写入时**记录(那才是「本次调用落了几份」确定已知的时刻);
693
+ // 旧判据「同 quote + 相邻 ts 差 < 2s」是拿两个间接信号反推该事实,已实证四类失效并**整段退役**:
694
+ // ① 假阳性:合规逐件调用复用同句(用户两次都说「可以」)→ 必然误报;
695
+ // ② 假阴性:并录时给每份换不同 quote → 零告警(判据的实际效果是引导伪装者改 quote);
696
+ // ③ 噪声不可消退:不按日期门豁免,退役前本仓 22 条 advisory 中 14 条为该告警,且文案自认「不可区分」;
697
+ // ④ 字段职责冲突:`quote` 同时当「对质凭据」与「并录指纹」——现归还单一职责,只记用户原话。
698
+ // 无 `batch` 字段的历史行(schema 演进前,本仓全部 67 行均无该字段)→ **静默跳过**,不降级出账、不回溯:
699
+ // 按 audit-gate-hardening P3 教训(「扫了只产生不可消除的 advisory 噪声,真漏点被淹没」),无判定依据
700
+ // 的行不该产出告警。VALID_STAGES 复用外层声明(复核 P2-3 更正:此前本块内另有一份同名声明遮蔽外层)。
701
+ {
702
+ const byBatch = new Map(); // batch → 该批的台账行(仅计合法跳转 stage)
703
+ for (const e of ledger) {
704
+ if (!e || !VALID_STAGES.has(e.stage)) continue;
705
+ if (typeof e.batch !== 'string' || !e.batch) continue; // 历史行无 batch → 无判定依据,跳过
706
+ if (!byBatch.has(e.batch)) byBatch.set(e.batch, []);
707
+ byBatch.get(e.batch).push(e);
708
+ }
709
+ for (const [b, rows] of byBatch) {
710
+ // 判据取组内**任一行** of>1(复核 P2-3 更正:原取 rows[0].of 使结论依赖行序——同批首行 of=1、
711
+ // 次行 of=2 会漏报。confirm-doc 对同批所有行写同一个 of=docs.length,故正常批次组内一致;
712
+ // 改用 some 消除该顺序脆弱性,并对 craft 出来的不一致批次按「最大值」报,方向偏严不偏松)
713
+ if (!rows.some((r) => typeof r.of === 'number' && r.of > 1)) continue;
714
+ // of > 1 = 一次调用落多份态。delegated 形态下这正是「并录」(入口已拒多份,此处抓历史/绕行);
715
+ // TTY 形态天然逐份过目(用户亲手键入),of>1 不构成违规——故只对 delegated 行报。
716
+ if (!rows.some((r) => r.source === 'chat-delegated')) continue;
717
+ const of = Math.max(...rows.map((r) => (typeof r.of === 'number' ? r.of : 0)));
718
+ const docs = rows.map((r) => String(r.doc).replace(/^workflow\//, '')).join('、');
719
+ 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`);
720
+ }
721
+ }
722
+ }
723
+
501
724
  // --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
502
725
  if (blockers.length) {
503
726
  process.stderr.write(`闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n\n${blockers.join('\n')}\n\n`);
@@ -509,6 +732,29 @@ if (warnings.length) {
509
732
  }
510
733
  process.exit(0);
511
734
 
735
+ // bindingSha256:done 内容绑定的复原重算(检查 15 专用)——confirm-doc.mjs computeFingerprint 的逆推:
736
+ // lines 已 CRLF 归一(split(/\r?\n/)),frontmatter 区内首个「状态:」行保分隔符换值为 prev(`状态: done`
737
+ // 双空格 → `状态: approved`——与前向按原行字面计算对称,非规范分隔符不误伤;行尾空白等更奇异格式仍会
738
+ // 失配,触发前提本身已违反「确认落态唯一入口」约定,接受),全文剔「确认指纹:」行后 join('\n') 再 sha256。
739
+ function bindingSha256(lines, prevStatus) {
740
+ const isDelim = (l) => /^---\s*$/.test(l);
741
+ const out = [];
742
+ let i = 0;
743
+ if (isDelim(lines[0] || '')) {
744
+ out.push(lines[0]);
745
+ let replaced = false;
746
+ for (i = 1; i < lines.length; i++) {
747
+ if (isDelim(lines[i])) { out.push(lines[i]); i++; break; }
748
+ if (!replaced && /^状态:/.test(lines[i])) {
749
+ out.push(lines[i].replace(/^(\s*状态:\s*)\S.*$/, `$1${prevStatus}`));
750
+ replaced = true;
751
+ } else out.push(lines[i]);
752
+ }
753
+ }
754
+ for (; i < lines.length; i++) out.push(lines[i]);
755
+ return createHash('sha256').update(out.filter((l) => !/^确认指纹:/.test(l)).join('\n'), 'utf8').digest('hex');
756
+ }
757
+
512
758
  function readdirOrNull(dir) {
513
759
  try { return fs.readdirSync(dir); } catch { return null; }
514
760
  }