agentic-flow-kit 0.7.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 (148) 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/{build.md → wf-build.md} +3 -4
  62. package/modules/hosts/opencode/commands/{design.md → wf-design.md} +2 -2
  63. package/modules/hosts/opencode/commands/{maintain.md → wf-maintain.md} +1 -1
  64. package/modules/hosts/opencode/commands/{plan.md → wf-plan.md} +3 -3
  65. package/modules/hosts/opencode/commands/{review.md → wf-review.md} +1 -1
  66. package/modules/hosts/opencode/commands/{source-sync-check.md → wf-source-sync-check.md} +1 -1
  67. package/modules/hosts/opencode/commands/{sync-hosts.md → wf-sync-hosts.md} +1 -1
  68. package/modules/hosts/opencode/commands/{test.md → wf-test.md} +3 -1
  69. package/modules/hosts/trae/agents/implementer.md +1 -1
  70. package/modules/hosts/trae/agents/independent-reviewer.md +1 -1
  71. package/modules/hosts/trae/agents/ui-verifier.md +1 -1
  72. package/modules/hosts/trae/commands/wf-build.md +3 -4
  73. package/modules/hosts/trae/commands/wf-design.md +2 -2
  74. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  75. package/modules/hosts/trae/commands/wf-plan.md +3 -3
  76. package/modules/hosts/trae/commands/wf-review.md +1 -1
  77. package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -1
  78. package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
  79. package/modules/hosts/trae/commands/wf-test.md +3 -1
  80. package/modules/hosts/zcode/agents/implementer.md +1 -1
  81. package/modules/hosts/zcode/agents/independent-reviewer.md +1 -1
  82. package/modules/hosts/zcode/agents/ui-verifier.md +1 -1
  83. package/package.json +1 -1
  84. package/src/cli.mjs +16 -4
  85. package/src/doctor.mjs +58 -5
  86. package/src/fresh-init.test.mjs +36 -0
  87. package/src/gates.test.mjs +126 -0
  88. package/src/init.mjs +8 -1
  89. package/src/init.test.mjs +7 -1
  90. package/src/profiles.mjs +10 -4
  91. package/src/run-tests.mjs +3 -0
  92. package/src/stack-profile.test.mjs +89 -0
  93. package/src/sync-hosts.mjs +12 -6
  94. package/src/sync-hosts.test.mjs +40 -17
  95. package/templates/AGENTS.md +4 -4
  96. package/templates/_agents/board/index.html +3 -3
  97. package/templates/_agents/commands/build.md +4 -5
  98. package/templates/_agents/commands/design.md +4 -4
  99. package/templates/_agents/commands/maintain.md +1 -1
  100. package/templates/_agents/commands/new-task.md +13 -2
  101. package/templates/_agents/commands/plan.md +4 -4
  102. package/templates/_agents/commands/review.md +1 -1
  103. package/templates/_agents/commands/source-sync-check.md +1 -1
  104. package/templates/_agents/commands/sync-hosts.md +1 -1
  105. package/templates/_agents/commands/test.md +4 -2
  106. package/templates/_agents/hooks/commit-check.cjs +45 -24
  107. package/templates/_agents/lane-surfaces.txt +14 -0
  108. package/templates/_agents/roles/implementer.md +1 -1
  109. package/templates/_agents/roles/independent-reviewer.md +1 -1
  110. package/templates/_agents/roles/ui-verifier.md +1 -1
  111. package/templates/_agents/scripts/check-lane-surface.mjs +164 -0
  112. package/templates/_agents/scripts/check-lane-surface.test.mjs +200 -0
  113. package/templates/_agents/scripts/check-ledger.mjs +65 -0
  114. package/templates/_agents/scripts/check-ledger.test.mjs +127 -0
  115. package/templates/_agents/scripts/check-loop-rev.test.mjs +337 -0
  116. package/templates/_agents/scripts/check-loop.mjs +460 -268
  117. package/templates/_agents/scripts/check-loop.test.mjs +677 -17
  118. package/templates/_agents/scripts/check-metric-claims.mjs +275 -0
  119. package/templates/_agents/scripts/commit-check-trigger.test.mjs +64 -1
  120. package/templates/_agents/scripts/confirm-doc.mjs +159 -15
  121. package/templates/_agents/scripts/confirm-doc.test.mjs +362 -10
  122. package/templates/_agents/scripts/doctor.test.mjs +105 -4
  123. package/templates/_agents/scripts/fill-intent.mjs +8 -7
  124. package/templates/_agents/scripts/fill-intent.test.mjs +12 -3
  125. package/templates/_agents/scripts/fill-plan.mjs +27 -11
  126. package/templates/_agents/scripts/fill-plan.test.mjs +148 -8
  127. package/templates/_agents/scripts/fill-spec.mjs +18 -3
  128. package/templates/_agents/scripts/fill-spec.test.mjs +198 -1
  129. package/templates/_agents/scripts/gate-checklist.mjs +4 -1
  130. package/templates/_agents/scripts/gen-workflow-index.mjs +6 -2
  131. package/templates/_agents/scripts/gen-workflow-index.test.mjs +12 -0
  132. package/templates/_agents/scripts/policy.mjs +46 -0
  133. package/templates/_agents/scripts/solidify-task.mjs +101 -0
  134. package/templates/_agents/scripts/stage-gates.mjs +256 -0
  135. package/templates/_agents/scripts/trust-mode.mjs +67 -0
  136. package/templates/_agents/scripts/wf-journal.mjs +71 -2
  137. package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
  138. package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
  139. package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
  140. package/templates/_agents/workflows/pipeline-closing.md +1 -1
  141. package/templates/_githooks/pre-commit +49 -2
  142. package/templates/_githooks/pre-push +56 -12
  143. package/templates/workflow/README.md +22 -3
  144. package/templates/workflow/incidents/_TEMPLATE.md +4 -0
  145. package/templates/workflow/intents/_TEMPLATE.md +1 -0
  146. /package/modules/hosts/opencode/commands/{deploy.md → wf-deploy.md} +0 -0
  147. /package/modules/hosts/opencode/commands/{gate-checklist.md → wf-gate-checklist.md} +0 -0
  148. /package/modules/hosts/opencode/commands/{new-task.md → wf-new-task.md} +0 -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
 
@@ -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);