agentic-flow-kit 0.3.1 → 0.5.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 (87) hide show
  1. package/README.md +2 -0
  2. package/modules/hosts/omp/agents/implementer.md +45 -6
  3. package/modules/hosts/omp/agents/independent-reviewer.md +42 -5
  4. package/modules/hosts/omp/agents/ui-verifier.md +41 -6
  5. package/modules/hosts/opencode/agents/implementer.md +45 -1
  6. package/modules/hosts/opencode/agents/independent-reviewer.md +42 -1
  7. package/modules/hosts/opencode/agents/ui-verifier.md +41 -1
  8. package/modules/hosts/opencode/commands/build.md +55 -7
  9. package/modules/hosts/opencode/commands/deploy.md +51 -6
  10. package/modules/hosts/opencode/commands/design.md +34 -6
  11. package/modules/hosts/opencode/commands/maintain.md +71 -6
  12. package/modules/hosts/opencode/commands/new-task.md +59 -6
  13. package/modules/hosts/opencode/commands/plan.md +32 -6
  14. package/modules/hosts/opencode/commands/review.md +40 -6
  15. package/modules/hosts/opencode/commands/test.md +56 -8
  16. package/modules/hosts/trae/agents/implementer.md +44 -2
  17. package/modules/hosts/trae/agents/independent-reviewer.md +42 -1
  18. package/modules/hosts/trae/agents/ui-verifier.md +39 -3
  19. package/modules/hosts/trae/commands/wf-build.md +55 -7
  20. package/modules/hosts/trae/commands/wf-deploy.md +51 -6
  21. package/modules/hosts/trae/commands/wf-design.md +34 -6
  22. package/modules/hosts/trae/commands/wf-maintain.md +71 -6
  23. package/modules/hosts/trae/commands/wf-new-task.md +59 -6
  24. package/modules/hosts/trae/commands/wf-plan.md +32 -6
  25. package/modules/hosts/trae/commands/wf-review.md +40 -6
  26. package/modules/hosts/trae/commands/wf-test.md +56 -7
  27. package/modules/hosts/zcode/agents/implementer.md +45 -6
  28. package/modules/hosts/zcode/agents/independent-reviewer.md +42 -5
  29. package/modules/hosts/zcode/agents/ui-verifier.md +41 -5
  30. package/package.json +6 -3
  31. package/src/cli.mjs +15 -2
  32. package/src/doctor.mjs +140 -13
  33. package/src/init.test.mjs +25 -2
  34. package/src/pack.test.mjs +24 -0
  35. package/src/run-tests.mjs +4 -11
  36. package/src/sync-hosts.mjs +193 -0
  37. package/src/sync-hosts.test.mjs +170 -0
  38. package/src/sync.mjs +1 -0
  39. package/src/sync.test.mjs +10 -0
  40. package/templates/AGENTS.md +2 -2
  41. package/templates/_agents/commands/build.md +20 -0
  42. package/templates/_agents/commands/design.md +1 -0
  43. package/templates/_agents/commands/gate-checklist.md +91 -0
  44. package/templates/_agents/commands/maintain.md +1 -1
  45. package/templates/_agents/commands/plan.md +2 -1
  46. package/templates/_agents/commands/review.md +1 -0
  47. package/templates/_agents/commands/source-sync-check.md +103 -0
  48. package/templates/_agents/commands/sync-hosts.md +79 -0
  49. package/templates/_agents/commands/test.md +2 -1
  50. package/templates/_agents/notes/runtime-env.md +1 -1
  51. package/templates/_agents/rule-budgets.txt +1 -1
  52. package/templates/_agents/scripts/check-loop.mjs +517 -0
  53. package/templates/_agents/scripts/check-loop.sh +5 -808
  54. package/templates/_agents/scripts/check-loop.test.mjs +481 -0
  55. package/templates/_agents/scripts/doctor.test.mjs +298 -0
  56. package/templates/_agents/scripts/ensure-board.mjs +158 -0
  57. package/templates/_agents/scripts/fill-intent.mjs +59 -0
  58. package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
  59. package/templates/_agents/scripts/fill-plan.mjs +60 -0
  60. package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
  61. package/templates/_agents/scripts/fill-spec.mjs +52 -0
  62. package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
  63. package/templates/_agents/scripts/gate-checklist.mjs +192 -0
  64. package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
  65. package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
  66. package/templates/_agents/scripts/kb-search.mjs +3 -1
  67. package/templates/_agents/scripts/source-sync-check.mjs +131 -0
  68. package/templates/_agents/scripts/source-sync-check.test.mjs +156 -0
  69. package/templates/_agents/scripts/verify.mjs +8 -10
  70. package/templates/_agents/scripts/verify.test.mjs +13 -1
  71. package/templates/_agents/scripts/wf-journal.mjs +158 -0
  72. package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
  73. package/templates/_agents/scripts/workflow-board-server.mjs +18 -10
  74. package/templates/_agents/scripts/workflow-enums.mjs +68 -0
  75. package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
  76. package/templates/_agents/scripts/workflows-check.mjs +260 -0
  77. package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
  78. package/templates/_agents/workflow-enums.txt +19 -0
  79. package/templates/_agents/workflows/_TEMPLATE.md +74 -0
  80. package/templates/_agents/workflows/pipeline-closing.md +29 -0
  81. package/templates/_agents/workflows/source-sync-repair.md +30 -0
  82. package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
  83. package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
  84. package/templates/workflow/README.md +2 -2
  85. package/templates/_agents/cache/kb-index.json +0 -1
  86. package/templates/_agents/scripts/check-loop.test.sh +0 -1042
  87. package/templates/_agents/scripts/ensure-board.ps1 +0 -48
