coding.mjs 36.8 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 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
// 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 § 二` 的模块顺序列出全部后端模块,**再在末尾追加唯一一个前端聚合模块**(仅当存在前端 FE 时)。每项:',
    '  - `id`: 模块标识(后端为英文蛇形 module id;前端聚合模块固定用 `frontend-phase`)。',
    '  - `done`: 该模块是否已完成(按上面的判定)。',
    '  - `reqs`: **仅后端模块**填本模块**未完成**后端 REQ 的有序列表(已 `verdict=approve`,见 `docs/superpowers/reviews/*-<REQ>.md` 的 REQ 跳过);模块已 done → 空数组。**前端聚合模块 `reqs` 恒为空数组**。',
    '  - `feItems`: **仅前端聚合模块**填——把**全部模块**的**未完成**前端 FE-NN 汇总为一个有序列表(已 approve 的 FE 跳过)放进 `frontend-phase` 这一项。**后端模块 `feItems` 恒为空数组**(前端不分摊到后端模块)。',
    '- 即:后端模块只承载 `reqs`、`feItems=[]`;末尾的 `frontend-phase` 模块只承载 `feItems`、`reqs=[]`。整个项目至多一个前端聚合模块,对应至多一个 `milestone/frontend-phase` tag。',
    '- 不要返回任何额外字段(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 <默认分支 main/master>...HEAD\`(三点 diff,区间 = 本功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)找出改动文件,判定哪些落在**其它模块**的目录下(按 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 <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`,
          '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。',
          `- § ⑤:读 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate.md\`。`,
          '- § ⑧ 偏离清单:额外审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出。',
          '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。',
        ].join('\n')
      : [
          `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\` / \`--name-status\` / \`git log <默认分支>..HEAD --oneline\`(区间 = 功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)。`,
          `- § ② / § ⑨:读 \`${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')
}

// ---- 功能分支生命周期:进入模块前建/切功能分支(milestone 的 merge 源)----
// 幂等支持续跑:分支已存在则 checkout 续跑,否则从默认分支开新支。
function branchSetupPrompt(module) {
  const id = module?.id ?? '<module>'
  const fe = id === 'frontend-phase'
  const branch = fe ? 'frontend-phase' : `module-${id}`
  return [
    `# branch-setup — ${fe ? '前端阶段' : `模块 ${id}`} 功能分支准备(幂等)`,
    '',
    commonContract(fe ? 'frontend' : 'backend'),
    '',
    '## 目标',
    `为本${fe ? '前端阶段' : '模块'}准备功能分支 \`${branch}\`,使后续 featureLoop / testGate / report 的 commit 都落在该分支上;milestone stage 再把它 \`merge --no-ff\` 回默认分支。**本 stage 内重入幂等**。`,
    '',
    '## 流程(顺序执行,任一硬错误 → 停下打印诊断,不自动 stash / 覆盖)',
    `1. **探测默认分支**:用 \`git -C ${ROOT} rev-parse --verify\` 依次试本地 \`main\` / \`master\`,取第一个存在的为 \`default_branch\`;都不存在 → 失败。`,
    `2. **校验工作树干净**:\`git -C ${ROOT} status --porcelain\` 非空 → 失败并打印 dirty 文件清单(进入模块前必须是干净状态)。`,
    `3. **建 / 切功能分支**(幂等):`,
    `   - 若 \`git -C ${ROOT} rev-parse --verify ${branch}\` 成功(分支已存在,续跑场景)→ \`git -C ${ROOT} checkout ${branch}\`。`,
    `   - 否则 → \`git -C ${ROOT} checkout <default_branch>\` 后 \`git -C ${ROOT} checkout -b ${branch}\`(从含上一里程碑成果的默认分支开新支)。`,
    `4. 确认当前已在 \`${branch}\`:\`git -C ${ROOT} rev-parse --abbrev-ref HEAD\` == \`${branch}\`,否则失败。`,
    '',
    '## 结束',
    `- 输出一行 \`branch-setup: ${id} → ${branch}\`。`,
  ].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(终止态,非对话框)
// fix 后重新跑 verify(功能复验,verify 内部失败即 throw → halt),再进入下一轮 review,
// 使 fixPrompt 对子代理"上层会重新跑 verify + review"的承诺成真。
async function reviewWithFixLoop(id, phase, verifyResult) {
  const grp = phase === 'backend' ? 'Backend' : 'Frontend'
  for (let round = 1; round <= 5; round++) {
    const r = await agent(reviewPrompt(id, phase, round), {label:`review:${phase}:${id}:r${round}`, phase: grp, 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: grp})
    await agent(verifyPrompt(id, phase, `(第 ${round} 轮 fix 后复验)`), {label:`reverify:${phase}:${id}:r${round}`, phase: grp})
  }
  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 {
    // C1:进入模块前建/切功能分支(milestone 的 merge 源)。
    await agent(branchSetupPrompt(module), {label:`branch:${module.id}`, phase:'Milestone'})
    if (module.reqs.length) {                          // 后端段(frontend-phase 模块 reqs 为空 → 跳过)
      await featureLoop(module.reqs, 'backend')
      await testGate(module, 'backend')
      await agent(crossModulePrompt(module), {label:`xmod:${module.id}`, phase:'Milestone'})   // 替代被删 hook
    }
    if (module.feItems.length) {                        // 前端段(仅末尾 frontend-phase 聚合模块)
      await featureLoop(module.feItems, 'frontend')
      await testGate(module, 'frontend')
    }
    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 的逐模块摘要。
// 注:Workflow 运行时在异步包装上下文中执行脚本体,顶层 `return` 即为结果(与 `export const meta`
// 并存)。这是 Workflow 脚本的契约,**不是**独立 ESM 模块——因此 `node --check` 会报 Illegal
// return statement,但运行时正确(不要据 node --check 改成 export default,那会让结果丢失)。
return { results }