agentic-flow-kit 0.5.0 → 0.7.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 (85) hide show
  1. package/README.md +2 -0
  2. package/modules/gates/dotnet-ca/check-architecture.sh +23 -5
  3. package/modules/hosts/omp/agents/implementer.md +2 -0
  4. package/modules/hosts/omp/agents/independent-reviewer.md +2 -0
  5. package/modules/hosts/omp/agents/ui-verifier.md +2 -0
  6. package/modules/hosts/opencode/agents/implementer.md +2 -0
  7. package/modules/hosts/opencode/agents/independent-reviewer.md +2 -0
  8. package/modules/hosts/opencode/agents/ui-verifier.md +2 -0
  9. package/modules/hosts/opencode/commands/build.md +20 -1
  10. package/modules/hosts/opencode/commands/design.md +2 -0
  11. package/modules/hosts/opencode/commands/gate-checklist.md +77 -0
  12. package/modules/hosts/opencode/commands/maintain.md +2 -3
  13. package/modules/hosts/opencode/commands/plan.md +4 -1
  14. package/modules/hosts/opencode/commands/review.md +1 -0
  15. package/modules/hosts/opencode/commands/source-sync-check.md +89 -0
  16. package/modules/hosts/opencode/commands/sync-hosts.md +66 -0
  17. package/modules/hosts/opencode/commands/test.md +4 -2
  18. package/modules/hosts/trae/agents/implementer.md +2 -0
  19. package/modules/hosts/trae/agents/independent-reviewer.md +2 -0
  20. package/modules/hosts/trae/agents/ui-verifier.md +2 -0
  21. package/modules/hosts/trae/commands/wf-build.md +20 -1
  22. package/modules/hosts/trae/commands/wf-design.md +2 -0
  23. package/modules/hosts/trae/commands/wf-gate-checklist.md +78 -0
  24. package/modules/hosts/trae/commands/wf-maintain.md +2 -3
  25. package/modules/hosts/trae/commands/wf-plan.md +4 -1
  26. package/modules/hosts/trae/commands/wf-review.md +1 -0
  27. package/modules/hosts/trae/commands/wf-source-sync-check.md +90 -0
  28. package/modules/hosts/trae/commands/wf-sync-hosts.md +67 -0
  29. package/modules/hosts/trae/commands/wf-test.md +4 -2
  30. package/modules/hosts/trae/hooks/post-edit-check.cjs +3 -2
  31. package/modules/hosts/trae/hooks/pre-shell-check.cjs +40 -9
  32. package/modules/hosts/trae/hooks.json +1 -1
  33. package/modules/hosts/zcode/agents/implementer.md +2 -0
  34. package/modules/hosts/zcode/agents/independent-reviewer.md +2 -0
  35. package/modules/hosts/zcode/agents/ui-verifier.md +2 -0
  36. package/package.json +2 -2
  37. package/src/add-gate.mjs +5 -3
  38. package/src/doctor.mjs +121 -18
  39. package/src/gate-dotnet-ca.test.mjs +124 -0
  40. package/src/init.mjs +79 -17
  41. package/src/init.test.mjs +60 -1
  42. package/src/profiles.mjs +19 -2
  43. package/src/render.mjs +64 -4
  44. package/src/run-tests.mjs +3 -0
  45. package/src/sync-hosts.mjs +3 -2
  46. package/src/sync.mjs +64 -20
  47. package/src/sync.test.mjs +42 -0
  48. package/src/trae-hooks.test.mjs +115 -0
  49. package/templates/AGENTS.md +1 -0
  50. package/templates/_agents/board/index.html +32 -3
  51. package/templates/_agents/commands/build.md +5 -4
  52. package/templates/_agents/commands/design.md +1 -0
  53. package/templates/_agents/commands/maintain.md +1 -2
  54. package/templates/_agents/commands/plan.md +2 -0
  55. package/templates/_agents/commands/test.md +2 -1
  56. package/templates/_agents/hooks/check-pairing-incremental.sh +20 -7
  57. package/templates/_agents/metric-claims.txt +81 -0
  58. package/templates/_agents/roles/implementer.md +2 -0
  59. package/templates/_agents/roles/independent-reviewer.md +2 -0
  60. package/templates/_agents/roles/ui-verifier.md +2 -0
  61. package/templates/_agents/scripts/agg-delegations.cjs +39 -30
  62. package/templates/_agents/scripts/agg-delegations.test.mjs +32 -0
  63. package/templates/_agents/scripts/check-loop.mjs +538 -16
  64. package/templates/_agents/scripts/check-loop.test.mjs +1116 -0
  65. package/templates/_agents/scripts/confirm-doc.mjs +228 -0
  66. package/templates/_agents/scripts/confirm-doc.test.mjs +321 -0
  67. package/templates/_agents/scripts/doctor.test.mjs +81 -3
  68. package/templates/_agents/scripts/ensure-board.mjs +29 -2
  69. package/templates/_agents/scripts/gate-checklist.mjs +3 -0
  70. package/templates/_agents/scripts/gate-checklist.test.mjs +3 -3
  71. package/templates/_agents/scripts/gen-wiki-board.mjs +38 -24
  72. package/templates/_agents/scripts/gen-wiki-board.test.mjs +18 -0
  73. package/templates/_agents/scripts/gen-workflow-metrics.mjs +31 -3
  74. package/templates/_agents/scripts/kb-search.mjs +22 -8
  75. package/templates/_agents/scripts/kb-search.test.mjs +38 -8
  76. package/templates/_agents/scripts/source-sync-check.mjs +30 -5
  77. package/templates/_agents/scripts/source-sync-check.test.mjs +133 -8
  78. package/templates/_agents/scripts/verify-wiki-consistency.mjs +46 -23
  79. package/templates/_agents/scripts/workflow-board-server.mjs +64 -9
  80. package/templates/_agents/scripts/workflow-board-server.test.mjs +136 -0
  81. package/templates/_agents/scripts/workflow-enums.mjs +4 -2
  82. package/templates/_agents/scripts/workflows-check.test.mjs +14 -3
  83. package/templates/_agents/workflow-enums.txt +3 -3
  84. package/templates/_agents/workflows/_TEMPLATE.md +1 -0
  85. package/templates/_githooks/pre-commit +16 -1
