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,150 @@
1
+ #!/usr/bin/env node
2
+ // wf-journal.test.mjs — run journal 测试(2026-09-26 orchestration-journal-human)
3
+ // 方法:纯函数(replayRun/planStages/latestRun)fixture 直测 + CLI add/status spawn 冒烟(临时 journal + 临时 workflows 目录)
4
+ // + 真实仓库 pipeline-closing 解析(human 形态行在位)。
5
+ // 判据:① schema 各字段与 attempt/run 语义 ② 末次生效 ③ 就绪/待定/中止分层 ④ 坏行容忍 ⑤ 9 列/human 列两代表全解析
6
+ // 用法:node templates/_agents/scripts/wf-journal.test.mjs(npm test 随跑)
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { replayRun, planStages, latestRun } from './wf-journal.mjs';
13
+ import { parseStages } from './workflows-check.mjs';
14
+
15
+ let pass = 0;
16
+ let fail = 0;
17
+ const check = (desc, cond, detail = '') => {
18
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
19
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
20
+ };
21
+
22
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
23
+ const JOURNAL_CLI = path.join(SCRIPT_DIR, 'wf-journal.mjs');
24
+
25
+ const mkline = (over = {}) => JSON.stringify({ ts: '2026-09-26T10:00:00Z', run: 'w@20260926-1000', wf: 'w', stage: 'a', status: 'pass', attempt: 1, ...over });
26
+ const STAGES = parseStages(`| id | after | role | step | task | files | accept | gate | human | retries |
27
+ |----|-------|------|------|------|-------|--------|------|-------|---------|
28
+ | a | — | implementer | — | 做 A | src/** | 过 | | | |
29
+ | ask | a | — | — | 用户确认 | — | 一句可以 | | 用户确认 | |
30
+ | b | ask | — | — | — | — | — | npm test | | |
31
+ | c | b | — | — | — | — | — | npm run build | | |
32
+ `) || [];
33
+
34
+ // ---- S1 replayRun:字段进 state、total 计数 ----
35
+ {
36
+ const r = replayRun([mkline(), mkline({ stage: 'ask', form: 'human', status: 'pass', note: '用户点头' })], 'w@20260926-1000');
37
+ check('S1 replayRun 记 2 条且 form/note 进 state',
38
+ r.total === 2 && r.state.a.status === 'pass' && r.state.ask.form === 'human' && r.state.ask.note === '用户点头',
39
+ JSON.stringify(r));
40
+ }
41
+
42
+ // ---- S2 末次生效 + attempt 取最大(fail→pass 重试)----
43
+ {
44
+ const r = replayRun([
45
+ mkline({ stage: 'b', status: 'fail', attempt: 1 }),
46
+ mkline({ stage: 'b', status: 'fail', attempt: 2 }),
47
+ mkline({ stage: 'b', status: 'pass', attempt: 3 }),
48
+ ], 'w@20260926-1000');
49
+ check('S2 末次状态生效 pass 且 attempt=3',
50
+ r.state.b.status === 'pass' && r.state.b.attempt === 3,
51
+ JSON.stringify(r.state.b));
52
+ }
53
+
54
+ // ---- S3 其他 run / 坏行不混入 ----
55
+ {
56
+ const r = replayRun([
57
+ mkline({ stage: 'a' }),
58
+ mkline({ stage: 'x', run: 'w@OTHER' }),
59
+ '{ broken json',
60
+ mkline({ stage: 'a', status: 'nope' }),
61
+ ], 'w@20260926-1000');
62
+ check('S3 异 run 过滤 + 坏 JSON/坏 status 计 bad 不进 state',
63
+ r.total === 1 && r.bad === 2 && !r.state.x,
64
+ JSON.stringify(r));
65
+ }
66
+
67
+ // ---- S4 latestRun:取该 wf 最后一条 run ----
68
+ {
69
+ const run = latestRun([mkline({ run: 'w@A' }), mkline({ run: 'w@B', stage: 'a' })], 'w');
70
+ check('S4 latestRun 取末条 w@B', run === 'w@B', String(run));
71
+ }
72
+
73
+ // ---- S5 planStages:空状态 → 第 0 层就绪;pass 后下层就绪;fail 下层待定 ----
74
+ {
75
+ const p0 = planStages(STAGES, {});
76
+ const p1 = planStages(STAGES, { a: { status: 'pass', attempt: 1 } });
77
+ const p2 = planStages(STAGES, { a: { status: 'pass', attempt: 1 }, ask: { status: 'fail', attempt: 1 } });
78
+ check('S5 分层:首层 a 就绪 → a 过后 ask 就绪 → ask fail 时 b/c 待定且 ask 重试就绪',
79
+ p0.ready.map((s) => s.id).join() === 'a'
80
+ && p1.ready.map((s) => s.id).join() === 'ask'
81
+ && p2.ready.some((s) => s.id === 'ask' && s.retry === 1)
82
+ && p2.pending.map((s) => s.id).join() === 'b,c',
83
+ JSON.stringify({ p0, p1, p2 }));
84
+ }
85
+
86
+ // ---- S6 planStages:blocked 标注中止 + 依赖中止提示 ----
87
+ {
88
+ const p = planStages(STAGES, { a: { status: 'pass', attempt: 1 }, ask: { status: 'blocked', attempt: 1, note: '用户驳回' } });
89
+ check('S6 blocked 进中止列且下游标依赖中止',
90
+ p.blocked.length === 1 && p.blocked[0].id === 'ask' && p.blocked[0].note === '用户驳回'
91
+ && p.pending.some((s) => s.id === 'b' && s.depBlocked),
92
+ JSON.stringify(p));
93
+ }
94
+
95
+ // ---- S7 CLI add 冒烟:attempt 自动计次 + run 复用 ----
96
+ {
97
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'wfj-cli-'));
98
+ const j = path.join(tmp, 'j.jsonl');
99
+ const run = (args) => spawnSync(process.execPath, [JOURNAL_CLI, ...args], { encoding: 'utf8' });
100
+ const r1 = run(['add', '--wf', 'w', '--stage', 'a', '--status', 'pass', '--journal', j]);
101
+ const r2 = run(['add', '--wf', 'w', '--stage', 'a', '--status', 'fail', '--journal', j]);
102
+ const lines = fs.readFileSync(j, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
103
+ check('S7 add×2 attempt 自动 1→2 且 run 复用同 id',
104
+ r1.status === 0 && r2.status === 0 && lines.length === 2
105
+ && lines[0].attempt === 1 && lines[1].attempt === 2 && lines[0].run === lines[1].run,
106
+ JSON.stringify({ r1: r1.stdout + r1.stderr, r2: r2.stdout + r2.stderr, lines }));
107
+ fs.rmSync(tmp, { recursive: true, force: true });
108
+ }
109
+
110
+ // ---- S8 CLI status 冒烟:临时 workflows 目录 + 坏行容忍 ----
111
+ {
112
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'wfj-st-'));
113
+ const wfRoot = path.join(tmp, 'workflows');
114
+ fs.mkdirSync(wfRoot, { recursive: true });
115
+ fs.writeFileSync(path.join(wfRoot, 'demo.md'),
116
+ '---\nname: demo\ndescription: d\n---\n\n' +
117
+ '| id | after | role | step | task | files | accept | gate | retries |\n' +
118
+ '|----|-------|------|------|------|-------|--------|------|---------|\n' +
119
+ '| a | — | — | — | — | — | — | npm test | |\n' +
120
+ '| b | a | — | — | — | — | — | npm run build | |\n');
121
+ const j = path.join(tmp, 'j.jsonl');
122
+ fs.writeFileSync(j, '{ broken\n' + mkline({ wf: 'demo', stage: 'a' }) + '\n');
123
+ const r = spawnSync(process.execPath, [JOURNAL_CLI, 'status', '--wf', 'demo', '--journal', j, '--wf-root', wfRoot], { encoding: 'utf8' });
124
+ check('S8 status:坏行告警不炸、a 已完成、b 就绪',
125
+ r.status === 0 && /坏行 1 条/.test(r.stderr) && /已完成(1):a/.test(r.stdout) && /就绪(1):b/.test(r.stdout),
126
+ JSON.stringify({ out: r.stdout, err: r.stderr }));
127
+ fs.rmSync(tmp, { recursive: true, force: true });
128
+ }
129
+
130
+ // ---- S9 真实仓库:pipeline-closing 解析含 human 形态 confirm 行;无 journal 时首层就绪 ----
131
+ {
132
+ const ROOT = path.resolve(SCRIPT_DIR, '..', '..', '..');
133
+ const wfFile = path.join(ROOT, '.agents', 'workflows', 'pipeline-closing.md');
134
+ if (!fs.existsSync(wfFile)) {
135
+ console.log('SKIP S9(非包源仓库,无 pipeline-closing)');
136
+ } else {
137
+ const stages = parseStages(fs.readFileSync(wfFile, 'utf8')) || [];
138
+ const confirm = stages.find((s) => s.id === 'confirm');
139
+ const j = path.join(os.tmpdir(), `wfj-real-${Date.now()}.jsonl`);
140
+ const r = spawnSync(process.execPath, [JOURNAL_CLI, 'status', '--wf', 'pipeline-closing', '--journal', j], { encoding: 'utf8', cwd: ROOT });
141
+ check('S9 真实 pipeline-closing:confirm 为 human 形态且无 journal 时 author 首层就绪',
142
+ !!confirm && confirm.human && !confirm.role && stages.length >= 10
143
+ && r.status === 0 && /就绪(第 0 层.*author/.test(r.stdout),
144
+ JSON.stringify({ confirm, out: r.stdout }));
145
+ fs.rmSync(j, { force: true });
146
+ }
147
+ }
148
+
149
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
150
+ process.exit(fail ? 1 : 0);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // workflow 实时看板服务:本地只读 http + SSE 文件监听,可视化 AI 执行流程(intents/incidents/plans/specs)。
3
- // 用法:node .agents/scripts/workflow-board-server.mjs [--port 8933]
3
+ // 用法:node .agents/scripts/workflow-board-server.mjs [--port 8933];多项目并行请用 ensure-board.mjs 拉起(自动上探可用端口)
4
4
  // 零依赖(node:http/node:fs/node:path/node:url/node:child_process);只读 workflow/ 与 git,仅绑 127.0.0.1。
5
5
  // API:GET /(看板页) /marked.min.js(vendor) /api/board /api/doc?file=<相对路径> /api/history?file=<相对路径> /api/events(SSE)
6
6
  // 实时边界:文档内容与状态随落盘实时(fs.watch→SSE 推送);git 历史仅含已提交记录(git 语义)。
@@ -9,6 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { execFile } from 'node:child_process';
12
+ import { ENUMS } from './workflow-enums.mjs';
12
13
 
13
14
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
14
15
  const WORKFLOW = path.join(ROOT, 'workflow');
@@ -17,6 +18,7 @@ const DOC_TYPES = ['intents', 'incidents', 'plans', 'specs'];
17
18
 
18
19
  const portArg = process.argv.indexOf('--port');
19
20
  const PORT = portArg > 0 ? Number(process.argv[portArg + 1]) || 8933 : 8933;
21
+ const STARTED_AT = Date.now(); // 随 /api/board 自报(startedAt),供 ensure-board.mjs 判定旧代码重启,不碰进程启动时间 API
20
22
 
21
23
  // ---- front matter + 标题解析(对照 _TEMPLATE.md 受限子集:每行 `键: 值`)----
22
24
  function parseDoc(text) {
@@ -50,17 +52,17 @@ function parseAcceptance(text) {
50
52
  }
51
53
 
52
54
  // ---- 配对断裂检测:按 slug 聚合同族(同名 intent/incident/plan/spec),异常卡附 alerts ----
53
- // 规则(口径对齐 check-loop.sh 的硬断档 + 状态枚举,看板为预警层、不阻断):
55
+ // 规则(口径对齐 check-loop.sh 的硬断档 + 状态枚举,看板为预警层、不阻断;枚举读单源 workflow-enums.txt):
54
56
  // 孤儿 spec / 孤儿 plan(无同名 intent·incident 入口);入口 done 但 plan 未终态(看板启发式);
55
57
  // 入口缺 plan(intent 一律要求;incident 需非 legacy 且级别 L1/L2/L3);L2/L3 入口缺同名 spec;
56
58
  // spec L3 确认三件缺失(确认结果 / 确认时间 / 正文独立复核行);
57
- // 状态不在枚举内或缺失(intent·spec·plan: approved/done/superseded/cancelled;incident: open/fixed/closed)
58
- const PLAN_TERMINAL = ['done', 'superseded', 'cancelled'];
59
+ // 状态不在枚举内或缺失(doc=confirmed 集;incident=all 集)
60
+ const PLAN_TERMINAL = ENUMS['doc.status.terminal'];
59
61
  const STATUS_ENUM = {
60
- intents: ['approved', 'done', 'superseded', 'cancelled'],
61
- specs: ['approved', 'done', 'superseded', 'cancelled'],
62
- plans: ['approved', 'done', 'superseded', 'cancelled'],
63
- incidents: ['open', 'fixed', 'closed'],
62
+ intents: ENUMS['doc.status.confirmed'],
63
+ specs: ENUMS['doc.status.confirmed'],
64
+ plans: ENUMS['doc.status.confirmed'],
65
+ incidents: ENUMS['incident.status.all'],
64
66
  };
65
67
  const NO_STATUS = '(未填)';
66
68
  const L3_REVIEW = /^- 独立复核:[ \t]*[^<\s]/m; // 正文独立复核行须有实质内容(对照 check-loop 锚定)
@@ -93,7 +95,7 @@ function detectAlerts(cards) {
93
95
  alerts.push('入口缺 plan:无同名 plan');
94
96
  if (isEntry && !legacyIncident && ['L2', 'L3'].includes(c.level) && !types.has('specs'))
95
97
  alerts.push(`${c.level} 入口缺同名 spec`);
96
- if (c.type === 'specs' && c.level === 'L3' && !['superseded', 'cancelled'].includes(c.status)) {
98
+ if (c.type === 'specs' && c.level === 'L3' && !ENUMS['doc.status.abandoned'].includes(c.status)) {
97
99
  if (c.confirm !== 'approved') alerts.push(`L3 确认缺失:确认结果须为 approved(现 ${c.confirm || '缺失'})`);
98
100
  if (!c.confirmTime) alerts.push('L3 确认缺失:须记录确认时间');
99
101
  if (!c.hasReview) alerts.push('L3 复核缺失:须记录新会话独立复核结论');
@@ -155,7 +157,7 @@ async function scanBoard() {
155
157
  }
156
158
  detectAlerts(cards);
157
159
  cards.sort((a, b) => b.date.localeCompare(a.date) || a.type.localeCompare(b.type));
158
- return { root: ROOT, counts, cards };
160
+ return { root: ROOT, pid: process.pid, startedAt: STARTED_AT, counts, cards };
159
161
  }
160
162
 
161
163
  // ---- 路径白名单:resolve 后必须仍在 workflow/ 内 ----
@@ -240,6 +242,12 @@ const server = http.createServer(async (req, res) => {
240
242
  }
241
243
  });
242
244
 
245
+ // 绑定失败明确退出(EADDRINUSE 常见于直跑撞端口;ensure-board 会先探活选端口,这里兜底可读报错)
246
+ server.on('error', (e) => {
247
+ console.error(`workflow 看板启动失败(端口 ${PORT}${e.code === 'EADDRINUSE' ? ' 已被占用' : ''}):${e.code || e.message}`);
248
+ process.exit(1);
249
+ });
250
+
243
251
  server.listen(PORT, '127.0.0.1', () => {
244
252
  console.log(`workflow 看板: http://127.0.0.1:${PORT} (Ctrl+C 停止;只读 workflow/,不写任何文件)`);
245
253
  });
@@ -0,0 +1,68 @@
1
+ // workflow-enums 读取器——.agents/workflow-enums.txt 单源(2026-09-25 enum-single-source)
2
+ // 消费方:workflow-board-server.mjs / kb-search.mjs / gen-workflow-index.mjs / fill-{intent,plan,spec}.mjs
3
+ // (check-loop.sh 为 POSIX sh,自行 sed 读取同文件,口径同本读取器的键)。
4
+ // fail-loud 无 fallback:kit 自带该文件,缺失/坏数据属安装破损——直接抛错崩得可见,
5
+ // doctor §2 布局门同时把关文件存在性。禁止在任何消费方内再抄一份枚举字面量。
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const ENUMS_FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'workflow-enums.txt');
11
+
12
+ // parseEnums(text):`key=a b` 行 → { key: ['a','b'] };非法行/重复键/重复值抛错(单源纪律)
13
+ export function parseEnums(text) {
14
+ const out = {};
15
+ for (const line of String(text).split(/\r?\n/)) {
16
+ if (!line || line.startsWith('#')) continue;
17
+ const m = line.match(/^([a-z][a-z0-9.]*)=(\S.*)$/);
18
+ if (!m) throw new Error('workflow-enums 行格式非法(须 key=value value): ' + line);
19
+ if (m[1] in out) throw new Error('workflow-enums 键重复: ' + m[1]);
20
+ const vals = m[2].trim().split(/\s+/);
21
+ if (new Set(vals).size !== vals.length) throw new Error('workflow-enums 值重复: ' + line);
22
+ out[m[1]] = vals;
23
+ }
24
+ return out;
25
+ }
26
+
27
+ const NEED_KEYS = [
28
+ 'doc.status.all', 'doc.status.confirmed', 'doc.status.active', 'doc.status.terminal', 'doc.status.abandoned',
29
+ 'incident.status.all', 'incident.status.active', 'level.all',
30
+ ];
31
+
32
+ // validateEnums(enums):跨键不变量(子集关系 + 活跃/终态互斥 + 关键态在位)——坏数据尽早抛错
33
+ export function validateEnums(e) {
34
+ for (const k of NEED_KEYS) {
35
+ if (!Array.isArray(e[k]) || !e[k].length) throw new Error('workflow-enums 缺键或空值: ' + k);
36
+ }
37
+ const sub = (a, b) => {
38
+ for (const v of e[a]) if (!e[b].includes(v)) throw new Error(`workflow-enums 不变量破: ${a} 含 ${b} 外的值 ${v}`);
39
+ };
40
+ sub('doc.status.confirmed', 'doc.status.all');
41
+ sub('doc.status.active', 'doc.status.all');
42
+ sub('doc.status.terminal', 'doc.status.all');
43
+ sub('doc.status.abandoned', 'doc.status.all');
44
+ sub('doc.status.abandoned', 'doc.status.terminal');
45
+ sub('incident.status.active', 'incident.status.all');
46
+ if (e['doc.status.active'].some((v) => e['doc.status.terminal'].includes(v))) {
47
+ throw new Error('workflow-enums 不变量破: doc.status.active 与 doc.status.terminal 重叠');
48
+ }
49
+ if (!e['doc.status.confirmed'].includes('approved') || !e['doc.status.terminal'].includes('done')) {
50
+ throw new Error('workflow-enums 不变量破: approved 须在 confirmed、done 须在 terminal(确认/关单语义锚点)');
51
+ }
52
+ return e;
53
+ }
54
+
55
+ // loadEnums(file = 单源标准位置):读文件 → parse → validate;缺文件抛错(fail-loud)
56
+ export function loadEnums(file = ENUMS_FILE) {
57
+ let text;
58
+ try {
59
+ text = fs.readFileSync(file, 'utf8');
60
+ } catch (err) {
61
+ throw new Error('workflow-enums 单源文件不可读(kit 自带,跑 flow-kit sync 恢复): ' + file + ' — ' + err.message);
62
+ }
63
+ return validateEnums(parseEnums(text));
64
+ }
65
+
66
+ // 模块级常量:import 即加载即校验(标准位置)
67
+ export const ENUMS = loadEnums();
68
+ export default ENUMS;
@@ -0,0 +1,110 @@
1
+ // workflow-enums.test.mjs — 枚举单源读取器测试(2026-09-25 enum-single-source)
2
+ // 判据:① 真实单源文件解析 + 不变量通过 ② 坏数据(坏行/重复键/重复值/缺键/子集破/重叠)fail-loud
3
+ // ③ 缺文件 fail-loud ④ sh 消费方同口径(sed 可取到全部 8 键)
4
+ // 用法:node templates/_agents/scripts/workflow-enums.test.mjs(npm test 随跑)
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { execSync } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { parseEnums, validateEnums, loadEnums, ENUMS } from './workflow-enums.mjs';
10
+
11
+ let pass = 0;
12
+ let fail = 0;
13
+ const check = (desc, cond, detail = '') => {
14
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
15
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
16
+ };
17
+ const throwsWith = (fn, kw) => {
18
+ try { fn(); return false; } catch (e) { return String(e.message).includes(kw); }
19
+ };
20
+
21
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
22
+ const ENUMS_TXT = path.join(SCRIPT_DIR, '..', 'workflow-enums.txt');
23
+
24
+ // ---- S1 模块级 ENUMS 即加载即校验(真实文件),8 键齐 ----
25
+ {
26
+ const keys = Object.keys(ENUMS).sort();
27
+ check('S1 真实单源文件加载成功且 8 键齐',
28
+ keys.length === 8 && keys.join(',') === 'doc.status.abandoned,doc.status.active,doc.status.all,doc.status.confirmed,doc.status.terminal,incident.status.active,incident.status.all,level.all',
29
+ JSON.stringify(keys));
30
+ }
31
+
32
+ // ---- S2 真实文件关键值口径(check-loop/看板/检索的行为锚点,值变更须连本测试一起改)----
33
+ {
34
+ check('S2 关键集值口径:confirmed 不含 draft、active=draft+approved、terminal 含 done、abandoned ⊆ terminal',
35
+ !ENUMS['doc.status.confirmed'].includes('draft')
36
+ && ENUMS['doc.status.active'].join('+') === 'draft+approved'
37
+ && ENUMS['doc.status.terminal'].includes('done')
38
+ && ENUMS['doc.status.abandoned'].every((v) => ENUMS['doc.status.terminal'].includes(v))
39
+ && ENUMS['level.all'].length === 4,
40
+ JSON.stringify(ENUMS));
41
+ }
42
+
43
+ // ---- S3 坏行 fail-loud ----
44
+ {
45
+ check('S3 非法行抛错', throwsWith(() => parseEnums('乱行 no-equals-sign'), '行格式非法'));
46
+ }
47
+
48
+ // ---- S4 重复键 fail-loud ----
49
+ {
50
+ check('S4 重复键抛错', throwsWith(() => parseEnums('a.b=x\na.b=y'), '键重复'));
51
+ }
52
+
53
+ // ---- S5 重复值 fail-loud ----
54
+ {
55
+ check('S5 重复值抛错', throwsWith(() => parseEnums('a.b=x x'), '值重复'));
56
+ }
57
+
58
+ // ---- S6 缺键 fail-loud(validateEnums)----
59
+ {
60
+ check('S6 缺键抛错', throwsWith(() => validateEnums({ 'level.all': ['L0'] }), '缺键'));
61
+ }
62
+
63
+ // ---- S7 子集破 fail-loud ----
64
+ {
65
+ const bad = {
66
+ 'doc.status.all': ['draft', 'approved', 'done'], 'doc.status.confirmed': ['approved', 'done', 'ghost'],
67
+ 'doc.status.active': ['draft'], 'doc.status.terminal': ['done'], 'doc.status.abandoned': ['cancelled'],
68
+ 'incident.status.all': ['open'], 'incident.status.active': ['open'], 'level.all': ['L0'],
69
+ };
70
+ check('S7 子集不变量破抛错', throwsWith(() => validateEnums(bad), '不变量破'));
71
+ }
72
+
73
+ // ---- S8 活跃/终态重叠 fail-loud ----
74
+ {
75
+ const bad = Object.fromEntries(Object.entries(ENUMS).map(([k]) => [k, [...ENUMS[k]]]));
76
+ bad['doc.status.terminal'] = [...bad['doc.status.terminal'], 'draft'];
77
+ check('S8 active 与 terminal 重叠抛错', throwsWith(() => validateEnums(bad), '重叠'));
78
+ }
79
+
80
+ // ---- S9 缺文件 fail-loud(loadEnums)----
81
+ {
82
+ check('S9 单源文件缺失抛错(fail-loud 无 fallback)',
83
+ throwsWith(() => loadEnums(path.join(SCRIPT_DIR, '..', 'no-such-enums.txt')), '不可读'));
84
+ }
85
+
86
+ // ---- S10 注释行与空行忽略 ----
87
+ {
88
+ const e = parseEnums('# 注释\n\nlevel.all=L0 L1\n');
89
+ check('S10 注释与空行忽略(仅 1 键)', Object.keys(e).length === 1 && e['level.all'].join('+') === 'L0+L1');
90
+ }
91
+
92
+ // ---- S11 sh 消费方同口径:check-loop.sh 的 sed 取法能取到全部 8 键 ----
93
+ {
94
+ const key = 'doc.status.confirmed';
95
+ const val = execSync(`sh -c "sed -n 's/^${key}=//p' '${ENUMS_TXT}' | head -n 1"`, { encoding: 'utf8' }).trim();
96
+ check('S11 sh sed 取键与 node 读取同值(跨语言单源口径)',
97
+ val === ENUMS[key].join(' '), `sh=[${val}] node=[${ENUMS[key].join(' ')}]`);
98
+ }
99
+
100
+ // ---- S12 CRLF 行尾容忍(autocrlf 检出坑:rule-budgets.txt 同约定;node 侧 split(/\r?\n/) 天然免疫,显式锁定)----
101
+ {
102
+ const crlf = 'doc.status.all=draft approved done superseded cancelled\r\nlevel.all=L0 L1 L2 L3\r\n';
103
+ const e = parseEnums(crlf);
104
+ check('S12 CRLF 行尾不残留 \\r(值尾无回车)',
105
+ e['level.all'].join('+') === 'L0+L1+L2+L3' && !e['doc.status.all'].some((v) => v.includes('\r')),
106
+ JSON.stringify(e));
107
+ }
108
+
109
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
110
+ process.exit(fail ? 1 : 0);