文章以给停用用户脚本添加 dry-run 为例,将任务文件分为作者、执行者、审查者和操作者各自负责的四部分。配套 Python 脚本判断交接阶段,并通过提交 SHA、验收命令和测试证据约束交付。
大多数交接模板都是一个人填写,其他人阅读。这个模板反过来:任务文件分成四个部分,每个部分由不同的人负责。任务发起人填写第一部分,执行者填写第二部分,审查者填写第三部分,负责部署或运行变更的人填写第四部分。一个 60 行的 Python 脚本会读取这个文件,告诉你接下来轮到谁。
本文用一个小型运维变更来演示完整流程。这类任务,coding agent 很容易做到“差一点就对了”:给停用不活跃用户的脚本添加 --dry-run。文中的人物都是虚构的。脚本、测试数据库、commit,以及下文的所有输出,都来自我在本地运行的一个演示仓库(Python 3.13.5、SQLite 3.51.0)。
第三列最重要。每个人都使用自己已有的访问权限开展工作。执行者在自己的 clone 中,使用自己的 Agent 完成任务,完全不需要生产环境凭据。只有操作人员会接触生产数据库。如果执行者发现自己需要某种尚未拥有的权限或资源,就把它写进“Stop and ask if”,而不是借用别人的账号。
对于脚本来说,“部署”就是让它对真实数据执行操作,因此第四个角色是操作人员,而不是发布流水线。这仍然是一个独立于“代码正确”的决策。
# Task: [one line]
## 1. Author
- Owner: [who decides what done means]
- Outcome: [observable result]
- Allowed changes: [files or modules]
- Do not touch: [what stays as it is]
- Acceptance check: [command or steps the reviewer will run]
- Stop and ask if: [conditions]
## 2. Runner
- Runner: [name]
- Claimed: [date]
- Environment: [where you ran it, with whose access]
- Delivered commit: [sha]
- Evidence: [output, logs]
- Not tested: [be specific]
## 3. Reviewer
- Reviewer: [name, not the runner]
- Commit reviewed: [sha]
- Check run: [what you ran]
- Verdict: [accepted / sent back]
- Reason: [one or two sentences]
## 4. Operator
- Operator: [who runs or deploys it]
- Target: [environment]
- Dry-run output reviewed: [by whom, when]
- Window: [when]
- Undo plan: [how to reverse it]
凡是仍然保留 [方括号] 的内容,都视为空白。
handoff.py 会解析每个 ## n. Role 标题下面的 - Key: value 行。第一个存在未填字段的角色,就是接下来需要接手的人。在此基础上,它还会应用四条规则:
if filled(run.get("Runner", "")) and run.get("Runner") == rev.get("Reviewer"):
problems.append("runner and reviewer are the same person")
if "prod" in run.get("Environment", "").lower():
problems.append("runner environment mentions production; runs against prod belong to the operator")
verdict = rev.get("Verdict", "").lower()
reviewed, delivered = rev.get("Commit reviewed", ""), run.get("Delivered commit", "")
if filled(reviewed) and reviewed != delivered:
if verdict == "accepted":
problems.append(f"acceptance is for {reviewed}, but the runner now delivers {delivered}")
next_role = ("Reviewer", [f"review of {delivered} (last review was {reviewed})"])
elif verdict == "sent back":
next_role = ("Runner", ["a new Delivered commit (sent back: " + rev.get("Reason", "") + ")"])
if any(filled(v) for v in op.values()) and verdict != "accepted":
problems.append("operator section started before the reviewer accepted")
发现问题时,脚本会以状态码 1 退出,因此可以把它放进 CI,检查包含任务文件的 PR。
任务发起人做好准备。 支持运维负责人 Nadia 填写第 1 部分。预期结果是:--dry-run 打印出将被停用的用户,不写入任何数据。验收检查使用她编写的 acceptance.sh。这个脚本会创建一个包含 8 个用户的全新测试数据库,导出数据库,执行 dry run,再次导出数据库并比较哈希值。随后,它会实际执行一次,再检查哪些 ID 发生了变化。
$ python3 handoff.py 1-prepared.md
next: Runner (missing: Runner, Claimed, Environment, Delivered commit, Evidence, Not tested)
执行者交付。 Arman 认领任务,在自己的 clone 中使用 coding agent 开展工作,所有操作都只针对 fixture.db。dry run 看起来没问题:
$ python3 deactivate_inactive.py --db fixture.db --today 2026-10-16 --dry-run
would deactivate ben@example.test
would deactivate dev@example.test
would deactivate gus@example.test
3 user(s), cutoff 2026-04-19
他在第 2 部分填入 commit ebe6594。
$ python3 handoff.py 2-delivered.md
next: Reviewer (missing: Reviewer, Commit reviewed, Check run, Verdict, Reason)
审查者退回修改。 Kofi 对 ebe6594 运行了任务发起人提供的检查:
$ sh acceptance.sh deactivate_inactive.py
1. dry run lists 3 users: 3
2. dry run leaves the database as is: before 68cec51f91d1 after 3721c4144bf3
FAIL: dry run changed the database
audit_log rows: 3
3. real run deactivates ids: 2,4,7
4. admin 5 still active: 1
5. second run finds: 0 user(s), cutoff 2026-04-19
6. audit rows after real runs: 3
ACCEPTANCE: FAIL
dry run 打印了正确的用户名,也正确跳过了 UPDATE,但它仍然写入了三条 audit_log 记录,声称这些用户已被停用。插入操作位于 if not args.dry_run: 条件保护之外,却在退出时会自动提交事务的 with sqlite3.connect(...) 代码块之内:
for user_id, email in users:
print(("would deactivate " if args.dry_run else "deactivated ") + email)
conn.execute("INSERT INTO audit_log (user_id, action, at) VALUES (?, ?, ?)",
(user_id, "deactivate", args.today))
if not args.dry_run:
conn.execute("UPDATE users SET active = 0 WHERE id = ?", (user_id,))
执行者自己提供的证据无法暴露这个问题,因为他只检查了打印出来的列表和 users 表。任务发起人的检查会比较整个数据库,因此发现了问题。
Kofi 填写“sent back”,并说明原因。
$ python3 handoff.py 3-sent-back.md
next: Runner (missing: a new Delivered commit (sent back: the dry run inserts 3 audit_log rows, so the log would say 3 users were deactivated when none were))
执行者修复问题。 修复方式是把两处写操作都移进同一个代码块,只在实际执行时运行。Arman 将第 2 部分更新为 commit 677d974。旧的审查结论仍然留在第 3 部分,检查器不会允许它沿用到新 commit:
$ python3 handoff.py 4-redelivered.md
next: Reviewer (missing: review of 677d974 (last review was ebe6594))
审查者验收通过。 Kofi 对 677d974 重新运行检查:dry run 前后的数据库导出哈希值都是 68cec51f91d1,实际执行修改了 ID 为 2、4、7 的用户,第二次执行找到了 0 个用户。他将第 3 部分更新为“accepted”。
$ python3 handoff.py 5-accepted.md
next: Operator (missing: Operator, Target, Dry-run output reviewed, Window, Undo plan)
操作人员安排执行。 Ines 当值,拥有生产数据库的访问权限。她填写第 4 部分:目标是生产环境的用户数据库;实际执行前,与 Nadia 一起审阅 dry-run 输出;执行时间是每周备份完成后的周二 10:00;回滚方案是针对本次执行产生的 audit_log 记录中的用户 ID,执行 UPDATE users SET active = 1。
$ python3 handoff.py 6-ready-to-run.md
next: nobody, handoff complete
生产环境中的实际执行不在这个演示仓库的范围内。第 4 部分记录的是计划,我并不是说已经真正执行过了。
回滚方案依赖 audit_log 的准确性,而第一个 commit 恰恰会破坏这一点。这就是为什么操作人员在执行任何操作之前,都应该先看到审查者的结论;检查器也强制要求这一点。
下面分别是将 Arman 同时填为执行者和审查者的文件,以及验收记录仍然指向旧 commit 的文件:
$ python3 handoff.py x-self-review.md
next: Operator (missing: Operator, Target, Dry-run output reviewed, Window, Undo plan)
PROBLEM: runner and reviewer are the same person
$ python3 handoff.py x-stale-accept.md
next: Reviewer (missing: review of 677d974 (last review was ebe6594))
PROBLEM: acceptance is for ebe6594, but the runner now delivers 677d974
两者都会以状态码 1 退出。
任务文件是纯文本,因此可以与代码放在一起,在同一个 pull request 中接受审查。使用任务跟踪工具也能提供同样的结构,同时减少手动填写。面向人类与 AI 团队的工作管理,需要让认领、交付和验收始终作为独立记录存在,并分别由不同的人负责。Wagglet 关于如何让人类与 AI 的任务配对真正有意义的文章,讨论了人员协作这一面:谁来选择任务、如何认领和释放任务,以及为什么交付不应该悄无声息地变成验收通过。
检查器相信人们填写的内容。它无法判断 Kofi 是否真的运行了 acceptance.sh。请粘贴输出,或者在 CI 中运行检查并附上对应任务的链接。
姓名只是字符串。“Arman”和“arman”会被视为不同的人。在实际系统中,应该比较认证系统里的用户名。
prod 规则采用子字符串匹配。它能捕捉明显的情况,但无法识别不包含“prod”的生产环境主机名。
这个示例仅适用于 SQLite。对于小型测试数据库,比较导出内容的哈希值是可行的。对于真实数据库,应比较脚本获准修改的表,同时检查不允许修改的表的行数。
操作人员部分是一份计划。本示例在进入生产环境之前结束。
本文在 AI 辅助下撰写,并由我审阅。团队成员是虚构的;代码、commit,以及上文的每一项输出,都来自本地实际运行。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。