@@ -1,16 +1,20 @@
1
1
  #!/bin/sh
2
- # 闭环配对(增量版):本次暂存的 intents/ incidents/ 入口文档必须已有同名 plan
2
+ # 闭环配对(增量版):本次暂存的 intents/ incidents/ 入口文档必须已有同名 plan(L2/L3 另须同名 spec)
3
3
  # 目的:把 check-loop.sh「配对断裂」hard-block 的发现时机从 pre-push 提前到 commit
4
4
  # (papercut 2026-09-15:maintain.md 只写「incident≡intent 免另立 intent」,未把配 plan 写成显式步骤,
5
- # 按文档字面执行三次 commit 全绿、push 才爆红,只能后补)
6
- # 口径与 check-loop.sh 对齐:入口文档(intents/ incidents/ 直下 .md)↔ workflow/plans/ 同名;
7
- # 同 commit 携带 plan 合法(检查的是工作区文件存在,staged 新建 plan 同样命中)。
8
- # 已知边界:legacy 豁免读的是工作区文件而非暂存 blob——add 后又改工作区时可能误豁免/误拦(逐文件取 blob 的 fork 成本大于收益,接受)。
5
+ # 按文档字面执行三次 commit 全绿、push 才爆红,只能后补;
6
+ # 2026-09-27 audit-gate-hardening 补齐 spec 档——此前只查 plan,L2/L3 缺 spec 仍要拖到 push 才爆红)
7
+ # 口径与 check-loop.sh 对齐:入口文档(intents/ incidents/ 直下 .md)↔ workflow/plans/ 同名,
8
+ # L2/L3 级别另须 workflow/specs/ 同名(check-loop 检查 1 同款分档);
9
+ # 同 commit 携带 plan/spec 合法(检查的是工作区文件存在,staged 新建同样命中)。
10
+ # 已知边界:legacy 豁免与级别读取均基于工作区文件而非暂存 blob——add 后又改工作区时可能误豁免/误拦(逐文件取 blob 的 fork 成本大于收益,接受)。
9
11
 
