oh-my-openagent 的 work-with-pr 执行计划:以 atlas 钩子 worktree_path 崩溃修复为例,拆解 Agent 全生命周期 PR 工作流
本文以 oh-my-openagent 仓库中由 work-with-pr 技能生成的一份真实执行计划 execution-plan.md 为主体,完整还原“从隔离 worktree 建立、根因修复、测试覆盖、本地验证,到原子提交、PR 创建、多门禁验证循环、合并与清理”的端到端 PR 交付流程,并结合当前仓库源码(boulder-state 包与 omo-opencode 的 atlas 钩子)说明该计划针对的缺陷为何成立、以及计划中各步骤与仓库实际实现之间的对应关系。读完本文,你可以掌握一种把单点缺陷修复组织为可审计、可回滚、门禁驱动的 Agent 交付方案的方法,并理解 readBoulderState() 这类 JSON 状态读取函数中的运行时类型隐患。
一、这份执行计划是什么
execution-plan.md 是一份具体的任务执行计划,标题为 “Execution Plan — Fix atlas hook crash on missing worktree_path”,即“修复 atlas 钩子在缺少 worktree_path 字段时崩溃”的完整实施蓝图。它存放在技能评估工作区 work-with-pr-workspace 的 iteration-1/eval-2/with_skill/outputs/ 目录下,与 code-changes.md、pr-description.md、verification-strategy.md 等产出物并列,共同构成一次 work-with-pr 技能评估运行的完整记录(同级目录还有 without_skill/ 对照组、grading.json 与 timing.json)。
该计划是 work-with-pr 技能定义 的一次具体实例化:技能文档定义了 Phase 0~4 的通用生命周期(Setup → Implement → PR Creation → Verify Loop → Merge),而这份执行计划则把它落到一个具体缺陷上——atlas 会话空闲钩子(session.idle)在处理 boulder.json 状态文件时,一旦 worktree_path 字段缺失或为 null 就可能崩溃。
二、缺陷背景:readBoulderState() 的运行时类型隐患
计划将根因定位在 readBoulderState():它对 boulder.json 执行原始 JSON.parse 后直接 as BoulderState 类型断言,运行时类型违约因此被放行。这一点可以与当前仓库源码相互印证:
- 当前实现位于 read-state.ts,函数签名为
readBoulderState(directory: string): BoulderState | null。其核心逻辑是:
const content = readFileSync(filePath, "utf-8")
const parsed = JSON.parse(content)
// 仅校验 parsed 是非空对象,随后:
normalizeState(parsed)
const state = parsed as BoulderState // 直接断言,字段级校验有限
可以看到,normalizeState() 只规范化了 session_ids、session_origins、task_sessions、works 等会话类字段(见 read-state.ts),对 worktree_path 这一字段并没有做 string | undefined 的强制约束。
-
而类型契约明确要求可选字符串:在 types.ts 中,
BoulderState、BoulderWorkState等结构体均以worktree_path?: string声明该字段(types.ts)。也就是说,契约允许“不存在”(undefined),但下游代码若假定“存在即字符串”,null或其他类型就会穿透as BoulderState断言流入后续逻辑。 -
下游消费方是 atlas 钩子。idle-event.ts 中的
handleAtlasSessionIdle()在会话空闲时解析活跃 boulder 会话并驱动续跑逻辑;idle-continuation.ts 则直接读取currentBoulder.worktree_path传入续跑上下文。一旦该值在运行时是null,注入续跑提示词时就会出现计划中所说的[Worktree: null]之类的脏数据乃至崩溃路径。
路径映射说明:执行计划成文时使用的相对路径为
src/features/boulder-state/storage.ts与src/hooks/atlas/idle-event.ts;从当前源码结构看,仓库已进行 monorepo 化重组,同一逻辑现分别位于packages/boulder-state/src/storage/read-state.ts与packages/omo-opencode/src/hooks/atlas/idle-event.ts(后者仍保留idle-event.ts文件名与scheduleRetry调用链,idle-event.ts)。理解这一映射关系,才能把计划中的修复步骤落到当前代码上。
三、Phase 0:建立隔离 worktree
计划的第一步不是写代码,而是建立隔离的执行环境:
# 1. 从 origin/dev 建立 worktree
git fetch origin dev
git worktree add ../omo-wt/fix-atlas-worktree-path-crash origin/dev
# 2. 在 worktree 内创建特性分支
cd ../omo-wt/fix-atlas-worktree-path-crash
git checkout -b fix/atlas-worktree-path-crash
这里的两个细节与 work-with-pr 技能定义 的设计原则完全一致:
- worktree 放在仓库的同级目录(
../omo-wt/)而非仓库内部——避免 git 嵌套仓库问题,同时保证用户主工作目录中的未提交改动不受污染; - “一 PR 一 worktree”的隔离模型——主工作目录被视为只读上下文,分支切换可能摧毁其中的在途工作;隔离也使多个独立 PR 可以并行构建互不干扰。
值得注意的是,计划从 origin/dev 而非本地分支建立基线,这与技能中 BASE_BRANCH="dev" 且 “CI blocks PRs to master” 的约束呼应:所有 PR 一律以 dev 为基。
四、Phase 1:五步实施(实现、防护、测试、验证、提交)
这是计划的主体,共五步,层次清晰:先修根因,再补防御,然后补测试,接着本地验证,最后原子提交。
Step 1:在 readBoulderState() 中净化 worktree_path
- 在 JSON 解析之后对
worktree_path做净化(sanitize); - 确保
worktree_path的运行时形态只能是string | undefined,绝不接受null或其他类型; - 计划明确指出这是根因修复:原始
JSON.parse+as BoulderState断言允许类型违约在运行时存活。
对照当前 read-state.ts 的实现,这正是 normalizeState() 应当承担、但当前尚未覆盖 worktree_path 的字段级规范化——把“拒绝非字符串值”的逻辑放入这里,与现有 normalizeSessionFields()、normalizeWorkSessionFields() 的写法保持一致,是最贴合当前代码风格的落点。
Step 2:在 idle-event.ts 中加防御性守卫
- 在把
boulderState.worktree_path交给injectContinuation之前,先验证它是字符串; - 在
scheduleRetry回调中施加同样的守卫; - 目标是形成纵深防御:即使
readBoulderState被绕过(例如状态来自其他写入路径),空闲事件处理器也不会崩溃。
这与当前 idle-event.ts 的结构吻合:handleAtlasSessionIdle() 在多个前置条件(后台任务运行中、续跑冷却期、停滞检测等)不满足时调用 scheduleRetry({ ctx, sessionID, sessionState, options })(idle-event.ts)——守卫需要同时覆盖“立即续跑”和“延迟重试”两条路径,这正是计划强调 scheduleRetry 回调的原因。
Step 3:测试覆盖(given/when/then 风格)
计划要求新增三类测试,且遵循既有测试模式:
| 测试场景 | 断言目标 |
|---|---|
boulder.json 中不存在 worktree_path 字段 |
session.idle 正常处理,不崩溃 |
boulder.json 中 worktree_path: null |
session.idle 正常工作,且提示词中不出现 [Worktree: null] |
readBoulderState 的净化行为 |
将 null 的 worktree_path 归一化为 undefined |
当前仓库中 atlas 钩子的测试组织方式印证了这种“按事件切片”的模式:idle-continuation.test.ts、boulder-continuation-injector.test.ts、index.test.ts 等测试文件与各自的实现文件一一对应;boulder-state 包侧则有 read-state.test.ts 专门覆盖 readBoulderState 的解析与归一化行为。计划中指定的 src/hooks/atlas/index.test.ts 在当前布局下对应 index.test.ts,字段净化断言则自然落在 read-state.test.ts 的领域内。
Step 4:本地验证(与 CI 同构的前置检查)
bun run typecheck
bun test src/hooks/atlas/
bun test src/features/boulder-state/
bun run build
这四条命令复刻了 CI 将执行的检查,并按受影响模块收窄测试范围(只跑 src/hooks/atlas/ 与 src/features/boulder-state/ 两个相关目录)。技能定义中将其定位为“约 3–5 分钟 CI 往返的廉价预过滤”,而非验证手段本身——它的作用是避免把明显失败推上远端。
Step 5:原子提交
git add src/features/boulder-state/storage.ts src/hooks/atlas/idle-event.ts src/hooks/atlas/index.test.ts
git commit -m "fix(atlas): prevent crash when boulder.json missing worktree_path field
readBoulderState() performs unsafe cast of parsed JSON as BoulderState.
When worktree_path is absent or null in boulder.json, downstream code
in idle-event.ts could receive null where string|undefined is expected.
- Sanitize worktree_path in readBoulderState (reject non-string values)
- Add defensive typeof check in idle-event before passing to continuation
- Add test coverage for missing and null worktree_path scenarios"
这条提交信息本身就是可复用范本:标题行一句话概括修复,正文解释根因(不安全断言 + 字段可为 null),再用 bullet 列出三个变更面。3 个文件对应“根因修复 + 防御守卫 + 测试”三条 bullet,符合技能中“每个提交配对实现与测试”的原子提交策略——CI 失败时可单独隔离一个逻辑单元而不必整体回退。
五、Phase 2:PR 创建
git push -u origin fix/atlas-worktree-path-crash
gh pr create \
--base dev \
--title "fix(atlas): prevent crash when boulder.json missing worktree_path" \
--body-file /tmp/pull-request-atlas-worktree-fix.md
三个要点:
--base dev与 Phase 0 的基线选择保持一致;- PR 正文通过
--body-file从预写的文件载入(对应同目录产出物 pr-description.md),避免在命令内嵌大段文本; - 技能定义要求 PR 正文面向未跟进实现过程的英文审阅者,按审阅相关区域(而非文件)组织变更,并让 QA 证据可审计——“测试全绿”不构成完成判据,到达 OpenCode/Codex 运行面的改动必须有落到磁盘的实机证据。
六、Phase 3:无上限的验证循环(三道门禁)
- Gate A (CI):gh pr checks --watch — 等待所有检查变绿
- Gate B (review-work):运行 5 智能体评审(Oracle goal、Oracle quality、Oracle security、QA execution、context mining)
- Gate C (Cubic):等待 cubic-dev-ai[bot] 回复 "No issues found"
- 任一门禁失败:fix-commit-push,重新进入验证循环
这体现了 work-with-pr 技能的核心机制——循环没有迭代上限,任何失败门禁都会把执行路由回 Phase 1 的修复轨道,修复须遵守同样的范围纪律(只修门禁指出的问题、行为变化则补新 QA 证据、原子提交、重新从门禁 A 开始全量验证)。这份执行计划相比技能通用定义还多了一道 Gate B(5 智能体评审),说明门禁组合可按任务风险裁剪,但“门禁失败即回炉、不得绕过”的不变量保持不变。
七、Phase 4:合并与清理
gh pr merge --squash --delete-branch
git worktree remove ../omo-wt/fix-atlas-worktree-path-crash
合并完成后立即移除 worktree,防止磁盘膨胀;技能定义还要求在删除前把 worktree 内生成的 .omo/ 状态(任务状态、计划、notepad)同步回主仓库,因为 .omo/ 通常被 gitignore,其中的文件不会随合并带回。需要说明的差异点:当前 SKILL.md 明确写着“本仓库要求 merge commit,永不使用 --squash 或 --rebase”,而这份较早的执行计划使用的是 --squash——从文档演进看,合并策略在技能迭代中被收紧为 gh pr merge --merge 路线,执行计划反映的是当时的版本约定。
八、这份执行计划的可迁移价值
把 execution-plan.md 放回 work-with-pr 技能 的坐标系里看,它展示了这套工作流的几个关键工程决策:
- 隔离先行:任何实现都发生在任务专属 worktree 中,主工作目录只作为只读上下文,为并行多 PR 与失败恢复留出了空间(技能规定失败时不得删除 worktree,保留人工接管入口);
- 根因与防御分层:Step 1 修状态读取层的根因,Step 2 在消费层补守卫,两层互为冗余,单点失效不致崩溃;
- 测试与实现一一对应:三个测试场景分别锁定“缺失”“null”“归一化”三种运行时形态,与根因分析中的类型违约面精确对齐;
- 门禁驱动的收敛:CI、多智能体评审、外部 bot(Cubic)构成多源交叉验证,失败即回炉的无上限循环保证“交付”等价于“所有活动门禁同时通过”。
如果你在当前仓库中遇到类似的 JSON 状态文件类型违约问题,这份计划给出的排查路径依然有效:先定位 JSON.parse + as X 断言点(参见 read-state.ts 的处理方式),再沿下游消费链(参见 idle-event.ts 与 idle-continuation.ts)确认违约值如何扩散,最后按“净化根因 + 消费端守卫 + 三场景测试 + 本地同构验证 + 原子提交”的顺序组织交付。
九、相关文件索引
| 角色 | 路径 |
|---|---|
| 本文主体文档 | execution-plan.md |
| 技能定义(通用生命周期) | work-with-pr/SKILL.md |
| 配套产出物 | code-changes.md、pr-description.md、verification-strategy.md |
| 状态读取实现(根因所在) | packages/boulder-state/src/storage/read-state.ts |
| 状态类型契约 | packages/boulder-state/src/types.ts |
| atlas 空闲事件处理 | packages/omo-opencode/src/hooks/atlas/idle-event.ts |
| 续跑注入(worktree_path 消费端) | packages/omo-opencode/src/hooks/atlas/idle-continuation.ts |
| 相关测试 | index.test.ts、read-state.test.ts |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00