@@ -0,0 +1,67 @@
1
+ // fill-plan.test.mjs — fill-plan 工具测试(2026-09-25 doc-fill-tools)
2
+ import { renderPlan } from './fill-plan.mjs';
3
+
4
+ let pass = 0, failCount = 0;
5
+ function check(name, cond, detail = '') {
6
+ if (cond) { pass++; console.log('PASS ' + name); }
7
+ else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
8
+ }
9
+
10
+ // ---- 场景 1:L1 极简 2 节 ----
11
+ {
12
+ const { body } = renderPlan({ topic: 't', level: 'L1' });
13
+ 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 + ' 节');
17
+ }
18
+
19
+ // ---- 场景 2:L2 完整 4 节 ----
20
+ {
21
+ const { body } = renderPlan({ topic: 't', level: 'L2' });
22
+ const sections = body.match(/^##\s.+$/gm) || [];
23
+ check('S2 L2 含节 ## 改动面', body.includes('## 改动面'));
24
+ check('S2 L2 含节 ## 任务拆解', body.includes('## 任务拆解'));
25
+ check('S2 L2 含节 ## 执行顺序', body.includes('## 执行顺序'));
26
+ check('S2 L2 含节 ## 验证方式', body.includes('## 验证方式'));
27
+ check('S2 L2 共 5 节(4 节 + 确认与复核)', sections.length === 5, '实际 ' + sections.length + ' 节');
28
+ }
29
+
30
+ // ---- 场景 3:L3 与 L2 同结构 ----
31
+ {
32
+ const { body } = renderPlan({ topic: 't', level: 'L3' });
33
+ check('S3 L3 含节 ## 改动面', body.includes('## 改动面'));
34
+ check('S3 L3 含节 ## 任务拆解', body.includes('## 任务拆解'));
35
+ }
36
+
37
+ // ---- 场景 4:非法 level 抛错 ----
38
+ {
39
+ let threw = false;
40
+ try { renderPlan({ topic: 't', level: 'X' }); } catch { threw = true; }
41
+ check('S4 非法 level 抛错', threw);
42
+ }
43
+
44
+ // ---- 场景 5:默认 L1 ----
45
+ {
46
+ const { body } = renderPlan({ topic: 't' });
47
+ check('S5 默认级别 L1', /级别:\s*L1/.test(body.match(/^---([\s\S]*?)---/)[1]));
48
+ }
49
+
50
+ // ---- 场景 6:frontmatter 含 3 字段(与 _TEMPLATE 对齐——L1 plan 无日期字段)----
51
+ {
52
+ const { body } = renderPlan({ topic: 't', level: 'L1' });
53
+ const fm = body.match(/^---([\s\S]*?)---/)[1];
54
+ check('S6 frontmatter 含状态', /状态:/.test(fm));
55
+ check('S6 frontmatter 含级别', /级别:/.test(fm));
56
+ check('S6 frontmatter 含模块', /模块:/.test(fm));
57
+ }
58
+
59
+ // ---- 场景 7:与入口配对提示 ----
60
+ {
61
+ const { body } = renderPlan({ topic: 't', level: 'L1' });
62
+ check('S7 含「对应入口」段', body.includes('对应入口:'));
63
+ check('S7 含「对应 spec」段(L1 可省略)', body.includes('对应 spec:'));
64
+ }
65
+
66
+ console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
67
+ process.exit(failCount ? 1 : 0);
@@ -0,0 +1,52 @@
1
+ // flow-kit fill-spec:起草 spec 草稿(功能行为 / 数据流 / 系统改动 / 约束遵守映射 / 风险评估 共 5 节)
2
+ // 用法:node .agents/scripts/fill-spec.mjs --topic "test" --output /tmp/x.md
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { ENUMS } from './workflow-enums.mjs';
6
+
7
+ const TODAY = new Date().toISOString().slice(0, 10);
8
+ const LEVELS = ENUMS['level.all']; // 级别词表单源(.agents/workflow-enums.txt)
9
+
10
+ function parseArgs(argv) {
11
+ const out = { topic: null, level: 'L2', output: null };
12
+ for (let i = 0; i < argv.length; i++) {
13
+ const a = argv[i];
14
+ const next = () => { const v = argv[++i]; if (v === undefined) return null; return v; };
15
+ if (a === '--topic') out.topic = next();
16
+ else if (a === '--level') out.level = next();
17
+ else if (a === '--output') out.output = next();
18
+ }
19
+ return out;
20
+ }
21
+
22
+ function fail(msg) { console.error(`❌ ${msg}`); process.exit(1); }
23
+
24
+ const SECTIONS = [
25
+ { title: '功能行为', hint: '<完整描述本 spec 涉及的功能 / 行为变化;分用户场景列;含边界与异常>' },
26
+ { title: '数据流', hint: '<请求→处理→持久化→回应的端到端路径;标出各环节涉及的服务 / 表 / 缓存 / 第三方调用>' },
27
+ { title: '系统改动', hint: '<分模块列出本 spec 引入的所有改动;含新增 / 修改 / 删除;与 plan §改动面 一一对应>' },
28
+ { title: '约束遵守映射', hint: '<逐条对照根 AGENTS.md 与目录级 AGENTS.md(如有)的红线 / 约束,说明本 spec 如何满足;逐条回应而非简单列举>' },
29
+ { title: '风险评估', hint: '<列出本 spec 引入的风险点:数据 / 接口 / 性能 / 安全 / 回滚难度 / 用户影响;评估每个的等级与缓解措施>' },
30
+ ];
31
+
32
+ export function renderSpec({ topic, level = 'L2', date = TODAY }) {
33
+ if (!LEVELS.includes(level)) throw new Error(`level 不在枚举 ${LEVELS.join('|')}:${level}`);
34
+ const fm = `---\n状态: draft\n级别: ${level}\n日期: ${date}\n模块: pipeline\n备注: <spec 关联 intent / plan 的同名主题>\n---\n`;
35
+ 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`;
36
+ for (const { title, hint } of SECTIONS) {
37
+ body += `## ${title}\n\n${hint}\n\n`;
38
+ }
39
+ body += `## 确认与复核\n\n- 确认日期:\n- 复核:L3 必须独立复核(independent-reviewer,新会话);L2 推荐独立复核;L1 可省\n`;
40
+ return { frontmatter: fm, body: fm + body, sections: SECTIONS.map((s) => s.title) };
41
+ }
42
+
43
+ const isMain = import.meta.url === `file:///${process.argv[1]}` || process.argv[1]?.endsWith('fill-spec.mjs');
44
+ if (isMain) {
45
+ const a = parseArgs(process.argv.slice(2));
46
+ if (!a.topic || !a.output) fail('必填:--topic / --output;选填:--level(默认 L2)');
47
+ if (!LEVELS.includes(a.level)) fail(`level 必须在 ${LEVELS.join('|')}:${a.level}`);
48
+ const { body } = renderSpec(a);
49
+ fs.mkdirSync(path.dirname(a.output), { recursive: true });
50
+ fs.writeFileSync(a.output, body);
51
+ console.log(`✅ 已生成 spec 草稿:${a.output}(状态 draft / 级别 ${a.level})`);
52
+ }
@@ -0,0 +1,61 @@
1
+ // fill-spec.test.mjs — fill-spec 工具测试(2026-09-25 doc-fill-tools)
2
+ import { renderSpec } from './fill-spec.mjs';
3
+
4
+ let pass = 0, failCount = 0;
5
+ function check(name, cond, detail = '') {
6
+ if (cond) { pass++; console.log('PASS ' + name); }
7
+ else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
8
+ }
9
+
10
+ // ---- 场景 1:5 节正文齐 ----
11
+ {
12
+ const { body } = renderSpec({ topic: 'test', level: 'L2' });
13
+ const expectedSections = ['功能行为', '数据流', '系统改动', '约束遵守映射', '风险评估', '确认与复核'];
14
+ for (const s of expectedSections) check('S1 含节 ## ' + s, body.includes('## ' + s));
15
+ check('S1 共 6 节标题', (body.match(/^##\s/gm) || []).length === 6);
16
+ }
17
+
18
+ // ---- 场景 2:frontmatter 含 5 字段 ----
19
+ {
20
+ const { body } = renderSpec({ topic: 't', level: 'L2' });
21
+ const fm = body.match(/^---([\s\S]*?)---/);
22
+ check('S2 frontmatter 含状态 draft', /状态:\s*draft/.test(fm[1]));
23
+ check('S2 frontmatter 含级别 L2', /级别:\s*L2/.test(fm[1]));
24
+ check('S2 frontmatter 含日期', /日期:\s*\d{4}-\d{2}-\d{2}/.test(fm[1]));
25
+ check('S2 frontmatter 含模块', /模块:/.test(fm[1]));
26
+ check('S2 frontmatter 含备注', /备注:/.test(fm[1]));
27
+ }
28
+
29
+ // ---- 场景 3:L3 也合法(默认 L2,但显式 L3 也接受)----
30
+ {
31
+ const { body } = renderSpec({ topic: 't', level: 'L3' });
32
+ check('S3 L3 合法', /级别:\s*L3/.test(body.match(/^---([\s\S]*?)---/)[1]));
33
+ }
34
+
35
+ // ---- 场景 4:非法 level 抛错 ----
36
+ {
37
+ let threw = false;
38
+ try { renderSpec({ topic: 't', level: 'XX' }); } catch { threw = true; }
39
+ check('S4 非法 level 抛错', threw);
40
+ }
41
+
42
+ // ---- 场景 5:默认 L2 ----
43
+ {
44
+ const { body } = renderSpec({ topic: 't' });
45
+ check('S5 默认级别 L2', /级别:\s*L2/.test(body.match(/^---([\s\S]*?)---/)[1]));
46
+ }
47
+
48
+ // ---- 场景 6:日期透传 ----
49
+ {
50
+ const { body } = renderSpec({ topic: 't', date: '2026-12-31' });
51
+ check('S6 自定义日期透传', body.includes('日期: 2026-12-31'));
52
+ }
53
+
54
+ // ---- 场景 7:L3 必填提示独立复核 ----
55
+ {
56
+ const { body } = renderSpec({ topic: 't', level: 'L3' });
57
+ check('S7 L3 节确认与复核含独立复核提示', body.includes('L3 必须独立复核'));
58
+ }
59
+
60
+ console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
61
+ process.exit(failCount ? 1 : 0);
@@ -0,0 +1,192 @@
1
+ // flow-kit gate-checklist:三处口径一致性检查(doctor §检查项 ↔ check-loop §检查项,显式配对登记表)
2
+ // 目的:任何一处加新检查项,另一处不会自动同步——本工具按登记表给出对照表 + 断档/未登记告警。
3
+ // 机制(2026-09-25 gate-checklist-registry 重写):关键词猜测匹配退役(实测 18×14 项只匹配 2、报 28 条
4
+ // 噪声缺点,真漏点不可见),改为显式配对登记表 PAIRS(单源本文件)——沿 workflow-modules.txt
5
+ // 的单源思路与 check-loop「frontmatter 受限子集替代全文正则猜测」同一演进路径。
6
+ // 解析面(只读不改两处源码,均为既有稳定结构):
7
+ // doctor:src/doctor.mjs 节注释 `// N. 标题`(N 支持 6.5 小节号)
8
+ // check-loop:.agents/scripts/check-loop.sh 头部检查项注释 `# N. 标题 [severity]`(多行标题截断仅影响展示,配对按 id)
9
+ // 登记三形态:
10
+ // { doctor, cl } 直接配对——同一检查两处实现
11
+ // { doctor, cl: null } 声明独有——doctor 装户体检面,无 check-loop 对应(原 intent「只报告不修复」拍板的已知漏点入账)
12
+ // { doctor: '7', cl } 经 §7 覆盖——doctor §7 整体运行 check-loop,各 #N 自动流入 doctor
13
+ // 报告(只报告不修复,B-b;exit 恒 0):
14
+ // 断档:登记表声明的 id 在对应侧已不存在(检查项被删/改号而登记未跟)
15
+ // 未登记:任一侧出现登记表之外的 § 检查项(新检查项没对账——本工具的核心信号)
16
+ // 用法:node .agents/scripts/gate-checklist.mjs --diff
17
+ // node .agents/scripts/gate-checklist.mjs --json
18
+ // 零依赖;不修改 doctor / check-loop 既有检查项
19
+ // 测试:node templates/_agents/scripts/gate-checklist.test.mjs(fixture + 真实仓库 baseline)
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+
23
+ function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
24
+
25
+ // parseDoctorSections:扫 doctor.mjs 节注释 → [{id, title}]。两种真实格式:
26
+ // `// N. 标题`(整数节号带点,如 // 1. Node 版本)与 `// N.M 标题`(小节号不带点,如 // 6.5 delegations)。
27
+ // 整数无点(如正文注释 // 4 宿主目录映射)不匹配——避免函数体普通注释误判为节。
28
+ function parseDoctorSections(doctorSrc) {
29
+ const out = [];
30
+ const re = /^\s*\/\/\s+(\d+\.\d+|\d+\.)\s+([^\n]+)/gm;
31
+ let m;
32
+ while ((m = re.exec(doctorSrc)) !== null) {
33
+ const title = m[2].split(/[((]/)[0].trim() || m[2].trim();
34
+ out.push({ id: m[1].replace(/\.$/, ''), title });
35
+ }
36
+ return out;
37
+ }
38
+
39
+ // parseCheckLoopChecks:扫 check-loop 检查项注释(`// N. 标题 [severity]` 于 .mjs 头部清单——2026-09-26
40
+ // check-loop-node 迁移后的主位;旧装户 sync 前只有 .sh 时按 `# N. 标题` fallback,登记表口径不变)
41
+ function parseCheckLoopChecks(clSrc) {
42
+ const checks = [];
43
+ const re = /^\s*(?:\/\/|#)\s*(\d+)\.\s+(.+?)(?:\s*\[(hard-block|warning|advisory)\])?\s*$/gm;
44
+ let m;
45
+ while ((m = re.exec(clSrc)) !== null) {
46
+ checks.push({ id: m[1], title: m[2].trim(), severity: m[3] || null });
47
+ }
48
+ return checks;
49
+ }
50
+
51
+ // ---- 配对登记表(单源契约;2026-09-25 gate-checklist-registry 全量登记)----
52
+ // 纪律:doctor / check-loop 任一处加新 § 检查项,必须同步在本表登记(直接配对 / 声明独有 / 经 §7 覆盖),
53
+ // 否则工具报「未登记」。删/改检查项须同步改本表,否则报「断档」。
54
+ const PAIRS = [
55
+ // 直接配对:同一检查两处实现
56
+ { doctor: '5', cl: '2', note: '占位符残留' },
57
+ { doctor: '6', cl: '11', note: 'workflow/INDEX.md 漂移' },
58
+ // 声明独有:doctor 装户体检面(原 intent 拍板「只报告不修复」的已知漏点入账)
59
+ { doctor: '1', cl: null, note: 'Node 版本——装户体检独有' },
60
+ { doctor: '2', cl: null, note: '目录布局——装户体检独有' },
61
+ { doctor: '3', cl: null, note: 'git 仓库与钩子——装户体检独有' },
62
+ { doctor: '4', cl: null, note: 'kit.json managed 台账——装户体检独有' },
63
+ { doctor: '6.5', cl: null, note: 'delegations 台账结构——装户体检独有' },
64
+ { doctor: '6.6', cl: null, note: 'owned 漂移——装户体检独有' },
65
+ { doctor: '6.7', cl: null, note: '跨宿主薄适配——装户体检独有' },
66
+ { doctor: '6.8', cl: null, note: 'workflows 编排脚本 lint——装户体检独有' },
67
+ { doctor: '8', cl: null, note: '看板端口——信息级独有' },
68
+ // 经 §7 覆盖:doctor §7 整体运行 check-loop(#2/#11 已直接配对,不重复登记)
69
+ { doctor: '7', cl: '1', note: '经 §7 整体运行覆盖' },
70
+ { doctor: '7', cl: '3', note: '经 §7 整体运行覆盖' },
71
+ { doctor: '7', cl: '4', note: '经 §7 整体运行覆盖' },
72
+ { doctor: '7', cl: '5', note: '经 §7 整体运行覆盖' },
73
+ { doctor: '7', cl: '6', note: '经 §7 整体运行覆盖' },
74
+ { doctor: '7', cl: '7', note: '经 §7 整体运行覆盖' },
75
+ { doctor: '7', cl: '8', note: '经 §7 整体运行覆盖' },
76
+ { doctor: '7', cl: '9', note: '经 §7 整体运行覆盖' },
77
+ { doctor: '7', cl: '10', note: '经 §7 整体运行覆盖' },
78
+ { doctor: '7', cl: '12', note: '经 §7 整体运行覆盖' },
79
+ { doctor: '7', cl: '13', note: '经 §7 整体运行覆盖' },
80
+ { doctor: '7', cl: '14', note: '经 §7 整体运行覆盖' },
81
+ ];
82
+
83
+ // gateChecklist:按登记表对照两侧检查项(pure function;pairs 可注入供测试)
84
+ // 返回 { doctorCount, checkLoopCount, pairs, matched, broken, unregistered, doctor, checkLoop }
85
+ // matched = 登记两侧都存在的条目(直接配对 + 经 §7 覆盖)
86
+ // broken = 登记声明的 id 在对应侧已不存在
87
+ // unregistered = 两侧出现登记表之外的 § 检查项(新检查项未对账——核心信号)
88
+ export function gateChecklist({ doctorSrc, checkLoopSrc, pairs = PAIRS }) {
89
+ const doctor = parseDoctorSections(doctorSrc);
90
+ const checkLoop = parseCheckLoopChecks(checkLoopSrc);
91
+ const doctorIds = new Set(doctor.map((d) => d.id));
92
+ const clIds = new Set(checkLoop.map((c) => c.id));
93
+ const declaredDoctor = new Set(pairs.map((p) => p.doctor).filter(Boolean));
94
+ const declaredCl = new Set(pairs.map((p) => p.cl).filter(Boolean));
95
+
96
+ const broken = [];
97
+ for (const p of pairs) {
98
+ if (p.doctor && !doctorIds.has(p.doctor)) broken.push({ side: 'doctor', id: p.doctor, note: p.note || '' });
99
+ if (p.cl && !clIds.has(p.cl)) broken.push({ side: 'check-loop', id: p.cl, note: p.note || '' });
100
+ }
101
+ const unregistered = [];
102
+ for (const d of doctor) {
103
+ if (!declaredDoctor.has(d.id)) unregistered.push({ side: 'doctor', id: d.id, title: d.title });
104
+ }
105
+ for (const c of checkLoop) {
106
+ if (!declaredCl.has(c.id)) unregistered.push({ side: 'check-loop', id: c.id, title: c.title });
107
+ }
108
+ const matched = pairs.filter((p) => p.doctor && doctorIds.has(p.doctor) && p.cl && clIds.has(p.cl));
109
+ return {
110
+ doctorCount: doctor.length,
111
+ checkLoopCount: checkLoop.length,
112
+ pairs,
113
+ matched,
114
+ broken,
115
+ unregistered,
116
+ doctor,
117
+ checkLoop,
118
+ };
119
+ }
120
+
121
+ const clip = (s, n) => (String(s).length > n ? String(s).slice(0, n) + '…' : String(s));
122
+
123
+ function printDiff(result) {
124
+ console.log('▶ flow-kit gate-checklist --diff(显式配对登记表)');
125
+ const direct = result.pairs.filter((p) => p.doctor !== '7' && p.cl).length;
126
+ const via = result.pairs.filter((p) => p.doctor === '7').length;
127
+ const solo = result.pairs.filter((p) => !p.cl).length;
128
+ console.log(` doctor §检查项: ${result.doctorCount} | check-loop §检查项: ${result.checkLoopCount} | 直接配对 ${direct} + 经 §7 覆盖 ${via} + 声明独有 ${solo}`);
129
+ const doctorById = new Map(result.doctor.map((d) => [d.id, d]));
130
+ const clById = new Map(result.checkLoop.map((c) => [c.id, c]));
131
+ console.log(' 对照表(登记表展开):');
132
+ for (const p of result.pairs) {
133
+ const d = doctorById.get(p.doctor);
134
+ if (!p.cl) {
135
+ console.log(` ● §${p.doctor} ${clip(d?.title || '?', 22)}(声明独有:${p.note})`);
136
+ } else {
137
+ const c = clById.get(p.cl);
138
+ const mark = d && c ? '✓' : '✗';
139
+ console.log(` ${mark} §${p.doctor} ${clip(d?.title || '?', 22)} ↔ #${p.cl} ${clip(c?.title || '?', 26)}(${p.note})`);
140
+ }
141
+ }
142
+ if (result.broken.length) {
143
+ console.log(' 断档(登记的检查项已不存在——删/改检查项后登记表未跟):');
144
+ for (const b of result.broken) {
145
+ console.log(` ⚠️ ${b.side === 'doctor' ? 'doctor §' + b.id : 'check-loop #' + b.id} 已不存在(登记备注:${b.note || '无'})`);
146
+ }
147
+ }
148
+ if (result.unregistered.length) {
149
+ console.log(' 未登记(新检查项未对账——配对 / 声明独有 / 经 §7 三选一登记进 PAIRS):');
150
+ for (const u of result.unregistered) {
151
+ console.log(` ⚠️ ${u.side === 'doctor' ? 'doctor §' + u.id : 'check-loop #' + u.id} ${clip(u.title, 30)}`);
152
+ }
153
+ }
154
+ if (!result.broken.length && !result.unregistered.length) {
155
+ console.log(' ✅ 登记完整(0 断档 / 0 未登记)');
156
+ }
157
+ }
158
+
159
+ const isMain = process.argv[1] && process.argv[1].endsWith('gate-checklist.mjs');
160
+ if (isMain) {
161
+ let jsonMode = false;
162
+ for (const a of process.argv.slice(2)) {
163
+ if (a === '--json') jsonMode = true;
164
+ else if (a === '--diff') { /* 默认模式,兼容显式传参 */ }
165
+ else fail('未知选项:' + a + '(flow-kit gate-checklist --diff / --json)');
166
+ }
167
+ const pkgRoot = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : null;
168
+ const base = pkgRoot || process.cwd();
169
+ const doctorPath = path.join(base, 'src', 'doctor.mjs');
170
+ const clPathMjs = path.join(base, '.agents', 'scripts', 'check-loop.mjs');
171
+ const clPath = fs.existsSync(clPathMjs) ? clPathMjs : path.join(base, '.agents', 'scripts', 'check-loop.sh');
172
+ if (!fs.existsSync(doctorPath)) fail('找不到 doctor.mjs:' + doctorPath);
173
+ if (!fs.existsSync(clPath)) fail('找不到 check-loop(.mjs / .sh 均无):' + clPath);
174
+ const doctorSrc = fs.readFileSync(doctorPath, 'utf8');
175
+ const checkLoopSrc = fs.readFileSync(clPath, 'utf8');
176
+ const result = gateChecklist({ doctorSrc, checkLoopSrc });
177
+ if (jsonMode) {
178
+ process.stdout.write(JSON.stringify({
179
+ doctorCount: result.doctorCount,
180
+ checkLoopCount: result.checkLoopCount,
181
+ matched: result.matched.map((p) => ({ doctor: p.doctor, cl: p.cl, note: p.note || '' })),
182
+ solo: result.pairs.filter((p) => !p.cl).map((p) => ({ doctor: p.doctor, note: p.note || '' })),
183
+ broken: result.broken,
184
+ unregistered: result.unregistered,
185
+ }, null, 2) + '\n');
186
+ } else {
187
+ printDiff(result);
188
+ }
189
+ // diff/json 模式总 exit 0;断档/未登记不视作错误(人类阅读用;B-b 决策「只报告不修复」)
190
+ process.exit(0);
191
+ }
192
+ export default gateChecklist;
@@ -0,0 +1,204 @@
1
+ // gate-checklist.test.mjs — 显式配对登记表测试(2026-09-25 gate-checklist-registry)
2
+ // 方法:fixture mini doctor.mjs / check-loop.sh 源码 + 注入 pairs,调 gateChecklist() 拿结构化结果;
3
+ // 真实仓库两场景做 baseline;--json CLI spawn 冒烟回归原「字段丢失」bug。
4
+ // 判据:① 两侧解析正确 ② 三种登记形态不误报 ③ 断档/未登记检测命中 ④ 真实仓库 0 断档 0 未登记
5
+ // 用法:node templates/_agents/scripts/gate-checklist.test.mjs(npm test 随跑)
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { gateChecklist } from './gate-checklist.mjs';
11
+
12
+ let pass = 0;
13
+ let failCount = 0;
14
+ function check(name, cond, detail = '') {
15
+ if (cond) { pass++; console.log('PASS ' + name); }
16
+ else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
17
+ }
18
+
19
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
20
+ const SRC_ROOT = path.resolve(SCRIPT_DIR, '..', '..', '..');
21
+
22
+ // fixture:5 节 doctor(1 / 5 / 6.5 / 7 / 9)+ 2 项 check-loop(#2 / #3)
23
+ const FIXTURE_DOCTOR = `// doctor fixture
24
+ function f() {
25
+ // 1. Node 版本(装户体检独有)
26
+ const a = 1;
27
+ // 5. 占位符残留(与 check-loop #2 直接配对)
28
+ const b = 2;
29
+ // 6.5 delegations 台账结构(小节号解析)
30
+ const c = 3;
31
+ // 7. check-loop(fixture 里也有 §7,供经 §7 覆盖登记)
32
+ const e = 5;
33
+ // 9. 未登记的新检查项(应报未登记)
34
+ const d = 4;
35
+ return [a, b, c, d, e];
36
+ }
37
+ `;
38
+ const FIXTURE_CL = `#!/bin/sh
39
+ # 2. 模板字段占位符残留 [warning]
40
+ # 3. incidents 复盘三件套完整性
41
+ # 7. 未登记的新检查项
42
+ exit 0
43
+ `;
44
+ // fixture 登记表:5↔2 直接配对、1 声明独有、6.5 声明独有、7→#3 经 §7 覆盖;
45
+ // §99 / #99 是登记了但两侧都不存在的 id(应报断档×2)
46
+ const FIXTURE_PAIRS = [
47
+ { doctor: '5', cl: '2', note: '占位符' },
48
+ { doctor: '1', cl: null, note: '独有' },
49
+ { doctor: '6.5', cl: null, note: '独有' },
50
+ { doctor: '7', cl: '3', note: '经 §7' },
51
+ { doctor: '99', cl: null, note: '断档——doctor 侧不存在' },
52
+ { doctor: '1', cl: '99', note: '断档——check-loop 侧不存在' },
53
+ ];
54
+
55
+ const run = (pairs = FIXTURE_PAIRS, doctorSrc = FIXTURE_DOCTOR, clSrc = FIXTURE_CL) =>
56
+ gateChecklist({ doctorSrc, checkLoopSrc: clSrc, pairs });
57
+
58
+ // ---- S1 解析:doctor 5 节(含 6.5 小节号、§7 与未登记的 9)----
59
+ {
60
+ const r = run();
61
+ check('S1 fixture doctor 解析 5 节且含 6.5 小节号',
62
+ r.doctorCount === 5 && r.doctor.some((d) => d.id === '6.5' && d.title.includes('delegations')),
63
+ JSON.stringify(r.doctor));
64
+ }
65
+
66
+ // ---- S2 解析:check-loop 3 项(#2 带 severity,#3 无)----
67
+ {
68
+ const r = run();
69
+ const c2 = r.checkLoop.find((c) => c.id === '2');
70
+ const c3 = r.checkLoop.find((c) => c.id === '3');
71
+ check('S2 fixture check-loop 解析 3 项且 severity 提取正确',
72
+ r.checkLoopCount === 3 && c2.severity === 'warning' && c3.severity === null,
73
+ JSON.stringify(r.checkLoop));
74
+ }
75
+
76
+ // ---- S3 直接配对:5↔2 双侧存在 → matched 命中 ----
77
+ {
78
+ const r = run();
79
+ check('S3 直接配对 §5↔#2 进入 matched',
80
+ r.matched.some((p) => p.doctor === '5' && p.cl === '2'),
81
+ JSON.stringify(r.matched.map((p) => [p.doctor, p.cl])));
82
+ }
83
+
84
+ // ---- S4 声明独有:§1 / §6.5 不产生任何 findings ----
85
+ {
86
+ const r = run();
87
+ const hit = [...r.broken, ...r.unregistered].filter((f) => f.id === '1' || f.id === '6.5');
88
+ check('S4 声明独有(§1 / §6.5)不误报',
89
+ hit.length === 0,
90
+ JSON.stringify(hit));
91
+ }
92
+
93
+ // ---- S5 经 §7 覆盖:7→#3 不产生 findings 且进 matched ----
94
+ {
95
+ const r = run();
96
+ check('S5 经 §7 覆盖登记不误报且进 matched',
97
+ r.matched.some((p) => p.doctor === '7' && p.cl === '3') && !r.unregistered.some((u) => u.id === '3'),
98
+ JSON.stringify(r.unregistered));
99
+ }
100
+
101
+ // ---- S6 断档:登记 §99 / #99 两侧不存在 → broken ×2 ----
102
+ {
103
+ const r = run();
104
+ const bDoc = r.broken.find((b) => b.side === 'doctor' && b.id === '99');
105
+ const bCl = r.broken.find((b) => b.side === 'check-loop' && b.id === '99');
106
+ check('S6 登记断档两侧各报一条(doctor §99 / check-loop #99)',
107
+ r.broken.length === 2 && !!bDoc && !!bCl,
108
+ JSON.stringify(r.broken));
109
+ }
110
+
111
+ // ---- S7 未登记:doctor §9 与 check-loop #7 → unregistered ×2 ----
112
+ {
113
+ const r = run();
114
+ const uDoc = r.unregistered.find((u) => u.side === 'doctor' && u.id === '9');
115
+ const uCl = r.unregistered.find((u) => u.side === 'check-loop' && u.id === '7');
116
+ check('S7 未登记两侧各报一条(doctor §9 / check-loop #7)',
117
+ r.unregistered.length === 2 && !!uDoc && !!uCl,
118
+ JSON.stringify(r.unregistered));
119
+ }
120
+
121
+ // ---- S8 空源码 + 空登记表不崩(0 findings)----
122
+ {
123
+ const r = run([], '', '');
124
+ check('S8 空源码 + 空登记表不崩(0/0 且 0 findings)',
125
+ r.doctorCount === 0 && r.checkLoopCount === 0 && r.broken.length === 0 && r.unregistered.length === 0,
126
+ JSON.stringify(r));
127
+ }
128
+
129
+ // ---- S8b 整数无点注释不误判为节(doctor.mjs:257 // 4 宿主目录映射 形态)----
130
+ {
131
+ const doctorSrc = `// 4. kit.json managed 台账(真节)
132
+ const x = 1;
133
+ // 4 宿主目录映射(与 profiles 同源——正文注释,非节)
134
+ const y = 2;
135
+ `;
136
+ const r = gateChecklist({ doctorSrc, checkLoopSrc: '', pairs: [{ doctor: '4', cl: null, note: '独有' }] });
137
+ check('S8b 整数无点的正文注释不误判(仅解析 1 节 §4 且标题为真节)',
138
+ r.doctorCount === 1 && r.doctor[0].title.includes('kit.json') && r.broken.length === 0 && r.unregistered.length === 0,
139
+ JSON.stringify(r.doctor));
140
+ }
141
+
142
+ // ---- S9 重复 id 登记(同 cl 两个直接配对)不崩且均可命中 ----
143
+ {
144
+ const pairs = [
145
+ { doctor: '5', cl: '2', note: 'a' },
146
+ { doctor: '1', cl: '2', note: 'b' },
147
+ ];
148
+ const r = run(pairs);
149
+ check('S9 多条目登记同一 cl 不崩(matched 2)',
150
+ r.matched.length === 2 && r.unregistered.some((u) => u.side === 'check-loop' && u.id === '3'),
151
+ JSON.stringify({ m: r.matched.length, u: r.unregistered }));
152
+ }
153
+
154
+ const realDoctor = () => fs.readFileSync(path.join(SRC_ROOT, 'src', 'doctor.mjs'), 'utf8');
155
+ // check-loop 解析面与 CLI 同口径:.mjs 优先(2026-09-26 迁移后主位),fallback .sh(旧装户)
156
+ const realCl = () => {
157
+ const mjs = path.join(SRC_ROOT, '.agents', 'scripts', 'check-loop.mjs');
158
+ return fs.readFileSync(fs.existsSync(mjs) ? mjs : path.join(SRC_ROOT, '.agents', 'scripts', 'check-loop.sh'), 'utf8');
159
+ };
160
+ const hasReal = fs.existsSync(path.join(SRC_ROOT, 'src', 'doctor.mjs'));
161
+
162
+ // ---- S10 真实仓库 baseline:PAIRS 全量登记 → 0 断档 / 0 未登记 ----
163
+ if (hasReal) {
164
+ const r = gateChecklist({ doctorSrc: realDoctor(), checkLoopSrc: realCl() });
165
+ check('S10 真实仓库登记完整(0 断档 / 0 未登记)',
166
+ r.broken.length === 0 && r.unregistered.length === 0,
167
+ JSON.stringify({ broken: r.broken, unregistered: r.unregistered }));
168
+ }
169
+
170
+ // ---- S11 真实仓库计数:doctor ≥ 12 节 / check-loop ≥ 14 项 ----
171
+ if (hasReal) {
172
+ const r = gateChecklist({ doctorSrc: realDoctor(), checkLoopSrc: realCl() });
173
+ check('S11 真实仓库 doctor ≥ 12 节且 check-loop ≥ 14 项(baseline)',
174
+ r.doctorCount >= 12 && r.checkLoopCount >= 14,
175
+ `doctor ${r.doctorCount} / check-loop ${r.checkLoopCount}`);
176
+ }
177
+
178
+ // ---- S12 --json CLI 冒烟:键齐无 undefined(回归原 .title 字段丢失 bug)----
179
+ if (hasReal) {
180
+ const p = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gate-checklist.mjs'), '--json'], { cwd: SRC_ROOT, encoding: 'utf8' });
181
+ let j = null;
182
+ try { j = JSON.parse(p.stdout); } catch { /* fail below */ }
183
+ const keysOk = j && ['doctorCount', 'checkLoopCount', 'matched', 'solo', 'broken', 'unregistered'].every((k) => k in j);
184
+ const noHoles = j && JSON.stringify(j).includes('undefined') === false
185
+ && j.matched.every((m) => typeof m.doctor === 'string' && typeof m.cl === 'string' && 'note' in m);
186
+ check('S12 --json 可解析、键齐、条目无 undefined 丢字段、exit 0',
187
+ p.status === 0 && !!keysOk && !!noHoles,
188
+ String(p.stdout).slice(0, 200));
189
+ }
190
+
191
+ // ---- S13 doctor 多行节注释:首行即标题(6.6 形态)解析不漏 ----
192
+ {
193
+ const doctorSrc = `// 6.6 owned 漂移校验(kit.owned 列表盘面 sha 不一致;
194
+ // 漂移信号靠本校验给装户可见性)
195
+ const x = 1;
196
+ `;
197
+ const r = gateChecklist({ doctorSrc, checkLoopSrc: '', pairs: [{ doctor: '6.6', cl: null, note: '独有' }] });
198
+ check('S13 多行节注释(§6.6 形态)首行命中且登记不报 findings',
199
+ r.doctorCount === 1 && r.doctor[0].title.includes('owned') && r.broken.length === 0 && r.unregistered.length === 0,
200
+ JSON.stringify(r.doctor));
201
+ }
202
+
203
+ console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
204
+ process.exit(failCount ? 1 : 0);
@@ -10,6 +10,7 @@
10
10
  // 生成区:INDEX.md 的 GENERATED:BEGIN/END 锚点注释之间整段重写,锚点外内容原样保留。
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
+ import { ENUMS } from './workflow-enums.mjs';
13
14
 
14
15
  const USAGE = `用法:node .agents/scripts/gen-workflow-index.mjs [--dry-run] [--check] [--help]
15
16
 
@@ -32,14 +33,15 @@ const WORKFLOW = 'workflow';
32
33
  const INDEX_P = path.join(WORKFLOW, 'INDEX.md');
33
34
  const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
34
35
  const TYPE_LABEL = { intents: 'INTENT', specs: 'SPEC', plans: 'PLAN', incidents: 'INCIDENT' };
35
- const ACTIVE_STATUS = ['draft', 'approved', 'open']; // 其余(done/fixed/closed/superseded/cancelled)折叠进档案计数
36
+ // 活跃口径 = 单源 doc.status.active ∪ incident.status.active;其余状态折叠进档案计数
37
+ const ACTIVE_STATUS = [...ENUMS['doc.status.active'], ...ENUMS['incident.status.active']];
36
38
 
37
39
  const BEGIN = '<!-- GENERATED:BEGIN — gen-workflow-index.mjs 整段重写,手工说明写在本行之前 -->';
38
40
  const END = '<!-- GENERATED:END -->';
39
41
  const HEADER = `# workflow 索引 — 活跃层与档案(生成物,勿手改)
40
42
 
41
43
  > 生成器 \`node .agents/scripts/gen-workflow-index.mjs\`(状态变更 / 新建文档后重跑;\`--check\` 校验漂移,check-loop 会告警)。
42
- > 活跃 = draft / approved / open(在跑);其余状态折叠进档案计数,检索用 \`node .agents/scripts/kb-search.mjs "<词>"\`(默认含全部状态、活跃优先排序)。
44
+ > 活跃 = ${ACTIVE_STATUS.join(' / ')}(在跑);其余状态折叠进档案计数,检索用 \`node .agents/scripts/kb-search.mjs "<词>"\`(默认含全部状态、活跃优先排序)。
43
45
  > 模块词表见 \`.agents/workflow-modules.txt\`。
44
46
  `;
45
47
 
@@ -16,6 +16,7 @@ import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  import { createHash } from 'node:crypto';
18
18
  import { fileURLToPath } from 'node:url';
19
+ import { ENUMS } from './workflow-enums.mjs';
19
20
 
20
21
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
21
22
  const MODULES_FILE = path.join(SCRIPT_DIR, '..', 'workflow-modules.txt');
@@ -78,7 +79,8 @@ if (moduleFilter && !MODULES.includes(moduleFilter)) {
78
79
  process.exit(1);
79
80
  }
80
81
 
81
- const ACTIVE_STATUS = ['draft', 'approved', 'open']; // 与 workflow/INDEX.md 活跃口径一致(fixed 归档案,默认也可命中)
82
+ // 活跃口径 = 单源 doc.status.active ∪ incident.status.active(与 workflow/INDEX.md 活跃层一致,fixed 归档案默认也可命中)
83
+ const ACTIVE_STATUS = [...ENUMS['doc.status.active'], ...ENUMS['incident.status.active']];
82
84
  const WF_SECTIONS = ['背景', '目标', '根因', '复盘三件套', '验收标准', '影响面'];
83
85
  const WIKI_EXCLUDE = new Set(['INDEX.md', '知识沉淀总览.html']);
84
86
  const WIKI_MAX_KB = 180;