oh-my-pi / robomp 工作区脏状态提醒(dirty_state_reminder)解析:让编码 Agent 在轮次结束时绝不丢失未推送的工作
2026/9/12 17:19:32 网站建设 项目流程

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.pygh_push_branch门禁链)。读完你将掌握这套"先收尾、后推送、绝不静默丢弃"的工作区收尾机制的完整工作原理与可复用的工程实践。


一、问题背景:轮次结束时的"未推送工作"为什么致命

robomp 是一个面向 GitHub Issue / PR 的自动化编码 Agent 服务,它运行在"轮次(turn)"模型之上:每个任务由初始 prompt 驱动,Agent 借助工作区内的 git 仓库修改代码、提交、推送、开 PR。从python/robomp/src/prompts/kickoff_directive.md可以确认,代码类任务的正常收尾链路是:

  1. {{workspace.branch}}上提交修改;
  2. 调用gh_push_branch(推送分支,内部先跑bun run fixbun check);
  3. 调用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.summarygit_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 addgit 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.tomlabort_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.

这一轮必须以下列两种情况之一结束,没有第三种:

  1. gh_push_branch成功(工作已推到远端);
  2. 工作区干净(无未提交修改、无领先于origin的提交),并在评论中解释原因。

这个"二选一"的设计保证了:要么工作安全落盘在远端,要么 Agent 明确解释了为什么无需推送——机器人永远不会在不做任何说明的情况下吞掉工作。


三、源码级实现:提醒从哪里来、怎么被注入

3.1 模板渲染:persona.dirty_state_reminder

模板在python/robomp/src/persona.pydirty_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 命令:

指标命令说明
uncommittedgit status --porcelain=v1 --untracked-files=normal行数即未提交条目数,最多采样前 10 行
unpushedgit 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)
  1. 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 的提交,则拒绝并给出指令而非猜测。
  2. bun check(静态检查门禁):检查失败必须修复根因后重试(即模板中的 "fix root cause; do not skip gate")。
  3. _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_checksgh_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中有三个测试用例精确对应本节机制:

  1. test_run_rpc_sends_dirty_state_reminder_when_worktree_has_unpushed_work(第 842 行):模拟"轮次结束时有未推送提交"→ 断言提醒被注入、提醒文本包含Unpushed commits与具体提交哈希,且模板占位符{{已被完全渲染("template placeholder leaked" 断言);
  2. test_run_rpc_skips_dirty_state_reminder_when_worktree_is_clean(第 873 行):工作区干净 → 只产生 1 个 prompt,无额外提醒;
  3. 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_branchgh_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路径。

这套设计的工程启示可以总结为三条可复用的原则:

  1. 给 Agent 的不是催促,而是它自己的遗忘清单——dirty.summary精确列出路径与提交哈希,让二次收尾有据可依;
  2. 可恢复场景与不可恢复场景严格分离——工作区脏永远是 recoverable(走 push / restore / 评论),只有环境缺陷才允许abort_task
  3. 每次兜底都有预算与日志——提醒有task_completion_max_reminders上限,耗尽后记录rpc_dirty_state_unfinished供人工审查,既不无限循环,也不静默吞掉失败。

对于想要在自己构建的 Agent 编排系统中复用的开发者,可以仿照此模式:在轮次结束时探测 git 工作区状态,将git statusgit rev-list --count HEAD --not --remotes=origin的结果渲染成带具体文件的提醒模板,并配套"门禁链 + 逃生舱 + 预算上限"的完整约束。


七、结语

dirty_state_reminder.md表面上是 17 行的提示词模板,实质是 robomp 对"Agent 轮次结束≠工作完成"这一工程现实的正式回应。它通过worker.py的轮次后探测、git_ops.py的精确脏状态计算、persona.py的模板渲染,以及host_tools.pygh_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询