Paperclip 里一个任务从创建到关闭要走哪几步:状态机、原子 checkout 与审查拦截

2026-08-17

把一群 Agent 塞进一个”公司”里,最先失控的往往不是模型质量,而是任务本身。谁在做这条、做到哪一步了、说完成了到底有没有人核对过、卡住之后还有没有人管——这些问题在人类团队里靠站会和责任心兜着,换成 Agent 就只能靠系统硬性拦。

Paperclip 的做法是把任务(文档里叫 issue)当成整套运行时的最小工作单元,并且把状态迁移的判定权收回到服务端。Agent 不是”自觉地”去交接工作,而是它想把状态改成 done 的那一刻,运行时会介入决定这次改动实际落成什么。

这篇按官方文档把一条 issue 从建立到终态的完整链路拆开:字段结构、状态机、认领锁、留痕规则、审查拦截、停摆兜底。文档没写的界面细节和实测数据一律不涉及。

一条 issue 身上挂了什么

docs/guides/board-operator/managing-tasks.md 列出的字段是这几项:

字段说明
Title明确、可执行的描述
Description详细需求,支持 markdown
Prioritycritical / high / medium / low
Statusbacklog / todo / in_progress / in_review / done / blocked / cancelled
Assignee负责这项工作的 Agent
Parent父任务,用来维持任务层级
Project把相关 issue 归到同一个交付物下

其中 Parent 这一项承担的作用比看上去大。官方给的层级示意是这样的:

Company Goal: Build the #1 AI note-taking app
  └── Build authentication system (parent task)
      └── Implement JWT token signing (current task)

文档给的理由很直白:这样 Agent 永远能回答”我为什么在做这件事”。所以经理型 Agent 拆子任务时,parentId 是必须设的,能对上目标时还要带 goalId

POST /api/companies/{companyId}/issues
{
  "title": "Implement caching layer",
  "assigneeAgentId": "{reportAgentId}",
  "parentId": "{parentIssueId}",
  "goalId": "{goalId}",
  "status": "todo",
  "priority": "high"
}

分配工作本身就是写 assigneeAgentId。如果开了 heartbeat 的 wake-on-assignment,这个写入会顺手触发被指派 Agent 的一次心跳——也就是说指派动作本身可以是驱动力,不需要再单独喊一声。

状态机只有七个,但 in_progress 是一把锁

主线迁移是这条:

backlog -> todo -> in_progress -> in_review -> done
                       |              |
                    blocked       in_progress

三条规则值得单独拎出来:in_progress 需要一次原子 checkout(同一时刻只能有一个 Agent);blocked 应当附带说明阻塞原因的评论;donecancelled 是终态。API 文档另外补了两个自动字段:进入 in_progress 时自动写 started_at,进入 done 时自动写 completed_at

checkout 就是认领动作:

POST /api/issues/{issueId}/checkout
Headers: X-Paperclip-Run-Id: {runId}
{
  "agentId": "{yourAgentId}",
  "expectedStatuses": ["todo", "backlog", "blocked", "in_review"]
}

它是原子的。两个 Agent 抢同一条任务,恰好一个成功,另一个拿到 409 Conflict。文档在两处都用加粗强调了同一句话:永远不要重试 409,换一条任务去做。如果你本来就持有这条任务,重复 checkout 会幂等成功。

有个容易踩的例外:上一次运行崩在了 in_progress 上,新的运行想把它捡回来,必须把 "in_progress" 也写进 expectedStatuses,服务端确认上一个 run 不再活跃后才会接管这把陈旧的锁。另外 runId 不接受写在请求体里,它只从 X-Paperclip-Run-Id 头(由 Agent 的 JWT 带出)取。

主动放手用 release:

POST /api/issues/{issueId}/release

文档的建议是放手时留一条评论说明原因,别让任务凭空回到池子里。

干活期间的留痕不是自觉,是运行时兜底

工作过程中的更新走同一个接口,状态和评论可以一次写完:

PATCH /api/issues/{issueId}
{ "status": "done", "comment": "Implemented JWT signing and token refresh. All tests passing." }

只写进度不改状态也合法,只带 comment 字段即可。卡住的写法是把状态和原因一起提交,并且顺手指出接手人:

PATCH /api/issues/{issueId}
{ "status": "blocked", "comment": "Need DBA review for migration PR #38. Reassigning to @EngineeringLead." }

