// workflows/coding.mjs // // 整个 ERP Coding(B 阶段)= 一个静默、全自动的 Workflow 脚本。 // 设计原则见仓库根 README.md「阶段 B」与「设计原则」节;featureLoop 顺序 for-await 的取舍 // 详见 featureLoop 函数处的注释。运行时禁用日期 / 随机数 builtin,所有"今天"由子代理解析。 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' }, ], // 注:'Behavior' phase 已删除——前端行为验收并入 per-FE reviewWithFixLoop 的 approve 子门, // 所有行为相关 agent()/adjudicate() 的 phase 入参统一用 'Frontend'(与 reviewWithFixLoop grp 一致)。 } 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'}} } } } } } // REVIEW_SCHEMA:reviewer 裁决;issues 结构化对象(summary/locator/severity)驱动 fix。 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:'object', additionalProperties:false, required:['summary','locator','severity'], properties:{ summary:{type:'string'}, locator:{type:'string'}, severity:{type:'string', enum:['blocker','high','medium','low']} } } } } } // STAGE_RESULT_SCHEMA:派生 stage 统一返回。 // status=halt 不再立即 fail-fast——先经 adjudicate() 仲裁(retry/continue/halt)才可能终止。 // decisions[]:stage 自主决策日志(缺值时不再停下,而是挑最有依据的默认/解读并登记于此),上层汇总进结果供人工事后审阅。 const STAGE_RESULT_SCHEMA = { type:'object', additionalProperties:false, required:['status'], properties:{ status:{type:'string', enum:['ok','halt']}, reason:{type:'string'}, artifactPath:{type:'string'}, summary:{type:'string'}, decisions:{ type:'array', items:{ type:'object', additionalProperties:false, required:['question','choice','rationale'], properties:{ question:{type:'string'}, choice:{type:'string'}, rationale:{type:'string'}, confidence:{type:'string', enum:['high','medium','low']} } } } } } // ADJUDICATE_SCHEMA:仲裁子代理在确定性 halt 之前的裁决—— // retry = 失败疑似一次性/可纠正,携 guidance 重跑上游; // continue = 缺陷不阻断正确性、可安全前进(降级为口头建议); // halt = 确属不可恢复(结构性缺失无旁证 / git 树需人工 / 会污染源码或伪造业务语义)。 const ADJUDICATE_SCHEMA = { type:'object', additionalProperties:false, required:['action','rationale'], properties:{ action:{type:'string', enum:['retry','continue','halt']}, guidance:{type:'string'}, rationale:{type:'string'} } } const GATE_SCHEMA = { type:'object', additionalProperties:false, required:['status'], properties:{ status:{type:'string',enum:['green','red']}, failures:{type:'array',items:{type:'string'}} } } // BEHAVIOR_GATE_SCHEMA:前端行为门(per-FE behavior 子门)返回。 // 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 覆盖率 / 环境错误分别结构化, // JS 据 source/kind 分流(交互硬边界转 must-fix,文字按 source 二分 allowContinue,envError 走 retry, // build-failed 确定性短路)。设计:见 docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 3/6/7。 const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, required:['status','routesPlanned','routesReached','controlsEnumerated'], properties:{ status:{type:'string', enum:['green','red']}, routesPlanned:{type:'integer'}, // 本 FE 关联路由数(覆盖率分母来源;per-FE 只数 feScope.routes,不数 router 全部) routesReached:{type:'integer'}, // 实际带鉴权加载成功的本 FE 路由数 controlsEnumerated:{type:'integer'}, // live 枚举到的本 FE 白名单控件数(空覆盖必须可见) authState:{type:'string'}, // 以何角色登录 / 覆盖角色 / 未覆盖角色集 // interactionFailures.locator:行为硬问题的源码定位(组件文件 [+ DOM 描述])。per-FE 行为门必须反查到 // 组件文件路径才能转 must-fix 喂 fix;反查不出(B 类)→ 不入 interactionFailures,归 coverageGap(不放行)。 // 交互层硬边界:no-observable-effect / js-error / console-error / missing-docs05-call / binding-garbage interactionFailures:{ type:'array', items:{ type:'object', additionalProperties:false, required:['page','control','kind','detail'], properties:{ page:{type:'string'}, control:{type:'string'}, kind:{type:'string', enum:['no-observable-effect','js-error','console-error','missing-docs05-call','binding-garbage']}, detail:{type:'string'}, locator:{type:'string'} } } }, // 组件文件路径 [+ DOM 选择器/绑定片段描述];有则可转 must-fix 喂 fix // 文字层软边界:source 决定 allowContinue(sentinel 客观 bug 不可 continue;i18n/literal/semantic 可 adjudicate continue) textIssues:{ type:'array', items:{ type:'object', additionalProperties:false, required:['page','region','expected','actual','source'], properties:{ page:{type:'string'}, region:{type:'string'}, expected:{type:'string'}, actual:{type:'string'}, source:{type:'string', enum:['sentinel','i18n','literal','semantic']}, locator:{type:'string'} } } }, // 覆盖率缺口:写证据 + recordDecisions,不单独 halt(空覆盖由 controlsEnumerated==0 兜底) // build-failed-sibling-unimpl:兄弟 FE 未实现导致本 FE 之外路由/组件编译缺件(预期中途态,不归本 FE 缺陷) // locator-not-resolvable:行为硬问题连组件文件都反查不出(B 类),计入未覆盖阻断 approve,不静默放行 coverageGaps:{ type:'array', items:{ type:'object', additionalProperties:false, required:['page','reason','detail'], properties:{ page:{type:'string'}, reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed','build-failed-sibling-unimpl','locator-not-resolvable']}, detail:{type:'string'} } } }, // 环境错误(与业务断言失败严格区分):none 表示无环境问题;build-failed 是确定性短路(既不 retry 也不 halt)。 // build-failed 时 rootCausePath 写报错根因文件路径——落在非本 FE 路径=兄弟未实现(短路放行),落在本 FE=真构建 bug。 envError:{ type:'object', additionalProperties:false, required:['kind'], properties:{ kind:{type:'string', enum:['port-conflict','stack-not-ready','seed-error','auth-failed','timeout','build-failed','none']}, detail:{type:'string'}, ports:{type:'string'}, pids:{type:'string'}, rootCausePath:{type:'string'} } }, // decisions[]:复用 STAGE_RESULT 形状,缺值自主决策日志 decisions:{ type:'array', items:{ type:'object', additionalProperties:false, required:['question','choice','rationale'], properties:{ question:{type:'string'}, choice:{type:'string'}, rationale:{type:'string'}, confidence:{type:'string', enum:['high','medium','low']} } } }, artifactPath:{type:'string'} } } // ── 微步骤 schemas(runBranchSetup / runMilestone / runCrossModule 用)───────── const WT_SCHEMA = { type:'object', additionalProperties:false, required:['clean'], properties:{ clean:{type:'boolean'}, dirty:{type:'array', items:{type:'string'}} } } const DEFAULT_BRANCH_SCHEMA = { type:'object', additionalProperties:false, required:['branch'], properties:{ branch:{type:'string'} } } const EXISTS_SCHEMA = { type:'object', additionalProperties:false, required:['exists'], properties:{ exists:{type:'boolean'} } } const FIELD_VALUE_SCHEMA = { type:'object', additionalProperties:false, required:['found','value'], properties:{ found:{type:'boolean'}, value:{type:'string'}, lineNumber:{type:'integer'} } } // CHECKBOX_STATE_SCHEMA:docs/08 功能行勾选态;state 必填——只 require found 时 cb.state 缺失会静默走 checked 分支。 const CHECKBOX_STATE_SCHEMA = { type:'object', additionalProperties:false, required:['found','state'], properties:{ found:{type:'boolean'}, state:{type:'string', enum:['checked','unchecked']}, lineNumber:{type:'integer'} } } const ALREADY_MERGED_SCHEMA = { type:'object', additionalProperties:false, required:['alreadyMerged'], properties:{ alreadyMerged:{type:'boolean'} } } const REPORT_PATH_SCHEMA = { type:'object', additionalProperties:false, required:['found'], properties:{ found:{type:'boolean'}, path:{type:'string'}, currentTagValue:{type:'string'} } } const CHANGED_FILES_SCHEMA = { type:'object', additionalProperties:false, required:['files'], properties:{ files:{type:'array', items:{type:'object', additionalProperties:false, required:['status','path'], properties:{ status:{type:'string'}, path:{type:'string'} } } } } } const CROSS_CLASSIFY_SCHEMA = { type:'object', additionalProperties:false, required:['crossModule'], properties:{ crossModule:{type:'array', items:{type:'object', additionalProperties:false, required:['file','targetModule','reason','impact'], properties:{ file:{type:'string'}, targetModule:{type:'string'}, reason:{type:'string'}, impact:{type:'string'} } } } } } // 所有 action 步骤(写文件 / git 改写仓库状态)统一返回 success/error;JS 据此抛错 halt。 const ACTION_RESULT_SCHEMA = { type:'object', additionalProperties:false, required:['success'], properties:{ success:{type:'boolean'}, error:{type:'string'}, detail:{type:'string'} } } const ROOT = args?.projectRoot || '.' // ROOT 必须是绝对路径——相对 '.' 会绑定到子代理隐式 cwd,无保证。 if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { throw new Error(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`) } // ── Feature-loop stage prompts(共享非交互契约见 featureStageContract)── function isFrontend(phase) { return phase === 'frontend' } // 从 spec/plan 等 artifactPath 文件名提取 `YYYY-MM-DD` 前缀,下游所有日期相关产物(plan / verify / // review report)一律复用同一日期,避免长跑或次日 resume 时各 sub-agent 各自解析"今天"导致路径分叉。 // 纯字符串运算,不触发非确定性内建(Workflow runtime 仅禁用 time/random builtin)。 function dateFromArtifactPath(artifactPath) { const fname = (artifactPath || '').split('/').pop() || '' const m = fname.match(/^(\d{4}-\d{2}-\d{2})-/) if (!m) throw new Error(`HALT invalid-artifactPath: 文件名缺少 YYYY-MM-DD 前缀 (${JSON.stringify(artifactPath)})`) // 进一步排查 pattern 合法但语义无效的日期(如 9999-99-99-foo.md): // 正则只判位数;下面校验年/月/日落在真实日历范围内,防止下游 plan/verify 以无意义日期级联生成产物。 const [, yStr, moStr, dStr] = m[0].match(/^(\d{4})-(\d{2})-(\d{2})-/) || [] const y = Number(yStr), mo = Number(moStr), d = Number(dStr) if (!(y >= 2024 && y <= 2099) || !(mo >= 1 && mo <= 12) || !(d >= 1 && d <= 31)) { throw new Error(`HALT invalid-date-prefix: 文件名日期前缀语义无效 (${JSON.stringify(artifactPath)}),年须在 2024-2099、月 1-12、日 1-31`) } return m[1] } // 所有子代理共享的"非交互静默"硬约束。 function featureStageContract(phase) { const fe = isFrontend(phase) return [ '## 硬约束(非交互子代理)', '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md` → `docs/05-API接口契约.md` → `prototype/`(前端布局/交互权威)→ `src/styles/tokens.css`(前端色值)→ `CLAUDE.md` → 现有代码。', '- 仍查不到时——**优先自主决策继续,不要停下**:基于现有代码约定 / 技术规范 / 同类实现,挑选**最有依据的解读或合理默认值**,把该决策写进产物显著位置,并在返回的 `decisions[]` 中逐条登记 `{question, choice, rationale, confidence}`(这是默认动作,项目目标是全自动静默、尽可能少 halt)。', '- 红线:**绝不**留 `【人工填写:】` / `TBD` / `TODO` 占位;**绝不**编造与现有约定/技术规范冲突的"事实";自主默认必须可被现有证据支撑且记入 `decisions[]`。', '- 仅当缺失的是**无法自洽决策的硬事实**(如某表结构 / 业务主键语义完全缺失且无任何旁证,任何默认都可能污染源码或伪造业务语义)时,才以 `status:halt` 结束并把阻塞点写清;上层会再经仲裁评估能否继续,halt 是最后手段而非首选。', '- 输出纪律:本次若做过任何自主默认 / 解读,成功返回(status:ok)**必须**带 `decisions[]`(逐条 `{question,choice,rationale,confidence}`,与上面登记要求一致);完全没有自主决策时才可省略——别照抄"输出"段里不含 decisions 的最简示例而漏登记。', '- 全部输出文档**使用中文**。', `- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe ? '实现文件必须落在 `frontend/` 下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。' : '产出范围限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约;**禁止**写 `frontend/` 路径下的实现(UI 推迟到前端阶段)。'}`, `- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `REQ-XXX-NNN`。'}`, ].join('\n') } // commitBlock:spec/plan/verify/review 共用的"写完 → add → commit → 失败 halt"四行块。 function commitBlock(addPath, msg, tail = '- commit 失败 → halt,把 stderr 摘要写进 reason。') { return [ '## commit', `- 写完后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`, ` 1. \`git -C ${ROOT} add ${addPath}\``, ` 2. \`git -C ${ROOT} commit -m "${msg}"\``, tail, ].join('\n') } // Router:读 docs/08 §二/§三 + git tag,重算进度,返回 ROUTER_SCHEMA。 function routerPrompt(root) { return [ '# Coding Router — 从账本重算进度', '', `项目根:\`${root}\``, '', '你是 Coding 阶段的路由子代理。**只读不写**(不改任何代码 / 文档),仅从状态账本重算"哪些模块还要跑",返回结构化结果。', '', '## 读取来源(账本 = docs/08 + git tag,里程碑和功能级完成都以 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 tag -l "milestone/*"`:列出已打的里程碑 tag。', '4. `git -C tag -l "req-done/*"`:列出已通过 review 并落地的功能级完成 tag。`docs/08` checkbox 只作可视化,不作为跳过功能的真值。', '', '## 完成判定(每个模块独立)', '- 后端模块 `done = true` 当且仅当:§二 该模块 `里程碑:` 字段 == `milestone/` **且** `git tag -l "milestone/"` 能查到该 tag。任一缺失 → `done = false`。', '- 前端 item(FE-NN)归属一个"逻辑前端模块"。前端阶段整体 `done` 当且仅当 §三 `整体里程碑:` == `milestone/frontend-phase` 且 `git tag -l "milestone/frontend-phase"` 存在。', '- 后端 REQ / 前端 FE 的功能级完成判定:仅当 `git tag -l "req-done/"` 能查到该 tag 才视为已 approve。不要因为存在 review markdown 或 docs/08 checkbox 已勾就跳过;若 tag 缺失,必须把该 id 放回待跑列表。', '', '## 输出(必须符合下发的 JSON schema)', '- `modules`: 数组。**先**按 `docs/02 § 二` 的模块顺序列出全部后端模块,**再在末尾追加唯一一个前端聚合模块**(仅当存在前端 FE 时)。每项:', ' - `id`: 模块标识(后端为英文蛇形 module id;前端聚合模块固定用 `frontend-phase`)。', ' - `done`: 该模块/前端阶段是否已完成(按上面的 milestone 判定)。', ' - `reqs`: **仅后端模块**填本模块**缺少 `req-done/` tag** 的后端 REQ 有序列表;模块已 done → 空数组。**前端聚合模块 `reqs` 恒为空数组**。', ' - `feItems`: **仅前端聚合模块**填——把**全部模块**缺少 `req-done/` tag 的前端 FE-NN 汇总为一个有序列表放进 `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}`, '', featureStageContract(phase), '', '## 目标', `静默派生 \`${id}\` 的实现规格(无 Q&A)。需求歧义本应在 Plan 期的结构化 per-REQ 表单锁定,前端布局/交互以 \`${ROOT}/prototype/\` 为权威;这里**只消费已锁定的事实**,不再澄清。`, '', '## 收集上下文', fe ? [ `- 关联 REQ 卡片:\`${ROOT}/docs/01-需求清单//.md\`(提取业务校验规则、acceptance、UI 描述)。`, `- 关联 prototype:Read \`${ROOT}/prototype/**/*.html\`(含 anchor 时聚焦相应区域),作为页面布局权威。`, `- API 契约:\`${ROOT}/docs/05-API接口契约.md\`,按本 FE 关联的 REQ 过滤出消费的端点。`, `- Design Tokens:\`${ROOT}/src/styles/tokens.css\`(色值 / 状态色单一来源;只用 var(--color-*),禁硬编码 hex)。**与 prototype 的色值冲突时以 tokens.css 为准**(prototype 管结构/布局/交互)。`, `- 前端组件库:\`${ROOT}/docs/04-技术规范.md § 零\` 的 \`frontend.ui_lib\`,决定组件选型。`, ].join('\n') : [ `- REQ 卡片:\`${ROOT}/docs/01-需求清单//${id}.md\`。**忽略 UI 描述**(控件类型 / 按钮位置 / 列表布局),但校验规则、业务规则仍要落到后端 DTO + service。`, `- 涉及的数据表定义:\`${ROOT}/docs/03-数据库设计文档.md\`(必要时实时查 mysql 只读)。`, `- API 契约:\`${ROOT}/docs/05-API接口契约.md\` 中本 REQ 相关端点。`, ].join('\n'), '', '## 写 spec', `- 落盘路径:\`docs/superpowers/specs/<当天日期 YYYY-MM-DD>-${id}.md\`(项目根相对)。当天日期由你在自身上下文解析;**spec 是本功能链上唯一会解析"今天"的 stage**,下游 plan/verify/review 的产物日期一律复用本 spec 文件名前缀(脚本会从 artifactPath 读取)。`, `- 若已经存在 \`docs/superpowers/specs/*-${id}.md\`(resume 场景),**复用最新一份的日期前缀**,不要起新日期前缀的文件;按需 Edit 已存在的 spec 而不是另起新文件。`, fe ? '- 规格至少含:关联 REQ + 关联原型;组件树(按页面 / 区域分块,推导自 prototype DOM);页面状态机(loading / empty / error / 正常 / 表单提交中 至少 5 态);消费的后端端点(对齐 docs/05);业务规则前端复刻清单(逐条:规则 / 触发时机 / 报错文案 / 来源 REQ);Design Tokens 引用清单(`var(--color-*)`)。' : '- 规格覆盖:goal / 输入输出 / 业务规则 / 约束 / schema / API 引用 / acceptance criteria。', fe ? [ '', '## 行为验收作用域结构化小节(per-FE 行为门唯一断言依据,**强制写到 spec 头部**)', '- 在 spec 文件头部(紧随标题/关联 REQ 之后)写一个**结构化小节**,标题逐字为 `## 行为验收作用域`,内含两条机器可读清单:', ' ```', ' ## 行为验收作用域', ' - 关联路由: [/orders, /orders/:id]', ' - 负责控件白名单: [data-testid=order-submit, /orders 页 "提交" 按钮, ...]', ' ```', `- **关联路由**:从 \`${ROOT}/frontend/\` router 配置(用 Grep 定位)取本 FE 真正负责渲染的路由 path(与 router 一致;带参动态路由保留 \`:id\` 占位)。**只列本 FE 路由**,不要列兄弟 FE / 共享路由。`, '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外 / 共享控件归 coverageGap,绝不算本 FE 缺陷。', '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。', ].join('\n') : '', '', commitBlock('', `docs(spec:${id}): 派生规格`), '', '## 自审(inline 修,无须等待)', `- 占位符扫描:\`TBD\` / \`TODO\` / \`【人工填写:】\`${fe ? ' / `controller` / `service` / `SQL` / `migration`(前端 spec 不应出现后端字样)' : ''} → 命中即修;修不掉的缺值按硬约束失败。`, '- 内部一致性 / 范围检查(单 plan 能消化吗)/ 歧义检查(任一 requirement 两种解读 → 挑一个写明)。', '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', '- 成功:`{ "status": "ok", "artifactPath": "docs/superpowers/specs/YYYY-MM-DD-' + id + '.md", "summary": "<1-2 句中文摘要>" }`', '- 失败:`{ "status": "halt", "reason": "<缺值阻塞点:缺哪个值 / 应在哪个 Plan 闸门锁定 / 为何无法继续>" }`', '- `artifactPath` 必须为项目根相对路径(无前导斜杠),文件名首段必须是 `YYYY-MM-DD`;schema 是 `additionalProperties:false`,不要返回额外字段。', ].join('\n') } // ---- stage 2:spec → 任务级 TDD 计划(原 feature-plan / fe-feature-plan)---- // specPath:调用方传入的 spec artifactPath(含 YYYY-MM-DD 前缀),plan 复用该日期。 function planPrompt(id, phase, specPath) { const fe = isFrontend(phase) return [ `# ${fe ? 'fe-feature-plan' : 'feature-plan'} — 任务级计划 ${id}`, '', featureStageContract(phase), '', '## 输入', `- 上游 spec:\`${specPath}\`(已由 spec stage 落盘;不存在则 halt)。**plan 文件名日期前缀必须与 spec 一致**:取 spec 文件名首段 \`YYYY-MM-DD\`,写到 plan 路径,不要重新解析"今天"。`, fe ? `- \`${ROOT}/docs/04-技术规范.md § 二 前端规范\`(§ 2.1 目录约定 = 落盘位置;状态管理 / 请求封装 / 错误处理);色值 / 样式见 \`${ROOT}/src/styles/tokens.css\`;测试栈见 § 零。用 Grep 在 \`${ROOT}/frontend/\` 定位现有文件。` : `- \`${ROOT}/docs/04-技术规范.md\`(编码规范 + § 1.2 分层结构 = 后端落盘)。用 Grep 在现有代码定位待修改文件。`, '', '## 计划写作原则', '- Plan 告诉 TDD 执行者**做什么**,不是**怎么写代码**(执行者是同模型、全上下文的 tdd stage)。', `- Plan 锁定**文件边界 + 测试意图 + ${fe ? 'props 契约' : 'API 形状'} + 完成判据**;代码由 TDD 红绿循环产出。`, '- **禁止 dump 整个文件内容**(build.gradle / 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/\` 开头;命中 \`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 + 自审', `- 落盘路径:\`docs/superpowers/plans/<同 spec 的 YYYY-MM-DD>-${id}.md\`,文件头含 Goal / Architecture / Tech Stack + checkbox 任务。`, '- 自审:占位符扫描(按硬约束清单);spec coverage(spec 每节至少指向一个 task,补 gap);类型一致性(签名 / 方法名 / 错误码 / props 一致)。', '', commitBlock('', `docs(plan:${id}): 任务级 TDD 计划`), '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', '- 成功:`{ "status": "ok", "artifactPath": "docs/superpowers/plans/YYYY-MM-DD-' + id + '.md", "summary": "<1-2 句中文摘要:任务数 / 涉及文件作用域>" }`', '- 失败:`{ "status": "halt", "reason": "<阻塞点描述>" }`', '- 日期前缀必须与 spec 同;schema 是 `additionalProperties:false`。', ].filter(Boolean).join('\n') } // ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)---- // planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。 function tddPrompt(id, phase, planPath) { const fe = isFrontend(phase) return [ `# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`, '', featureStageContract(phase), '', '## 输入', `- 计划文件:\`${planPath}\`(不存在则 halt)。`, `- 测试命令来源:\`${ROOT}/docs/04-技术规范.md § 零\`${fe ? ' 的 `frontend.unit_test_runner` / `frontend.e2e_runner` / `frontend.test_command` / `frontend.e2e_command`(缺失则默认 `pnpm test:ci` / `pnpm e2e:ci`)。' : ' 确认的后端测试命令(如 Gradle task / `./scripts/test.mjs`);缺失则默认 `node scripts/test.mjs`(与 test-gate 一致)。'}`, '', '## 流程', fe ? '' : '- **Schema 改动前置**(仅当 plan 声明需要):第一个任务写 migration 文件 `V__.sql`(`` = 现有 `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 复刻。' : '', fe ? `- **占位替换(保证中途可构建 + per-FE 行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT}/frontend/\` router 定位本 FE 路由 path 的 import 行;仍在 \`frontend/\` 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。` : '', '', '## 护栏', '- **绝不**在主会话直接跑测试(gradle / pnpm / playwright / scripts/test.mjs)——必须通过 Agent 子会话。', fe ? '- **绝不**写非 `frontend/` 路径的 `impl_file`;命中 `backend/` / `sql/` / `scripts/` → 硬停并打印 `不允许写非前端文件:`。' : '- **后端阶段路径硬护栏**:任意 `impl_file` 以 `frontend/` 开头 → 硬停并打印 `后端阶段不允许写前端代码:`,不再继续 TDD。', '- 每次 commit 含 REQ/FE 标签,不混合无关改同。', '', '## 同测试重试账本(硬上限 10 次 / 测试)', '- 你必须**显式**为每个出现过红色的测试维护一个内存账本 `attempts[::] = N`,每次该测试的"写失败实现 → 再跑"算 1 次。', '- 每次失败跑后,**在自身输出中显式打印一行** JSON:`{ "attempts": { "::": N } }`(便于 review/审计追溯)。', '- 任一测试的 `attempts >= 10` → **立刻 halt**:返回 `{status:"halt", reason:"tdd-test-stuck: :: 已尝试 10 次"}`,把"该测试名 / 最近一次 failing_assertion / 已尝试的修复摘要"写进 reason,**不要**无限重试。', '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', '- 全部任务通过:`{ "status": "ok", "summary": "<完成的任务数 / 引入的文件清单摘要>" }`(artifactPath 可省)。', '- 任意护栏 / 账本上限 / 缺值 → `{ "status": "halt", "reason": "<具体阻塞点>" }`。', ].filter(Boolean).join('\n') } // ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)---- // specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件, // 避免 reverify 覆盖前轮证据)。 function verifyPrompt(id, phase, implSummary, specPath, round = 0) { const fe = isFrontend(phase) const suffix = round === 0 ? 'verify' : `verify-r${round}` return [ `# ${fe ? 'fe-feature-verify' : 'feature-verify'} — 证据验证 ${id}${round > 0 ? `(第 ${round} 轮 fix 后复验)` : ''}`, '', featureStageContract(phase), '', '## 目标', `把 \`${id}\` 的功能测试**派发到 Agent 子会话**执行,按结构化结果渲染证据。**主会话从不直接跑测试,也不自由编写证据。**`, `- 上游 spec:\`${specPath}\`(日期前缀来源);本次产物文件名前缀必须 = spec 文件名首段 \`YYYY-MM-DD\`。`, implSummary ? `- 上游 TDD 摘要:${implSummary}` : '', '', '## 流程', 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`** → 渲染证据后 halt,不进入 review。', ].join('\n') : [ `- 测试目标:从 plan 或项目标准命令确定(Gradle task / 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`** → 渲染证据后 halt,不进入 review。', ].join('\n'), `- 证据落盘路径固定为 \`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}-${suffix}.md\`(与 review 报告同目录;round=0 → \`-verify.md\`;round>=1 → \`-verify-r.md\`,**每轮独立文件不覆盖前轮**)。同时把核心结构化结果摘要打印到会话便于上层 review stage 引用,**不要**自行另起目录或自由命名文件。`, '', commitBlock('<证据 artifactPath>', `docs(verify:${id}${round > 0 ? `:r${round}` : ''}): 证据验证`, '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'), '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', `- 全部通过:\`{ "status": "ok", "artifactPath": "docs/superpowers/reviews/YYYY-MM-DD-${id}-${suffix}.md", "summary": "" }\`。`, '- 任一红色 / 越界 / 缺值 → `{ "status": "halt", "reason": "<具体阻塞点>", "artifactPath": "<已写入的证据路径(如有)>" }`。', ].filter(Boolean).join('\n') } // ---- stage 5a:AI 自审 diff(原 feature-review / fe-feature-review)——委托统一 reviewer agent ---- // lastVerifySummary:round>1 时传入上轮 fix 后复验摘要,让 reviewer 看到"上轮 must-fix 真的修了什么"。 // specPath:spec artifactPath(日期前缀来源 + reviewer 上下文输入)。 function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { const fe = isFrontend(phase) return [ `# ${fe ? 'fe-feature-review' : 'feature-review'} — AI 自审 ${id}(第 ${round} 轮)`, '', featureStageContract(phase), '', '## 目标', `对 \`${id}\` 本轮引入的代码 diff 做 AI 自审,给出 \`approve\` 或 \`request-changes\` 裁决。`, '', '## 输入给 reviewer', `- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`, fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '', `- **phase = ${fe ? 'frontend → 附加前端 7 维 checklist。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`, fe ? `- **行为验收作用域小节校验(per-FE 行为门前置真值,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——行为门只能据此确定本 FE 路由作用域。` : '', round > 1 && lastVerifySummary ? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。` : '', '', '## 输出(必须符合下发的 REVIEW JSON schema)', `- \`verdict\`: \`approve\` | \`request-changes\`;\`round\`: 整数(本轮 = ${round})。`, `- \`issues\`: 结构化 must-fix 数组。\`approve\` 时必须为空数组 \`[]\`;\`request-changes\` 时**必须非空**,每项形如 \`{ "summary": "<一句问题>", "locator": "<文件路径或 file:line>", "severity": "blocker|high|medium|low" }\`。`, `- \`locator\` **必须含可定位文件路径**(项目根相对,例如 \`backend/src/main/java/.../FooController.java\` 或 \`frontend/src/views/Bar.vue:42\`);没有具体文件无法定位 → 该项不是 must-fix(降级为口头建议,不要塞进 issues)。`, `- 渲染审阅报告写入 \`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md\`(\`verdict\` 字段与返回值一致)。报告内可写更丰富的建议 / 风险 / 亮点;issues 数组只放硬性 must-fix。`, `- **不要**在本步骤里编辑 docs/08 的 \`- [ ]\` checkbox——该 side effect 由上层 Workflow 的 micro step 在 approve 后另行落盘(你只负责裁决)。`, '- 不要返回额外字段(schema 是 `additionalProperties:false`)。', '', commitBlock(`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md`, `docs(review:${id}:r${round}): `, '- commit 失败时仍按 schema 返回 verdict / issues;commit 错误信息打印到日志即可(不要在 schema 中夹带额外字段)。'), ].filter(Boolean).join('\n') } // ---- stage 5b:按 review must-fix 修复并重新 commit(review 循环的 fix 步)---- // issues:结构化对象数组 {summary, locator, severity}(见 REVIEW_SCHEMA)。 function fixPrompt(id, phase, issues) { const fe = isFrontend(phase) const list = Array.isArray(issues) && issues.length ? issues.map((x, i) => ` ${i + 1}. [${x.severity}] ${x.summary} — locator: \`${x.locator}\``).join('\n') : ' (上一轮 review 未提供 must-fix 清单——不应出现,调用方会先 halt)' return [ `# ${fe ? 'fe-feature' : 'feature'} fix — 修复 review must-fix ${id}`, '', featureStageContract(phase), '', '## 待修复 must-fix(已结构化)', list, '', '## 流程', '- 逐项编辑 locator 指向的代码文件(遵守阶段路径作用域护栏)。', `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOT} cat-file -e HEAD:\`(locator 形如 \`path:line\` 时取 \`path\`)。文件不存在 → halt,把 locator 写进 reason,不要"修一个不存在的文件"。`, `- 修复后 commit:\`fix(): 修复 review must-fix ${fe ? `FE: ${id}` : `REQ: ${id}`}\`(不混合无关改动)。`, '- 修复完成后本步骤即结束;上层 Workflow 会重新跑 verify + review(下一轮)。', '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', `- 全部修完:\`{ "status": "ok", "summary": "<已修复 ${Array.isArray(issues) ? issues.length : 0} 项的 1-2 句摘要>" }\`。`, '- 任意阻塞(locator 文件不存在 / 越界 / 缺值)→ `{ "status": "halt", "reason": "<具体阻塞点 + locator>" }`。', ].filter(Boolean).join('\n') } // ---- 测试闸(原 test-gate)---- // attempt:1 = 首次跑;2 = 上轮 red 后的 flake 重试。每次 attempt 写到独立证据文件,避免 retry // 把首次 red 证据覆盖掉(report § ⑤ 失去 flake 信号)。 function gatePrompt(module, phase, attempt = 1) { const fe = isFrontend(phase) const id = module?.id ?? '' const phaseId = fe ? 'frontend-phase' : id return [ `# test-gate — ${fe ? '前端阶段' : `模块 ${id}`} 硬测试闸(phase=${phase}, attempt=${attempt})`, '', featureStageContract(phase), '', '## 目标', `打里程碑 tag 前的唯一硬测试门。**派发 Agent 子会话**跑测试,绿则通过,红则失败。**绝不**在主会话直接跑测试,红色时**绝不**跳过。`, attempt > 1 ? `- 本次 = 第 ${attempt} 次(上一次 red,本轮用于辨识 flaky);证据**写到独立文件**不要覆盖前一次。` : '', '', '## 命令', 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/${phaseId}-test-gate-r${attempt}.md\` 并 commit 到当前分支(每个 attempt 独立文件,retry 不覆盖前一次 red 证据)。`, `- 文件头注明 \`attempt: ${attempt}\` + 命令 + 时间窗口(如可从子会话拿到),便于 report § ⑤ 识别 flake。`, '', '## 输出(必须符合下发的 GATE JSON schema)', '- `status`: `green`(`exit_code = 0` 且 `failed = 0`)| `red`;`failures`: 失败用例摘要(green 时可省略 / 空数组)。', '- 不要返回额外字段。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。', ].filter(Boolean).join('\n') } // ---- 前端行为验收(per-FE behavior 子门)---- // 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。 // 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发, // 起本 FE 全栈 + sentinel 种子,枚举本 FE 路由控件/文字,硬问题转可 fix must-fix→重验,行为 green 才放行 approve。 // 门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend') // (其路径护栏命中 backend/sql/scripts 即越界硬停,与门必须运行 setup-test-db / 起后端 / 生成 SQL 种子自相矛盾)。 // behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符; // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright, // 唯一**可写** = .tmp/behavior-gate//r/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 function behaviorGateContract() { return [ '## 硬约束(非交互行为验收子代理)', '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate//r/\`(spec/种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/reviews/--behavior-r-a.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/reviews/assets/...\`)。`, `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, '- **per-FE 中途态豁免(关键)**:本门在 **per-FE 模式**下运行——`frontend/` 中**本 FE 之外**的路由/组件可能尚未实现,属预期中途态。遇到指向未建路由的链接 / 404 / 编译缺件(兄弟 FE 或骨架占位未覆盖),一律记 `coverageGaps[reason="build-failed-sibling-unimpl"]` 或 `envError.kind="build-failed"`(按根因路径归属,见 step0/step2),**绝不**归为本 FE 的 `interactionFailures`。**本 FE 路由清单(feScope.routes)是唯一断言作用域**;白名单外 / 共享控件归 coverageGap,不算本 FE 缺陷。', '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。', '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / FE id / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。', ].join('\n') } // behaviorGatePrompt:per-FE 行为验收子代理的完整流水线提示(step0-6 + schema)。 // id:本 FE id(如 FE-07);specPath:本 FE spec(含 ## 行为验收作用域 小节,feScope 来源 + 日期前缀); // behaviorRound:approve 子门内的行为 fix 轮(1..BEHAVIOR_FE_MAX);attempt:本轮内环境 race 重试序号(1..)。 // 每 (FE × behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。 function behaviorGatePrompt(id, specPath, behaviorRound, attempt) { const safeId = id ?? 'FE' const tmpDir = `${ROOT}/.tmp/behavior-gate/${safeId}/r${behaviorRound}` const date = (() => { try { return dateFromArtifactPath(specPath) } catch { return '' } })() const evidence = `docs/superpowers/reviews/${date}-${safeId}-behavior-r${behaviorRound}-a${attempt}.md` return [ `# behavior — 前端 per-FE 行为验收(headless,FE=${safeId}, behaviorRound=${behaviorRound}, attempt=${attempt})`, '', behaviorGateContract(), '', '## 目标', `用真实全栈运行证明本 FE \`${safeId}\` 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。`, `单个子会话内**收敛完成**:冷起栈 → 逐**本 FE 路由**枚举 + 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。`, `- 本 FE 行为验收作用域唯一真值 = spec \`${specPath}\` 头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先 Read 该 spec 取出 feScope;缺该小节 → \`envError.kind="stack-not-ready"\` 并在 detail 写明(不应出现:reviewer 已校验它存在)。`, behaviorRound > 1 || attempt > 1 ? `- 本次 = behaviorRound ${behaviorRound} / attempt ${attempt}(上一次 red / envError / fix 后重验);证据**写到独立文件 r${behaviorRound}-a${attempt}** 不要覆盖前一次。` : '', '', '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 ? `本次是本 FE 首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/${safeId}/\` 目录(清掉本 FE 历史残留 runner/种子/spec),再新建本轮子目录 \`${tmpDir}/\`。` : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本 FE 起的全部子进程**并透传结构化结果。**绝不**让前台 spring-boot:run / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`, `- \`${ROOT}/.tmp/behavior-gate/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, '', '## step0 探测 + build 归因(确定性短路前置,依赖 build-failed kind)', `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/config-vars.yaml\`。`, '- runner 自负冷起后端 + 前端 headless(无既有 webServer 可复用)。**起 dev / source-map 模式**(注入定位辅助:`data-testid` 约定 / Vue `__file`),便于把 page+selector 映射回组件文件。', '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——', ` - 落在**非本 FE 的 \`frontend/\` 路径**(兄弟 FE 组件缺失 / 骨架占位未覆盖 / 指向未建路由)→ \`envError.kind="build-failed"\` + \`rootCausePath=<非本FE路径>\`(**预期中途态**,不是本 FE bug)。`, ' - 落在**本 FE 路径**(feScope 关联组件)→ 才是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]`(带 locator=组件文件)。', ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。', '', '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)', '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。', '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**本 FE 每路由的预期控件与文字来源**;每路由标注所需登录角色。', '- 带参动态路由用**种子已知主键**实例化;无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。', '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。', '', '## step2 起栈四段严格时序(schema 由 Flyway 在后端启动时才建)', `1) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`, '2) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', '3) **此时才跑种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** 种子(先父后子)。失败 → `envError.kind="seed-error"` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断),**不**混进交互 RED。', ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——字符串列逐字段唯一编码(如 `CUST_NAME_S001`,抓绑错字段)+ 行序号保 UNIQUE;数值列用高位魔数;enum 列从 docs/03 值域取并标注。插入前扫 Flyway / config-vars 既有初始数据(admin_init 等)键,sentinel 主键偏移到不冲突区;断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。', '4) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。', '', '## step2.5 鉴权 bootstrap(确定性前置)', '- 用 config-vars `admin_init` 或种子已知凭据,经 `docs/05` 登录端点**真实登录**拿 JWT,注入 Playwright `storageState`;`authState` 记角色覆盖(覆盖 / 未覆盖角色集)。', '- 登录失败 = `envError.kind="auth-failed"`(环境 race,走 retry),**绝不**当成死控件。', '', '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;只驱动本 FE feScope)', '- **只枚举/驱动 feScope.routes + feScope.controlWhitelist**(本 FE 白名单控件)。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 本 FE 推导清单,分子 = live 枚举。', '- 分母有但首帧无的控件:runner 尝试**驱动到出现态**(种子保列表非空触发行级操作 / 进多步流程下屏 / 展开 dropdown / 切 tab 后二次枚举);仍不可达 → `coverageGaps[reason="deep-control-not-driven"]`,不静默判 green。到不了的路由 → `coverageGaps[reason="unreachable-auth"|"unreachable-no-route"]`,与「到达了但控件死」严格区分。', '- **白名单外 / 共享控件**:若属其它未 approve FE 或共享区 → 归 `coverageGaps[reason="deep-control-not-driven"]`,**绝不**归本 FE 的 `interactionFailures`。', '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。', '- `routesReached` / `controlsEnumerated` 据实填(本 FE 子集空覆盖必须可见)。', '', '## step4 推导期望', '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。', '', '## step5 断言(两层 + 可观测效果白名单 + 硬问题带源码 locator)', '- **交互层可观测效果白名单**:URL 变化 / docs05 网络调用(`page.on("request")` 比对端点)/ DOM 变更 / 校验信息 / 弹层 / toast / 原生对话框(枚举前注册 `page.on("dialog")`,confirm/alert/beforeunload 计合法效果,防 confirm 阻塞误判 missing-docs05-call)/ 下载(`page.on("download")`)/ 新标签(`page.on("popup")` / `target=_blank`)。', ' - 无任何效果 → `interactionFailures[kind="no-observable-effect"]`;JS 异常 → `js-error`;`console.error` → `console-error`;应发未发网络调用 → `missing-docs05-call`。断言用 auto-waiting / `expect.poll`,**不用**固定 sleep。', '- **文字层**:动态文字格对比该 region 字段的唯一 sentinel(抓绑错字段)。', '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`;双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。', '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 approve)。', '- **行为硬问题必须带源码 locator(转 must-fix 喂 fix 的前置)**:', ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。', ' - **B 类(连组件文件都反查不出)**:**不静默降级放行**——归 `coverageGaps[reason="locator-not-resolvable"]`(计入未覆盖,使本轮不能判 green),或归 `envError.kind="stack-not-ready"` 走 retry。绝不把无 locator 的硬问题塞进 `interactionFailures` 不带 locator(上层会因无 locator 走 adjudicate(allowContinue:false),绝不放行)。', '', `## step6 证据落盘 + commit(运行时行为,沿用证据 commit 习惯)`, `- 写 \`${evidence}\`:本 FE feScope / 推导期望 / 逐控件判定 / routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ coverageGaps / 截图。`, `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/reviews/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`, commitBlock(`${evidence} docs/superpowers/reviews/assets`, `docs(behavior:${safeId}:r${behaviorRound}-a${attempt}): per-FE 行为验收证据`), '', '## 输出(必须符合下发的 BEHAVIOR_GATE JSON schema)', '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + 无阻断性 envError + 本 FE 覆盖非空)| `red`。', '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数本 FE feScope**;空覆盖必须可见)。', '- `interactionFailures` / `textIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`(含 `source="sentinel"` 的 textIssue)。', '- `envError`: 无环境问题填 `{ "kind": "none" }`;有则填对应 kind + detail + ports + pids;`build-failed` 时填 `rootCausePath`。', '- 做过任何自主默认 → `decisions[]` 逐条登记。`artifactPath` = 证据报告项目根相对路径。', '- 不要返回额外字段(schema 是 `additionalProperties:false`)。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。', ].filter(Boolean).join('\n') } // ---- 前端骨架占位 stage(runFrontendSkeleton 用)---- // 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A,blocker)。 // 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(未实现 FE 路由指向 FeStub 占位) // + 不指悬空 path 的共享导航——保证「前端只建了一部分」的任意时刻 app 仍可构建可起、每个 FE 路由可达。 // 由此 per-FE 行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点、build-failed 退化为罕见兜底。 // feItems:本前端阶段的全部 FE-NN(来自 Router 的 frontend-phase 聚合模块),即 router 全量路由表的清单。 function frontendSkeletonPrompt(feItems) { const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(Router 未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' return [ '# fe-skeleton — 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位)', '', featureStageContract('frontend'), '', '## 目标', '在逐 FE 实现开始**之前**,一次性建出前端「可构建可起」的骨架:App 外壳 + router **全量** lazy 路由表(每个 FE 路由都声明,未实现的指向占位组件 `FeStub`)+ 不指悬空 path 的共享导航。', '保证后续「只建了一部分 FE」的任意时刻 `vite build` / dev server 都能起、每个 FE 路由都可达(加载到占位);逐 FE 实现时再把对应路由的 import 从 `FeStub` 换成真组件。', '', `## 本前端阶段 FE 清单(router 全量路由表必须覆盖的全部 FE)`, `- ${list}`, '', '## 收集上下文(确定技术栈 + 目录约定 + 路由)', `- \`${ROOT}/docs/04-技术规范.md § 零\`(\`frontend.ui_lib\` / framework / 构建工具)+ \`§ 二 前端规范\`(§ 2.1 目录约定 = 落盘位置 / 路由库 / 入口文件名)。`, `- \`${ROOT}/docs/08-模块任务管理.md § 三\`(前端阶段元数据 + \`功能:\` 下全部 \`FE-NN\` 行;与上面清单核对,以本提示给出的清单为准)。`, `- \`${ROOT}/docs/01-需求清单/\` 各 FE 关联 REQ + \`${ROOT}/prototype/\`(页面/路由结构权威)+ \`${ROOT}/docs/05-API接口契约.md\`,据此推导每个 FE-NN 对应的**路由 path**(带参动态路由保留 \`:id\` 占位)。`, `- 用 Grep 在 \`${ROOT}/frontend/\` 探测现有 App 外壳 / 入口 / router 是否已存在(幂等:已存在则按需补齐,不重复创建/不覆盖已实现的真组件)。`, '', '## 产出(全部落在 `frontend/` 路径内——遵守前端阶段路径作用域护栏)', '1. **App 外壳 + 入口**:`frontend/src/App.*` 与入口 `frontend/src/main.*`(按 framework / docs/04 约定的扩展名;不存在才创建)。挂载共享布局 + ``(或等价 outlet)。', '2. **router 全量路由表**(按 docs/04 § 2.1 约定的路由文件位置,如 `frontend/src/router/index.*`):', ' - **每个** FE-NN 对应路由都声明,**全部用 lazy import**(`component: () => import(...)` 或 framework 等价的动态 import;**绝不** eager `import X from ...` 顶部静态引入,否则未建组件会让整表编译失败)。', ' - **未实现的 FE 路由全部指向占位组件 `FeStub`**:`component: () => import("../views/_stub/FeStub.vue")`(或 framework 等价)。逐 FE 实现后由 tdd stage 把对应路由 import 换成真组件。', ' - 路由 path 取自上面推导的 FE→path 映射;带参路由用 `:id` 等占位。', '3. **占位组件 `FeStub`**:`frontend/src/views/_stub/FeStub.vue`(framework 非 Vue 时落对应等价文件,如 `FeStub.tsx`),最小渲染一个带 `data-fe-stub` 属性的元素(如 `
占位
`;行为门据 `data-fe-stub` 识别占位态)。**不实现任何业务逻辑**。', '4. **共享布局/导航**:导航链接**全部指向已在 router 声明的路由 path**(不指向任何不存在的 path),保证任意时刻无悬空链接。', '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', '', '## 自检(可构建)', '- 推断本项目前端 build / typecheck 命令(docs/04 § 零 / `frontend/package.json` scripts)。若可在子会话内安全跑(不挂死),**派 Agent 子会话**跑一次 build / dev-server 就绪探测确认骨架可构建可起;不可行则至少静态核对「全部 FE 路由已声明 + 全 lazy + 导航无悬空 path + FeStub 存在」。', '- 占位符扫描:`TBD` / `TODO` / `【人工填写:】` → 命中即修。', '', commitBlock('frontend/', 'feat(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位', '- commit 失败 → halt,把 stderr 摘要写进 reason。'), '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', '- 成功:`{ "status": "ok", "summary": "<已声明的 FE 路由数 / 入口与 router 文件路径摘要>" }`(artifactPath 可省)。', '- 任一护栏 / 缺值(如无法推导某 FE 的路由 path 且无任何旁证)→ `{ "status": "halt", "reason": "<具体阻塞点>" }`。', '- 做过自主默认 → `decisions[]` 逐条登记;schema 是 `additionalProperties:false`,不要返回额外字段。', ].filter(Boolean).join('\n') } // fe-skeleton 幂等判定:检测 router 是否已声明本阶段全部 FE 路由(全量 + 全 lazy)。 // router/state 是骨架真实完成态;fe-skeleton-done tag 只作补记,避免陈旧 tag 跳过缺失骨架。 function frontendSkeletonStatePromptM(feItems) { const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(无)' return [ '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)', microStepContract(), '', `用 Grep / Read 检查 \`${ROOT}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),且占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在。`, '- 全部满足(骨架已建齐)→ `{ "exists": true }`', '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub)→ `{ "exists": false }`', '## 输出(EXISTS_SCHEMA)', ].join('\n') } // ---- 微步骤 prompt builders(runBranchSetup / runMilestone / runCrossModule 用)---- // 每个 prompt 单职责、短文本;返回严格 schema;执行(action)步统一返回 ACTION_RESULT_SCHEMA。 function microStepContract() { return [ '## 硬约束(非交互子代理)', '- 你是 Workflow 派生的**非交互子代理**,绝不弹问。', '- 全部输出**使用中文**。', `- 项目根 = \`${ROOT}\`。所有 git 命令必须用 \`git -C ${ROOT} ...\`;Read/Edit/Write 的路径都以 \`${ROOT}\` 为根。`, '- 严格按下方"输出"段返回 schema 字段;**不要**在 schema 外追加自由叙述。', ].join('\n') } // ============================================================================ // 仲裁 / 自主决策基础设施(halt 收敛) // 设计:原先每个"缺值 / 结构违约 / 重试耗尽"点都直接 throw HALT 让整阶段 fail-fast。 // 现在改为先经 adjudicate() 仲裁——retry(带 guidance 重跑)/ continue(降级前进)/ halt(确属不可恢复)。 // stage 自身也被要求优先自主决策继续(见 featureStageContract),其默认/解读记入 decisions[] 汇总。 // 仅 git 树冲突 / 配置错 / id 形状错(assertSafeId)保持硬 halt——这些不可由 LLM 代决。 // ============================================================================ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限;超出则确定性 halt(防无限循环) // per-FE 行为子门预算(二维,钉死防证据覆盖;设计 §6.4): // - BEHAVIOR_FE_MAX = approve 子门内的行为 fix 轮硬上限(每 FE);超限 throw HALT。**不**复用 review 的 10 轮、 // **不**让 REVIEW_HARD_ROUNDS × 行为重试隐式相乘——典型一次过(1 轮),最坏 3 轮。 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 const BEHAVIOR_FE_MAX = 3 const BEHAVIOR_ATTEMPT_MAX = 2 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 const autonomousDecisions = [] function recordDecisions(site, decisions) { if (!Array.isArray(decisions)) return for (const d of decisions) { if (!d) continue autonomousDecisions.push({ site, question:d.question, choice:d.choice, rationale:d.rationale, confidence:d.confidence }) log(`decision ${site}: ${d.question || '?'} → ${d.choice || '?'} (${d.confidence || '?'})`) } } function adjudicatePromptM(site, context) { const ctx = typeof context === 'string' ? context : JSON.stringify(context, null, 2) return [ `# 仲裁:\`${site}\` 触发潜在 halt,请裁决 retry / continue / halt`, microStepContract(), '', '## 你的角色', '你是 ERP 编码 Workflow 的**仲裁子代理**。某上游步骤触发了一个原本会让整阶段 fail-fast 停下的护栏。', '项目目标是全自动静默、尽可能少停。请在**不损坏 git 工作树、不伪造业务事实、不污染源码**的前提下,尽量让流程继续。', '', '## 触发上下文', '```', ctx, '```', '', '## 裁决口径', '- `retry`:失败疑似一次性 / 可纠正(子代理输出不符 schema 约定、git 命令瞬时失败、上游漏给某字段)。**必须**在 `guidance` 写清"重跑时要修正什么",下游会把它原样注入重跑提示。', '- `continue`:缺陷不阻断正确性、可安全前进(reviewer 的非必须建议 / 可降级为口头建议的 issue / 纯可视化副作用缺失 / 已可由后续 verify / test-gate 兜底的疑虑)。在 `rationale` 说明为何安全。', '- `halt`:确属不可恢复——结构性缺失且无任何旁证、git 树冲突需人工、继续会污染源码 / 伪造业务语义。在 `rationale` 写清人工需要做什么。', '- 若上下文含 `"allowContinue": false`,**不得**选 continue(如红色测试不可跳过),只在 retry / halt 间选。', '## 输出(ADJUDICATE_SCHEMA)', '- `{ "action": "retry|continue|halt", "guidance": "", "rationale": "<裁决理由>" }`', ].join('\n') } async function adjudicate(site, context, grp, round) { const verdict = await agent(adjudicatePromptM(site, context), {label:`adjudicate:${site}:r${round}`, phase: grp, schema: ADJUDICATE_SCHEMA}) log(`adjudicate ${site} r${round}: ${verdict.action}${verdict.rationale ? ' — ' + verdict.rationale : ''}`) return verdict } // runStage:跑一个 STAGE_RESULT 派生 stage(spec/plan/tdd/verify/fix/report)。 // ① 登记 decisions[];② status:halt 或 validate() 报结构问题 → 经 adjudicate 决定 retry/continue/halt。 // makePrompt(guidanceTail) 接收仲裁追加指令串(adjGuidance 已格式化);validate(res) 返回 null=通过 / 问题串。 // allowContinue=true 只用于后续 reviewer / behavior 会再次兜底的软 stage;流程前提默认不可 continue。 // allowContinue=false:本 stage 的 halt 代表**硬正确性边界**(功能测试红色 verify/reverify、路径越界/卡死 tdd、 // test-gate 红 report),仲裁只许 retry/halt,**绝不 continue 放行**残缺/越界状态去 approve / milestone。 async function runStage(makePrompt, { site, grp, label, validate, allowContinue = false }) { let guidance = '' for (let round = 1; round <= ADJUDICATE_MAX; round++) { const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA}) recordDecisions(site, res.decisions) let problem = null if (res.status === 'halt') problem = `stage 返回 status:halt;reason: ${res.reason || '(空)'}` else if (validate) { try { problem = validate(res) } catch (e) { problem = String(e?.message || e) } } if (!problem) return res const verdict = await adjudicate(site, { problem, stageResult: res, allowContinue }, grp, round) if (verdict.action === 'continue' && allowContinue) return res if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || problem}`) guidance = verdict.guidance || '' // retry:带 guidance 重跑 } throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) } // runAction:跑一个 ACTION_RESULT 微步骤(git / 文件写),失败时经 adjudicate 决定 retry/continue/halt。 // allowContinue=true 时 continue 视为"接受失败并前进"(仅用于纯可视化等可安全跳过的副作用)。 async function runAction(makePrompt, { site, grp, label, allowContinue = false }) { let guidance = '' for (let round = 1; round <= ADJUDICATE_MAX; round++) { const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: ACTION_RESULT_SCHEMA}) if (r.success) return r const verdict = await adjudicate(site, { problem:`action 失败:${r.error || ''}${r.detail ? '\n' + r.detail : ''}`, allowContinue }, grp, round) if (verdict.action === 'continue' && allowContinue) return r if (verdict.action === 'halt' || verdict.action === 'continue') throw new Error(`HALT ${site}: ${verdict.rationale || r.error || ''}`) guidance = verdict.guidance || '' // retry:带 guidance 重跑 } throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) } // recoverDirtyWorktreePromptM:branchSetup / milestone 前置的"工作树干净"被打破时的自主恢复(class D 部分)。 // 子代理检查脏文件——全是本阶段合法产物 → 自动 commit 后继续;含越界/不明改动 → 不提交、返回失败让上层 halt。 // **分支护栏(branch)**:自动 commit 只允许发生在目标功能分支上。若当前 HEAD 不在 branch(如里程碑后 HEAD // 停在默认分支、resume 时残留落在默认分支),绝不 add -A/commit——否则会把绕过 review/test-gate 的改动 // 直接提交进默认分支,且该改动对模块 `...HEAD` 三点 diff 不可见(污染 cross-module / 完成报告)。 function recoverDirtyWorktreePromptM(dirty, branch, scopeHint) { const list = (dirty || []).map(p => `- ${p}`).join('\n') || '(调用方未给清单,请自行 `git status --porcelain` 复核)' return [ '# 工作树不干净——判定能否自主提交后继续', microStepContract(), '', '## 背景', `分支切换 / 里程碑前要求工作树干净,当前存在未提交改动。${scopeHint || ''}`, '在**不丢失工作、不混入越界改动、不提交到错误分支**的前提下尽量让流程继续。', '', '## 脏文件清单', list, '', '## 流程', `0. **分支护栏(必须先做)**:跑 \`git -C ${ROOT} rev-parse --abbrev-ref HEAD\`。若当前分支 **!= \`${branch}\`**(目标功能分支),**绝不提交**——直接返回 \`{ "success": false, "error": "dirty-on-wrong-branch", "detail": "HEAD=<当前分支>, expected ${branch};拒绝把残留提交到非功能分支,留给人工" }\`。只有当前已在 \`${branch}\` 才继续 step 1。`, `1. 逐一检查改动(\`git -C ${ROOT} status --porcelain\`,必要时 \`git -C ${ROOT} diff\`)。`, `2. **全部都是本阶段合法产物**(spec/plan/verify/review/report/源码/migration,且落在当前阶段路径作用域内)→ \`git -C ${ROOT} add -A\` 后 \`git -C ${ROOT} commit -m "chore: 自动提交上一步残留改动"\`,返回 \`{ "success": true, "detail": "committed-in-scope" }\`。`, '3. 含**越界 / 不明 / 与本阶段无关**的改动(手工临时文件、其它模块代码、构建产物等)→ **不要提交**,返回 `{ "success": false, "error": "dirty-out-of-scope", "detail": "<可疑文件 + 原因>" }`。', '## 输出(ACTION_RESULT_SCHEMA)', ].join('\n') } // ── 微步骤:可重用 read(多个 orchestrator 共用)── function detectDefaultBranchPromptM() { return [ '# 检测本地默认分支', microStepContract(), '', `用 \`git -C ${ROOT} rev-parse --verify refs/heads/\` 依次试 \`main\` / \`master\`,取第一个 exit=0 的为默认分支。`, '## 输出(DEFAULT_BRANCH_SCHEMA)', '- 两者其一存在:`{ "branch": "main" }` 或 `{ "branch": "master" }`', '- 都不存在:本步骤失败(返回 schema 失败即可,调用方会 halt)。', ].join('\n') } function worktreeCleanPromptM() { return [ '# 检查工作树是否干净', microStepContract(), '', `跑 \`git -C ${ROOT} status --porcelain\`,按行解析 dirty 文件路径(第 4 字符起)。`, '## 输出(WT_SCHEMA)', '- 干净:`{ "clean": true }`', '- 不干净:`{ "clean": false, "dirty": ["", ...] }`', ].join('\n') } function checkBranchExistsPromptM(branch) { return [ `# 本地分支 \`${branch}\` 是否存在`, microStepContract(), '', `跑 \`git -C ${ROOT} rev-parse --verify refs/heads/${branch}\`(用 2>/dev/null 抑制 stderr)。`, '## 输出(EXISTS_SCHEMA)', '- exit=0 → `{ "exists": true }`;非 0 → `{ "exists": false }`', ].join('\n') } function currentBranchPromptM() { return [ '# 当前所在分支', microStepContract(), '', `跑 \`git -C ${ROOT} rev-parse --abbrev-ref HEAD\`。`, '## 输出(BRANCH_NAME_SCHEMA)', '- `{ "branch": "" }`', ].join('\n') } // ── 微步骤:分支生命周期 action ── function checkoutExistingBranchPromptM(branch) { return [ `# 切到已存在的本地分支 \`${branch}\``, microStepContract(), '', `跑 \`git -C ${ROOT} checkout ${branch}\`。`, '## 输出(ACTION_RESULT_SCHEMA)', '- 成功:`{ "success": true }`', '- 失败:`{ "success": false, "error": "" }`', ].join('\n') } function createBranchFromPromptM(fromBranch, newBranch) { return [ `# 从 \`${fromBranch}\` 新建并切到 \`${newBranch}\``, microStepContract(), '', `按序跑:\`git -C ${ROOT} checkout ${fromBranch}\`,然后 \`git -C ${ROOT} checkout -b ${newBranch}\`。`, '## 输出(ACTION_RESULT_SCHEMA)', '- 全成功:`{ "success": true }`;任一失败:`{ "success": false, "error": "" }`', ].join('\n') } // ── 微步骤:REQ/FE 完成态 git tag(featureLoop dedup 的唯一 ground truth)── // req-done/ 是功能级 git tag,approve 时打一次;featureLoop 入口先 check,存在就 skip, // 避免 Router LLM 自审失误导致已 approve 的 REQ 被重新 spec→plan→tdd(撞 V、污染源码)。 function checkReqDoneTagPromptM(id) { return [ `# tag \`req-done/${id}\` 是否存在(功能级 dedup 真值)`, microStepContract(), '', `跑 \`git -C ${ROOT} tag -l req-done/${id}\`。`, '## 输出(EXISTS_SCHEMA)', '- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`', ].join('\n') } function createReqDoneTagPromptM(id, phase) { return [ `# 打 annotated tag \`req-done/${id}\`(${phase==='frontend'?'前端 FE':'后端 REQ'} approve 后落地)`, microStepContract(), '', `跑 \`git -C ${ROOT} tag -a req-done/${id} -m "feature(${id}): approved by code-reviewer (phase=${phase})"\`。`, `先用 \`git -C ${ROOT} tag -l req-done/${id}\` 检查;已存在则视为成功(幂等)直接返回 success。`, '## 输出(ACTION_RESULT_SCHEMA)', '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "" }`', ].join('\n') } // ── 微步骤:milestone 专用 ── function checkAlreadyMergedPromptM(branch, defaultBranch) { return [ `# \`${branch}\` 是否已合入 \`${defaultBranch}\``, microStepContract(), '', `先跑 \`git -C ${ROOT} checkout ${defaultBranch}\` 确保 HEAD 在 ${defaultBranch};然后跑 \`git -C ${ROOT} merge-base --is-ancestor ${branch} HEAD\`。`, '## 输出(ALREADY_MERGED_SCHEMA)', '- 第二条 exit=0 → `{ "alreadyMerged": true }`(功能分支已是 HEAD 祖先,无需再 merge)', '- 非 0 → `{ "alreadyMerged": false }`', '- checkout 自身失败 → 整步失败(schema 失败即可)。', ].join('\n') } function executeMergePromptM(defaultBranch, branch, phaseId) { return [ `# 把 \`${branch}\` 合并进 \`${defaultBranch}\`(已确认尚未合入,已在默认分支)`, microStepContract(), '', `跑 \`git -C ${ROOT} merge --no-ff ${branch} -m "merge(${phaseId}): integrate ${branch}"\`。`, '- 成功 → `{ "success": true }`', '- 合并冲突 / 其它失败 → `{ "success": false, "error": "", "detail": "" }`', '- **不要**自动 \`git merge --abort\` / 自动 stash / 自动改文件——把树留给人工处理。', '## 输出(ACTION_RESULT_SCHEMA)', ].join('\n') } function readDocs08FieldPromptM(fe, id) { const section = fe ? '§ 三' : '§ 二' const title = fe ? '# 读 docs/08 § 三 `整体里程碑:` 字段当前值' : `# 读 docs/08 § 二 模块 \`${id}\` 的 \`里程碑:\` 字段当前值` const locator = fe ? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`- 整体里程碑: \` 行。` : `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section} 中 module id == \`${id}\` 的 bullet 段,取其 \` - 里程碑: \` 子项。` const missing = fe ? '- § 三 或该行不存在:`{ "found": false, "value": "" }`' : `- 模块 \`${id}\` 或该字段不存在:\`{ "found": false, "value": "" }\`` return [ title, microStepContract(), '', locator, '## 输出(FIELD_VALUE_SCHEMA)', '- 命中:`{ "found": true, "value": "<冒号后去空白的当前值>", "lineNumber": <行号> }`', missing, ].join('\n') } function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) { const scope = fe ? `§ 三 整体里程碑` : `§ 二 模块 ${id} 里程碑` const oldStr = fe ? '- 整体里程碑: —' : ' - 里程碑: —' const newStr = fe ? `- 整体里程碑: ${targetValue}` : ` - 里程碑: ${targetValue}` // 后端模块多个 bullet 同时含 ` - 里程碑: —`:必须按调用方传入的精确行号定位,否则在多模块 docs/08 // 里 Edit 会替换到第一处(通常不是本模块),把别的模块误标 milestone-complete。 const lineGuard = (typeof lineNumber === 'number' && Number.isFinite(lineNumber)) ? `先 Read \`${ROOT}/docs/08-模块任务管理.md\` 第 ${lineNumber} 行(1-based),确认该行字面量等于 \`${oldStr}\`;不等则 halt(返回 \`{success:false, error:"line-${lineNumber}-mismatch: actual="}\`)。然后仅替换第 ${lineNumber} 行;其余位置同名行**严禁**改动。` : `严禁全局替换:通过定位上下文(${fe ? '§ 三' : `§ 二 中 module_id == \`${id}\` 的 bullet 段`})找到该 bullet 的 \`里程碑\` 子项行,仅替换这一行。` return [ `# 把 docs/08 ${scope} 从 \`—\` 改为 \`${targetValue}\` 并 commit`, microStepContract(), '', `调用方已确认字段当前值 = \`—\`(你不必再读一遍)。`, `1. ${lineGuard} Edit \`${ROOT}/docs/08-模块任务管理.md\`:把整行 \`${oldStr}\` 替换为 \`${newStr}\`(精确字符串替换;**只动一处**)。`, `2. 跑 \`git -C ${ROOT} add docs/08-模块任务管理.md\`。`, `3. 跑 \`git -C ${ROOT} commit -m "chore(${phaseId}): record ${targetValue} in docs/08"\`。`, '## 输出(ACTION_RESULT_SCHEMA)', '- 三步全 OK:`{ "success": true }`;任一失败:`{ "success": false, "error": "" }`', ].join('\n') } // ── 微步骤:docs/08 功能行 checkbox(reviewer approve 后的可观测 side effect;read-then-write)── function readDocs08CheckboxPromptM(fe, id) { const section = fe ? '§ 三' : '§ 二' const kind = fe ? '功能' : 'REQ' const locator = fe ? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`功能:\` 项,从中找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。` : `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section},找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。` return [ `# 读 docs/08 ${section} ${kind} \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`, microStepContract(), '', locator, '## 输出(CHECKBOX_STATE_SCHEMA)', `- 命中 \`- [x] ${id} ...\`:\`{ "found": true, "state": "checked", "lineNumber": <行号> }\``, `- 命中 \`- [ ] ${id} ...\`:\`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``, '- 找不到:`{ "found": false, "state": "unchecked" }`(state 仍必填,避免 schema 失败掩盖真实缺口)。', ].join('\n') } function writeDocs08CheckboxPromptM(fe, id, phase, lineNumber) { const scope = fe ? `§ 三 功能 ${id}` : `§ 二 REQ ${id}` const lineGuard = (typeof lineNumber === 'number' && Number.isFinite(lineNumber)) ? `先 Read \`${ROOT}/docs/08-模块任务管理.md\` 第 ${lineNumber} 行(1-based),确认该行去掉行首空白后以 \`- [ ] ${id} \` 开头;不满足则返回 \`{success:false, error:"line-${lineNumber}-mismatch: actual="}\`。然后只替换第 ${lineNumber} 行的第一个 \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本。` : `定位 docs/08 ${scope} 中去掉行首空白后以 \`- [ ] ${id} \` 开头的唯一一行,只替换该行第一个 \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本。` return [ `# 把 docs/08 ${scope} 的 \`[ ]\` 勾选为 \`[x]\` 并 commit`, microStepContract(), '', `调用方已读到状态 = \`unchecked\`(你不必再读一遍)。`, `1. ${lineGuard}`, `2. 跑 \`git -C ${ROOT} add docs/08-模块任务管理.md\`。`, `3. 跑 \`git -C ${ROOT} commit -m "chore(${phase}:${id}): mark ${id} approved in docs/08"\`。`, '## 输出(ACTION_RESULT_SCHEMA)', '- 三步全 OK:`{ "success": true }`;任一失败:`{ "success": false, "error": "" }`', ].join('\n') } function checkTagExistsPromptM(tagName) { return [ `# tag \`${tagName}\` 是否存在`, microStepContract(), '', `跑 \`git -C ${ROOT} tag -l ${tagName}\`。`, '## 输出(EXISTS_SCHEMA)', '- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`', ].join('\n') } function createTagPromptM(phaseId, fe) { return [ `# 打 annotated tag \`milestone/${phaseId}\``, microStepContract(), '', `跑 \`git -C ${ROOT} tag -a milestone/${phaseId} -m "milestone(${phaseId}): ${fe ? '前端' : '后端'}阶段完成"\`。`, '## 输出(ACTION_RESULT_SCHEMA)', '- 成功:`{ "success": true }`;失败:`{ "success": false, "error": "" }`', ].join('\n') } // fe-skeleton-done:前端骨架占位 stage 的补记 tag;真实完成态以 router/state 检测为准。 function createFeSkeletonTagPromptM() { return [ '# 打 annotated tag `fe-skeleton-done`(前端骨架占位已建)', microStepContract(), '', `先用 \`git -C ${ROOT} tag -l fe-skeleton-done\` 检查;已存在则视为成功(幂等)直接返回 success。`, `否则跑 \`git -C ${ROOT} tag -a fe-skeleton-done -m "chore(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位已建"\`。`, '## 输出(ACTION_RESULT_SCHEMA)', '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "" }`', ].join('\n') } function findReportPromptM(phaseId) { return [ `# 找最新的 \`${phaseId}\` 完成报告并读取 § ⑫ 的 milestone tag 字段当前值`, microStepContract(), '', `用 Glob 在 \`${ROOT}/docs/superpowers/module-reports/\` 查找 \`*-${phaseId}.md\`(按文件名 YYYY-MM-DD 日期前缀降序取最新一份)。`, 'Read 该文件,定位 § ⑫("里程碑"小节)。', '## 输出(REPORT_PATH_SCHEMA)', `- 找到:\`{ "found": true, "path": "docs/superpowers/module-reports/", "currentTagValue": "<§ ⑫ 当前的字面值(应为 \\\`{{milestone_tag}}\\\` 或 \\\`milestone/${phaseId}\\\` 之一)>" }\``, '- 完全没有匹配文件:`{ "found": false }`', ].join('\n') } function updateReportPromptM(reportPath, targetTag, phaseId) { return [ `# 把 \`${reportPath}\` § ⑫ 的 \`{{milestone_tag}}\` 替换为 \`${targetTag}\` 并 commit`, microStepContract(), '', `1. Edit \`${ROOT}/${reportPath}\`:把字面量 \`{{milestone_tag}}\` 替换为 \`${targetTag}\`(精确替换;如多处出现就全部替换)。`, `2. \`git -C ${ROOT} add ${reportPath}\`;3. \`git -C ${ROOT} commit -m "docs(${phaseId}): record ${targetTag} in completion report"\`。`, '## 输出(ACTION_RESULT_SCHEMA)', '- 全 OK:`{ "success": true }`;失败:`{ "success": false, "error": "" }`', ].join('\n') } // ── 微步骤:cross-module 专用 ── function collectCrossModuleChangedPromptM(defaultBranch) { return [ `# 收集功能分支自 \`${defaultBranch}\` 分叉以来的全部改动文件`, microStepContract(), '', `跑 \`git -C ${ROOT} diff --name-status ${defaultBranch}...HEAD\`(三点 diff)。按行解析每行 \`\\t\`(status 通常为 M/A/D/R/C 等)。`, '## 输出(CHANGED_FILES_SCHEMA)', '- `{ "files": [ { "status": "M", "path": "backend/.../X.java" }, ... ] }`', '- diff 为空 → `{ "files": [] }`', ].join('\n') } function classifyCrossModulePromptM(moduleId, files) { const filesText = files.map(f => `- ${f.status} ${f.path}`).join('\n') return [ `# 把改动文件分类:哪些落在**非本模块 \`${moduleId}\`** 的目录下`, microStepContract(), '', `本模块目录归属以 \`${ROOT}/docs/08-模块任务管理.md § 二\` 中本模块 bullet 的 \`路径:\` 字段为准。Read 它以建立"路径 → 模块"映射(粒度/分层约定见 docs/04 § 1.2/2.1)。`, '', '## 改动文件清单', filesText, '', '## 判定规则', `- 落在本模块路径(\`${moduleId}\`)下 → **不算**跨模块。`, '- 落在其它模块路径下 → 算跨模块,给出该文件归属的目标模块 id。', '- 落在共享根(如 `docs/`、`scripts/`、`sql/migrations/`、`README.md` 等)→ **不算**跨模块。', '', '## 输出(CROSS_CLASSIFY_SCHEMA)', '- `{ "crossModule": [ { "file": "...", "targetModule": "module_x", "reason": "<本模块哪个 REQ-XXX-NNN 迫使改它,1 句>", "impact": "<目标模块哪些 API/行为/调用方/测试受影响,1-3 句>" }, ... ] }`', '- 无跨模块改动:`{ "crossModule": [] }`', '- **不要留 `TBD(CC 补)`**:本步骤就是补齐的唯一时机;推不出原因 / 影响 → 整步失败(schema 失败即可,调用方会 halt)。', ].join('\n') } // dedup-and-rewrite 不再 append:resume / 多次跑同一模块时,append 会产生重复行污染 § ⑦。 // 改为整体重写:读现有行 → 与本次 items 合并 → 按 (file, targetModule) dedup(本次 items 覆盖旧值) // → 按 (targetModule, file) 排序 → 整表重写。commit 前用 `git diff --quiet` 判定,无变更则跳过 commit。 function writeCrossModuleLogPromptM(moduleId, items) { const newRowsJson = JSON.stringify(items, null, 2) return [ `# 把跨模块改动以 dedup-and-rewrite 方式写入 \`docs/superpowers/module-reports/${moduleId}-cross-module.md\``, microStepContract(), '', `目标文件(项目根相对):\`docs/superpowers/module-reports/${moduleId}-cross-module.md\`。`, '', '## 流程', `1. **读现有行**:如果文件存在,用 Read 取出表格内已有的数据行(跳过表头与分隔行)。把每行解析为 \`{ file, targetModule, reason, impact }\`,得到 \`existingRows\`。文件不存在 → \`existingRows = []\`。`, '2. **合并 + dedup**:把"本次新增行 JSON"中的项加入 `existingRows`,按 `(file + "\\u0001" + targetModule)` 作为 dedup key——**本次新增项覆盖旧项**(同一 file × targetModule 的最新原因 / 影响为准)。', '3. **排序**:按 `(targetModule, file)` 字典序升序。', '4. **整体重写**:用 Write 把整个文件重写为:', ' ```', ' # 跨模块改动日志', ' ', ' | 文件 | 目标模块 | 原因 | 影响 |', ' |---|---|---|---|', ' <已排序的全部行>', ' ```', `5. **空变更跳过 commit**:跑 \`git -C ${ROOT} diff --quiet -- docs/superpowers/module-reports/${moduleId}-cross-module.md\`。`, ' - exit_code = 0(无变更)→ 不要 commit,直接返回 `{ "success": true, "detail": "no-diff-skip-commit" }`。', ` - exit_code != 0(有变更)→ \`git add\` + \`git commit -m "chore(${moduleId}): record cross-module log"\`。`, '', '## 本次新增行(JSON,作为合并输入)', '```json', newRowsJson, '```', '', '## 输出(ACTION_RESULT_SCHEMA)', '- 写成功且有/无 commit:`{ "success": true, "detail": "" }`', '- 任一步失败:`{ "success": false, "error": "" }`', ].join('\n') } // ---- 模块完成报告(原 module-report)---- function reportPrompt(module) { const id = module?.id ?? '' const fe = id === 'frontend-phase' const phaseId = fe ? 'frontend-phase' : id return [ `# module-report — ${fe ? '前端阶段' : `模块 ${id}`} 12 节完成报告`, '', featureStageContract(fe ? 'frontend' : 'backend'), '', '## 目标', `test-gate 绿后渲染标准化 **12 节**完成报告,commit 到当前分支(供 milestone 标记)。**只读 git 摘要,不读 diff 正文进上下文。**`, '', '## 前置', `- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`, fe ? `- **前端行为验收已并入 per-FE review 循环**(reviewer approve 子门,行为 green 是 \`req-done/\` 的前置真值)——report **不再**校验阶段级 behavior-gate 文件(已不再产生)。**对每个 \`req-done/\` tag 即视为该 FE 行为已过**(避免双真值)。可选轻量校验:每个 FE 存在对应 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/--behavior-r*-a*.md\` 且最后一份非 RED;缺证据不 halt(仅在 § ⑤/⑧ 标注)。` : '', '', '## 收集输入(取摘要而非正文)', 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-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。**另把 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/-FE-*-behavior-r*-a*.md\`(按 FE → behaviorRound → attempt 排序)的 flake / 环境 race(envError,含 build-failed 短路)/ 文字 continue 记录一并纳入 § ⑤ 汇总**。`, `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/-FE-*-behavior-r*-a*.md\` 汇总各 FE 的 \`coverageGaps\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', ].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-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。`, `- § ⑥ 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 节。硬验证:§ ⑧ 必须列举所有偏离(无则写"无偏离")。', `- 写入 \`docs/superpowers/module-reports/<当天日期 YYYY-MM-DD>-${phaseId}.md\`(项目根相对;resume 时复用已存在的 \`*-${phaseId}.md\` 最新日期前缀,不要起新文件),连同跨模块日志(如存在)一起 commit 到当前分支(milestone 的 worktree-clean 前置依赖此 commit)。`, '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', `- 成功:\`{ "status": "ok", "artifactPath": "docs/superpowers/module-reports/YYYY-MM-DD-${phaseId}.md", "summary": "<1-2 句中文摘要:测试是否 flake / 主要变更 / 是否有偏离>" }\`。`, '- 失败:`{ "status": "halt", "reason": "<阻塞点(如最后一次 test-gate red / 跨模块日志缺失)>" }`。', ].join('\n') } // ---- runBranchSetup:原 branchSetupPrompt 的散文流程 → JS 编排 + 微步骤 agent ---- // 幂等:分支已存在则 checkout,否则从默认分支新建。条件分支由 JS 判定,子代理只负责执行单一动作。 async function runBranchSetup(module) { const id = module?.id ?? '' const fe = id === 'frontend-phase' const branch = fe ? 'frontend-phase' : `module-${id}` const lbl = (k) => `branch:${k}:${id}` const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) // 工作树脏:先自主恢复(in-scope 残留 → 自动 commit);含越界改动则恢复失败 → halt(留给人工)。 const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) if (!wt.clean) { const rec = await agent(recoverDirtyWorktreePromptM(wt.dirty, branch, `分支 setup 前置(目标分支 ${branch})。`), {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`) } const exists = await agent(checkBranchExistsPromptM(branch), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) if (exists.exists) { await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout')}) } else { await runAction(g => createBranchFromPromptM(def.branch, branch) + g, {site:`branchSetup-create:${branch}`, grp:'Milestone', label: lbl('create')}) } // HEAD 确认:不符则经仲裁重切(retry)或留人工(halt)。 let head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj) if (verdict.action !== 'retry') throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${verdict.rationale || `HEAD on ${head.branch}`}`) await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-recheckout:${branch}`, grp:'Milestone', label: lbl('recheckout')}) head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) } if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${ADJUDICATE_MAX} 轮后 HEAD 仍在 ${head.branch}`) log(`branch-setup: ${id} → ${branch}`) } // ---- runMilestone:原 milestonePrompt 的 6 步散文流程 → JS 编排 ---- // 所有"已是目标态则跳过"的条件由 JS 在 read 结果上判定,子代理只执行确定性动作。 async function runMilestone(module) { const id = module?.id ?? '' const fe = id === 'frontend-phase' const phaseId = fe ? 'frontend-phase' : id const branch = fe ? 'frontend-phase' : `module-${id}` const targetTag = `milestone/${phaseId}` const lbl = (k) => `milestone:${k}:${phaseId}` // step 1: worktree clean precondition(脏树先自主恢复 in-scope 残留;含越界改动则 halt 留人工) const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) if (!wt.clean) { const rec = await agent(recoverDirtyWorktreePromptM(wt.dirty, branch, `里程碑前置(阶段 ${phaseId},应在功能分支 ${branch})。`), {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) if (!rec.success) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) } // step 2: detect default branch const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) // step 3: merge (idempotent — skip if already an ancestor) // merge 冲突保持**硬 halt**:自动 abort/stash/改文件均不安全,把树留给人工(设计原则不变)。 const merged = await agent(checkAlreadyMergedPromptM(branch, def.branch), {label: lbl('merged?'), phase: 'Milestone', schema: ALREADY_MERGED_SCHEMA}) if (!merged.alreadyMerged) { const r = await agent(executeMergePromptM(def.branch, branch, phaseId), {label: lbl('merge'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) if (!r.success) throw new Error(`HALT milestone-merge ${phaseId}: ${r.error || ''}${r.detail ? '\n' + r.detail : ''}`) } // step 4: docs/08 field (idempotent — read first, only write if at initial '—') let field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) for (let adj = 1; !field.found && adj <= ADJUDICATE_MAX; adj++) { const verdict = await adjudicate(`milestone-docs08-missing:${phaseId}`, { problem:`docs/08 ${fe ? '§ 三' : `§ 二 模块 ${id}`} 里程碑字段未找到` }, 'Milestone', adj) if (verdict.action !== 'retry') throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${verdict.rationale || '字段不存在'}`) field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) } if (!field.found) throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍未找到字段`) if (field.value === '—') { await runAction(g => writeDocs08FieldPromptM(fe, id, targetTag, phaseId, field.lineNumber) + g, {site:`milestone-docs08-write:${phaseId}`, grp:'Milestone', label: lbl('field-write')}) } else if (field.value !== targetTag) { const verdict = await adjudicate(`milestone-docs08-unexpected:${phaseId}`, { problem:`docs/08 里程碑字段当前 = ${JSON.stringify(field.value)}(行 ${field.lineNumber || '?'}),期望 '—' 或 '${targetTag}'`, allowContinue:true }, 'Milestone', 1) if (verdict.action === 'halt') throw new Error(`HALT milestone-docs08-unexpected ${phaseId}: ${verdict.rationale || JSON.stringify(field.value)}`) log(`milestone ${phaseId}: docs/08 字段非预期值(${JSON.stringify(field.value)}),仲裁判放行`) } // else: 已是 targetTag → 静默跳过(续跑场景) // step 5: report § ⑫ FIRST(关键顺序:tag 必须指向"§ ⑫ 已落地"的 commit,否则 // `git checkout milestone/` 看到的报告 § ⑫ 仍是 placeholder。原版顺序 tag → § ⑫ 是已知 bug, // 此处显式倒过来;下面 step 6 的 tag 才会指向新鲜 commit。) let rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) for (let adj = 1; !rpt.found && adj <= ADJUDICATE_MAX; adj++) { const verdict = await adjudicate(`milestone-report-missing:${phaseId}`, { problem:`未找到匹配 docs/superpowers/module-reports/*-${phaseId}.md 的报告文件` }, 'Milestone', adj) if (verdict.action !== 'retry') throw new Error(`HALT milestone-report-missing ${phaseId}: ${verdict.rationale || '报告文件缺失'}`) rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) } if (!rpt.found) throw new Error(`HALT milestone-report-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍无报告文件`) if (rpt.currentTagValue === '{{milestone_tag}}') { await runAction(g => updateReportPromptM(rpt.path, targetTag, phaseId) + g, {site:`milestone-report-update:${phaseId}`, grp:'Milestone', label: lbl('report')}) } else if (rpt.currentTagValue !== targetTag) { const verdict = await adjudicate(`milestone-report-unexpected:${phaseId}`, { problem:`${rpt.path} § ⑫ 当前 = ${JSON.stringify(rpt.currentTagValue)},期望占位符 {{milestone_tag}} 或 ${targetTag}`, allowContinue:true }, 'Milestone', 1) if (verdict.action === 'halt') throw new Error(`HALT milestone-report-unexpected ${phaseId}: ${verdict.rationale || JSON.stringify(rpt.currentTagValue)}`) log(`milestone ${phaseId}: 报告 § ⑫ 非预期值(${JSON.stringify(rpt.currentTagValue)}),仲裁判放行`) } // else: 已是 targetTag → 静默跳过(resume 幂等) // step 6: annotated tag (idempotent — tag exists 时静默跳过) const tag = await agent(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) if (!tag.exists) { await runAction(g => createTagPromptM(phaseId, fe) + g, {site:`milestone-tag:${phaseId}`, grp:'Milestone', label: lbl('tag')}) } log(`milestone: ${phaseId} → ${targetTag}`) } // ---- runCrossModule:原 crossModulePrompt 的"diff → 分类 → 写日志" → JS 编排 ---- // diff 和写文件是机械动作;"按 docs/08 § 二 路径归属判定哪些是跨模块"需要 LLM 判断,独立成一步。 async function runCrossModule(module) { const id = module?.id ?? '' const lbl = (k) => `xmod:${k}:${id}` const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) const changed = await agent(collectCrossModuleChangedPromptM(def.branch), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA}) if (!changed.files.length) { log(`cross-module-log: 模块 ${id} 无文件改动,跳过`) return } const classified = await agent(classifyCrossModulePromptM(id, changed.files), {label: lbl('classify'), phase: 'Milestone', schema: CROSS_CLASSIFY_SCHEMA}) if (!classified.crossModule.length) { log(`cross-module-log: 模块 ${id} 无跨模块改动,跳过`) return } await runAction(g => writeCrossModuleLogPromptM(id, classified.crossModule) + g, {site:`crossModule-write:${id}`, grp:'Milestone', label: lbl('write')}) log(`cross-module-log: 模块 ${id} 更新 ${classified.crossModule.length} 行`) } // ---- runFrontendSkeleton:前端骨架占位 stage 的 JS 编排(设计 § 2,前置依赖 A)---- // 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(FeStub 占位)+ 无悬空导航。 // 幂等(resume 安全):router/state 是唯一真实完成态;fe-skeleton-done 只作补记,避免陈旧 tag 跳过缺失骨架。 async function runFrontendSkeleton(feItems) { const lbl = (k) => `fe-skeleton:${k}` // step 1: 检查 router 是否已声明全 FE 路由;已建则只确保补记 tag 存在。 const state = await agent(frontendSkeletonStatePromptM(feItems), {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) if (state.exists) { log('fe-skeleton: router 已声明全部 FE 路由,确保 fe-skeleton-done tag 存在') await runAction(g => createFeSkeletonTagPromptM() + g, {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag')}) return } // step 2: 派子代理生成骨架(成功后子代理自行 commit;此处仅经 runStage 仲裁 halt 收敛)。 await runStage(g => frontendSkeletonPrompt(feItems) + g, {site:'fe-skeleton', grp:'Frontend', label: lbl('gen')}) // step 3: 打 fe-skeleton-done 补记 tag。 await runAction(g => createFeSkeletonTagPromptM() + g, {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag')}) log(`fe-skeleton: 已生成前端骨架(覆盖 ${(feItems || []).length} 个 FE 路由),打 fe-skeleton-done tag`) } // ============================================================================ // 编排逻辑(结构按 plan 骨架;featureLoop / reviewWithFixLoop / testGate / 顶层循环) // ============================================================================ // ---- 单功能链(后端 / 前端同构)---- // **顺序 for-await**(不是 pipeline)。理由: // - tdd / fix stage 会编辑共享工作树并 git commit;并发会争 .git/index.lock、撞 migration V。 // - pipeline 的"stage throw → item 掉 null、pipeline 永不 reject"语义会吞掉 reviewWithFixLoop / // verify / tdd 的 HALT throw,让模块主循环 try/catch 捕获不到,残缺模块照样被推进到 milestone。 // 顺序 for-await 让 throw 自然冒泡到主循环 try → catch → break,使 fail-fast 真正生效。 // // 派生 stage 全部 schema 化:spec/plan/tdd/verify/fix 共用 STAGE_RESULT_SCHEMA,统一经 runStage 跑: // stage 优先自主决策继续(缺值挑默认/解读并记入 decisions[]);返回 status:halt 或结构校验失败时不再立即 // fail-fast,而是经 adjudicate 仲裁 retry/continue/halt(最多 ADJUDICATE_MAX 轮),把"无法继续"收敛为最后手段。 // 功能级 dedup 真值 = `req-done/` git tag:featureLoop 入口先 check,存在则 skip(Router 文档/ // LLM 自审失误不再导致已 approve 的 REQ 被重新 spec→plan→tdd 污染源码 / 撞 V)。 // // 语义边界(重要):`req-done/` 表示"该功能在写 tag 时已通过 reviewer approve",**不**表示 // "实现自此再未变化"。若 testGate / 后续模块工作中人工或子代理改动了已 approve 功能的代码,重跑 // coding-start 时此 dedup 会跳过 spec/plan/tdd/verify/review,**不会**再次审阅这些后期改动。 // 这是有意的设计:避免在共享工作树里因为别的模块的 cross-cut 改动反复重跑前面所有 REQ。 // 若需要"approve 后改动必须再次走 review"的语义,请在改动前手动删除对应 `req-done/` tag。 async function featureLoop(items, phase) { const grp = phase === 'backend' ? 'Backend' : 'Frontend' for (const id of items) { // 入口 dedup:req-done/ 已存在 → 已 approve,整段 skip。 const done = await agent(checkReqDoneTagPromptM(id), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) if (done.exists) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } const spec = await runStage(g => deriveSpecPrompt(id, phase) + g, { site:`spec:${phase}:${id}`, grp, label:`spec:${phase}:${id}`, validate: r => { if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)' dateFromArtifactPath(r.artifactPath) // 文件名日期前缀非法 → 抛,被 runStage 捕获转为仲裁 return null }, }) // spec 经仲裁 continue 时 artifactPath 仍可能不带合法日期前缀——防御取值,避免重算抛出把 continue 变成隐式 halt。 let specDate = null try { specDate = dateFromArtifactPath(spec.artifactPath) } catch { specDate = null } const plan = await runStage(g => planPrompt(id, phase, spec.artifactPath) + g, { site:`plan:${phase}:${id}`, grp, label:`plan:${phase}:${id}`, validate: r => { if (!r.artifactPath) return 'plan 返回 ok 但缺 artifactPath' if (specDate && dateFromArtifactPath(r.artifactPath) !== specDate) return `plan 日期前缀与 spec 不一致:plan=${r.artifactPath} / spec=${spec.artifactPath}` return null }, }) // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界, // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。 const impl = await runStage(g => tddPrompt(id, phase, plan.artifactPath) + g, { site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, }) // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界, // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。 const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', spec.artifactPath, 0) + g, { site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, }) const reviewResult = await reviewWithFixLoop(id, phase, v0, spec.artifactPath) log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`) // approve 后落地 dedup 真值:req-done/ tag(失败经仲裁重试,确不可恢复才 halt)。 await runAction(g => createReqDoneTagPromptM(id, phase) + g, { site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, }) } } // review→fix 循环。halt 收敛点: // - 软上限 REVIEW_SOFT_ROUNDS 轮起每轮经仲裁决定**再延一轮(retry) 或 收尾(halt)**——禁止 continue(approve 只能来自 // reviewer,仲裁不得在仍有未修 must-fix 时凌驾它放行);绝对硬上限 REVIEW_HARD_ROUNDS 防无限循环。 // - reviewer 契约小瑕疵不再直接 halt:缺 locator 的 issue 降级为口头建议丢弃;若一条可定位 issue 都不剩(无可执行 // must-fix),经仲裁决定 continue(视为无 must-fix → approve)/ retry(带 guidance 重判)/ halt。 // - fix 经 runStage(显式 allowContinue,可 continue 跳过——未修的 must-fix 由后续 reviewer 重新 flag 兜底); // reverify 经 runStage 但 allowContinue:false(复验红色 = 修复没生效,绝不放行)。 // - approve 后的 docs/08 checkbox 是纯可视化副作用(req-done tag 才是完成真值),缺失/写失败一律 log 跳过不 halt。 const REVIEW_SOFT_ROUNDS = 5 const REVIEW_HARD_ROUNDS = 10 // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 async function flipDocs08Checkbox(fe, id, phase, grp) { const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA}) if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return } if (cb.state === 'checked') return if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return } const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) if (!wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${wr.error || ''}),跳过——cosmetic,不阻断`) } async function reviewWithFixLoop(id, phase, verifyResult, specPath) { const grp = phase === 'backend' ? 'Backend' : 'Frontend' const fe = isFrontend(phase) let lastVerify = verifyResult let lastIssuesCount = 0 let reviewGuidance = '' // 仲裁 retry 时注入下一轮 review 的纠正指令 // softPassed 提升到 reviewWithFixLoop 顶层作用域(与本 FE review 同寿命,跨 behaviorRound 持久)—— // 行为软文字一旦被仲裁 continue 放行(降级),重跑后即便仍在 textIssues 也不再追问,避免反复消耗仲裁预算。 const behaviorSoftPassed = new Set() for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) { const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 const r = await agent( reviewPrompt(id, phase, round, lastVerifySummary, specPath) + adjGuidance(reviewGuidance), {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'erp-workflow:code-reviewer'} ) reviewGuidance = '' // 已消费 if (r.verdict === 'approve') { // approve 闸显式 AND(设计 §6.2):reviewer.verdict==='approve' ∧ behaviorSubGate green(仅前端)。 // 后端逐字不变(无行为维度);前端:静态 approve 后**不立即 return**,先进 per-FE 行为 approve 子门—— // 起本 FE 全栈验「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才放行; // 行为 green ⇒ 才 flipDocs08Checkbox + return(req-done tag 落点 featureLoop 不动,语义自动升级为「静态过+行为过」)。 if (fe) { await behaviorSubGate(id, specPath, grp, behaviorSoftPassed) } await flipDocs08Checkbox(fe, id, phase, grp) return { id, phase, approved:true, rounds:round } } // request-changes:保留带 locator 的 must-fix;缺 locator 的降级丢弃(fix 步无从定位)。 const issues = Array.isArray(r.issues) ? r.issues.filter(x => x && typeof x.locator === 'string' && x.locator.trim()) : [] const dropped = (Array.isArray(r.issues) ? r.issues.length : 0) - issues.length if (dropped > 0) log(`review ${phase}:${id} r${round}: 丢弃 ${dropped} 个缺 locator 的 issue(降级为口头建议)`) if (issues.length === 0) { // 无任何可执行 must-fix(空 issues 或全缺 locator)→ 仲裁,而非直接 halt。 const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`, { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)', reviewerIssues: r.issues || [] }, grp, round) // continue 视为「无 must-fix → 静态 approve」——前端仍须先过行为 approve 子门(行为 green 是任何 approve return 的前置)。 if (verdict.action === 'continue') { if (fe) await behaviorSubGate(id, specPath, grp, behaviorSoftPassed) await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round } } if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`) reviewGuidance = verdict.guidance || '' // retry:带 guidance 重判(进入下一轮) continue } lastIssuesCount = issues.length await runStage(g => fixPrompt(id, phase, issues) + g, { site:`fix:${phase}:${id}:r${round}`, grp, label:`fix:${phase}:${id}:r${round}`, allowContinue: true, }) // reverify allowContinue:false:fix 后复验红色 = 修复没真正生效,绝不 continue 放行去 approve。 lastVerify = await runStage( g => verifyPrompt(id, phase, `(第 ${round} 轮 fix 后复验,上轮 must-fix: ${issues.length} 项)`, specPath, round) + g, { site:`reverify:${phase}:${id}:r${round}`, grp, label:`reverify:${phase}:${id}:r${round}`, allowContinue: false }, ) if (round >= REVIEW_SOFT_ROUNDS) { // 软上限到顶:仲裁只在"再延一轮(retry) / 收尾(halt)"间选。**禁止 continue 放行 approve**—— // 此处仍有 reviewer 认定的可定位 must-fix 未清,仲裁不得凌驾专用 code-reviewer 直接判 approve(approve 只能来自 reviewer 本身)。 const verdict = await adjudicate(`review-extend:${phase}:${id}`, { problem:`已 ${round} 轮 review 仍未 approve(上轮 ${lastIssuesCount} 项可定位 must-fix 未清)`, lastVerify: lastVerify.summary || lastVerify.reason || '', allowContinue:false }, grp, round) if (verdict.action !== 'retry') throw new Error(`HALT review-unresolved ${phase}:${id}: ${verdict.rationale || `${round} 轮仍有未修 must-fix`}`) // retry → 继续跑到硬上限(approve 仍由后续轮的 reviewer 决定) } } throw new Error(`HALT review-unresolved ${phase}:${id}: ${REVIEW_HARD_ROUNDS} 轮 review 仍未 approve(最后一次 reverify ${lastVerify?.status || '?'},最后一轮 must-fix ${lastIssuesCount} 项)`) } // flake 重试:每个 attempt 写独立证据文件 `-test-gate-r.md`,不覆盖前一次 red 证据(report § ⑤ 用得到)。 // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 async function testGate(module, phase) { let attempt = 1 let g = await agent(gatePrompt(module, phase, attempt), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) if (g.status === 'red') { // 自动重试 1 次(防 flaky) attempt = 2 g = await agent(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) } // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { const verdict = await adjudicate(`test-gate-red:${phase}:${module.id}`, { problem:`test-gate 第 ${attempt} 次仍 red`, failures: g.failures || [], allowContinue:false }, 'Gate', adj) if (verdict.action !== 'retry') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) attempt += 1 // retry:再跑一个独立 attempt 证据文件 g = await agent(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) } if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) return g } // ---- 前端 per-FE 行为验收控制流(runBehaviorGateOnce + behaviorSubGate)---- // 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 6.3 / 7。 // 行为验收并入 per-FE reviewWithFixLoop 的 approve 子门——reviewer 即将 approve 时才触发,绝不每 review round 起栈。 // behaviorSubGate 失败分层(per-FE 缩 scope,保留原 runBehaviorGate 的分层语义): // - build-failed(兄弟 FE 未实现 / 占位未覆盖,根因落非本 FE 路径)= 确定性短路:记 coverageGap + decisions, // 本轮行为门视为「本 FE 非缺陷」直接放行 approve(预期中途态,不 retry 不 halt)。 // - envError(其它) / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。 // - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 approve。 // - behaviorHard = interactionFailures + sentinel textIssues:有 locator → 降维喂 fixPrompt 跑 fix(fix 后功能 reverify + 下一轮重跑行为); // 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不 approve。 // - BEHAVIOR_FE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。 // envBlocked / ifails:per-FE bg 的环境/空覆盖与交互失败判定(build-failed 不计 envBlocked——它走确定性短路分支)。 function behaviorEnvBlocked(r) { const k = r.envError && r.envError.kind const ev = (k && k !== 'none' && k !== 'build-failed') ? r.envError : null const emptyCov = (Number(r.controlsEnumerated) === 0) || (Number(r.routesReached) === 0) return { ev, emptyCov, blocked: !!ev || emptyCov } } function behaviorIfails(r) { return Array.isArray(r.interactionFailures) ? r.interactionFailures : [] } // runBehaviorGateOnce:跑一次本 FE 行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 // 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 behaviorSubGate 推进)。 // behaviorRound:approve 子门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。 async function runBehaviorGateOnce(id, specPath, grp, behaviorRound) { const lbl = (a) => `behavior:${id}:r${behaviorRound}:a${a}` let attempt = 1 let bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) recordDecisions(`behavior:${id}`, bg.decisions) // build-failed 短路:根因落非本 FE 路径(兄弟未实现)→ 直接返回(外层据此放行 approve),不重试不仲裁。 const isBuildFailedShortCircuit = (r) => r.envError && r.envError.kind === 'build-failed' if (isBuildFailedShortCircuit(bg)) return bg // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。 while (behaviorEnvBlocked(bg).blocked && attempt < BEHAVIOR_ATTEMPT_MAX) { attempt += 1 bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) recordDecisions(`behavior:${id}`, bg.decisions) if (isBuildFailedShortCircuit(bg)) return bg } let envState = behaviorEnvBlocked(bg) for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) { const reason = envState.ev ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}` : `behavior 空覆盖:routesReached=${bg.routesReached} controlsEnumerated=${bg.controlsEnumerated}(绝不带空覆盖判 green)` const verdict = await adjudicate(`behavior-env:${id}`, { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, allowContinue:false }, grp, adj) if (verdict.action !== 'retry') throw new Error(`HALT behavior-env ${id}: ${verdict.rationale || reason}`) attempt += 1 bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) recordDecisions(`behavior:${id}`, bg.decisions) if (isBuildFailedShortCircuit(bg)) return bg envState = behaviorEnvBlocked(bg) } if (envState.blocked) throw new Error(`HALT behavior-env ${id}: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) return bg } // behaviorSubGate:reviewer approve 的「行为 approve 子门」。green 才允许 reviewWithFixLoop return approve。 // softPassed:由 reviewWithFixLoop 顶层注入,跨 behaviorRound 持久(软文字一旦放行不再追问)。 // green ≡ behaviorHard.length===0 ∧ envError∈{none,build-failed} ∧ 本 FE 覆盖非空(或 build-failed 短路)。 async function behaviorSubGate(id, specPath, grp, softPassed) { const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_FE_MAX; behaviorRound++) { const bg = await runBehaviorGateOnce(id, specPath, grp, behaviorRound) // 1) build-failed 短路(依赖 B):兄弟未实现 / 占位未覆盖 → green-by-skip 放行。但骨架(lazy router + FeStub) // 令「合法的兄弟未实现 build-failed」极罕见,故一个 build-failed 更可能是本 FE 引入的真共享代码回归; // 绝不凭未校验的 LLM 归因静默放行——先过轻量前置校验(comment §107-108 声称 load-bearing 的边界,此前无 JS 兜底): // a) 必须有 rootCausePath(否则无从判定根因落点); // b) 不得同时携带交互硬问题(interactionFailures / source=sentinel 文字)——那是真缺陷搭车。 // 任一不满足 = 「脏」build-failed → 不短路,过 adjudicate(allowContinue:false) retry/halt,绝不 green-by-skip。 if (bg.envError && bg.envError.kind === 'build-failed') { const rootCausePath = (bg.envError.rootCausePath || '').trim() const hardRiders = behaviorIfails(bg).length + (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length const dirty = !rootCausePath ? 'build-failed 未给 rootCausePath(无法判定根因是否落在本 FE 之外)' : hardRiders ? `build-failed 同时携带 ${hardRiders} 项交互/sentinel 硬问题(疑似本 FE 真构建 bug 搭车)` : null if (dirty) { const verdict = await adjudicate(`behavior-buildfailed-dirty:${id}`, { problem:`build-failed 归因不可信,绝不短路放行:${dirty}(rootCausePath=${rootCausePath || '∅'})`, envError: bg.envError, allowContinue:false }, grp, behaviorRound) if (verdict.action !== 'retry') throw new Error(`HALT behavior-buildfailed ${id}: ${verdict.rationale || dirty}`) continue // retry → 下一 behaviorRound 重跑整门 } // 干净的 build-failed(有 rootCausePath 且无硬问题搭车)→ green-by-skip 放行,记低置信证据。 recordDecisions(`behavior-build-failed:${id}`, [{ question:`本 FE ${id} 行为验收遇 build-failed(根因 ${rootCausePath})`, choice:'green-by-skip(兄弟 FE 未实现属预期中途态,本 FE 非缺陷,放行 approve)', rationale: bg.envError.detail || '', confidence:'low' }]) log(`behavior ${id}: build-failed 短路放行(根因非本 FE:${rootCausePath}),记证据不阻断`) return } // 2) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 // locator-not-resolvable(B 类硬问题反查不出)计入未覆盖——下面会因 behaviorHard 仍非空或覆盖不足而不 green。 for (const cg of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) { if (!cg) continue recordDecisions(`behavior-coverage:${id}`, [{ question:`覆盖缺口 ${cg.page}(${cg.reason})`, choice:'记录不阻断', rationale: cg.detail || '', confidence:'low' }]) } // 3) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。 // 永不阻断 approve;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。 let softRetry = false for (const ti of (Array.isArray(bg.textIssues) ? bg.textIssues : [])) { if (!ti || ti.source === 'sentinel') continue // sentinel 归 behaviorHard,不在软文字处理 if (softPassed.has(regionKey(ti))) continue const site = `behavior-text:${id}:${ti.page || '?'}:${ti.region || '?'}` const verdict = await adjudicate(site, { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 approve):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, textIssue: ti, allowContinue: true }, grp, behaviorRound) if (verdict.action === 'continue') { recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`, choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }]) softPassed.add(regionKey(ti)); continue } if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || `文字不符 source=${ti.source}`}`) softRetry = true; break // retry → 重跑本 behaviorRound(跳到下一轮迭代重起整门) } if (softRetry) continue // 3.5) B 类硬问题(locator-not-resolvable coverageGap):连组件文件都反查不出,不静默放行—— // 计入未覆盖阻断 approve,走 adjudicate(allowContinue:false) retry/halt(绝不当 green 放行,降级≠放行)。 const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : []).filter(cg => cg && cg.reason === 'locator-not-resolvable') if (bClass.length) { const summary = bClass.map(cg => `${cg.page} — ${cg.detail}`).join('; ') const verdict = await adjudicate(`behavior-bclass:${id}`, { problem:`behavior 硬问题连组件文件都反查不出(B 类,不可降级放行,计入未覆盖阻断 approve):${summary}`, coverageGaps: bClass, allowContinue:false }, grp, behaviorRound) if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass ${id}: ${verdict.rationale || summary}`) continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) } // 3.6) 覆盖率对账(确定性兜底):空覆盖只兜 ==0;这里兜 0 cg && ROUTE_GAP.has(cg.reason) && typeof cg.page === 'string' && cg.page.trim()) .map(cg => cg.page.trim())) const routeGapCount = routeGapPages.size const missedRoutes = Math.max(0, planned - reached) const unaccounted = Math.max(0, missedRoutes - routeGapCount) if (planned > 0 && unaccounted > 0) { const verdict = await adjudicate(`behavior-undercoverage:${id}`, { problem:`本 FE 路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`, coverageGaps: bg.coverageGaps || [], allowContinue: false }, grp, behaviorRound) if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage ${id}: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`) continue // retry → 下一 behaviorRound 重跑整门 } // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues。 const sentinelHard = (Array.isArray(bg.textIssues) ? bg.textIssues : []) .filter(t => t && t.source === 'sentinel') .map(t => ({ page:t.page, control:t.region, kind:'binding-garbage', detail:`sentinel 不符 期望=${t.expected} 实际=${t.actual}`, locator:t.locator })) const behaviorHard = [...behaviorIfails(bg), ...sentinelHard] const hasEnvSignal = !!(bg.envError && bg.envError.kind && bg.envError.kind !== 'none') const hasAnyClassifiedSignal = hasEnvSignal || behaviorHard.length > 0 || (Array.isArray(bg.textIssues) && bg.textIssues.length > 0) || (Array.isArray(bg.coverageGaps) && bg.coverageGaps.length > 0) if (bg.status === 'red' && !hasAnyClassifiedSignal) { const verdict = await adjudicate(`behavior-red-unclassified:${id}`, { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green', behaviorResult: bg, allowContinue:false }, grp, behaviorRound) if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified ${id}: ${verdict.rationale || 'status:red 无分类原因'}`) continue } // 5) green 判定:behaviorHard 为空 ∧ 无 B 类未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 子门 green 放行。 if (behaviorHard.length === 0) { log(`behavior ${id} green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) return } // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不 approve)。 const withLoc = behaviorHard.filter(x => typeof x.locator === 'string' && x.locator.trim()) const noLoc = behaviorHard.filter(x => !(typeof x.locator === 'string' && x.locator.trim())) if (noLoc.length) { const summary = noLoc.map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ') const verdict = await adjudicate(`behavior-noloc-hard:${id}`, { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue/approve):${summary}`, interactionFailures: noLoc, allowContinue:false }, grp, behaviorRound) if (verdict.action !== 'retry') throw new Error(`HALT behavior-noloc-hard ${id}: ${verdict.rationale || summary}`) continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) } // 7) 有 locator 的硬问题 → 降维成 {summary,locator,severity} 喂现有 fixPrompt 跑 fix(schema 不合并、fix 入参合并)。 const fixIssues = withLoc.map(f => ({ summary: `[behavior:${f.kind}] ${f.page}:${f.control} — ${f.detail}`, locator: f.locator, severity: 'high', })) await runStage(g => fixPrompt(id, 'frontend', fixIssues) + g, { site:`behavior-fix:${id}:r${behaviorRound}`, grp, label:`behavior-fix:${id}:r${behaviorRound}`, allowContinue: true, }) // 8) fix 后功能复验(allowContinue:false):behaviorSubGate 的 fix 改的是 frontend/ UI 源码,可能引入功能回归—— // 先跑 scoped 组件测试 reverify(不起全栈,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。 await runStage( g => verifyPrompt(id, 'frontend', `(behaviorRound ${behaviorRound} 行为 fix 后功能复验,本轮 must-fix: ${fixIssues.length} 项)`, specPath, REVIEW_HARD_ROUNDS + behaviorRound) + g, { site:`behavior-reverify:${id}:r${behaviorRound}`, grp, label:`behavior-reverify:${id}:r${behaviorRound}`, allowContinue: false }, ) // 进入下一 behaviorRound → 重跑本 FE 行为验收 } throw new Error(`HALT behavior-unresolved ${id}: ${BEHAVIOR_FE_MAX} 轮 per-FE 行为子门仍未 green(硬问题未清)`) } phase('Router') // Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。 // id 形状(assertSafeId)是**安全护栏**——失败立即硬 halt,绝不重试绕过。 // reqs/feItems 互斥违例可由仲裁带 guidance 重跑 router 纠正(绝对上限 ADJUDICATE_MAX)。 const ID_PATTERN = /^[A-Za-z0-9_-]+$/ function assertSafeId(kind, value) { if (typeof value !== 'string' || !ID_PATTERN.test(value)) { throw new Error(`HALT router-invalid-${kind}: ${JSON.stringify(value)}(必须匹配 /^[A-Za-z0-9_-]+$/,用于安全地拼入 git 命令)`) } } function routerViolation(modules) { for (const m of modules) { const isFE = m.id === 'frontend-phase' if (isFE && Array.isArray(m.reqs) && m.reqs.length) return `frontend-phase 聚合模块的 reqs 必须为空,实测含 ${m.reqs.length} 项 (${m.reqs.join(',')})` if (!isFE && Array.isArray(m.feItems) && m.feItems.length) return `后端模块 ${m.id} 的 feItems 必须为空(前端只在 frontend-phase 聚合),实测含 ${m.feItems.length} 项 (${m.feItems.join(',')})` } return null } let routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA}) for (let adj = 1; adj <= ADJUDICATE_MAX; adj++) { const violation = routerViolation(routed.modules) if (!violation) break const verdict = await adjudicate('router-violation', { problem: violation }, 'Router', adj) if (verdict.action !== 'retry') throw new Error(`HALT router-violation: ${verdict.rationale || violation}`) routed = await agent(routerPrompt(ROOT) + adjGuidance(verdict.guidance || ''), {label:`router:r${adj + 1}`, phase:'Router', schema: ROUTER_SCHEMA}) } const finalViolation = routerViolation(routed.modules) if (finalViolation) throw new Error(`HALT router-violation: ${ADJUDICATE_MAX} 轮仲裁后仍违例:${finalViolation}`) // id 安全护栏:最终选定的 routed 必须全部通过(assertSafeId 硬 halt)。 for (const m of routed.modules) { assertSafeId('module-id', m.id) for (const r of m.reqs || []) assertSafeId('req-id', r) for (const f of m.feItems || []) assertSafeId('fe-id', f) } const todo = routed.modules.filter(m => !m.done) log(`coding: ${todo.length}/${routed.modules.length} modules to run`) const results = [] let haltedAtIdx = -1 for (const [idx, module] of todo.entries()) { try { phase('Milestone') await runBranchSetup(module) if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过) phase('Backend') await featureLoop(module.reqs, 'backend') phase('Gate') await testGate(module, 'backend') phase('Milestone') await runCrossModule(module) // 替代被删 hook,JS 编排:diff → 分类 → 写日志 } if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块) phase('Frontend') // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达, // 使 per-FE 行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 await runFrontendSkeleton(module.feItems) // 前端行为验收已并入 featureLoop→reviewWithFixLoop 的 per-FE approve 子门(reviewer approve 时起本 FE 全栈验 // 「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才打 req-done)——不再有阶段级末尾独立行为门。 await featureLoop(module.feItems, 'frontend') phase('Gate') await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright)保留,与 per-FE 行为验收职责正交 } phase('Milestone') // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"—— // 绝不 continue 放行去打 milestone(否则可能在红色测试上 milestone)。 await runStage(g => reportPrompt(module) + g, { site:`report:${module.id}`, grp:'Milestone', label:`report:${module.id}`, allowContinue: false, }) await runMilestone(module) results.push({ module: module.id, status:'done' }) } catch (e) { results.push({ module: module.id, status:'halted', reason: String(e.message||e) }) haltedAtIdx = idx break // 整阶段 fail-fast:halt 后停,等人工修复后重跑 coding-start } } // pending:halt 后被跳过的剩余模块(M5)。caller / coding-start 可据此告知用户"修好后还有哪些待跑", // 而不是仅看到一个 halted 模块就误以为只剩一个。 const pending = haltedAtIdx >= 0 ? todo.slice(haltedAtIdx + 1).map(m => ({ module: m.id, status: 'pending' })) : [] // Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志 // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 // 注:decisions 仅覆盖**本次运行实际新跑**的 stage;resume 时被 req-done/milestone tag 跳过的已完成功能, // 其决策不会重新登记于此——需到对应 docs/superpowers/specs|plans/-.md 产物显著位置查阅。 // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`, // 否则 Workflow 拿不到 results / pending。 return { results, pending, decisions: autonomousDecisions }