这次暴露出来的问题,不是模型不够强

这轮真正把问题暴露出来的,不是写作本身,而是发布链路。几篇文章在不同 Console 里并发推进时,表面上看,每个窗口都在各写各的,互不相干。可一旦进入正式发布,问题就堆到一起了。

主干会同时前进,部署又还是 VPS 上的手工链路,长一点的 scpssh 命令还可能超时。于是系统开始出现一种很典型的错位:前半段是并发写作,后半段却没有一个真正的总控层把这些并发结果收口。

所以真正要重构的不是提示词,不是哪一个 agent 够不够聪明,而是项目控制层。只要这层没有收口,多开几个 Console 只会把混乱放大得更快。

多窗口并发写作本来就没错

很多人一看到“多窗口同时写、同时提交、同时部署”,第一反应是把并发本身当成问题。其实不是。并发写作本来就应该存在。你写 Karpathy 的文章,我在另一个窗口改一个专题导读,再开第三个窗口整理玩家信号,这些动作本来就不应该互相阻塞。

真正的问题是,过去这几种动作被放在了同一条隐式流水线里。写作阶段和发布阶段没有被严格拆开。于是本该只在本地并发的东西,一路冲进了主干、部署和验证阶段。

正确的做法应该是:写作并发,集成串行,部署串行。前面可以像多线程,后面必须像单写者数据库。

为什么这次不先上 jj

jj 有它的优点,这一点不需要回避。多个 workspace、自动快照、对远端状态更严的安全检查,确实都很适合并行开发。要是只讨论“本地多个工作区怎么舒服一点”,它甚至比裸 Git 更顺。

但这次要解决的不是版本控制层的体验,而是项目控制层的秩序。jj 会让本地 worktree 和冲突管理更顺,不会自动替你回答下面这些问题:

  • 谁可以推进 master
  • 哪个发布单已经过门禁
  • 现在是哪个 SHA 在跑 staging
  • 生产部署超时以后从哪一步恢复
  • 哪个 Console 正在占用哪篇文章

这些问题不属于 VCS 的舒适度问题,而属于状态机问题。所以 jj 不是没有价值,只是不应该排在第一位。

GSD2 为什么比 jj 更接近答案

GSD2 最有价值的,不是它的命令表,而是它把项目推进本身当成一个系统来设计。官方文档里反复强调几件事:状态要落盘、worktree 要隔离、长任务要拆成 milestone 和 slice、超时以后要能恢复、并发执行以后回主线时要顺序收口。

这套思路和我们眼前的问题是对得上的。我们现在缺的,恰恰不是再多一个“能写代码的 agent”,而是一个能把下面这些东西统一起来的控制层:

  • workstream 的所有权
  • release candidate 的状态
  • 主干合并的单写者策略
  • staging -> production 的部署锁
  • 超时以后的恢复点

换句话说,GSD2 真正有价值的地方不是“它也能跑 agent”,而是“它默认就把系统当成状态机来看”。

但这不等于今天就把系统整体切到 GSD2

这里最容易犯的错,是从一个极端跳到另一个极端。看到 GSD2 的状态机更对路,就想立刻把 OMX 全拆掉,然后让 GSD2 全面接管。这个动作看上去很干净,实际未必稳。

原因很现实。GSD2 官方默认的交付模型更偏向 PR 合并后的 Dev、Test、Prod promotion pipeline,而且更像 package 或 npm 生态。Freelemon 当前的现实不是这样。我们现在还是 Git 主干、VPS、systemd、Gunicorn、内容站验证脚本这一套。

如果今天直接整套替换,风险不是“工具不好用”,而是“新控制层和现网适配器还没对齐”。最后很可能不是更干净,而是两套世界观在仓库里对撞。

所以这次更稳的答案不是“整体换血”,而是“先让 Freelemon 长出一套 GSD2 风格的控制层”。

OMX 真正的问题,不是弱,而是层级不对

OMX 在这套系统里不是完全没价值。它对 Codex 的交互体验、技能调用、会话记忆和多代理编排,确实是有效的。把它当成交互外挂层,它是成立的。

问题出在过去默认让它承担了太多“总控”的期待。可在这个仓库里,正式发布主线实际上还是 Git + runbook + VPS 部署。也就是说,真正的主干推进、部署锁和恢复逻辑,并没有被 OMX 真正接住。

这就是为什么我们这次更接近的结论不是“OMX 不好用”,而是“它更适合执行层,不适合继续兼任项目总控层”。

迁移期内最合理的位置,是让 OMX 退回到自己擅长的地方:交互、技能、子代理执行。至于谁来管主干、发布、部署和恢复,应该交给单一控制面的状态机。

新系统应该长成什么样

我现在更认可的结构,只有三层。

第一层是 workstream。一条 workstream 对应一个 worktree、一个 branch、一份状态文件,以及一组明确声明的 claimed_pathsclaimed_slugs。谁在写什么,必须先声明,再推进。

第二层是 release candidate。它不是“当前目录里有什么就发什么”,而是一次明确的发布单:来源于哪个 workstream、基于哪个主干 SHA、包含哪些文件、包含哪些 slug、门禁有没有通过。

第三层是 deployment job。一个 deployment job 只做一件事:把某个已经合入主干的 SHA,按顺序推进到某个环境里,并把状态写下来。它必须知道自己现在是卡在 bundle、upload、install、reload 还是 verify。

在这三层之上,再加三把锁:

  • master 合并锁
  • staging 部署锁
  • production 部署锁

这样多窗口写作就还能保持并发,但一进入共享资源,就自动变成串行。

