agentic-flow-kit 1.7.16 → 1.8.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 (160) hide show
  1. package/README.md +11 -7
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-maintain.md +1 -1
  4. package/modules/hosts/claude/commands/wf-new-task.md +9 -8
  5. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  6. package/modules/hosts/claude/commands/wf-review.md +1 -1
  7. package/modules/hosts/claude/commands/wf-test.md +1 -1
  8. package/modules/hosts/codex/commands/wf-build.md +1 -1
  9. package/modules/hosts/codex/commands/wf-maintain.md +1 -1
  10. package/modules/hosts/codex/commands/wf-new-task.md +9 -8
  11. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  12. package/modules/hosts/codex/commands/wf-review.md +1 -1
  13. package/modules/hosts/codex/commands/wf-test.md +1 -1
  14. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  15. package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
  16. package/modules/hosts/cursor/commands/wf-new-task.md +9 -8
  17. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  18. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  19. package/modules/hosts/cursor/commands/wf-test.md +1 -1
  20. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  21. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  22. package/modules/hosts/opencode/commands/wf-new-task.md +9 -8
  23. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  24. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  25. package/modules/hosts/opencode/commands/wf-test.md +1 -1
  26. package/modules/hosts/trae/commands/wf-build.md +1 -1
  27. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  28. package/modules/hosts/trae/commands/wf-new-task.md +9 -8
  29. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  30. package/modules/hosts/trae/commands/wf-review.md +1 -1
  31. package/modules/hosts/trae/commands/wf-test.md +1 -1
  32. package/package.json +1 -1
  33. package/src/cli.mjs +6 -2
  34. package/src/doctor.mjs +11 -2
  35. package/src/executor.mjs +25 -8
  36. package/src/executor.test.mjs +4 -1
  37. package/src/fresh-init.test.mjs +1 -1
  38. package/src/init.mjs +20 -3
  39. package/src/profiles.mjs +7 -3
  40. package/src/rule-budget.test.mjs +2 -2
  41. package/src/stack-profile.test.mjs +6 -0
  42. package/src/validate.mjs +6 -1
  43. package/src/validate.test.mjs +1 -1
  44. package/templates/.gitattributes +4 -1
  45. package/templates/AGENTS.md +2 -2
  46. package/templates/_agents/commands/build.md +1 -1
  47. package/templates/_agents/commands/maintain.md +1 -1
  48. package/templates/_agents/commands/new-task.md +9 -8
  49. package/templates/_agents/commands/plan.md +1 -1
  50. package/templates/_agents/commands/review.md +1 -1
  51. package/templates/_agents/commands/test.md +1 -1
  52. package/templates/_agents/engine-lock.json +130 -42
  53. package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
  54. package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
  55. package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
  56. package/templates/_agents/lane-surfaces.txt +4 -1
  57. package/templates/_agents/level-hints.txt +1 -1
  58. package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
  59. package/templates/_agents/mechanism-registry.md +27 -0
  60. package/templates/_agents/rule-budgets.txt +2 -2
  61. package/templates/_agents/scripts/agg-delegations.cjs +21 -3
  62. package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
  63. package/templates/_agents/scripts/agg-judgments.mjs +80 -0
  64. package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
  65. package/templates/_agents/scripts/archive-sweep.mjs +233 -0
  66. package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
  67. package/templates/_agents/scripts/check-attribution.mjs +37 -0
  68. package/templates/_agents/scripts/check-convergence.mjs +119 -0
  69. package/templates/_agents/scripts/check-engine-integrity.mjs +4 -1
  70. package/templates/_agents/scripts/check-funnel.mjs +56 -0
  71. package/templates/_agents/scripts/check-hygiene.mjs +5 -5
  72. package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
  73. package/templates/_agents/scripts/check-lane-surface.mjs +2 -2
  74. package/templates/_agents/scripts/check-lane-surface.test.mjs +5 -5
  75. package/templates/_agents/scripts/check-ledger-invariant.mjs +403 -152
  76. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +433 -3
  77. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -5
  78. package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
  79. package/templates/_agents/scripts/check-loop.mjs +215 -34
  80. package/templates/_agents/scripts/check-loop.test.mjs +570 -49
  81. package/templates/_agents/scripts/check-prejudgment.mjs +59 -0
  82. package/templates/_agents/scripts/check-registry.mjs +45 -0
  83. package/templates/_agents/scripts/check-scope.mjs +96 -0
  84. package/templates/_agents/scripts/check-scope.test.mjs +96 -0
  85. package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
  86. package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
  87. package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
  88. package/templates/_agents/scripts/compact-ledgers.mjs +3 -3
  89. package/templates/_agents/scripts/confirm-doc.mjs +114 -38
  90. package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
  91. package/templates/_agents/scripts/edit-face-check.mjs +5 -3
  92. package/templates/_agents/scripts/fill-intent.mjs +5 -3
  93. package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
  94. package/templates/_agents/scripts/fill-plan.mjs +10 -6
  95. package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
  96. package/templates/_agents/scripts/fill-spec.mjs +6 -3
  97. package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
  98. package/templates/_agents/scripts/fixture-scripts.mjs +2 -2
  99. package/templates/_agents/scripts/fixture-scripts.test.mjs +6 -1
  100. package/templates/_agents/scripts/gate-checklist.mjs +6 -0
  101. package/templates/_agents/scripts/gate-seg.mjs +1 -1
  102. package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
  103. package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
  104. package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
  105. package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
  106. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -3
  107. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
  108. package/templates/_agents/scripts/gen-workflow-metrics.mjs +3 -3
  109. package/templates/_agents/scripts/git-core.mjs +45 -0
  110. package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
  111. package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
  112. package/templates/_agents/scripts/kb-search.mjs +19 -4
  113. package/templates/_agents/scripts/kb-search.test.mjs +33 -0
  114. package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
  115. package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
  116. package/templates/_agents/scripts/ledger-shards.mjs +307 -0
  117. package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
  118. package/templates/_agents/scripts/level-criteria.mjs +9 -10
  119. package/templates/_agents/scripts/level-criteria.test.mjs +7 -5
  120. package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
  121. package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
  122. package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
  123. package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
  124. package/templates/_agents/scripts/pipeline-run.mjs +6 -22
  125. package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
  126. package/templates/_agents/scripts/policy.mjs +28 -0
  127. package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
  128. package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
  129. package/templates/_agents/scripts/rewrite-file.mjs +133 -0
  130. package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
  131. package/templates/_agents/scripts/rule-budget.mjs +83 -0
  132. package/templates/_agents/scripts/run-tests.mjs +99 -0
  133. package/templates/_agents/scripts/run-tests.test.mjs +105 -0
  134. package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
  135. package/templates/_agents/scripts/stage-gates.mjs +34 -12
  136. package/templates/_agents/scripts/trust-mode.mjs +4 -4
  137. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  138. package/templates/_agents/scripts/verify.mjs +135 -58
  139. package/templates/_agents/scripts/verify.test.mjs +156 -47
  140. package/templates/_agents/scripts/wf-journal.mjs +8 -4
  141. package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
  142. package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
  143. package/templates/_agents/scripts-test-exempt.txt +1 -0
  144. package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
  145. package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
  146. package/templates/_agents/workflow-enums.txt +8 -1
  147. package/templates/_githooks/pre-commit +110 -90
  148. package/templates/_githooks/pre-merge-commit +27 -23
  149. package/templates/_githooks/pre-push +4 -3
  150. package/templates/_github/workflows/kit-ci.yml +4 -3
  151. package/templates/workflow/README.md +14 -7
  152. package/templates/workflow/incidents/_TEMPLATE.md +4 -1
  153. package/templates/workflow/intents/_TEMPLATE.md +4 -2
  154. package/templates/workflow/papercuts.md +2 -0
  155. package/templates/workflow/plans/_TEMPLATE.md +10 -5
  156. package/templates/workflow/regression-checklist.md +1 -0
  157. package/templates/workflow/specs/_TEMPLATE.md +17 -1
  158. package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
  159. package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
  160. package/templates/_agents/scripts/rule-budget.sh +0 -74
