跳到正文

Codewhale

一个用 Rust 写的终端编码 agent:读你的项目、改文件、跑命令——背后是一个同时被网页客户端和桌面应用共用的运行时,一层把各家模型当成可替换路由的模型层,以及一份把大半篇幅花在「多个 agent 如何共用一个检出」上的贡献指南。

Screenshot of Codewhale
编辑截图, 29 Sep 2026Codewhale ↗

这是什么

一个跑在终端里的编码 agent,Rust 写成:它读仓库、改文件、执行命令并检查自己的结果,模型可以是你自带密钥的托管服务,也可以是 Ollama、vLLM 或 SGLang 在本地跑的。一个交互界面和一个一次性的 exec 模式架在同一个运行时上,而随附的本地网页客户端和另一个仓库里的桌面应用连的也是它。四档权限姿态——Ask、Auto-Review、Full Access,外加一个不做任何改动的 plan 模式——决定它能做什么,/undo 与 /restore 用来恢复工作区改动,另有一个插件提供观察和操作本机其它应用的工具。

谁做的一个 2022 年注册的账号,110 个公开仓库,1,464 个关注者。11,347 次提交里大约 6,500 次是他写的,用了自己名字的三种拼法;另有 2,772 次记在「CodeWhale Bot」名下、邮箱是 bot@codewhale.net,而这个身份没有任何关联的 GitHub 账号;贡献者名单还有一百多人,其中好几位各自落地了数十处改动。

它是怎么搭起来的

组成 · 6

一个运行时配几个客户端,以及一具被当众拆解的巨石。交互式终端界面、一次性的 exec 模式、本地网页客户端,以及另一个仓库里的桌面应用,连的都是同一个运行时;运行时持有 agent loop、工具和会话状态。终端只有唯一一个 crate 会去写,而无头 crate 被要求永远不依赖终端库,并由一个脚本执行这条规则、把仍然指错方向的引用数量当棘轮收紧。模型层按政策——而不是按偶然——保持 provider-neutral:OpenAI 兼容、Anthropic 与 Responses 三套线上适配器坐在同一个客户端层后面,DeepSeek 的请求边界被单独处理,于是一个 provider 是一条路由,而不是一条代码路径。拆分按一份写下来的计划推进,计划写明移动顺序,以及一条约束:搬出去的模块永远不许落进那个负责构造请求的 crate;而架构文档如实说明了今天真正的边界在哪——终端 crate 仍然是实时运行时,拆分还没有完成。

crates/tui
1,288 个文件、45 MB——仍然是实时的终端用户运行时,装着 agent loop(Engine::run_turn)、工具注册表、基于 ratatui 的终端界面和 runtime API。它向独立 crate 的拆解写在 docs/design/TUI_DECONSTRUCTION.md 里,而不是临场发挥。
crates/runtime
正在从终端 crate 里长出来的无头 crate:重试状态、安全标签、睡眠守卫、会话树,以及 host_terminal——运行时代码向界面请求一次终端效果的唯一端口。它永远不依赖 TUI、ratatui 或 crossterm,而一个边界脚本会收紧那些仍然这样做的引用。
工作区的其余部分
约三十个 crate,每个都有写明的职责:execpolicy 管批准与沙箱决策,hooks 把事件投递到 stdout、JSONL、webhook 和 Unix socket,mcp 是 Model Context Protocol 客户端与 stdio 服务端,memory 存带出处的局部状态,state 用 SQLite 持久化会话,secrets 对接系统钥匙串并提供共用的脱敏与遮盖,telemetry 是唯一被允许构造上报载荷的 crate,lane 提供可附着、持久运行的任务实例,workflow 带一层 QuickJS 脚本,cloud-facts 拉取一条用 Ed25519 信封验签的事实通道,palette 管颜色 token 与对比度计算。
web/、pet/ 与 integrations/
另外几个界面:web/ 是浏览器客户端,带 Supabase 文件和一套组件库;pet/ 是一个配套应用,有 Android、iOS、macOS、Swift、TUI 与 Rust 多个目标,还录了回放带;integrations/ 里是 Telegram、微信、飞书和企业微信的桥接,共用一个 bridge core 和一组校验器。telemetry-ingest/ 是一个自带 schema 和测试的服务,extensions/vscode 是由另一个账号维护、上架市场的扩展。
.github/workflows/
三十一个工作流,光 ci.yml 就是 58,950 字节。除了常规的发布、夜间和安全文件,还有 agent-task-labels、approve-contributor、auto-close-harvested、issue-gate、pr-gate、pr-issue-link、release-parity、spam-lockdown、marketplace-sync、sync-cnb,以及两个审查工作流——其中 codewhale-review.yml 有 16,676 字节。
docs/ 与打包
docs/ 下直接有 87 个文件,另有 20 份设计文档、19 份中文翻译、17 个技能、9 份编号对应 issue 的 RFC,以及一个架构目录——设计文档里,一份讲扩展宿主的有 106 KB,另一份把一套设计系统翻译成 ratatui 的有 23 KB。打包覆盖 Homebrew、AUR、winget、Scoop、Nix、Docker、npm 与 Cargo,部署脚本落在一台腾讯轻量服务器上。