为什么超时和提权也必须进设计

如果把这次重构只理解成“多窗口状态管理”,最后一定还会再慢回来。因为真正拖慢流程的,不只是冲突,还有长命令超时和提权碎片化。

这次真实链路里已经出现过:

  • 目录直传中途断开
  • ssh 命令超时
  • 服务已经 reload,但生产目录里缺一个文件
  • 修复时不得不重新拼一段提权命令

这些都说明一件事:超时和提权不是执行层的边角问题,而是控制层必须直接面对的设计问题。

所以后面的系统必须把部署拆成可恢复阶段:

  • bundle_built
  • archive_uploaded
  • remote_unpacked
  • files_installed
  • service_reloaded
  • verify_passed

只要状态写下来了,超时就不再意味着重头再来。提权也一样,不能继续落在一次性的长命令上,而应该收口成少数稳定入口。

这轮已经开始怎么落地

如果只把上面的判断停在文档里,这件事很快又会回到‘每次出问题再临时补一条命令’。所以这轮讨论完以后,我们没有再停在工具比较,而是先把控制面的骨架直接落进仓库里。

第一步是把状态真源单独拎出来。现在仓库里已经开始出现一套新的 .gsd/ 结构,里面分别记录 workstream、release candidate、deployment job 和 lock。谁在写哪篇文章,哪次发布基于哪个 SHA,要不要推进 staging,这些信息不再只靠会话记忆和窗口上下文保存。

第二步是把入口固定下来。前面已经先落了 scripts/control_plane.pyscripts/workstream_manager.pyscripts/release_manager.py,把并发写作登记、发布单组装和主干锁管理收口成稳定脚本。接着又补了 scripts/build_release_bundle.pyscripts/deploy_release.py。后者现在还没有直接接管真实远端部署,但已经先把‘拿环境锁、创建 deployment job、推进阶段、完成或失败时怎么收口’固定下来了。

这一步其实很关键。因为用户后面真正烦的,不只是流程乱,而是反复被提权和等待打断。过去那种做法,是每到一段长命令就重新弹一次。现在更合理的思路是:普通写作、内容门禁、状态查看默认不提权;真正需要高权限的发布动作,逐步收口到少数固定入口。这样即便以后仍然需要授权,也应该是对稳定前缀做一次性授权,而不是每次重新为一串临时命令停下来。

第三步是把这套骨架写进验证。我们已经补了针对 claim 冲突、独占锁、deployment job 状态推进和 bundle 生成的单元测试。这样这轮重构就不只是‘理念更顺’,而是开始有最基本的可验证行为。

换句话说,这件事现在已经不是‘准备以后做’,而是已经迈进了第一段真正的实现。后面要补的,是把真实的远端上传、解包、reload 和验证动作接进 deploy_release.py 这类稳定入口里,而不是再倒回去拼一串 ad hoc 命令。

我们最后为什么没有选“直接替换”

因为这次真正想要的不是换一个更潮的工具,而是把系统的控制层收紧。只要控制层还没长出来,今天换 jj,明天换 GSD2,后天再拔 OMX,本质上都只是工具层表面迁移。

真正有价值的顺序应该是:

1. 先定义单一控制面的状态机。 2. 把并发写作和串行发布的边界写死。 3. 把部署变成可恢复流程。 4. 再决定 OMX 最终保留多少。 5. 最后才决定要不要把 jj 或 GSD2 的更多部分接进来。

这个顺序看上去不炫,但它更像系统工程,而不是工具崇拜。

补充进展

这篇文章写到这里以后,仓库里的第二阶段又往前推了一步。deploy_release.py 已经不再只是状态骨架,而是接上了真实远端执行链路:本地打 bundle,远端建临时目录,上传 archive,解包,按 manifest 安装文件,按变更类型 reload 或 restart,然后再跑 verify_site_publish.py。

更关键的是,这条链路现在开始真正利用 deployment job 的状态落盘。超时会被标记成 timed_out,环境锁可以保留,下一次可以从上一个已完成阶段继续,而不是默认整条发布链路重跑。当前仍然没有让系统自动改 Caddy 和 fail2ban,这部分继续明确走 runbook。

再补两件看起来小、其实很关键的事

这轮继续往前推时,我们又补了两层以前很容易被省掉的东西。第一层是监控。现在 deploy_release.py 不只是执行发布,还会把 deployment job、锁状态、命令摘要和服务快照写进 .gsd/monitoring/deployments 下面。这样以后就算超时、恢复或 reload 后异常,也不需要再从聊天记录和命令历史里倒推。

第二层是工作区整洁。过去老说工作区脏,不只是因为临时文件多,更因为多条写作链路共用一个根目录。现在至少先把 .gsd、.omx、临时脚本和发布 bundle 这类本地状态收口进 .gitignore;更根本的方案,还是让每篇文章进独立 worktree,把仓库根目录收回成集成和发布通道。

我的答案

如果只能给一句话,那就是:

这次该重构的不是 agent,而是控制面。

jj 值得以后再看,但不是第一优先级。GSD2 的状态机思路最接近我们真正缺的东西,但也不适合今天原样接管现网。OMX 不需要立刻粗暴移除,它更适合退回执行层,等新的控制层稳定以后,再决定最终去留。

真正应该先做的,是把 Freelemon 变成一个“多 workstream 并发写作、单写者集成主干、staging -> production 串行部署、所有长步骤可恢复”的系统。

等这层长出来以后,工具的选择才会自然收敛。否则,换来换去,只是把混乱从一套壳搬到另一套壳里。