让编码 Agent 跑出一段补丁并不难,难的是补丁之后的事:CI 红了谁去把它叫醒继续修,评审提了意见谁去接住,全绿之后谁来合并。Optio 卖的正是这段「后半程」。它是一个自托管的编排控制面,跑在你自己的 Kubernetes 集群或任意装了守护进程的机器上,把票变成一次 squash 合并的 PR:CI 失败时带失败上下文自动续跑,评审要求修改时接住评论并推修复,全绿时合并并关闭 issue。0.20.0 把它从原本「CI 失败续跑」的单一工具重做成通用工作流平台,并新增了对 Pi 智能体的支持,基础镜像换到了带十月安全修复的 Node.js 26.12.1。它由 jonwiggins 开源,MIT 协议,TypeScript 实现。本教程按自建前置判断、三类工作体、写一份工作流、闭环续跑、Controller MCP、用量账本与预算六步把它落地。
Step 1:先判断你该不该自建
自建的收益是明确的:代码、密钥与 Agent 日志全部留在你控制的基础设施里,并且可以按仓库切换或对比不同 Agent 厂商。代价同样明确——你要跑 Helm、Postgres、Redis 与一个控制面,还要为它做升级、备份与告警。项目自己的判断也很坦率:如果团队没有集群、也不打算运维集群,托管选项更简单;如果 Agent 工作量小到开一个本地 CLI 会话就够,那么为了它跑一整套数据库与控制面并不划算。
# 自建之前先确认这几件事都成立
# 1) 已有合规 K8s:GKE / EKS / AKS / 任何符合规范的集群
# 2) 能长期运维 Postgres 与 Redis(备份、升级、监控)
# 3) 有明确的数据驻留或租户隔离要求,无法使用托管服务
# 4) Agent 工作量足够大,摊得平固定运维成本
# 部署形态:Helm chart + Postgres + Redis + 控制面
# 控制面用对账循环驱动,pod 状态不放在内存里
一个实现细节值得留意,因为它决定了它能不能扛住重启:控制面靠对账循环工作,pod 状态不保存在内存里。也就是说编排器重启之后,它会把实际状态与期望状态重新对一遍再继续,而不是「忘了自己在干什么」。这是把长流程交给机器跑的前提——一次部署不该让正在进行的任务人间蒸发。
它是控制面,不是 Agent:Optio 不提供模型能力,驱动的是你已有的 Agent CLI,因此你仍然要为 Claude Code、Codex 或其它运行时的订阅与用量付费。它的价值在触发、续跑、合并与记账这一层,不要指望装上它智能体就变聪明了。
Step 2:三类工作体:按形状分,不按功能分
Optio 把 Agent 的活按形状分成三类,而不是按功能罗列。Tasks 是「票变 PR」:手动提交,或从 GitHub Issues、Linear、Jira、Notion 进来,在 git worktree 里干活,产出 PR 并追到合并。Jobs 是不做仓库检出的参数化运行,面向报告、告警分诊、依赖审计、数据库查询与发 Slack。Persistent Agents 是长驻的命名进程,带稳定 slug、收件箱与一个循环状态机,随时可以被别的 Agent 或人发消息。0.20.0 把 Pi 智能体的支持加到了 pod 任务、作业、常驻 Agent 与 PR 审查四类上。
# 三类工作体的分工
# Tasks 票 → PR:手动 / GitHub Issues / Linear / Jira / Notion
# 在独立 worktree 里工作,产出 PR 并追到合并
# Jobs 无仓库检出的参数化运行:报告、告警分诊、依赖审计、DB 查询、发消息
# Persistent Agents 长驻命名进程:稳定 slug + 收件箱 + 循环状态机
分类的意义在于选错形状会让后面处处别扭。需要改代码并留下 PR 的活,用 Task;只是定期跑一段查询并发个报告,用 Job——硬塞进 Task 会白搭一次仓库检出;需要长期在线、随时被点名的,才用常驻 Agent。判断标准就一条:这次运行要不要碰代码。
Step 3:写一份工作流:when、where、who、what
一份工作流回答四个问题,加上收尾行为与权限设置。when 是触发:manual 立即开跑,schedule 用五段式 cron 加 IANA 时区(默认 UTC,并会正确处理夏令时),webhook 则接 Slack、Jira、Linear、GitHub、PagerDuty、Datadog 或 Sentry 的事件,把事件载荷变成提示词参数。where 是位置:跑在集群 pod 里,还是跑在任意装了守护进程的机器上。who 是运行时:claude-code、codex、gemini、pi 或其它。what 是提示词本身。工作流可以在 Web 界面里配,也可以写成 YAML 同步给编排器。
{
"spec": {
"name": "Daily results review",
"when": {
"type": "schedule",
"config": { "cronExpression": "0 9 * * 1-5", "timezone": "America/Denver" }
},
"where": { "runTarget": "cluster" },
"who": { "runtime": "claude-code" },
"what": { "prompt": "Review yesterday's work results and summarize failures and follow-ups." },
"then": "exits",
"settings": { "controllerAccess": "read" }
}
}
时区这一项值得单独拎出来:不少定时任务事故都出在「以为用的是本地时间」。这里明确用五段式 cron 加 IANA 时区名,默认是 UTC,并且会处理夏令时;想让它在丹佛时间每个工作日早上九点跑,就把 timezone 写成 America/Denver,而不是自己换算成 UTC 再写死——后者在夏令时切换那天一定会错一次。写完先手工试跑一次,再去看运行列表与日志确认行为符合预期。
Webhook 载荷是不可信输入:它会被拼进提示词交给 Agent,而 Agent 手上有仓库写权限与工具。来自外部系统的标题、正文与评论都可能夹带指令。给这类工作流配上最小权限的运行时、限定可触及的仓库,并对合并动作保留人工环节——不要因为「反正只是个机器人」就让它自己合。
Step 4:从 CI 红到合并的闭环
把这一段跑通,才算真的用上了 Optio。Agent 在独立的 git worktree 里工作,产出一个 PR;随后控制面持续跟进 CI 与合并状态。CI 失败时,Agent 会带着失败上下文被自动唤醒继续修;评审人提出修改意见时,Agent 会接住这些评论并推送修复;一切通过之后,PR 被 squash 合并,关联的 issue 被关闭。这条环,而不是生成补丁本身,才是它真正的卖点。
# 一次 Task 的生命周期
# 1) 提交任务(手动 / GitHub Issues / Linear / Jira / Notion)
# 2) 在独立 git worktree 中工作,产出 PR
# 3) CI 失败 → 带失败上下文自动续跑
# 4) 评审意见 → 接住评论,推送修复
# 5) 全绿 → squash 合并,关闭 issue
独立 worktree 这个设计是承重墙:多个任务可以同时在同一个仓库上推进而互不干扰,每个任务看到的是自己那一份工作树。代价是它对仓库与 CI 有要求——测试得能在一次全新检出上跑通,构建产物不能依赖本地脏状态。上 Task 之前,先用一个真实的小任务在干净检出上验证一遍你的 CI,这比事后排查「为什么 Agent 那次总是红」要省事得多。
Step 5:Controller MCP:让别的 Agent 也能派活
除了 Web 界面与 YAML,Optio 还对外暴露一个 Controller MCP:外部客户端可以发现工作、创建工作、读取运行与日志,甚至让不同运行时的 Agent 互相发现与对话。常用工具是创建工作、试跑、列运行、取日志与列 Agent。列 Agent 会返回开启了 controller 权限的常驻 Agent 与活跃的任务或作业 Agent,可以按 kind(persistent-agent、repo-task、job-run)或 runtime 过滤,每条记录带地址、状态、投递方式与一个能否被发消息的标记。手机端也有原生 iOS 与 Android 应用,用来远程接管会话。
# 创建 → 试跑 → 看结果
optio_create_work # 建一个工作(manual 类型立即开跑,schedule 类型等待触发)
optio_run_work # 立即试跑一次
optio_list_runs # 列出运行
optio_get_run_logs # 取某次运行的日志
# 跨 Agent 发现
optio_list_agents # 按 kind 或 runtime 过滤,带 address、状态、投递方式、canMessage
有一句必须记住:写操作是异步且非幂等的。发起之后你会拿回 work、run 或 message 的 id,请把它存下来再用它去查状态;如果响应在网络上丢了,正确做法是先去查列表或收件箱确认到底创建成功没有,然后再决定是否重发。凭「没收到回执」就重发,很可能得到两份重复的工作——而这两份都会真实地烧掉 token。
重发之前先查,不要凭回执判断:创建与派发都是异步非幂等操作。响应丢失不等于没创建。先查列表或收件箱确认状态,再决定重发,否则你会为一个已经在跑的任务付两次钱,并且可能得到两份互相冲突的 PR。
Step 6:用量账本与月度预算:把花销管在前面
Agent 和普通脚本不一样:它会重试、会思考、会一轮一轮地调模型。一个定时任务跑一个月,消耗可能远超想象。Optio 的应对是一本用量账本:每类任务的计数器在同一个数据库事务里写入,覆盖 Repo Task、Job 运行、PR 审查、常驻 Agent 对话与本机终端等所有来源,做到不重不漏。理解这几条记账规则,你看到的数字才可信。
# 账本规则(决定了你看到的数字是否可信)
# Token 口径 输入(含缓存读/写)+ 输出;缓存命中是子集,不重复计费
# 费用口径 模型上报真实成本 与 API 等价估算 分开标注,估算不是发票
# 归属规则 私人任务归 owner;组织任务归任务本身;子任务计入父任务
# 只增不减 删除运行记录不会抹掉已记账用量;重复或回退的快照不会拉低总额
# 历史数据 一次性回填的合计会标记 historical,与日常数据区分
在这本账之上是月度预算,可以分别设在组织、个人与单个工作三个层级,超支时可以选择暂停新任务。这把「月底收到一张看不懂的账单」变成了「跑到某个阈值就自己停下」。把权限与预算按层级对齐,是这套机制最实用的用法:组织层设总额上限兜底,个人层给成员各自的空间,单个工作层给那些可能失控的实验性任务单独一道闸。
估算是估算,不是发票:账本把模型上报的真实成本与 API 等价估算分开标注,后者只是折算口径。对账请以模型厂商的实际账单为准。另外删除任务或运行记录都不会抹掉已记账用量,所以别指望靠删记录来「清理」账目——它恰恰是为了防止这件事而设计的。
常见问题速查
| 现象 | 原因与处理 |
|---|---|
| 不知道该不该自建 | 没有集群或工作量小到本地 CLI 就够,托管方案更简单 |
| 编排器重启后任务丢了 | 靠对账循环驱动,pod 状态不在内存;检查集群侧资源与持久化存储 |
| 定时任务时间不对 | 五段式 cron 加 IANA 时区,默认 UTC;不要手工换算成 UTC 写死 |
| 夏令时切换那天跑错点 | 用 timezone 指定 IANA 时区名,框架会处理夏令时 |
| Job 白跑一次仓库检出 | Job 不做仓库检出;需要改代码请用 Task |
| Agent 每次都在干净检出上失败 | 先验证 CI 能否在全新检出上跑通,Task 依赖这一点 |
| CI 红了没人管 | 闭环本应带失败上下文自动续跑;检查工作流是否配了跟进 CI 状态 |
| 响应丢了不知道建没建成功 | 写操作异步且非幂等;先查列表或收件箱再决定是否重发 |
| 产生了两份重复工作 | 凭回执就重发导致的;务必先按 id 或列表确认状态 |
| 费用数字看不懂 | 上报成本与 API 等价估算分开标注;对账以厂商实际账单为准 |
| 删了任务账还在 | 账本只增不减是刻意设计,防止靠删记录改账 |
| 担心某个任务烧穿预算 | 在单个工作层单独设月度预算,超支可选择暂停新任务 |