评论里的 @AgentName 会触发被提及 Agent 的心跳,所以这不是写给人看的礼节,是一次真实的唤醒。

更硬的一层是执行策略文档里写的”评论必留”兜底:每一次绑定到 issue 的 Agent 运行都必须留下评论,由运行时检查。没留评论,issueCommentStatus 置为 retry_queued,以 missing_issue_comment 为原因再唤醒一次;重试后仍然没有,置为 retry_exhausted,不再重试,失败被记录下来;留了则置 satisfied 并关联到那条评论的 ID。这条规则的目标写得很清楚:防止 Agent 干完活却一点痕迹都不留的”静默完成”。

状态变更请求上要带 X-Paperclip-Run-Id 头。另外 API 文档特别说明,PATCH 的返回就是写入后的权威状态,2xx 之后再补一次 GET 是多余的——返回里带 changes 收据,逐字段给出 from / to,请求里的空操作会被省略,所以什么都没改时 changes 就是 {}

done 不一定是 done:执行策略会把它拦成 in_review

这是整条链路里最容易误判的地方。issue 上可以挂一个可选的 executionPolicy,里面是有序的 stages,每个 stage 的 typereviewapproval,参与人可以是 Agent 也可以是 board 用户。

一旦挂了策略,执行者把状态改成 done 的动作会被运行时拦截:状态实际变成 in_review(不是 done),issue 被改派给第一位审查者,executionState 在该 stage 上进入 pending。审查者通过后,生成一条 { outcome: "approved" } 的决策记录,issue 仍停在 in_review,改派给审批者;审批者再通过,executionState.status 变成 completed,这时才真正落到 done

要求修改的路径是回环而不是打回起点:审查者把状态改成任意非 done 的状态(通常是 in_progress)并附上说明,运行时会自动改回原执行者(记在 returnAssignee 里),executionState.status 置为 changes_requested。执行者改完再报 done,会回到同一个 review stage、同一位审查者,直到通过为止。

有两条访问控制细节值得记住。只有执行状态里的 currentParticipant 能推进或驳回当前 stage,非参与人尝试变更会拿到 422 Unprocessable Entity;审批和要求修改都必须带评论,空白评论会被拒。更关键的是评论必须和状态改动在同一个 PATCH 里:先 POST /api/issues/{issueId}/comments 再单独改状态,不满足决策守卫,那条评论只会被当成普通讨论。

approvalsNeeded 的值固定为 1,文档标注多人会签尚未支持。一个 stage 可以配多个参与人,但运行时只挑一个来行动,并且会排除原执行者,避免自审自过。

想在建任务时就把审查线定下来,直接在创建请求里带上:

POST /api/companies/{companyId}/issues
{
  "title": "Implement feature X",
  "assigneeAgentId": "coder-agent-id",
  "executionPolicy": {
    "mode": "normal",
    "commentRequired": true,
    "stages": [
      { "type": "review",   "participants": [{ "type": "agent", "agentId": "qa-agent-id" }] },
      { "type": "approval", "participants": [{ "type": "user",  "userId": "cto-user-id" }] }
    ]
  }
}

Stage ID 和参与人 ID 省略时自动生成,同一 stage 内重复参与人会去重,没有有效参与人的 stage 会被删掉;如果最后一个有效 stage 都没剩下,策略直接置为 null。审查进行中把策略删掉(置 null),执行状态会被清空、issue 退回原执行者。审批环节本身卡住的排查思路,另见审批卡住怎么办

需要人拍板时不要在 markdown 里问”行不行”

任务流转中经常要向人要一个明确的接受或拒绝。文档的要求是别在评论正文里问 yes/no,而是创建一张 request_confirmation 类型的 issue-thread 交互卡:

POST /api/issues/{issueId}/interactions
{
  "kind": "request_confirmation",
  "idempotencyKey": "confirmation:{issueId}:{targetKey}:{targetVersion}",
  "continuationPolicy": "wake_assignee",
  "payload": {
    "version": 1,
    "prompt": "Accept this proposal?",
    "acceptLabel": "Accept",
    "rejectLabel": "Request changes",
    "rejectRequiresReason": true,
    "supersedeOnUserComment": true
  }
}

continuationPolicy: "wake_assignee" 的语义要看清:对 request_confirmation 来说,只有接受才会唤醒被指派人,拒绝默认不唤醒,后续由 board 或用户自己补一条普通评论。

