agentic-flow-kit 0.8.0 → 0.9.1

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 (141) hide show
  1. package/README.md +93 -69
  2. package/modules/gates/README.md +24 -0
  3. package/modules/gates/generated-readonly/README.md +13 -0
  4. package/modules/gates/generated-readonly/check-generated.mjs +61 -0
  5. package/modules/gates/generated-readonly/local-pre-commit.line +1 -0
  6. package/modules/gates/node-layer/README.md +9 -0
  7. package/modules/gates/node-layer/check-node-layer.mjs +29 -0
  8. package/modules/gates/node-layer/local-pre-commit.line +1 -0
  9. package/modules/gates/py-import/README.md +9 -0
  10. package/modules/gates/py-import/check-py-import.mjs +30 -0
  11. package/modules/gates/py-import/local-pre-commit.line +1 -0
  12. package/modules/hosts/claude/agents/implementer.md +52 -0
  13. package/modules/hosts/claude/agents/independent-reviewer.md +49 -0
  14. package/modules/hosts/claude/agents/ui-verifier.md +48 -0
  15. package/modules/hosts/claude/commands/wf-build.md +79 -0
  16. package/modules/hosts/claude/commands/wf-deploy.md +57 -0
  17. package/modules/hosts/claude/commands/wf-design.md +42 -0
  18. package/modules/hosts/claude/commands/wf-gate-checklist.md +77 -0
  19. package/modules/hosts/claude/commands/wf-maintain.md +76 -0
  20. package/modules/hosts/claude/commands/wf-new-task.md +65 -0
  21. package/modules/hosts/claude/commands/wf-plan.md +41 -0
  22. package/modules/hosts/claude/commands/wf-review.md +47 -0
  23. package/modules/hosts/claude/commands/wf-source-sync-check.md +89 -0
  24. package/modules/hosts/claude/commands/wf-sync-hosts.md +66 -0
  25. package/modules/hosts/claude/commands/wf-test.md +66 -0
  26. package/modules/hosts/codex/agents/implementer.md +52 -0
  27. package/modules/hosts/codex/agents/independent-reviewer.md +49 -0
  28. package/modules/hosts/codex/agents/ui-verifier.md +48 -0
  29. package/modules/hosts/codex/commands/wf-build.md +79 -0
  30. package/modules/hosts/codex/commands/wf-deploy.md +57 -0
  31. package/modules/hosts/codex/commands/wf-design.md +42 -0
  32. package/modules/hosts/codex/commands/wf-gate-checklist.md +77 -0
  33. package/modules/hosts/codex/commands/wf-maintain.md +76 -0
  34. package/modules/hosts/codex/commands/wf-new-task.md +65 -0
  35. package/modules/hosts/codex/commands/wf-plan.md +41 -0
  36. package/modules/hosts/codex/commands/wf-review.md +47 -0
  37. package/modules/hosts/codex/commands/wf-source-sync-check.md +89 -0
  38. package/modules/hosts/codex/commands/wf-sync-hosts.md +66 -0
  39. package/modules/hosts/codex/commands/wf-test.md +66 -0
  40. package/modules/hosts/codex/skills/flow-kit/SKILL.md +14 -0
  41. package/modules/hosts/cursor/agents/implementer.md +52 -0
  42. package/modules/hosts/cursor/agents/independent-reviewer.md +49 -0
  43. package/modules/hosts/cursor/agents/ui-verifier.md +48 -0
  44. package/modules/hosts/cursor/commands/wf-build.md +79 -0
  45. package/modules/hosts/cursor/commands/wf-deploy.md +57 -0
  46. package/modules/hosts/cursor/commands/wf-design.md +42 -0
  47. package/modules/hosts/cursor/commands/wf-gate-checklist.md +77 -0
  48. package/modules/hosts/cursor/commands/wf-maintain.md +76 -0
  49. package/modules/hosts/cursor/commands/wf-new-task.md +65 -0
  50. package/modules/hosts/cursor/commands/wf-plan.md +41 -0
  51. package/modules/hosts/cursor/commands/wf-review.md +47 -0
  52. package/modules/hosts/cursor/commands/wf-source-sync-check.md +89 -0
  53. package/modules/hosts/cursor/commands/wf-sync-hosts.md +66 -0
  54. package/modules/hosts/cursor/commands/wf-test.md +66 -0
  55. package/modules/hosts/omp/agents/implementer.md +1 -1
  56. package/modules/hosts/omp/agents/independent-reviewer.md +1 -1
  57. package/modules/hosts/omp/agents/ui-verifier.md +1 -1
  58. package/modules/hosts/opencode/agents/implementer.md +1 -1
  59. package/modules/hosts/opencode/agents/independent-reviewer.md +1 -1
  60. package/modules/hosts/opencode/agents/ui-verifier.md +1 -1
  61. package/modules/hosts/opencode/commands/wf-build.md +3 -4
  62. package/modules/hosts/opencode/commands/wf-design.md +2 -2
  63. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  64. package/modules/hosts/opencode/commands/wf-plan.md +3 -3
  65. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  66. package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -1
  67. package/modules/hosts/opencode/commands/wf-test.md +3 -1
  68. package/modules/hosts/trae/agents/implementer.md +1 -1
  69. package/modules/hosts/trae/agents/independent-reviewer.md +1 -1
  70. package/modules/hosts/trae/agents/ui-verifier.md +1 -1
  71. package/modules/hosts/trae/commands/wf-build.md +3 -4
  72. package/modules/hosts/trae/commands/wf-design.md +2 -2
  73. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  74. package/modules/hosts/trae/commands/wf-plan.md +3 -3
  75. package/modules/hosts/trae/commands/wf-review.md +1 -1
  76. package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -1
  77. package/modules/hosts/trae/commands/wf-test.md +3 -1
  78. package/modules/hosts/zcode/agents/implementer.md +1 -1
  79. package/modules/hosts/zcode/agents/independent-reviewer.md +1 -1
  80. package/modules/hosts/zcode/agents/ui-verifier.md +1 -1
  81. package/package.json +45 -45
  82. package/src/cli.mjs +16 -4
  83. package/src/doctor.mjs +49 -1
  84. package/src/fresh-init.test.mjs +36 -0
  85. package/src/gates.test.mjs +126 -0
  86. package/src/init.mjs +8 -1
  87. package/src/init.test.mjs +7 -1
  88. package/src/profiles.mjs +10 -4
  89. package/src/run-tests.mjs +3 -0
  90. package/src/stack-profile.test.mjs +89 -0
  91. package/src/sync-hosts.test.mjs +26 -6
  92. package/templates/AGENTS.md +4 -4
  93. package/templates/_agents/board/index.html +3 -3
  94. package/templates/_agents/commands/build.md +4 -5
  95. package/templates/_agents/commands/design.md +4 -4
  96. package/templates/_agents/commands/maintain.md +1 -1
  97. package/templates/_agents/commands/new-task.md +13 -2
  98. package/templates/_agents/commands/plan.md +4 -4
  99. package/templates/_agents/commands/review.md +1 -1
  100. package/templates/_agents/commands/source-sync-check.md +1 -1
  101. package/templates/_agents/commands/test.md +4 -2
  102. package/templates/_agents/hooks/commit-check.cjs +45 -24
  103. package/templates/_agents/lane-surfaces.txt +14 -0
  104. package/templates/_agents/roles/implementer.md +1 -1
  105. package/templates/_agents/roles/independent-reviewer.md +1 -1
  106. package/templates/_agents/roles/ui-verifier.md +1 -1
  107. package/templates/_agents/scripts/check-lane-surface.mjs +164 -0
  108. package/templates/_agents/scripts/check-lane-surface.test.mjs +200 -0
  109. package/templates/_agents/scripts/check-loop-rev.test.mjs +337 -0
  110. package/templates/_agents/scripts/check-loop.mjs +460 -268
  111. package/templates/_agents/scripts/check-loop.test.mjs +677 -17
  112. package/templates/_agents/scripts/check-metric-claims.mjs +275 -0
  113. package/templates/_agents/scripts/commit-check-trigger.test.mjs +64 -1
  114. package/templates/_agents/scripts/confirm-doc.mjs +159 -15
  115. package/templates/_agents/scripts/confirm-doc.test.mjs +362 -10
  116. package/templates/_agents/scripts/doctor.test.mjs +102 -1
  117. package/templates/_agents/scripts/fill-intent.mjs +8 -7
  118. package/templates/_agents/scripts/fill-intent.test.mjs +12 -3
  119. package/templates/_agents/scripts/fill-plan.mjs +27 -11
  120. package/templates/_agents/scripts/fill-plan.test.mjs +148 -8
  121. package/templates/_agents/scripts/fill-spec.mjs +18 -3
  122. package/templates/_agents/scripts/fill-spec.test.mjs +198 -1
  123. package/templates/_agents/scripts/gate-checklist.mjs +4 -1
  124. package/templates/_agents/scripts/gen-workflow-index.mjs +6 -2
  125. package/templates/_agents/scripts/gen-workflow-index.test.mjs +12 -0
  126. package/templates/_agents/scripts/policy.mjs +46 -0
  127. package/templates/_agents/scripts/pre-push.test.mjs +82 -0
  128. package/templates/_agents/scripts/solidify-task.mjs +107 -0
  129. package/templates/_agents/scripts/solidify-task.test.mjs +126 -0
  130. package/templates/_agents/scripts/stage-gates.mjs +256 -0
  131. package/templates/_agents/scripts/trust-mode.mjs +67 -0
  132. package/templates/_agents/scripts/wf-journal.mjs +71 -2
  133. package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
  134. package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
  135. package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
  136. package/templates/_agents/workflows/pipeline-closing.md +1 -1
  137. package/templates/_githooks/pre-commit +33 -2
  138. package/templates/_githooks/pre-push +58 -10
  139. package/templates/workflow/README.md +22 -3
  140. package/templates/workflow/incidents/_TEMPLATE.md +4 -0
  141. package/templates/workflow/intents/_TEMPLATE.md +1 -0
