diff --git a/workflows/coding.mjs b/workflows/coding.mjs index f57c838..fce8e75 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -328,7 +328,14 @@ function featureStageContract(phase, c) { return [ '## 硬约束(非交互子代理)', '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', - '- **测试执行子会话的物理实现(授权)**:下文「派发 Agent 子会话跑测试 / 绝不在主会话直接跑测试」的护栏,其**真实意图**是「测试执行与你的主推理流隔离、只回收结构化结果(不把全文 stdout 灌进上下文)」。本运行环境若**没有** Agent/Task 子会话派发工具(用 ToolSearch `select:Task` 无匹配即属此情形),则以 **`Bash` 的 `run_in_background` 隔离后台子进程**作为该子会话的物理实现:派一个 detached 子进程跑测试命令、输出重定向到 `.tmp/` 日志文件,你只读回 `{command, exit_code, failing_assertion / passed / failed}` 等结构化摘要。这**已满足**护栏意图,故**绝不**因「缺 Agent 派发工具」而 halt——这不是硬事实缺失、不是缺值阻塞,retry 也不会改变工具面,halt 只会空耗一次重跑。**仍然禁止**:把测试命令在主推理流里内联同步执行并把全文 stdout 读进上下文。下文出现「Agent 子会话」一律按本条解释为「隔离后台子进程子会话」。', + // 「Agent 子会话」在本项目运行环境里恒不可用(Workflow 派生的子代理工具面只有编排类,无 Task 派发)。 + // 2026-06-13 曾因此连炸 3 次 HALT(BAC-Position-Query / DeliverMode-Query / CustomerGrade-Add),42b6ed8 + // 补了本授权条款后再未复发。但原文让每个 stage「用 ToolSearch select:Task 探测」并按自主决策处理, + // 于是同一个结论被重复探测、重复推理、重复登记——实测某项目 RESUME.md 2320 条决策里 123 条(5.3%) + // 都在说这一件事,还随 handoff 反复读回上下文。故降级为**既定执行约定**:不必探测、不登记、不升级。 + '- **测试执行子会话的物理实现(既定约定,不是自主决策)**:下文「派发 Agent 子会话跑测试 / 绝不在主会话直接跑测试」的护栏,其**真实意图**是「测试执行与你的主推理流隔离、只回收结构化结果(不把全文 stdout 灌进上下文)」。本项目运行环境**常态没有** Agent/Task 子会话派发工具,**不必特意用 ToolSearch 去探测确认**——直接以 **`Bash` 的 `run_in_background` 隔离后台子进程**作为该子会话的物理实现:派一个 detached 子进程跑测试命令、输出重定向到 `.tmp/` 日志文件,你只读回 `{command, exit_code, failing_assertion / passed / failed}` 等结构化摘要。这**已满足**护栏意图。(工具面里确实有 Task/Agent 派发工具时,用它当然也可以,等价。)', + ' - **不要登记、不要升级、不要 halt**:这条是既定约定,**不是**你做的自主决策——**绝不**写进 `decisions[]`,**绝不**为此向仲裁升级,**绝不**因「缺 Agent 派发工具」而 halt。它不是硬事实缺失、不是缺值阻塞,retry 也不会改变工具面。', + ' - **仍然禁止**:把测试命令在主推理流里内联同步执行并把全文 stdout 读进上下文。下文出现「Agent 子会话」一律按本条解释为「隔离后台子进程子会话」。', '- **后台等待纪律(硬约束,违反即前功尽弃)**:把测试/闸门派到后台子进程后,必须在**同一回合内**等到终态——用重复的 `Bash` 调用轮询(`sleep N` 后读日志尾/exit 标记文件,长跑闸门每 60~120s 一轮,多少轮都行)。**绝不**使用 Monitor / ScheduleWakeup / 任何「装个监听器然后结束回合等通知」的模式:你是 Workflow 派生子代理,**结束回合=宣告完成**,会被判「未调 StructuredOutput」使整个模块 halt,任何通知都不会把你唤醒(AGT test-gate 2026-07-11 实锤教训)。拿到终态先写证据再调 StructuredOutput,一个回合内闭环。', '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md` → `docs/05-API接口契约.md` → `prototype/`(前端布局/交互权威)→ `src/styles/tokens.css`(前端色值)→ `CLAUDE.md` → 现有代码。', '- 仍查不到时——**优先自主决策继续,不要停下**:基于现有代码约定 / 技术规范 / 同类实现,挑选**最有依据的解读或合理默认值**,把该决策写进产物显著位置,并在返回的 `decisions[]` 中逐条登记 `{question, choice, rationale, confidence}`(这是默认动作,项目目标是全自动静默、尽可能少 halt)。', @@ -936,6 +943,10 @@ function prototypePreviewPrompt(feItems, attempt, c) { '', '## step3 证据落盘 + commit', `- 写 \`${evidence}\`:按页一小节(原型相对路径 / 截图链接 / controlsExercised / jsErrors / consoleErrors / 视口决策),头部写 pagesFound-Rendered 汇总。渲染失败 / JS 错用**红字**标注为**阻断项**(上层据此 halt——回 /erp-workflow:plan-start 或 /add-req 修原型后重跑,**不在 coding 改**);console.error 标注为 advisory。`, + // 幂等判据(省时改造):本门无条件重跑曾让同一份 prototype/ 被渲染 3 次(每次 3–7 分钟)。 + // 证据头部钉一行 prototypeCommit,下次开跑前的 state 微步骤据它与 prototype/ 当前 tip 比对决定是否跳过。 + // 必须是 prototype/ 的**路径限定** tip(不是仓库 HEAD)——否则任何无关提交都会让判据失效、退化回每次重跑。 + `- **头部第一行必须逐字写**(幂等判据,下次开跑靠它决定能否跳过本门,格式错=白跑一次):\`- prototypeCommit: <哈希>\`,其中 \`<哈希>\` = \`git -C ${c.root} log -1 --format=%H -- prototype/\` 的完整输出(40 位)。该命令无输出(\`prototype/\` 从未提交)→ 写 \`- prototypeCommit: none\`。`, commitBlock(`${evidence} ${assets}`, 'docs(preview:prototype): 静态原型渲染基线 + 交互冒烟证据', undefined, c), '', '## 输出(必须符合下发的 PROTOTYPE_PREVIEW JSON schema)', @@ -947,6 +958,26 @@ function prototypePreviewPrompt(feItems, attempt, c) { ].filter(Boolean).join('\n') } +// preview 幂等判定:prototype/ 自上次渲染证据以来没变过 → 整门可跳过。 +// 动机:本门原先无条件跑,实测同一份 prototype/ 在一次增量里被渲染 3 次(每次 3–7 分钟,STAGE_MIN.preview); +// resume 越多浪费越大。判据取 prototype/ 的**路径限定** git tip 与证据头部记录的 prototypeCommit 比对—— +// 确定性、无需重跑渲染。判不准一律 exists:false(宁可白跑一次,不可跳过真的变更过的原型)。 +function prototypePreviewStatePromptM(c) { + const evidence = 'docs/superpowers/module-reports/prototype-preview.md' + return [ + '# 检测静态原型渲染门是否可跳过(prototype/ 自上次渲染以来未变)', + microStepContract(c.root), + '', + `1. 跑 \`git -C ${c.root} log -1 --format=%H -- prototype/\` 取 \`prototype/\` 的路径限定 tip 哈希(记为 CUR;无输出记 \`none\`)。`, + `2. Read \`${c.root}/${evidence}\`。文件不存在 → \`{ "exists": false }\`(从未跑过本门)。`, + '3. 从该文件**头部**找逐字形如 `- prototypeCommit: <哈希>` 的行,取其值(记为 REC)。找不到该行 → `{ "exists": false }`(旧版证据无判据,按未跑过处理)。', + '4. `CUR === REC` 且两者都不是 `none` → `{ "exists": true }`(原型未变,渲染基线仍有效,本门可跳过)。', + '5. 其余一切情形(不相等 / 任一为 `none` / 哈希格式异常 / 文件读取失败)→ `{ "exists": false }`。', + '- **判不准就返回 false**:跳过一次本该跑的渲染门,会让过期截图基线流到下游行为门比对;多跑一次只是几分钟。两者不对称,别赌。', + '## 输出(EXISTS_SCHEMA)', + ].join('\n') +} + // ---- 前端行为验收(阶段级行为门,v3)---- // 设计权威:docs/design/2026-06-05-frontend-behavior-stage-gate.md。 // 时机:featureLoop(frontend) 把本轮 feItems 全部跑完静态 review(req-done tag 已打)之后、testGate 之前, @@ -990,7 +1021,15 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { // 改 frontend/ 源码——期望(来自 prototype/REQ/docs05)与 runner(DOM/locator 驱动)在同一前端阶段内 // 不随 fix 失效。故只重跑不重推导。**运行栈仍绝不复用**(每次冷起,契约未变)。 const carryDir = `${c.root}/.tmp/behavior-gate/frontend-phase/carry` - const reuse = behaviorRound > 1 || attempt > 1 + // 复用分三态(省时改造 · 跨 run): + // - mustReuse(round>1 || attempt>1):同一次 run 内的后续轮,carry 必定是本 run 刚写的 → 无条件复用。 + // - mayReuse(round1 && attempt1):本次是新一轮 coding.mjs run 的首跑。原先这里**无条件删掉整个 + // frontend-phase/ 目录(含 carry)**,而每个新 run 都从 behaviorRound=1 起步——carry 于是永远活不过 + // 一次 run,每次 resume 都要重烧约 18 分钟重推导(见本文件下方 carry 注释里的实测拆分)。 + // 改为:保留 carry,让子代理按「作用域 FE 集 + 各 FE spec 的 git tip」校验它是否仍适用,通过才复用。 + // 失效条件收敛于 spec——门的期望完全派生自 spec/prototype/docs05,而 fix 只改 frontend/ 源码。 + const mustReuse = behaviorRound > 1 || attempt > 1 + const mayReuse = !mustReuse const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-r${behaviorRound}-a${attempt}.md` return [ `# behavior — 前端阶段级行为验收(headless,frontend-phase, behaviorRound=${behaviorRound}, attempt=${attempt})`, @@ -1008,19 +1047,41 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', '- **冷起栈(运行时硬约束)**:本门**不经 playwright.config 跑**(项目的 `webServer` 只管前端,起不了后端 + 测试库副本 + 种子),故**没有可复用的起栈入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', '- **前端就绪判定照抄后端那套**:spawn 前端后**必须轮询到端口真正可连**才开始驱动页面,不允许起完就跑。前端 dev server 未就绪却直接连,会产出一批「控件找不到 / 页面空白」的假 RED,归因时极难与真缺陷区分——这是本项目 e2e 侧最贵的一类假红灯。超时未就绪 → `envError.kind="stack-not-ready"`(走 retry),**不要**记成 `interactionFailures`。', - `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 - ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子/carry),再新建本轮子目录 \`${tmpDir}/\` 与派生物暂存区 \`${carryDir}/\`。` - : `本次 behaviorRound=${behaviorRound} / attempt=${attempt} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建;**\`${carryDir}/\` 绝不删**——它装着上一次已推导好的 runner/种子/期望,本次要复用(见下方「派生物复用」)。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, + `- **入口清目录(跑前第一步,去串味)**:${mayReuse + ? `本次是本 run 的首轮首次 → 删除 \`${c.root}/.tmp/behavior-gate/frontend-phase/\` 下**除 \`carry/\` 外**的全部子目录(即历史 \`r*/\` 残留 runner 与种子),再新建本轮子目录 \`${tmpDir}/\`。**\`${carryDir}/\` 此时绝不删**——它可能是上一次 coding.mjs run 留下的、仍然适用的推导结果,是否复用由下方「carry 校验」判定,判定不通过时才由你清掉重推。` + : `本次 behaviorRound=${behaviorRound} / attempt=${attempt} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建;**\`${carryDir}/\` 绝不删**——它装着本 run 已推导好的 runner/种子/期望,本次要复用(见下方「派生物复用」)。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, // 派生物复用(省时改造):只重跑不重推导。首次跑完必须落 carry,后续轮次必须先读 carry。 - reuse + mustReuse ? [ - `- **派生物复用(本次必须先做,${behaviorRound > 1 ? `上一轮 fix 只改了 \`frontend/\` 源码` : `上一次是同轮 attempt 重试`})**:先 Read \`${carryDir}/expectations.md\`(上次已推导的:全部 FE 作用域并集 / 逐路由预期控件与文字来源 / 每路由所需角色 / sentinel 值表 / 组件文件反查表)与 \`${carryDir}/run.mjs\`、\`${carryDir}/sentinel.sql\`。`, + `- **派生物复用(本次必须先做,${behaviorRound > 1 ? `上一轮 fix 只改了 \`frontend/\` 源码` : `上一次是同轮 attempt 重试`})**:先 Read \`${carryDir}/expectations.md\`(上次已推导的:清单内 FE 的作用域并集 / 逐路由预期控件与文字来源 / 每路由所需角色 / sentinel 值表 / 组件文件反查表)与 \`${carryDir}/run.mjs\`、\`${carryDir}/sentinel.sql\`。`, ` - **三者齐备 → 直接复用**:把 \`${carryDir}/run.mjs\` / \`sentinel.sql\` 复制进本轮 \`${tmpDir}/\` 后使用,**跳过 step1 / step4 的重新推导**(期望以 \`expectations.md\` 为准),直接进 step2 起栈。这是本次的默认路径。`, ` - **只在有明确证据时才改**:上次证据显示 runner 自身有 bug(选择器过时 / 断言写错 / 漏驱动),或 \`frontend/\` 的路由表在上一轮 fix 中真的变了 → 就地**增量修补**复制过来的 runner,并把修补同步回 \`${carryDir}/run.mjs\`;**绝不**因为"想重写得更好"而从头重推导。`, ` - 缺文件 / 内容不可用 → 按首次流程完整推导,跑完照样落回 \`${carryDir}/\`(缺失是异常,在证据报告里注明)。`, ` - **不复用的部分(硬约束)**:运行栈(每次冷起)、数据库副本(每次重建)、演示/sentinel 数据的**注入动作**(每次重灌)、证据报告(每次独立文件)。复用的只是"推导结果与脚本"。`, ].join('\n') - : `- **派生物落盘(本次是首次推导,必须做)**:step1/step4 推导完成后,把 ① 推导结果写成 \`${carryDir}/expectations.md\`(全部 FE 作用域并集 / 逐路由预期控件与文字来源 / 每路由所需角色 / sentinel 值表 / route→组件文件反查表)、② runner 写一份到 \`${carryDir}/run.mjs\`、③ sentinel 种子写一份到 \`${carryDir}/sentinel.sql\`。后续 behaviorRound / attempt 会直接复用这三份**跳过重新推导**(fix 只改 \`frontend/\` 源码,不使这些失效),省掉每轮十几分钟的重复侦查。carry 与本轮 \`${tmpDir}/\` 同属临时区、跑完即弃,**不提交**。`, + : [ + // 跨 run 复用:carry 可能来自上一次 coding.mjs run。它的有效性只取决于「作用域 FE 集」与 + // 「各 FE spec 内容」——门的全部期望都派生自 spec/prototype/docs05,而两次 run 之间发生的 + // 只有 frontend/ 源码 fix(不影响期望)。故校验这两项即可,不必因为"换了一次 run"就重推。 + `- **carry 校验(本次是本 run 首跑,先判能否复用上一次 run 留下的推导结果)**:Read \`${carryDir}/expectations.md\`、\`${carryDir}/run.mjs\`、\`${carryDir}/sentinel.sql\`。任一缺失 → 判**失效**,直接走下方「完整推导」。`, + ` - 三者齐备时,从 \`expectations.md\` **头部**取两行判据(首次推导时按下方「落盘」格式写入,格式不符即判失效):`, + ' - `- carryScope: <逗号分隔的 FE 列表>` —— 与本次清单**逐项比对(集合相等,顺序无关)**。不等 → 失效。', + ` - \`- carrySpecs: =<短哈希> ...\` —— 对其中每个 FE,跑 \`git -C ${c.root} log -1 --format=%h -- <该 FE 当前采用的 spec 文件路径>\`,与记录值比对。任一不等 → 失效。`, + ` - 再补一道**新版本探测**:对清单内每个 FE,Glob \`${c.root}/docs/superpowers/specs/*-.md\`,若存在**日期前缀比 carrySpecs 记录的那份更新**的 spec 文件(add-req 重出规格会这样)→ 判**失效**(记录的哈希没变,但作用域真值已换文件)。`, + ` - **校验通过 → 复用**:把 \`run.mjs\` / \`sentinel.sql\` 复制进 \`${tmpDir}/\` 使用,**跳过 step1 / step4 的重新推导**(期望以 \`expectations.md\` 为准),直接进 step2 起栈。在证据报告里注明「carry 复用(来自上一次 run),校验项:scope 相等 + specs 哈希一致 + 无更新版本 spec」。`, + ` - **判失效 → 完整推导**:先删除并重建 \`${carryDir}/\`(清掉过期派生物,防新旧混用),再按 step1/step4 从头推导,跑完按下方格式落盘。`, + ' - **判不准(读不到、格式怪、哈希取不到)一律按失效处理**:多花约 18 分钟重推,比拿过期期望跑出一份看似 green 的假证据便宜得多。', + '', + `- **派生物落盘(首次推导 / 判失效重推后必须做)**:把 ① 推导结果写成 \`${carryDir}/expectations.md\`、② runner 写一份到 \`${carryDir}/run.mjs\`、③ sentinel 种子写一份到 \`${carryDir}/sentinel.sql\`。`, + ` - \`expectations.md\` **头两行必须逐字按以下格式**(下一次 run 的校验全靠它,写错=下次白白重推 18 分钟):`, + ' ```', + ` - carryScope: ${(feItems || []).join(', ')}`, + ' - carrySpecs: =<短哈希> =<短哈希> ...', + ' ```', + ` 其中短哈希 = \`git -C ${c.root} log -1 --format=%h -- <该 FE 采用的 spec 文件路径>\`(同一 FE 有多份规格时取**最新日期**那份,与 step1 取作用域用的是同一份)。哈希取不到(spec 未提交)→ 该项写 \`none\`,这会让下次校验必然失效并重推,是预期行为。`, + ' - 正文接着写:清单内 FE 的作用域并集 / 逐路由预期控件与文字来源 / 每路由所需角色 / sentinel 值表 / route→组件文件反查表。', + ` - 后续 behaviorRound / attempt **以及下一次 coding.mjs run** 都会复用这三份跳过重新推导,省掉每次十几分钟的重复侦查。carry 与本轮 \`${tmpDir}/\` 同属临时区、**不提交**。`, + ].join('\n'), `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本门起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, // runner 的 status 必须是**算出来的**。曾实际发生:生成的 run.mjs 把 status 初始化为 'red' // 后再未赋值,恒红且不带任何分类信号——若子代理照单全收,上层 runBehaviorGate 的 @@ -1041,8 +1102,10 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { ' - 根因不可归到 `frontend/` 源码(依赖 / 工具链 / 无法定位)→ `envError.kind="build-failed"`(如能定位仍填 `rootCausePath`)。', ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。', '', - `## step1 路由真值发现(覆盖率分母 = 全部 FE 作用域路由并集)${reuse ? ' —— **本次复用 carry,整节跳过**' : ''}`, - reuse ? `- 本次已从 \`${carryDir}/expectations.md\` 取得分母与逐路由归属,**不要**重新逐 FE Read spec、不要重新对账 router(那是上一次已做完的确定性推导,重做只是重复烧时间)。仅当 carry 缺失/不可用时才回退执行本节。` : '', + `## step1 路由真值发现(覆盖率分母 = 本次清单内 FE 的作用域路由并集)${mustReuse ? ' —— **本次复用 carry,整节跳过**' : ' —— **carry 校验通过则整节跳过**'}`, + mustReuse + ? `- 本次已从 \`${carryDir}/expectations.md\` 取得分母与逐路由归属,**不要**重新逐 FE Read spec、不要重新对账 router(那是上一次已做完的确定性推导,重做只是重复烧时间)。仅当 carry 缺失/不可用时才回退执行本节。` + : `- **carry 校验通过时整节跳过**:分母与逐路由归属直接取自 \`${carryDir}/expectations.md\`,**不要**重新逐 FE Read spec、不要重新对账 router。**判失效时才执行本节**(下面的内容即完整推导流程),跑完按「派生物落盘」写回 carry。`, '- 分母来源 = **本次清单内每个 FE** 的 spec `## 行为验收作用域` 小节 `关联路由:` 清单的**并集(去重)**;`routesPlanned` = 并集路由数。逐路由标注归属 FE(证据分小节与硬问题归因用)。', `- 与 \`${c.root}/frontend/\` router 配置对账:清单内 FE 的作用域声明但 router 缺失的路由 → \`coverageGaps[reason="unreachable-no-route"]\`;router 声明但不属清单内任何 FE 作用域的路由记证据(不入分母、不断言)——**增量重跑时往轮已验收 FE 的路由就落在这一类,属正常,不是缺口**。`, '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**;每路由标注所需登录角色。', @@ -1052,7 +1115,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { `1) \`node ${c.root}/scripts/setup-test-db.mjs\`(复制源库→一次性副本;源库绝不被 drop)。建副本前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`, '2) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此把本轮新迁移 apply 到副本,历史里已有的自动跳过);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', `3) **注入演示种子**:\`node ${c.root}/scripts/seed-demo-data.mjs\`(幂等账本 \`_demo_seed_history\` 自动跳过源库已灌过的文件,把新 \`sql/seed/*.sql\` 演示数据灌进副本)。失败 → \`envError.kind="seed-error"\` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / 引用序错 / 类型截断 / schema 未初始化),**不**混进交互 RED。`, - `4) **此时才跑 sentinel 种子**:${reuse ? `直接灌 \`${carryDir}/sentinel.sql\`(上次已派生好,**不要**重新按 docs/03 推导——库副本每次重建,数据当然要重灌,但 SQL 本身不必重写)` : '按 `docs/03-数据库设计文档.md` 派生 **按语义引用有序的 INSERT** sentinel 种子(先被引用方后引用方;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)'}。失败 → \`envError.kind="seed-error"\` + 结构化根因,**不**混进交互 RED。`, + `4) **此时才跑 sentinel 种子**:${mustReuse ? `直接灌 \`${carryDir}/sentinel.sql\`(上次已派生好,**不要**重新按 docs/03 推导——库副本每次重建,数据当然要重灌,但 SQL 本身不必重写)` : `carry 校验通过 → 直接灌 \`${carryDir}/sentinel.sql\`(**不要**重新按 docs/03 推导;库副本每次重建、数据当然要重灌,但 SQL 本身不必重写);判失效 → 按 \`${c.root}/docs/03-数据库设计文档.md\` 派生 **按语义引用有序的 INSERT** sentinel 种子(先被引用方后引用方;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)`}。失败 → \`envError.kind="seed-error"\` + 结构化根因,**不**混进交互 RED。`, ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——数值主键**一律 ≥100000**(固定区间,不再动态扫描既有键:初始数据 1–999 / 演示种子 1000–9999 已由区间约定隔离,sentinel 落 ≥100000 天然不冲突);字符串列**仍逐字段唯一编码**(`_S` 样式,如 `CUST_NAME_S001`,抓绑错字段——演示数据已被禁用该样式,故 sentinel 独占)+ 行序号保 UNIQUE;enum 列从 docs/03 值域取并标注。断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。', '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', `- \`finally\` **硬要求 kill 本门起的全部子进程**,**并跑 \`node ${c.root}/scripts/drop-test-db.mjs\` 删副本**(无论成败;源库不动);端口 + pid 写入 \`envError.ports\` / \`envError.pids\`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。`, @@ -1069,8 +1132,10 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。', '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。', '', - `## step4 推导期望${reuse ? ' —— **本次复用 carry,整节跳过**' : ''}`, - reuse ? `- 期望以 \`${carryDir}/expectations.md\` 为准(含每控件预期效果、每文字区域预期内容与来源、sentinel 值表)。**不要**重新读 prototype/REQ/docs05 推导——上一轮 fix 只改了 \`frontend/\` 实现,不改这些期望的来源。` : '', + `## step4 推导期望${mustReuse ? ' —— **本次复用 carry,整节跳过**' : ' —— **carry 校验通过则整节跳过**'}`, + mustReuse + ? `- 期望以 \`${carryDir}/expectations.md\` 为准(含每控件预期效果、每文字区域预期内容与来源、sentinel 值表)。**不要**重新读 prototype/REQ/docs05 推导——上一轮 fix 只改了 \`frontend/\` 实现,不改这些期望的来源。` + : `- **carry 校验通过时整节跳过**:期望以 \`${carryDir}/expectations.md\` 为准(含每控件预期效果、每文字区域预期内容与来源、sentinel 值表)。**不要**重新读 prototype/REQ/docs05 推导——两次 run 之间只有 \`frontend/\` 源码 fix,不改这些期望的来源。**判失效时才执行本节**,跑完写回 carry。`, '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。', '', '## step5 断言(三层:交互/文字/样式 + 可观测效果白名单 + 硬问题带源码 locator)', @@ -1384,6 +1449,14 @@ function adjudicatePromptM(site, context, root) { '- `continue`:缺陷不阻断正确性、可安全前进(reviewer 的非必须建议 / 可降级为口头建议的 issue / 纯可视化副作用缺失 / 已可由后续 verify / test-gate 兜底的疑虑)。在 `rationale` 说明为何安全。', '- `halt`:确属不可恢复——结构性缺失且无任何旁证、git 树冲突需人工、继续会污染源码 / 伪造业务语义。在 `rationale` 写清人工需要做什么。', '- 若上下文含 `"allowContinue": false`,**不得**选 continue(如红色测试不可跳过),只在 retry / halt 间选。', + '', + // 仲裁只套 microStepContract,拿不到 featureStageContract 里的「测试执行子会话」授权。2026-06-13 因此 + // 判过 halt(BAC-DeliverMode-Query:仲裁自己 ToolSearch 复核确认无 Task 工具 → 认定结构性能力缺口 → + // 要求人工注入工具)。stage 侧已在 42b6ed8 授权,但仲裁侧一直是空的——把同一口径同步过来堵住复发。 + '## 已知非阻塞情形(**绝不**因下列理由判 halt)', + '- **「本环境没有 Agent/Task 子会话派发工具,无法满足『派子会话跑测试』护栏」**:这是本项目运行环境的**常态**,不是结构性能力缺口。护栏的真实意图是「测试执行与主推理流隔离、只回收结构化结果」,用 `Bash` 的 `run_in_background` detached 子进程 + 日志重定向 + 只读回结构化摘要**已经满足**,且该做法已在 stage 侧硬约束里明确授权。', + ' - 上游若以此为由请求裁决 → 判 `retry`,`guidance` 写:「按 stage 硬约束「测试执行子会话的物理实现」条执行——用 Bash run_in_background 派 detached 子进程跑测试、输出重定向到 .tmp/ 日志、只读回 {command, exit_code, passed, failed} 结构化摘要;不要再为工具面缺失升级或 halt」。', + ' - **绝不**在 `rationale` 里要求人工"为子代理注入 Agent/Task 派发工具"——retry 不会改变工具面,这只会把流程停在一个永远等不到的前提上。', '## 输出(ADJUDICATE_SCHEMA)', '- `{ "action": "retry|continue|halt", "guidance": "", "rationale": "<裁决理由>" }`', ].join('\n') @@ -3120,6 +3193,17 @@ const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed' // 终波恒宽 1、主根执行(c.lane==null);静态渲染无 DB、无需 ERP_TEST_DB_SCHEMA 注入。 async function runPrototypePreview(feItems, c) { const lbl = (a) => `preview:proto:a${a}` + + // step 0(省时改造):prototype/ 自上次渲染证据以来没变过 → 整门跳过。 + // 本门原先无条件跑:一次增量里 resume 三次就渲染三次同一份原型(每次 3–7 分钟)。 + // 判据是确定性的 git 路径限定 tip 比对,判不准返回 false(宁可多跑,不可用过期基线)。 + const fresh = await agentR(prototypePreviewStatePromptM(c), + {label: 'preview:proto:state?', phase: 'Preview', schema: EXISTS_SCHEMA}) + if (fresh.exists) { + log('preview: prototype/ 自上次渲染证据以来未变,跳过静态原型渲染门(截图基线沿用)') + return + } + let attempt = 1 let pv = await agentR(prototypePreviewPrompt(feItems, attempt, c), {label: lbl(attempt), phase: 'Preview', schema: PROTOTYPE_PREVIEW_SCHEMA})