agentic-flow-kit 0.4.0 → 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 (86) hide show
  1. package/README.md +1 -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 +138 -11
  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 +1 -1
  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 +1 -1
  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 +9 -8
  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 +1 -1
  85. package/templates/_agents/cache/kb-index.json +0 -1
  86. package/templates/_agents/scripts/check-loop.test.sh +0 -1042
@@ -0,0 +1,156 @@
1
+ // source-sync-check.test.mjs — 装户面同步一致性检查工具测试(2026-09-25 source-sync-check)
2
+ // 方法:fixture mini 包源(templates/_agents/) + 装副本(.agents/),调 sourceSyncCheck() 拿三类差异
3
+ // + 实际仓库扫描做 baseline
4
+ // 判据:①fixture 三类齐全 ②cache/ 排除 ③kit.json/settings.json 排除 ④sha 一致 ⑤baseline ≥ 100 ⑥JSON 可解析
5
+ // 用法:node .agents/scripts/source-sync-check.test.mjs
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { sourceSyncCheck } from './source-sync-check.mjs';
11
+
12
+ let pass = 0, failCount = 0;
13
+ function check(name, cond, detail = '') {
14
+ if (cond) { pass++; console.log('PASS ' + name); }
15
+ else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
16
+ }
17
+
18
+ const SRC_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
19
+
20
+ // ---- fixture:mini 包源 + 装副本,3 类差异各造 1 个 ----
21
+ function mkFixture() {
22
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-'));
23
+ const pkgRoot = path.join(root, 'pkg');
24
+ const target = path.join(root, 'target');
25
+ const pkgBase = path.join(pkgRoot, 'templates', '_agents');
26
+ const tgtBase = path.join(target, '.agents');
27
+ fs.mkdirSync(path.join(pkgBase, 'commands'), { recursive: true });
28
+ fs.mkdirSync(path.join(pkgBase, 'scripts'), { recursive: true });
29
+ fs.mkdirSync(path.join(pkgBase, 'cache'), { recursive: true }); // 应排除
30
+ fs.mkdirSync(path.join(tgtBase, 'commands'), { recursive: true });
31
+ fs.mkdirSync(path.join(tgtBase, 'scripts'), { recursive: true });
32
+ fs.mkdirSync(path.join(tgtBase, 'hooks'), { recursive: true }); // 孤儿
33
+ fs.mkdirSync(path.join(tgtBase, 'cache'), { recursive: true }); // 装副本 cache 也应排除
34
+ // 一致文件(cmdA.md):包源 + 装副本 同内容
35
+ fs.writeFileSync(path.join(pkgBase, 'commands', 'cmdA.md'), '# cmdA v1\n');
36
+ fs.writeFileSync(path.join(tgtBase, 'commands', 'cmdA.md'), '# cmdA v1\n');
37
+ // 缺失(cmdB.md):包源有 / 装副本无
38
+ fs.writeFileSync(path.join(pkgBase, 'commands', 'cmdB.md'), '# cmdB v1\n');
39
+ // 孤儿(orphan-real.json):装副本 hooks/ 有 / 包源无(普通孤儿,应被报告)
40
+ fs.writeFileSync(path.join(tgtBase, 'hooks', 'orphan-real.json'), '{}\n');
41
+ // 白名单孤儿(commit-check.config.json):装副本 hooks/ 有 / 包源无,但 init 渲染产物不应报孤儿
42
+ fs.writeFileSync(path.join(tgtBase, 'hooks', 'commit-check.config.json'), '{}\n');
43
+ // 装副本独有(应排除):kit.json / settings.json
44
+ fs.writeFileSync(path.join(tgtBase, 'kit.json'), '{}\n');
45
+ fs.writeFileSync(path.join(tgtBase, 'settings.json'), '{}\n');
46
+ // 漂移(scriptX.mjs):包源 v1 / 装副本 v2
47
+ fs.writeFileSync(path.join(pkgBase, 'scripts', 'scriptX.mjs'), 'export const X = 1;\n');
48
+ fs.writeFileSync(path.join(tgtBase, 'scripts', 'scriptX.mjs'), 'export const X = 2;\n');
49
+ // cache/ 排除:包源 + 装副本 都有,但不应被计入
50
+ fs.writeFileSync(path.join(pkgBase, 'cache', 'foo.md'), 'cache-pkg\n');
51
+ fs.writeFileSync(path.join(tgtBase, 'cache', 'foo.md'), 'cache-tgt\n');
52
+ return { pkgRoot, target };
53
+ }
54
+
55
+ // ---- 场景 1:fixture 三类差异齐全(缺失 1 / 孤儿 1 / 漂移 1)----
56
+ {
57
+ const { pkgRoot, target } = mkFixture();
58
+ const r = sourceSyncCheck({ pkgRoot, target });
59
+ check('S1 fixture 缺失 1(cmdB.md)', r.missing.length === 1 && r.missing[0].rel === 'commands/cmdB.md', JSON.stringify(r.missing));
60
+ check('S1 fixture 孤儿 1(orphan-real.json)', r.orphan.length === 1 && r.orphan[0].rel === 'hooks/orphan-real.json', JSON.stringify(r.orphan));
61
+ check('S1 fixture 漂移 1(scriptX.mjs)', r.drift.length === 1 && r.drift[0].rel === 'scripts/scriptX.mjs', JSON.stringify(r.drift));
62
+ check('S1 fixture 白名单项(commit-check.config.json)不在孤儿', !r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json'));
63
+ }
64
+
65
+ // ---- 场景 2:cache/ 目录排除 ----
66
+ {
67
+ const { pkgRoot, target } = mkFixture();
68
+ const r = sourceSyncCheck({ pkgRoot, target });
69
+ check('S2 cache/ 排除(缺失/孤儿/漂移不含 cache/foo.md)',
70
+ !r.missing.some((m) => m.rel.includes('cache/')) &&
71
+ !r.orphan.some((o) => o.rel.includes('cache/')) &&
72
+ !r.drift.some((d) => d.rel.includes('cache/')));
73
+ }
74
+
75
+ // ---- 场景 3:装副本独有(kit.json / settings.json)排除 ----
76
+ {
77
+ const { pkgRoot, target } = mkFixture();
78
+ const r = sourceSyncCheck({ pkgRoot, target });
79
+ check('S3 装副本独有(kit.json / settings.json)排除',
80
+ !r.orphan.some((o) => o.rel === 'kit.json' || o.rel === 'settings.json'));
81
+ }
82
+
83
+ // ---- 场景 4:sha 一致文件不出现在漂移列表 ----
84
+ {
85
+ const { pkgRoot, target } = mkFixture();
86
+ const r = sourceSyncCheck({ pkgRoot, target });
87
+ check('S4 一致文件 cmdA.md 不在漂移', !r.drift.some((d) => d.rel === 'commands/cmdA.md'));
88
+ }
89
+
90
+ // ---- 场景 5:实际仓库扫描 baseline(包源 ≥ 30 份 / 装副本 ≥ 30 份)----
91
+ {
92
+ const pkgRoot = SRC_ROOT;
93
+ const target = SRC_ROOT;
94
+ const r = sourceSyncCheck({ pkgRoot, target });
95
+ check('S5 实际仓库 包源 ≥ 30 份', r.pkgCount >= 30, '实际 ' + r.pkgCount);
96
+ check('S5 实际仓库 装副本 ≥ 30 份', r.tgtCount >= 30, '实际 ' + r.tgtCount);
97
+ // 装副本 kit.json / settings.json 不应在孤儿
98
+ check('S5 实际仓库 装副本独有(kit.json / settings.json)排除',
99
+ !r.orphan.some((o) => o.rel === 'kit.json' || o.rel === 'settings.json'));
100
+ }
101
+
102
+ // ---- 场景 6:--json 输出可解析(CLI 子流程)----
103
+ {
104
+ const { pkgRoot, target } = mkFixture();
105
+ // 直接调 sourceSyncCheck + 序列化模拟 --json 输出
106
+ const r = sourceSyncCheck({ pkgRoot, target });
107
+ const jsonOut = JSON.stringify({
108
+ pkgCount: r.pkgCount, tgtCount: r.tgtCount,
109
+ missing: r.missing.map((m) => m.rel),
110
+ orphan: r.orphan.map((o) => o.rel),
111
+ drift: r.drift.map((d) => ({ rel: d.rel, pkgSha: d.pkgSha.slice(0, 8), tgtSha: d.tgtSha.slice(0, 8) })),
112
+ }, null, 2);
113
+ let parsed = null;
114
+ try { parsed = JSON.parse(jsonOut); } catch {}
115
+ check('S6 --json 输出可解析', parsed && typeof parsed === 'object' && Array.isArray(parsed.missing));
116
+ }
117
+
118
+ // ---- 场景 8:白名单 RHO 文件过滤(hooks/commit-check.config.json + kit.json + settings.json 都不报孤儿)----
119
+ {
120
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-rho-'));
121
+ const pkgRoot = path.join(root, 'pkg');
122
+ const target = path.join(root, 'target');
123
+ const pkgBase = path.join(pkgRoot, 'templates', '_agents');
124
+ const tgtBase = path.join(target, '.agents');
125
+ fs.mkdirSync(pkgBase, { recursive: true });
126
+ fs.mkdirSync(path.join(tgtBase, 'hooks'), { recursive: true });
127
+ fs.mkdirSync(tgtBase, { recursive: true });
128
+ // 3 个白名单项都装副本独有
129
+ fs.writeFileSync(path.join(tgtBase, 'hooks', 'commit-check.config.json'), '{}\n');
130
+ fs.writeFileSync(path.join(tgtBase, 'kit.json'), '{}\n');
131
+ fs.writeFileSync(path.join(tgtBase, 'settings.json'), '{}\n');
132
+ // 1 个非白名单孤儿作对照(应被报告)
133
+ fs.writeFileSync(path.join(tgtBase, 'hooks', 'real-orphan.json'), '{}\n');
134
+ const r = sourceSyncCheck({ pkgRoot, target });
135
+ check('S8 白名单 3 项(commit-check.config.json / kit.json / settings.json)不报孤儿',
136
+ !r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json' || o.rel === 'kit.json' || o.rel === 'settings.json'));
137
+ check('S8 非白名单孤儿(real-orphan.json)仍报孤儿',
138
+ r.orphan.length === 1 && r.orphan[0].rel === 'hooks/real-orphan.json', JSON.stringify(r.orphan));
139
+ }
140
+
141
+ // ---- 场景 9:实际仓库 hooks/commit-check.config.json 不在孤儿 ----
142
+ {
143
+ const r = sourceSyncCheck({ pkgRoot: SRC_ROOT, target: SRC_ROOT });
144
+ check('S9 实际仓库 hooks/commit-check.config.json 不在孤儿',
145
+ !r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json'), JSON.stringify(r.orphan));
146
+ }
147
+
148
+ // ---- 场景 7:空目录 / 不存在的目录不崩 ----
149
+ {
150
+ const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-empty-'));
151
+ const r = sourceSyncCheck({ pkgRoot: empty, target: empty });
152
+ check('S7 空目录 fixture 不崩(0/0)', r.pkgCount === 0 && r.tgtCount === 0);
153
+ }
154
+
155
+ console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
156
+ process.exit(failCount ? 1 : 0);
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script)
3
- // 用途:关单前一键过门——按写死顺序执行 npm test → check-loop.sh,逐项输出 pass/fail。
2
+ // verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script;2026-09-26 check-loop-node 第 2 步直连 node)
3
+ // 用途:关单前一键过门——按写死顺序执行 npm test → check-loop,逐项输出 pass/fail。
4
4
  // 语义:只编排不裁决——两条命令都是既有门禁原样跑,本脚本不新增校验、不读配置、无状态;
5
- // 任一步非零即 fail-fast 退出 1,失败原因经 stdio inherit 原样透传不吞;
6
- // 缺 sh 环境 fail-closed 明确报错(关单门不可跳过,不像 run-tests 那样跳过并提示)。
5
+ // 任一步非零即 fail-fast 退出 1,失败原因经 stdio inherit 原样透传不吞。
6
+ // check-loop 为 node 实现(经 check-loop.mjs 直跑——node 即本脚本运行时必然可用,
7
+ // 旧「缺 sh 环境 fail-closed」分支随 sh 依赖消失退役)。
7
8
  // 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具,不触真实 npm test 与 workflow)
8
9
  // 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"](--test-cmd 仅供测试注入)
9
10
  import { spawnSync } from 'node:child_process';
@@ -13,7 +14,7 @@ import { fileURLToPath } from 'node:url';
13
14
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
15
  const args = process.argv.slice(2);
15
16
  const testCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : 'npm test';
16
- const checkLoop = path.join(SCRIPT_DIR, 'check-loop.sh');
17
+ const checkLoop = path.join(SCRIPT_DIR, 'check-loop.mjs');
17
18
 
18
19
  console.log(`[verify] ▶ 1/2 ${testCmd}`);
19
20
  const t = spawnSync(testCmd, { shell: true, stdio: 'inherit' });
@@ -24,12 +25,9 @@ if (t.status !== 0) {
24
25
  console.log('[verify] ✅ 1/2 测试通过');
25
26
 
26
27
  console.log('[verify] ▶ 2/2 check-loop');
27
- const c = spawnSync('sh', [checkLoop], { stdio: 'inherit' });
28
+ const c = spawnSync(process.execPath, [checkLoop], { stdio: 'inherit' });
28
29
  if (c.status !== 0) {
29
- const hint = c.error?.code === 'ENOENT'
30
- ? '环境无 sh(装 Git Bash / WSL)——关单门不可跳过,不静默放行'
31
- : '按上方输出修复后原路重跑';
32
- console.error(`[verify] ❌ 2/2 check-loop 未通过(exit ${c.status ?? c.error?.code})——${hint}`);
30
+ console.error(`[verify] ❌ 2/2 check-loop 未通过(exit ${c.status ?? c.error?.code})——按上方输出修复后原路重跑`);
33
31
  process.exit(1);
34
32
  }
35
33
  console.log('[verify] ✅ 2/2 闭环校验通过');
@@ -19,10 +19,22 @@ const check = (desc, cond, detail = '') => {
19
19
  else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
20
20
  };
21
21
 
22
- // 空闭环夹具(四目录齐全、无文档 → check-loop 无断档)
22
+ // 空闭环夹具(四目录齐全、无文档 → check-loop 无断档;枚举单源随 fixture 内联——check-loop 启动段 fail-loud 要求)
23
23
  const mkfix = () => {
24
24
  const d = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-fix-'));
25
25
  for (const s of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
26
+ fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
27
+ fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'), [
28
+ 'doc.status.all=draft approved done superseded cancelled',
29
+ 'doc.status.confirmed=approved done superseded cancelled',
30
+ 'doc.status.active=draft approved',
31
+ 'doc.status.terminal=done superseded cancelled',
32
+ 'doc.status.abandoned=superseded cancelled',
33
+ 'incident.status.all=open fixed closed',
34
+ 'incident.status.active=open',
35
+ 'level.all=L0 L1 L2 L3',
36
+ '',
37
+ ].join('\n'), 'utf8');
26
38
  return d;
27
39
  };
28
40
  // 配对断裂夹具(L1 intent 无同名 plan → hard-block)
@@ -0,0 +1,158 @@
1
+ // wf-journal — 编排执行 run journal(2026-09-26 orchestration-journal-human,审查改进方向 4)
2
+ // 用途:宿主 AI 按 .agents/workflows/_TEMPLATE.md 纪律,每个 stage 每次尝试出结果(pass/fail/blocked)
3
+ // 记一行 JSONL;中止后重跑先看 status——已 pass 不重跑,从就绪层续(断点续跑与重试计量)。
4
+ // 存储:.agents/cache/orchestration-runs.jsonl(本地运行态缓存,已 gitignore,可丢弃——丢失仅失去
5
+ // 续跑记忆,不影响任何门禁;status 对空 journal 天然全量输出不误报)。
6
+ // 行 schema:{ ts, run, wf, stage, form?, status, attempt, note? }
7
+ // run = <wf>@<YYYYMMDD-HHMM>;同 run 同 stage 多行按末次状态生效(重试后过 = pass),attempt 取最大。
8
+ // 语义口径(与 _TEMPLATE.md 四形态一致;stages 表解析复用 workflows-check.mjs 的 parseStages 单源)。
9
+ // 用法:
10
+ // node .agents/scripts/wf-journal.mjs add --wf <编排名> --stage <id> --status <pass|fail|blocked>
11
+ // [--form role|step|gate|human] [--attempt N] [--note <文本>] [--run <runId>] [--new-run]
12
+ // [--journal <文件>](默认 .agents/cache/orchestration-runs.jsonl,按 cwd)
13
+ // node .agents/scripts/wf-journal.mjs status --wf <编排名> [--run <runId>] [--journal <文件>] [--wf-root <目录>]
14
+ // 测试:node templates/_agents/scripts/wf-journal.test.mjs(fixture + 真实仓库场景)
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { parseStages } from './workflows-check.mjs';
18
+
19
+ function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
20
+
21
+ const DEFAULT_JOURNAL = () => path.join(process.cwd(), '.agents', 'cache', 'orchestration-runs.jsonl');
22
+ const DEFAULT_WF_ROOT = () => path.join(process.cwd(), '.agents', 'workflows');
23
+ const STATUSES = ['pass', 'fail', 'blocked'];
24
+ const FORMS = ['role', 'step', 'gate', 'human'];
25
+
26
+ // 读 journal 原始行(文件不存在/空 → [];目录不存在自动可建由 add 处理)
27
+ export function readJournalLines(journalFile) {
28
+ try {
29
+ return fs.readFileSync(journalFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ // replayRun:重放一个 run 的记录 → { state: {stage: {status, attempt, form, note, ts}}, total, bad }
36
+ // 末次状态生效;attempt 取出现过的最大值;坏行跳过(计数返回,调用方决定告警口径)
37
+ export function replayRun(rawLines, run) {
38
+ const state = {};
39
+ let total = 0;
40
+ let bad = 0;
41
+ for (const line of rawLines) {
42
+ let j;
43
+ try { j = JSON.parse(line); } catch { bad++; continue; }
44
+ if (!j || typeof j !== 'object' || j.run !== run) continue;
45
+ if (!j.stage || !STATUSES.includes(j.status)) { bad++; continue; }
46
+ total++;
47
+ const prev = state[j.stage];
48
+ state[j.stage] = {
49
+ status: j.status,
50
+ attempt: Math.max(prev?.attempt ?? 0, Number(j.attempt) || 1),
51
+ ...(j.form ? { form: j.form } : {}),
52
+ ...(j.note ? { note: j.note } : {}),
53
+ ts: j.ts,
54
+ };
55
+ }
56
+ return { state, total, bad };
57
+ }
58
+
59
+ // latestRun:journal 中该 wf 最后一条的 run id(无记录 → null)
60
+ export function latestRun(rawLines, wf) {
61
+ let run = null;
62
+ for (const line of rawLines) {
63
+ try {
64
+ const j = JSON.parse(line);
65
+ if (j && j.wf === wf && j.run) run = j.run;
66
+ } catch { /* 坏行不参与 run 推断 */ }
67
+ }
68
+ return run;
69
+ }
70
+
71
+ // planStages:stages(parseStages 产物)× 状态 → { passed, ready, pending, blocked }
72
+ // passed = 末次 pass;ready = 依赖全 pass 且自身未 pass(末次 fail 标注重试就绪);blocked = 自身末次 blocked;pending = 其余
73
+ export function planStages(stages, state) {
74
+ const passed = [];
75
+ const ready = [];
76
+ const pending = [];
77
+ const blocked = [];
78
+ const passedSet = new Set(stages.filter((s) => state[s.id]?.status === 'pass').map((s) => s.id));
79
+ for (const s of stages) {
80
+ const st = state[s.id];
81
+ if (st?.status === 'pass') { passed.push({ id: s.id }); continue; }
82
+ if (st?.status === 'blocked') { blocked.push({ id: s.id, note: st.note || '' }); continue; }
83
+ const deps = s.after ? s.after.split(/[,,、\s]+/).filter(Boolean) : [];
84
+ if (deps.every((d) => passedSet.has(d))) {
85
+ ready.push({ id: s.id, ...(st?.status === 'fail' ? { retry: st.attempt } : {}) });
86
+ } else {
87
+ pending.push({ id: s.id, ...(deps.some((d) => state[d] && state[d].status !== 'pass') && (state[deps.find((d) => state[d] && state[d].status !== 'pass')]?.status === 'blocked') ? { depBlocked: true } : {}) });
88
+ }
89
+ }
90
+ return { passed, ready, pending, blocked };
91
+ }
92
+
93
+ const pad = (n) => String(n).padStart(2, '0');
94
+ const runIdOf = (wf, d = new Date()) =>
95
+ `${wf}@${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
96
+
97
+ function parseArgs(argv) {
98
+ const out = { cmd: argv[0] };
99
+ for (let i = 1; i < argv.length; i++) {
100
+ const a = argv[i];
101
+ if (a.startsWith('--')) out[a.slice(2)] = argv[++i] ?? true;
102
+ else fail(`未知参数:${a}(用法见文件头注释)`);
103
+ }
104
+ return out;
105
+ }
106
+
107
+ const isMain = process.argv[1] && process.argv[1].endsWith('wf-journal.mjs');
108
+ if (isMain) {
109
+ const a = parseArgs(process.argv.slice(2));
110
+ const journalFile = a.journal || DEFAULT_JOURNAL();
111
+ if (a.cmd === 'add') {
112
+ if (!a.wf || !a.stage || !a.status) fail('add 必填:--wf / --stage / --status(pass|fail|blocked)');
113
+ if (!STATUSES.includes(a.status)) fail(`--status 须为 ${STATUSES.join('|')}(现 ${a.status})`);
114
+ if (a.form && !FORMS.includes(a.form)) fail(`--form 须为 ${FORMS.join('|')}(现 ${a.form})`);
115
+ const lines = readJournalLines(journalFile);
116
+ const run = a.run || (a['new-run'] ? runIdOf(a.wf) : latestRun(lines, a.wf) || runIdOf(a.wf));
117
+ const attempt = a.attempt
118
+ ? Number(a.attempt)
119
+ : lines.filter((l) => { try { const j = JSON.parse(l); return j.run === run && j.stage === a.stage; } catch { return false; } }).length + 1;
120
+ const row = {
121
+ ts: new Date().toISOString(), run, wf: a.wf, stage: a.stage,
122
+ status: a.status, attempt,
123
+ ...(a.form ? { form: a.form } : {}), ...(a.note ? { note: a.note } : {}),
124
+ };
125
+ fs.mkdirSync(path.dirname(journalFile), { recursive: true });
126
+ fs.appendFileSync(journalFile, JSON.stringify(row) + '\n');
127
+ console.log(`✅ journal += ${a.wf}/${a.stage} ${a.status}(attempt ${attempt},run ${run})`);
128
+ process.exit(0);
129
+ }
130
+ if (a.cmd === 'status') {
131
+ if (!a.wf) fail('status 必填:--wf(编排名)');
132
+ const wfRoot = a['wf-root'] || DEFAULT_WF_ROOT();
133
+ const wfFile = path.join(wfRoot, `${a.wf}.md`);
134
+ if (!fs.existsSync(wfFile)) fail(`找不到编排脚本:${wfFile}`);
135
+ const stages = parseStages(fs.readFileSync(wfFile, 'utf8'));
136
+ if (!stages) fail(`编排脚本缺 stages 表:${wfFile}`);
137
+ const lines = readJournalLines(journalFile);
138
+ const run = a.run || latestRun(lines, a.wf);
139
+ console.log(`▶ wf-journal status — ${a.wf}${run ? ` @ ${run}` : '(无 journal 记录,按首轮输出)'}`);
140
+ if (!run) {
141
+ const { ready, pending } = planStages(stages, {});
142
+ console.log(` ▶ 就绪(第 0 层,${ready.length}):${ready.map((s) => s.id).join('、') || '无'}`);
143
+ console.log(` ⏸ 待定(${pending.length}):${pending.map((s) => s.id).join('、') || '无'}`);
144
+ process.exit(0);
145
+ }
146
+ const { state, total, bad } = replayRun(lines, run);
147
+ if (bad) console.error(`⚠️ journal 坏行 ${bad} 条已跳过(缓存可丢弃,不影响门禁)`);
148
+ const plan = planStages(stages, state);
149
+ console.log(` (${total} 条记录)`);
150
+ console.log(` ✅ 已完成(${plan.passed.length}):${plan.passed.map((s) => s.id).join('、') || '无'}`);
151
+ console.log(` ▶ 就绪(${plan.ready.length}):${plan.ready.map((s) => (s.retry ? `${s.id}(重试就绪×${s.retry})` : s.id)).join('、') || '无'}`);
152
+ console.log(` ⏸ 待定(${plan.pending.length}):${plan.pending.map((s) => s.id + (s.depBlocked ? '(依赖中止)' : '')).join('、') || '无'}`);
153
+ console.log(` ⛔ 中止(${plan.blocked.length}):${plan.blocked.map((s) => `${s.id}${s.note ? `(${s.note})` : ''}`).join('、') || '无'}`);
154
+ process.exit(0);
155
+ }
156
+ fail(`未知命令:${a.cmd ?? ''}(add | status,用法见文件头注释)`);
157
+ }
158
+ export default { replayRun, planStages, latestRun, readJournalLines };
@@ -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);
@@ -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');
@@ -51,17 +52,17 @@ function parseAcceptance(text) {
51
52
  }
52
53
 
53
54
  // ---- 配对断裂检测:按 slug 聚合同族(同名 intent/incident/plan/spec),异常卡附 alerts ----
54
- // 规则(口径对齐 check-loop.sh 的硬断档 + 状态枚举,看板为预警层、不阻断):
55
+ // 规则(口径对齐 check-loop.sh 的硬断档 + 状态枚举,看板为预警层、不阻断;枚举读单源 workflow-enums.txt):
55
56
  // 孤儿 spec / 孤儿 plan(无同名 intent·incident 入口);入口 done 但 plan 未终态(看板启发式);
56
57
  // 入口缺 plan(intent 一律要求;incident 需非 legacy 且级别 L1/L2/L3);L2/L3 入口缺同名 spec;
57
58
  // spec L3 确认三件缺失(确认结果 / 确认时间 / 正文独立复核行);
58
- // 状态不在枚举内或缺失(intent·spec·plan: approved/done/superseded/cancelled;incident: open/fixed/closed)
59
- const PLAN_TERMINAL = ['done', 'superseded', 'cancelled'];
59
+ // 状态不在枚举内或缺失(doc=confirmed 集;incident=all 集)
60
+ const PLAN_TERMINAL = ENUMS['doc.status.terminal'];
60
61
  const STATUS_ENUM = {
61
- intents: ['approved', 'done', 'superseded', 'cancelled'],
62
- specs: ['approved', 'done', 'superseded', 'cancelled'],
63
- plans: ['approved', 'done', 'superseded', 'cancelled'],
64
- 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'],
65
66
  };
66
67
  const NO_STATUS = '(未填)';
67
68
  const L3_REVIEW = /^- 独立复核:[ \t]*[^<\s]/m; // 正文独立复核行须有实质内容(对照 check-loop 锚定)
@@ -94,7 +95,7 @@ function detectAlerts(cards) {
94
95
  alerts.push('入口缺 plan:无同名 plan');
95
96
  if (isEntry && !legacyIncident && ['L2', 'L3'].includes(c.level) && !types.has('specs'))
96
97
  alerts.push(`${c.level} 入口缺同名 spec`);
97
- 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)) {
98
99
  if (c.confirm !== 'approved') alerts.push(`L3 确认缺失:确认结果须为 approved(现 ${c.confirm || '缺失'})`);
99
100
  if (!c.confirmTime) alerts.push('L3 确认缺失:须记录确认时间');
100
101
  if (!c.hasReview) alerts.push('L3 复核缺失:须记录新会话独立复核结论');
@@ -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;