@@ -3,6 +3,10 @@
3
3
  // 判据:① 指纹算法(CRLF 归一 / 剔指纹行防自引用 / 内容敏感) ② 跳转唯一合法性 ③ 落态只动两行
4
4
  // ④ 台账追加 schema ⑤ 核心——非 TTY spawn(模拟 AI 调用路径)无 --delegated 必须被拒
5
5
  // ⑥ 委托代录(--delegated):免 TTY 落态 + 台账如实记 source/quote;空原话拒跑
6
+ // ⑦ 逐阶段前置门(2026-09-30 stage-gate-machine):specs/plans 的 draft→approved 须入口(plan 另须
7
+ // L2/L3 时同名 spec)已确认;未过 → exit 2 + 未落盘未记账(S19-S22)
8
+ // ⑧ done 前置门(2026-09-30 confirm-gate-approved-history):→done 须 git 历史已留 approved 态
9
+ // (先 done 后提交 / 两跳同批提交 → 拒绝);非 git / legacy / 存量豁免声明 → 跳过(S25-S29)
6
10
  // 用法:node templates/_agents/scripts/confirm-doc.test.mjs(npm test 随跑)
7
11
  import fs from 'node:fs';
8
12
  import os from 'node:os';
@@ -21,6 +25,14 @@ const check = (desc, cond, detail = '') => {
21
25
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
22
26
  const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
23
27
 
28
+ // 台账读取 helper(2026-09-30 复核 P2-1):缺失/空行 → 安全返回([])——防「门拒路径下台账未生成」时
29
+ // 未捕获 ENOENT 截断后续用例(注入验证实证:原裸读在门失效时崩溃、无「合计」行)
30
+ const readLedgerOf = (root) => {
31
+ const p = path.join(root, '.agents', 'confirmations.jsonl');
32
+ if (!fs.existsSync(p)) return [];
33
+ return fs.readFileSync(p, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
34
+ };
35
+
24
36
  // ---- S1 指纹:CRLF 归一(同内容不同行尾同指纹)----
25
37
  {
26
38
  const lf = '---\n状态: draft\n---\n正文';
@@ -119,12 +131,16 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
119
131
  const docP = path.join(root, 'workflow', 'plans');
120
132
  fs.mkdirSync(docP, { recursive: true });
121
133
  fs.writeFileSync(path.join(docP, '2026-09-27-d.md'), '---\n状态: draft\n级别: L2\n---\n# P\n');
134
+ // 逐阶段门夹具(2026-09-30 stage-gate-machine):plan 批准须先有已确认入口——
135
+ // 入口用存量口径(日期早于 confirmDocsEffective)免台账;级别 L1 免 spec 档
136
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
137
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-d.md'), '---\n状态: approved\n级别: L1\n日期: 2026-01-01\n---\n# I(S11 入口夹具)\n');
122
138
  const before = fs.readFileSync(path.join(docP, '2026-09-27-d.md'), 'utf8');
123
139
  const fpExpect = computeFingerprint(before);
124
140
  // spawnSync 无 TTY(管道 stdin)——模拟 AI 会话内委托代录调用
125
141
  const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-27-d.md', '--delegated', '2选2'], { cwd: root, encoding: 'utf8' });
126
142
  const after = fs.readFileSync(path.join(docP, '2026-09-27-d.md'), 'utf8');
127
- const led = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
143
+ const led = readLedgerOf(root);
128
144
  const j = led[led.length - 1];
129
145
  check('S11 委托代录:exit 0 + draft→approved + 指纹行 + 台账 source=chat-delegated / quote=原话 / 指纹全量与 frontmatter 前 16 位配对',
130
146
  r.status === 0 && after.includes('状态: approved') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
@@ -145,7 +161,7 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
145
161
  const fp2 = computeFingerprint(before);
146
162
  const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-e.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
147
163
  const after = fs.readFileSync(path.join(docP, '2026-09-27-e.md'), 'utf8');
148
- const led = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
164
+ const led = readLedgerOf(root);
149
165
  check('S12 委托第二跳:approved→done + 指纹行换新值 + 台账 stage=done / source 如实',
150
166
  r.status === 0 && after.includes('状态: done') && after.includes(`确认指纹: ${fp2.slice(0, 16)}`) && !after.includes(fp1.slice(0, 16))
151
167
  && led[led.length - 1].stage === 'done' && led[led.length - 1].source === 'chat-delegated',
@@ -178,7 +194,7 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
178
194
  const fpExpect = computeFingerprint(before);
179
195
  const r = spawnSync(process.execPath, [CLI, 'workflow/incidents/2026-09-28-i.md', '--delegated', '修好了'], { cwd: root, encoding: 'utf8' });
180
196
  const after = fs.readFileSync(path.join(docP, '2026-09-28-i.md'), 'utf8');
181
- const led = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
197
+ const led = readLedgerOf(root);
182
198
  const j = led[led.length - 1];
183
199
  check('S15 incidents 委托代录:exit 0 + open→fixed + 指纹行 + 台账 stage=fixed / prev=open',
184
200
  r.status === 0 && after.includes('状态: fixed') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
@@ -198,7 +214,7 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
198
214
  const fpExpect = computeFingerprint(before);
199
215
  const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-28-c.md', '--to', 'cancelled', '--delegated', '不做了'], { cwd: root, encoding: 'utf8' });
200
216
  const after = fs.readFileSync(path.join(docP, '2026-09-28-c.md'), 'utf8');
201
- const led = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
217
+ const led = readLedgerOf(root);
202
218
  const j = led[led.length - 1];
203
219
  check('S16 --to cancelled 委托代录:exit 0 + draft→cancelled + 台账 stage=cancelled / prev=draft',
204
220
  r.status === 0 && after.includes('状态: cancelled') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
@@ -263,7 +279,7 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
263
279
  JSON.stringify({ r1: r1.status, r2: r2.status, stderr: r2.stderr }));
264
280
  // 调用事实(2026-09-28 batch-ledger-audit):每次进程调用 **一个** batch;两次独立调用 batch **不同**,
265
281
  // 各自 seq=1 / of=1——这是并录审计「逐件 vs 并录」的判别基础(复核 P2-2:plan 要求覆盖此面)
266
- const led2 = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
282
+ const led2 = readLedgerOf(root);
267
283
  const [e1, e2] = led2.slice(-2);
268
284
  check('S18b 两次独立调用各生成独立 batch(不同批、各自 seq=1/of=1)',
269
285
  e1.batch !== e2.batch && e1.seq === 1 && e2.seq === 1 && e1.of === 1 && e2.of === 1,
@@ -275,7 +291,9 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
275
291
  // 该路径仅 TTY 可达(delegated 已被入口限为 1 份),故注入两个**仅测试用**逃生门:
276
292
  // CONFIRM_DOC_TEST_TTY=1 —— 放行 isTTY 判定(本环境无 TTY)
277
293
  // CONFIRM_DOC_TEST_ANSWERS —— 按序注入问答应答(见下「为什么不能用管道 stdin」)
278
- // 两者均不改动任何落态/记账语义。
294
+ // NODE_ENV=test —— TTY 逃生门双条件的第二键(2026-09-28 check16-inline-debt:
295
+ // 单变量可被生产会话顺手设置绕过 TTY 门,收紧为双条件)
296
+ // 三者均不改动任何落态/记账语义。
279
297
  //
280
298
  // **为什么不能用 spawnSync 的 input 管道(2026-09-28 更正)**:此前本用例用 `input:'可以\n可以\n'`
281
299
  // 并断言「首份」了事,注释把根因误记为「readline 对第二份提问会 unsettled / 管道输入不被逐次消费」。
@@ -285,9 +303,9 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
285
303
  const root = mk2docs();
286
304
  const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md', 'workflow/plans/2026-09-27-s.md'], {
287
305
  cwd: root, encoding: 'utf8',
288
- env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以,可以' },
306
+ env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以,可以', NODE_ENV: 'test' },
289
307
  });
290
- const ledN = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
308
+ const ledN = readLedgerOf(root);
291
309
  const stI = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-s.md'), 'utf8');
292
310
  const stP = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8');
293
311
  check('S18c 单次调用落 2 份(TTY 多文档)端到端 → batch 相同 / seq 1,2 / of 均=2 / 两份都落态',
@@ -306,15 +324,349 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
306
324
  const root = mk2docs();
307
325
  const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md', 'workflow/plans/2026-09-27-s.md'], {
308
326
  cwd: root, encoding: 'utf8',
309
- env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以' }, // 只给一份应答
327
+ env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以', NODE_ENV: 'test' }, // 只给一份应答
310
328
  });
311
- const led1 = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
329
+ const led1 = readLedgerOf(root);
312
330
  const stP1 = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8');
313
331
  check('S18d 注入应答不足 → 仅首份落态,次份按跳过处理(不落态不记账)',
314
332
  led1.length === 1 && led1[0].seq === 1 && led1[0].of === 2 && stP1.includes('状态: draft'),
315
333
  JSON.stringify({ status: r.status, ledger: led1 }));
316
334
  fs.rmSync(root, { recursive: true, force: true });
317
335
  }
336
+ {
337
+ // 【T3 双条件】仅设 CONFIRM_DOC_TEST_TTY=1 而未设 NODE_ENV=test → 逃生门不生效,仍按非 TTY 拒绝
338
+ // (2026-09-28 check16-inline-debt:单变量可被生产会话顺手设置绕过 TTY 门,收紧为双条件——
339
+ // 本用例钉住「缺第二键即不放行」,与 S18c/S18d 的「双键齐 → 放行」构成双向覆盖)
340
+ const root = mk2docs();
341
+ const env1 = { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以' };
342
+ delete env1.NODE_ENV; // 显式剔除——防宿主环境恰好携带 NODE_ENV 时用例失真
343
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md'], {
344
+ cwd: root, encoding: 'utf8', env: env1,
345
+ });
346
+ const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
347
+ const afterI = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-s.md'), 'utf8');
348
+ check('S18e 仅 CONFIRM_DOC_TEST_TTY=1 无 NODE_ENV=test → 仍非 TTY 拒绝(exit 1 + 零台账 + 文档未动)',
349
+ r.status === 1 && /不可代确认/.test(r.stderr) && noLedger && afterI.includes('状态: draft'),
350
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
351
+ fs.rmSync(root, { recursive: true, force: true });
352
+ }
353
+ }
354
+
355
+ // ---- S19【逐阶段门】spec 无同名入口 → 拒绝落账(2026-09-30 stage-gate-machine)----
356
+ {
357
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg1-'));
358
+ const docP = path.join(root, 'workflow', 'specs');
359
+ fs.mkdirSync(docP, { recursive: true });
360
+ fs.writeFileSync(path.join(docP, '2026-09-30-g.md'), '---\n状态: draft\n级别: L2\n---\n# S\n');
361
+ const before = fs.readFileSync(path.join(docP, '2026-09-30-g.md'), 'utf8');
362
+ const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-g.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
363
+ const after = fs.readFileSync(path.join(docP, '2026-09-30-g.md'), 'utf8');
364
+ const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
365
+ check('S19 逐阶段门:spec 无同名入口 → exit 2 + 未落盘未记账 + 回退提示',
366
+ r.status === 2 && /逐阶段前置门/.test(r.stderr) && /未找到同名入口/.test(r.stderr) && after === before && noLedger,
367
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
368
+ fs.rmSync(root, { recursive: true, force: true });
369
+ }
370
+
371
+ // ---- S20【逐阶段门】入口存在但未确认(draft)→ 拒绝 ----
372
+ {
373
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg2-'));
374
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
375
+ fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
376
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-h.md'), '---\n状态: draft\n级别: L2\n日期: 2026-09-30\n---\n# I\n');
377
+ fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-h.md'), '---\n状态: draft\n级别: L2\n---\n# S\n');
378
+ const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-h.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
379
+ const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
380
+ check('S20 逐阶段门:入口未确认(draft)→ exit 2 + 提示状态、零落账',
381
+ r.status === 2 && /状态「draft」/.test(r.stderr) && noLedger,
382
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
383
+ fs.rmSync(root, { recursive: true, force: true });
384
+ }
385
+
386
+ // ---- S21【逐阶段门】L1 入口已确认 → plan 放行(存量口径免台账;级别 L1 免 spec 档)----
387
+ {
388
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg3-'));
389
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
390
+ fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
391
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-j.md'), '---\n状态: approved\n级别: L1\n日期: 2026-01-01\n---\n# I\n');
392
+ fs.writeFileSync(path.join(root, 'workflow', 'plans', '2026-09-30-j.md'), '---\n状态: draft\n级别: L1\n---\n# P\n');
393
+ const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-j.md', '--delegated', '继续'], { cwd: root, encoding: 'utf8' });
394
+ const after = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-30-j.md'), 'utf8');
395
+ check('S21 逐阶段门:L1 入口 approved(存量口径)→ plan 放行落态',
396
+ r.status === 0 && after.includes('状态: approved'),
397
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
398
+ fs.rmSync(root, { recursive: true, force: true });
399
+ }
400
+
401
+ // ---- S22【逐阶段门】L2 入口已确认但缺 spec → plan 拒绝 ----
402
+ {
403
+ // 入口须有**台账行**(不复用存量日期短路路径)——夹具写 confirmed 行使前置成立,仅缺 spec
404
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg4-'));
405
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
406
+ fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
407
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
408
+ const fp4 = '4'.repeat(64);
409
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-k.md'), `---\n状态: approved\n级别: L2\n日期: 2026-09-30\n确认指纹: ${fp4.slice(0, 16)}\n---\n# I\n`);
410
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-09-30T04:00:00.000Z', doc: 'workflow/intents/2026-09-30-k.md', stage: 'approved', fingerprint: fp4, prev: 'draft', source: 'chat-delegated', batch: 'k4', seq: 1, of: 1 }) + '\n');
411
+ fs.writeFileSync(path.join(root, 'workflow', 'plans', '2026-09-30-k.md'), '---\n状态: draft\n级别: L2\n---\n# P\n');
412
+ const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-k.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
413
+ const after = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-30-k.md'), 'utf8');
414
+ check('S22 逐阶段门:L2 入口(台账行)缺同名 spec → plan 拒绝(exit 2 + 未落态)',
415
+ r.status === 2 && /缺同名 spec/.test(r.stderr) && after.includes('状态: draft'),
416
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
417
+ fs.rmSync(root, { recursive: true, force: true });
418
+ }
419
+
420
+ // ---- S23【逐阶段门】入口「台账行」正路放行(非存量口径;复核 P2-4④)----
421
+ {
422
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg5-'));
423
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
424
+ fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
425
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
426
+ const fp = 'b'.repeat(64);
427
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-m.md'), `---\n状态: approved\n级别: L2\n日期: 2026-09-30\n确认指纹: ${fp.slice(0, 16)}\n---\n# I\n`);
428
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-09-30T02:00:00.000Z', doc: 'workflow/intents/2026-09-30-m.md', stage: 'approved', fingerprint: fp, prev: 'draft', source: 'chat-delegated', batch: 't9', seq: 1, of: 1 }) + '\n');
429
+ fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-m.md'), '---\n状态: draft\n级别: L2\n---\n# S\n');
430
+ const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-m.md', '--delegated', '继续'], { cwd: root, encoding: 'utf8' });
431
+ const after = fs.readFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-m.md'), 'utf8');
432
+ check('S23 逐阶段门:入口状态 approved + 台账行(正路)→ spec 放行',
433
+ r.status === 0 && after.includes('状态: approved'),
434
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
435
+ fs.rmSync(root, { recursive: true, force: true });
436
+ }
437
+
438
+ {
439
+ // N1:存量短路(confirm-doc 侧)——legacy 入口无级别 → spec 放行
440
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-n1-'));
441
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
442
+ fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
443
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-n1.md'), '---\n状态: approved\n日期: 2026-01-01\n流程: legacy\n---\n# I(存量、无级别)\n');
444
+ fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-n1.md'), '---\n状态: draft\n级别: L2\n---\n# S\n');
445
+ const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-n1.md', '--delegated', 'n1 回归'], { cwd: root, encoding: 'utf8' });
446
+ const after = fs.readFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-n1.md'), 'utf8');
447
+ check('S24 N1 存量短路:legacy 入口无级别 → confirm-doc 放行(不锁死确认)',
448
+ r.status === 0 && after.includes('状态: approved'),
449
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
450
+ fs.rmSync(root, { recursive: true, force: true });
451
+ }
452
+
453
+ // ---- S25–S29【done 前置门】approved 历史留痕(2026-09-30 confirm-gate-approved-history)----
454
+ // git helper:与 check-loop.test 同口径(win32 → git.exe;-c 注入 user 免全局 config 依赖)
455
+ const GITBIN = process.platform === 'win32' ? 'git.exe' : 'git';
456
+ const gitAt = (root, ...args) => spawnSync(GITBIN, args, { cwd: root, encoding: 'utf8' });
457
+ const gitHead = (root) => {
458
+ gitAt(root, 'init', '-q');
459
+ gitAt(root, '-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-q', '--allow-empty', '-m', 'init');
460
+ };
461
+
462
+ // S25:有 HEAD 但文档从未提交(先 done 后提交形态)→ done 拒绝
463
+ {
464
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh1-'));
465
+ fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
466
+ const p = path.join(root, 'workflow', 'plans', '2026-09-30-d1.md');
467
+ fs.writeFileSync(p, '---\n状态: approved\n级别: L2\n日期: 2026-09-30\n---\n# P\n');
468
+ const before = fs.readFileSync(p, 'utf8');
469
+ gitHead(root);
470
+ const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-d1.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
471
+ const after = fs.readFileSync(p, 'utf8');
472
+ const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
473
+ check('S25 done 门:历史无 approved(先 done 后提交形态)→ exit 2 + 零落账 + 零写盘',
474
+ r.status === 2 && /done 前置门/.test(r.stderr) && /从未出现/.test(r.stderr) && after === before && noLedger,
475
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 260) }));
476
+ fs.rmSync(root, { recursive: true, force: true });
477
+ }
478
+
479
+ // S26:approved 态已提交(留痕)→ done 放行落账
480
+ {
481
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh2-'));
482
+ fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
483
+ const p = path.join(root, 'workflow', 'plans', '2026-09-30-d2.md');
484
+ fs.writeFileSync(p, '---\n状态: approved\n级别: L2\n日期: 2026-09-30\n---\n# P\n');
485
+ gitHead(root);
486
+ gitAt(root, 'add', '-A');
487
+ gitAt(root, '-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-q', '-m', 'approved');
488
+ const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-d2.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
489
+ const after = fs.readFileSync(p, 'utf8');
490
+ const ledger = readLedgerOf(root); // 复核 P2-1:统一 helper(缺失 → [],不崩溃)
491
+ check('S26 done 门:approved 态已提交(留痕)→ 放行落态 + 台账 stage=done',
492
+ r.status === 0 && after.includes('状态: done') && ledger.some((e) => e.stage === 'done' && e.prev === 'approved'),
493
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
494
+ fs.rmSync(root, { recursive: true, force: true });
495
+ }
496
+
497
+ // S27:非 git 环境 → 跳过(不误拦;沿检查 14 口径)
498
+ {
499
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh3-'));
500
+ fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
501
+ const p = path.join(root, 'workflow', 'plans', '2026-09-30-d3.md');
502
+ fs.writeFileSync(p, '---\n状态: approved\n级别: L2\n---\n# P\n');
503
+ const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-d3.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
504
+ const after = fs.readFileSync(p, 'utf8');
505
+ check('S27 done 门:非 git 环境 → 跳过放行',
506
+ r.status === 0 && after.includes('状态: done'),
507
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
508
+ fs.rmSync(root, { recursive: true, force: true });
509
+ }
510
+
511
+ // S28:legacy 标记 → 跳过(存量口径;intent 文档承载——spec/plan 不产生 legacy 标记)
512
+ {
513
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh4-'));
514
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
515
+ const p = path.join(root, 'workflow', 'intents', '2026-09-30-d4.md');
516
+ fs.writeFileSync(p, '---\n状态: approved\n级别: L1\n日期: 2026-01-01\n流程: legacy\n---\n# I\n');
517
+ gitHead(root);
518
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-d4.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
519
+ const after = fs.readFileSync(p, 'utf8');
520
+ check('S28 done 门:legacy 标记 → 跳过放行',
521
+ r.status === 0 && after.includes('状态: done'),
522
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
523
+ fs.rmSync(root, { recursive: true, force: true });
524
+ }
525
+
526
+ // S29:正文「存量确认态豁免(」声明 → 跳过(显式逃生口;与检查 14 同标记)
527
+ {
528
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh5-'));
529
+ fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
530
+ const p = path.join(root, 'workflow', 'specs', '2026-09-30-d5.md');
531
+ fs.writeFileSync(p, '---\n状态: approved\n级别: L2\n---\n# S\n\n存量确认态豁免(S29 构造:声明式逃生口覆盖)\n');
532
+ gitHead(root);
533
+ const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-d5.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
534
+ const after = fs.readFileSync(p, 'utf8');
535
+ check('S29 done 门:存量豁免声明 → 跳过放行',
536
+ r.status === 0 && after.includes('状态: done'),
537
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
538
+ fs.rmSync(root, { recursive: true, force: true });
539
+ }
540
+
541
+ // ---- S30【协作道批量】--batch:delegated 多份 L0/L1 一次代录 → 全部落态 + 台账 brief:true(2026-09-30 hybrid-governance-risk-lanes)----
542
+ {
543
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt1-'));
544
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
545
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt1.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I1\n');
546
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt2.md'), '---\n状态: draft\n级别: L0\n日期: 2026-09-30\n---\n# I2\n');
547
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt1.md', 'workflow/intents/2026-09-30-bt2.md', '--delegated', '批量放行', '--batch'], { cwd: root, encoding: 'utf8' });
548
+ const led = readLedgerOf(root);
549
+ const st1 = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt1.md'), 'utf8');
550
+ const st2 = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt2.md'), 'utf8');
551
+ check('S30 --batch:2 份 L0/L1 一次代录 → 全部 approved + 台账 brief:true / of=2 / seq 递增',
552
+ r.status === 0 && led.length === 2
553
+ && led.every((e) => e.brief === true && e.source === 'chat-delegated' && e.of === 2 && e.stage === 'approved')
554
+ && led[0].seq === 1 && led[1].seq === 2
555
+ && st1.includes('状态: approved') && st2.includes('状态: approved'),
556
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300), led }));
557
+ fs.rmSync(root, { recursive: true, force: true });
558
+ }
559
+
560
+ // ---- S31【协作道批量】--batch 混入 L2 → 该份逐份拒绝(exit 2 + 未落盘未记账),L1 份照常落态 ----
561
+ {
562
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt2-'));
563
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
564
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt3.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I3\n');
565
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt4.md'), '---\n状态: draft\n级别: L2\n日期: 2026-09-30\n---\n# I4\n');
566
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt3.md', 'workflow/intents/2026-09-30-bt4.md', '--delegated', '批量放行', '--batch'], { cwd: root, encoding: 'utf8' });
567
+ const led = readLedgerOf(root);
568
+ const st4 = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt4.md'), 'utf8');
569
+ check('S31 --batch 混入 L2 → L2 拒绝(exit 2 + 未落盘未记账)+ L1 落态',
570
+ r.status === 2 && /不在协作道/.test(r.stderr) && led.length === 1 && led[0].doc.endsWith('bt3.md') && st4.includes('状态: draft'),
571
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300), led }));
572
+ fs.rmSync(root, { recursive: true, force: true });
573
+ }
574
+
575
+ // ---- S32 --batch 组合约束:与 --auto / --to 互斥(exit 1,零落账)----
576
+ {
577
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt3-'));
578
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
579
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt5.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I5\n');
580
+ const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt5.md', '--delegated', 'x', '--batch', '--auto'], { cwd: root, encoding: 'utf8' });
581
+ const r2 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt5.md', '--delegated', 'x', '--batch', '--to', 'cancelled'], { cwd: root, encoding: 'utf8' });
582
+ const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
583
+ check('S32 --batch 与 --auto / --to 互斥 → 均 exit 1 + 零台账',
584
+ r1.status === 1 && r2.status === 1 && noLedger,
585
+ JSON.stringify({ r1: r1.status, r2: r2.status }));
586
+ fs.rmSync(root, { recursive: true, force: true });
587
+ }
588
+
589
+ // ---- S33 简洁审计:单份 delegated L0/L1(无 --batch)→ 台账 brief:true + 紧凑输出;L2 份不带 brief ----
590
+ {
591
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt4-'));
592
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
593
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt6.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I6\n');
594
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt6.md', '--delegated', '单份放行'], { cwd: root, encoding: 'utf8' });
595
+ const led = readLedgerOf(root);
596
+ check('S33 单份 L1 代录 → brief:true + 紧凑输出(协作道/异步审计标记)',
597
+ r.status === 0 && led.length === 1 && led[0].brief === true && /协作道\/异步审计/.test(r.stdout),
598
+ JSON.stringify({ status: r.status, stdout: String(r.stdout).slice(0, 200), led }));
599
+ fs.rmSync(root, { recursive: true, force: true });
600
+ }
601
+
602
+ // ---- S34–S37【Trusted 自动泳道】trust-mode.json = Trusted(level 2)→ 非 TTY 免旗标隐式自治(2026-09-30 hybrid-governance-explore-hardening)----
603
+ const writeTrust = (root, cfg) => {
604
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
605
+ fs.writeFileSync(path.join(root, '.agents', 'trust-mode.json'), JSON.stringify(cfg));
606
+ };
607
+ // S34:Trusted + L1 intent 免旗标 → draft→approved 落态 + 台账 source=ai-auto-trust-L2;第二跳 → done(全闭环)
608
+ {
609
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tr1-'));
610
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
611
+ writeTrust(root, { enabled: true, level: 2, name: 'Trusted', enabledAt: '2026-09-30T00:00:00.000Z' });
612
+ const p = path.join(root, 'workflow', 'intents', '2026-09-30-tr1.md');
613
+ fs.writeFileSync(p, '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I\n');
614
+ const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr1.md'], { cwd: root, encoding: 'utf8' });
615
+ const led1 = readLedgerOf(root);
616
+ check('S34 Trusted 隐式:非 TTY 无旗标 L1 draft→approved + 台账 source=ai-auto-trust-L2',
617
+ r1.status === 0 && fs.readFileSync(p, 'utf8').includes('状态: approved')
618
+ && led1.length === 1 && led1[0].source === 'ai-auto-trust-L2' && led1[0].stage === 'approved' && led1[0].prev === 'draft',
619
+ JSON.stringify({ status: r1.status, stderr: String(r1.stderr).slice(0, 300), led: led1 }));
620
+ // 第二跳:非 git 环境 done 门跳过(S27 同口径)→ Level 2 全闭环直达 done
621
+ const r2 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr1.md'], { cwd: root, encoding: 'utf8' });
622
+ const led2 = readLedgerOf(root);
623
+ check('S34b Trusted 隐式第二跳:approved→done(Level 2 全闭环)',
624
+ r2.status === 0 && fs.readFileSync(p, 'utf8').includes('状态: done')
625
+ && led2.length === 2 && led2[1].stage === 'done' && led2[1].prev === 'approved',
626
+ JSON.stringify({ status: r2.status, stderr: String(r2.stderr).slice(0, 300), led: led2 }));
627
+ fs.rmSync(root, { recursive: true, force: true });
628
+ }
629
+ // S35:Trusted 隐式边界——L2 文档与 incidents 均不过自治门(exit 2 未落账)
630
+ {
631
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tr2-'));
632
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
633
+ fs.mkdirSync(path.join(root, 'workflow', 'incidents'), { recursive: true });
634
+ writeTrust(root, { enabled: true, level: 2, name: 'Trusted' });
635
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-tr2.md'), '---\n状态: draft\n级别: L2\n日期: 2026-09-30\n---\n# I\n');
636
+ fs.writeFileSync(path.join(root, 'workflow', 'incidents', '2026-09-30-tr3.md'), '---\n状态: open\n级别: L1\n---\n# I\n');
637
+ const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr2.md'], { cwd: root, encoding: 'utf8' });
638
+ const r2 = spawnSync(process.execPath, [CLI, 'workflow/incidents/2026-09-30-tr3.md'], { cwd: root, encoding: 'utf8' });
639
+ check('S35 Trusted 隐式边界:L2 文档与 incidents 均拒绝(exit 2 未过自治门)+ 零台账',
640
+ r1.status === 2 && r2.status === 2 && readLedgerOf(root).length === 0,
641
+ JSON.stringify({ r1: r1.status, r2: r2.status, stderr1: String(r1.stderr).slice(0, 200) }));
642
+ fs.rmSync(root, { recursive: true, force: true });
643
+ }
644
+ // S36:Standard(level 1)不隐式——非 TTY 无旗标仍拒(隐式泳道仅 Trusted;fail-closed)
645
+ {
646
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tr3-'));
647
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
648
+ writeTrust(root, { enabled: true, level: 1, name: 'Standard' });
649
+ const p = path.join(root, 'workflow', 'intents', '2026-09-30-tr4.md');
650
+ fs.writeFileSync(p, '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I\n');
651
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr4.md'], { cwd: root, encoding: 'utf8' });
652
+ check('S36 Standard 不隐式:非 TTY 无旗标 → 仍 exit 1 拒绝 + 零写盘',
653
+ r.status === 1 && /不可代确认/.test(r.stderr) && fs.readFileSync(p, 'utf8').includes('状态: draft'),
654
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
655
+ fs.rmSync(root, { recursive: true, force: true });
656
+ }
657
+ // S37:Trusted 隐式边界——多份 / --to 放弃态 → 不隐式,仍拒(exit 1)
658
+ {
659
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tr4-'));
660
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
661
+ writeTrust(root, { enabled: true, level: 2, name: 'Trusted' });
662
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-tr5.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I\n');
663
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-tr6.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I\n');
664
+ const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr5.md', 'workflow/intents/2026-09-30-tr6.md'], { cwd: root, encoding: 'utf8' });
665
+ const r2 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr5.md', '--to', 'cancelled'], { cwd: root, encoding: 'utf8' });
666
+ check('S37 Trusted 隐式边界:多份 / --to 放弃态 → 不隐式仍拒(exit 1 + 零台账)',
667
+ r1.status === 1 && r2.status === 1 && /不可代确认/.test(r1.stderr) && readLedgerOf(root).length === 0,
668
+ JSON.stringify({ r1: r1.status, r2: r2.status, stderr1: String(r1.stderr).slice(0, 160), stderr2: String(r2.stderr).slice(0, 160) }));
669
+ fs.rmSync(root, { recursive: true, force: true });
318
670
  }
319
671
 
320
672
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
@@ -27,7 +27,7 @@ if (!fs.existsSync(SRC)) {
27
27
  console.error(`doctor.test.mjs:SKIP——未找到 ${SRC}(装户环境无包源 src/;本套件仅在包源仓库/dogfooding 跑 npm test 时有意义)`);
28
28
  process.exit(0);
29
29
  }
30
- const { checkOwnedDrift, checkAdapterDrift, checkLedgerCoverage } = await import(pathToFileURL(SRC).href);
30
+ const { checkOwnedDrift, checkAdapterDrift, checkLedgerCoverage, checkDelegationSnapshotFreshness } = await import(pathToFileURL(SRC).href);
31
31
 
32
32
  let pass = 0;
33
33
  let fail = 0;
@@ -372,5 +372,106 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
372
372
  fs.rmSync(pkg, { recursive: true, force: true });
373
373
  }
374
374
 
375
+ // ---- 场景 19:快照最新(snapshotMonth >= ledgerMonth)→ stale=false ----
376
+ {
377
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
378
+ fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
379
+ fs.writeFileSync(path.join(root, 'workflow', 'delegations.md'), [
380
+ '## 委派结果',
381
+ '',
382
+ '| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |',
383
+ '|------|----------|------------|------|------|',
384
+ '| 2026-09-20 | x | y | 一次通过 | z |',
385
+ '| 2026-09-26 | x | y | 一次通过 | z |',
386
+ '',
387
+ '## 月度聚合快照',
388
+ '',
389
+ '| 月份 | 有效任务 | 一次通过率 | 返工次数 |',
390
+ '|------|----------|------------|----------|',
391
+ '| 2026-09 | 2 | 100% | 0 |',
392
+ '',
393
+ ].join('\n'));
394
+ const r = checkDelegationSnapshotFreshness(root);
395
+ check('场景 19:快照最新 → stale=false skipped=false',
396
+ r.stale === false && r.skipped === false && r.ledgerMonth === '2026-09' && r.snapshotMonth === '2026-09',
397
+ JSON.stringify(r));
398
+ fs.rmSync(root, { recursive: true, force: true });
399
+ }
400
+
401
+ // ---- 场景 20:快照陈旧(snapshotMonth < ledgerMonth)→ stale=true ----
402
+ {
403
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
404
+ fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
405
+ fs.writeFileSync(path.join(root, 'workflow', 'delegations.md'), [
406
+ '## 委派结果',
407
+ '',
408
+ '| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |',
409
+ '|------|----------|------------|------|------|',
410
+ '| 2026-09-10 | x | y | 一次通过 | z |',
411
+ '',
412
+ '## 月度聚合快照',
413
+ '',
414
+ '| 月份 | 有效任务 | 一次通过率 | 返工次数 |',
415
+ '|------|----------|------------|----------|',
416
+ '| 2026-08 | 1 | 100% | 0 |',
417
+ '',
418
+ ].join('\n'));
419
+ const r = checkDelegationSnapshotFreshness(root);
420
+ check('场景 20:快照陈旧(08 < 09)→ stale=true',
421
+ r.stale === true && r.skipped === false && r.ledgerMonth === '2026-09' && r.snapshotMonth === '2026-08',
422
+ JSON.stringify(r));
423
+ fs.rmSync(root, { recursive: true, force: true });
424
+ }
425
+
426
+ // ---- 场景 21:有数据行但无快照行 → stale=true snapshotMonth=null ----
427
+ {
428
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
429
+ fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
430
+ fs.writeFileSync(path.join(root, 'workflow', 'delegations.md'), [
431
+ '## 委派结果',
432
+ '',
433
+ '| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |',
434
+ '|------|----------|------------|------|------|',
435
+ '| 2026-09-25 | x | y | 一次通过 | z |',
436
+ '',
437
+ ].join('\n'));
438
+ const r = checkDelegationSnapshotFreshness(root);
439
+ check('场景 21:无快照行 → stale=true snapshotMonth=null',
440
+ r.stale === true && r.skipped === false && r.snapshotMonth === null,
441
+ JSON.stringify(r));
442
+ fs.rmSync(root, { recursive: true, force: true });
443
+ }
444
+
445
+ // ---- 场景 22:无台账数据行 → skipped=true ----
446
+ {
447
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
448
+ fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
449
+ fs.writeFileSync(path.join(root, 'workflow', 'delegations.md'), [
450
+ '# delegations',
451
+ '',
452
+ '## 月度聚合快照',
453
+ '',
454
+ '| 月份 | 有效任务 |',
455
+ '|------|----------|',
456
+ '| 2026-09 | 0 |',
457
+ '',
458
+ ].join('\n'));
459
+ const r = checkDelegationSnapshotFreshness(root);
460
+ check('场景 22:无数据行 → skipped=true',
461
+ r.skipped === true && r.stale === false,
462
+ JSON.stringify(r));
463
+ fs.rmSync(root, { recursive: true, force: true });
464
+ }
465
+
466
+ // ---- 场景 23:delegations.md 不存在 → skipped=true ----
467
+ {
468
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
469
+ const r = checkDelegationSnapshotFreshness(root);
470
+ check('场景 23:文件不存在 → skipped=true',
471
+ r.skipped === true && r.stale === false,
472
+ JSON.stringify(r));
473
+ fs.rmSync(root, { recursive: true, force: true });
474
+ }
475
+
375
476
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
376
477
  process.exit(fail ? 1 : 0);
@@ -1,5 +1,5 @@
1
1
  // flow-kit fill-intent:起草 intent 草稿(背景与问题/目标/非目标/约束/影响面/触达红线/验收标准 共 7 节)
2
- // 读 workflow/intents/_TEMPLATE.md 的节标题 → 输出符合 frontmatter 受限子集(5 字段)的草稿到指定文件
2
+ // 读 workflow/intents/_TEMPLATE.md 的节标题 → 输出符合 frontmatter 受限子集(6 字段,含 risk_level 风险泳道)的草稿到指定文件
3
3
  // 用法:node .agents/scripts/fill-intent.mjs --module pipeline --level L1 --topic "test" --output /tmp/x.md
4
4
  // 零依赖;CLI 解析用最简字符串匹配;不替代 AI 起草——本工具只输出模板与提示句
5
5
  import fs from 'node:fs';
@@ -27,22 +27,23 @@ function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
27
27
 
28
28
  const SECTIONS = [
29
29
  { title: '背景与问题', hint: '<为什么做;写明需求来源(工单 / 沟通记录 / 待办事项)>' },
30
+ { title: '历史教训/防复发', hint: '- 检索结果:`node .agents/scripts/kb-search.mjs "<关键词>" --type incidents` 命中结果\n- 避坑指南:<针对历史根因,本次如何预防/规避>' },
30
31
  { title: '目标', hint: '- <可验证的目标>' },
31
32
  { title: '非目标(L1 微改动无实质内容可删本节,不硬填)', hint: '- <明确不做的,防止范围蔓延>' },
32
33
  { title: '约束(L1 微改动无实质内容可删本节,不硬填)', hint: '- <复用什么、不许动什么>' },
33
- { title: '影响面', hint: '- 模块:<词表之一,见 .agents/workflow-modules.txt>\n- 数据库:无 / <表名>(涉及 schema 变更 → L3;无 schema 的运行时 / 管线结构面亦为 L3,判据见 .agents/commands/new-task.md §级别判断)\n- 前端页面:<路由或组件,无则删本行>' },
34
- { title: '触达红线(对照 AGENTS.md,勾选仅标记触及范围;具体如何满足在同名 spec 中说明)', hint: '- [ ] <按本项目 AGENTS.md 红线逐行补;无则删本行>\n- [ ] 规则 / 契约变更(编码权威 / 共享契约 / 接口签名 / 既有参数语义 / 全局口径)→ 级别至少 L2\n- [ ] schema / 迁移 SQL / DI 链 / 认证与中间件管线 → 级别 L3\n\n> 触及任一红线即为 L2 / L3,必须立 ../specs/YYYY-MM-DD-<主题>.md 写明如何满足,方可起草 plan。' },
35
- { title: '验收标准(可测试)', hint: '- [ ] <逐条可测试;写不出可测试判据 = 还没想清楚>\n\n> **闭环对账**:关单在 test 阶段(不依赖 deploy)。intent 置 done 前逐条勾验,每条勾选项后补证据——`- [x] <判据>(证据:<commit SHA / 测试用例名 / 冒烟脚本输出>)`。\n> done 状态仍有未勾项会被 check-loop 拦截(2026-09-12 起新建 intent 为 hard-block,存量 intent 仅 warning 提示);勾选但缺「证据:」为 warning。' },
34
+ { title: '影响面', hint: '- 模块:<词表之一,见 .agents/workflow-modules.txt>\n- 数据库:无 / <表名>(涉及 schema 变更 $\rightarrow$ L3;无 schema 的运行时 / 管线结构面亦为 L3,判据见 .agents/commands/new-task.md §级别判断)\n- 前端页面:<路由或组件,无则删本行>' },
35
+ { title: '触达红线(对照 AGENTS.md,勾选仅标记触及范围;具体如何满足在同名 spec 或 Quick-Plan 中说明)', hint: '- [ ] <按本项目 AGENTS.md 红线逐行补;无则删本行>\n- [ ] 规则 / 契约变更(编码权威 / 共享契约 / 接口签名 / 既有参数语义 / 全局口径)$\rightarrow$ 级别至少 L2\n- [ ] schema / 迁移 SQL / DI 链 / 认证与中间件管线 $\rightarrow$ 级别 L3\n\n> 触及任一红线即为 L2 / L3,必须立 ../specs/YYYY-MM-DD-<主题>.md 写明如何满足,方可起草 plan。' },
36
+ { title: '验收标准(可测试)', hint: '- [ ] <逐条可测试;写不出可测试判据 = 还没想清楚>\n\n> **闭环对账**:关单在 test 阶段(不依赖 deploy)。intent 置 done 前逐条勾验,每条补证据——`- [x] <判据>(证据:<commit SHA / 测试用例名 / 冒烟脚本输出>)`。\n> done 状态仍有未勾项会被 check-loop 拦截(2026-09-12 起新建 intent 为 hard-block,存量 intent 仅 warning 提示);勾选但缺「证据:」为 hard-block。' },
36
37
  ];
37
38
 
38
39
  export function renderIntent({ topic, module, level, notes, date = TODAY }) {
39
40
  if (!LEVELS.includes(level)) throw new Error('level 不在枚举 ' + LEVELS.join('|') + ':' + level);
40
- const fm = '---\n状态: draft\n级别: ' + level + '\n日期: ' + date + '\n模块: ' + module + '\n备注: ' + (notes || '<可选:附注自由文本,check-loop 不解析>') + '\n---\n';
41
- let body = '# INTENT — ' + topic + '\n\n<!-- 复制本模板为 YYYY-MM-DD-<主题>.md 后填写;plans/ 下同名文件与本文件配对 -->\n<!-- frontmatter 受限子集(2026-09-13):每行 键: 值;状态∈draft/approved/done/superseded/cancelled(严格枚举,附注写备注键);级别∈L0/L1/L2/L3;check-loop 只扫 frontmatter 取机器字段,正文不再写状态/级别行 -->\n\n';
41
+ const fm = '---\n状态: draft\n级别: ' + level + '\nrisk_level: ' + level + '\n日期: ' + date + '\n模块: ' + module + '\n备注: ' + (notes || '<可选:附注自由文本,check-loop 不解析>') + '\n---\n';
42
+ let body = '# INTENT — ' + topic + '\n\n<!-- 复制本模板为 YYYY-MM-DD-<主题>.md 后填写;plans/ 下同名文件与本文件配对 -->\n<!-- frontmatter 受限子集(2026-09-13):每行 键: 值;状态∈draft/approved/done/superseded/cancelled(严格枚举,附注写备注键);级别∈L0/L1/L2/L3;risk_level=级别(混合治理风险泳道选道:L0/L1 协作道异步审计 / L2/L3 防御道同步确认);check-loop 只扫 frontmatter 取机器字段,正文不再写状态/级别行 -->\n\n';
42
43
  for (const { title, hint } of SECTIONS) {
43
44
  body += '## ' + title + '\n\n' + hint + '\n\n';
44
45
  }
45
- body += '## 确认与复核\n\n- 确认日期:\n- 确认人:用户(对话内一句"可以"即确认)\n- 确认范围:\n- 复核:L1 不要求独立复核\n';
46
+ body += '## 确认与复核\n\n- 确认日期:\n- 确认人:用户(对话内明确放行即确认)\n- 确认范围:\n- 复核:L1 不要求独立复核\n';
46
47
  return { frontmatter: fm, body: fm + body, sections: SECTIONS.map((s) => s.title) };
47
48
  }
48
49