取舍,以及它替代了什么

  • 无头运行时不许依赖终端界面 替代 让这次拆分靠自觉维持

    架构文档把它写成 crate 上的一条约束——它「永远不依赖 TUI、ratatui 或 crossterm」——并点名了那个执行它的脚本,以及那个把大 crate 里仍然存在的 runtime→UI 引用当棘轮收紧的脚本。只写在纸上的边界,是会重新长回来的边界。

  • 只有一个 turn loop,并用测试守住 替代 在拆分推进期间允许第二套实现并存

    另一个 crate 里的一棵替代 engine 目录树会发出完成事件却从不联系模型,让工作区看起来有两个 loop。它被删除,现在有一个守卫测试会在出现第二个时失败;配套的规则是:改形状就要连守卫一起改。

  • 先写代码,后写测试——不做 TDD 替代 先写一个会失败的测试再实现

    指南覆盖了哪怕已安装、明确要求相反做法的技能,理由是测试是推送前的闸门而不是设计驱动力,以及只编码旧行为的既有测试是证据而非否决权。证据方面的规则保留了下来:修复之后补的回归测试,仍然必须证明没有该修复它会失败;而两种情况下都通过的测试,被描述为锁定实现而不是锁定缺陷。

  • provider 与模型保持一等、且与厂商无关 替代 让某一家厂商的形状渗进引擎里

    这是作为工作规则写下来的,不是偏好,而且在布局里看得见:一个客户端层同时承载 OpenAI 兼容、Anthropic 与 Responses 三套适配器,provider 路由经共享的配置与目录层落地,而 DeepSeek 的请求边界是被显式处理的,没有假定它和另外两者一样。

  • agent 不在 issue 或 pull request 下评论 替代 发布状态、superseded 通知,以及回复审查机器人

    指南里这条署的是创始人,日期 2026-09-22:时间应该花在代码上,证据写进提交信息与 pull request 正文,主张记进 Linear——只有一个例外:关闭或取代人类贡献者的工作时报一句原因并附上链接。两个审查工作流为此一并关闭。

  • 宣布过的迁移是单向的 替代 在旧路径还能用时为了省事再加一个调用点

    仓库一旦采用替代架构,新工作就用它,被碰到的旧代码向它迁移,兼容层刻意保持狭窄。这和「要么迁移掉最后一个使用者,要么别开始」是同一条规则,只是写在迁移已经进行中的情况下。