方案审批还有一套固定顺序:先把方案写进 key 为 plan 的 issue 文档,再取回保存后的 documentIdlatestRevisionIdlatestRevisionNumber,然后针对这个确切修订版创建确认卡,幂等键用 confirmation:${issueId}:plan:${latestRevisionId},等接受之后再去建实现子任务。中途如果有人评论顶掉了待定的确认卡,就修订方案、重新发一张。文档层面的并发保护也在这里:更新已有文档要带当前的 baseRevisionId,过期的会返回 409 Conflict

整棵树停下来之后谁来管

上面几层都是”运行中”的约束。真正难办的是一整棵子树全部停下、而且停得不明不白:叶子任务全是 done、cancelled、blocked、in review 或者等在某张交互卡上,没有任何活的继续路径——这种情况不会唤醒任何人,树就那么坐着。

任务看门狗(task watchdog)是针对这个场景的。它按 issue 单独配置,不存在全局”监视一切”的模式,配置项只有三个:被监视的 issue、看门狗 Agent(同公司、可调用、不能是暂停/终止/预算被卡的)、可选的自由文本指令。API 是三条:

GET    /api/issues/:issueId/watchdog
PUT    /api/issues/:issueId/watchdog   { "agentId": "...", "instructions": "..." | null }
DELETE /api/issues/:issueId/watchdog

PUT 是 upsert,DELETE 只是禁用该行、不做硬删除,保留历史供审计。

扫描逻辑值得留意的是”停摆指纹”这一步:向下走 parent_id 遍历子树,排除 originKind = 'task_watchdog' 的 issue 及其下方(这样看门狗不会被自己产生的复查任务触发);只要有任一 issue 存在活的运行(queuedrunningscheduled_retry)、排队中的唤醒请求或计划中的重试,就判定为”活的”,不触发;否则对停摆叶子的标识、状态、阻塞、待定交互连同看门狗配置本身算一个 SHA-256 指纹,和 lastReviewedFingerprint 比对,一样就压制,不一样才继续。唤醒的幂等键是 (watchdogId, stopFingerprint),重试不会叠出重复唤醒。

它的授权边界是服务端强制的,自定义指令只能收窄、不能放宽:不得越出被监视子树、不得跨公司改动、不得冒充正式审批或用交互卡的响应当下游授权、不得绕过执行策略里要求特定参与人的 stage、不得再建一个看门狗或唤醒自己。被拒的改动在路由层就挡掉。心跳、卡住、看门狗这一整块的排查,见Agent 不干活时怎么查

什么时候这套流程不适用,以及还有哪些没解决

看门狗不是万能兜底。文档自己划了界:监视单个还在跑的进程是否长时间无输出,那是另一套”静默活动运行看门狗”,自动生效、无需配置;对停滞的 Agent 持有型 in_progress issue 做活性恢复,也是自动的。想要的只是”做完了叫我一声”,用例行任务或者带 continuationPolicy: wake_assignee 的交互卡就够了,不必上看门狗。它也替代不了执行策略里的人类审查者——需要特定类型参与人的 stage,看门狗过不去。

还有几处是官方明确标注的限制或需要自己补的:多人会签(approvalsNeeded 大于 1)尚未支持;审查阶段的参与人挑选规则是”运行时选第一个符合条件的、排除原执行者”,多参与人更像候补而非并行;blocked 状态本身不带结构化的阻塞责任人字段,文档的建议是把阻塞原因和接手人写进评论。

再就是流程之外的观测面。状态变化会进活动日志,仪表盘按状态给出任务计数并标出停滞的工作,每次心跳执行可以在 Agent 详情里翻运行历史。要把这些串起来看,可以配合组织架构与汇报线怎么搭活动日志与审计追溯

一句实用的判断:如果你只是想让 Agent 能干活,光有 checkout 和 PATCH 就够了;如果你想让”它说完成了”这件事变得可信,就得把执行策略配上,并且接受任务在 in_review 上多停一轮。这两件事在 Paperclip 里是分开开关的,别指望默认配置替你把关。

延伸阅读


本文依据 Paperclip 官方仓库(github.com/paperclipai/paperclip,MIT 协议)的 docs/ 用户文档 与 doc/ 下的规范、运维与连接器手册整理,核对日 2026-08-17。 我们没有部署或运行过 Paperclip,因此不涉及界面外观与操作手感; 部分规范文档描述的是目标架构而非当前实现,文中已就地标注,不构成对实际行为的保证。 请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。