oh-my-pi / robomp 工作区脏状态提醒(dirty_state_reminder)解析:让编码 Agent 在轮次结束时绝不丢失未推送的工作
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
导读
本文以python/robomp/src/prompts/dirty_state_reminder.md这份提示词模板为骨架,系统讲解 robomp 机器人在处理 GitHub Issue 时如何兜住"轮次结束但工作区仍有未提交修改或未推送提交"这一高发故障:它是什么、在什么时机被注入、模板每一段想驱动 Agent 做什么,以及背后的源码级实现(persona.py的模板渲染、worker.py的提醒循环与预算、git_ops.py的脏状态探测、host_tools.py的gh_push_branch门禁链)。读完你将掌握这套"先收尾、后推送、绝不静默丢弃"的工作区收尾机制的完整工作原理与可复用的工程实践。
一、问题背景:轮次结束时的"未推送工作"为什么致命
robomp 是一个面向 GitHub Issue / PR 的自动化编码 Agent 服务,它运行在"轮次(turn)"模型之上:每个任务由初始 prompt 驱动,Agent 借助工作区内的 git 仓库修改代码、提交、推送、开 PR。从python/robomp/src/prompts/kickoff_directive.md可以确认,代码类任务的正常收尾链路是:
- 在
{{workspace.branch}}上提交修改; - 调用
gh_push_branch(推送分支,内部先跑bun run fix与bun check); - 调用
gh_open_pr(跑完整bun run test后开 PR)。
但 Agent 是概率性系统,它可能在生成"终态回复"时忘记收尾。此时如果机器人直接结束会话,工作区会被回收,未提交的修改、未推送的提交将永久丢失。dirty_state_reminder.md就是为堵住这个洞而生的"最后一道兜底":在轮次以脏状态结束时,把工作区里被遗忘的内容重新喂回给 Agent,强制它完成收尾。
一句话定位:这份文档是 robomp 的**轮次收尾强制器(turn-completion enforcer)**的提示词模板,只处理"工作区脏"这一种可恢复场景。
二、模板逐段解析:原文档完整内容
原文模板(python/robomp/src/prompts/dirty_state_reminder.md)本身是一份 Jinja 风格模板,全文共 17 行,下面逐段拆解其语义与设计意图。
2.1 触发信号(第 1 行)
Turn ended with unpushed work in worktree.这是注入该提醒时的开场白,向 Agent 宣告:你的这一轮已经结束,但工作区里还有没推出去的工作。紧接着,模板会携带三个上下文变量:
Issue: {{repo.full_name}}#{{issue.number}} — {{issue.title}} Branch: `{{workspace.branch}}`repo.full_name/issue.number/issue.title:当前正在处理的 Issue 的完整定位信息,防止 Agent 在多任务状态下混淆上下文;workspace.branch:Agent 应在其上工作的分支名。这也是后续gh_push_branch的默认目标分支。
2.2 脏状态摘要注入(第 6~8 行)
End-of-turn workspace state: {{dirty.summary}}dirty.summary由git_ops.inspect_dirty_state生成(详见第四节),是一段人工可读的多行摘要,列出具体的未提交文件路径与未推送提交的 oneline 信息。它的价值在于:给 Agent 的不只是一句"你还有活没干完",而是确切的、它自己忘记的文件与提交清单。
2.3 风险提示(第 10 行)
Any nonzero count → roboomp discards work when session ends. Act on this summary:这一句是整个提醒的"高压电":明确告知 Agent,只要摘要中的计数非零,会话结束时 robomp 会丢弃这些工作。这是行为约束,不是建议——Agent 必须立即行动。
2.4 分类处置指令(第 12~13 行)
模板把脏状态分成两类并给出互斥的处置路径:
未提交的修改(Uncommitted changes):
- 如果修改是刻意的 →
git add并git commit; - 如果是误操作 →
git restore撤销; - 如果工作已经就绪 → 提交前先运行
bun run fix。原因:格式化/静态检查门禁会在推送时拒绝fix退出非零的提交,所以要先在本地把格式问题消化掉。
未推送的提交(Unpushed commits):
- 在
bun run fix成功之后,调用gh_push_branch; - 如果
gh_push_branch因其他原因拒绝推送,必须修复根因后重试,不得跳过门禁("do not skip gate")。
注意原文对两条路径都强调了门禁:格式化门禁是推送的前置条件,跳过它只会换来 CI 或推送端的拒绝,最终仍然要回头处理。
2.5 收尾后的行为与禁忌(第 15 行)
If fix genuinely complete and gates pass, push, then comment on PR with one-line summary of changes since previous push. Do not re-classify issue, re-post original preamble, or call `abort_task`; recoverable.推送成功后要做的动作:
- 在 PR 上留下一行式摘要,说明自上次推送以来的改动;
- 不得重新分类 Issue、不得重发原始开场白(preamble)、不得调用
abort_task。
其中abort_task的禁忌尤其值得展开:从python/robomp/src/prompts/host_tools.toml中abort_task的参数描述可以看到,它只用于"无法绕过的编排器/环境缺陷(损坏的文件系统权限、缺失的系统工具、损坏的 git 元数据、harness 缺陷)",而工作区脏是正常的工作流问题(recoverable),应当用gh_push_branch/gh_post_comment解决,绝不走abort_task这条静默放弃路径。
2.6 硬性结束条件(第 17 行)
MUST end turn with either successful `gh_push_branch`, or clean worktree (no uncommitted changes; no commits ahead of `origin`) and explanation in a comment.这一轮必须以下列两种情况之一结束,没有第三种:
gh_push_branch成功(工作已推到远端);- 工作区干净(无未提交修改、无领先于
origin的提交),并在评论中解释原因。
这个"二选一"的设计保证了:要么工作安全落盘在远端,要么 Agent 明确解释了为什么无需推送——机器人永远不会在不做任何说明的情况下吞掉工作。
三、源码级实现:提醒从哪里来、怎么被注入
3.1 模板渲染:persona.dirty_state_reminder
模板在python/robomp/src/persona.py的dirty_state_reminder()函数(第 206~232 行)中被加载并渲染:
def dirty_state_reminder(*, repo, issue, workspace, dirty) -> str: """Reminder injected when the worktree has uncommitted or unpushed work.""" return render( _load("dirty_state_reminder.md"), { "repo": repo, "issue": issue, "workspace": workspace, "dirty": { "uncommitted": dirty.uncommitted, "unpushed": dirty.unpushed, "summary": dirty.summary, }, }, )函数 docstring 明确说明:它由worker._drive_turn在模型输出终态轮次、但留下了 robomp 本会丢弃的更改时触发;嵌入模板的摘要来自git_ops.inspect_dirty_state,因此 Agent 能看到它确切忘记的路径/提交。
3.2 触发循环与提醒预算:worker._drive_turn
真正的调度逻辑在python/robomp/src/worker.py的_drive_turn()(第 301 行起):
max_reminders = settings.task_completion_max_reminders ... while reminders_used < max_reminders: if not needs_completion: dirty = _probe_workspace_dirty(inputs.workspace, inputs.slot_uid) if not dirty.is_dirty: break # 工作区干净,直接退出 ... reminder = persona.dirty_state_reminder(...) next_turn = _run(reminder)关键点:
- 触发条件:
_probe_workspace_dirty返回的DirtyState.is_dirty为真(uncommitted > 0 or unpushed > 0),说明轮次结束时工作区不干净; - 预算上限:提醒不是无限重试的。
reminders_used < max_reminders(来自设置项task_completion_max_reminders)限制了最多注入多少次提醒,防止一个始终无法收尾的 Agent 把会话拖入死循环; - 探测容错:
_probe_workspace_dirty(第 281~298 行)对inspect_dirty_state的异常做兜底——如果探测本身抛异常(例如工作区在探测间隙被清理),就返回"干净"状态。docstring 直言:"a corrupted workspace doesn't pin the agent in a reminder loop"(损坏的工作区不会把 Agent 钉死在提醒循环里)。
另外,第 426~438 行在提醒预算耗尽后还会做一次final_dirty探测并记录rpc_dirty_state_unfinished警告日志,把"给了提醒仍未收尾"这个事实留给操作者审查,而不是静默吞掉。
3.3 脏状态如何被计算:git_ops.inspect_dirty_state
python/robomp/src/git_ops.py中的DirtyState数据类(第 649~666 行)与inspect_dirty_state()(第 703~765 行)是提醒的数据来源:
@dataclass(slots=True, frozen=True) class DirtyState: uncommitted: int # git status --porcelain 的条目数 unpushed: int # HEAD 中从任何 origin/* ref 不可达的提交数 summary: str # 供提醒 prompt 嵌入的多行描述,两者皆零时为空 @property def is_dirty(self) -> bool: return self.uncommitted > 0 or self.unpushed > 0探测逻辑对应的真实 git 命令:
| 指标 | 命令 | 说明 |
|---|---|---|
uncommitted | git status --porcelain=v1 --untracked-files=normal | 行数即未提交条目数,最多采样前 10 行 |
unpushed | git rev-list --count HEAD --not --remotes=origin | 计数 HEAD 中不被任何origin/*可达的提交——即工作区被丢弃时会丢失的提交 |
| 未推送提交样例 | git log --max-count=min(unpushed, 5) --oneline HEAD --not --remotes=origin | 最多给出 5 条 oneline,供摘要展示 |
值得注意的容错设计:inspect_dirty_state会吞掉底层 git 调用的错误,把"探测失败"当作"干净"处理,理由是"a broken git binary can't pin the agent in a reminder loop forever"(损坏的 git 二进制不能把 Agent 永远钉在提醒循环里)。
summary的组装(第 755~765 行)也是分级的:
Uncommitted changes (N): <前10条路径> … and (N-10) more # 超过 10 条时追加 Unpushed commits (M): <前5条 oneline>四、收尾动作的落地:gh_push_branch 的门禁链
模板要求未推送提交必须走gh_push_branch,而该工具的实现在python/robomp/src/host_tools.py(第 1229~1271 行),其门禁链与模板指令一一对应:
_run_pre_publish_bun_fix(bindings, args, tool_name="gh_push_branch", stage="push", skip_checks=skip) _run_pre_publish_bun_check(bindings, args, tool_name="gh_push_branch", stage="push", skip_checks=skip) head = _guarded_push_branch(bindings, args, "gh_push_branch", branch)bun run fix(格式化门禁):_run_pre_publish_bun_fix(第 383 行起)在仓库定义了scripts.fix时才执行;格式化器改动的任何 diff 会被 amend 进 Agent 的 HEAD 提交,避免 PR 历史里出现孤立的style:提交。它在跑格式化器之前先做一次"脏树门禁"——任何未提交的改动都会导致拒绝,防止既有编辑被git add -A悄悄卷进 amend。若 HEAD 位于origin/<base>上或由他人创作、无可安全吸收 diff 的提交,则拒绝并给出指令而非猜测。bun check(静态检查门禁):检查失败必须修复根因后重试(即模板中的 "fix root cause; do not skip gate")。_guarded_push_branch(推送本身,第 856 行起):- 推送前重新固定提交者身份(
git config user.name/email); - 重写提交消息中 shell 字面量
\n转义; - 快照 HEAD SHA;
- 身份门禁:
origin/<base>..HEAD之间每个提交的作者必须与配置身份一致,否则拒绝推送,要求git commit --amend --reset-author --no-edit; - 推送传输使用
--force-with-lease,正是为了安全地恢复本地历史重写(amend)后的推送场景。
- 推送前重新固定提交者身份(
逃生舱skip_checks:gh_push_branch的参数定义在python/robomp/src/prompts/host_tools.toml中:
branch = "Optional branch override; defaults to the workspace branch." skip_checks = "Bypass `bun run fix` + `bun check`. Use ONLY after verifying the failure exists on `main` and is NOT caused by your diff. Dirty-tree gate still runs — commit everything first."它只允许在"失败源于main且与自己的 diff 无关"时使用,且脏树门禁仍然无条件执行——这正是模板第 13 行 "do not skip gate" 的另一层含义:跳过门禁必须有确凿依据,且永远不能带着未提交内容推送。
五、测试验证:三个用例钉死行为契约
python/robomp/tests/test_worker.py中有三个测试用例精确对应本节机制:
test_run_rpc_sends_dirty_state_reminder_when_worktree_has_unpushed_work(第 842 行):模拟"轮次结束时有未推送提交"→ 断言提醒被注入、提醒文本包含Unpushed commits与具体提交哈希,且模板占位符{{已被完全渲染("template placeholder leaked" 断言);test_run_rpc_skips_dirty_state_reminder_when_worktree_is_clean(第 873 行):工作区干净 → 只产生 1 个 prompt,无额外提醒;test_run_rpc_caps_dirty_state_reminders_at_budget(第 901 行):工作区持续脏 → 提醒不会超过预算次数,防止死循环。
这三个用例分别验证了"该触发就触发""干净不打扰""有预算不失控"三个行为契约,与模板中Any nonzero count的判定、MUST end turn ... or clean worktree的硬性结束条件形成闭环。
六、在提示词体系中的位置与工程启示
dirty_state_reminder并不是孤立文件,它与 robomp 的其它提示词构成完整的收尾体系:
completion_reminder.md:triage 轮次在终态工具触发前结束时注入,要求补齐gh_push_branch/gh_open_pr等收尾动作——两者分工为"缺动作"与"有脏状态"两种失败模式;kickoff_directive.md第 28 行:任务初始指令中已预先声明收尾规范(提交 →gh_push_branch→gh_open_pr),dirty_state_reminder是对这个规范的兜底执行;system_append.md第 71~78 行:声明 Agent 可以自行预跑格式化器(gh_push_branch/gh_open_pr也会跑并把格式化 diff amend 进 HEAD),并规定"两次连续相同错误的gh_push_branch拒绝后,应修复、用有依据的skip_checks=true,或gh_post_comment升级——绝不无限重试";system_append_pr_review.md:PR 审查任务被明确禁止调用gh_push_branch/gh_open_pr等写操作,因此脏状态提醒只服务于实施类任务,审查类任务走review_completion_reminder路径。
这套设计的工程启示可以总结为三条可复用的原则:
- 给 Agent 的不是催促,而是它自己的遗忘清单——
dirty.summary精确列出路径与提交哈希,让二次收尾有据可依; - 可恢复场景与不可恢复场景严格分离——工作区脏永远是 recoverable(走 push / restore / 评论),只有环境缺陷才允许
abort_task; - 每次兜底都有预算与日志——提醒有
task_completion_max_reminders上限,耗尽后记录rpc_dirty_state_unfinished供人工审查,既不无限循环,也不静默吞掉失败。
对于想要在自己构建的 Agent 编排系统中复用的开发者,可以仿照此模式:在轮次结束时探测 git 工作区状态,将git status与git rev-list --count HEAD --not --remotes=origin的结果渲染成带具体文件的提醒模板,并配套"门禁链 + 逃生舱 + 预算上限"的完整约束。
七、结语
dirty_state_reminder.md表面上是 17 行的提示词模板,实质是 robomp 对"Agent 轮次结束≠工作完成"这一工程现实的正式回应。它通过worker.py的轮次后探测、git_ops.py的精确脏状态计算、persona.py的模板渲染,以及host_tools.py的gh_push_branch门禁链,把"不丢工作"从一句口号变成了可测试、有预算、有日志的强制执行机制——这也是它在 test_worker.py 中拥有专属测试用例的原因。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考