@@ -71,13 +71,31 @@ function check(name, cond, detail = '') {
71
71
 
72
72
  // ---- 场景 8:risk_level 风险泳道字段(混合治理,= 级别,全枚举落 frontmatter)----
73
73
  {
74
- for (const level of ['L0', 'L1', 'L2', 'L3']) {
74
+ for (const level of ['L1', 'L2', 'L3']) { // L0 已退役——L0 现走 renderIntent 抛「level 不在枚举」(S8b)
75
75
  const { body } = renderIntent({ topic: 't', module: 'pipeline', level });
76
76
  const fm = body.match(/^---([\s\S]*?)---/)[1];
77
77
  check('S8 risk_level=' + level + ' 写入 frontmatter', new RegExp('^risk_level:\\s*' + level + '$', 'm').test(fm));
78
78
  }
79
79
  }
80
80
 
81
+ // ---- 场景 8b:L0 已退役(2026-10-13 自源仓收敛系列 1 回灌)→ renderIntent fail-loud ----
82
+ {
83
+ let threw = '';
84
+ try { renderIntent({ topic: 't', module: 'pipeline', level: 'L0' }); } catch (e) { threw = e.message; }
85
+ check('S8b level=L0 → 抛 level 不在枚举 L1|L2|L3', threw.includes('level 不在枚举 L1|L2|L3'), threw);
86
+ }
87
+
88
+ // ---- 场景 9:预判键 scaffold(2026-10-13 自 Shipyard §25 prejudgment-ledger 回灌)----
89
+ // intents≡require 恒真;level 与「级别」双写同值——与路由预判一致性检查对账零 WARN(advisory 灰度)
90
+ {
91
+ for (const level of ['L1', 'L2', 'L3']) { // L0 已退役——L0 现走 renderIntent 抛「level 不在枚举」(S8b)
92
+ const { body } = renderIntent({ topic: 't', module: 'pipeline', level });
93
+ const fm = body.match(/^---([\s\S]*?)---/)[1];
94
+ check('S9 预判=' + level + ' scaffold(kind=require level=级别)',
95
+ new RegExp('^预判:\\s*kind=require level=' + level + '$', 'm').test(fm));
96
+ }
97
+ }
98
+
81
99
 
82
100
  // ---- 历史坑注入 kbHits(2026-10-09 engine-quality-round3 W2)----
83
101
  {
@@ -1,7 +1,8 @@
1
1
  // flow-kit fill-plan:起草 plan 草稿(L1 Quick-Plan 三节 / L2-L3 完整 4 节)
2
2
  // 用法:node .agents/scripts/fill-plan.mjs --level L1 --topic "test" --output /tmp/x.md [--root <仓库根>]
3
3
  // L1:改动方案 + 约束与风险 + 验证计划(Quick-Plan 必填三节)
4
- // L2/L3:改动方案 + 任务拆解 + 执行顺序 + 验证计划
4
+ // L1 直达(2026-10-13 自 Shipyard 回灌):本 plan 即入口、不另立 intent;无同名 intent/incident 时起草门
5
+ // 放行(--level L1)。修复类仍可另立 incident。L2/L3:改动方案 + 任务拆解 + 执行顺序 + 验证计划
5
6
  // 起草门(2026-09-30 stage-gate-machine):output 落于工作区(路径含 workflow 段)时,须同主题入口已确认,
6
7
  // 且入口级别(取入口 frontmatter,非 --level)为 L2/L3 时同名 spec 已确认;未过 → 拒绝生成(exit 2)。
7
8
  import fs from 'node:fs';
@@ -27,14 +28,14 @@ function parseArgs(argv) {
27
28
  function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
28
29
 
29
30
  const L1_SECTIONS = [
30
- { title: '改动方案', hint: '- <文件/组件>:<做什么;判据细节直接写进条目,如「L771 message.success 改『更新成功』」>' },
31
+ { title: '改动方案', hint: '- <文件/组件>:<做什么;判据细节直接写进条目,如「L771 message.success 改『更新成功』」>\n\n> build.md 确认的改动清单确认后回填本节落盘(对话留痕仅供当次)——改动面 + 任务勾选态 + git status = 会话重入三锚(2026-10-11 session-context)。' },
31
32
  { title: '约束与风险', hint: '- 约束:<对照 AGENTS.md 红线,说明如何满足;不许动什么>\n- 风险:<潜在影响 / 破坏性风险>' },
32
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 + 配置开关预案>' },
33
34
  ];
34
35
 
35
36
  const FULL_SECTIONS = [
36
37
  L1_SECTIONS[0],
37
- { title: '任务拆解(L2/L3 必填;L1 仅多文件多步骤时用,单任务微改动删本节)', hint: '1. <任务>\n - 判据:<怎样算完成;尽量对应一个测试或可复现操作>\n - 风险:低 / 中 / 高(<原因>)\n2. <任务>\n - 判据:…\n - 风险:…' },
38
+ { title: '任务拆解(L2/L3 必填;L1 仅多文件多步骤时用,单任务微改动删本节)', hint: '- [ ] 1. <任务>\n - 判据:<怎样算完成;尽量对应一个测试或可复现操作>\n - 风险:低 / 中 / 高(<原因>)\n - 留痕:(勾选时回填 commit SHA / 测试名 / 实测输出)\n- [ ] 2. <任务>\n - 判据:…\n - 风险:…\n - 留痕:…\n\n> 任务即勾选态(2026-10-11 session-context):完成一项立刻勾选并回填留痕;压缩 / 新会话续作先读本表勾选态 + git status 重建进度,禁凭记忆补勾。' },
38
39
  { title: '执行顺序(L2/L3 必填;L1 单文件微改动删本节)', hint: '<1 → 2 → 3;标注依赖关系>' },
39
40
  L1_SECTIONS[2],
40
41
  ];
@@ -43,7 +44,10 @@ export function renderPlan({ topic, level = 'L1' }) {
43
44
  if (!LEVELS.includes(level)) throw new Error('level 不在枚举 ' + LEVELS.join('|') + ':' + level);
44
45
  const sections = level === 'L1' ? L1_SECTIONS : FULL_SECTIONS;
45
46
  const fm = '---\n状态: draft\n级别: ' + level + '\n模块: pipeline\n---\n';
46
- 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';
47
+ const entryLine = level === 'L1'
48
+ ? '对应入口:本 plan(L1 不再另立 intent)。修复类另保留 ../incidents/YYYY-MM-DD-<主题>.md\n对应 spec:省略(仅 L2/L3)\n\n'
49
+ : '对应入口:../intents/YYYY-MM-DD-<主题>.md 或 ../incidents/YYYY-MM-DD-<主题>.md(保留实际一项)\n对应 spec:../specs/YYYY-MM-DD-<主题>.md\n\n';
50
+ let body = '# PLAN — ' + topic + '\n\n<!-- 与 intents/ 或 incidents/ 下同名入口文档配对;L2/L3 必须先有 ../specs/ 同名 spec 确认通过;AI 起草、用户对话内确认后开工 -->\n<!-- frontmatter 受限子集(2026-09-13):状态∈draft/approved/done/superseded/cancelled;级别∈L1/L2/L3(2026-10-13 L0 退役);正文不再写状态/级别行 -->\n<!-- L1 快车道 (Quick-Plan)(2026-09-30 更新):合并 Spec 与 Plan 核心。必填三节:[改动方案, 约束与风险, 验证计划]。仅多文件多步骤时保留任务拆解/执行顺序。L1 直达(2026-10-13 自 Shipyard 回灌):本文件即入口,不另立 intent -->\n\n' + entryLine;
47
51
  for (const { title, hint } of sections) {
48
52
  body += '## ' + title + '\n\n' + hint + '\n\n';
49
53
  }
@@ -58,7 +62,7 @@ if (isMain) {
58
62
  if (!LEVELS.includes(a.level)) fail('level 必须在 ' + LEVELS.join('|') + ':' + a.level);
59
63
  const ws = resolveWorkspace(a.output, a.root);
60
64
  if (ws) {
61
- const gate = draftGateFor('plan', ws.root, ws.base);
65
+ const gate = draftGateFor('plan', ws.root, ws.base, { level: a.level });
62
66
  if (!gate.ok) {
63
67
  console.error('❌ 起草门未过(stage-gates):' + gate.reason);
64
68
  console.error(' ' + gate.hint + '——本份未生成(确认前置后重跑本命令)');
@@ -70,6 +74,6 @@ if (isMain) {
70
74
  const { body } = renderPlan(a);
71
75
  fs.mkdirSync(path.dirname(a.output), { recursive: true });
72
76
  fs.writeFileSync(a.output, body);
73
- console.log('✅ 已生成 plan 草稿:' + a.output + '(状态 draft / 级别 ' + a.level + ' / ' + (a.level === 'L1' ? 'Quick-Plan 三节' : '完整 4 节') + ')');
77
+ console.log('✅ 已生成 plan 草稿:' + a.output + '(状态 draft / 级别 ' + a.level + ' / ' + (a.level === 'L1' ? 'Quick-Plan 三节(plan 即入口)' : '完整 4 节') + ')');
74
78
  }
75
79
  export default renderPlan;
@@ -31,6 +31,10 @@ function check(name, cond, detail = '') {
31
31
  check('S2 L2 含节 ## 执行顺序', body.includes('## 执行顺序'));
32
32
  check('S2 L2 含节 ## 验证计划', body.includes('## 验证计划'));
33
33
  check('S2 L2 共 5 节(4 节 + 确认与复核)', sections.length === 5, '实际 ' + sections.length + ' 节');
34
+ // 任务即勾选态(2026-10-13 自 Shipyard 回灌 session-context)
35
+ check('S2 任务即勾选态:复选框编号形态', /- \[ \] 1\. <任务>/.test(body));
36
+ check('S2 任务即勾选态:留痕行', body.includes('留痕:(勾选时回填 commit SHA / 测试名 / 实测输出)'));
37
+ check('S2 任务即勾选态:三锚注释', body.includes('会话重入三锚'));
34
38
  }
35
39
 
36
40
  // ---- 场景 3:L3 与 L2 同结构 ----
@@ -86,10 +90,22 @@ const runTool = (root, base, level) => spawnSync(process.execPath,
86
90
  [CLI, '--topic', base, '--level', level, '--output', path.join(root, 'workflow', 'plans', `${base}.md`)],
87
91
  { cwd: root, encoding: 'utf8' });
88
92
  {
93
+ // S8(2026-10-13 自 Shipyard 回灌 L1 即入口):无同名入口 + --level L1 → plan 自己就是入口,放行生成;
94
+ // 对照 S8b:L2 无同名入口仍拒(直达语义只对 L1 开)。
89
95
  const root = mkws();
90
96
  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')),
97
+ const f = path.join(root, 'workflow', 'plans', '2026-09-30-none.md');
98
+ check('S8 L1 即入口:无同名入口 + --level L1 → 放行生成(plan 即入口)',
99
+ r.status === 0 && fs.existsSync(f)
100
+ && fs.readFileSync(f, 'utf8').includes('对应入口:本 plan(L1 不再另立 intent)'),
101
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
102
+ fs.rmSync(root, { recursive: true, force: true });
103
+ }
104
+ {
105
+ const root = mkws();
106
+ const r = runTool(root, '2026-09-30-none2', 'L2');
107
+ check('S8b 起草门:无同名入口 + --level L2 → exit 2 且零文件产出(直达仅限 L1)',
108
+ r.status === 2 && /起草门未过/.test(r.stderr) && !fs.existsSync(path.join(root, 'workflow', 'plans', '2026-09-30-none2.md')),
93
109
  JSON.stringify({ status: r.status, stderr: r.stderr }));
94
110
  fs.rmSync(root, { recursive: true, force: true });
95
111
  }
@@ -113,7 +129,7 @@ const runTool = (root, base, level) => spawnSync(process.execPath,
113
129
  fs.rmSync(root, { recursive: true, force: true });
114
130
  }
115
131
  {
116
- // S10b 探索泳道(2026-09-30 hybrid-governance-explore-hardening):L0/L1 draft intent → 放行 plan 起草
132
+ // S10b 探索泳道(2026-09-30 hybrid-governance-explore-hardening):L1 draft intent → 放行 plan 起草
117
133
  // (「先起草后确认」;收口由 check-loop --hardening 加固门兜底)
118
134
  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
135
  const r = runTool(root, '2026-09-30-exp', 'L1');
@@ -1,4 +1,4 @@
1
- // flow-kit fill-spec:起草 spec 草稿(功能行为 / 数据流 / 系统改动 / 约束遵守映射 / 风险评估 共 5 节)
1
+ // flow-kit fill-spec:起草 spec 草稿(功能行为 / 数据流 / 系统改动 / 约束遵守映射 / 验收判据 / 风险评估 共 6 节)
2
2
  // 用法:node .agents/scripts/fill-spec.mjs --topic "test" --output /tmp/x.md [--root <仓库根>]
3
3
  // 起草门(2026-09-30 stage-gate-machine):output 落于工作区(路径含 workflow 段)时,须同主题入口已确认
4
4
  // (判定口径见 stage-gates.mjs);未过 → 拒绝生成(exit 2);仓外草稿(无 workflow 段)不校验。
@@ -30,15 +30,18 @@ const SECTIONS = [
30
30
  { title: '数据流', hint: '<请求→处理→持久化→回应的端到端路径;标出各环节涉及的服务 / 表 / 缓存 / 第三方调用>' },
31
31
  { title: '系统改动', hint: '<分模块列出本 spec 引入的所有改动;含新增 / 修改 / 删除;与 plan §改动面 一一对应>' },
32
32
  { title: '约束遵守映射', hint: '<逐条对照根 AGENTS.md 与目录级 AGENTS.md(如有)的红线 / 约束,说明本 spec 如何满足;逐条回应而非简单列举>' },
33
+ { title: '验收判据', hint: '<逐条登记可观察行为 × 证据(存在的文件路径,或全量门命令且 workflow/verify-evidence.md 有不早于本 spec 日期的全绿行)。人工走查状态写「人工」、证据留空(截图等图片路径不认;需留痕改存 .md 报告后引路径)。对账段不核对正文相关性>', table: true },
33
34
  { title: '风险评估', hint: '<列出本 spec 引入的风险点:数据 / 接口 / 性能 / 安全 / 回滚难度 / 用户影响;评估每个的等级与缓解措施>' },
34
35
  ];
35
36
 
36
37
  export function renderSpec({ topic, level = 'L2', date = TODAY }) {
37
38
  if (!LEVELS.includes(level)) throw new Error(`level 不在枚举 ${LEVELS.join('|')}:${level}`);
38
39
  const fm = `---\n状态: draft\n级别: ${level}\n日期: ${date}\n模块: pipeline\n备注: <spec 关联 intent / plan 的同名主题>\n---\n`;
39
- let body = `# SPEC — ${topic}\n\n<!-- 与 intents/ 下同名入口文档配对;L2/L3 必须先有 ../intents/YYYY-MM-DD-<主题>.md approved -->\n<!-- frontmatter 受限子集(2026-09-13):状态∈draft/approved/done/superseded/cancelled;级别∈L0/L1/L2/L3;正文不再写状态/级别行 -->\n\n`;
40
- for (const { title, hint } of SECTIONS) {
40
+ let body = `# SPEC — ${topic}\n\n<!-- 与 intents/ 下同名入口文档配对;L2/L3 必须先有 ../intents/YYYY-MM-DD-<主题>.md approved -->\n<!-- frontmatter 受限子集(2026-09-13):状态∈draft/approved/done/superseded/cancelled;级别∈L1/L2/L3(2026-10-13 L0 退役);正文不再写状态/级别行 -->\n\n`;
41
+ for (const { title, hint, table } of SECTIONS) {
41
42
  body += `## ${title}\n\n${hint}\n\n`;
43
+ // 验收判据是对账段(B6b 落地后编号以 check-loop.mjs 头部清单为准)的机器输入:直出表骨架(表头签名固定,作者只填行——占位行被检查豁免)
44
+ if (table) body += `| 判据(可观察行为) | 证据(测试文件 / 命令 / 证据文件) | 状态 |\n|---|---|---|\n| <行为 1> | <路径或命令> | <覆盖 / 未覆盖 / 人工> |\n\n`;
42
45
  }
43
46
  body += `## 确认与复核\n\n- 确认日期:\n- 复核:L3 必须独立复核(independent-reviewer,新会话);L2 推荐独立复核;L1 可省\n`;
44
47
  return { frontmatter: fm, body: fm + body, sections: SECTIONS.map((s) => s.title) };
@@ -12,12 +12,20 @@ function check(name, cond, detail = '') {
12
12
  else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
13
13
  }
14
14
 
15
- // ---- 场景 1:5 节正文齐 ----
15
+ // ---- 场景 1:6 节正文齐 ----
16
16
  {
17
17
  const { body } = renderSpec({ topic: 'test', level: 'L2' });
18
- const expectedSections = ['功能行为', '数据流', '系统改动', '约束遵守映射', '风险评估', '确认与复核'];
18
+ const expectedSections = ['功能行为', '数据流', '系统改动', '约束遵守映射', '验收判据', '风险评估', '确认与复核'];
19
19
  for (const s of expectedSections) check('S1 含节 ## ' + s, body.includes('## ' + s));
20
- check('S1 共 6 节标题', (body.match(/^##\s/gm) || []).length === 6);
20
+ check('S1 共 7 节标题', (body.match(/^##\s/gm) || []).length === 7);
21
+ }
22
+
23
+ // ---- 场景 1b:验收判据表骨架(2026-10-13 自 Shipyard 回灌——对账段机器输入,表头签名固定)----
24
+ {
25
+ const { body } = renderSpec({ topic: 't', level: 'L2' });
26
+ check('S1b 验收判据表头三列签名', body.includes('| 判据(可观察行为) | 证据(测试文件 / 命令 / 证据文件) | 状态 |'));
27
+ check('S1b 验收判据表分隔行', body.includes('|---|---|---|'));
28
+ check('S1b 验收判据占位行', body.includes('| <行为 1> | <路径或命令> | <覆盖 / 未覆盖 / 人工> |'));
21
29
  }
22
30
 
23
31
  // ---- 场景 2:frontmatter 含 5 字段 ----
@@ -94,7 +102,7 @@ const runTool = (root, base) => spawnSync(process.execPath,
94
102
  fs.rmSync(root, { recursive: true, force: true });
95
103
  }
96
104
  {
97
- // S9b 探索泳道(2026-09-30 hybrid-governance-explore-hardening):L0/L1 draft intent → 放行 spec 起草
105
+ // S9b 探索泳道(2026-09-30 hybrid-governance-explore-hardening):L1 draft intent → 放行 spec 起草
98
106
  // (「先起草后确认」;L2/L3 防御道维持入口确认前置——S9 照旧)
99
107
  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
108
  const r = runTool(root, '2026-09-30-exp');
@@ -14,7 +14,7 @@
14
14
  //
15
15
  // 依赖发现口径(刻意取宽):**静态 import/export from**、**动态 import()**、以及**裸文件名字面量**
16
16
  // (形如 path.join(here, 'kb-search.mjs') 的 spawn 依赖——wiki-search.mjs 就只靠这一条挂住 kb-search;
17
- // check-hygiene 靠它挂住 rule-budget.sh 与两个生成器)。裸文件名只在**解析到同目录真实文件**时才计入,
17
+ // check-hygiene 靠它挂住 rule-budget.mjs 与两个生成器)。裸文件名只在**解析到同目录真实文件**时才计入,
18
18
  // 故 `'../hooks/commit-check.cjs'` 这类跨目录字面量天然被排除(scripts/ 下不存在同名文件)。
19
19
  // 宁可多拷(多一个文件不会让测试变假绿),不可漏拷(漏拷就是假绿)。
20
20
  import fs from 'node:fs';
@@ -102,7 +102,7 @@ export function scriptClosure(entries, { from = SCRIPT_DIR } = {}) {
102
102
  }
103
103
 
104
104
  // copyScriptClosure:把入口的整个依赖闭包拷进 destScriptsDir,返回实际拷入的文件名。
105
- // opts.extra —— 额外要拷的同级非脚本件(如 rule-budget.sh 之外的 .txt 词表由 copyAgentsFiles 负责)。
105
+ // opts.extra —— 额外要拷的同级非脚本件(如 rule-budget.mjs 之外的 .txt 词表由 copyAgentsFiles 负责)。
106
106
  export function copyScriptClosure(destScriptsDir, entries, opts = {}) {
107
107
  const from = opts.from || SCRIPT_DIR;
108
108
  fs.mkdirSync(destScriptsDir, { recursive: true });
@@ -83,7 +83,12 @@ const synth = (files) => {
83
83
  {
84
84
  const dest = path.join(tmpRoot('copy'), 'scripts');
85
85
  const copied = copyScriptClosure(dest, ['check-loop.mjs']);
86
- check('S7 check-loop 闭包含新台账依赖', ['check-loop.mjs', 'ledger-loader.mjs', 'ledger-prune.mjs'].every((f) => copied.includes(f)), copied.join(','));
86
+ check('S7 check-loop 闭包含新台账依赖(含 policy.mjs——B6b-1 分道/policy 锚随闭包自动跟随)', ['check-loop.mjs', 'ledger-loader.mjs', 'ledger-prune.mjs', 'policy.mjs'].every((f) => copied.includes(f)), copied.join(','));
87
+ // B6b-2 六段模块(2026-10-13):check-loop.mjs import 的段模块须随闭包自动带上——漏拷 = 被测脚本
88
+ // 在夹具里 resolve 失败 → 门禁静默不跑(本文件头注的假绿形态)。显式断言六件在场,不靠运气。
89
+ check('S7 check-loop 闭包含 B6b-2 六段模块(import 闭包自动跟随)',
90
+ ['check-spec-verif.mjs', 'check-registry.mjs', 'check-convergence.mjs', 'check-attribution.mjs', 'check-prejudgment.mjs', 'check-funnel.mjs'].every((f) => copied.includes(f)),
91
+ copied.join(','));
87
92
  check('S7 拷贝后 assertFixtureScripts 不炸(闭包自洽)', assertFixtureScripts(dest) === true);
88
93
  // 真跑一次被测脚本,确认夹具不是「文件齐了但跑不起来」
89
94
  const fx = path.dirname(dest);
@@ -88,6 +88,12 @@ const PAIRS = [
88
88
  { doctor: '7', cl: '18', note: '经 §7 整体运行覆盖(委派台账对账)' },
89
89
  { doctor: '7', cl: '19', note: '经 §7 整体运行覆盖(逐阶段审计——起草先于入口确认 / 台账审批顺序倒置)' },
90
90
  { doctor: '7', cl: '20', note: '经 §7 整体运行覆盖(引擎脚本测试覆盖,2026-10-01 gate-script-test-coverage)' },
91
+ { doctor: '7', cl: '21', note: '经 §7 整体运行覆盖(spec 验证落地核查,2026-10-13 B6b-2 回灌)' },
92
+ { doctor: '7', cl: '22', note: '经 §7 整体运行覆盖(机制使用量台账,2026-10-13 B6b-2 回灌)' },
93
+ { doctor: '7', cl: '23', note: '经 §7 整体运行覆盖(判据级收敛对账,2026-10-13 B6b-2 回灌)' },
94
+ { doctor: '7', cl: '24', note: '经 §7 整体运行覆盖(incident 归因字段核查,2026-10-13 B6b-2 回灌)' },
95
+ { doctor: '7', cl: '25', note: '经 §7 整体运行覆盖(路由预判一致性,2026-10-13 B6b-2 回灌)' },
96
+ { doctor: '7', cl: '26', note: '经 §7 整体运行覆盖(闭环漏斗「协议前」占比动作阈值,2026-10-13 B6b-2 回灌)' },
91
97
  ];
92
98
 
93
99
  // gateChecklist:按登记表对照两侧检查项(pure function;pairs 可注入供测试)
@@ -7,7 +7,7 @@
7
7
  // 默认路径(无 --gate-stats)下 segs 只在内存里攒、不打印不写盘 → 输出契约逐字节不变(硬约束:
8
8
  // pre-push / pre-commit 消费者 + doctor 的 WARN 行计数 + check-loop.test.mjs 223 断言全依赖它)。
9
9
  //
10
- // 耗时口径**刻意包含子进程时间**(检查 11 调两个生成器 --check、检查 13 调 sh rule-budget.sh)——
10
+ // 耗时口径**刻意包含子进程时间**(检查 11 调两个生成器 --check、检查 13 调 rule-budget.mjs)——
11
11
  // 门禁真实成本本就要含子进程;不做剔除,但看板与 spec 均显式声明该口径防误读。
12
12
  export function gateSeg(collector, id, label) {
13
13
  const now = Date.now();
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // gen-effective-policy.mjs — 当前生效规则视图生成器(2026-10-13 自 Shipyard.Material 参考实现回灌,
3
+ // 源仓沿革留痕在包源仓 git 历史与 CHANGELOG——装户径不引实例路径)
4
+ // 背景:豁免锚经 policy v1→v8 演进后,「存量豁免 vs 受管」的合成语义只活在 check-loop.mjs 源码里,
5
+ // 规则空间永久二分且不可读——本脚本把 policy.mjs 单源渲染为人可读的 workflow/effective-policy.md,
6
+ // 与 INDEX.md 同款「生成物 + --check 防漂移」纪律;verify.mjs 挂 --check 门(CI 强制随 policy 变更重生成)。
7
+ // 渲染内容:当前生效 policyVersion(单源 policy.mjs#loadKitPolicy 读 .agents/kit.json,缺省回落 v1)、
8
+ // 逐锚键的生效日 / 消费检查项 / 分级现状(hard·已生效 / advisory·已生效 / 未到期——锚日 > 今日的
9
+ // 升级锚标注「待升级」)。
10
+ // 判定单源:锚定义与版本回落 = policy.mjs POLICIES / loadKitPolicy(本脚本不写日期、不读第二份配置,
11
+ // 只读只渲染);键→检查项映射取自 check-loop.mjs 头部检查清单(kit 现役 1-20 项,编号是文档引用锚)。
12
+ // 用法:node .agents/scripts/gen-effective-policy.mjs [--root <dir>] [--check]
13
+ // --check:与磁盘 workflow/effective-policy.md 比对,漂移 exit 1(CI/verify 用)。
14
+ // 比对前双侧行尾归一(CRLF→LF)。生成器仍只写 LF,--check 不改文件。
15
+ // 干净的 autocrlf 检出是 CRLF,字节相等会假红,且重生成只留下不可提交的行尾差。
16
+ // 测试:node .agents/scripts/gen-effective-policy.test.mjs(fixture 注入 --root)
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { POLICIES, loadKitPolicy } from './policy.mjs';
21
+
22
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
23
+ const argv = process.argv.slice(2);
24
+ const argOf = (k) => {
25
+ const i = argv.indexOf(k);
26
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
27
+ };
28
+ const ROOT = argOf('--root') || path.resolve(SCRIPT_DIR, '..', '..');
29
+ const OUT = path.join(ROOT, 'workflow', 'effective-policy.md');
30
+
31
+ // 键 → 消费检查项与分级说明(单源 check-loop.mjs 头部检查清单编号;说明文本与本脚本共同演进)
32
+ const CONSUMERS = {
33
+ moduleSince: ['检查 12', 'frontmatter「模块:」枚举合法性(词表单源 .agents/workflow-modules.txt)', 'advisory'],
34
+ check14Since: ['检查 14', '新 done 的 spec/plan 须有 approved git 历史(kit 口径恒 advisory 永不升级 hard)', 'advisory'],
35
+ confirmDocsEffective: ['检查 15', 'docs 确认台账两条件准入(台账行 ∪ 自报日期≥锚)', 'hard'],
36
+ confirmIncidentsEffective: ['检查 15', 'incidents 档确认阈值(fixed/closed 配对)', 'hard'],
37
+ bindingTs: ['检查 15', '四终态内容绑定(台账行 ts≥锚 才比对 sha256)', 'hard'],
38
+ check17UnclosedAfter: ['检查 17', '发版提交树未收口三件套——版本 tag 锚(非日期)', 'hard'],
39
+ stageGateSince: ['检查 19', '逐阶段审计(起草先于入口确认 / 台账顺序)', 'advisory'],
40
+ riskLevelSince: ['检查 1', 'intent risk_level 双字段协议(laneOfEntry 判道的生效锚——锚前建档单字段口径不判高)', 'hard'],
41
+ delegationSince: ['检查 18', '委派台账对账——锚前存量豁免', 'advisory'],
42
+ verifySince: ['检查 8', '测试绿声明对账(无 SHA 声明「测试绿」须 24h 内凭证)+ confirm-doc →done 前置', 'advisory'],
43
+ verifyDocSince: ['检查 8', '凭证按 doc 精确绑定的生效时刻(→done 前置与对账按 doc 匹配,不带 --doc 的行不给任何单背书)', 'advisory'],
44
+ scopeSince: ['检查 15', 'scope 子项(机器派生改动面 scope{files,sha256}——缺 scope 灰度 warning、摘要不符 hard)', 'hard'],
45
+ ledgerDocChainSince: ['检查 15', '台账哈希链域从物理序改为按 doc 逻辑序(写入侧 confirm-doc 同批生效)', 'hard'],
46
+ // B6b-2 六段 + §3x + 复核上下文(2026-10-13 自源仓回灌,消费面自本批起在场)
47
+ incidentRegrSince: ['检查 3x', 'incident 防复发验证证据门(fixed/closed 须落自动化用例/回归清单引用)', 'hard'],
48
+ specVerifSince: ['检查 21', 'spec 验证落地核查(验证节无测试文件/命令引用——advisory)', 'advisory'],
49
+ attributionSince: ['检查 24', 'incident 归因字段(来源/影响环境必填且在枚举——advisory)', 'advisory'],
50
+ convergenceSince: ['检查 23', '判据级收敛对账 advisory 档(证据无效 WARN)', 'advisory'],
51
+ convergenceHardSince: ['检查 23', '判据级收敛对账 hard 档(证据无效 HARD-BLOCK)', 'hard'],
52
+ reviewerSince: ['检查 1', '独立复核上下文标识(复核行缺「复核上下文:」——advisory)', 'advisory'],
53
+ };
54
+
55
+ // today:视图基准日。EFFECTIVE_POLICY_TODAY 为测试注入缝(跨锚日稳定性用;生产不设)。
56
+ const today = () => process.env.EFFECTIVE_POLICY_TODAY || new Date().toISOString().slice(0, 10);
57
+
58
+ function render(viewDateStr) {
59
+ const policy = loadKitPolicy(ROOT);
60
+ const v = POLICIES[policy.policyVersion] || {};
61
+ const todayStr = viewDateStr || today();
62
+ const lines = [];
63
+ lines.push('# 当前生效规则视图(生成物,勿手改)');
64
+ lines.push('');
65
+ lines.push('> 生成器 `node .agents/scripts/gen-effective-policy.mjs`(policy.mjs 单源渲染;漂移由 verify.mjs `effective-policy` 门 --check 把关——改 policy 后须重生成并随同提交)。');
66
+ lines.push(`> 当前生效:**policyVersion ${policy.policyVersion}**(kit.json 声明 ${policy.requestedVersion}${policy.requestedVersion === policy.policyVersion ? '' : ',不在 POLICIES 表内已回落 v1'});视图日期 ${todayStr}。`);
67
+ lines.push('');
68
+ lines.push('| 锚键 | 生效日/锚 | 消费检查项 | 语义 | 分级 | 现状 |');
69
+ lines.push('|---|---|---|---|---|---|');
70
+ for (const key of Object.keys(v)) {
71
+ const raw = v[key];
72
+ const c = CONSUMERS[key] || ['—', '(未登记消费方——policy 加新键时同步登 CONSUMERS)', '—'];
73
+ const isDate = /^\d{4}-\d{2}-\d{2}$/.test(String(raw));
74
+ // 升级锚语义:日期 > 今日 = 未到期(维持低档);其余 = 已生效
75
+ const status = !isDate ? '生效(版本锚)' : (String(raw) <= todayStr ? '已生效' : `待升级(${raw} 起)`);
76
+ lines.push(`| \`${key}\` | ${raw} | ${c[0]} | ${c[1]} | ${c[2]} | ${status} |`);
77
+ }
78
+ lines.push('');
79
+ lines.push('<!-- GENERATED:BEGIN — gen-effective-policy.mjs 整段重写,手工说明写在本行之前 -->');
80
+ return lines.join('\n') + '\n';
81
+ }
82
+
83
+ const text = render();
84
+ // 行尾归一(与 gen-workflow-index.mjs normEol 同句):只影响 --check 判定,写盘仍是 LF。
85
+ const normEol = (s) => String(s).replace(/\r\n/g, '\n');
86
+ if (argv.includes('--check')) {
87
+ let disk = '';
88
+ try {
89
+ disk = fs.readFileSync(OUT, 'utf8');
90
+ } catch { /* 缺文件视作空——走漂移分支(新装户由 init 生成器首跑种子本文件) */ }
91
+ // 锚日时间炸弹拆除(源仓 2026-10-12 CI 实证:锚日当天重渲染按当日判「已生效」→ 与仓内视图必差
92
+ // → --check 必红——任何未来日期锚都会在锚日引爆)。修法:--check 对**磁盘文件自带的视图日期**重渲染
93
+ // (生成物与自身生成日自洽,跨锚日字节稳定);显式重生成(默认模式)才前进视图日期并翻转状态列。
94
+ // 视图日期行缺失的存量件回落当日口径。
95
+ const mDate = disk.match(/视图日期 (\d{4}-\d{2}-\d{2})/);
96
+ const checkText = mDate ? render(mDate[1]) : text;
97
+ if (normEol(disk) === normEol(checkText)) {
98
+ console.log('[effective-policy] ✅ 与磁盘一致(无漂移;按磁盘视图日期重渲染比对)');
99
+ process.exit(0);
100
+ }
101
+ console.error('[effective-policy] ❌ 漂移:policy 变更后未重生成 workflow/effective-policy.md');
102
+ console.error(' 修法:node .agents/scripts/gen-effective-policy.mjs && git add workflow/effective-policy.md(随 policy 变更同笔提交)');
103
+ process.exit(1);
104
+ }
105
+ fs.mkdirSync(path.dirname(OUT), { recursive: true });
106
+ fs.writeFileSync(OUT, text);
107
+ console.log(`✅ 已生成 ${path.relative(process.cwd(), OUT)}`);
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // gen-effective-policy.test.mjs — 生效规则视图生成器 fixture 套件(2026-10-13 自 Shipyard.Material
3
+ // 参考实现回灌时随件移植;夹具口径改 kit:注入 .agents/kit.json 的 policyVersion 取值 / 缺失 / 非法三态)
4
+ // 方法:临时根注入 kit.json,断言生成内容与 --check 语义。
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
12
+ const SCRIPT = path.join(SCRIPT_DIR, 'gen-effective-policy.mjs');
13
+
14
+ let pass = 0;
15
+ let fail = 0;
16
+ const check = (desc, cond, detail = '') => {
17
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
18
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
19
+ };
20
+ const mkfix = (policyVersion) => {
21
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'eff-policy-test-'));
22
+ fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
23
+ if (policyVersion !== undefined) fs.writeFileSync(path.join(d, '.agents', 'kit.json'), JSON.stringify({ kit: 'agentic-flow-kit', policyVersion }));
24
+ return d;
25
+ };
26
+ const run = (root, ...args) => {
27
+ const r = spawnSync(process.execPath, [SCRIPT, '--root', root, ...args], { encoding: 'utf8' });
28
+ return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
29
+ };
30
+
31
+ // 场景 1:v8 生成——当前版本行 + 全锚键行 + 表结构(真实 v8 锚均为历史日期,待升级分支由场景 4 注入钉住)
32
+ {
33
+ const d = mkfix(8);
34
+ const r = run(d);
35
+ const doc = fs.readFileSync(path.join(d, 'workflow', 'effective-policy.md'), 'utf8');
36
+ check('场景 1:v8 生成成功且含版本声明与锚键行',
37
+ r.code === 0 && doc.includes('**policyVersion 8**') && doc.includes('`ledgerDocChainSince`') && doc.includes('`bindingTs`') && doc.includes('kit.json 声明 8'), r.out + doc);
38
+ }
39
+ // 场景 2:--check 一致 → 0;篡改磁盘 → 1;再生成 → 恢复 0
40
+ {
41
+ const d = mkfix(8);
42
+ run(d);
43
+ check('场景 2a:生成后 --check 一致', run(d, '--check').code === 0);
44
+ fs.appendFileSync(path.join(d, 'workflow', 'effective-policy.md'), 'drift\n');
45
+ const r2 = run(d, '--check');
46
+ check('场景 2b:磁盘漂移 → --check exit 1 且含修法', r2.code === 1 && r2.out.includes('重生成'), r2.out);
47
+ run(d);
48
+ check('场景 2c:重生成后 --check 恢复 0', run(d, '--check').code === 0);
49
+ }
50
+ // 场景 2d/2e:行尾不算漂移,内容差仍算(autocrlf 假红)
51
+ {
52
+ const d = mkfix(8);
53
+ run(d);
54
+ const p = path.join(d, 'workflow', 'effective-policy.md');
55
+ const lf = fs.readFileSync(p, 'utf8');
56
+ fs.writeFileSync(p, lf.replace(/\n/g, '\r\n'));
57
+ check('场景 2d:CRLF 检出、内容相同 → --check 0', run(d, '--check').code === 0);
58
+ fs.writeFileSync(p, lf.replace(/\n/g, '\r\n').replace('policyVersion', 'policyVerslon'));
59
+ const r = run(d, '--check');
60
+ check('场景 2e:CRLF 且内容漂移 → --check exit 1', r.code === 1 && r.out.includes('重生成'), r.out);
61
+ }
62
+ // 场景 2f:锚日翻转稳定性(源仓 2026-10-12 CI 生产事故钉:视图生成于 D1,次日系统当日 --check
63
+ // 按当日重渲染判「已生效」→ 与仓内「待升级」必差 → 每个未来日期锚都在锚日引爆)。
64
+ // 修法语义:--check 对磁盘自带「视图日期」重渲染——不依赖未来锚存在,视图日期行本身在旧实现下就会漂移。
65
+ {
66
+ const d = mkfix(8);
67
+ const d1 = new Date(Date.now() - 24 * 3600 * 1000).toISOString().slice(0, 10);
68
+ const gen = spawnSync(process.execPath, [SCRIPT, '--root', d], { encoding: 'utf8', env: { ...process.env, EFFECTIVE_POLICY_TODAY: d1 } });
69
+ const doc = fs.readFileSync(path.join(d, 'workflow', 'effective-policy.md'), 'utf8');
70
+ const chk = run(d, '--check'); // 不带 env——按系统当日跑,模拟「次日对账」
71
+ check('场景 2f:视图日期 D1 生成的视图,次日 --check 仍一致(按磁盘视图日期重渲染,锚日不引爆)',
72
+ gen.status === 0 && doc.includes(`视图日期 ${d1}`) && chk.code === 0,
73
+ `gen=${gen.status} docViewDate=${(doc.match(/视图日期 (\S+)/) || [])[1]} check=${chk.code}\n${chk.out}`);
74
+ }
75
+
76
+ // 场景 3:非法版本号 → 回落 v1(渲染回落声明);缺 kit.json → 同 v1
77
+ {
78
+ const d = mkfix(99);
79
+ const r = run(d);
80
+ const doc = fs.readFileSync(path.join(d, 'workflow', 'effective-policy.md'), 'utf8');
81
+ check('场景 3a:非法 policyVersion=99 → 回落 v1 且声明回落',
82
+ r.code === 0 && doc.includes('**policyVersion 1**') && doc.includes('kit.json 声明 99') && doc.includes('已回落'), doc);
83
+ const d2 = mkfix(undefined);
84
+ const r2 = run(d2);
85
+ const doc2 = fs.readFileSync(path.join(d2, 'workflow', 'effective-policy.md'), 'utf8');
86
+ check('场景 3b:缺 kit.json → v1 基线', r2.code === 0 && doc2.includes('**policyVersion 1**'), doc2);
87
+ }
88
+ // 场景 4:待升级判定——不依赖墙钟:EFFECTIVE_POLICY_TODAY 注入 2026-09-24,
89
+ // v8 的 check14Since=2026-09-26 > 视图基准日 → 「待升级」;moduleSince=2026-09-22 ≤ → 「已生效」
90
+ {
91
+ const d = mkfix(8);
92
+ const r = spawnSync(process.execPath, [SCRIPT, '--root', d], { encoding: 'utf8', env: { ...process.env, EFFECTIVE_POLICY_TODAY: '2026-09-24' } });
93
+ const doc = fs.readFileSync(path.join(d, 'workflow', 'effective-policy.md'), 'utf8');
94
+ check('场景 4:升级锚按视图基准日判定(moduleSince 已生效 / check14Since 待升级)',
95
+ r.status === 0 && doc.includes('`check14Since` | 2026-09-26 | 检查 14 | 新 done 的 spec/plan 须有 approved git 历史(kit 口径恒 advisory 永不升级 hard) | advisory | 待升级(2026-09-26 起)')
96
+ && doc.includes('`moduleSince` | 2026-09-22 | 检查 12'), doc);
97
+ }
98
+
99
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
100
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // gen-verify-evidence.mjs — 验证绿凭证摘要入库生成器(2026-10-13 自 Shipyard.Material 参考实现回灌,
3
+ // 源仓沿革留痕在包源仓 git 历史与 CHANGELOG——装户径不引实例路径)
4
+ // 背景:verifications.jsonl 是 gitignore 的本地台账(24h 窗口、跨机不可见)——历史提交「当时验证过什么」
5
+ // 的机器事实只活在本地。本脚本把全绿行按 headShort 去重(同 SHA 取最新 ts)渲染进仓内
6
+ // workflow/verify-evidence.md,使绿凭证成为随库持久、跨机可对质的生成物(全量台账仍留本地)。
7
+ // 语义:一行 = 「**验证时刻的工作树**(其基线提交 = 短 SHA,工作树可含未提交改动)过 verify 全量门禁
8
+ // 全绿(门数 / 套件计数)」;非证据链签名——本地台账可伪造的诚实边界不变(check-loop 检查 15 头注释
9
+ // 口径),入库摘要只是把「曾有凭证」从易失升级为难失。
10
+ // 归因提醒:短 SHA 只是**基线提交**,不等于被测内容。关单应「先提交、后跑门」,否则绿行 SHA 指向
11
+ // 未含本次改动的上一提交——同一 SHA 可能既有绿行(脏工作区跑)又有红记录。
12
+ // 追加保全:已入仓的行不因本地台账缺失而被抹掉——台账 per-worktree 且 gitignore,整段重写会让
13
+ // 跨机/换 worktree 的历史凭证消失。合并口径 = 仓内既有行 ∪ 本地台账绿行,同 headShort 取更新 ts
14
+ // (平局取本地台账,保持「重跑刷新凭证」语义)。代价(已接受):仓内错行不会被重生成自动纠正——
15
+ // 须人工改正;若本地台账仍持同一 SHA,重生成会按台账重写该行(须连同台账处理)。
16
+ // 时机:verify.mjs 全绿尾部自动重生成(事件原子化——绿事实与仓内摘要同刻生成,消除双真相分叉)
17
+ // + 关单时随三件套同笔提交;CI 侧重生成于 CI 工作区、不回流本仓,无副作用;--check 只供本地自检。
18
+ // 台账口径:kit 绿行形状 = {ts, exitCode:0, suite:'npm test', gates?, headShort?, passed?, failed?,
19
+ // doc?, runId?}(verify.mjs 落账单源)——本脚本只认 exitCode===0 且 suite==='npm test' 且带 headShort
20
+ // 的行(无 SHA 的旧 schema 行不渲染);passed/failed 语义 = 套件计数(非断言),doc = --doc 绑定单。
21
+ // 用法:node .agents/scripts/gen-verify-evidence.mjs [--root <dir>] [--check]
22
+ // 测试:node .agents/scripts/gen-verify-evidence.test.mjs(fixture 注入 --root)
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
28
+ const argv = process.argv.slice(2);
29
+ const argOf = (k) => {
30
+ const i = argv.indexOf(k);
31
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
32
+ };
33
+ const ROOT = argOf('--root') || path.resolve(SCRIPT_DIR, '..', '..');
34
+ const LEDGER = path.join(ROOT, '.agents', 'verifications.jsonl');
35
+ const OUT = path.join(ROOT, 'workflow', 'verify-evidence.md');
36
+
37
+ // 六列数据行:短SHA / 时间 / 门数 / 套件计数 / 绑定单 / runId
38
+ const ROW_RE = /^\|\s*`([0-9a-f]{7,40})`\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|\s*$/;
39
+
40
+ /** 按本地台账条目渲染数据行 */
41
+ function renderRow(e) {
42
+ const cnt = Number.isFinite(e.passed) ? `PASS ${e.passed} / FAIL ${e.failed ?? 0}` : '—';
43
+ return `| \`${e.headShort}\` | ${e.ts} | ${e.gates ?? '—'} | ${cnt} | ${e.doc || '—'} | ${e.runId || '—'} |`;
44
+ }
45
+
46
+ /**
47
+ * 解析本文件既有数据行(追加保全用)。
48
+ * 只认形态完整的数据行;说明行 / 表头 / 注释一律忽略。读不到文件 = 无既有行。
49
+ */
50
+ function parseCommittedRows() {
51
+ const out = [];
52
+ let disk = '';
53
+ try { disk = fs.readFileSync(OUT, 'utf8'); } catch { return out; }
54
+ for (const raw of disk.split(/\r?\n/)) {
55
+ const m = raw.trim().match(ROW_RE);
56
+ if (m) out.push({ headShort: m[1], ts: m[2].trim(), line: raw.trim() });
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function render() {
62
+ const rows = [];
63
+ try {
64
+ for (const line of fs.readFileSync(LEDGER, 'utf8').split(/\r?\n/)) {
65
+ if (!line.trim()) continue;
66
+ try {
67
+ const e = JSON.parse(line);
68
+ if (e.exitCode === 0 && e.suite === 'npm test' && e.headShort) rows.push(e);
69
+ } catch { /* 坏行跳过——台账是 append-only 事实流,不因单行格式失败中断 */ }
70
+ }
71
+ } catch { /* 无台账 = 空表(新装户/新 clone 首跑前) */ }
72
+ // 行统一形态 { headShort, ts, line }。先放**仓内既有行**(追加保全),再叠加本地台账绿行。
73
+ const bySha = new Map();
74
+ for (const r of parseCommittedRows()) bySha.set(r.headShort, r);
75
+ // 同 headShort 取更新 ts(重跑刷新凭证,不堆行);平局取本地台账
76
+ for (const e of rows) {
77
+ const prev = bySha.get(e.headShort);
78
+ if (!prev || String(e.ts) >= String(prev.ts)) bySha.set(e.headShort, { headShort: e.headShort, ts: String(e.ts), line: renderRow(e) });
79
+ }
80
+ const ordered = [...bySha.values()].sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
81
+ const lines = [];
82
+ lines.push('# 验证绿凭证摘要(生成物,勿手改)');
83
+ lines.push('');
84
+ lines.push('> 生成器 `node .agents/scripts/gen-verify-evidence.mjs`(verify.mjs 全绿尾部自动重生成;源 .agents/verifications.jsonl 为本地台账)。');
85
+ lines.push('> 一行 = 验证时刻**工作树**过 verify 全量门禁全绿(短 SHA 为该时刻的**基线提交**,工作树可能含未提交改动——关单宜「先提交、后跑门」);`套件计数` 为测试门编排器的套件 PASS/FAIL(无汇总行时缺省);`绑定单` 为 --doc 绑定的工作流文档(不带的行不给任何单背书)。非签名链——诚实边界同 check-loop 检查 15。');
86
+ lines.push('> 行来源 = 本地台账绿行 ∪ 本文件既有行(**追加保全**:跨机 / 换 worktree 不失历史;代价:错行不会被重生成自动纠正,须人工改正后重跑)。');
87
+ lines.push('');
88
+ lines.push('| 提交(短SHA) | 时间(UTC) | 门数 | 套件计数 | 绑定单 | runId |');
89
+ lines.push('|---|---|---|---|---|---|');
90
+ for (const e of ordered) lines.push(e.line);
91
+ lines.push('');
92
+ lines.push(`<!-- GENERATED:END — 共 ${ordered.length} 行;gen-verify-evidence.mjs 生成(既有行追加保全,勿手改) -->`);
93
+ return lines.join('\n') + '\n';
94
+ }
95
+
96
+ const text = render();
97
+ if (argv.includes('--check')) {
98
+ let disk = '';
99
+ try { disk = fs.readFileSync(OUT, 'utf8'); } catch { /* 缺文件走漂移分支 */ }
100
+ if (disk === text) { console.log('[verify-evidence] ✅ 与磁盘一致'); process.exit(0); }
101
+ console.error('[verify-evidence] 漂移:本地台账有新绿行未入摘要——关单时跑 node .agents/scripts/gen-verify-evidence.mjs 并随同提交');
102
+ process.exit(1);
103
+ }
104
+ fs.mkdirSync(path.dirname(OUT), { recursive: true });
105
+ fs.writeFileSync(OUT, text);
106
+ // 数据行计数:直接数六列数据行,不做总行数算术(生成物自描述)
107
+ console.log(`✅ 已生成 ${path.relative(process.cwd(), OUT)}(绿凭证 ${text.split(/\r?\n/).filter((l) => ROW_RE.test(l.trim())).length} 行)`);