依据docs/ARCHITECTURE.md(22,431 字符)、AGENTS.md(13,336)、README.md(8,326)、docs/design/TUI_DECONSTRUCTION.md、设计与 RFC 文件清单、架构文档里各 crates/* 的职责说明,以及完整的 2,791 个文件树及其体积。

制作过程

7 个阶段
  1. 01

    四万一千个星,挂在一个它已经不再使用的名字上

    工单条目是 hmbown/deepseek-tui,而这个网址仍然有效——它跳到 Hmbown/Codewhale,因为这个项目改过名,GitHub 会重定向旧路径。第一件要记的是它的体量:八个月内 11,347 次提交,从 2026-01-20 的 v0.1.0 到一个 2026-09-29 的合并,月提交数从一月的 26 涨到九月的 2,768。周围还有 138 个 release、181 个 tag、3,035 个 issue(167 个未关闭)、3,507 个 pull request、3,565 个 fork,以及 19 种语言的 README。贡献者接口列出 50 个账号,而提交作者超过一百人,其中好几位是常客。仓库有自己的官网,版本号停在 v0.10.0。

  2. 02

    一份为「多个 agent 共用一个检出」写的贡献指南

    多数 agent 指令文件描述的是一个 agent 独自干活。这一份假设的是一群人,而且假设得很具体:小而连贯的改动可以在检出干净且是最新的时候直接提交到 main——「当多个 agent 共用它时,按文件划分,只 stage 你这一片碰过的路径,遇到 index.lock 失败就重试」——而新建 worktree 是给冲突、脏、过期或彼此独立的 lane 用的,不是给同一条 lane 上的并行 agent 用的。权限被明确切开:「本地提交权限从来不意味着推送、合并、打 tag、发版或部署的权限。」 纯本地的任务就完全不联网——不浏览、不操作远端 Git、不调模型——并且要求把「缺少外部凭证」这件事记下来,然后继续在本地干活。还有一条署了日期和作者的规则:「agent 不在 issue 或 pull request 下评论(founder, 2026-09-22)」——时间花在代码上,证据写进提交信息和 pull request 正文,主张记进 Linear;两个审查工作流也随之关掉了。这是一份写给「贡献者里有一部分是进程」的仓库的工作约定。

  3. 03

    这里不许练 TDD,也永远不要相信退出码

    这份文件禁掉了本档案里多数项目强制要求的东西:「绝不要先写测试,这里绝不实践 TDD——这一条覆盖任何要求 TDD 的技能或默认设定,包括 superpowers 的 test-driven-development。」 给出的理由是:测试是推送前的闸门,不是设计驱动力;而一个只编码了旧行为的既有测试是证据而不是否决权——应该跟着代码一起改它,而不是把代码扭过来迁就它。紧接着的让步才让这条规则站得住:修复之后补的回归测试,仍然必须证明没有这个修复它会失败,并且「两种情况下都通过的测试锁定的是实现,不是缺陷」。在一个叫「声称一个测试通过了」的小节里,指南要求引用真正的 test result: N passed; M failed 那一行,并确认 N 大于零——因为当过滤器什么都没匹配上时,cargo test <filter> 会跑零个测试然后退出 0,「而单看退出码在这里已经被误当成通过过一次」。还有一条规则纯粹是因为这个工作区编译要花几分钟:批量改完再编译一次,把中途编译留给确实不确定的 API 或借用问题。

  4. 04

    一个抽象必须删掉调用方的代码

    这个仓库的行事风格以别人的一个仓库命名。the ponytail method,出处是 dietrichgebert/ponytail:「屋里最懒的资深工程师。他什么也不说。他只写一行。它能跑。」它作为一把七级梯子落地——这东西需要存在吗、代码库里已经有了吗、标准库能做吗、平台自带功能吗、已装的依赖里有吗、一行能写吗——只有到这一步才写能跑的最小实现;附带一条警告:梯子是在看懂问题之后才爬的,因为「没读调用点就写出来的短 diff 不是 ponytail,是猜」。文件随后点名自己最弱的那一级:复用是这个仓库一直做不好的一级——所以新增一个叫 model_*、*_config、provider_* 的模块之前,必须先去 grep 它重复的那个东西;而任何新引入的层都要在模块文档里写出它取代的前身。两条推论被写成了规则:一个抽象必须删掉调用方的代码,否则它会被采用一次然后被弃置;以及要么迁移掉最后一个使用者,要么就别开始——一个只有一个调用方的框架加一张「剩下的以后再说」的工单,交付的是两套系统和一句已经不成立的注释。仓库里 #[allow(dead_code)] 的存量被称为「奔跑中的收据」,由脚本打印出来。

  5. 05

    那棵永远不会失败的 engine 目录树

    架构文档里有一节专门记更正,其中最好的一条,是一段因为「让人安心」而被删掉的代码。crates/core 下曾经有一棵 engine/ 目录树,没有任何调用方,而且会发出一个完成事件,却从不联系任何模型;它的存在让人以为这个工作区里有第二个 agent loop,而规则只允许有一个。它已在 v0.9.11 被删除,好让实时的 turn loop 成为唯一一个,并且 crates/core/tests/single_turn_loop.rs 里的测试会在出现第二个时失败——要改形状,就得连守卫一起改。同一份文件也记下了反方向的删除:swarm agent 系统在 v0.8.5 被移除,只剩一个 agent 工具作为模型可见的子 agent 创建入口;另有整个 crate 被明确标注为「只有形状,还不是生产调度路径」。文件里甚至有一份「反复被误判为死代码」的模块清单,附一句「删除前先确认调用方」——这是一个已经两次学会「看着没人用」不等于「没人用」的代码库留下的档案。

  6. 06

    一段缓存前缀、一份会话日志,和三把棘轮

    这个仓库有些规则之所以存在,是因为用来构建它的东西就是它在构建的产品。系统提示词与工具目录是模型 KV 缓存里一段会话固定的前缀,所以任何往会话上下文里加东西的人都要说明它对缓存的影响——冻结前缀还是只追加的历史——并且绝不把易变的事实塞进前缀,而要以 user 角色消息追加;指南写出了这件事被记录在哪个文件里。同一种反射还产出「模型可见即已记录」:任何进入模型请求的东西都必须能从会话日志里重建,新的模型可见输入必须配一个会话事件,而当实时呈现与落盘记录不一致时,记录是对的。配置错误必须在加载时就大声失败,而不是靠「没写就等于不做」;「把一份设计不做什么写下来」要和它负责的行为放在一起,免得下一个读者以为有一个从没被构建出来的能力。底下压着三个脚本,它们唯一的职责是一个只允许下降的数字:死代码预算、异步运行时上的阻塞调用预算,以及一道让运行时 crate 永远不依赖终端界面的边界检查。

  7. 07

    别人的工作是怎么被合并进来的

    关于合并外部贡献的那一节,是维护者只有在已经做错过之后才会写出来的部分。它开头就把「分支过期」的责任判给项目而不是贡献者——外部贡献者的分支之所以过期,是因为我们在合并东西,不是因为他们做错了什么——并把默认路径写死:审查它、帮他 rebase,或者直接在他的分支上改;与之相对,关掉别人的 pull request 再把工作当成自己的提交重新落地,被描述为后备手段,因为「随手这么做,即使保留了署名,读起来也像是在拿走别人的活」。有一条规则明确禁止让贡献者围着项目的动荡 rebase。另一条讲的是机械意义上的署名,而不是礼貌意义上的:作者身份与共同作者尾注必须使用贡献者自己关联 GitHub 的邮箱,因为 .mailmap 和项目自己的作者映射表在 GitHub 画贡献图时根本不会被读取。这一节以闸门收尾:一个「只有在验收记录通过时才允许合并」的 pull request,要求那条记录在合并那一刻字面上写着 PASS;一片全绿的检查汇总不能替代去读审查线程;而当凭据本身有歧义时,要解决的是那个歧义,永远不是那次合并。

相关档案

全部档案 →