oh-my-openagent ulw-loop 未提交 Diff 抢救实录:CLI 批量 Steering 契约与 Validation-Batch 关闭语义的双缺陷修复
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
本篇技术复盘基于 oh-my-openagent(OmO)仓库中.omo/evidence/20260721-ulw-loop-gajae-adoption/的证据目录,完整还原了omo-codex插件下ulw-loop组件一次"未提交变更抢救"(uncommitted diff salvage)的全过程:一个丢失的 worker 在 17 个文件里留下未提交的 tracked changes,团队逐 hunk 审查后保留三块有效内容,其中两块最终演化为真实缺陷修复——cli-steering.ts的批量 steeringsource默认值契约,以及checkpoint.ts/validation-batch.ts的 validation-batch 关闭语义。文章将结合 cli-steering.ts、validation-batch.ts、checkpoint.ts 等源码,讲清每个修复的根因、红绿测试流程、错误码语义与最终验证矩阵。读完你将掌握:如何安全地从遗留 diff 中筛选范围外内容、如何用 TDD 固化两个并发语义缺陷、以及 ulw-loop 组件 checkpoint / steering / validation-batch 三条核心链路的底层实现。
一、事件背景:为什么会有"需要抢救的未提交 diff"
该事件发生在 ulw-loop gajae-adoption 计划的 todo-6(真实表面 QA 与全量门禁)执行期间。根据 README.md 的记载,该计划共 7 项验收内容:checkpoint 自动推进(auto-advance)、原子化steer --proposals-json、validation-batch 模式、validation-batch 与 checkpoint/steering 的强制约束、docs/help 同步、全量门禁与内置 CLI QA、以及最终验证波次 F1–F4。前 5 项由历史提交完成,todo-6 从持久状态恢复执行时,发现丢失的 worker 在 17 个仅涉及 ulw-loop 的文件中留下了未提交的 tracked changes。
抢救决策的核心纪律记录在原文档 Status 一节:
The lost worker left tracked changes in 17 ulw-loop-only files. Every hunk was inspected before commit. No out-of-scope tracked path from that diff was kept.
即:逐 hunk 人工审查,凡不属于 ulw-loop 范围的 tracked path 一律不保留。这是一次典型的"范围纪律"演练——遗留 diff 中往往混有格式化噪音、无关重建产物与真实缺陷修复,抢救者必须逐块分类,而不是整包提交。
二、抢救出的三类有效内容(Kept hunks)
原文档将保留内容分为三类,下面逐一结合源码展开。
2.1 格式与导入顺序清理:biome-ignore-all format与 import ordering
第一类保留内容是 ulw-loop 源码与测试中的格式/导入顺序修复:在已经处于"紧凑/行数预算受限"(compact/line-budget-constrained)的文件中补充biome-ignore-all format注释并调整 import 顺序。这类修改由npm run check(即biome check .无修复通过)与组件测试双重验证。
为什么 ulw-loop 会有"行数预算"约束?从源码可以直观看到:cli-steering.ts、validation-batch.ts、checkpoint.ts、steering-batch.ts的文件首行都带有// biome-ignore-all format: ...注释,例如 cli-steering.ts 第 1 行 注明"keep this module under the mandated pure LOC budget"。这意味着这些模块受"纯逻辑行数上限"约束,无法按 biome 默认格式排版,必须通过 ignore 注释豁免,同时保持 import 顺序稳定。抢救中把这些注释与排序恢复到与既有约定一致,属于低风险但必要的一致性维护。
2.2 修复一:批量 CLI Steering 的source默认值契约
这是第一个真实缺陷,由 todo-6 的 real-surface QA 发现:当--proposals-json中的某个条目缺失source字段时,会报ULW_LOOP_STEERING_SOURCE_INVALID错误。根因与修复契约如下。
source的合法取值定义在 constants.ts 第 31 行:
export type UlwLoopSteeringSource = "user_prompt_submit" | "finding" | "cli";在单条 steering 的解析路径中,parseSteeringSource 早已实现了"缺省即 cli"的语义:
export function parseSteeringSource(argv: readonly string[]): UlwLoopSteeringSource { const value = readValue(argv, "--source"); if (value === undefined) return "cli"; return isSource(value) ? value : fail(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES }); }而批量路径--proposals-json在 proposalFromObject 中虽然也写了const source = readObject(value, "source") ?? "cli";,但在 todo-6 的这批遗留改动之前,该默认值并未真正生效——QA 实测一个不含source的条目直接以ULW_LOOP_STEERING_SOURCE_INVALID失败。修复方式是让批量条目先经过normalizeSteeringProposal({ source: "cli", ...rawElement })契约归一化,确保每个元素在进入后续校验前都已具备确定的source。
normalizeSteeringProposal(cli-steering.ts 第 98-104 行)负责把 proposal 的所有文本字段统一 trim 并剔除空串、按需附加goalId/targetGoalId/criterionId/childGoals/pendingOrder等字段,是批量与单条两条路径共享的"归一化闸门"。批量入口 parseSteeringProposals 的逻辑要点如下:
- 未传
--proposals-json时退化为单条解析(parseSteeringProposal); --kind与--proposals-json互斥,同时出现报ULW_LOOP_STEERING_BATCH_CONFLICT(见 cli-steering.ts 第 109 行);--proposals-json必须是非空 JSON 数组,否则报ULW_LOOP_STEERING_BATCH_ARRAY_REQUIRED;- 每个条目必须是对象,
kind必须在ULW_LOOP_STEERING_MUTATION_KINDS之内,source必须在SOURCES之内。
ULW_LOOP_STEERING_MUTATION_KINDS定义在 constants.ts 第 20-28 行,共 7 种:
add_subgoal / split_subgoal / reorder_pending / revise_pending_wording / revise_criterion / annotate_ledger / mark_blocked_superseded对应的 CLI 必填参数在 cli-steering.ts 的 STEERING_KIND_HELP 中逐条列出(如add_subgoal需要--title、--objective、--evidence、--rationale;split_subgoal需要--goal-id、--children等),字段缺失会抛出ULW_LOOP_STEERING_FIELD_REQUIRED或ULW_LOOP_STEERING_FIELD_EMPTY,--goal-id缺失则是ULW_LOOP_GOAL_ID_REQUIRED。
批量执行与原子性由 steering-batch.ts 的steerUlwLoopBatch承载:所有提案先在内存中顺序准备(prepareBatch),期间每个提案会先按idempotencyKey ?? promptSignature查 ledger 去重(命中则标记deduped),再执行validateUlwLoopSteeringProposal不变式校验,全部通过后逐个applySteeringMutation。只要有一个提案被拒绝,整批失败,并只在 ledger 写入一条steering_rejected审计(rejectedLedgerEntry按索引汇总所有拒绝原因,见 steering-batch.ts 第 106-109 行);全部通过则以多条steering_accepted(revise_criterion用criteria_revised)审计落盘,必要时附加一条batch_updated。这种"全有或全无"的设计正是"原子化 steering 批次"的底层保证。
2.3 修复二:aggregate-final checkpoint 关闭 validation-batch 的语义
第二个真实缺陷同样来自 todo-6 生命周期实测:当某个 goal(证据记录中的G003)**同时是 validation-batch 的最终成员(batch-final)和整个计划的最终成员(run-final)**时,checkpoint 会在考虑当前这次完成之前就抛出ULW_LOOP_VALIDATION_BATCH_OPEN,导致合法的最终完成被误拒绝。修复落在 checkpoint.ts 与 validation-batch.ts 两处。
先看 validation-batch 的 schema 与约束。--validation-batch-json的解析与校验在 parseValidationBatches 与 validateBatches:
- 每个 batch 必须包含
batchId、至少两个memberIds、finalGoalId; finalGoalId必须是成员之一(否则ULW_LOOP_VALIDATION_BATCH_FINAL_NOT_MEMBER);- 成员必须是计划中已存在的 goal(
ULW_LOOP_VALIDATION_BATCH_MEMBER_UNKNOWN); - 同一 goal 不得出现在多个 batch(
ULW_LOOP_VALIDATION_BATCH_OVERLAP); - batchId 与 memberIds 均不得重复。
关闭语义相关的关键函数有三个:
- batchClosedBy:判断某个 goal 是否为某 batch 的 final 成员;
- requireBatchFinalReady:batch 最终成员 checkpoint 时,若其他成员尚未 resolve,抛
ULW_LOOP_VALIDATION_BATCH_OPEN(fail-closed); - requireAllValidationBatchesClosed:计划级关闭检查,其签名带
closingGoalId?: string——正在完成的 goal 会被排除在"未关闭"判断之外,这正是本次修复的核心:此前未把"当前完成即 batch-final"这一情况纳入排除,导致G003同时充当 batch-final 与 run-final 时先被ULW_LOOP_VALIDATION_BATCH_OPEN卡住。
checkpoint 侧的完整完成分支(checkpoint.ts 第 171-226 行)如今呈现为:
const aggregate = codexGoalMode(plan) === "aggregate"; const final = isFinalRunCompletionCandidate(plan, goal); const closesBatch = batchClosedBy(plan, goal.id) !== undefined; if (final) { requireAllCriteriaPass(goal); requireAllPlanCriteriaPass(plan); requireAllValidationBatchesClosed(plan, goal.id); // 排除当前 goal } else if (aggregate) requireEssentialCriteriaPass(goal); else requireAllCriteriaPass(goal); // ... if (closesBatch) requireBatchFinalReady(plan, goal); if (closesBatch && args.qualityGateJson === undefined) throw new UlwLoopError("Validation batch final checkpoint requires --quality-gate-json.", "ULW_LOOP_VALIDATION_BATCH_GATE_REQUIRED"); // ... requireBatchGate(plan, goal, qualityGate);要点解读:
closesBatch分支:凡是要关闭 batch 的 goal 都必须先过requireBatchFinalReady,即其余成员必须全部 resolve;否则 fail-closed 拒绝。- batch 最终成员必须携带质量门:
--quality-gate-json缺失时报ULW_LOOP_VALIDATION_BATCH_GATE_REQUIRED;requireBatchGate 还会核对所有成员的 success criteria 状态(存在非 pass 项报ULW_LOOP_VALIDATION_BATCH_CRITERIA_PENDING;覆盖率与成员 criteria 不匹配报ULW_LOOP_VALIDATION_BATCH_GATE_MISMATCH)。 - 成功关闭时写入审计:checkpoint 提交时追加一条
batch_closedledger entry(见 checkpoint.ts 第 234-235 行),kind: "batch_closed"也在 constants.ts 的 ledger 事件全集 中登记。
此外,updateBatchesAfterSupersede(validation-batch.ts 第 67-76 行)处理 supersede 场景下的 batch 成员替换:被替换的 goal 位置由 replacementIds 平铺填充,若被替换者恰好是 final 成员,则新 final 取替换列表末位,从而保证 batch 结构在 steering 后依然自洽。
三、范围外副作用的识别与恢复(Discarded/restored side effects)
原文档明确记录了一类必须丢弃并恢复的副作用:bun run test:codex在运行过程中重建了packages/omo-codex/plugin/components/codegraph/dist/cli.js,且使用了与预期不同的 bundle 注释前缀(alternate bundle comment prefixes)。这属于代码图组件的构建产物漂移,与 ulw-loop 的改动范围无关,属于"out of scope"。
处理方式:从 codegraph 组件的包目录(package cwd)重新构建一次该组件,将其恢复为受控状态;最终 tracked status 显示 codegraph 无任何 diff。这一步骤说明,抢救遗留 diff 时不仅要决定"留什么",还要能识别"哪些是测试/构建副作用造成的噪音"并主动还原,避免把无关重建产物混入提交。
四、验证矩阵:红测试 → 聚焦绿测试 → 全量门禁
原文档的 Verification 一节给出了一条完整、可复现的 TDD 验证链,这也是整篇证据最值得复用的部分。
第一步:先跑红(Red),证明两个缺陷真实存在:
npm test -- cli-steering-batch # 失败:批量条目缺失 source npm test -- validation-batch-checkpoint # 失败:batch-final 的 aggregate 完成被误拒绝对应的测试文件即 test/cli-steering-batch.test.ts 与 test/validation-batch-checkpoint.test.ts。
第二步:聚焦绿(Green),修复后只跑相关用例并做类型检查:
npm test -- cli-steering-batch steering-batch && npm run typecheck npm test -- validation-batch-checkpoint checkpoint && npm run typecheck第三步:全量门禁:
npm run check && npm test # biome check + 组件全量测试 bun run test:codex # 仓库根级 Codex 兼容性门禁据 task-6.md 的记录,组件全量测试从抢救前的 40 文件 / 412 测试增长到 reviewer-fix 后的 40 文件 / 418 测试,根级 Codex 门禁 510 个测试 0 失败;此外还构建了dist/cli.js并在隔离的临时 git 仓库中完成了 e2e 生命周期转录(task-6-e2e-transcript.txt)。真实~/.codex/config.toml的 SHA-256 在 QA 前后完全一致(780c740e…61d5),证明全程未污染真实 Codex 配置,所有临时 QA 仓库也均被清理。
五、e2e 生命周期解读:三条特性在同一计划上的交汇
task-6-e2e-transcript.txt 用一段可复现的转录完整演示了"checkpoint 自动推进 + 原子化 steering 批次 + validation-batch 关闭"在同一计划上的串行交汇:
$ create-goals --validation-batch-json VB001 created goals=3 batch=VB001 version=1 $ checkpoint G001 -> complete(自动推进) checkpoint=G001-goal-alpha->complete next=G002-goal-beta:in_progress $ steer --proposals-json 2 个不含 source 的条目(CLI 默认 source=cli) steer accepted=true results=2 acceptedItems=2 $ checkpoint G002(非 batch-final 成员)-> complete $ 记录 G003 剩余 criteria 与最终质量门工件(coverage=6/6) $ checkpoint G003 final(batch 关闭 + aggregate 完成) checkpoint=G003-goal-gamma->complete aggregate=complete $ 最终 status:aggregate=complete complete=3/3 criteriaPass=9/9 $ ledger 中出现 batch_closed 行: {"kind":"batch_closed","goalId":"G003-goal-gamma","message":"VB001"}随后在同一临时仓库的隔离会话中验证 fail-closed 场景:故意在 batch 仍有未 resolve 成员时对 final 成员执行 checkpoint,得到:
{"ok": false, "error": {"code": "ULW_LOOP_VALIDATION_BATCH_OPEN", "message": "Validation batch has unresolved members.", "details": {"batchId": "VB001", "open": ["G001-goal-alpha"]}}}这段转录是理解本文两个修复价值的直观入口:G003同时充当 batch-final 与 run-final,正是触发"关闭语义缺陷"的场景;两个--proposals-json条目均未写source,正是触发"默认值契约缺陷"的场景。两处修复合流后,生命周期得以一路绿灯走到aggregate=complete。
六、工程启示:从一次抢救中沉淀的四个原则
- 范围纪律先行:17 个文件逐 hunk 审查、非 ulw-loop 路径一律不保留,是所有后续动作的前提;遗留 diff 里的"格式化噪音 + 构建副作用 + 真实修复"必须分层处理。
- TDD 固化并发语义缺陷:
source默认值与 batch 关闭语义都是"缺省行为/边界条件"类缺陷,最容易在真实 QA 中暴露;先用cli-steering-batch、validation-batch-checkpoint两个用例复现红,再修复转绿,最后跑npm run check && npm test与bun run test:codex全量回归,形成闭环。 - fail-closed 胜过静默通过:
ULW_LOOP_VALIDATION_BATCH_OPEN、ULW_LOOP_VALIDATION_BATCH_GATE_REQUIRED、ULW_LOOP_STEERING_BATCH_CONFLICT等一批带ULW_LOOP_*前缀的类型化错误码,让每一次拒绝都可被机器读取、可被 QA 断言,而不是静默吞掉非法状态。 - 副作用可还原:构建产物漂移(codegraph
dist/cli.js)通过从包目录重建即可还原,且最终 tracked status 必须干净;同时用配置哈希(~/.codex/config.tomlSHA-256 前后一致)证明 QA 过程不污染真实环境。
如需继续深入,可进一步阅读 ulw-loop 组件 README、steering.ts(单条 steering 校验与变异应用)、checkpoint-final.test.ts(final 关闭路径的既有回归覆盖)以及 reviewer-fix-report.md(独立评审阶段的阻塞项修复记录),从测试侧反向验证本文所述两条修复的边界。
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考