上周的一次 Claude Code 会话告诉我,我的订单队列里有 180 个未处理项,其中没有一项超过 13 天,而 254 条记录中的 221 条是由 Claude Code 会话写入的。它统计了自己。系统里最大的单个邮箱正是系统自身:36 条订单,其中约 30 条是工具在改进自己的工具。
我已经有一段时间没看那份列表了。这才是值得记录的部分。
本文内容:每个项目一个 TODO.md 在达到十个项目时如何失效、最终定型的替代方案、三个失败案例以及每个案例留下的闸门,以及整个系统至今仍让我付出的代价。如果你同时处理多个仓库,多到无法全部记住,那么中间部分值得借鉴,无论你用什么工具驱动它们。
十个项目与一个 Markdown 文件
去年年底,我开始用 Claude Code 跨多个项目工作。那时整个系统就是每个项目文件夹里的一个 TODO.md。我打开一个会话,告诉它需要做什么,文件负责记录其余部分。对于两三个项目来说,这完全没问题,我会推荐这种做法。
后来项目变成了十个。十个活跃项目,十个 Mac Studio 上的文件夹,十个 TODO 文件,三件事同时开始出错。
想法在回家路上遗失了。我在外边突然想到什么,而记下来的地方是项目文件夹里的 Markdown 文件,机器却不在我面前。等我回去,想法已经消失了。
早晨变成了一次我不想做的决定。十份列表,没有共享视图,也没有跨列表的排序。今天我需要处理哪个项目?诚实的答案通常是我上次碰过的那个项目,这恰恰与优先级相反。
项目会连续几周保持沉默。不是因为它们已经完成,而是因为我的设置里没有任何东西会主动举手。一个文件不会告诉你它已被忽略。
大约一个月前,我决定需要一个能告诉我什么最重要、跨越所有项目的工具。不是又一份列表,而是列表之上的那一层。
现在的样子
上周我做了之前一直拖着的事:写了一份手册。不是代码文档,而是给操作者的手册,而操作者就是我。记录我在哪里做什么、存在哪些命令、笔记如何变成订单,以及哪四个地方会停下来等待人工。它有八个章节。写这份手册的过程让人不舒服,但这种不舒服是有益的,因为一个需要为其唯一用户编写手册的工具,正在告诉你它是如何成长的。
以下是那份手册中可泛化的部分。任何可能变成工作的事都会进入一个收集点,然后只走一条路径。
(此处省略示意图。两张流程图在 原文 中。)
在这背后运行着两个时钟装置,它们都不会思考。一个 launch agent 每 15 分钟清空一次投递箱(StartInterval 900),把条目分拣到对应项目的列表里。另一个每 30 分钟(StartInterval 1800)取出已批准的订单,在 ~/.cache/master-dispatch-worktrees 下的 git 工作树中以无头模式运行,故意放在所有仓库之外。排序步骤不涉及任何模型,这就是它可以如此频繁运行的原因。
我用得最多的部分其实最不聪明:我给自己发一封邮件。
(此处省略示意图。两张流程图在 原文 中。)
这就是我遇到的第一个问题的解决方案。我永远能触达的地方是自己的收件箱,所以它成了入口点。它做不到的是把笔记变成工作。笔记变成订单后就停在那里,这也引出了为什么需要闸门的原因。
三个会话,一个文件
7 月 17 日,我有三个 Claude Code 会话并行运行,它们都在修改一个保存各项目支出限制的共享配置文件。每个会话读取文件,修改自己的那一行,然后把整个文件写回去。这是经典的读-修改-写操作,只是写入者是代理而非线程,而我当时根本没把它当作并发问题来考虑。最后写入者获胜。一个会话的异常被下一个会话静默覆盖。
修复方案很平常:共享资源现在只能通过一个小的 mit-lock 包装器来写入,这是一个基于 mkdir 的锁,带有 10 分钟的过期超时和审计日志。一个 pre-tool hook 会拒绝任何不经过该包装器就写入受保护路径的 Bash 命令。
不平常的是背后的认知转变。我之前一直把并行会话当作并行的人类,以为它们会注意到。它们不会注意到。它们是进程,当两个进程共享状态时,你就欠它们与对待两个线程相同的纪律。这就是其他一切都建立其上的三条规则的由来:每个仓库只有一个写入者,每个共享资源只有一个所有者,跨项目工作只有一个通道。
制造的工作多于完成的工作的工具
这把我带回到开头的计数。
到 7 月 28 日,队列里有 180 个未处理订单,没有一个超过 13 天。这意味着自 7 月 20 日以来,系统每天产生约 20 个新订单,而我每天只能关闭 2 到 5 个。254 条投递箱记录中有 221 条是由 AI 会话而非我本人写入的,其中 163 条来自编排仓库自身的会话。
没有东西坏掉。每一份订单都是合理的。这恰恰是问题所在。Claude Code 会话想要变得有用,而如果你在任何真实的仓库里打开一个,它就会发现十个真正的改进点:一次可以更干净的重构、一份过时的文档、一个可以更严格的测试。把十个合理的观察乘以十五个仓库,你得到的就是单个人永远排不完的洪流。我的收件箱里填满的不是错误,而是好主意。
修复方案颠倒了举证责任。在此之前,一份订单只要没有人主动删除就存在。现在它只有在能命名触发条件时才会出现:影响到用户或客户的故障、对安全、金钱或数据的风险、部署阻塞,或者我亲口说出来。“会更干净”“顺便注意到”“为了保持一致”都不是触发条件。这些会进入我阅读的会话摘要,而不是进入流水线。反向应用这条规则,一次就归档了 19 条元订单,把总数从 165 降到 144。
我故意没有在这里构建的东西才是有趣的。显而易见的做法是让 AI 看门人:让模型判断每份 incoming 订单并拒绝噪音。我没有这么做,因为同一周有一份措辞糟糕的订单通过了,它很长、结构良好且完全可信,如果按字面执行,就会把 186 个客户邮箱的出站邮件路由到一个从未设计来承载它们的服务的。模型看门人会直接放行。它会包含它本该捕捉的失败类别。有些问题不会因为再加一个模型而变得更好。
看错东西的守卫
最近的一次是昨天,这是我第四次遇到同一类失败。
“一个会话只能写入自己的仓库”这条规则由一个 pre-tool hook 强制执行,直到昨天,这个 hook 还是通过查看命令来工作的。它知道写入外部路径的命令形态:输出重定向、tee、git -C、sed -i。7 月 28 日,一个 rollout 脚本通过 Python heredoc 写入了 21 个外部仓库。hook 看到的是一个带字符串的 python3 调用,就让它通过了,因为 heredoc 不在看起来像写入的列表里。
修正方案是停止猜测意图。现在有一个检查,会在自主运行前记录每个仓库的 git 状态,并在运行后进行比较。它不在乎变更如何产生,只在乎是否在不该出现的地方出现了变更。它在三个地方 fail closed:缺少检查会阻止 dispatcher 启动,基线失败会中止订单,缺少基线会被视为差异而非全清。
模式匹配会猜测命令将要做什么。比较前后状态会测量它实际做了什么。我花了四次尝试才学会这一点,而我不会在第一天就设计成这样。
成本与仍未解决的问题
编排层包含 32 个 shell 脚本和大约 6300 行代码,有 145 个测试针对副本运行,从不触碰真实订单。二十一个 hook 在工具调用之前、期间和之后触发:仓库边界、资源锁、阻止曾破坏 199/200 行的数据库 dump 标志的检查、拒绝包含未在验证列表上的事实的外发邮件。没有任何东西是设计出来的。每一块都出现在有东西穿过去的地方。
仍然有四个地方会停下来等待我,这就是没有意外发生的原因。一份订单在获得我批准前什么都不会做,包括我自己提交的订单。标记为 dialogue 的工作不会在我不在场时运行。完成的工作永远不会自己合并。任何文本中提到付款、客户或生产部署的内容,即使已获批准,也会在启动前被拦住,因为 7 月 27 日,有一笔 50 欧元客户退款正坐在队列中,已获批准,标记为自主,即将执行。
今天诚实的状态是:账面上有 149 份订单,其中 122 份已批准并等待中。触发规则减缓了流入,但没有逆转它。根本的张力没有解决,我也不确定能否解决,因为这不是工具里的 bug。一台生成候选工作的速度快于人类评估速度的机器,永远会以队列告终,而我建造的每一道闸门,都是我故意选择让自己成为瓶颈的地方。
如果去年年底我就用十个项目、每个项目一个 Markdown 文件的起点开始,我会做的不同之处是:先构建投递箱和批准闸门,其他什么都不建。这两部分是我唯一从未需要修复的部分。这个系统里其他的一切,我都造了两次。
如果你也在运行类似的东西,并且找到了一种在不让人逐条阅读的情况下保持流入诚实的方法,我很想听听。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.