10
12
  cd "$(git rev-parse --show-toplevel)" || exit 1
11
13
 
12
14
  fail=0
13
- for f in $(git diff --cached --name-only --diff-filter=ACMR | grep -E '^workflow/(intents|incidents)/[^/]+\.md$' | grep -v '_TEMPLATE\.md$'); do
15
+ # -c core.quotepath=off:quotepath 默认 true 时非 ASCII 路径被引号转义("workflow/\346..."),
16
+ # ^workflow/ 前缀匹配失配 → 门禁静默跳过——教训与修法同 check-wiki-ledger.sh(2026-09-27 gate-coverage)
17
+ for f in $(git -c core.quotepath=off diff --cached --name-only --diff-filter=ACMR | grep -E '^workflow/(intents|incidents)/[^/]+\.md$' | grep -v '_TEMPLATE\.md$'); do
14
18
  # 闭环引擎启用前的回填件带「流程: legacy」豁免配对检查(与 check-loop 同口径)
15
19
  if grep -q '^流程: legacy' "$f"; then
16
20
  continue
@@ -22,9 +26,18 @@ for f in $(git diff --cached --name-only --diff-filter=ACMR | grep -E '^workflow
22
26
  echo " L1 极简 plan 只需「改动面 + 验证方式」两节(复制 workflow/plans/_TEMPLATE.md)"
23
27
  fail=1
24
28
  fi
29
+ lvl=$(sed -n 's/^级别:[[:space:]]*//p' "$f" | head -1)
30
+ if [ "$lvl" = "L2" ] || [ "$lvl" = "L3" ]; then
31
+ if [ ! -f "workflow/specs/$base.md" ]; then
32
+ echo "闭环配对(增量)— BLOCK:"
33
+ echo " 入口缺同名 spec: $f(级别 $lvl 须三件套,应在 workflow/specs/$base.md)"
34
+ echo " 与 pre-push check-loop 检查 1 同口径——先起草 spec 走 design.md,再提交"
35
+ fail=1
36
+ fi
37
+ fi
25
38
  done
26
39
 
27
40
  if [ $fail -ne 0 ]; then
28
- echo "pre-commit: 入口文档缺同名 plan,提交已阻止(与 pre-push check-loop 同口径,发现时机提前到 commit)" >&2
41
+ echo "pre-commit: 入口文档配对不全(plan / L2-L3 spec),提交已阻止(与 pre-push check-loop 同口径,发现时机提前到 commit)" >&2
29
42
  fi
30
43
  exit $fail
@@ -0,0 +1,81 @@
1
+ # 量化断言「指标签名」单源登记表(check-loop 检查 16 共读)
2
+ #
3
+ # 背景(2026-09-28 claim-exceeds-fix):结论文档里跨文档引用的量化值若各自手写,
4
+ # 极易在事实变更后不同步——本仓已实证:同一指标在一处改了、另一处没改,
5
+ # 而「五处文件同步」这句话本身无人核对(该单真正被门禁抓住的方式,是事后人工复核)。
6
+ # 本表把「哪个指标、当前取值多少」收敛为单源,供 check-loop 检查 16 对账。
7
+ #
8
+ # 用法:活跃态文档(draft/approved/open)引用某个已登记指标时,按**签名形态**书写,
9
+ # 检查 16 会实时取数比对。签名形态:\{\{指标名\}\}(双花括号包裹指标名,小写点分)。
10
+ # 例(下面是「讲语法」,故加反斜杠转义;**写真断言时不加反斜杠**):
11
+ # 本仓台账共 \{\{ledger.lines\}\} 行,其中无 batch 字段 \{\{ledger.linesWithoutBatch\}\} 行。
12
+ # 写作时**不要手写数字**——写完跑 check-loop,工具会把实时值填进提示里供你替换。
13
+ # 转义按「逐个出现」生效:同一行里 \{\{x\}\} 是示意、\{\{y\}\} 是真断言时,只豁免前者。
14
+ # (本例的指标名曾写成 ledger.total / ledger.noBatch 两个**并不存在**的名字——本表不被
15
+ # 检查 16 扫描(它是 .txt 非 .md),故这类笔误不会被自动抓到,只能靠复核。已据实更正。)
16
+ #
17
+ # 为什么用显式签名而非全文数字扫描(据实说明,非偷懒):
18
+ # 本仓活跃文档中「N 行 / N 条 / N 份」类表述有 49 处(实测:活跃态 21 份文档逐行计),绝大多数是**历史叙述**
19
+ # (描述某次提交当时的规模,天然不会随当前事实变化)。全文扫描会对这些正确的
20
+ # 历史叙述产生大量假阳性——而 check-loop.mjs 已把「无判定依据的行不产出不可
21
+ # 消除噪声」立为红线(沿 2026-09-27 audit-gate-hardening P3 教训:噪声淹没真漏点)。
22
+ # 故本检查**只校验显式声明的签名**:假阳性恒为 0,代价是覆盖面依赖作者登记。
23
+ #
24
+ # 格式:每行 `<指标名> = <取数表达式>`;`#` 开头为注释;空行忽略。
25
+ # 取数表达式(闭集,全部本地确定性、零网络):
26
+ # ledger.lines 台账 .agents/confirmations.jsonl 的非空行数
27
+ # ledger.linesWithoutBatch 台账中不含 "batch" 字段的行数(schema 演进前历史行)
28
+ # ledger.linesWithBatch 台账中含 "batch" 字段的行数
29
+ # docs.count.<sub> workflow/<sub> 下文档份数——**与文档扫描同口径**:以数字开头的
30
+ # .md、排除 _TEMPLATE.md、且仓库模式下只计 HEAD 已跟踪件
31
+ # (故并行会话的未跟踪新档不计入);sub ∈ intents/specs/plans/incidents
32
+ # docs.count.all 上述四类合计
33
+ # ── 新增指标(2026-09-28 adopter-derivers 起)────────────────────────────────
34
+ # 分两层取数,**内置优先**:
35
+ # ① 引擎内置 8 个指标(下表)——装户零配置即用,不需要做任何事。
36
+ # ② 项目自有指标——写 `.agents/metric-derivers.cjs`(**owned**,sync 永不覆盖),
37
+ # 用 CommonJS 导出 `derivers` 对象(指标名 → 取数函数),并在此表加同名一行。
38
+ # **后缀必须是 `.cjs`、写法必须是 CommonJS**(`module.exports`)。
39
+ # 为什么不用 `.mjs`:引擎用 `require()` 同步载入,而 `require(esm)` 仅在
40
+ # Node ≥20.19 / ≥22.12 可用——**实测 Node 18.20 / 20.18 / 22.11 全部
41
+ # ERR_REQUIRE_ESM**,而本仓 engines 声明 `>=18.0.0`。故 `.cjs` 是唯一
42
+ # 全版本可用的形态(同步、零版本依赖)。
43
+ # 旧的 `.mjs` 路径已弃用:若残留在盘上,检查 16 会**明确提示迁移**。
44
+ #
45
+ # 示例 `.agents/metric-derivers.cjs`:
46
+ # module.exports = {
47
+ # derivers: {
48
+ # 'my.migrations': (ctx) => ctx.glob('backend/migrations/*.sql').length,
49
+ # 'my.apiCount': (ctx) => ctx.countFiles('src/api', (f) => f.endsWith('.controller.ts')),
50
+ # },
51
+ # };
52
+ # ctx 提供(同步,免你重复造轮子;其余请自行 `require('node:fs')`):
53
+ # ctx.root 仓库根绝对路径
54
+ # ctx.read(rel) 读文本文件(LF 归一),不存在返回 ''
55
+ # ctx.glob(pattern) 极简 glob(* / **),返回相对路径数组
56
+ # ctx.countFiles(dir, pred) 递归计数(可传谓词)
57
+ #
58
+ # 三条硬约定(违反即 fail-loud):
59
+ # 1. **必须是同步函数**(返回 number,不得 async/Promise)——本检查零网络、要求本地确定性
60
+ # 2. 必须**确定性**:不得依赖网络、不得依赖当前时间
61
+ # 3. 返回值须为**有限数字**(Number.isFinite)
62
+ #
63
+ # 失败语义(**绝不静默降级**):模块语法错 / 抛异常 / 未导出 derivers / 返回值非法
64
+ # → 一律出 `[WARN 装户取数器载入失败]` 或 `[WARN 指标取数失败]`,**不会**退化成
65
+ # 「只用内置指标」——否则你会误以为「忘了登记」而查不到真因。
66
+ # 指标名与内置同名时:**内置优先**,你的定义被忽略(会出 advisory 明示)。
67
+ #
68
+ # 注:本检查**不设取数超时**——请让取数器保持轻量(秒级)。死循环会拖慢门禁。
69
+ # 另:装户模块的「取数器」在引擎侧(check-loop.mjs,managed);本表与 derivers.cjs 同归 owned。
70
+ #
71
+ # 注意:不登记 advisory 总数类指标——它由 check-loop 自身产出(检查 16 是其一部分),
72
+ # 自引用会让「报告本身改变被报告的数」,产生不可稳定对账的循环。
73
+
74
+ ledger.lines = ledger.lines
75
+ ledger.linesWithoutBatch = ledger.linesWithoutBatch
76
+ ledger.linesWithBatch = ledger.linesWithBatch
77
+ docs.count.intents = docs.count.intents
78
+ docs.count.specs = docs.count.specs
79
+ docs.count.plans = docs.count.plans
80
+ docs.count.incidents = docs.count.incidents
81
+ docs.count.all = docs.count.all
@@ -1,5 +1,7 @@
1
1
  # Implementer
2
2
 
3
+ > 公共角色契约(本文件):`.agents/roles/implementer.md`。宿主薄适配(`.opencode/` / `.trae/` / `.zcode/` 下 `agents/implementer.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
4
+
3
5
  ## 职责
4
6
 
5
7
  执行已经由主智能体和用户确认、范围明确的实现任务。适用范围仅限:
@@ -1,5 +1,7 @@
1
1
  # Independent Reviewer
2
2
 
3
+ > 公共角色契约(本文件):`.agents/roles/independent-reviewer.md`。宿主薄适配(`.opencode/` / `.trae/` / `.zcode/` 下 `agents/independent-reviewer.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
4
+
3
5
  ## 职责
4
6
 
5
7
  在独立上下文中对 spec、plan 或代码 diff 做只读复核,发现原作者可能遗漏的正确性、契约、架构、数据安全和回归风险。
@@ -1,5 +1,7 @@
1
1
  # UI Verifier
2
2
 
3
+ > 公共角色契约(本文件):`.agents/roles/ui-verifier.md`。宿主薄适配(`.opencode/` / `.trae/` / `.zcode/` 下 `agents/ui-verifier.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
4
+
3
5
  ## 职责
4
6
 
5
7
  对已经实现的 UI 改动执行浏览器实测并收集可复核证据。只验证,不修复。
@@ -4,7 +4,7 @@
4
4
  * 输出各月指标(一次通过率 / 平均返工次数 / 主兜底占比)、并发扩容门判定、可粘贴的月度快照行。
5
5
  * 结果取值:一次通过 | 返工×N | 主兜底 | 返工待修(未闭环,不计入率)。
6
6
  * 用法:node .agents/scripts/agg-delegations.cjs [--month=YYYY-MM](默认输出全部月份 + 全量累计)
7
- * 门槛定义见 workflow/delegations.md §并发扩容门槛,两处须同步改。
7
+ * 门槛定义见 workflow/delegations.md §并发扩容门槛(doc 为权威,脚本向文档对齐——2026-09-27 board-kb-p1)。
8
8
  */
9
9
  'use strict';
10
10
  const fs = require('fs');
@@ -88,8 +88,11 @@ function countIncidents() {
88
88
  return byMonth;
89
89
  }
90
90
 
91
- // 门槛(与 workflow/delegations.md §并发扩容门槛 同步)
92
- const GATE = { minSample: 20, minPassRate: 0.9, maxAvgRework: 0.3, minDelegated: 5, maxFallbackRate: 0.1 };
91
+ // 门槛(2026-09-27 board-kb-p1 对齐 workflow/delegations.md §并发扩容门槛声明——doc 为权威):
92
+ // 月度口径项:一次完成率 ≥90% | 月度返工次数 = 0 | 无主兜底信号(=0)(旧「平均返工≤0.3 / 兜底≤10%」为宽松分叉,退役)
93
+ // 样本护栏项(脚本侧统计可信度门槛,非口径分叉):月样本量 ≥20 | 委派样本 ≥5
94
+ // 连续性:扩容需「本月 + 上一自然月」连续两月月度口径全达标(delegations.md「连续两个月」)
95
+ const GATE = { minSample: 20, minPassRate: 0.9, minDelegated: 5 };
93
96
 
94
97
  function metrics(rows) {
95
98
  // 未知结果(结果列拼错/漏填)单列计数,不混进「待修」——待修=未闭环,未知=记录本身有问题(2026-09-24 口径分离)
@@ -126,29 +129,26 @@ function fmtNum(x) {
126
129
  return x == null ? '—' : x.toFixed(2);
127
130
  }
128
131
 
129
- function gate(m, incidents) {
132
+ // gateMonth:单月门槛判定(pure,供测试)——口径项 + 样本护栏项分开列
133
+ function gateMonth(m) {
130
134
  const items = [];
131
- items.push({ no: 1, ok: m.total >= GATE.minSample, desc: `样本量 ${m.total}/${GATE.minSample}` });
132
- items.push({
133
- no: 2,
134
- ok: m.passRate != null && m.passRate >= GATE.minPassRate,
135
- desc: `一次通过率 ${fmtRate(m.passRate)}≥90%`,
136
- });
137
- items.push({
138
- no: 3,
139
- ok: m.avgRework != null && m.avgRework <= GATE.maxAvgRework,
140
- desc: `平均返工 ${fmtNum(m.avgRework)}≤0.3`,
141
- });
142
- const delegOk = m.delegatedValid >= GATE.minDelegated && m.fallbackRate != null && m.fallbackRate <= GATE.maxFallbackRate;
143
- items.push({
144
- no: 4,
145
- ok: delegOk,
146
- desc: `委派样本 ${m.delegatedValid}≥5 且主兜底 ${fmtRate(m.fallbackRate)}≤10%`,
147
- });
148
- items.push({ no: 5, ok: null, desc: '门禁不失守(人工对照当月 incident 定性)' });
149
- const hardOk = items.filter((i) => i.ok !== null).every((i) => i.ok);
150
- const bad = items.filter((i) => i.ok === false).map((i) => i.no).join('+');
151
- return { items, verdict: hardOk ? '✅(待人工确认门5)' : `❌ 未达标项:${bad}` };
135
+ items.push({ no: 1, ok: m.total >= GATE.minSample, desc: `样本量 ${m.total}/${GATE.minSample}`, kind: '样本护栏' });
136
+ items.push({ no: 2, ok: m.passRate != null && m.passRate >= GATE.minPassRate, desc: `一次完成率 ${fmtRate(m.passRate)}≥90%`, kind: '口径' });
137
+ items.push({ no: 3, ok: m.reworkSum === 0, desc: `月度返工次数 ${m.reworkSum}=0`, kind: '口径' });
138
+ items.push({ no: 4, ok: m.fallback === 0, desc: `主兜底 ${m.fallback}=0(无主兜底信号)`, kind: '口径' });
139
+ items.push({ no: 5, ok: m.delegatedValid >= GATE.minDelegated, desc: `委派样本 ${m.delegatedValid}≥5`, kind: '样本护栏' });
140
+ items.push({ no: 6, ok: null, desc: '门禁不失守(人工对照当月 incident 定性)', kind: '人工' });
141
+ return { items, monthOk: items.filter((i) => i.ok !== null).every((i) => i.ok) };
142
+ }
143
+
144
+ // expansionVerdict:连续两个月扩容判定(pure,供测试)——本月与上一自然月 monthOk 均真才「可扩容」;
145
+ // prevMissing = 无上月数据(首月/断档);items 供未达标项列出
146
+ function expansionVerdict(monthOk, prevOk, prevMissing, items) {
147
+ if (monthOk && prevOk) return '✅ 连续两月达标(可扩容,待人工确认门6)';
148
+ if (monthOk && prevMissing) return '⚠️ 本月达标(连续性 1/2——上月无数据)';
149
+ if (monthOk) return '⚠️ 本月达标(连续性 1/2——上月未达标)';
150
+ const bad = (items || []).filter((i) => i.ok === false).map((i) => i.no).join('+');
151
+ return `❌ 未达标(连续性中断):${bad || '月度口径项未全过'}`;
152
152
  }
153
153
 
154
154
  function main() {
@@ -165,14 +165,18 @@ function main() {
165
165
  console.log(`# 量化证据聚合(${argMonth || '全部月份'}) 生成于 ${new Date().toISOString().slice(0, 10)}\n`);
166
166
 
167
167
  const snapshotRows = [];
168
+ const monthResults = new Map(); // ym → { monthOk }(连续两月判定用)
168
169
  for (const ym of months) {
169
170
  const m = metrics(byMonth[ym]);
170
- const g = gate(m, incidents[ym] || 0);
171
+ const { items, monthOk } = gateMonth(m);
172
+ monthResults.set(ym, monthOk);
173
+ const prevYm = (() => { const [y, mo] = ym.split('-').map(Number); const d = new Date(Date.UTC(y, mo - 2, 1)); return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`; })();
174
+ const verdict = expansionVerdict(monthOk, monthResults.get(prevYm), !months.includes(prevYm), items);
171
175
  console.log(`## ${ym}`);
172
176
  console.log(` 有效任务 ${m.total}(待修 ${m.pending}${m.unknown ? `、未知结果 ${m.unknown}` : ''})|一次通过 ${m.pass}(${fmtRate(m.passRate)})|返工总次数 ${m.reworkSum}(平均 ${fmtNum(m.avgRework)})|主兜底 ${m.fallback}/${m.delegatedValid}(${fmtRate(m.fallbackRate)})|incident ${incidents[ym] || 0} 起`);
173
- for (const i of g.items) console.log(` 门${i.no} ${i.ok === null ? '[人工]' : i.ok ? '✅' : '❌'} ${i.desc}`);
174
- console.log(` 扩容门判定:${g.verdict}\n`);
175
- snapshotRows.push(`| ${ym} | ${m.total} | ${fmtRate(m.passRate)} | ${fmtNum(m.avgRework)} | ${fmtRate(m.fallbackRate)} | ${incidents[ym] || 0} | ${g.verdict} | 样本含待修${m.pending}${m.unknown ? `、未知${m.unknown}` : ''} |`);
177
+ for (const i of items) console.log(` 门${i.no}[${i.kind}] ${i.ok === null ? '[人工]' : i.ok ? '✅' : '❌'} ${i.desc}`);
178
+ console.log(` 扩容门判定:${verdict}\n`);
179
+ snapshotRows.push(`| ${ym} | ${m.total} | ${fmtRate(m.passRate)} | ${fmtNum(m.avgRework)} | ${fmtRate(m.fallbackRate)} | ${incidents[ym] || 0} | ${verdict} | 样本含待修${m.pending}${m.unknown ? `、未知${m.unknown}` : ''} |`);
176
180
  }
177
181
 
178
182
  if (!argMonth && rows.length) {
@@ -189,4 +193,9 @@ function main() {
189
193
  }
190
194
  }
191
195
 
192
- main();
196
+ // isMain 守卫 + 纯函数导出(board-kb-p1:gateMonth / expansionVerdict 供测试 import,import 不执行 main)
197
+ if (require.main === module) {
198
+ main();
199
+ } else {
200
+ module.exports = { parseResult, metrics, gateMonth, expansionVerdict, GATE };
201
+ }
@@ -87,6 +87,38 @@ const runAgg = (root) => spawnSync(process.execPath, [path.join(root, '.agents',
87
87
  fs.rmSync(root, { recursive: true, force: true });
88
88
  }
89
89
 
90
+ // ---- 场景 4-5:扩容门口径(2026-09-27 board-kb-p1 对齐 delegations.md:返工 0 / 无主兜底 / 连续两月)----
91
+ // 纯函数经 module.exports require(isMain 守卫后 import 不执行 main)
92
+ import { createRequire } from 'node:module';
93
+ const requireCjs = createRequire(import.meta.url);
94
+ const { metrics, gateMonth, expansionVerdict } = requireCjs(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
95
+
96
+ // 场景 4:审查反例——30 任务、2 主兜底、平均返工 0.25(旧口径四门全过,声明口径下必须 ❌)
97
+ {
98
+ const rows = [
99
+ ...Array.from({ length: 22 }, (_, i) => ({ scope: '委派', result: '一次通过' })),
100
+ ...Array.from({ length: 6 }, (_, i) => ({ scope: '委派', result: '返工×1' })),
101
+ { scope: '委派', result: '主兜底' }, { scope: '委派', result: '主兜底' },
102
+ ];
103
+ const m = metrics(rows);
104
+ const g = gateMonth(m);
105
+ check('场景 4:反例(2 主兜底 + 平均返工 0.25)门3 返工=0 ❌', g.items.find((i) => i.no === 3).ok === false, JSON.stringify(g.items));
106
+ check('场景 4:反例门4 主兜底=0 ❌ 且 monthOk=false(声明口径下不达标)',
107
+ g.items.find((i) => i.no === 4).ok === false && g.monthOk === false, `monthOk=${g.monthOk}`);
108
+ const good = metrics([{ scope: '自做', result: '一次通过' }, { scope: '委派', result: '一次通过' }]);
109
+ const g2 = gateMonth(good);
110
+ check('场景 4:返工 0 + 无主兜底月 → 口径项全 ✅(样本护栏项单独列出)',
111
+ g2.items.filter((i) => i.kind === '口径').every((i) => i.ok === true) && g2.items.find((i) => i.no === 1).ok === false,
112
+ JSON.stringify(g2.items));
113
+ }
114
+ // 场景 5:连续两月判定四态
115
+ {
116
+ check('场景 5:本月+上月均达标 → ✅ 可扩容', expansionVerdict(true, true, false, []).includes('✅'));
117
+ check('场景 5:本月达标、上月无数据 → ⚠️ 1/2', expansionVerdict(true, undefined, true, []).includes('⚠️'));
118
+ check('场景 5:本月达标、上月未达标 → ⚠️ 1/2', expansionVerdict(true, false, false, []).includes('⚠️'));
119
+ check('场景 5:本月未达标 → ❌ 且列出未达标项号', expansionVerdict(false, true, false, [{ no: 3, ok: false }, { no: 4, ok: false }]).match(/❌.*3\+4/));
120
+ }
121
+
90
122
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
91
123
  if (fail) {
92
124
  console.log(`\n${USAGE}`);