coding.mjs
33.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
// workflows/coding.mjs
//
// 整个 ERP Coding(B 阶段)= 一个静默、全自动的 Workflow 脚本。
//
// 设计原则(见 docs/superpowers/specs/2026-05-26-workflow-migration-design.md):
// - 所有 stage 都是 agent() 子代理,物理上无法 AskUserQuestion → 编码期结构性静默。
// - 缺值不再问人:派生 stage 把具体阻塞点写进产物并 throw(fail-fast,合法 halt → 终止态,非对话框)。
// - 后端 / 前端功能循环由同一份 featureLoop(items, phase) 驱动;phase 切换 reviewer checklist、
// 测试命令、路径作用域(backend/ vs frontend/)、id 格式(REQ-XXX-NNN vs FE-NN)。
// - 状态账本 = docs/08 §二/§三 + git tag;halt 后重跑 coding-start,router 从账本+tag 重算进度。
// - reviewer 统一为 agents/code-reviewer.md,review stage 用 agentType:'code-reviewer'。
//
// 运行时约束:Workflow 运行时禁用非确定性内建(Date.now / Math.random 等)。本脚本不调用它们;
// 凡需要"当天日期"的产物路径(<YYYY-MM-DD>-<id>.md),一律由子代理在其自身上下文中解析并落盘,
// 脚本只负责编排,不计算日期 / 随机数。
export const meta = {
name: 'erp-coding',
description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.',
phases: [
{ title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' },
{ title: 'Gate' }, { title: 'Milestone' },
],
}
const ROUTER_SCHEMA = { type:'object', additionalProperties:false,
required:['modules'], properties:{ modules:{ type:'array', items:{
type:'object', additionalProperties:false,
required:['id','done','reqs','feItems'],
properties:{ id:{type:'string'}, done:{type:'boolean'},
reqs:{type:'array',items:{type:'string'}},
feItems:{type:'array',items:{type:'string'}} } } } } }
const REVIEW_SCHEMA = { type:'object', additionalProperties:false,
required:['verdict','round','issues'], properties:{
verdict:{type:'string',enum:['approve','request-changes']},
round:{type:'integer'}, issues:{type:'array',items:{type:'string'}} } }
const GATE_SCHEMA = { type:'object', additionalProperties:false,
required:['status'], properties:{ status:{type:'string',enum:['green','red']},
failures:{type:'array',items:{type:'string'}} } }
const ROOT = args?.projectRoot || '.'
// ============================================================================
// Stage prompt builders(纯字符串构造;只用 ROOT / id / phase / 入参,不触非确定性内建)
//
// 每个 prompt 的共同契约(见 commonContract):
// - 子代理是非交互的,物理上无法弹窗;缺任何值都不要"问人"——把具体阻塞点写进产物并失败。
// - phase=backend 与 phase=frontend 的差异(路径作用域 / id 形态 / 测试命令来源)逐条写明。
// - 所有输出文档用中文。
// ============================================================================
function isFrontend(phase) { return phase === 'frontend' }
// 所有子代理共享的"非交互静默"硬约束。
function commonContract(phase) {
const fe = isFrontend(phase)
return [
'## 硬约束(非交互子代理)',
'- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。',
'- 需要具体值时,按此顺序自行获取:(1) 先查 `.env.local`、`docs/07-环境配置.md`、`docs/04-技术规范.md`、`docs/05-API接口契约.md`、`docs/06-UI交互规范.md`、`CLAUDE.md`、现有代码;(2) 仍查不到 → **不要编造、不要留 `【人工填写:】` / `TBD` / `TODO` 占位**,而是把**具体的阻塞点**(缺哪个值、应在哪个 Plan 期闸门锁定、为何无法继续)写进产物,然后让本步骤**失败**(以非零结果 / 显式 throw 结束),由上层 Workflow 转为带诊断的 halt。',
'- 全部输出文档**使用中文**。',
`- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe
? '实现文件必须落在 `frontend/`(或 `docs/09-项目目录结构.md § 前端目录结构` 声明的前端根)下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。'
: '产出范围限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约;**禁止**写 `frontend/` 路径下的实现(UI 推迟到前端阶段)。'}`,
`- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `REQ-XXX-NNN`。'}`,
].join('\n')
}
// Router:读 docs/08 §二/§三 + git tag,重算进度,返回 ROUTER_SCHEMA。
function routerPrompt(root) {
return [
'# Coding Router — 从账本重算进度',
'',
`项目根:\`${root}\``,
'',
'你是 Coding 阶段的路由子代理。**只读不写**(不改任何代码 / 文档),仅从状态账本重算"哪些模块还要跑",返回结构化结果。',
'',
'## 读取来源(账本 = docs/08 + git tag,二者一致才算完成)',
'1. `docs/08-模块任务管理.md § 二`(后端模块元数据):逐个模块取 `id`(英文蛇形 module id)、本模块的 REQ 列表(按 `docs/02-开发计划.md § 二 开发顺序清单` 的顺序,A5 约束保证同模块 REQ 连续),以及该模块的 `里程碑:` 字段。',
'2. `docs/08-模块任务管理.md § 三`(前端阶段元数据):取 `整体里程碑:` 字段,以及 `功能:` 项下所有 `- [ ] FE-NN ...` / `- [x] FE-NN ...` 行(FE 清单)。前端 item 形如 `FE-NN`。',
'3. `git -C <root> tag -l "milestone/*"`:列出已打的里程碑 tag。',
'',
'## 完成判定(每个模块独立)',
'- 后端模块 `done = true` 当且仅当:§二 该模块 `里程碑:` 字段 == `milestone/<module_id>` **且** `git tag -l "milestone/<module_id>"` 能查到该 tag。任一缺失 → `done = false`。',
'- 前端 item(FE-NN)归属一个"逻辑前端模块"。前端阶段整体 `done` 当且仅当 §三 `整体里程碑:` == `milestone/frontend-phase` 且 `git tag -l "milestone/frontend-phase"` 存在。',
'',
'## 输出(必须符合下发的 JSON schema)',
'- `modules`: 数组,按 `docs/02 § 二` 的模块顺序排列。每项:',
' - `id`: 模块标识(后端为英文蛇形 module id;前端聚合为单一逻辑模块时用 `frontend-phase`)。',
' - `done`: 该模块是否已完成(按上面的判定)。',
' - `reqs`: 本模块**未完成**后端 REQ 的有序列表(已 `verdict=approve`(见 `docs/superpowers/reviews/*-<REQ>.md`)的 REQ 跳过)。模块已 done → 空数组。',
' - `feItems`: 本模块关联的**未完成**前端 FE-NN 列表(已 approve 的 FE 跳过);无前端 → 空数组。',
'- 不要返回任何额外字段(schema 为 `additionalProperties:false`)。',
'',
'## 缺值处理',
'- docs/08 §二/§三 缺失 / 格式不符 / 无法解析 → **不要猜**:把具体的解析失败点写入返回前的诊断并使本步骤失败(让 Workflow halt),由人工修复 Plan 产物后重跑 `coding-start`。',
].join('\n')
}
// ---- 功能内循环 stage 1:派生 spec(原 feature-brainstorm / fe-feature-brainstorm)----
function deriveSpecPrompt(id, phase) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-brainstorm' : 'feature-brainstorm'} — 派生规格 ${id}`,
'',
commonContract(phase),
'',
'## 目标',
`静默派生 \`${id}\` 的实现规格(无 Q&A)。需求歧义本应在 Plan 期的结构化 per-REQ 表单 / 前端 scope-lock 锁定;这里**只消费已锁定的事实**,不再澄清。`,
'',
'## 收集上下文',
fe
? [
`- 关联 REQ 卡片:\`${ROOT}/docs/01-需求清单/<module>/<REQ>.md\`(提取业务校验规则、acceptance、UI 描述)。`,
`- 关联 prototype:Read \`${ROOT}/prototype/**/*.html\`(含 anchor 时聚焦相应区域),作为页面布局权威。`,
`- API 契约:\`${ROOT}/docs/05-API接口契约.md\`,按本 FE 关联的 REQ 过滤出消费的端点。`,
`- Design Tokens:\`${ROOT}/docs/06-UI交互规范.md § 二\`(色值 / 状态色引用源)。`,
`- 前端组件库:\`${ROOT}/docs/04-技术规范.md § 零\` 的 \`frontend.ui_lib\`,决定组件选型。`,
].join('\n')
: [
`- REQ 卡片:\`${ROOT}/docs/01-需求清单/<module>/${id}.md\`。**忽略 UI 描述**(控件类型 / 按钮位置 / 列表布局),但校验规则、业务规则仍要落到后端 DTO + service。`,
`- 涉及的数据表定义:\`${ROOT}/docs/03-数据库设计文档.md\`(必要时实时查 mysql 只读)。`,
`- API 契约:\`${ROOT}/docs/05-API接口契约.md\` 中本 REQ 相关端点。`,
].join('\n'),
'',
'## 写 spec',
`- 落盘 \`${ROOT}/docs/superpowers/specs/<当天日期 YYYY-MM-DD>-${id}.md\`(当天日期由你在自身上下文解析,脚本不传日期)。`,
fe
? '- 规格至少含:关联 REQ + 关联原型;组件树(按页面 / 区域分块,推导自 prototype DOM);页面状态机(loading / empty / error / 正常 / 表单提交中 至少 5 态);消费的后端端点(对齐 docs/05);业务规则前端复刻清单(逐条:规则 / 触发时机 / 报错文案 / 来源 REQ);Design Tokens 引用清单(`var(--color-*)`)。'
: '- 规格覆盖:goal / 输入输出 / 业务规则 / 约束 / schema / API 引用 / acceptance criteria。',
'',
'## 自审(inline 修,无须等待)',
`- 占位符扫描:\`TBD\` / \`TODO\` / \`【人工填写:】\`${fe ? ' / `controller` / `service` / `SQL` / `migration`(前端 spec 不应出现后端字样)' : ''} → 命中即修;修不掉的缺值按硬约束失败。`,
'- 内部一致性 / 范围检查(单 plan 能消化吗)/ 歧义检查(任一 requirement 两种解读 → 挑一个写明)。',
'',
'## 结束',
`- 成功:输出一行 \`${fe ? 'fe-' : ''}feature-brainstorm: ${id} → <spec path>\`,把该 spec 路径作为本步骤结果返回(供下游 plan stage 使用)。`,
'- 不要输出"交给下一步 / 等待检查"之类的桥接叙述。',
].join('\n')
}
// ---- stage 2:spec → 任务级 TDD 计划(原 feature-plan / fe-feature-plan)----
function planPrompt(id, phase, spec) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-plan' : 'feature-plan'} — 任务级计划 ${id}`,
'',
commonContract(phase),
'',
'## 输入',
`- 上游 spec:${spec ? `\`${spec}\`` : `\`${ROOT}/docs/superpowers/specs/<当天日期>-${id}.md\``}(不存在则失败)。`,
fe
? `- \`${ROOT}/docs/04-技术规范.md § 一 前端架构\`(路由 / 状态库 / 组件目录约定 / 测试栈);\`${ROOT}/docs/09-项目目录结构.md § 前端目录结构\`(落盘位置)。用 Grep 在 \`${ROOT}/frontend/\` 定位现有文件。`
: `- \`${ROOT}/docs/04-技术规范.md\` 与 \`${ROOT}/docs/09-项目目录结构.md\`(编码规范 + 目录规范)。用 Grep 在现有代码定位待修改文件。`,
'',
'## 计划写作原则',
'- Plan 告诉 TDD 执行者**做什么**,不是**怎么写代码**(执行者是同模型、全上下文的 tdd stage)。',
`- Plan 锁定**文件边界 + 测试意图 + ${fe ? 'props 契约' : 'API 形状'} + 完成判据**;代码由 TDD 红绿循环产出。`,
'- **禁止 dump 整个文件内容**(pom.xml / entity / config / 组件源码)到 plan——避免双 source of truth 漂移。',
fe ? '- 每个任务标注"测试先行类型" = **jsdom 组件测试** OR **Playwright E2E**。' : '',
'- DRY、YAGNI、TDD、frequent commits。',
'',
'## 任务结构(每个 task = 一个 red-green-commit 单元,4 step)',
'1. 写失败测试(给 `test_file::test_name` + 测试意图);2. 实现最小代码(给 `impl_file`);3. 子会话验证 PASS;4. commit。任务粒度 2-5 分钟。',
fe
? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\`(或 docs/09 声明的前端根)开头;命中 \`backend/\` / \`sql/\` / \`scripts/\` → 修正后重渲染。`
: `- **硬护栏**:任务粒度限定后端文件(controller / service / repository / DTO / 校验 / SQL migration);**禁止**生成 \`frontend/\` 路径任务。`,
'- 允许写死的少数场景:DDL / migration 语句、合同级常量(错误码 / JWT claim / Redis key / 路由 path / API client 签名 / Design Tokens 名)、可选的测试断言 sketch。其余一律散文 + 签名描述。',
'- 首次出现的类 / 方法 / 组件 / hook / API client 函数必须给出签名;跨 task 的签名 / 错误码 / props 类型必须一致。',
'',
'## 写 plan + 自审',
`- 落盘 \`${ROOT}/docs/superpowers/plans/<当天日期 YYYY-MM-DD>-${id}.md\`,文件头含 Goal / Architecture / Tech Stack + checkbox 任务。`,
'- 自审:占位符扫描(按硬约束清单);spec coverage(spec 每节至少指向一个 task,补 gap);类型一致性(签名 / 方法名 / 错误码 / props 一致)。',
'',
'## 结束',
`- 成功:输出一行 \`${fe ? 'fe-' : ''}feature-plan: ${id} → <plan path>\`,把该 plan 路径作为结果返回。`,
].filter(Boolean).join('\n')
}
// ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)----
function tddPrompt(id, phase, plan) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`,
'',
commonContract(phase),
'',
'## 输入',
`- 计划文件:${plan ? `\`${plan}\`` : `\`${ROOT}/docs/superpowers/plans/<当天日期>-${id}.md\``}(不存在则失败)。`,
`- 测试命令来源:\`${ROOT}/docs/04-技术规范.md § 零\`${fe
? ' 的 `frontend.unit_test_runner` / `frontend.e2e_runner` / `frontend.test_command` / `frontend.e2e_command`(缺失则默认 `pnpm test:ci` / `pnpm e2e:ci`)。'
: ' 确认的后端测试命令(如 Maven profile / `./scripts/test.mjs`)。'}`,
'',
'## 流程',
fe ? '' : '- **Schema 改动前置**(仅当 plan 声明需要):第一个任务写 migration 文件 `V<n>__<snake_case>.sql`(`<n>` = 现有 `sql/migrations/V*.sql` 最大版本号 + 1,只含 DDL),**同步**把新 CREATE / ALTER 反向更新到 `docs/03-数据库设计文档.md` 对应表小节(docs/03 是 schema 的 SSoT),migration + docs/03 改动同一 commit。',
'- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。',
fe
? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。'
: '',
'',
'## 护栏',
'- **绝不**在主会话直接跑测试(mvn / pnpm / playwright / scripts/test.mjs)——必须通过 Agent 子会话。',
fe
? '- **绝不**写非 `frontend/`(或 docs/09 前端根)路径的 `impl_file`;命中 `backend/` / `sql/` / `scripts/` → 硬停并打印 `不允许写非前端文件:<impl_file>`。'
: '- **后端阶段路径硬护栏**:任意 `impl_file` 以 `frontend/` 开头 → 硬停并打印 `后端阶段不允许写前端代码:<impl_file>`,不再继续 TDD。',
'- 每次 commit 含 REQ/FE 标签,不混合无关改动。',
'- **同一测试修复超过 10 次仍失败 → 立即失败(halt)**,把"哪个测试、失败断言、已尝试的修复"写进诊断;**不要**问人、不要无限重试。',
'',
'## 结束',
`- 全部任务通过:输出一行 \`${fe ? 'fe-' : ''}feature-tdd: ${id} 完成\`,把"已实现 + 已 commit"摘要作为结果返回(供 verify stage)。`,
].filter(Boolean).join('\n')
}
// ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)----
function verifyPrompt(id, phase, impl) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-verify' : 'feature-verify'} — 证据验证 ${id}`,
'',
commonContract(phase),
'',
'## 目标',
`把 \`${id}\` 的功能测试**派发到 Agent 子会话**执行,按结构化结果渲染证据。**主会话从不直接跑测试,也不自由编写证据。**`,
impl ? `(上游 TDD 摘要:${impl})` : '',
'',
'## 流程',
fe
? [
`- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 取(缺失默认 \`pnpm test:ci\` / \`pnpm e2e:ci\`)。`,
'- 派子会话依次跑 unit + e2e,子会话只返回结构化 JSON:`{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt}, e2e:{...同结构} }`(`stdout_excerpt` ≤ 30 行)。',
'- **任一目标 `exit_code != 0` 或 `failed > 0`** → 渲染证据后失败,不进入 review。',
].join('\n')
: [
`- 测试目标:从 plan 或项目标准命令确定(Maven profile / pnpm script / pytest path / \`${ROOT}/docs/04-技术规范.md § 零\` 的后端命令)。`,
'- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行,不塞全文 stdout)。',
'- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后失败,不进入 review。',
].join('\n'),
`- 证据渲染并打印到会话;如需落盘,写 \`${ROOT}/docs/superpowers/reviews/\` 旁的证据位(沿用项目既有约定)。`,
'',
'## 结束',
`- 全部通过:输出一行 \`${fe ? 'fe-' : ''}feature-verify: ${id} 通过\`,把验证摘要作为结果返回(供 review stage)。`,
].filter(Boolean).join('\n')
}
// ---- stage 5a:AI 自审 diff(原 feature-review / fe-feature-review)——委托统一 reviewer agent ----
function reviewPrompt(id, phase, round) {
const fe = isFrontend(phase)
return [
`# ${fe ? 'fe-feature-review' : 'feature-review'} — AI 自审 ${id}(第 ${round} 轮)`,
'',
commonContract(phase),
'',
'## 目标',
`对 \`${id}\` 本轮引入的代码 diff 做 AI 自审,给出 \`approve\` 或 \`request-changes\` 裁决。`,
'',
'## 输入给 reviewer',
`- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${ROOT}/docs/superpowers/specs/<当天日期>-${id}.md\`。`,
fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '',
`- **phase = ${fe ? 'frontend → 附加前端 7 维 checklist(a11y / 对比度 / 响应式 等);主观维度仅标记明显问题,不因主观判断触发 request-changes(避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`,
'',
'## 输出(必须符合下发的 REVIEW JSON schema)',
`- \`verdict\`: \`approve\` | \`request-changes\`;\`round\`: 整数(本轮 = ${round});\`issues\`: must-fix 问题清单(approve 时可空数组)。`,
`- 渲染审阅报告写入 \`${ROOT}/docs/superpowers/reviews/<当天日期 YYYY-MM-DD>-${id}.md\`(\`verdict\` 字段与返回值一致——router / 进度判定靠它)。`,
`- approve 时,把 \`${ROOT}/docs/08-模块任务管理.md\` ${fe ? '§ 三' : '§ 二'} 中本 ${fe ? 'FE' : 'REQ'} 的 \`- [ ] ${id} ...\` 改为 \`- [x] ${id} ...\`(功能级可视化;模块完成仍以里程碑 tag 为准)。`,
'- 不要返回额外字段(schema 为 `additionalProperties:false`)。',
].filter(Boolean).join('\n')
}
// ---- stage 5b:按 review must-fix 修复并重新 commit(review 循环的 fix 步)----
function fixPrompt(id, phase, issues) {
const fe = isFrontend(phase)
const list = Array.isArray(issues) && issues.length
? issues.map((x, i) => ` ${i + 1}. ${x}`).join('\n')
: ' (上一轮 review 的 must-fix 清单——见 ' + (fe ? '§三' : '§二') + ' 对应 review 报告)'
return [
`# ${fe ? 'fe-feature' : 'feature'} fix — 修复 review must-fix ${id}`,
'',
commonContract(phase),
'',
'## 待修复 must-fix',
list,
'',
'## 流程',
'- 逐项编辑 must-fix 指向的代码文件(遵守阶段路径作用域护栏)。',
`- 修复后 commit:\`fix(<scope>): 修复 review must-fix ${fe ? `REQ_ID: ${id}` : id}\`(不混合无关改动)。`,
'- 修复完成后本步骤即结束;上层 Workflow 会重新跑 verify + review(下一轮)。',
'- **缺值仍不要问人**:按硬约束把阻塞点写进诊断并失败。',
'',
'## 结束',
`- 输出一行 \`${fe ? 'fe-' : ''}feature-fix: ${id} 已修复 ${Array.isArray(issues) ? issues.length : ''} 项\`。`,
].join('\n')
}
// ---- 测试闸(原 test-gate)----
function gatePrompt(module, phase) {
const fe = isFrontend(phase)
const id = module?.id ?? '<module>'
return [
`# test-gate — ${fe ? '前端阶段' : `模块 ${id}`} 硬测试闸(phase=${phase})`,
'',
commonContract(phase),
'',
'## 目标',
`打里程碑 tag 前的唯一硬测试门。**派发 Agent 子会话**跑测试,绿则通过,红则失败。**绝不**在主会话直接跑测试,红色时**绝不**跳过。`,
'',
'## 命令',
fe
? `- 前端:命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 拼接(缺失则 \`pnpm test:ci && pnpm e2e:ci\`),跑 vitest + playwright(含全 FE 回归)。`
: `- 后端:跑 \`${ROOT}/scripts/test.mjs\`(跨平台 Node 测试入口;含本模块新增 + 已合并模块回归)。`,
'- 子会话只返回结构化 JSON:`{command, exit_code, passed, failed, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行含 FAIL 摘要)。',
'',
'## 证据 + commit',
`- 渲染证据写入 \`${ROOT}/docs/superpowers/module-reports/${fe ? 'frontend-phase' : `${id}`}-test-gate.md\` 并 commit 到当前分支(保证证据随里程碑可审计)。`,
'',
'## 输出(必须符合下发的 GATE JSON schema)',
'- `status`: `green`(`exit_code = 0` 且 `failed = 0`)| `red`;`failures`: 失败用例摘要(green 时可省略 / 空数组)。',
'- 不要返回额外字段。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。',
].join('\n')
}
// ---- 跨模块改动记录(替代被删的 cross-module hook + cross-module-log skill)----
function crossModulePrompt(module) {
const id = module?.id ?? '<module>'
return [
`# cross-module-log — 记录模块 ${id} 的跨模块改动`,
'',
commonContract('backend'),
'',
'## 目标',
`替代被删的 \`log-cross-module\` hook + \`cross-module-log\` skill:扫描本模块周期内对**非本模块**文件的改动,落跨模块日志(原因 + 影响评估),供 module-report § ⑦ 嵌入。`,
'',
'## 流程',
`- 用 \`git -C ${ROOT} diff --name-status\`(区间:模块分支起点 → HEAD)找出改动文件,判定哪些落在**其它模块**的目录下(按 docs/09 目录归属)。`,
`- 写 / 更新 \`${ROOT}/docs/superpowers/module-reports/${id}-cross-module.md\`,每行列:时间戳(你自身上下文解析当天,脚本不传)/ 目标模块 / 文件 / 改动摘要 / **原因**(本模块哪个 REQ 迫使改它)/ **影响评估**(目标模块哪些 API / 行为 / 调用方受影响、现有测试是否仍有效、是否需新测试,1-3 句)。`,
'- 无跨模块改动 → 输出 `cross-module-log: 无跨模块改动,跳过`,不创建文件。',
'- **不要留 `TBD(CC 补)`**:本步骤就是补齐的唯一时机;推不出原因/影响 → 按硬约束写阻塞点并失败。',
'',
'## 结束',
`- 输出一行 \`cross-module-log: 模块 ${id} 更新 N 行 / 跳过\`。`,
].join('\n')
}
// ---- 模块完成报告(原 module-report)----
function reportPrompt(module) {
const id = module?.id ?? '<module>'
const fe = id === 'frontend-phase'
return [
`# module-report — ${fe ? '前端阶段' : `模块 ${id}`} 12 节完成报告`,
'',
commonContract(fe ? 'frontend' : 'backend'),
'',
'## 目标',
`test-gate 绿后渲染标准化 **12 节**完成报告,commit 到当前分支(供 milestone 标记)。**只读 git 摘要,不读 diff 正文进上下文。**`,
'',
'## 前置',
`- 验证上游 test-gate 已绿:读 \`${ROOT}/docs/superpowers/module-reports/${fe ? 'frontend-phase' : `${id}`}-test-gate.md\`;红则停。`,
'',
'## 收集输入(取摘要而非正文)',
fe
? [
'- § ① `module_id = frontend-phase`,`module_name = 前端阶段(整体)`。',
`- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`,
`- § ③ 文件变更:\`git -C ${ROOT} diff --stat\`(区间 \`frontend-phase\` 分支起点 → HEAD)。`,
'- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。',
`- § ⑤:读 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate.md\`。`,
'- § ⑧ 偏离清单:额外审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出。',
'- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。',
].join('\n')
: [
`- § ③ 文件变更:\`git -C ${ROOT} diff --stat\` / \`--name-status\` / \`git log --oneline\`(区间:module 分支起点 → HEAD)。`,
`- § ② / § ⑨:读 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-<本模块 REQ>.md\`。`,
`- § ⑤:读 \`${ROOT}/docs/superpowers/module-reports/${id}-test-gate.md\`。`,
`- § ⑥ Migration:\`git -C ${ROOT} diff --name-only --diff-filter=A -- 'sql/migrations/V*.sql'\` 列新增,每个读第一行作说明。`,
`- § ⑦ 跨模块改动:读 \`${ROOT}/docs/superpowers/module-reports/${id}-cross-module.md\`(如存在;其中不应再有 \`TBD(CC 补)\`,上一步 cross-module-log 已补齐)。`,
'- § ④ 读写的表:grep 定位涉 SQL 文件后按需读片段,**不全量读 docs/03**。',
].join('\n'),
'',
'## 渲染 + 验证 + commit',
'- 渲染 12 节。硬验证:§ ⑧ 必须列举所有偏离(无则写"无偏离")。',
`- 写入 \`${ROOT}/docs/superpowers/module-reports/<当天日期 YYYY-MM-DD>-${fe ? 'frontend-phase' : `${id}`}.md\`,连同跨模块日志(如存在)一起 commit 到当前分支(milestone 的 worktree-clean 前置依赖此 commit)。`,
'',
'## 结束',
`- 输出一行 \`module-report: ${fe ? 'frontend-phase' : id} → <report path>\`。`,
].join('\n')
}
// ---- 里程碑:本地 merge --no-ff + tag + 回写 docs/08(原 milestone-tag,单 stage 内幂等)----
function milestonePrompt(module) {
const id = module?.id ?? '<module>'
const fe = id === 'frontend-phase'
const phaseId = fe ? 'frontend-phase' : id
return [
`# milestone-tag — ${fe ? '前端阶段' : `模块 ${id}`} 本地集成 + 打里程碑(幂等)`,
'',
commonContract(fe ? 'frontend' : 'backend'),
'',
'## 目标',
`把当前分支(${fe ? '`frontend-phase`' : `\`module-${id}\``})本地合并进默认分支并打 \`milestone/${phaseId}\` tag,把 tag 名回写 docs/08 + 报告 § ⑫。**全程无人工介入**;本 stage 内**重入幂等**(先写 docs/08,再打 tag,已存在则跳过)。`,
'',
'## 流程(顺序执行,任一硬错误 → 停下打印诊断,不自动 stash / 覆盖 / --abort)',
'1. **验证 worktree 干净**:`git -C ' + ROOT + ' status --porcelain` 非空 → 失败并打印 dirty 文件清单(检查 test-gate / module-report 是否都已 commit)。',
`2. **探测默认分支**:用 \`git -C ${ROOT} rev-parse --verify\` 依次试本地 \`main\` / \`master\`,取第一个存在的为 \`default_branch\`;都不存在 → 失败。`,
`3. **本地集成**:\`git -C ${ROOT} checkout <default_branch>\` 后 \`git -C ${ROOT} merge --no-ff ${fe ? 'frontend-phase' : `module-${id}`} -m "merge(${phaseId}): integrate ${fe ? 'frontend-phase' : `module-${id}`}"\`。合并冲突 → 失败并打印冲突文件清单(引导人工解决后重跑 coding-start)。`,
`4. **回写 docs/08 + commit**:在 default_branch 上 Edit \`${ROOT}/docs/08-模块任务管理.md\`:${fe
? '§ 三 `- 整体里程碑: —` 改为 `- 整体里程碑: milestone/frontend-phase`'
: `§ 二 该模块 \` - 里程碑: —\` 改为 \` - 里程碑: milestone/${id}\``};commit \`chore(${phaseId}): record milestone/${phaseId} in docs/08\`。`,
`5. **打 annotated tag**(幂等):\`git -C ${ROOT} tag -a milestone/${phaseId} -m "milestone(${phaseId}): ${fe ? '前端' : '后端'}阶段完成"\`;tag 已存在则跳过。`,
`6. **追加 tag 到报告 § ⑫**:Edit 当天报告 \`${ROOT}/docs/superpowers/module-reports/<日期>-${phaseId}.md\` 的 § ⑫,把 \`{{milestone_tag}}\` 替换为 \`milestone/${phaseId}\`(已替换则跳过);commit \`docs(${phaseId}): record milestone/${phaseId} in completion report\`。`,
'',
'## 结束',
`- 输出一行 \`milestone-tag: ${phaseId} → milestone/${phaseId}\`。不要在本 stage 内回调 coding-start——推进下一模块由上层 Workflow 的循环负责。`,
].join('\n')
}
// ============================================================================
// 编排逻辑(结构按 plan 骨架;featureLoop / reviewWithFixLoop / testGate / 顶层循环)
// ============================================================================
// ---- 单功能链(后端 / 前端同构)----
async function featureLoop(items, phase) {
return pipeline(items,
(id) => agent(deriveSpecPrompt(id, phase), {label:`spec:${phase}:${id}`, phase: phase==='backend'?'Backend':'Frontend'}),
(spec, id) => agent(planPrompt(id, phase, spec), {label:`plan:${phase}:${id}`, phase: phase==='backend'?'Backend':'Frontend'}),
(plan, id) => agent(tddPrompt(id, phase, plan), {label:`tdd:${phase}:${id}`, phase: phase==='backend'?'Backend':'Frontend'}),
(impl, id) => agent(verifyPrompt(id, phase, impl), {label:`verify:${phase}:${id}`, phase: phase==='backend'?'Backend':'Frontend'}),
(v, id) => reviewWithFixLoop(id, phase, v),
)
}
// 有界 5 轮修复;超出 → throw(终止态,非对话框)
async function reviewWithFixLoop(id, phase, verifyResult) {
for (let round = 1; round <= 5; round++) {
const r = await agent(reviewPrompt(id, phase, round), {label:`review:${phase}:${id}:r${round}`, phase: phase==='backend'?'Backend':'Frontend', schema: REVIEW_SCHEMA, agentType:'code-reviewer'})
if (r.verdict === 'approve') return { id, phase, approved:true, rounds:round }
await agent(fixPrompt(id, phase, r.issues), {label:`fix:${phase}:${id}:r${round}`, phase: phase==='backend'?'Backend':'Frontend'})
}
throw new Error(`HALT review-unresolved ${phase}:${id} after 5 rounds`)
}
async function testGate(module, phase) {
let g = await agent(gatePrompt(module, phase), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA})
if (g.status === 'red') { // 自动重试 1 次(防 flaky)
g = await agent(gatePrompt(module, phase) + '\n(retry once for flakiness)', {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA})
}
if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${(g.failures||[]).join('; ')}`)
return g
}
phase('Router')
const routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA})
const todo = routed.modules.filter(m => !m.done)
log(`coding: ${todo.length}/${routed.modules.length} modules to run`)
const results = []
for (const module of todo) {
try {
await featureLoop(module.reqs, 'backend')
await testGate(module, 'backend')
if (module.feItems.length) { await featureLoop(module.feItems, 'frontend'); await testGate(module, 'frontend') }
await agent(crossModulePrompt(module), {label:`xmod:${module.id}`, phase:'Milestone'}) // 替代被删 hook
await agent(reportPrompt(module), {label:`report:${module.id}`, phase:'Milestone'})
await agent(milestonePrompt(module), {label:`milestone:${module.id}`, phase:'Milestone'}) // git merge --no-ff + tag + 更新 docs/08(单 stage 内幂等)
results.push({ module: module.id, status:'done' })
} catch (e) {
results.push({ module: module.id, status:'halted', reason: String(e.message||e) })
break // 整阶段 fail-fast:halt 后停,等人工修复后重跑 coding-start
}
}
// Workflow 结果:跑完 / halt 的逐模块摘要。
// 注:plan 骨架原文是顶层 `return { results }`;裸 top-level return 在独立 ESM 模块下
// 无法通过 `node --check`(Illegal return statement),故改为 `export default`——
// Workflow 运行时读取模块的默认导出作为结果,语义等价、结构其余部分与骨架逐行一致。
export default { results }