Commit 2337cc32d3fc831021cedd5780384c6f98f3c5b2

Authored by yanghl
1 parent d4b9e53d

fix(coding): 行为门 runner 的 status 必须推导得出,禁止带默认值声明

实跑暴露:行为门子代理生成的一次性 run.mjs 把 status 初始化为 'red' 后再未
赋值,恒红且不带任何分类信号。本轮靠子代理自己察觉才没误判,但 r1~r3 同样
失真——任何依赖该字段做判定的地方都会恒判红,且会撞上 runBehaviorGate 的
「red 无分类原因」守卫,被判 HALT behavior-red-unclassified。

根因在插件侧:behaviorGatePrompt 只说 runner 要「透传结构化结果」,从未规定
status 怎么算,生成方式完全放任。

契约补四条:
- status 必须由信号数组末尾一次性推导,判据与「输出」节逐字一致
- 变量初值用 null 或直接末尾 const,禁止 let status = 'red'/'green' 这类带
  默认值的声明(恒红比恒绿更隐蔽,看起来像门在严格把关)
- runner 输出前自检:red 必须有至少一条分类信号能解释;自检不过则非零退出,
  不要输出无法解释的 red
- 子代理返回的 status 必须等于 runner 自检后的值,不得手工改判;不一致是
  runner 的 bug,修 runner 重跑

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showing 1 changed file with 8 additions and 0 deletions
workflows/coding.mjs
@@ -997,6 +997,14 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { @@ -997,6 +997,14 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) {
997 ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子),再新建本轮子目录 \`${tmpDir}/\`。` 997 ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子),再新建本轮子目录 \`${tmpDir}/\`。`
998 : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, 998 : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`,
999 `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本门起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, 999 `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本门起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`,
  1000 + // runner 的 status 必须是**算出来的**。曾实际发生:生成的 run.mjs 把 status 初始化为 'red'
  1001 + // 后再未赋值,恒红且不带任何分类信号——若子代理照单全收,上层 runBehaviorGate 的
  1002 + // 「red 无分类原因」守卫会把它判成 HALT behavior-red-unclassified。故把推导规则写死在契约里。
  1003 + `- **runner 的 \`status\` 必须由信号数组末尾推导,绝不允许"初始化成某个字面量再指望后续覆盖"**:`,
  1004 + ` - 先收集 \`interactionFailures\` / \`textIssues\` / \`styleIssues\` / \`coverageGaps\` / \`envError\` 与覆盖计数;**最后**一次性计算 \`status\`,判据与下方「输出」节的定义逐字一致。`,
  1005 + ` - 变量初值用 \`null\`(或干脆不声明,直接在末尾 \`const status = ...\`)。**绝不**写 \`let status = 'red'\` / \`= 'green'\` 这种带默认值的声明——那样一旦推导分支漏掉赋值,默认值会静默流到输出,而且恒红比恒绿更隐蔽(看起来像"门在严格把关")。`,
  1006 + ` - runner 输出前**自检**:\`status\` 必须 ∈ {green,red};若为 \`red\`,必须至少有一条 \`interactionFailures\` / \`textIssues\` / \`styleIssues\` / \`coverageGaps\` 或一个非 \`none\` 的 \`envError\` 能解释它。自检不过 → runner 以非零码退出并打印诊断,**不要**输出一个无法解释的 red(上层守卫会把它当未分类红灯,走仲裁甚至 halt)。`,
  1007 + ` - 你返回给上层的 \`status\` 必须**等于 runner 自检通过后算出的那个值**,不要另行改判;runner 结果与你的观察不一致时,那是 runner 的 bug,修 runner 重跑,而不是在返回值里手工覆盖。`,
1000 `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`, 1008 `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`,
1001 `- \`${c.root}/.tmp/behavior-gate/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, 1009 `- \`${c.root}/.tmp/behavior-gate/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`,
1002 '', 1010 '',