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,298 @@
1
+ #!/usr/bin/env node
2
+ // doctor 加 owned 漂移校验后的 fixture 驱动测试(2026-09-25 wf-runtime 复盘)
3
+ // 测试 src/doctor.mjs 导出的 checkOwnedDrift(target):覆盖 4 场景——
4
+ // ① owned 全对齐 → drift=0 gone=[] total>0 skipped=false
5
+ // ② 手改装副本(盘面 sha 变) → drift=1 gone=[] total=1
6
+ // ③ owned 文件缺失 → drift=0 gone=[rel] total=1
7
+ // ④ kit.json 无 owned 字段 → skipped=true total=0
8
+ // 用法:node .agents/scripts/doctor.test.mjs(在仓库根执行,需 src/doctor.mjs 存在——dogfooding 模式)
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { createHash } from 'node:crypto';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
+
15
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
16
+ // 上溯三级:templates/_agents/scripts/ → 仓库根
17
+ const ROOT = path.resolve(SCRIPT_DIR, '..', '..', '..');
18
+ const SRC = path.join(ROOT, 'src', 'doctor.mjs');
19
+ if (!fs.existsSync(SRC)) {
20
+ console.error(`doctor.test.mjs:未找到 ${SRC}——仅在包源仓库(dogfooding)跑 npm test 时调用`);
21
+ process.exit(1);
22
+ }
23
+ const { checkOwnedDrift, checkAdapterDrift } = await import(pathToFileURL(SRC).href);
24
+
25
+ let pass = 0;
26
+ let fail = 0;
27
+ const check = (desc, cond, detail = '') => {
28
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
29
+ else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
30
+ };
31
+
32
+ const sha256Of = (s) => createHash('sha256').update(s).digest('hex');
33
+
34
+ const mkfix = ({ kitContent, files }) => {
35
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-owned-test-'));
36
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
37
+ fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify(kitContent, null, 2));
38
+ for (const [rel, content] of Object.entries(files || {})) {
39
+ const abs = path.join(root, rel);
40
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
41
+ fs.writeFileSync(abs, content);
42
+ }
43
+ return root;
44
+ };
45
+
46
+ const mkKit = (owned) => ({
47
+ kit: 'agentic-flow-kit',
48
+ version: '0.4.0',
49
+ options: { hosts: [], stack: 'none', boardPort: '8933' },
50
+ managed: [],
51
+ owned,
52
+ });
53
+
54
+ // ---- 场景 1:owned 全对齐 → drift=0 gone=[] total=1 skipped=false ----
55
+ {
56
+ const AGENTS_BODY = '# test AGENTS.md\n';
57
+ const root = mkfix({
58
+ kitContent: mkKit([{ rel: 'AGENTS.md', sha256: sha256Of(AGENTS_BODY) }]),
59
+ files: { 'AGENTS.md': AGENTS_BODY },
60
+ });
61
+ const r = checkOwnedDrift(root);
62
+ check('场景 1:owned 全对齐 → drift=0 gone=[] total=1 skipped=false',
63
+ r.drift === 0 && r.gone.length === 0 && r.total === 1 && r.skipped === false,
64
+ JSON.stringify(r));
65
+ fs.rmSync(root, { recursive: true, force: true });
66
+ }
67
+
68
+ // ---- 场景 2:手改装副本(盘面 sha 变)→ drift=1 ----
69
+ {
70
+ const OLD_BODY = '# old version\n';
71
+ const NEW_BODY = '# new version — hand-edited\n';
72
+ const root = mkfix({
73
+ kitContent: mkKit([{ rel: 'AGENTS.md', sha256: sha256Of(OLD_BODY) }]), // 台账是旧 sha
74
+ files: { 'AGENTS.md': NEW_BODY }, // 盘面是新 sha
75
+ });
76
+ const r = checkOwnedDrift(root);
77
+ check('场景 2:手改装副本 → drift=1 gone=[] total=1 skipped=false',
78
+ r.drift === 1 && r.gone.length === 0 && r.total === 1 && r.skipped === false,
79
+ JSON.stringify(r));
80
+ fs.rmSync(root, { recursive: true, force: true });
81
+ }
82
+
83
+ // ---- 场景 3:owned 文件缺失 → drift=0 gone=['AGENTS.md'] ----
84
+ {
85
+ const root = mkfix({
86
+ kitContent: mkKit([{ rel: 'AGENTS.md', sha256: 'a'.repeat(64) }]),
87
+ files: {}, // 不写 AGENTS.md
88
+ });
89
+ const r = checkOwnedDrift(root);
90
+ check('场景 3:owned 文件缺失 → drift=0 gone=["AGENTS.md"] total=1 skipped=false',
91
+ r.drift === 0 && r.gone.length === 1 && r.gone[0] === 'AGENTS.md' && r.total === 1 && r.skipped === false,
92
+ JSON.stringify(r));
93
+ fs.rmSync(root, { recursive: true, force: true });
94
+ }
95
+
96
+ // ---- 场景 4:kit.json 无 owned 字段 → skipped=true total=0 ----
97
+ {
98
+ const kit = { kit: 'agentic-flow-kit', version: '0.4.0', options: {}, managed: [] };
99
+ const root = mkfix({ kitContent: kit, files: { 'AGENTS.md': '# any\n' } });
100
+ const r = checkOwnedDrift(root);
101
+ check('场景 4:kit.json 无 owned 字段 → skipped=true total=0',
102
+ r.skipped === true && r.total === 0,
103
+ JSON.stringify(r));
104
+ fs.rmSync(root, { recursive: true, force: true });
105
+ }
106
+
107
+ // ---- 场景 5:kit.json 不存在 → skipped=true ----
108
+ {
109
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-owned-test-'));
110
+ const r = checkOwnedDrift(root);
111
+ check('场景 5:kit.json 不存在 → skipped=true total=0',
112
+ r.skipped === true && r.total === 0,
113
+ JSON.stringify(r));
114
+ fs.rmSync(root, { recursive: true, force: true });
115
+ }
116
+
117
+ // ---- 场景 6:kit.json 解析失败 → skipped=true 含 error ----
118
+ {
119
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-owned-test-'));
120
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
121
+ fs.writeFileSync(path.join(root, '.agents', 'kit.json'), '{ broken json');
122
+ const r = checkOwnedDrift(root);
123
+ check('场景 6:kit.json 解析失败 → skipped=true 含 error',
124
+ r.skipped === true && typeof r.error === 'string' && r.error.length > 0,
125
+ JSON.stringify(r));
126
+ fs.rmSync(root, { recursive: true, force: true });
127
+ }
128
+
129
+ // ---- 场景 7:多个 owned 文件,部分漂移 → drift=1 gone=[other] ----
130
+ {
131
+ const root = mkfix({
132
+ kitContent: mkKit([
133
+ { rel: 'AGENTS.md', sha256: sha256Of('# a\n') },
134
+ { rel: 'workflow/incidents/_TEMPLATE.md', sha256: sha256Of('# tpl\n') },
135
+ { rel: 'wiki/INDEX.md', sha256: sha256Of('# w\n') },
136
+ ]),
137
+ files: {
138
+ 'AGENTS.md': '# a\n', // 对齐
139
+ 'wiki/INDEX.md': '# w\n', // 对齐
140
+ // workflow/incidents/_TEMPLATE.md 不写 → 缺失
141
+ },
142
+ });
143
+ const r = checkOwnedDrift(root);
144
+ check('场景 7:多个 owned 一个缺失 → drift=0 gone=1项 total=3 skipped=false',
145
+ r.drift === 0 && r.gone.length === 1 && r.gone[0] === 'workflow/incidents/_TEMPLATE.md' && r.total === 3 && r.skipped === false,
146
+ JSON.stringify(r));
147
+ fs.rmSync(root, { recursive: true, force: true });
148
+ }
149
+
150
+ // ---- 场景 8:owned 漂移 WARN 已严化为 FAIL(2026-09-25 doctor-owned-drift-strict 复盘)----
151
+ // 源码层断言:src/doctor.mjs 第 6.6 节主流程输出必须用 add('FAIL', …) 而非 add('WARN', …);
152
+ // 这是规则严度参数化的最小防回归——不依赖运行时执行 doctor(避免造完整 fixture)。
153
+ {
154
+ const doctorSrc = fs.readFileSync(SRC, 'utf8');
155
+ // 断言:owned 漂移分支必须用 'FAIL'
156
+ const failDrift = /ownedRes\.drift\)\s*add\('FAIL'/.test(doctorSrc);
157
+ check('场景 8a:src/doctor.mjs 第 6.6 节 owned 漂移分支已用 add(\'FAIL\', …)',
158
+ failDrift,
159
+ `src/doctor.mjs 第 6.6 节应为 add('FAIL', ...) 而非 add('WARN', ...)`);
160
+ // 断言:owned gone 分支必须用 'FAIL'
161
+ const failGone = /ownedRes\.gone\.length\)\s*add\('FAIL'/.test(doctorSrc);
162
+ check('场景 8b:src/doctor.mjs 第 6.6 节 owned gone 分支已用 add(\'FAIL\', …)',
163
+ failGone,
164
+ `src/doctor.mjs 第 6.6 节 gone 分支应为 add('FAIL', ...)`);
165
+ // 断言:doctor 主流程 results 含 FAIL 时 process.exit(1)(fail-loud)
166
+ const failLoud = /results\.some\(\(r\)\s*=>\s*r\.level\s*===\s*'FAIL'\)\s*\?\s*1\s*:\s*0/.test(doctorSrc);
167
+ check('场景 8c:doctor 主流程 results 含 FAIL 时 exit 1(fail-loud)',
168
+ failLoud,
169
+ `src/doctor.mjs 应保留 process.exit(1) fail-loud 行为`);
170
+ }
171
+
172
+ // ---- 场景 9-14:checkAdapterDrift(2026-09-25 cross-host-sync)—— 装户侧跨宿主薄适配正文段漂移 ----
173
+ const mkfixAdapter = ({ authority, adapters, pkgMarker }) => {
174
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-adapter-test-'));
175
+ // authorityRoot:.agents/{commands,roles}
176
+ if (authority) {
177
+ for (const [rel, content] of Object.entries(authority)) {
178
+ const abs = path.join(root, '.agents', rel);
179
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
180
+ fs.writeFileSync(abs, content);
181
+ }
182
+ }
183
+ // adapters:<HOST_DIR>/<sub>/<file>
184
+ if (adapters) {
185
+ for (const [rel, content] of Object.entries(adapters)) {
186
+ const abs = path.join(root, rel);
187
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
188
+ fs.writeFileSync(abs, content);
189
+ }
190
+ }
191
+ // pkgMarker:模拟包源(templates/_agents + modules/hosts)
192
+ if (pkgMarker) {
193
+ fs.mkdirSync(path.join(root, 'templates/_agents'), { recursive: true });
194
+ fs.mkdirSync(path.join(root, 'modules/hosts'), { recursive: true });
195
+ }
196
+ return root;
197
+ };
198
+
199
+ const ROLE_BODY = '# Implementer\n\n正文 role v1\n';
200
+ const CMD_BODY = '# Build\n\n正文 build v1\n';
201
+
202
+ // ---- 场景 9:装户侧薄适配全对齐 → drift=0 ----
203
+ {
204
+ const root = mkfixAdapter({
205
+ authority: {
206
+ 'commands/build.md': `---\ndesc: x\n---\n\n${CMD_BODY}`,
207
+ 'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
208
+ },
209
+ adapters: {
210
+ '.opencode/commands/build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
211
+ '.trae/commands/wf-build.md': `---\nname: wf-build\ndesc: trae\n---\n\n${CMD_BODY}`,
212
+ '.zcode/agents/implementer.md': `---\nname: impl\ndesc: zcode\n---\n\n${ROLE_BODY}`,
213
+ '.opencode/agents/implementer.md': `---\nname: impl\ndesc: oc\n---\n\n${ROLE_BODY}`,
214
+ },
215
+ });
216
+ const r = checkAdapterDrift(root);
217
+ check('场景 9:装户全对齐 → drift=0 total=4 skipped=false(1 cmd×2 适配 + 1 role×2 适配 .zcode+.opencode)',
218
+ r.drift === 0 && r.total === 4 && r.skipped === false,
219
+ JSON.stringify(r));
220
+ fs.rmSync(root, { recursive: true, force: true });
221
+ }
222
+
223
+ // ---- 场景 10:薄适配正文漂移 → drift > 0 ----
224
+ {
225
+ const root = mkfixAdapter({
226
+ authority: {
227
+ 'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
228
+ },
229
+ adapters: {
230
+ '.zcode/agents/implementer.md': `---\nname: impl\ndesc: zcode\n---\n\n# Implementer\n\n漂移正文 role v1\n`,
231
+ },
232
+ });
233
+ const r = checkAdapterDrift(root);
234
+ check('场景 10:薄适配正文漂移 → drift=1 total=1',
235
+ r.drift === 1 && r.total === 1 && r.skipped === false,
236
+ JSON.stringify(r));
237
+ fs.rmSync(root, { recursive: true, force: true });
238
+ }
239
+
240
+ // ---- 场景 11:包源环境 → skipped=true(不参与装户比对) ----
241
+ {
242
+ const root = mkfixAdapter({
243
+ pkgMarker: true,
244
+ authority: { 'commands/build.md': `---\ndesc: x\n---\n\n${CMD_BODY}` },
245
+ adapters: { '.opencode/commands/build.md': `---\ndesc: oc\n---\n\n漂移` },
246
+ });
247
+ const r = checkAdapterDrift(root);
248
+ check('场景 11:包源环境(templates/ + modules/ 同时存在)→ skipped=true',
249
+ r.skipped === true && typeof r.note === 'string' && r.note.includes('包源'),
250
+ JSON.stringify(r));
251
+ fs.rmSync(root, { recursive: true, force: true });
252
+ }
253
+
254
+ // ---- 场景 12:.agents 目录不存在 → skipped=true ----
255
+ {
256
+ const root = mkfixAdapter({});
257
+ const r = checkAdapterDrift(root);
258
+ check('场景 12:权威源目录不存在 → skipped=true 含 note',
259
+ r.skipped === true && typeof r.note === 'string' && r.note.includes('.agents'),
260
+ JSON.stringify(r));
261
+ fs.rmSync(root, { recursive: true, force: true });
262
+ }
263
+
264
+ // ---- 场景 13:frontmatter 差异不计漂移(B-b 语义:仅正文段比对) ----
265
+ {
266
+ const root = mkfixAdapter({
267
+ authority: { 'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}` },
268
+ adapters: {
269
+ '.zcode/agents/implementer.md': `---\nname: impl\ndesc: 自定义 zcode 风格\n---\n\n${ROLE_BODY}`,
270
+ },
271
+ });
272
+ const r = checkAdapterDrift(root);
273
+ check('场景 13:frontmatter 差异不算正文漂移 → drift=0 total=1',
274
+ r.drift === 0 && r.total === 1 && r.skipped === false,
275
+ JSON.stringify(r));
276
+ fs.rmSync(root, { recursive: true, force: true });
277
+ }
278
+
279
+ // ---- 场景 14:薄适配缺失不在 drift 统计(apply 不自动创建) ----
280
+ {
281
+ const root = mkfixAdapter({
282
+ authority: {
283
+ 'commands/build.md': `---\ndesc: x\n---\n\n${CMD_BODY}`,
284
+ 'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
285
+ },
286
+ adapters: {
287
+ '.opencode/commands/build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
288
+ },
289
+ });
290
+ const r = checkAdapterDrift(root);
291
+ check('场景 14:薄适配缺失不在 drift 统计 → drift=0 total=1',
292
+ r.drift === 0 && r.total === 1 && r.skipped === false,
293
+ JSON.stringify(r));
294
+ fs.rmSync(root, { recursive: true, force: true });
295
+ }
296
+
297
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
298
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ // ensure-board.mjs — workflow 看板按需手动拉起(默认不自动开启;只起不关:OS 重启即天然回收,无需 stop/status)
3
+ // 跨平台单入口(零依赖,Windows/Linux/macOS 同一脚本,取代旧 ensure-board.ps1):
4
+ // 基端口(--port 显式 > kit.json boardPort > 8933)向上探测首个可用端口(窗口 10)——
5
+ // 端口空闲 → 全新启动;探活 /api/board 比对 root:本项目看板 → 复用(代码文件晚于服务端 startedAt = 旧代码自动 kill 重启);
6
+ // 他人进程(其他项目看板 / 非看板服务)不动手不 kill,跳过试下一端口;全新启动成功后弹浏览器(重启/已运行不弹,SSE 自动重连)
7
+ // 身份与陈旧判定全部来自 /api/board 自报(pid/startedAt)——不碰 OS 专属的端口属主 / 进程启动时间 API。
8
+ // 用法:node .agents/scripts/ensure-board.mjs [--port 8933] [--no-open]
9
+ import net from 'node:net';
10
+ import http from 'node:http';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { spawn } from 'node:child_process';
15
+
16
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
17
+ const ROOT = path.resolve(SCRIPT_DIR, '../..'); // 与 server 的 ROOT 同口径
18
+ const SERVER = path.join(SCRIPT_DIR, 'workflow-board-server.mjs');
19
+ const CODE_FILES = [path.join(ROOT, '.agents', 'board', 'index.html'), SERVER]; // 前端 + 服务端:任一更新都算「旧代码」
20
+ const WINDOW = 10;
21
+
22
+ // ---- 参数与基端口 ----
23
+ const argv = process.argv;
24
+ const flagIdx = argv.indexOf('--port');
25
+ const eqArg = argv.find((a) => a.startsWith('--port='));
26
+ const portArg = flagIdx > -1 ? argv[flagIdx + 1] : eqArg && eqArg.slice('--port='.length);
27
+ const noOpen = argv.includes('--no-open');
28
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29
+
30
+ let BASE_PORT;
31
+ if (portArg && Number(portArg)) {
32
+ BASE_PORT = Number(portArg);
33
+ } else {
34
+ // 基端口取 kit 台账(init 询问值,与命令文档 8933 同源);无台账或解析失败回落 8933
35
+ try { BASE_PORT = Number(JSON.parse(fs.readFileSync(path.join(ROOT, '.agents', 'kit.json'), 'utf8')).options.boardPort) || 8933; }
36
+ catch { BASE_PORT = 8933; }
37
+ }
38
+
39
+ const samePath = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
40
+
41
+ // ---- 探活:端口占用用 net.connect;看板身份/属主用 /api/board 自报(isBoard=含 cards 字段;ours=root 指向本项目)----
42
+ // 不用全局 fetch:undici 连接池 keep-alive 句柄会拖住/污染退出(Windows 上强退触发 libuv 断言)
43
+ function portBusy(port) {
44
+ return new Promise((resolve) => {
45
+ const s = net.connect({ host: '127.0.0.1', port });
46
+ s.setTimeout(800);
47
+ s.on('connect', () => { s.destroy(); resolve(true); });
48
+ s.on('timeout', () => { s.destroy(); resolve(true); }); // 超时从保守视作占用;localhost 上近乎不存在
49
+ s.on('error', () => resolve(false));
50
+ });
51
+ }
52
+
53
+ // /api/board 用 http.get 正规解包(server 响应为 chunked,手工拼包解析不了);Connection: close + 默认 agent 不进连接池,无残留句柄
54
+ function httpProbe(port) {
55
+ return new Promise((resolve) => {
56
+ const req = http.get({ host: '127.0.0.1', port, path: '/api/board', headers: { Connection: 'close' }, timeout: 2000 }, (res) => {
57
+ let buf = '';
58
+ res.on('data', (d) => { buf += d; });
59
+ res.on('end', () => resolve({ status: res.statusCode, json: () => { try { return JSON.parse(buf); } catch { return null; } } }));
60
+ });
61
+ req.on('timeout', () => { req.destroy(); resolve({ status: 0 }); });
62
+ req.on('error', () => resolve({ status: 0 }));
63
+ });
64
+ }
65
+
66
+ async function probeBoard(port) {
67
+ const none = { isBoard: false };
68
+ const r = await httpProbe(port);
69
+ if (r.status !== 200) return none;
70
+ const j = r.json();
71
+ if (!j || typeof j !== 'object' || !('cards' in j)) return none;
72
+ return { isBoard: true, ours: !!(j.root && samePath(path.resolve(String(j.root)), ROOT)), pid: j.pid, startedAt: j.startedAt };
73
+ }
74
+
75
+ // 陈旧判定:代码文件最新 mtime 晚于服务端启动时刻(2s 容差)= 旧代码;无 startedAt(旧版服务端)返回 null 无法判定
76
+ function staleCode(startedAt) {
77
+ if (!startedAt) return null;
78
+ const newest = Math.max(...CODE_FILES.filter((f) => fs.existsSync(f)).map((f) => fs.statSync(f).mtimeMs));
79
+ return newest > startedAt + 2000;
80
+ }
81
+
82
+ // ---- 动作:kill / 拉起(detached + windowsHide,父进程退出后存活)/ 等监听 / 弹浏览器 ----
83
+ function startServer(port) {
84
+ const c = spawn(process.execPath, [SERVER, '--port', String(port)], { detached: true, stdio: 'ignore', windowsHide: true });
85
+ c.unref();
86
+ }
87
+
88
+ async function waitListen(port) {
89
+ const deadline = Date.now() + 8000;
90
+ while (Date.now() < deadline) {
91
+ if (await portBusy(port)) return true;
92
+ await sleep(300);
93
+ }
94
+ return false;
95
+ }
96
+
97
+ // intentional-simple: 弹浏览器 best-effort 三平台分流(win: cmd start / mac: open / linux: xdg-open),失败不打扰——链接已在输出里
98
+ function openBrowser(url) {
99
+ try {
100
+ if (process.platform === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
101
+ else spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
102
+ } catch { }
103
+ }
104
+
105
+ // ---- 主流程(返回退出码,自然排水退出——不用 process.exit:强退在 Windows 上会触发 libuv 断言)----
106
+ async function main() {
107
+ const skipped = [];
108
+ let action = null;
109
+ let port = 0;
110
+ let board = null;
111
+
112
+ for (let p = BASE_PORT; p < BASE_PORT + WINDOW; p++) {
113
+ if (!(await portBusy(p))) { port = p; action = 'fresh start'; break; }
114
+ const b = await probeBoard(p);
115
+ if (b.ours) {
116
+ port = p;
117
+ board = b;
118
+ const stale = staleCode(b.startedAt);
119
+ if (stale === true) {
120
+ try { process.kill(b.pid); } catch { } // 进程可能已自行退出,忽略
121
+ await sleep(500);
122
+ action = 'restarted (stale code detected)';
123
+ } else if (stale === null) {
124
+ action = 'legacy';
125
+ } else {
126
+ action = 'up-to-date';
127
+ }
128
+ break;
129
+ }
130
+ skipped.push(`端口 ${p}(pid ${b.pid ?? '未知'})`);
131
+ }
132
+ const url = `http://127.0.0.1:${port}`;
133
+
134
+ if (!action) {
135
+ console.log(`board: 端口 ${BASE_PORT}..${BASE_PORT + WINDOW - 1} 均被他人进程占用(未动手不 kill):${skipped.join(';')};可 --port 指定其他基端口`);
136
+ return 1;
137
+ }
138
+ if (action === 'legacy') {
139
+ console.log(`board: ${url} 是本项目旧版看板(无 startedAt,无法判定代码新旧),未自动重启;如需更新请手动重启`);
140
+ return 0;
141
+ }
142
+ if (action === 'up-to-date') {
143
+ console.log(`board: already up-to-date at ${url} (pid ${board.pid ?? '未知'})`);
144
+ return 0;
145
+ }
146
+ if (skipped.length) console.log(`board: 基端口段他人进程已跳过:${skipped.join(';')}`);
147
+
148
+ startServer(port);
149
+ if (!(await waitListen(port))) {
150
+ console.log(`board: FAILED to start on ${port}`);
151
+ return 1;
152
+ }
153
+ console.log(`board: ${action} -> ${url}`);
154
+ if (!noOpen && action === 'fresh start') openBrowser(url);
155
+ return 0;
156
+ }
157
+
158
+ process.exitCode = await main();
@@ -0,0 +1,59 @@
1
+ // flow-kit fill-intent:起草 intent 草稿(背景与问题/目标/非目标/约束/影响面/触达红线/验收标准 共 7 节)
2
+ // 读 workflow/intents/_TEMPLATE.md 的节标题 → 输出符合 frontmatter 受限子集(5 字段)的草稿到指定文件
3
+ // 用法:node .agents/scripts/fill-intent.mjs --module pipeline --level L1 --topic "test" --output /tmp/x.md
4
+ // 零依赖;CLI 解析用最简字符串匹配;不替代 AI 起草——本工具只输出模板与提示句
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { ENUMS } from './workflow-enums.mjs';
8
+
9
+ const TODAY = new Date().toISOString().slice(0, 10);
10
+ const LEVELS = ENUMS['level.all']; // 级别词表单源(.agents/workflow-enums.txt)
11
+
12
+ function parseArgs(argv) {
13
+ const out = { topic: null, module: null, level: 'L1', output: null, notes: '' };
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const a = argv[i];
16
+ const next = () => { const v = argv[++i]; if (v === undefined) return null; return v; };
17
+ if (a === '--topic') out.topic = next();
18
+ else if (a === '--module') out.module = next();
19
+ else if (a === '--level') out.level = next();
20
+ else if (a === '--output') out.output = next();
21
+ else if (a === '--notes') out.notes = next() || '';
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
27
+
28
+ const SECTIONS = [
29
+ { title: '背景与问题', hint: '<为什么做;写明需求来源(工单 / 沟通记录 / 待办事项)>' },
30
+ { title: '目标', hint: '- <可验证的目标>' },
31
+ { title: '非目标(L1 微改动无实质内容可删本节,不硬填)', hint: '- <明确不做的,防止范围蔓延>' },
32
+ { title: '约束(L1 微改动无实质内容可删本节,不硬填)', hint: '- <复用什么、不许动什么>' },
33
+ { title: '影响面', hint: '- 模块:<词表之一,见 .agents/workflow-modules.txt>\n- 数据库:无 / <表名>(涉及 schema 变更 → L3;无 schema 的运行时 / 管线结构面亦为 L3,判据见 .agents/commands/new-task.md §级别判断)\n- 前端页面:<路由或组件,无则删本行>' },
34
+ { title: '触达红线(对照 AGENTS.md,勾选仅标记触及范围;具体如何满足在同名 spec 中说明)', hint: '- [ ] <按本项目 AGENTS.md 红线逐行补;无则删本行>\n- [ ] 规则 / 契约变更(编码权威 / 共享契约 / 接口签名 / 既有参数语义 / 全局口径)→ 级别至少 L2\n- [ ] schema / 迁移 SQL / DI 链 / 认证与中间件管线 → 级别 L3\n\n> 触及任一红线即为 L2 / L3,必须立 ../specs/YYYY-MM-DD-<主题>.md 写明如何满足,方可起草 plan。' },
35
+ { title: '验收标准(可测试)', hint: '- [ ] <逐条可测试;写不出可测试判据 = 还没想清楚>\n\n> **闭环对账**:关单在 test 阶段(不依赖 deploy)。intent 置 done 前逐条勾验,每条勾选项后补证据——`- [x] <判据>(证据:<commit SHA / 测试用例名 / 冒烟脚本输出>)`。\n> done 状态仍有未勾项会被 check-loop 拦截(2026-09-12 起新建 intent 为 hard-block,存量 intent 仅 warning 提示);勾选但缺「证据:」为 warning。' },
36
+ ];
37
+
38
+ export function renderIntent({ topic, module, level, notes, date = TODAY }) {
39
+ if (!LEVELS.includes(level)) throw new Error('level 不在枚举 ' + LEVELS.join('|') + ':' + level);
40
+ const fm = '---\n状态: draft\n级别: ' + level + '\n日期: ' + date + '\n模块: ' + module + '\n备注: ' + (notes || '<可选:附注自由文本,check-loop 不解析>') + '\n---\n';
41
+ let body = '# INTENT — ' + topic + '\n\n<!-- 复制本模板为 YYYY-MM-DD-<主题>.md 后填写;plans/ 下同名文件与本文件配对 -->\n<!-- frontmatter 受限子集(2026-09-13):每行 键: 值;状态∈draft/approved/done/superseded/cancelled(严格枚举,附注写备注键);级别∈L0/L1/L2/L3;check-loop 只扫 frontmatter 取机器字段,正文不再写状态/级别行 -->\n\n';
42
+ for (const { title, hint } of SECTIONS) {
43
+ body += '## ' + title + '\n\n' + hint + '\n\n';
44
+ }
45
+ body += '## 确认与复核\n\n- 确认日期:\n- 确认人:用户(对话内一句"可以"即确认)\n- 确认范围:\n- 复核:L1 不要求独立复核\n';
46
+ return { frontmatter: fm, body: fm + body, sections: SECTIONS.map((s) => s.title) };
47
+ }
48
+
49
+ const isMain = process.argv[1] && process.argv[1].endsWith('fill-intent.mjs');
50
+ if (isMain) {
51
+ const a = parseArgs(process.argv.slice(2));
52
+ if (!a.topic || !a.module || !a.output) fail('必填:--topic / --module / --output;选填:--level(默认 L1)/ --notes');
53
+ if (!LEVELS.includes(a.level)) fail('level 必须在 ' + LEVELS.join('|') + ':' + a.level);
54
+ const { body } = renderIntent(a);
55
+ fs.mkdirSync(path.dirname(a.output), { recursive: true });
56
+ fs.writeFileSync(a.output, body);
57
+ console.log('✅ 已生成 intent 草稿:' + a.output + '(状态 draft / 级别 ' + a.level + ' / 模块 ' + a.module + ')');
58
+ }
59
+ export default renderIntent;
@@ -0,0 +1,73 @@
1
+ // fill-intent.test.mjs — fill-intent 工具测试(2026-09-25 doc-fill-tools)
2
+ // 方法:直接 import 模块调 renderIntent 拿字符串(不写盘),断言 frontmatter 字段 + 节数 + 枚举值合法性
3
+ // 用法:node .agents/scripts/fill-intent.test.mjs
4
+ import { renderIntent } from './fill-intent.mjs';
5
+
6
+ let pass = 0, failCount = 0;
7
+ function check(name, cond, detail = '') {
8
+ if (cond) { pass++; console.log('PASS ' + name); }
9
+ else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
10
+ }
11
+
12
+ // ---- 场景 1:L1 必填 frontmatter 字段都在 ----
13
+ {
14
+ const { body } = renderIntent({ topic: 'test', module: 'pipeline', level: 'L1' });
15
+ const fm = body.match(/^---([\s\S]*?)---/);
16
+ check('S1 frontmatter 含状态 draft', /状态:\s*draft/.test(fm[1]));
17
+ check('S1 frontmatter 含级别 L1', /级别:\s*L1/.test(fm[1]));
18
+ check('S1 frontmatter 含日期 YYYY-MM-DD', /日期:\s*\d{4}-\d{2}-\d{2}/.test(fm[1]));
19
+ check('S1 frontmatter 含模块 pipeline', /模块:\s*pipeline/.test(fm[1]));
20
+ check('S1 frontmatter 含备注', /备注:/.test(fm[1]));
21
+ }
22
+
23
+ // ---- 场景 2:L2/L3 frontmatter 同样含完整字段 ----
24
+ {
25
+ for (const level of ['L2', 'L3']) {
26
+ const { body } = renderIntent({ topic: 't', module: 'pipeline', level });
27
+ const fm = body.match(/^---([\s\S]*?)---/);
28
+ check('S2 级别 ' + level + ' frontmatter 完整', /状态:\s*draft/.test(fm[1]) && /级别:\s*/.test(fm[1]) && /日期:/.test(fm[1]));
29
+ }
30
+ }
31
+
32
+ // ---- 场景 3:非法 level 抛错(枚举严格)----
33
+ {
34
+ let threw = false;
35
+ try { renderIntent({ topic: 't', module: 'pipeline', level: 'L9' }); } catch { threw = true; }
36
+ check('S3 非法 level 抛错', threw);
37
+ }
38
+
39
+ // ---- 场景 4:7 节正文齐 ----
40
+ {
41
+ const { body } = renderIntent({ topic: 't', module: 'pipeline', level: 'L1' });
42
+ const expectedSections = ['背景与问题', '目标', '非目标', '约束', '影响面', '触达红线', '验收标准', '确认与复核'];
43
+ for (const s of expectedSections) check('S4 含节 ## ' + s, body.includes('## ' + s));
44
+ check('S4 共 8 节标题', (body.match(/^##\s/gm) || []).length === 8);
45
+ }
46
+
47
+ // ---- 场景 5:备注字段透传 ----
48
+ {
49
+ const { body } = renderIntent({ topic: 't', module: 'pipeline', level: 'L1', notes: '自定义备注' });
50
+ check('S5 自定义备注透传', body.includes('备注: 自定义备注'));
51
+ const { body: body2 } = renderIntent({ topic: 't', module: 'pipeline', level: 'L1' });
52
+ check('S5 默认备注占位', body2.includes('备注: <可选:附注自由文本,check-loop 不解析>'));
53
+ }
54
+
55
+ // ---- 场景 6:日期默认今日 ----
56
+ {
57
+ const today = new Date().toISOString().slice(0, 10);
58
+ const { body } = renderIntent({ topic: 't', module: 'pipeline', level: 'L1' });
59
+ check('S6 日期默认今日 ' + today, body.includes('日期: ' + today));
60
+ const { body: body2 } = renderIntent({ topic: 't', module: 'pipeline', level: 'L1', date: '2026-01-15' });
61
+ check('S6 自定义日期透传', body2.includes('日期: 2026-01-15'));
62
+ }
63
+
64
+ // ---- 场景 7:导出函数可单元测试 ----
65
+ {
66
+ check('S7 renderIntent 是函数', typeof renderIntent === 'function');
67
+ // 边界:缺 topic 应返回字符串(渲染时缺 topic → topic 是 undefined 字符串,不抛错——按 design 透传)
68
+ const r = renderIntent({ module: 'pipeline', level: 'L1' });
69
+ check('S7 缺 topic 仍可渲染(透传设计)', typeof r.body === 'string' && r.body.includes('INTENT — undefined'));
70
+ }
71
+
72
+ console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
73
+ process.exit(failCount ? 1 : 0);
@@ -0,0 +1,60 @@
1
+ // flow-kit fill-plan:起草 plan 草稿(L1 极简 2 节 / L2-L3 完整 4 节)
2
+ // 用法:node .agents/scripts/fill-plan.mjs --level L1 --topic "test" --output /tmp/x.md
3
+ // L1:改动面(必填)+ 验证方式(必填)
4
+ // L2/L3:改动面 + 任务拆解 + 执行顺序 + 验证方式
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { ENUMS } from './workflow-enums.mjs';
8
+
9
+ const TODAY = new Date().toISOString().slice(0, 10);
10
+ const LEVELS = ENUMS['level.all']; // 级别词表单源(.agents/workflow-enums.txt)
11
+
12
+ function parseArgs(argv) {
13
+ const out = { topic: null, level: 'L1', output: null };
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const a = argv[i];
16
+ const next = () => { const v = argv[++i]; if (v === undefined) return null; return v; };
17
+ if (a === '--topic') out.topic = next();
18
+ else if (a === '--level') out.level = next();
19
+ else if (a === '--output') out.output = next();
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
25
+
26
+ const L1_SECTIONS = [
27
+ { title: '改动面(L1 极简形态主节;L2/L3 可作任务拆解的汇总或删本节)', hint: '- <文件/组件>:<做什么;判据细节直接写进条目,如「L771 message.success 改『更新成功』>' },
28
+ { title: '验证方式', hint: '- 静态门:项目构建命令(+ 项目测试命令,测试项目就绪后)\n- 前端:cd frontend && npm run build(vue-tsc)\n- UI:.agents/commands/test.md(涉及页面改动必走,headless Chrome 实测)\n- L2 追加:<契约 / 规则面比对:编码结果抽样 / 接口契约断言 / 口径对账>\n- L3 追加:<含 schema 变更:备份 + 回滚 SQL 就绪后执行;仅运行时 / 管线:回退上一 release tag + 配置开关预案>' },
29
+ ];
30
+
31
+ const FULL_SECTIONS = [
32
+ L1_SECTIONS[0],
33
+ { title: '任务拆解(L2/L3 必填;L1 仅多文件多步骤时用,单任务微改动删本节)', hint: '1. <任务>\n - 判据:<怎样算完成;尽量对应一个测试或可复现操作>\n - 风险:低 / 中 / 高(<原因>)\n2. <任务>\n - 判据:…\n - 风险:…' },
34
+ { title: '执行顺序(L2/L3 必填;L1 单文件微改动删本节)', hint: '<1 → 2 → 3;标注依赖关系>' },
35
+ L1_SECTIONS[1],
36
+ ];
37
+
38
+ export function renderPlan({ topic, level = 'L1', date = TODAY }) {
39
+ if (!LEVELS.includes(level)) throw new Error('level 不在枚举 ' + LEVELS.join('|') + ':' + level);
40
+ const sections = level === 'L1' ? L1_SECTIONS : FULL_SECTIONS;
41
+ const fm = '---\n状态: draft\n级别: ' + level + '\n模块: pipeline\n---\n';
42
+ let body = '# PLAN — ' + topic + '\n\n<!-- 与 intents/ 或 incidents/ 下同名入口文档配对;L2/L3 必须先有 ../specs/ 同名 spec 确认通过;AI 起草、用户对话内确认后开工 -->\n<!-- frontmatter 受限子集(2026-09-13):状态∈draft/approved/done/superseded/cancelled;级别∈L0/L1/L2/L3;正文不再写状态/级别行 -->\n<!-- L1 极简形态(2026-09-13 瘦身,L1 默认):单/少文件微改动只填「改动面」+「验证方式」两节——改动面逐条列改哪个文件做什么(判据细节直接写进条目),验证一行静态门 + 按需 UI 实测;任务拆解/执行顺序仅 L1 多文件多步骤时保留 -->\n\n对应入口:../intents/YYYY-MM-DD-<主题>.md 或 ../incidents/YYYY-MM-DD-<主题>.md(保留实际一项)\n对应 spec:../specs/YYYY-MM-DD-<主题>.md(L1 可省略)\n\n';
43
+ for (const { title, hint } of sections) {
44
+ body += '## ' + title + '\n\n' + hint + '\n\n';
45
+ }
46
+ body += '## 确认与复核\n\n> 确认 = 用户在对话内一句话通过;确认后本 plan 状态 draft → approved 并回填本节(确认环节的机器可见态),done 只在关单出现——禁从 draft 直跳 done(2026-09-22 papercut)。\n- 确认结果:approved(YYYY-MM-DD 用户对话内确认);done(YYYY-MM-DD 关单,随入口文档置终态)\n- 确认门记录:plan 草稿全文过目 + 改动清单确认(build.md 两道门,逐次,不合并)\n- 复核:L1 不要求独立复核\n';
47
+ return { frontmatter: fm, body: fm + body, sections: sections.map((s) => s.title) };
48
+ }
49
+
50
+ const isMain = process.argv[1] && process.argv[1].endsWith('fill-plan.mjs');
51
+ if (isMain) {
52
+ const a = parseArgs(process.argv.slice(2));
53
+ if (!a.topic || !a.output) fail('必填:--topic / --output;选填:--level(默认 L1)');
54
+ if (!LEVELS.includes(a.level)) fail('level 必须在 ' + LEVELS.join('|') + ':' + a.level);
55
+ const { body } = renderPlan(a);
56
+ fs.mkdirSync(path.dirname(a.output), { recursive: true });
57
+ fs.writeFileSync(a.output, body);
58
+ console.log('✅ 已生成 plan 草稿:' + a.output + '(状态 draft / 级别 ' + a.level + ' / ' + (a.level === 'L1' ? '极简 2 节' : '完整 4 节') + ')');
59
+ }
60
+ export default renderPlan;