agentic-flow-kit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +36 -12
  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 +1 -1
  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/solidify-task.mjs +101 -0
  128. package/templates/_agents/scripts/stage-gates.mjs +256 -0
  129. package/templates/_agents/scripts/trust-mode.mjs +67 -0
  130. package/templates/_agents/scripts/wf-journal.mjs +71 -2
  131. package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
  132. package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
  133. package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
  134. package/templates/_agents/workflows/pipeline-closing.md +1 -1
  135. package/templates/_githooks/pre-commit +33 -2
  136. package/templates/_githooks/pre-push +56 -12
  137. package/templates/workflow/README.md +22 -3
  138. package/templates/workflow/incidents/_TEMPLATE.md +4 -0
  139. package/templates/workflow/intents/_TEMPLATE.md +1 -0
@@ -36,12 +36,12 @@ function check(name, cond, detail = '') {
36
36
  check('S3 非法 level 抛错', threw);
37
37
  }
38
38
 
39
- // ---- 场景 4:7 节正文齐 ----
39
+ // ---- 场景 4:9 节正文齐 ----
40
40
  {
41
41
  const { body } = renderIntent({ topic: 't', module: 'pipeline', level: 'L1' });
42
- const expectedSections = ['背景与问题', '目标', '非目标', '约束', '影响面', '触达红线', '验收标准', '确认与复核'];
42
+ const expectedSections = ['背景与问题', '历史教训/防复发', '目标', '非目标', '约束', '影响面', '触达红线', '验收标准', '确认与复核'];
43
43
  for (const s of expectedSections) check('S4 含节 ## ' + s, body.includes('## ' + s));
44
- check('S4 共 8 节标题', (body.match(/^##\s/gm) || []).length === 8);
44
+ check('S4 共 9 节标题', (body.match(/^##\s/gm) || []).length === 9);
45
45
  }
46
46
 
47
47
  // ---- 场景 5:备注字段透传 ----
@@ -69,5 +69,14 @@ function check(name, cond, detail = '') {
69
69
  check('S7 缺 topic 仍可渲染(透传设计)', typeof r.body === 'string' && r.body.includes('INTENT — undefined'));
70
70
  }
71
71
 
72
+ // ---- 场景 8:risk_level 风险泳道字段(混合治理,= 级别,全枚举落 frontmatter)----
73
+ {
74
+ for (const level of ['L0', 'L1', 'L2', 'L3']) {
75
+ const { body } = renderIntent({ topic: 't', module: 'pipeline', level });
76
+ const fm = body.match(/^---([\s\S]*?)---/)[1];
77
+ check('S8 risk_level=' + level + ' 写入 frontmatter', new RegExp('^risk_level:\\s*' + level + '$', 'm').test(fm));
78
+ }
79
+ }
80
+
72
81
  console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
73
82
  process.exit(failCount ? 1 : 0);
@@ -1,22 +1,26 @@
1
- // flow-kit fill-plan:起草 plan 草稿(L1 极简 2 节 / L2-L3 完整 4 节)
2
- // 用法:node .agents/scripts/fill-plan.mjs --level L1 --topic "test" --output /tmp/x.md
3
- // L1:改动面(必填)+ 验证方式(必填)
4
- // L2/L3:改动面 + 任务拆解 + 执行顺序 + 验证方式
1
+ // flow-kit fill-plan:起草 plan 草稿(L1 Quick-Plan 三节 / L2-L3 完整 4 节)
2
+ // 用法:node .agents/scripts/fill-plan.mjs --level L1 --topic "test" --output /tmp/x.md [--root <仓库根>]
3
+ // L1:改动方案 + 约束与风险 + 验证计划(Quick-Plan 必填三节)
4
+ // L2/L3:改动方案 + 任务拆解 + 执行顺序 + 验证计划
5
+ // 起草门(2026-09-30 stage-gate-machine):output 落于工作区(路径含 workflow 段)时,须同主题入口已确认,
6
+ // 且入口级别(取入口 frontmatter,非 --level)为 L2/L3 时同名 spec 已确认;未过 → 拒绝生成(exit 2)。
5
7
  import fs from 'node:fs';
6
8
  import path from 'node:path';
7
9
  import { ENUMS } from './workflow-enums.mjs';
10
+ import { draftGateFor, resolveWorkspace } from './stage-gates.mjs';
8
11
 
9
12
  const TODAY = new Date().toISOString().slice(0, 10);
10
13
  const LEVELS = ENUMS['level.all']; // 级别词表单源(.agents/workflow-enums.txt)
11
14
 
12
15
  function parseArgs(argv) {
13
- const out = { topic: null, level: 'L1', output: null };
16
+ const out = { topic: null, level: 'L1', output: null, root: null };
14
17
  for (let i = 0; i < argv.length; i++) {
15
18
  const a = argv[i];
16
19
  const next = () => { const v = argv[++i]; if (v === undefined) return null; return v; };
17
20
  if (a === '--topic') out.topic = next();
18
21
  else if (a === '--level') out.level = next();
19
22
  else if (a === '--output') out.output = next();
23
+ else if (a === '--root') out.root = next();
20
24
  }
21
25
  return out;
22
26
  }
@@ -24,22 +28,23 @@ function parseArgs(argv) {
24
28
  function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
25
29
 
26
30
  const L1_SECTIONS = [
27
- { title: '改动面(L1 极简形态主节;L2/L3 可作任务拆解的汇总或删本节)', hint: '- <文件/组件>:<做什么;判据细节直接写进条目,如「L771 message.success 改『更新成功』>' },
28
- { title: '验证方式', hint: '- 静态门:项目构建命令(+ 项目测试命令,测试项目就绪后)\n- 前端:cd frontend && npm run build(vue-tsc)\n- UI:.agents/commands/test.md(涉及页面改动必走,headless Chrome 实测)\n- L2 追加:<契约 / 规则面比对:编码结果抽样 / 接口契约断言 / 口径对账>\n- L3 追加:<含 schema 变更:备份 + 回滚 SQL 就绪后执行;仅运行时 / 管线:回退上一 release tag + 配置开关预案>' },
31
+ { title: '改动方案', hint: '- <文件/组件>:<做什么;判据细节直接写进条目,如「L771 message.success 改『更新成功』」>' },
32
+ { title: '约束与风险', hint: '- 约束:<对照 AGENTS.md 红线,说明如何满足;不许动什么>\n- 风险:<潜在影响 / 破坏性风险>' },
33
+ { title: '验证计划', hint: '- 静态门:项目构建命令(+ 项目测试命令,测试项目就绪后)\n- 前端:cd frontend && npm run build(vue-tsc)\n- UI:.agents/commands/test.md(涉及页面改动必走,headless Chrome 实测)\n- L2 追加:<契约 / 规则面比对:编码结果抽样 / 接口契约断言 / 口径对账>\n- L3 追加:<含 schema 变更:备份 + 回滚 SQL 就绪后执行;仅运行时 / 管线:回退上一 release tag + 配置开关预案>' },
29
34
  ];
30
35
 
31
36
  const FULL_SECTIONS = [
32
37
  L1_SECTIONS[0],
33
38
  { title: '任务拆解(L2/L3 必填;L1 仅多文件多步骤时用,单任务微改动删本节)', hint: '1. <任务>\n - 判据:<怎样算完成;尽量对应一个测试或可复现操作>\n - 风险:低 / 中 / 高(<原因>)\n2. <任务>\n - 判据:…\n - 风险:…' },
34
39
  { title: '执行顺序(L2/L3 必填;L1 单文件微改动删本节)', hint: '<1 → 2 → 3;标注依赖关系>' },
35
- L1_SECTIONS[1],
40
+ L1_SECTIONS[2],
36
41
  ];
37
42
 
38
43
  export function renderPlan({ topic, level = 'L1', date = TODAY }) {
39
44
  if (!LEVELS.includes(level)) throw new Error('level 不在枚举 ' + LEVELS.join('|') + ':' + level);
40
45
  const sections = level === 'L1' ? L1_SECTIONS : FULL_SECTIONS;
41
46
  const fm = '---\n状态: draft\n级别: ' + level + '\n模块: pipeline\n---\n';
42
- let body = '# PLAN — ' + topic + '\n\n<!-- 与 intents/ 或 incidents/ 下同名入口文档配对;L2/L3 必须先有 ../specs/ 同名 spec 确认通过;AI 起草、用户对话内确认后开工 -->\n<!-- frontmatter 受限子集(2026-09-13):状态∈draft/approved/done/superseded/cancelled;级别∈L0/L1/L2/L3;正文不再写状态/级别行 -->\n<!-- L1 极简形态(2026-09-13 瘦身,L1 默认):单/少文件微改动只填「改动面」+「验证方式」两节——改动面逐条列改哪个文件做什么(判据细节直接写进条目),验证一行静态门 + 按需 UI 实测;任务拆解/执行顺序仅 L1 多文件多步骤时保留 -->\n\n对应入口:../intents/YYYY-MM-DD-<主题>.md 或 ../incidents/YYYY-MM-DD-<主题>.md(保留实际一项)\n对应 spec:../specs/YYYY-MM-DD-<主题>.md(L1 可省略)\n\n';
47
+ let body = '# PLAN — ' + topic + '\n\n<!-- 与 intents/ 或 incidents/ 下同名入口文档配对;L2/L3 必须先有 ../specs/ 同名 spec 确认通过;AI 起草、用户对话内确认后开工 -->\n<!-- frontmatter 受限子集(2026-09-13):状态∈draft/approved/done/superseded/cancelled;级别∈L0/L1/L2/L3;正文不再写状态/级别行 -->\n<!-- L1 快车道 (Quick-Plan)(2026-09-30 更新):合并 Spec 与 Plan 核心。必填三节:[改动方案, 约束与风险, 验证计划]。仅多文件多步骤时保留任务拆解/执行顺序 -->\n\n对应入口:../intents/YYYY-MM-DD-<主题>.md 或 ../incidents/YYYY-MM-DD-<主题>.md(保留实际一项)\n对应 spec:../specs/YYYY-MM-DD-<主题>.md(L1 走快车道则省略)\n\n';
43
48
  for (const { title, hint } of sections) {
44
49
  body += '## ' + title + '\n\n' + hint + '\n\n';
45
50
  }
@@ -50,11 +55,22 @@ export function renderPlan({ topic, level = 'L1', date = TODAY }) {
50
55
  const isMain = process.argv[1] && process.argv[1].endsWith('fill-plan.mjs');
51
56
  if (isMain) {
52
57
  const a = parseArgs(process.argv.slice(2));
53
- if (!a.topic || !a.output) fail('必填:--topic / --output;选填:--level(默认 L1)');
58
+ if (!a.topic || !a.output) fail('必填:--topic / --output;选填:--level(默认 L1)/ --root');
54
59
  if (!LEVELS.includes(a.level)) fail('level 必须在 ' + LEVELS.join('|') + ':' + a.level);
60
+ const ws = resolveWorkspace(a.output, a.root);
61
+ if (ws) {
62
+ const gate = draftGateFor('plan', ws.root, ws.base);
63
+ if (!gate.ok) {
64
+ console.error('❌ 起草门未过(stage-gates):' + gate.reason);
65
+ console.error(' ' + gate.hint + '——本份未生成(确认前置后重跑本命令)');
66
+ process.exit(2);
67
+ }
68
+ } else {
69
+ console.error('ℹ️ 未定位到工作区(output 路径不含 workflow/ 段)——按仓外草稿处理,跳过起草门前置校验');
70
+ }
55
71
  const { body } = renderPlan(a);
56
72
  fs.mkdirSync(path.dirname(a.output), { recursive: true });
57
73
  fs.writeFileSync(a.output, body);
58
- console.log('✅ 已生成 plan 草稿:' + a.output + '(状态 draft / 级别 ' + a.level + ' / ' + (a.level === 'L1' ? '极简 2 节' : '完整 4 节') + ')');
74
+ console.log('✅ 已生成 plan 草稿:' + a.output + '(状态 draft / 级别 ' + a.level + ' / ' + (a.level === 'L1' ? 'Quick-Plan 三节' : '完整 4 节') + ')');
59
75
  }
60
76
  export default renderPlan;
@@ -1,4 +1,9 @@
1
- // fill-plan.test.mjs — fill-plan 工具测试(2026-09-25 doc-fill-tools)
1
+ // fill-plan.test.mjs — fill-plan 工具测试(2026-09-25 doc-fill-tools;2026-09-30 stage-gate-machine 补起草门场景)
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
2
7
  import { renderPlan } from './fill-plan.mjs';
3
8
 
4
9
  let pass = 0, failCount = 0;
@@ -7,31 +12,33 @@ function check(name, cond, detail = '') {
7
12
  else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
8
13
  }
9
14
 
10
- // ---- 场景 1:L1 极简 2 节 ----
15
+ // ---- 场景 1:L1 Quick-Plan 三节 ----
11
16
  {
12
17
  const { body } = renderPlan({ topic: 't', level: 'L1' });
13
18
  const sections = body.match(/^##\s.+$/gm) || [];
14
- check('S1 L1 含节 ## 改动面', body.includes('## 改动面'));
15
- check('S1 L1 含节 ## 验证方式', body.includes('## 验证方式'));
16
- check('S1 L1 共 3 节(改动面 + 验证方式 + 确认与复核)', sections.length === 3, '实际 ' + sections.length + ' 节');
19
+ check('S1 L1 含节 ## 改动方案', body.includes('## 改动方案'));
20
+ check('S1 L1 含节 ## 约束与风险', body.includes('## 约束与风险'));
21
+ check('S1 L1 含节 ## 验证计划', body.includes('## 验证计划'));
22
+ check('S1 L1 共 4 节(Quick-Plan 三节 + 确认与复核)', sections.length === 4, '实际 ' + sections.length + ' 节');
17
23
  }
18
24
 
19
25
  // ---- 场景 2:L2 完整 4 节 ----
20
26
  {
21
27
  const { body } = renderPlan({ topic: 't', level: 'L2' });
22
28
  const sections = body.match(/^##\s.+$/gm) || [];
23
- check('S2 L2 含节 ## 改动面', body.includes('## 改动面'));
29
+ check('S2 L2 含节 ## 改动方案', body.includes('## 改动方案'));
24
30
  check('S2 L2 含节 ## 任务拆解', body.includes('## 任务拆解'));
25
31
  check('S2 L2 含节 ## 执行顺序', body.includes('## 执行顺序'));
26
- check('S2 L2 含节 ## 验证方式', body.includes('## 验证方式'));
32
+ check('S2 L2 含节 ## 验证计划', body.includes('## 验证计划'));
27
33
  check('S2 L2 共 5 节(4 节 + 确认与复核)', sections.length === 5, '实际 ' + sections.length + ' 节');
28
34
  }
29
35
 
30
36
  // ---- 场景 3:L3 与 L2 同结构 ----
31
37
  {
32
38
  const { body } = renderPlan({ topic: 't', level: 'L3' });
33
- check('S3 L3 含节 ## 改动面', body.includes('## 改动面'));
39
+ check('S3 L3 含节 ## 改动方案', body.includes('## 改动方案'));
34
40
  check('S3 L3 含节 ## 任务拆解', body.includes('## 任务拆解'));
41
+ check('S3 L3 含节 ## 验证计划', body.includes('## 验证计划'));
35
42
  }
36
43
 
37
44
  // ---- 场景 4:非法 level 抛错 ----
@@ -63,5 +70,138 @@ function check(name, cond, detail = '') {
63
70
  check('S7 含「对应 spec」段(L1 可省略)', body.includes('对应 spec:'));
64
71
  }
65
72
 
73
+ // ---- 场景 8-13:起草门(2026-09-30 stage-gate-machine)——入口已确认;L2/L3 另须 spec 已确认 ----
74
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
75
+ const CLI = path.join(SCRIPT_DIR, 'fill-plan.mjs');
76
+ const mkws = (files = {}) => {
77
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fill-plan-gate-'));
78
+ fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
79
+ for (const [rel, body] of Object.entries(files)) {
80
+ fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
81
+ fs.writeFileSync(path.join(root, rel), body);
82
+ }
83
+ return root;
84
+ };
85
+ const runTool = (root, base, level) => spawnSync(process.execPath,
86
+ [CLI, '--topic', base, '--level', level, '--output', path.join(root, 'workflow', 'plans', `${base}.md`)],
87
+ { cwd: root, encoding: 'utf8' });
88
+ {
89
+ const root = mkws();
90
+ const r = runTool(root, '2026-09-30-none', 'L1');
91
+ check('S8 起草门:无同名入口 → exit 2 且零文件产出',
92
+ r.status === 2 && /起草门未过/.test(r.stderr) && !fs.existsSync(path.join(root, 'workflow', 'plans', '2026-09-30-none.md')),
93
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
94
+ fs.rmSync(root, { recursive: true, force: true });
95
+ }
96
+ {
97
+ const root = mkws({ 'workflow/intents/2026-09-30-l1.md': '---\n状态: approved\n级别: L1\n日期: 2026-01-01\n---\n# I\n' });
98
+ const r = runTool(root, '2026-09-30-l1', 'L1');
99
+ const f = path.join(root, 'workflow', 'plans', '2026-09-30-l1.md');
100
+ check('S9 起草门:L1 入口 approved(存量口径)→ 放行生成',
101
+ r.status === 0 && fs.existsSync(f) && fs.readFileSync(f, 'utf8').includes('状态: draft'),
102
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
103
+ fs.rmSync(root, { recursive: true, force: true });
104
+ }
105
+ {
106
+ // S10(2026-09-30 hybrid-governance-explore-hardening 更新):draft 且 L2 → 仍拒(防御道维持入口确认
107
+ // 前置);原 L1 draft 夹具转入 S10b(探索泳道放行)
108
+ const root = mkws({ 'workflow/intents/2026-09-30-draft.md': '---\n状态: draft\n级别: L2\n日期: 2026-09-30\n---\n# I\n' });
109
+ const r = runTool(root, '2026-09-30-draft', 'L1');
110
+ check('S10 起草门:入口 draft 且 L2 → exit 2 + 提示状态(防御道不变)',
111
+ r.status === 2 && /状态「draft」/.test(r.stderr),
112
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
113
+ fs.rmSync(root, { recursive: true, force: true });
114
+ }
115
+ {
116
+ // S10b 探索泳道(2026-09-30 hybrid-governance-explore-hardening):L0/L1 draft intent → 放行 plan 起草
117
+ // (「先起草后确认」;收口由 check-loop --hardening 加固门兜底)
118
+ const root = mkws({ 'workflow/intents/2026-09-30-exp.md': '---\n状态: draft\n级别: L1\nrisk_level: L1\n日期: 2026-09-30\n---\n# I\n' });
119
+ const r = runTool(root, '2026-09-30-exp', 'L1');
120
+ const f = path.join(root, 'workflow', 'plans', '2026-09-30-exp.md');
121
+ check('S10b 探索泳道:L1 入口 draft → 放行生成(先起草后确认)',
122
+ r.status === 0 && fs.existsSync(f) && fs.readFileSync(f, 'utf8').includes('状态: draft'),
123
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
124
+ fs.rmSync(root, { recursive: true, force: true });
125
+ }
126
+ {
127
+ // S10c 探索泳道回落:级别缺失、risk_level=L0 → 放行(risk_level 兜底判道)
128
+ const root = mkws({ 'workflow/intents/2026-09-30-exp0.md': '---\n状态: draft\nrisk_level: L0\n日期: 2026-09-30\n---\n# I\n' });
129
+ const r = runTool(root, '2026-09-30-exp0', 'L1');
130
+ check('S10c 探索泳道回落:级别缺失 + risk_level L0 → 放行',
131
+ r.status === 0 && fs.existsSync(path.join(root, 'workflow', 'plans', '2026-09-30-exp0.md')),
132
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
133
+ fs.rmSync(root, { recursive: true, force: true });
134
+ }
135
+ {
136
+ // S10d 探索泳道边界:级别 L2 合法时 risk_level L0 不回落 → 仍拒(fail-closed 就严)
137
+ const root = mkws({ 'workflow/intents/2026-09-30-expd.md': '---\n状态: draft\n级别: L2\nrisk_level: L0\n日期: 2026-09-30\n---\n# I\n' });
138
+ const r = runTool(root, '2026-09-30-expd', 'L1');
139
+ check('S10d 探索泳道边界:级别 L2(risk_level L0 不回落)→ 仍拒',
140
+ r.status === 2 && /状态「draft」/.test(r.stderr),
141
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
142
+ fs.rmSync(root, { recursive: true, force: true });
143
+ }
144
+ {
145
+ // 门禁后入口(日期 ≥ 生效日)须有台账行——夹具补 confirmed 行,才触达「缺 spec」分支
146
+ const root = mkws({ 'workflow/intents/2026-09-30-l2.md': `---\n状态: approved\n级别: L2\n日期: 2026-09-30\n确认指纹: ${'7'.repeat(16)}\n---\n# I\n` });
147
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
148
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-09-30T05:00:00.000Z', doc: 'workflow/intents/2026-09-30-l2.md', stage: 'approved', fingerprint: '7'.repeat(64), prev: 'draft', source: 'chat-delegated', batch: 'l2', seq: 1, of: 1 }) + '\n');
149
+ const r = runTool(root, '2026-09-30-l2', 'L2');
150
+ check('S11 起草门:L2 入口已确认但缺同名 spec → exit 2 + 缺 spec 提示',
151
+ r.status === 2 && /缺同名 spec/.test(r.stderr),
152
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
153
+ fs.rmSync(root, { recursive: true, force: true });
154
+ }
155
+ {
156
+ const root = mkws({
157
+ 'workflow/intents/2026-09-30-l2b.md': '---\n状态: approved\n级别: L2\n日期: 2026-01-01\n---\n# I\n',
158
+ 'workflow/specs/2026-09-30-l2b.md': '---\n状态: approved\n级别: L2\n日期: 2026-01-01\n---\n# S\n',
159
+ });
160
+ const r = runTool(root, '2026-09-30-l2b', 'L2');
161
+ const f = path.join(root, 'workflow', 'plans', '2026-09-30-l2b.md');
162
+ check('S12 起草门:L2 入口 + spec 均已确认(存量口径)→ 放行生成',
163
+ r.status === 0 && fs.existsSync(f),
164
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
165
+ fs.rmSync(root, { recursive: true, force: true });
166
+ }
167
+ {
168
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fill-plan-scratch-'));
169
+ const out = path.join(root, 'x.md');
170
+ const r = spawnSync(process.execPath, [CLI, '--topic', 'x', '--output', out], { cwd: root, encoding: 'utf8' });
171
+ check('S13 起草门:仓外草稿(无 workflow 段)→ 不拦 + 提示',
172
+ r.status === 0 && fs.existsSync(out) && /仓外草稿/.test(r.stderr),
173
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
174
+ fs.rmSync(root, { recursive: true, force: true });
175
+ }
176
+
177
+ {
178
+ // 场景 14:级别取入口 frontmatter(非 --level)——**门禁后入口 + 台账行**(早日期会走存量短路,
179
+ // 绕过级别取值逻辑;第三轮复核 P2-2 订正夹具)
180
+ const root = mkws({
181
+ 'workflow/intents/2026-09-30-lv.md': `---\n状态: approved\n级别: L2\n日期: 2026-09-30\n确认指纹: ${'a'.repeat(16)}\n---\n# I\n`,
182
+ 'workflow/specs/2026-09-30-lv.md': `---\n状态: approved\n级别: L2\n日期: 2026-09-30\n确认指纹: ${'b'.repeat(16)}\n---\n# S\n`,
183
+ });
184
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
185
+ const led = (doc, fp) => JSON.stringify({ ts: '2026-09-30T06:00:00.000Z', doc, stage: 'approved', fingerprint: fp, prev: 'draft', source: 'chat-delegated', batch: 'lv', seq: 1, of: 1 }) + '\n';
186
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'),
187
+ led('workflow/intents/2026-09-30-lv.md', 'a'.repeat(64)) + led('workflow/specs/2026-09-30-lv.md', 'b'.repeat(64)));
188
+ const r = runTool(root, '2026-09-30-lv', 'L1');
189
+ check('S14 级别取入口 frontmatter:入口 L2(台账行)+ spec 确认 → 传 --level L1 仍放行(不弱化前置)',
190
+ r.status === 0,
191
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
192
+ fs.rmSync(root, { recursive: true, force: true });
193
+ }
194
+ {
195
+ // 场景 15:入口 L1(门禁后 + 台账行)+ 传 --level L2 → 免 spec 档(级别以入口为准)
196
+ const root = mkws({ 'workflow/intents/2026-09-30-lv2.md': `---\n状态: approved\n级别: L1\n日期: 2026-09-30\n确认指纹: ${'c'.repeat(16)}\n---\n# I\n` });
197
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
198
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-09-30T06:10:00.000Z', doc: 'workflow/intents/2026-09-30-lv2.md', stage: 'approved', fingerprint: 'c'.repeat(64), prev: 'draft', source: 'chat-delegated', batch: 'lv2', seq: 1, of: 1 }) + '\n');
199
+ const r = runTool(root, '2026-09-30-lv2', 'L2');
200
+ check('S15 级别取入口 frontmatter:入口 L1 → 传 --level L2 不索要 spec、放行',
201
+ r.status === 0,
202
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
203
+ fs.rmSync(root, { recursive: true, force: true });
204
+ }
205
+
66
206
  console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
67
207
  process.exit(failCount ? 1 : 0);
@@ -1,20 +1,24 @@
1
1
  // flow-kit fill-spec:起草 spec 草稿(功能行为 / 数据流 / 系统改动 / 约束遵守映射 / 风险评估 共 5 节)
2
- // 用法:node .agents/scripts/fill-spec.mjs --topic "test" --output /tmp/x.md
2
+ // 用法:node .agents/scripts/fill-spec.mjs --topic "test" --output /tmp/x.md [--root <仓库根>]
3
+ // 起草门(2026-09-30 stage-gate-machine):output 落于工作区(路径含 workflow 段)时,须同主题入口已确认
4
+ // (判定口径见 stage-gates.mjs);未过 → 拒绝生成(exit 2);仓外草稿(无 workflow 段)不校验。
3
5
  import fs from 'node:fs';
4
6
  import path from 'node:path';
5
7
  import { ENUMS } from './workflow-enums.mjs';
8
+ import { draftGateFor, resolveWorkspace } from './stage-gates.mjs';
6
9
 
7
10
  const TODAY = new Date().toISOString().slice(0, 10);
8
11
  const LEVELS = ENUMS['level.all']; // 级别词表单源(.agents/workflow-enums.txt)
9
12
 
10
13
  function parseArgs(argv) {
11
- const out = { topic: null, level: 'L2', output: null };
14
+ const out = { topic: null, level: 'L2', output: null, root: null };
12
15
  for (let i = 0; i < argv.length; i++) {
13
16
  const a = argv[i];
14
17
  const next = () => { const v = argv[++i]; if (v === undefined) return null; return v; };
15
18
  if (a === '--topic') out.topic = next();
16
19
  else if (a === '--level') out.level = next();
17
20
  else if (a === '--output') out.output = next();
21
+ else if (a === '--root') out.root = next();
18
22
  }
19
23
  return out;
20
24
  }
@@ -43,8 +47,19 @@ export function renderSpec({ topic, level = 'L2', date = TODAY }) {
43
47
  const isMain = import.meta.url === `file:///${process.argv[1]}` || process.argv[1]?.endsWith('fill-spec.mjs');
44
48
  if (isMain) {
45
49
  const a = parseArgs(process.argv.slice(2));
46
- if (!a.topic || !a.output) fail('必填:--topic / --output;选填:--level(默认 L2)');
50
+ if (!a.topic || !a.output) fail('必填:--topic / --output;选填:--level(默认 L2)/ --root');
47
51
  if (!LEVELS.includes(a.level)) fail(`level 必须在 ${LEVELS.join('|')}:${a.level}`);
52
+ const ws = resolveWorkspace(a.output, a.root);
53
+ if (ws) {
54
+ const gate = draftGateFor('spec', ws.root, ws.base);
55
+ if (!gate.ok) {
56
+ console.error(`❌ 起草门未过(stage-gates):${gate.reason}`);
57
+ console.error(` ${gate.hint}——本份未生成(确认入口后重跑本命令)`);
58
+ process.exit(2);
59
+ }
60
+ } else {
61
+ console.error('ℹ️ 未定位到工作区(output 路径不含 workflow/ 段)——按仓外草稿处理,跳过起草门前置校验');
62
+ }
48
63
  const { body } = renderSpec(a);
49
64
  fs.mkdirSync(path.dirname(a.output), { recursive: true });
50
65
  fs.writeFileSync(a.output, body);
@@ -1,4 +1,9 @@
1
- // fill-spec.test.mjs — fill-spec 工具测试(2026-09-25 doc-fill-tools)
1
+ // fill-spec.test.mjs — fill-spec 工具测试(2026-09-25 doc-fill-tools;2026-09-30 stage-gate-machine 补起草门场景)
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
2
7
  import { renderSpec } from './fill-spec.mjs';
3
8
 
4
9
  let pass = 0, failCount = 0;
@@ -57,5 +62,197 @@ function check(name, cond, detail = '') {
57
62
  check('S7 L3 节确认与复核含独立复核提示', body.includes('L3 必须独立复核'));
58
63
  }
59
64
 
65
+ // ---- 场景 8-13:起草门(2026-09-30 stage-gate-machine)——output 落于工作区时须同主题入口已确认 ----
66
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
67
+ const CLI = path.join(SCRIPT_DIR, 'fill-spec.mjs');
68
+ const mkws = ({ entryFile = null, entryBody = null } = {}) => {
69
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fill-spec-gate-'));
70
+ fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
71
+ if (entryFile) {
72
+ fs.mkdirSync(path.join(root, 'workflow', entryFile.split('/')[0]), { recursive: true });
73
+ fs.writeFileSync(path.join(root, 'workflow', entryFile), entryBody);
74
+ }
75
+ return root;
76
+ };
77
+ const runTool = (root, base) => spawnSync(process.execPath,
78
+ [CLI, '--topic', base, '--output', path.join(root, 'workflow', 'specs', `${base}.md`)],
79
+ { cwd: root, encoding: 'utf8' });
80
+ {
81
+ const root = mkws();
82
+ const r = runTool(root, '2026-09-30-none');
83
+ check('S8 起草门:无同名入口 → exit 2 且零文件产出',
84
+ r.status === 2 && /起草门未过/.test(r.stderr) && !fs.existsSync(path.join(root, 'workflow', 'specs', '2026-09-30-none.md')),
85
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
86
+ fs.rmSync(root, { recursive: true, force: true });
87
+ }
88
+ {
89
+ const root = mkws({ entryFile: 'intents/2026-09-30-draft.md', entryBody: '---\n状态: draft\n级别: L2\n日期: 2026-09-30\n---\n# I\n' });
90
+ const r = runTool(root, '2026-09-30-draft');
91
+ check('S9 起草门:入口 draft → exit 2 + 提示状态',
92
+ r.status === 2 && /状态「draft」/.test(r.stderr),
93
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
94
+ fs.rmSync(root, { recursive: true, force: true });
95
+ }
96
+ {
97
+ // S9b 探索泳道(2026-09-30 hybrid-governance-explore-hardening):L0/L1 draft intent → 放行 spec 起草
98
+ // (「先起草后确认」;L2/L3 防御道维持入口确认前置——S9 照旧)
99
+ const root = mkws({ entryFile: 'intents/2026-09-30-exp.md', entryBody: '---\n状态: draft\n级别: L1\nrisk_level: L1\n日期: 2026-09-30\n---\n# I\n' });
100
+ const r = runTool(root, '2026-09-30-exp');
101
+ const f = path.join(root, 'workflow', 'specs', '2026-09-30-exp.md');
102
+ check('S9b 探索泳道:L1 入口 draft → 放行 spec 起草(先起草后确认)',
103
+ r.status === 0 && fs.existsSync(f) && fs.readFileSync(f, 'utf8').includes('状态: draft'),
104
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
105
+ fs.rmSync(root, { recursive: true, force: true });
106
+ }
107
+ {
108
+ const root = mkws({ entryFile: 'intents/2026-09-30-ok.md', entryBody: '---\n状态: approved\n级别: L1\n日期: 2026-01-01\n---\n# I\n' });
109
+ const r = runTool(root, '2026-09-30-ok');
110
+ const f = path.join(root, 'workflow', 'specs', '2026-09-30-ok.md');
111
+ check('S10 起草门:入口 approved(存量口径)→ 放行生成',
112
+ r.status === 0 && fs.existsSync(f) && fs.readFileSync(f, 'utf8').includes('状态: draft'),
113
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
114
+ fs.rmSync(root, { recursive: true, force: true });
115
+ }
116
+ {
117
+ const root = mkws({ entryFile: 'incidents/2026-09-30-inc.md', entryBody: '---\n状态: open\n级别: L2\n---\n# I\n\n## 时间线\n- 用户确认:过目\n' });
118
+ const r = runTool(root, '2026-09-30-inc');
119
+ check('S11 起草门:incident 入口(open + 用户确认留痕)→ 放行',
120
+ r.status === 0,
121
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
122
+ fs.rmSync(root, { recursive: true, force: true });
123
+ }
124
+ {
125
+ const root = mkws({ entryFile: 'incidents/2026-09-30-inc2.md', entryBody: '---\n状态: open\n级别: L2\n---\n# I\n' });
126
+ const r = runTool(root, '2026-09-30-inc2');
127
+ check('S12 起草门:incident 缺「用户确认」留痕 → exit 2',
128
+ r.status === 2 && /未过目确认/.test(r.stderr),
129
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
130
+ fs.rmSync(root, { recursive: true, force: true });
131
+ }
132
+ {
133
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fill-spec-scratch-'));
134
+ const out = path.join(root, 'x.md');
135
+ const r = spawnSync(process.execPath, [CLI, '--topic', 'x', '--output', out], { cwd: root, encoding: 'utf8' });
136
+ check('S13 起草门:仓外草稿(无 workflow 段)→ 不拦 + 提示',
137
+ r.status === 0 && fs.existsSync(out) && /仓外草稿/.test(r.stderr),
138
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
139
+ fs.rmSync(root, { recursive: true, force: true });
140
+ }
141
+
142
+ {
143
+ // 场景 14:入口 approved 但无台账行且日期 ≥ confirmDocsEffective → 拒(防手改状态冒充;复核 P2-4③)
144
+ const root = mkws({ entryFile: 'intents/2026-09-30-fake.md', entryBody: '---\n状态: approved\n级别: L1\n日期: 2026-09-30\n---\n# I\n' });
145
+ const r = runTool(root, '2026-09-30-fake');
146
+ check('S14 起草门:入口 approved 但无台账行(日期 ≥ 生效日)→ exit 2',
147
+ r.status === 2 && /无确认台账行/.test(r.stderr),
148
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
149
+ fs.rmSync(root, { recursive: true, force: true });
150
+ }
151
+
152
+ {
153
+ // N1(2026-09-30 增量复核):存量入口短路优先于级别校验——`流程: legacy` + 无级别 → 放行(不锁死)
154
+ const root = mkws({ entryFile: 'intents/2026-09-30-leg.md', entryBody: '---\n状态: approved\n日期: 2026-01-01\n流程: legacy\n---\n# I(存量、无级别)\n' });
155
+ const r = runTool(root, '2026-09-30-leg');
156
+ check('S15 存量短路:legacy 入口缺级别 → 放行(不被 fail-closed 锁死)',
157
+ r.status === 0 && fs.existsSync(path.join(root, 'workflow', 'specs', '2026-09-30-leg.md')),
158
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
159
+ fs.rmSync(root, { recursive: true, force: true });
160
+ }
161
+ {
162
+ // 对照:门禁后入口(日期 ≥ 生效日)+ **台账行**(第三轮复核 P1-2 订正:原夹具无台账行,
163
+ // 被上游「无确认台账行」拦下,从未进入级别校验——假绿)→ 须停在「缺合法级别」分支
164
+ const root = mkws({ entryFile: 'intents/2026-09-30-nolev.md', entryBody: `---\n状态: approved\n日期: 2026-09-30\n确认指纹: ${'d'.repeat(16)}\n---\n# I(门禁后、无级别)\n` });
165
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
166
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-09-30T07:00:00.000Z', doc: 'workflow/intents/2026-09-30-nolev.md', stage: 'approved', fingerprint: 'd'.repeat(64), prev: 'draft', source: 'chat-delegated', batch: 'nl', seq: 1, of: 1 }) + '\n');
167
+ const r = runTool(root, '2026-09-30-nolev');
168
+ check('S16 门禁后入口缺级别 → 拒,且理由为「缺合法级别」(断言理由 + 零产出,防上游分支/无关失败冒充)',
169
+ r.status === 2 && /缺合法「级别」字段/.test(r.stderr) && !fs.existsSync(path.join(root, 'workflow', 'specs', '2026-09-30-nolev.md')),
170
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
171
+ fs.rmSync(root, { recursive: true, force: true });
172
+ }
173
+ {
174
+ // N2 回归(第三轮复核 P1-1):模板时间线样例行必须真实可解析——照抄模板即应通过 incident 留痕判定
175
+ const tpl = fs.readFileSync(path.join(SCRIPT_DIR, '..', '..', 'workflow', 'incidents', '_TEMPLATE.md'), 'utf8');
176
+ const sample = tpl.split(/\r?\n/).find((l) => /^\s*[-*].*用户确认/.test(l)) || '';
177
+ const root = mkws({ entryFile: 'incidents/2026-09-30-tpl.md', entryBody: `---\n状态: open\n级别: L1\n---\n# I\n\n## 时间线\n${sample}\n` });
178
+ const r = runTool(root, '2026-09-30-tpl');
179
+ check('S18 模板样例可解析:照抄 _TEMPLATE.md 时间线样例行 → incident 入口放行',
180
+ r.status === 0 && sample !== '',
181
+ JSON.stringify({ status: r.status, sample, stderr: r.stderr }));
182
+ fs.rmSync(root, { recursive: true, force: true });
183
+ }
184
+
185
+ {
186
+ // 负例(第三轮复核 P1-1 放宽前缀后实证的假阳性):叙述句即便带标准日期前缀也不算过目留痕
187
+ const variants = ['- 2026-09-30 用户确认了方案(叙述)', '- 用户确认书已归档', '- <日期> 用户确认过细节'];
188
+ const results = variants.map((line) => {
189
+ const root = mkws({ entryFile: 'incidents/2026-09-30-neg.md', entryBody: `---\n状态: open\n级别: L1\n---\n# I\n\n## 时间线\n${line}\n` });
190
+ const r = runTool(root, '2026-09-30-neg');
191
+ fs.rmSync(root, { recursive: true, force: true });
192
+ return { line, status: r.status, stderr: r.stderr };
193
+ });
194
+ check('S19 负例:叙述句(含日期前缀)不算过目留痕 → 仍拒',
195
+ results.every((x) => x.status === 2 && /未过目确认/.test(x.stderr)),
196
+ JSON.stringify(results));
197
+ }
198
+ {
199
+ // 正面对照:既通过门、又产出文件——防「无关失败误绿」
200
+ const tpl = fs.readFileSync(path.join(SCRIPT_DIR, '..', '..', 'workflow', 'incidents', '_TEMPLATE.md'), 'utf8');
201
+ const sample = tpl.split(/\r?\n/).find((l) => /^\s*[-*].*用户确认/.test(l)) || '';
202
+ const root = mkws({ entryFile: 'incidents/2026-09-30-tpl2.md', entryBody: `---\n状态: open\n级别: L1\n---\n# I\n\n## 时间线\n${sample}\n` });
203
+ const r = runTool(root, '2026-09-30-tpl2');
204
+ const out = path.join(root, 'workflow', 'specs', '2026-09-30-tpl2.md');
205
+ check('S20 模板样例可解析(正面对照:exit 0 且产出 draft 文件)',
206
+ r.status === 0 && sample !== '' && fs.existsSync(out) && fs.readFileSync(out, 'utf8').includes('状态: draft'),
207
+ JSON.stringify({ status: r.status, sample, exists: fs.existsSync(out), stderr: r.stderr }));
208
+ fs.rmSync(root, { recursive: true, force: true });
209
+ }
210
+ {
211
+ // N1 存量短路(spec 档)——plan 门本体由 fill-plan.test / confirm-doc.test 覆盖
212
+ const root = mkws({ entryFile: 'intents/2026-09-30-legp.md', entryBody: '---\n状态: approved\n日期: 2026-01-01\n流程: legacy\n---\n# I(存量、无级别)\n' });
213
+ const r = runTool(root, '2026-09-30-legp');
214
+ check('S17 存量短路(spec 档):legacy 入口无级别 → 放行生成',
215
+ r.status === 0 && fs.existsSync(path.join(root, 'workflow', 'specs', '2026-09-30-legp.md')),
216
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
217
+ fs.rmSync(root, { recursive: true, force: true });
218
+ }
219
+
220
+ // ---- 形态矩阵(2026-09-30 四/五轮复核收敛):8 正例 + 5 负例一次性钉死 ----
221
+ // 每轮补丁式改正则都只修一半(放宽前缀 → 叙述句假阳性;加词边界 → 加粗围栏假阴性),故穷举形态:
222
+ // 任何后续正则改动若破坏任一正例或放行任一负例,本矩阵立即翻红。
223
+ {
224
+ const POSITIVE = [
225
+ '- 用户确认:草稿过目通过',
226
+ '- **用户确认**:草稿过目通过',
227
+ '- 2026-09-30 用户确认:草稿过目通过',
228
+ '- <日期> 用户确认:草稿过目通过',
229
+ '- 2026-09-30 **用户确认**:草稿过目通过',
230
+ '- <日期> **用户确认**:草稿过目通过',
231
+ '- 用户确认',
232
+ '- **用户确认**',
233
+ ];
234
+ const NEGATIVE = [
235
+ '- 2026-09-30 用户确认了方案(叙述)',
236
+ '- 用户确认书已归档',
237
+ '- <日期> 用户确认过细节',
238
+ '- 用户确认。',
239
+ '- 待用户确认:稍后处理',
240
+ ];
241
+ const run = (line) => {
242
+ const root = mkws({ entryFile: 'incidents/2026-09-30-mx.md', entryBody: `---\n状态: open\n级别: L1\n---\n# I\n\n## 时间线\n${line}\n` });
243
+ const r = runTool(root, '2026-09-30-mx');
244
+ fs.rmSync(root, { recursive: true, force: true });
245
+ return r.status;
246
+ };
247
+ const posBad = POSITIVE.filter((l) => run(l) !== 0);
248
+ const negBad = NEGATIVE.filter((l) => run(l) !== 2);
249
+ check('S21 形态矩阵·正例全放行(8 形态,含词前/词后加粗围栏)[须与 S22 负例配对——单条不承重]',
250
+ posBad.length === 0,
251
+ JSON.stringify({ failedLines: posBad }));
252
+ check('S22 形态矩阵·负例全拒绝(5 形态,叙述/无冒号/待办前缀)[须与 S21 正例配对——单条不承重]',
253
+ negBad.length === 0,
254
+ JSON.stringify({ failedLines: negBad }));
255
+ }
256
+
60
257
  console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
61
258
  process.exit(failCount ? 1 : 0);
@@ -61,7 +61,7 @@ const PAIRS = [
61
61
  { doctor: '3', cl: null, note: 'git 仓库与钩子——装户体检独有' },
62
62
  { doctor: '4', cl: null, note: 'kit.json managed 台账——装户体检独有' },
63
63
  { doctor: '4.5', cl: null, note: '台账覆盖率——装户体检独有(盘上有 managed 类文件未登记,2026-09-26 managed-ledger-adopt)' },
64
- { doctor: '6.5', cl: null, note: 'delegations 台账结构——装户体检独有' },
64
+ { doctor: '6.5', cl: null, note: 'delegations 台账结构 + 快照新鲜度——装户体检独有' },
65
65
  { doctor: '6.6', cl: null, note: 'owned 漂移——装户体检独有' },
66
66
  { doctor: '6.7', cl: null, note: '跨宿主薄适配——装户体检独有' },
67
67
  { doctor: '6.8', cl: null, note: 'workflows 编排脚本 lint——装户体检独有' },
@@ -81,6 +81,9 @@ const PAIRS = [
81
81
  { doctor: '7', cl: '14', note: '经 §7 整体运行覆盖' },
82
82
  { doctor: '7', cl: '15', note: '经 §7 整体运行覆盖' },
83
83
  { doctor: '7', cl: '16', note: '经 §7 整体运行覆盖(量化断言指标签名对账,2026-09-28 claim-exceeds-fix)' },
84
+ { doctor: '7', cl: '17', note: '经 §7 整体运行覆盖(发版提交树上仍未收口的 intent/spec/plan)' },
85
+ { doctor: '7', cl: '18', note: '经 §7 整体运行覆盖(委派台账对账)' },
86
+ { doctor: '7', cl: '19', note: '经 §7 整体运行覆盖(逐阶段审计——起草先于入口确认 / 台账审批顺序倒置)' },
84
87
  ];
85
88
 
86
89
  // gateChecklist:按登记表对照两侧检查项(pure function;pairs 可注入供测试)
@@ -177,8 +177,12 @@ function previewDiff(before, after) {
177
177
  if (added.length > 10) console.log(` + …(其余 ${added.length - 10} 行略)`);
178
178
  }
179
179
 
180
+ // 行尾归一比较(2026-09-30:fresh clone 检出 CRLF、生成段恒 LF——直接字符串相等会在克隆环境假阳性
181
+ // 「漂移」(doctor §6 与 check-loop 检查 11 均经 --check 判定);仅内容差异才算漂移)
182
+ const normEol = (s) => String(s).replace(/\r\n/g, '\n');
183
+
180
184
  if (CHECK) {
181
- if (out === existing) {
185
+ if (normEol(out) === normEol(existing)) {
182
186
  console.log(`✅ workflow/INDEX.md 与磁盘一致(${summary})`);
183
187
  process.exit(0);
184
188
  }
@@ -189,7 +193,7 @@ if (CHECK) {
189
193
 
190
194
  if (DRY) {
191
195
  console.log(`🔎 --dry-run:未写盘(${summary})`);
192
- if (out === existing) console.log('• workflow/INDEX.md:无变化');
196
+ if (normEol(out) === normEol(existing)) console.log('• workflow/INDEX.md:无变化');
193
197
  else previewDiff(existing, out);
194
198
  } else {
195
199
  fs.writeFileSync(INDEX_P, out, 'utf8');
@@ -85,6 +85,18 @@ const run = (root, args = []) => spawnSync(process.execPath, [GEN, ...args], { c
85
85
  fs.rmSync(root, { recursive: true, force: true });
86
86
  }
87
87
 
88
+ // ---- 场景 2b:CRLF 检出形态(fresh clone)--check 不误报(2026-09-30 行尾归一)----
89
+ {
90
+ const root = mkfix();
91
+ run(root);
92
+ const ip = path.join(root, 'workflow', 'INDEX.md');
93
+ fs.writeFileSync(ip, fs.readFileSync(ip, 'utf8').replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'), 'utf8');
94
+ const crlf = run(root, ['--check']);
95
+ check('场景 2b:INDEX 为 CRLF 检出形态 → --check exit 0(行尾归一,不假阳性)',
96
+ crlf.status === 0, `exit=${crlf.status}\n${crlf.stdout}${crlf.stderr}`);
97
+ fs.rmSync(root, { recursive: true, force: true });
98
+ }
99
+
88
100
  // ---- 场景 3:缺 `模块:` 容忍显示 —(不报错、不阻断)----
89
101
  {
90
102
  const root = mkfix();