v4.39.0
[4.39.0] - 2026-09-09
Phase 54「声明 / 求值一致性」。本仓的签名缺陷是「声明了机制,却没有代码求值它」 —— 4.36.0 失效的 eval trap、4.37.0 chrome-devtools 的散文承诺、4.38.0 verify 缺回退边,三次都是事故或审计倒推发现的。本轮把这条病族成建制清了一遍,其中一面有真实行为后果。
立项前提在执行中被实测推翻五条(112→108 计数 / 「14 个悬空事实」/ 断点行形态 / eval git fixture / DRY 抽取),全部显式更正未静默改写 —— 详 .planning/phases/54-declaration-evaluation-parity/SPEC.md。
Added
verify/second-opinion—— 跨模型第二意见第一次成为会被求值的编排单元(Phase 54 T2+T3)。capabilities.yaml里的codex能力(第二意见 / cross-AI peer review)此前被锁在一条死fires_when后面,而它引用的phase.requires_second_opinion全仓只出现那一次 —— 连事实都没声明过。- 判据机械派生,不是判断题:
harnessed facts取git diff --name-only <上一个 release tag>与 ORCHESTRATION SURFACE(workflows/judgments/·capabilities.yaml·role-prompts·disciplines/·defaults.yaml·phaseFactContext.ts·judgmentResolver.ts·exprBuilder.ts·facts.ts·ledger.ts· 任一workflows/**/SKILL*.md)的交集。集合从代码推导,不是从样本拟合 —— 早先草案用的是「最近三次发版恰好碰过的四条路径」,那是在缺陷被发现的地方过拟合,既误报(一行 ADR 拼写修正命中)又漏报(200 行改注入语义不命中),而且把capabilities.yaml/facts.ts这两个缺陷出生地排除在外。 - 基准是 release tag,不是
merge-base origin/main:后者在 commit-即-push-main 的纪律下塌缩成 HEAD,只看得见未提交改动,会漏掉多-commit milestone 的早期 commit。 - 条件性交付要求 = 条件性 fire 的 sub(零新机制)。
artifacts_expected是 phase 级静态列表,直接声明会让第二意见记录无条件必交、判据没命中也卡住,--force随即变成肌肉记忆。改为新 leafworkflows/verify/second-opinion/(serial order 90,在 code-review/paranoid 之后、simplify 之前),由delegates_to.gate控制它跑不跑;跑了就必交second-opinion.md(evidence guard 已 fail-closed),没跑则 ledger 记status: skipped+ 原因。跳过从隐形变成台账上一行。 requires_second_opinion为 root-flat 裸标识符(sisterchrome_devtools_available),buildDefaultGateContext种false—— 与 chrome-devtools 种true方向相反:那边未知不可删能力,这边未知不可加活。算不出来时harnessed facts的source直接写「criterion unavailable — <原因>」。
- 判据机械派生,不是判断题:
Changed
capabilities.yaml的fires_when改名routing_note,并从 schema 删掉旧名(Phase 54 T1)。judgment 文件里的fires_when是真求值的(judgmentResolver读triggers/rules);capabilities.yaml里同名的 108 条没有任何消费者 —— gate 引擎不读它,prompt.ts:72-96只渲染cmd/impl/aliases。同名异义就是误读的根源:读的人(包括未来的实施 subagent)会把它当生效路由,而项目 SPEC 检查表的 grep 恰恰会放行它们(检查表验「存在」,不验「被消费」)。- 改名把 108 条一次性变得自明,并让
additionalProperties: false承担守卫:写回fires_when直接是构建期错误,不是静默无效。门从「维护一张百条豁免表」缩成「schema 就是门」。 - 两处 schema 同步(
src/workflow/schema/capabilities.ts+scripts/check-workflow-schema.mjs的镜像);该.mjs里另一处fires_when: Type.String()是 judgment trigger 的真字段,未动。
- 改名把 108 条一次性变得自明,并让
Fixed
- 21 处 per-phase 迭代上限第一次真的生效(Phase 54 T0)。
workflows/**/workflow.yaml的phases[].max_iterations全部写成{{ defaults.ralph_max_iterations.<workflow>.<phase> }},而没有任何一处被解析过:loadPhases的插值只覆盖ph.invokes(实测 v3 下该字段恒为undefined,分支从不触发),且interpolate的STRICT = /\{\{\s*(\w+)\s*\}\}/g不认 dot-path、残留即抛错 —— 接进去只会把 21 处全变成异常。21 处以字面串抵达resolveMaxIterations,parseInt得 NaN,统统落回RALPH_DEFAULT_MAX_ITER = 20。声明 5 的 phase 实跑 20,四倍于写下的意图。- 修法不是补模板层,而是删掉 yaml 里的字段,让值只走
workflows/defaults.yaml查表 —— 与resolveAttemptBudget同一条已知能工作的路径,单一数据源。resolveMaxIterations新增可选第三参数,优先级 CLI flag > yaml 字面量(向后兼容手写数字)> defaults 查表 > 20。 - 边界:
checkpoint的 attempt budget(budget.ts按 sub 直读 defaults)一直是好的;坏的是 run 引擎的 per-phase 上限。两个不同的天花板,不要混。 - 顺带修 7 处键名漂移:5 处 workflow 的 phase id 与 defaults 键早已分家(如
verify-simplify的01-simplifyvs defaults 的01-code-simplifier);research的单个01-fan-out前缀键展开成 5 个真实 lane id(精确匹配下前缀键匹配不到任何 phase);verify-progress.03-persist对齐为真实的03-progress-update。 - 新增
tests/workflow/template-ref-resolvable.test.ts:defaults 表里的每个<workflow>.<phase>必须对应一个真实存在的 phase,且不得有 workflow.yaml 再写回不可解析的{{ defaults… }}。零正则抓源码 —— 直接读两个 yaml 拿真数据。
- 修法不是补模板层,而是删掉 yaml 里的字段,让值只走