@christang/keel

中文文档
5.227.0发布于 2026/11/08MIT构建来源已验证近 30 天下载 1.8万

Keel OpenSpec 执行纪律 CLI 用于 Claude Code, Codex, 和 OpenCode.

中文使用文档
文档目录29 节
  1. Keel 是做什么的
  2. 环境要求
  3. 安装
  4. 怎么用
  5. 常设授权
  6. 决策先例
  7. 无人值守运行
  8. 谁来合并
  9. Full 与 Lite
  10. agent 加载多少指导
  11. 这些命令,agent 是怎么用起来的
  12. 当正确答案是"什么都没变"
  13. 验证分层
  14. 声明一次红证明了什么
  15. 领域透镜
  16. 覆盖声明拿什么来核对
  17. 重新记录契约
  18. 暂停一个 change
  19. 归其他 worktree 所有的 change
  20. 外部模型 CLI
  21. 共享资源租约
  22. worktree 总览
  23. 会话之间的群聊
  24. 协调者巡查
  25. 负责人的语言
  26. 命令参考
  27. 开发
  28. 隐私
  29. License

Keel

English | 中文

面向 AI 编码 agent 的 OpenSpec 执行纪律 —— Claude Code、Codex、OpenCode、DSH 通用。

License: MIT Node Targets

Keel 是做什么的

OpenSpec 给项目一套 spec 驱动的工作流:proposal、 design、specs、tasks,以及记录改动的 archive。Claude Code(或 Codex)提供干活的 agent。 Keel 夹在两者中间,在 agent 走完一个 OpenSpec change 的过程中盯住它别跑偏。

放任不管时,agent 容易漂移:改了任务没提到的文件、上下文重置后丢了线索、或者没有证据就把活 勾成完成。Keel 加的是一层轻量、可校验的约束来防止这些,而且它建立在你已有的工具之上,而不是 替换它们。

Keel 加的东西:

  • 无状态连续性:keel context 每次会话都从 OpenSpec 和 Git 重算当前任务和下一步,所以工作 能扛住 /clear、compaction 和冷启动,不依赖对话记忆。
  • 确定性门禁:keel gate task-start | task-complete | change-close 做本地结构检查,返回 pass / fail / needs-review,并带真实的进程退出状态。它们只检查任务契约和证据是否齐备,不判断设计对错。
  • 写入守卫(Claude):task-start 之后,PreToolUse hook 会拒绝任何超出任务声明改动范围的 文件编辑。声明了 push:<branch glob> 时,它还会拒绝推到其他分支的 git push,有没有 task 都一样。
  • 预期对齐:在 specs 和 tasks 定稿前,Keel 把隐性假设摆出来,只针对真正会改变行为的那些提问。

Keel 尽量借力原生能力,而不是重造。spec 工作流就是原生 OpenSpec;执行技能、SessionStart 连续性 hook 和写入守卫 hook 都以一个普通的 Claude Code / Codex 插件分发。除了运行 OpenSpec 自己的 init, keel --init 还往你的 repo 里写一小块宿主面:AGENTS.md bootstrap 块、OpenSpec schema、 OpenSpec 工作流 skill 上的 overlay,以及在 Claude 上写入 .claude/settings.json 的插件自动更新条目。

环境要求

Node.js >=20.19.0(内置的 OpenSpec CLI 需要)。

安装

Claude Code —— 装插件就够了。插件就是发布版本的 npm 包 @christang/keel,和 keel CLI 来自同一个包, 所以技能和 hook 之外还带着 CLI;安装插件时 Claude 会按锁文件装好锁定版本的 OpenSpec(#337)。 agent 运行的 keel 命令用的就是插件带来的这一份:

claude plugin marketplace add TanglmChris/keel
claude plugin install keel@keel-marketplace

插件用你的 npm 客户端获取;只有 marketplace 清单 .claude-plugin/marketplace.json 从 GitHub 读取。 如果某台机器 clone GitHub 失败(SSH host key、代理等),可以改为用 url 类型的 marketplace 来源、通过 HTTPS 读取清单:

claude plugin marketplace add https://raw.githubusercontent.com/TanglmChris/keel/main/.claude-plugin/marketplace.json

或者在 ~/.claude/settings.json 里声明:

"extraKnownMarketplaces": {
  "keel-marketplace": {
    "source": {
      "source": "url",
      "url": "https://raw.githubusercontent.com/TanglmChris/keel/main/.claude-plugin/marketplace.json"
    }
  }
}

Claude Code 目前还不支持从 npm 包加载 marketplace(其参考文档把这种来源列为尚未实现),所以清单本身还不能也从 npm 来。

项目设置好之后,更新会自己到来:keel --init --target claude(以及 keel --install)会在项目的 .claude/settings.json 里为 keel-marketplace 声明自动更新,Claude 会先读这个声明,再考虑它自己默认的"关闭"。 新版本会在会话发出第一条消息后在后台下载。从 5.89.0 起,正在运行的会话的 hook 在下一次调用时就用上新版; 它的技能要在 /reload-plugins 之后或下次启动时才跟上。

想一次更新整台机器——全局 CLI、Claude 插件和 Codex 插件——就运行 keel --update。它为每个组件输出一行: 变了什么、何时生效、还剩什么要你做;细节以及各宿主对运行中会话的处理见 docs/updating.md。 不想自动更新,就把那一项的 autoUpdate 设为 false;项目写明的值 Keel 会保留,keel --doctor 会报告当前声明的是哪一个。

Anthropic 的插件目录里,Keel 以 keel-openspec 列出。每次发版都会把 npm 包的文件(清单改名、加上图标)提交到 claude-directory 分支,目录跟踪的就是这个分支。从本 marketplace 安装的插件仍叫 keel。

Codex,以及你自己的终端 —— 另外装一份 CLI(同时装上捆绑的 OpenSpec CLI):

npm install -g @christang/keel
keel --version
codex plugin marketplace add TanglmChris/keel --ref main --sparse .agents/plugins --sparse plugins/keel
codex plugin add keel@keel-marketplace     # Codex

--sparse 路径让 Codex 只拉插件本身,而不是整个仓库;网络慢时,整仓克隆可能超过 Codex 的 30 秒上限。

在 Codex 上,用 CLI 里的 /hooks 审阅当前的 hook 定义;安装本身不会授予 hook 信任。运行中的 Codex 会话在下一次 hook 调用时就用上更新后的插件,而改动了 hook 定义的版本需要再审阅一次。 用 keel mail role --set codex-maint 绑定一个邮箱角色;受信任的 hook 会在会话启动和下一次用户输入时 提示未读邮件,但不会把它标为已读。如果收件 hook 不可用,就显式使用 keel mail list 和 keel mail read。 Codex 闲着时不会自己醒来;在它的 worktree 里运行 keel chat wake add,有找它的消息到达时就会启动一轮(#203)。 原生的写入守卫执行仍未经验证。 版本、运行时证据、局限以及干净消费方上的 Full 模式检查,见 Codex 验收记录。

全局装的 keel 在 PATH 里排在插件那份前面,所以在 Claude Code 里 agent 会运行它。要么让它和插件同版本, 要么卸掉(npm rm -g @christang/keel);两者版本不一致时,会话启动那一行会指出来。

从 GitHub 安装最新未发布版本

打包当前 main 并安装该 tarball(跳过 npm registry):

Windows(PowerShell):

$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid())
New-Item -ItemType Directory -Path $tmp | Out-Null
npm pack github:TanglmChris/keel --pack-destination $tmp
$pkg = Get-ChildItem $tmp -Filter "christang-keel-*.tgz" | Select-Object -First 1
npm install -g $pkg.FullName
Remove-Item -Recurse -Force $tmp

Linux / macOS:

tmp_dir="$(mktemp -d)"
npm pack github:TanglmChris/keel --pack-destination "$tmp_dir"
npm install -g "$tmp_dir"/christang-keel-*.tgz
rm -rf "$tmp_dir"

怎么用

在项目根目录,先设置一次:

keel --init                 # 默认 target:claude
keel --init --target codex  # 或 opencode、dsh

--target dsh(DSH,DeepSeek Harness)写入、检查、移除的文件与 --target codex 完全相同:DSH 读仓库的 AGENTS.md 和 .agents/skills,所以没有单独的 DSH 副本,卸载其中一个会让另一个也失去这些文件。DSH 的出厂 profile 没有 session-start hook,所以六项能力都是 manual,入口是 AGENTS.md 的 bootstrap(「先运行 keel context」);keel --doctor --target dsh 会报告 PATH 上有没有 dsh。

keel --init 会跑 OpenSpec 初始化/更新,并写入 Keel 的宿主面。之后每次开始或恢复工作:

keel context                # 现在该做什么,从 OpenSpec + Git 重算
keel --doctor               # 检查各部分是否就位

要刷新 OpenSpec 生成的模板,运行 keel openspec update。更新成功后,Keel 会为仓库 中所有已安装的 target 恢复 overlay。keel --doctor 也会报告其他已安装 target 的 overlay 状态,所以 Claude 一侧正常不会掩盖 Codex overlay 的缺失。

用不用 subagent 由模型自己决定,subagent 与主 agent 同等对待:门禁、Touch 写入守卫 和证据规则适用于执行该步骤的任何一方。Keel 在 subagent 动手前不要求 brief、额外激活或另一个 agent 复跑,也不引导模型去用或不用它(#414)。

spec 相关的活走 OpenSpec 的工作流 skill(openspec-propose、openspec-apply-change、openspec-sync-specs、 openspec-archive-change);Keel 只生成 skill,不生成 OpenSpec 斜杠命令(#406)。Keel 自己的命令和可以手动调用的技能列在 docs/commands.zh-CN.md。要在任何项目里反馈 Keel 的问题,运行 /keel:feedback <一句话> 或 keel feedback "<一句话>":它给 Keel 仓库起草一份带版本、失败命令及其输出的 issue(--command、--output-file,其余用 --expected、--repro、--impact,--doctor 附上诊断输出),脱敏 token、Slack ID、邮箱和本机路径,列出相近的 issue,在运行 --create 之前不创建任何东西,而 --create 需要你的批准(--confirmed)或项目声明了 issue:TanglmChris/keel。Keel 的门禁在任务边界处运行。整个回路:

keel --init  →  keel context  →  openspec-apply-change(按顺序做 task)
   →  task-start(+ 写入守卫)  →  实现并验证
   →  task-complete  →  openspec-sync-specs · openspec-archive-change

在 Claude target 上,session-start hook 还会把这个状态展示给你,而不只是给 agent——在你输入任何内容 之前,显示一行:

Keel: add-user-auth#2.1 — next: task-start. Disposable projection; OpenSpec and Git are the authority.

设置 KEEL_SESSION_PANEL=1 可改为画成带 Keel 标志的边框面板。它默认关闭,打开后只改变呈现方式—— 两种形式里的状态和下一条命令完全相同。

宿主的 hook 没有报告的 compaction 之后,可以用 keel project --target <t> --event compaction --json 手动重新投影同样的状态。

写入守卫是一份一次性 manifest:keel/guard.json。在 Claude 上,通过的 task-start 会写入它(--no-guard 退出); keel guard start 显式写入,keel guard clear 删除。它记录 change 与 task、capsule 指纹、规范化后的 Touch 和权威文件的哈希,并且失败即关闭:Keel 读不了的 manifest 会拒绝编辑,而不是放行;它守的 change 不见了也一样拒绝, 除非那个 change 是在所守任务已勾选完成后归档的——那个任务已经结束,manifest 就不再拦任何写入(#491)。守卫检查文件编辑工具; Bash 命令造成的写入不检查,只有声明了 push: 范围时会检查 git push。

常设授权

对于没有授权依据的仓库操作,Keel 会先问;下次会话还会再问,因为对话里给的许可熬不过上下文重置。 改为在 keel/config.yaml 里一次性声明:

authorize:          # accepted names: commit, push, push:<branch glob>[@<role>], release, archive, continuation, issue:<owner>/<repo>[@<role>], protocol-refresh, chat-reply:<group>[@<role>], issue-close:<owner>/<repo>[@<role>], cleanup:scratch[@<role>], push-protected:<branch glob>[@<role>], shift-restore, self-shift, delete-branch:merged[@<role>], push-tag:<tag glob>[@<role>]
  - commit
  - push

没有自己写 Autonomy boundary: 的 task 会继承这份声明,编译出的 capsule 会把 keel/config.yaml 标为该条目的来源,这样继承来的授权永远不会被误当成 task 自己做的决定。自己写了边界的 task 保留自己的边界。

下文的序数按名字计,不按上面注释里的写法计:push 和 push:<branch glob> 是同一个名字,所以词汇共 15 个名字, continuation 是第五个。

continuation(第五个名字)会被接受,但什么都不改变。在你批准过 tasks.md 的同一个 change 里, 从一个已持久完成的 task 转到下一个未勾选的 task,默认就不需要确认(#414)。下一个 task 照样通过 keel gate task-start 启动、记录自己的 fingerprint;每个门禁、证据要求、语义 Review 和写入守卫都照常运行; 有自身触发条件的停止——阻塞、fingerprint 漂移、超出范围的需要、未解决的问题——照样停下。继续做下去不授权任何 仓库操作,也不调度任何东西。在词汇表早于这个词的旧版 Keel 上,这一条目无法识别,整份声明在改正之前不授权 任何东西——失败即关闭,绝不会悄悄放行。

issue:<owner>/<repo>(第六个名字)要写明它能触及的资源,不写就被拒绝;chat-reply:<group>(第八个)、 issue-close:<owner>/<repo> 和 cleanup:scratch(第九、第十个),push-protected:<branch glob>(第十一个),delete-branch:merged(第十四个),以及 push-tag:<tag glob>(第十五个) 也同样要写。其余名字作用于声明所在的 checkout,所以天然被限定在你声明它的那个仓库里。开 issue 用的凭据却不是: gh 是账号级的,一个裸的 issue 能触及你账号能碰到的每个仓库——悄无声息地成为文件里范围最大的条目,比 push 还大。写明仓库,授权的大小就和它字面说的一致。Keel 把这个范围带到 keel --doctor 和编译出的 capsule 里, 但不强制执行:它不调用任何 tracker 客户端,也观察不到它,正如它从不替你提交一样。关闭 issue 是另一个名字, issue-close:<owner>/<repo>(#257):由自动化 token 完成的合并不会触发 Closes #<n>,所以做完工作的会话要 自己关闭 issue,并附一条评论写明解决它的 pull request。Keel 以同样方式携带这个范围,同样不强制执行。

push:<branch glob> 把 push 收窄到会话可以推送的分支(#257)——例如在只有协调者推 main 的仓库里写 push:claude/* 和 push:worktree-*。每个 glob 一条;* 在一个 / 段内匹配,** 跨段匹配。和其他所有 范围不同,这一个是强制执行的:只要声明了任何 push: glob,Claude 写入守卫就会检查每个 Bash 调用里跑的 git push,有没有 task 都一样;推到其他分支的,或者看不出目标的——--all、--mirror、--tags、shell 变量、 没有配置推送目的地——一律拒绝,并提示改用 git push origin <branch>。同时带上附注 tag 的推送——--follow-tags, 或 git config 里设了 push.followTags——算作 tag 推送(#327):没有哪个 push: glob 允许它,所以会被拒绝并 提示 --no-follow-tags;除非它推送的每个分支都是 push-protected,那样就像任何受保护推送一样被询问。裸的 push 保持原来的含义,而把它和 push: glob 一起声明会被拒绝,因为它会悄悄放宽 glob。Codex 没有这样的 hook, 所以在那里这个范围只被携带,不被强制执行。

push-protected:<branch glob>(可带 @<role>)指定一个既不允许也不拒绝、而是要询问的分支(#280):推到它时 Claude 写入守卫返回 ask,于是 Claude Code 请你确认这一条命令,下一次推送还会再问——Keel 不存储任何批准。 push-protected:main@iecc-pm 只让绑定到 iecc-pm 的会话得到询问;其他会话一律拒绝。无法保证会询问的权限模式 bypassPermissions 或 dontAsk 也被拒绝。声明它本身就会打开推送检查,而在它旁边写裸的 push 会被拒绝。

push-tag:<tag glob>(可带 @<role>)让会话推送名字匹配 glob 的显式标签(#499):git push origin refs/tags/<name>、 <src>:refs/tags/<name> 和 git push origin tag <name>。不声明它时,Claude 写入守卫把每个标签 refspec 都当作 “不是分支”拒绝。不在 glob 内的标签,或由另一个角色的条目覆盖的标签,会被拒绝并写明 push-tag: 或那个角色;这个授权从不 询问。它不改变别的:--tags、--mirror、--follow-tags、push.followTags 和删除标签仍是原来的判定,也没有哪个分支授权 会顺带放行标签。只声明 push-tag: 的仓库只限制标签,不限制分支推送;它旁边写裸的 push 是允许的,因为它没有放宽任何分支。

每个带范围的条目——push:、issue:、issue-close:、cleanup:、chat-reply:、push-protected: 和 push-tag:——都可以 以 @<role> 结尾,或用角色模式如 @kaiwu-design-*,其中 * 匹配任意一串角色字符(#378)。这样的条目只作用于 chat 角色与之匹配的会话。会话在 capsule、keel context、keel --doctor 和它的 chat-reply 群里读到属于自己 角色的授权,keel --doctor 还会为每个已绑定角色列出作用于它的带范围条目。推送时,Claude 写入守卫只通过会话自己 角色的条目放行分支,对另一个角色的条目所覆盖的分支则拒绝并写明那个角色。只要声明了任何推送条目,这个检查对每个 会话都保持开启,所以没有被任何条目点名的角色是受限的,而不是不受限。tracker、chat 和 scratchpad 范围仍然只被 携带、不被强制执行:角色只决定由哪个会话读到它们。

shift-restore(#319)涵盖 Claude 桌面应用上因换班而启动的会话在 keel shift resume 之后必须马上做的两件事: 如果自己的 Remote Control 回来时是关着的,就把它重新打开;把自己的名字改回工作标题。桌面应用上被清空的会话不会保留原来的标题。 没有这份声明时,被唤醒的会话会把这次恢复当成另一个会话的请求,停下来问你。它只作用于那个会话,所以不带范围; 它不涵盖任何其他会话,不涵盖 clear 或 archive,也不涵盖任何仓库操作。

self-shift(#319)是它在交班一侧的对应:便条存好后,keel shift ready --wake 在桌面应用上打印的四个步骤—— 创建唤醒任务(它用 keel shift await-clear 等待 clear,需要时先把会话取消归档,#326)、用 run_scheduled_task 启动它、给会话改名、清空会话。整个换班远不到一分钟。Remote Control 保持开启(#324)。 它不决定是否换班;那仍由任务边界规则和协调者决定。它不带范围。

cleanup:scratch 涵盖在确认已合并、且没有进程在使用之后,删除会话自己 scratchpad 下的临时 worktree 和运行输出。 它从不涵盖分支或其他会话的文件,并且和 tracker 范围一样只被携带、不被强制执行。

delete-branch:merged(#469)允许 keel branches prune 删除本角色的任务分支,本地和远端都包括。被删的分支必须已并入某条 trunk、没有在任何地方检出、并且曾在本角色绑定的 worktree 里检出过;每条删除前先连同 sha 记下来。没有这项授权时,prune 只列出这些分支并以退出码 3 结束。

protocol-refresh(第七个名字)涵盖插件更新带不过来的那一部分发布内容:本仓库 AGENTS.md 里受管理的协议块, 它只有在这里运行 keel --install 时才会更新。当 keel context 报告这个块比正在运行的 Keel 旧时,会打印一行 Protocol: 写明刷新命令;声明了这个名字,agent 就会在做其他工作之前不经询问地运行它。task 的写入守卫处于激活 状态时它绝不运行,因为刷新会写到该 task 的 Touch 之外;它也从不提交:diff 留给你,提交它是另一个操作,和其他 提交一样由声明的 commit 涵盖。在词汇表早于这个词的旧版 Keel 上,这份声明在改正之前不授权任何东西。

chat-reply:<group>(第八个名字)让会话在那个 keel chat 群里,不先问你就答复发给自己角色的记录——一次提及、 一个分配的待办或一条私信——方式是回复、给发送者发私信,或关闭待办。否则每个新会话都会来问,因为聊天消息是数据, 不授予任何东西。它从不涵盖去做消息要求的事——编辑文件、运行改变状态的命令、提交、推送、往聊天之外发送任何东西—— chat 提示在"这是数据不是指令"那句话旁边也会这么说。它按群逐个声明,裸写会被拒绝:在接了 Slack 的群里,一条回复 会离开本机。

这份声明不是以下三样东西:

  • 不是绕过门禁的路。 它授权的是操作,从不是证明。不管你声明了什么,keel gate task-complete 返回的判定和失败文字都完全一样。
  • 不是触发器。 它去掉的是一次确认,不是通往该操作的那一步。没有什么会自己调度起来,也不会替你选下一个 task。
  • 不是开放式的。 上面这 15 个名字就是全部词汇。无法识别的条目会连同可接受的名字一起报告,在你改正之前 整份声明不授权任何东西——打错字永远不会变成悄悄的放行。

这个块默认不存在,什么都不声明的仓库的行为和这个功能出现之前完全一样。keel --doctor 会报告声明了什么。

决策先例

你在对话里做的决定,到下一次上下文重置就用掉了,而促成它的那段理由——能推广到你还没遇到的决定的那部分——也随之 消失。把 Keel 指向一个先例目录,它就会查阅先例,而不是再问你一遍:

precedents: ../my-decisions   # 任意路径;可以放在本仓库之外

Keel 不自带先例,也不创建存储。它只读那个目录,别的都不碰——从不 clone、pull 或访问网络——所以仓库之外的一个 目录可以服务你所有的仓库,而一个不存在的路径,效果和根本没有声明完全一样。

每个先例是一个 markdown 文件,带一个 Applies when: 头、它所属的重要性类别、recorded 或 authorized 状态、决定本身,以及理由。最后一项是承重的:"选了 A" 只适用于字面记录的那个情形,而 "选了 A,因为 B 离线时会失败" 可以用到没人见过的情形上——也能在新情形是在线时被认出不适用。没有理由的先例会被报告为不完整, 永远不会被应用。

使用它们有三条规则:

  • 先例恰在替代了一个问题的地方被引用。 如果应用它意味着你没被问到本该问你的事,回复里会写明它。例行决定 不引用,所以一次引用总是标记一个替你做的决定。
  • 只有你能提升先例。 先例以 recorded 进入,在问题照问的同时作为推荐提出。只有当你接受了向你提出的提升, 它才变成 authorized——绝不靠使用次数,那样会在无人看着时越过界线。
  • 先例回答的是重复出现的问题;它从不重新归类。 它可以在自己的类别内缩短一个决定,但不能把一个决定移出 必须问你的那些类别,先例积累得再多,也不会让某个类别不再重要。

和常设授权一样,先例为决定提供依据,从不替代证明:门禁、证据、review 和写入守卫都不受存储里任何东西的影响。 会话启动那一行只报告存储的规模和新旧——先例正文只在真正要做决定时才加载。

无人值守运行

一个循环最后需要的,是开始的许可。声明哪些 issue 可以不经询问就开工:

triage:             # 两种来源任一都可准入;不写则什么都不准入
  labels:           # issue 带的标签
    - auto
  issues:           # issue 编号,声明在这里而不是 issue 上
    - 62

直接写在 triage: 下的裸列表仍表示标签,所以在第二种来源出现之前写的声明,含义完全不变。

gh issue view 42 --json labels,number \
  --jq '"--labels \([.labels[].name]|join(",")) --issue \(.number)"' \
  | xargs keel triage

Keel 从不获取 issue。 你把 gh 返回的内容传进去,评估保持本地、离线、确定——正是这些性质让 Keel 的其他 每个回答值得信任。--labels / --issue 至少要给一个;两个都不给,等于要求它去获取。Keel 无法完整读懂的 triage: 块什么都不准入,并写明出错的条目——准入能读懂的那一半,就等于准入了你打错字那一条旁边的条目。

两种来源的单位都是一个 issue。 一个人贴标签或敲编号,都是针对某一个具体的 issue,所以这套策略准入的是你 一个一个挑选出来的集合——而不是去猜哪些 issue 看起来简单,那恰恰是不该自动化的判断。

它们的区别在于你的决定记在哪里,这也是两者都存在的原因:

  • 标签把它记在 issue 上。在 issue 来自别人的仓库里,这意味着一个运维开关落在了你让报告者用来分类的词汇里, 提交 issue 的人看得见也改得了。Keel 也无法确认是人贴的标签;如果你的自动化能给 issue 打标签,这个来源比 看上去更宽。
  • issue 编号把它记在你自己的文件里。报告者看不到它,只有提交者能改,它会出现在 diff 里,而且你可以撤销 一条而不动其他条目。

准入只回答"可以开始吗",之后的事一概不管。 对齐照样上报每一个重要选择,每个门禁照样运行,写入守卫照样约束。 具体来说:

  • 无人值守运行可以 triage、编写、实现、验证、在 authorize: 允许的地方推送,并开 pull request。
  • 它不可以合并。合并是未经审阅的决定变成你项目历史的那一刻,Keel 里没有任何声明能授权合并。
  • 准入来自这份声明,从不来自先例,不管存储里积累了多少 triage 历史——一个 issue 是否变成工作,是一个 要由你明确委托的决定。

Keel 不调度任何东西。 /loop、cron 和 CI 触发器属于你的运行时;Keel 负责的是让每一步都能有依据地做出 决定。唯一的例外是 keel shift ready --wake:一个一次性的等待者,在那个角色的下一个会话启动后写下一条唤醒记录, 然后退出——一个给自己换班的会话,没有别人来启动它。而一次在真正的决定处停下的运行,正是按设计结束的—— 别为了让它不再停下而放宽策略。

谁来合并

Keel 从不让 agent 合并——无人值守运行开出 pull request 就停下。仓库仍然可以按自己的规则在没有人的情况下合并: 在必需状态检查之后的 GitHub 自动合并,或者一个合并已通过内容的 workflow job。Keel 自己的仓库用的是后者—— .github/workflows/publish.yml 里的 land job 在负责人的 pull request 的 head commit 通过 full-gate 之后 合并它,然后在同一次运行里发布并发版,不用任何存储的 secret。如果你的仓库会自己合并,就写明:

merge: repository:full-gate

这样 keel context 会报告默认分支在 full-gate 通过时合并,并且合并前没有人审阅。不写的话,读你协议的人 会以为每个改动都有人看过,因为协议里关于合并只说了一句:agent 不可以合并。

merge: human 表达的是相反的意思。裸的 repository 会被拒绝——"没人审阅"只有写明了是什么取代了审阅者才算 诚实。它不是一项许可:authorize: 没有 merge 条目,也不该有。Keel 只读这份声明,从不读 GitHub,所以它无法 确认你的仓库真的那样合并;它报告的是你声明的内容。

Full 与 Lite

Full 模式(上面的 OpenSpec 流程)用于新功能、接口或协议变更、跨模块工作,或超过约 3 个文件 / 100 行的改动。 Lite 模式用于局部修复、小脚本、文档,或不改接口、影响可局部证明的测试;Lite 不写 OpenSpec 状态。这条规则 写在 keel --init 安装的块里,因为路由是一次会话的第一个决定,而一条只能从这份 README 找到的规则,只有已经 主动去找的 agent 才找得到。

体量门槛是风险的代理指标,不是风险本身,有些仓库恰好相反:一份只追加的记录,其 schema 改动只是一个字段的 diff, 却可能是项目里风险最高的改动;而横跨二十个文件的机械改名,风险却最低。把这条经验法则判断错的路径声明出来, 每条附上它为什么判断错:

full_mode_paths:
  - results/experiments.jsonl: 只追加;一个字段的 diff 无法回退

keel context 会报告声明了什么,这样例外会在做决定时出现,keel --doctor 则报告这份声明的健康状况。这里刻意 没有反方向的键:那个文件里的每一项声明都只去掉一次确认、从不去掉门禁,而一个把工作挡在流程之外的条目 会是第一个破坏这一点的。Keel 也不对任何路由决定设门禁——路由决定的是 change 是否存在,所以门禁没有可以绑定的 对象;Keel 做的是确保 agent 做决定时,规则和你的例外都摆在它面前。

被接受的原生 plan mode 计划只是会话态。其中影响 scope、完成定义或执行边界的决定,要在实现之前写进 proposal/design/specs/tasks;会话里的计划本身从来不是执行权威。

agent 加载多少指导

Keel 的技能承载两类内容,它们的价值朝相反方向变化。怎么做——如何拆分 task、按什么顺序做事——执行者越强, 就越不重要。让自己可被证伪——先红后绿、检查预言的失败字面量、fingerprint、Durable owner: 引用是否真的 存在——反而越重要,因为强执行者产出的是自信的工作,而这些正是能反驳它的检查。

所以技能中按步骤指导的那一半放在它旁边的 guidance.md 里,仓库可以声明自己不需要那一半:

executor_tier: high

默认是 standard,会读指导;缺失或拼错的声明也会读,所以一个没配置的仓库最坏也只是多付一次读取的代价。 keel context 和 keel --doctor 会报告这个等级。

等级只影响指导,别的一概不动——没有哪个门禁、准则、证据要求或 Review 随它变化。这不是这份 README 里的承诺: 指导文件会被检查,确保其中不含 Keel 用来陈述准则的任何词,所以等级永远只能跳过一个什么都不决定的文件。跳过 什么由声明决定、而不由 agent 自己判断,这是有意的——"我需要这份帮助吗?"恰恰是弱执行者最容易判断错的问题, 而它回答的还是关于它自己的问题。

目前只有一个技能带指导文件:tdd-or-test-first,一张按"什么会让 Acceptance 不成立"来挑选 Verify 策略的 表,附带正确和错误的选择,以及每种证据形式何时划算(#389)。其余四个技能几乎全是准则,拆开它们只会把必须 留下的那一半挪走。

这些命令,agent 是怎么用起来的

下面这些命令你几乎不用手敲。Keel 的意义在于纪律会自己跑起来:keel --init 把它装进 agent 自己的 工作流,agent 会在恰当的时刻去调用每一条命令。让这件事成立的有三样东西。

  • 协议:keel --init 会往你 repo 的 AGENTS.md 写一段 bootstrap 块(在 Claude 上由 CLAUDE.md 引入)。它写明 agent 要遵守的规则:每次会话先跑 keel context、把工作路由到 Full 或 Lite、 在任务边界处过门禁、只在任务声明的写入范围内改文件。这就是 agent 知道何时运行什么的来源。
  • 技能:keel-* 执行技能和 OpenSpec 工作流 skill 上的 overlay 带着 agent 走「对齐 → apply → review → 完成」,每一步调用门禁。新增或实质扩展的技能,要从 authoritative source 研究并记录出处与许可证, 用真实的 should-trigger 与 should-not-trigger 用例检验其 description,并至少通过一个 real task。 src/skills/<name>/SKILL.md 是唯一可移植权威,target metadata 只是附加,发现与激活由 target-native 运行时负责。
  • hook:SessionStart hook 在会话打开的那一刻自动跑连续性投影;PreToolUse hook 在每次编辑时 执行写入守卫。两者都不需要任何提示。

所以日常使用里你要运行的只有两条:想检查接线时运行 keel --doctor,以及每当它告诉你仓库落后于安装版本时运行 keel --install——协议版本存在你的 AGENTS.md 里,更新软件包并不会让它跟着动。下面的一切,都是 agent 替你 使用的词汇。

当正确答案是"什么都没变"

重构、搬移、声称数字不变的流程升级——这类改动正确的证据是零差异,而先红后绿没有表达它的形状。报告这个问题的 仓库里,有个 task 的 tasks.md 好几次写着"这个没有诚实的红",还为了凑形状重新记录了两次契约。准则本身是对的; 只是它没有地方安放。

- Verify:
  - Strategy: equivalence
  - Base: origin/main
  - Fields: wns, tns, cell_count
  - M1: node compare.js --base --head reports every field equal

equivalence 不欠红。它的准则是 base 和 head 在你点名的字段上一致,这比先红后绿更强——它还能抓住顺带改动了 某个结果的改动。门禁检查的是这种形状看似完整、实际什么都没比较的每一种情况:缺少 Base: 或 Fields:、字段集 为空、ref 解析不到任何东西,以及 Base: 解析到 HEAD。

它不是逃避先红后绿的出口。一个声明 equivalence 的 task,如果覆盖的是它所在 change 新增的场景,就会被拒绝, 除非有一个兄弟 task 以先红后绿策略覆盖同一条目——新的行为不是不变的行为,一个 task 无法同时证明两者。

而且 Evidence 不必再复述输出:

- M1: artifact openspec/changes/<change>/evidence/compare.json sha256:9f2c…

门禁检查文件存在且摘要匹配,并拒绝 change 自身目录之外的路径,因为归档会移动那个目录,指针就会断掉。Keel 对 字节做哈希,不读里面的任何内容——声明仍然是你的,摘要换来的是:审阅者打开的文件,就是你指的那个文件。

验证分层

Keel 把验证分成两层,让慢测试套件永远不会卡住你的 push:

  • 快速内环检查(fast inner-loop) —— 秒级,在本地 pre-push 和迭代时跑,无需等待就能挡住明显的破坏。
  • 全量门禁(full gate) —— 完整或慢的套件(golden 字节确定性测试、跨平台运行),在 CI 或 keel gate change-close 时跑。

任务的 Verify 检查保持快;慢的或穷尽的那一层归全量门禁,不放在本地 pre-push。在 keel/config.yaml ——也就是存放常设授权的同一个文件——里声明你的快检命令:

fast_check: npm test -- --fast   # 你项目的秒级检查

然后按需启用仓内的快 pre-push:

keel --install --with-git-hooks   # 写 .githooks/pre-push,设 core.hooksPath(仅本仓)
keel --doctor                     # 报告 fast_check、pre-push hook、core.hooksPath
keel --uninstall                  # 当 core.hooksPath 由 Keel 设置时回退

--with-git-hooks 需要显式开启:普通的 keel --install 绝不碰 git config,且这个覆盖仅限本仓、可逆。

声明一次红证明了什么

先红后绿的纪律要求你写一个在实现出现之前就失败的检查,keel gate task-complete 会拒绝缺少 M<n>.red Evidence 的 task。它以前查不到的是红为什么失败——因为夹具是空的而失败,或者因为 PATH 从没清理过而失败, 形状上都无可挑剔。

检查可以在末尾声明它的红必须呈现的失败:

- M1: `npm test -- --scenario widget` asserts the widget renders. Fails with: `widget is undefined`

完成时就要求检查的 .red Evidence 里出现那个字面量:

- M1.red: fail, for the right reason. The scenario reported `widget is undefined`.

这个子句位于检查文本之内,所以也在契约 fingerprint 之内。这正是关键:签名不能在观察到红之后再补上或改写而不 挪动锚点,这就是预言与转录的区别。Keel 不判断签名好不好——你可以声明一个任何失败都会打印的字符串——它只保证 是你先写下的,并且 review 看得见。

这个子句是可选的;不带它的检查行为与以前完全一样。在不可能有红的检查上声明——标了 (regression) 的检查,或者 任何红绿集合之外策略下的检查——会让 task-start 指名失败,而不是待在契约里什么也不做。没有给出字面量的 Fails with: 标记也一样。如果只是想在检查里提到这个标记而不是声明它,把它放进行内代码。

红可以是诚实的,检查却依然免疫

签名预言的是缺失功能的红。它对坏掉的功能的红一无所知,而两者可能毫不相干。一个断言两个工具输出一致的 一致性检查,有真实的红、正确的签名、真实的绿——却在一个 1000 倍的单位错误下依然保持绿色,而那正是它存在的意义, 原因是它的容差带着一个默认的绝对下限。发现这种问题的唯一办法,是把缺陷放进去看着。

Detects: 声明的就是这种注入——变异,以及它必须产生的失败:

- M1: `pytest tests/test_fmax.py` asserts synth fmax == sta fmax. Fails with: `AttributeError` Detects: `sed -i s/0.0005/0.5/ run_sta.py` -> `assert 5e-16 == 5e-13`

完成时要求检查的 .detects Evidence 里出现第二个字面量。子句可以串联,所以一个检查可以同时带两者——上面的例子 是一个检查、一行。

Keel 不运行变异,也不评判它。 它记录这个声明,并把它放在 review 看得见的地方,与其他每个检查结果的地位相同。 你可以声明一个任何检查都能抓住的注入;这个子句换来的是:你在运行之前就做了决定,而事后修改它会挪动 fingerprint。

(regression) 检查可以声明一个,而且那是最合适的地方:这类检查从构造上就没有诚实的红,所以注入是唯一能 证明它不是空转的东西。

一个数字可以自称是测量值

Basis:、Evidence 正文和 Findings 都是自由文本,其中的数字无论是测出来的、估出来的还是记出来的,读起来都一样。 Measured: 把一个数字绑定到它背后的输出:

- M1: `node bench.js` reports the fanout load. Measured: `1799.9`

完成时要求那个字面量出现在该检查自己的 M<n> Evidence 里——也就是记录命令及其输出的那一条。它需要主动开启, 而且刻意不做成管所有数字的规则:拿这个仓库的归档来量,那样的规则会波及 847 个行内代码片段,大多是版本字符串、 计数和不出现在任何命令输出里的引用,每一个都会是一次误停。

领域透镜

Keel 的核心是纯流程;它不带任何领域知识,也不带任何自己的决定。除了上面的先例存储,Keel 按需加载的另一个由用户 编写的面是领域指导,它住在你在仓库 keel/lenses/*.md 下编写的 透镜(lens) 里。每份透镜自描述:开头一行 Applies when: 写明触发它的信号(文件扩展名、artifact 形态), 并带一节 Execution and review checks。当一个 change 的 artifacts 或 Touch 匹配某份透镜时,对齐、测试、调试 和 review 技能会在下结论前读它——没有匹配时什么都不加载。也没有规则阻止技能去读它判断相关的另一份透镜。 Keel 保持领域无关;知识归你所有、由你编辑。

三份透镜以需要主动选用的模板形式随包提供(web、hardware、hardware-dsl),放在 assets/lenses/ 下。 它们永远不会被自动安装:

keel lenses list            # 内置模板 + keel/lenses/ 里已安装的透镜
keel lenses add web         # 把 web 模板复制到 keel/lenses/web.md,然后自行编辑
keel lenses add web --force # 覆盖已有的透镜

覆盖声明拿什么来核对

## Expectation Coverage 用三种方式之一关闭每条预期,每一种都会被检查。Durable owner: 必须指向一个存在的 路径,或一个已经承载了其内容的 https://… 引用。Discard reason: 必须给出理由。而引用了预期标识符的 Covered by:,会与它所指 task 的 Covers: 进行比对:

- E3: the records land in different flow generations (F4, D3). Covered by: 1.1

keel gate change-close 检查 task 1.1 的 Covers: 是否真的写了 F4 和 D3。没写时,拒绝信息会说明这一点, 并说出那个标识符在哪里——本 change 中哪个 task 的 Covers: 含有它,或者没有任何 task 含有它。

引用标识符是可选的。 一条不引用任何标识符的条目不会被拒绝,也不会被报告为有缺陷;很多预期本就是散文 ("文档和技能跟随上述行为变更"),为了满足解析器给它们编号,比不编号更糟。检查要你兑现的,是你自己选择做出的 声明。为了不让一次通过被理解得超出它的实际含义,关闭时会报告比对了多少条目、多少条没有比对。

重新记录契约

在工作开始后修改 task 的契约会挪动它的 fingerprint,Keel 会报告旧契约下产出的证据已经过时。有时这太宽了—— 给某个检查加一个分类标签并没有动它的断言,为此重跑一个三分钟的实验毫无收益。那就直说:

keel gate task-start --change <c> --task <t> --record --keep-evidence M1,M3

这样报告只会列出仍然过时的检查,列出你声明不受影响的那些,并说明这种收窄来自你的声明。Keel 不验证这个声明—— 锚点保存的是之前的 fingerprint,而不是它背后的 capsule,所以它无法把检查以前的文本和现在的文本作比较。把你的 理由写在 task 的 Reauthorizations 行里,审阅者可以在那里提出异议。完成的要求没有任何改变:每个检查仍需要 自己的 Evidence。

当 Git 能说清变了什么时,漂移信息就会直接说出来(#342)。锚点会在摘要旁记录 Keel 版本—— - Contract: keel-task-capsule/v1 sha256:<hex> (keel <version>)——漂移时,Keel 会用正在运行的编译器,从记录 该锚点的那个 commit 编译 task 的源。如果那个 commit 能复现锚点,信息就会列出自那以后变化了的 capsule 字段。 如果不能复现,但锚点来自另一个 Keel 版本、且那个 commit 编译出的正是今天的 fingerprint,那么原因是编译器变了: 用 task-start --record 重新记录,证据照样成立,无需 --keep-evidence 声明。其他情况下不作任何断言。一个已勾选 的 task,如果自记录 commit 以来唯一的变化是继承自 keel/config.yaml 的常设授权,就不会让 change-close 失败; 它会在一条警告里被点名。

暂停一个 change

一个你有意停下的 change——在等仓库之外的某件事,或者只是眼下不是优先项——可以就地写明,写在它自己的 openspec/changes/<name>/.openspec.yaml 里:

schema: keel-spec-driven
created: 2026-09-05
keel:
  status: paused
  reason: waiting on the competition brief; the priority is knowledge that needs no tooling
  since: 2026-09-05

这样 keel context 在推断下一步做什么时会跳过它,并说明跳过了哪个 change、为什么——一次你看不见的跳过,比它 所取代的错误推荐更糟。如果所有活跃的 change 都暂停了,context 会如实报告并附上各自的原因,而不是报告什么都 不存在。

这只改变 Keel 推荐什么。门禁、写入守卫和完成,行为都和没有它时完全一样,keel context --change <paused> 仍然会选中它——你是点名要它的。Keel 从不自己暂停一个 change。

归其他 worktree 所有的 change

当多个会话共用一个仓库、各自在自己的 worktree 里时,别处正在进行的 change 不决定你这个会话的状态(#241)。 keel context 只在属于本 worktree 的 change 上做推断,其余的作为背景列出:Change X (blocked) belongs to worktree Y on branch B; it does not set this session's status. 一个 change 属于别处,是指它的 .openspec.yaml 把另一个 chat 角色写为所有者,

keel:
  owner: verify

或者在没写所有者时,另一个 worktree 的分支有超出默认分支、触及它的 commit,而你的分支没有。只看已提交的历史; 未提交的文件从不起决定作用。 keel context --change <name> 仍然可以按名字选中任何 change。

没有选中任何东西时,keel context 和会话提示还会指出这个分支最近在做什么(#278):它超出默认分支的 commit 所引用 的最多五个 issue,最新的在前——recently referenced: #162, #182。这是从 Git 读出来的,不选中任何东西。

一个从不拥有 change 的会话——review、验证、协调者——可以按它的 chat 角色声明为只读(#246):

read_only_roles:
  - review

在绑定到列出角色的 worktree 里,keel context 只报告一行 idle 并写明角色,不列任何候选;SessionStart 和 compaction 后的提示会说没有选中 change,而不是让会话去挑一个。keel context --change 和 keel/HANDOFF.md 照常可用。

外部模型 CLI

会话可以把活交给外部模型 CLI 当委派对象——codex、DeepSeek Harness 的 dsh,或你自己注册的。调不调由模型 自己选择;Keel 负责的是接口、门禁,以及别人已经踩过的坑。宿主自带的 subagent 不在其中:模型自己更懂它们。

keel agents                      # 事实目录:每个可执行文件解析到哪里、本项目是否允许
keel agents codex                # 命令模板、沙箱、数据发往哪里、带日期的坑
keel agents brief codex --mode helper --dir . --out /tmp/brief.md --change <c> --task <t>

keel agents brief 从选中任务的 capsule 编译出一份任务说明,写成文件,再打印要运行的命令;这条命令会把 agent 的退出状态写进结果旁边的 .exit 文件。Keel 不启动任何东西;命令由会话自己运行,任何结果在算作证据之前, 会话都要复跑每个检查。以下情况会被拒绝:

  • 项目没有允许的 agent——没有声明时,一个都不允许;
  • 项目声明了不得外发的 Read 或 Touch 路径;
  • 在会话自己的 checkout 里做会写入的运行。宿主的写入守卫看不到外部进程,所以实现(以及 CLI 没有只读沙箱的 helper,比如 dsh)需要用 git worktree add 另建一个。
external_agents:
  allow:
    - codex
  egress_deny:
    - secrets/**: 受 NDA 约束的厂商密钥

事实目录只记事实——命令模板、沙箱、数据去向,以及每条都带日期和出处的坑——从不写某个 agent 擅长什么,因为常驻 在会话里的评价会左右它,而且换个模型就过时。哪些活适合交给外部 agent,由你自己写。工具路径属于本机事实: ~/.keel/agents.json 可以新增 agent 或覆盖某个字段,比如 {"agents": {"dsh": {"executable": "/path/to/dsh"}}}。Keel 看不到 agent 在任务说明之外还读了什么,所以 egress_deny 防的是交出去的内容,而不是它自己能打开的文件。

共享资源租约

会话通过有期限的租约在各 worktree 之间协调共享资源。其他持有者会被拒绝并记为等待者;持有者释放租约,或有人发现租约已过期时, 等待者会收到私信通知。租约对 Git 或文件系统不做任何强制。换班检查会报告本 worktree 仍持有的租约。见 docs/resource-lock.md。

worktree 总览

keel worktrees [--repo <path>] [--issues <file>] [--json] 列出每个 worktree 的分支或游离 HEAD、上游及 ahead/behind 计数、最后一次 commit、脏路径数、绑定的 chat 角色及在线状态,以及引用的 issue。被锁定、可清理和 已缺失的 worktree 也会显示。它读取本地状态,不写仓库,也不写 chat 存储,而且从不 fetch:ahead/behind 以最后一次 fetch 为准。issue 引用来自默认分支(origin 的 HEAD,否则本地 main,再否则 master)之外的 commit 标题和正文, 文本输出会写明用的是哪个默认分支。

用 --issues issues.json 显式传入 issue 时间,内容是一个 JSON 数组,例如 [{"number":12,"updatedAt":"2026-10-05T08:00:00Z"}](即 gh issue list --json number,updatedAt 的格式)。无法读取或格式错误的文件会被拒绝(exit 1),并写明文件名。 JSON 输出包含 defaultBranch 和 worktrees;每个 worktree 的 issues 仍是一个排好序的编号数组。带 --issues 时,issueUpdates 会加上 {number, updatedAt} 条目,没有时间的记为 null。默认分支未知时,不推断 issue 范围。

会话之间的群聊

在同一个仓库里干活的各个会话——Claude Code 会话、Codex 会话、无人值守的执行器,还有你——用 keel chat 共享一个工作群。群有维护中的成员名单,一条消息可以 @ 某个角色或 @all,轻量待办可以关联 issue。一条记录可以取代同一作者之前的记录(--supersedes <id>),被取代的那条随即从所有人的未读里消失;未读按新到旧排列。用 --ack 发出的记录会请它所指向的角色确认,方式是 keel chat ack <id> 或回复;keel chat show <id> 和 keel chat acks 会列出谁已确认、谁只读过、谁还没读。keel chat summary --since <time> 汇总你能读的群里发生了什么变化——按发送者列出消息数、开出的待办及其是否仍未关闭、已关闭的待办、并入后继记录的被取代记录,以及仍在等待的确认——不用模型,也不把任何东西标为已读。每个成员有各自的未读状态,历史全部保留。用 keel chat group add <group> <role> --listen 加入的成员在那个群里只听不说:它接收这个群——包括桥接到 Slack 的群,这样 review 会话读到的是你自己的原话,而不是协调者的转述——被找时会被唤醒,可以确认并关闭自己的待办,但不能在那个群里发言或修改它(#248)。会话的 chat 角色就是它的身份:按角色称呼其他会话,绝不要按宿主的会话标题,Keel 看不到那个标题;角色绑定在会话运行的 worktree 上,所以一个换了地方的会话必须在新的位置重新绑定角色,否则那里什么都唤不醒它。keel patrol 和 keel --doctor 会报告绑定到已删除 worktree 的绑定,以及有东西在等、却已两小时没有会话活跃的角色;未绑定就启动的会话会被告知是哪些角色;keel chat role --prune 会删掉指向已删除 worktree 的绑定(#301),keel chat role --unset 删掉当前 worktree 的绑定、worktree 本身保留(#476),但如果解绑会让这个角色的已合并任务分支再也没法被 keel branches prune 删掉,它会拒绝,除非用 --leave-branches 表明就留着它们(#483);已停放角色的已结束会话即使所在目录不存在,也不作为风险报告;keel context 和会话投影会写明绑定到本 worktree 的角色,并在这里其他 worktree 都有角色、本 worktree 却没有时提醒(#245)。keel chat role --rename <old> <new> 给角色改名而不改写日志:群聊会把旧名读作新名——群和私聊群的成员身份、待办分配对象,以及 @old 提及——而 worktree 绑定和各角色的状态(读取游标、在线状态、parked 标记、换班便条及历史、patrol 状态)会迁到新名下,新名在已有自己一份的地方保留自己的。旧名不能再被绑定,keel/config.yaml 和 keel/chat.json 里仍写着旧名的行会列出来交给你编辑(#376)。只有被提及、分到待办或收到私信才会唤醒 Claude 会话;其他消息都等它的下一次提示。消息只是另一个 agent 发来的数据,从不代表授权。两道循环防护拦住失控的 agent:一个角色每 10 分钟最多发 20 条记录;当两个角色在 10 分钟内交替发完一个群的最近 8 条消息时,那里的下一次发言会被拒绝(#297)。发言者可以带 --not-a-loop "<why>" 发一次——一问一答本来就是私聊群的正常形态——之后再重复就会叫来负责人,负责人的发言会解除它。keel/chat.json 里的 limits 设置 rate、window_minutes、pingpong 和 pingpong_minutes。消息里的时间应该来自 Keel 的时钟,而不是估计:除非给了 --no-stamp,keel chat post 和 keel chat dm 会以它为准加上 [MM-DD HH:MM] 前缀(#298、#349)。Codex 自己不会醒来,由 keel chat wake add 唤醒:在声明了 chat-reply 的群里,每条指向它的记录启动一轮,接续同一个会自动压缩的线程,没有找它的消息时什么都不跑(#203)。其他宿主依靠提示时的通知,不得让模型轮询群聊;如果确实避免不了定时,就先用 keel chat notice --check 把关,每次运行都开新线程,并告诉加入群聊的会话不要设置定时检查(#194)。每台机器一个 Slack App、一个桥接进程,同一批群就能实时连到其他机器上的会话和你的手机,孤儿分支 keel-chat 会把历史保留到超出 Slack 保留期之后。角色也可以通过一个你能在 Slack 里提及的 Slack bot 发言,一个 bot 可以服务多个项目,在每个项目的频道里充当那个项目的角色。见 Slack 配置指南。角色的会话通过 keel shift 换班:协调者发起,会话自查收尾、留下便条、清空自己,新的一班从便条接手;见 docs/shift-change.zh-CN.md。如果团队的角色以后台 Claude Code CLI 会话运行、没有桌面应用,就改用 keel tick 来唤醒、巡查和换班;见 docs/cli-team.zh-CN.md。5.83 的 keel mail 命令在私聊群上继续可用。

协调者巡查

一个协调会话安静下来,等它的每个人都会卡住(#260)。角色记下自己在等什么,由宿主按计划运行的巡查在条件成立时 告诉它:

keel chat wait soc --commit eb340a7 rerun R007 when the fix lands   # 或:--issue 42 --closed
git fetch && gh issue list --json number,title,state,labels,updatedAt > /tmp/issues.json
keel patrol --issues /tmp/issues.json        # 以协调者的角色运行

keel patrol 把条件已经成立的每条等待——远端出现某个 commit、某个 issue 已关闭——标为已满足,并给等待的角色发 一条私信,把它唤醒。它列出每个角色的在线状态、最近活动和未关闭的待办,并标出闲置超过 --idle(默认 30m)的 角色。它不 fetch、不调度、不调用模型,没有新情况时什么都不写;这时它以 0 退出,当协调者有事要处理——一个风险, 或它自己的等待已满足——时以 2 退出,只记录了东西或通知了其他角色时以 3 退出,出错时以 1 退出,所以一个廉价的 调度器——cron、/loop、CI——可以只在 2 时唤醒模型(#350)。keel chat waits 列出仍在等待的事项。 提交类等待可以用 --branch main 指定分支,这时只有那个远端分支包含该提交才算满足(#477)。不带 --branch 时, branches: 声明了 trunk: 的仓库等的是远端默认分支或任一声明的主干;没有声明的仓库等的是远端的任一分支, 登记回显和 keel chat waits 会写明 any branch of origin。

按小时运行的巡查应该报告变化而不是状态,否则它每小时都会为一张故意搁置的便条唤醒协调者(#295)。 keel patrol --since-last 为每个角色保存一份快照——换班状态、远端默认分支的末端、主 checkout 的未提交路径、 每个角色的闲置情况和未关闭待办,以及协调者收到的、未读超过 --idle 的指向它的记录——只为与上一次运行不同的 内容打印 changed: 行;第一次运行记录基线。它每次运行都写入快照,有变化时以 3 退出,只有风险变化时才以 2 退出。 加上 --notify,协调者会收到一条来自 keel-patrol 的私信,唤醒它的会话,内容只包含风险(#307):共享 checkout 在一次干净的运行之后出现了未提交路径、宿主用 --disk-free-gb 报告的剩余磁盘降到 --disk-min-gb(默认 20) 以下,以及一个角色连续两次催促都没有回应。失败或卡住的 CLI 交接、所在目录已不存在的已结束会话,只在出现或 变化的那次运行发送一次,之后只要还在,就打印为 risk (sent before)(#476)。换班状态变化和新 commit 只打印,不发送。每次运行都会往 chat 存储里的 patrol/log/<role>.log 追加一行,所以一次静默的运行也能审计。 宿主还可以用 --quota <percent> 传入每周配额的已用百分比:--quota-alerts(默认 90,95,99)里的每个阈值 在每个配额周期内成为一次风险,数字下降即开始新周期;达到 --quota-quiet-at(默认 90)或以上时,--nudge 什么都不发,因为每次催促都会唤醒一个大会话。

keel patrol --nudge 更进一步,自己去催每个闲置的角色,用一条来自 keel-patrol、会唤醒其会话的私信(#307): 当指向它的记录等待时间超过 --idle 时(最多每两小时一次);当分配给它的一个待办被关闭时,告诉它在这个任务边界 换班——如果还有另一个待办分给它,就用 keel shift ready --wake,否则用普通的 ready 等协调者——因为换班跟在 完成的任务之后,而宿主会在一个任务之内做 compaction(#313);以及当它用 ready --wake 留下的便条一直没被 恢复时。不带 --wake 留下的便条——协调者搁置某个角色——不会被打扰,Keel 自己的消息也从不算作等待。

带上 --issues 时(keel tick 会自己传),巡查还会分派 issue 更新(#242):keel/config.yaml 里的 issue_roles: 把一个标签映射到 一个角色,每个角色会为带着它标签的 issue 的每次更新收到一条私信——这样一个标为 role:verify 的交接无需第二条 消息就能到达 verify。一个键也可以是用 + 连起来的一组标签(#433),只匹配同时带着这些标签的 issue,这样 结构相同的几个团队就能共用能力标签。当一条匹配路由的标签是另一条的子集时,只有标签更多的那条的角色收到更新; 加了引号的键仍是单个标签。

issue_roles:
  role:verify: verify
  design: design-iecc
  design + dmc_rmw: design-rmw

在会话内创建的宿主计划任务会随该会话结束,所以换班或重启就会丢掉它。声明由哪个角色巡查、多久一次,那个角色的 新会话——以及它的 keel shift resume—— 就会被提醒重新创建它(#260);Keel 本身仍然不调度任何东西:

patrol:
  role: iecc-pm
  every: 30m

一个团队一个协调者的仓库可以列出多个(#434),每一项带同样的键,再加上 scope: group:<name>。带范围的协调者 只催促该聊天群的成员、只把 issue 分派给他们、也只报告他们的情况;不带范围的协调者负责不在任何范围里的角色, 同时在两个群里的角色两边都管,但同一窗口内只会被催一次。远端分支头、共享 checkout、绑定漂移、磁盘和配额由不带 范围的协调者报告,全部都带范围时由列在第一个的报告。每个角色收到自己的提醒,keel patrol task 渲染调用所在 worktree 的角色的任务(或 --role <role> 指定的),keel tick 在各自到期时分别运行:

patrol:
  - role: pm-iecc
    every: 60m
    scope: group:iecc
  - role: pm-rmw
    every: 60m
    scope: group:rmw

更好的做法是把巡查完全放到协调者的会话之外,作为一个运行在小模型上的宿主计划任务,这样它能熬过协调者清空自己 (#295)。加上 host_task: <id>(愿意的话再加 model: <id>,Keel 只是原样传递,从不替你选择),然后运行 keel patrol task:它打印出这个任务——id、由 every: 生成的 cron、模型,以及一份按路径针对协调者 worktree 运行 keel patrol --since-last --notify 的自包含提示。Keel 什么都不创建;由会话的模型用宿主自己的计划任务工具安装它, 因为计划由宿主自己保存。声明了 host_task: 后,提醒只会请你确认这个任务仍处于启用状态。请在一个文件夹未绑定任何 角色的会话里创建这个任务:宿主任务会在创建它的那个文件夹里开始运行。如果某次运行还是在某个角色的 worktree 里启动 了,Keel 会凭它的第一条提示(<scheduled-task …)认出它,随即恢复那个角色的在线状态并忽略这个会话,所以它既不会 把角色标为离线,也不会触发 keel shift ready --wake(#300)。

没有桌面应用时,keel tick 就是执行器:一个不用模型的步骤,由 launchd 每隔几分钟调用一次,一旦过了巡查建议的 下一次运行时间,就运行巡查。声明 runner: tick,再加上 cadence: adaptive 让巡查根据所见来选择下一次运行时间; keel tick task 会打印 launchd 任务。见 docs/cli-team.zh-CN.md。

tick 还会把以前由宿主任务传的数字交给巡查(#456)。它测量每个巡查 worktree 所在文件系统的剩余磁盘空间,对照 disk_min_gb:(默认 20)。它从 quota_file: 指定的文件里读取周配额百分比,前提是这个文件在 6 小时内写过。Keel 不会为了配额去执行任何命令;可以由 status line 来写这个文件,因为只有 status line 能拿到 rate_limits。文件过期, 或者根本没有声明时,tick 只说一次。两项都可以声明在任意一个 patrol: 项上,由负责全仓库风险的协调者报告。

# 在 ~/.claude/statusline-command.sh 里,把 JSON 读进 $input 之后
echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty' > ~/.claude/keel-quota.txt

tick 会自己替巡查取 issue 状态(#473),所以 issue_roles: 分派和 keel chat wait --issue <n> --closed 在 tick 下都能生效。声明了路由,或者有这样的等待未满足时,它运行 gh issue list 取最近更新的 100 个 issue, 再对列表里没有的每个被等待的 issue 运行 gh issue view,把结果作为 --issues 传给巡查。和配额不同,这是 Keel 自己执行的命令。没有任何东西需要 issue 时,不运行 gh。gh 不在、没登录或执行失败时,tick 说一次 issue 检查 没有执行以及原因,keel --doctor 也会根据上一次 tick 打印一行 issue checks。

负责人的语言

agent 读到的大多是英文——工具输出、issue、spec、Keel 自己的输出——回答你时也会滑向英文。声明你希望被使用的语言,这条规则就会在写回复的地方被重申:在 keel context、会话启动提示,以及每个 worktree 里每次提示时的 chat hook 中(#305)——在 Claude 上,还会在每 15 次工具调用或连续 10 分钟的工具调用之后重申,因为回合中途写的状态更新并不跟在某次提示之后(#311)。agent 仍可以用任何语言工作;标识符、命令、路径和引用的输出保持原样。

owner_language: zh

命令参考

keel/HANDOFF.md 存在时,会覆盖 keel context 选中的任务。它只有 YAML front matter:schema: keel-handoff/v1、 owner(指向 openspec/changes/<change>/ 下的文件)、action(discuss、author、task-start、task-complete 或 change-close)和 reason。keel context 会校验它,keel context --clear-handoff 删除它。

# 连续性 —— 无状态重算「现在该做什么」
keel context [--json] [--change <c> --task <t>]
keel context --clear-handoff                       # 删除 keel/HANDOFF.md
keel project --target <t> --event startup|resume|compaction|worktree [--expected-owner <owner>] --json
keel capabilities [--target <t>] [--json]          # 各 target 的自动化经验证能做什么

# 确定性门禁 → pass | fail | needs-review
keel gate task-start    --change <c> --task <t> [--record] [--keep-evidence M1,M3] [--no-guard] --json
keel gate task-complete --change <c> --task <t> [--base <git-ref>] --json
keel gate change-close  --change <c> --action sync|archive [--base <git-ref>] --json

# 写入守卫(Claude target)
keel guard start --change <c> --task <t> [--force] --json
keel guard status --json
keel guard clear  --json

# 领域透镜 —— keel/lenses/ 里由用户编写的指导
# (另一个由用户编写的面是先例存储,见上文)
keel lenses list
keel lenses add <name> [--force]

# 会话之间的群聊(接 Slack 见 docs/chat-slack-setup.zh-CN.md)
keel chat role --set <role> [--alias <a>] | --rename <old> <new>
keel chat group create <g> [--member <r>]... | add | remove | archive | list
keel chat <g> [<text>] [--since 2h] [--follow]     # 查看,或发言
keel chat dm <role> <text> | todo <g> --assignee <r> <text> | todos [--mine]
keel chat unread | read | notice | search <text>
keel chat bridge add | install | status | pause <2h> | stop | start | uninstall
keel chat wake now <role>                          # 恢复某个角色已结束的 CLI 会话
keel chat archive sync | pull
keel mail role | send | list | read                # 5.83 的邮箱;在私聊群上可用

# 换班、共享资源租约、worktree 总览
keel shift request | check | ready [--wake] | start | resume | status
keel lock acquire | release | list                 # 见 docs/resource-lock.md
keel worktrees [--repo <path>] [--issues <file>] [--json]

# 无人值守 triage —— 这个 issue 能否不经询问就开工?
# Keel 从不获取 issue;传入 gh 返回的内容。两个参数至少给一个。
keel triage [--labels <l1,l2>] [--issue <n>] [--json]

# 协调者巡查,以及宿主调度器调用的无模型步骤(docs/cli-team.zh-CN.md)
keel patrol [--issues <file>] [--idle <dur>] [--since-last] [--notify] [--nudge] [--json]
keel tick [--json] | keel tick task                # 执行一步;打印 launchd 任务

# 外部模型 CLI —— 事实目录,以及 Keel 编译的任务说明(Keel 不启动任何东西)
keel agents [name] [--json]
keel agents brief <name> --mode helper|implementation --dir <path> --out <file>

# 本角色已合入的任务分支:列出分支和 sha;不 fetch,也不删除
keel branches [repo] [--role <role>] [--json]
keel branches prune [repo] [--role <role>] [--json]   # 声明 authorize: delete-branch:merged 后才删除

# 安装 / 维护
keel --init    [--target <t>] [--dry-run] [--force-template-update]
keel --install [--target <t>] [--dry-run] [--force-template-update] [--with-git-hooks]
keel --check | --doctor  [--target <t>]
keel --uninstall | --clear  [--target <t>] [--dry-run]   # --clear 是同一种移除
keel --update [--dry-run] [--source <npm package or git spec>] [--adopt]   # --adopt:让空闲的后台会话重启到新插件上
keel --version | --help

# OpenSpec,通过 Keel 捆绑的 CLI 运行
keel openspec <args>

命令接受仓库路径,可以是位置参数,也可以是 --repo <path>;具体用哪种见 keel --help。 每组命令都会打印自己的完整子命令列表:keel chat help、keel mail help、keel shift --help、 keel lock --help、keel patrol --help 和 keel agents --help。

退出码:0 通过 · 1 输入错误 · 2 用法错误(未知 flag,或命令拒绝的 flag) · 3 策略失败 · 4 缺少语义 review。keel patrol 用 2 和 3 表示它发现了什么;见「协调者巡查」。

target 是探测出来的,不按名字假定:无法验证的运行时行为报告为 manual,而非 enforced。一个 repo 固定选一个 target,后续每次 --install / --check / --doctor / --uninstall 都用它。

开发

无构建步骤。src/skills/ 是可移植技能的唯一维护源;plugins/keel/skills/ 下的分发副本必须与之字节一致 (由校验强制)。

npm test                  # 一次 validate_plugin.py --all:baseline + 全部场景并行(--jobs N 控制并发),最后一行是 evidence: 证据分类
node scripts/run_python.js scripts/validate_plugin.py --evidence           # 哪些场景是确定性的、哪些对着替身宿主跑;--markdown 给发布说明用
node scripts/run_python.js scripts/validate_plugin.py --scenario <name>   # 单个场景
node scripts/run_python.js scripts/validate_plugin.py --group <module>    # 一个模块的场景,例如 --group chat
node scripts/bump_version.js <patch|minor|major>   # 一次改齐所有版本 pin

场景按职责放在 scripts/validation/ 下的模块里(contract、install、hosts、guard、chat、 shift、docs、selfcheck);registry.py 是唯一的有序注册表,common.py 放它们共用的辅助代码。

隐私

Keel 不运行服务器,自身不收集任何数据。PRIVACY.md 列出它在你本机存什么、什么会离开本机, 包括内置 OpenSpec CLI 的遥测及关闭方法。

License

MIT © 2026 TanglmChris。English 版 含有相同的章节。

版本与发布记录

@christang/keel 共发布 229 个版本,首次发布于 2026/07/19;过去 12 个月发布了 229 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
5.227.0latest2026/11/081 天前
5.226.02026/11/081 天前
5.225.02026/11/081 天前
5.224.02026/11/081 天前
5.223.02026/11/081 天前
5.222.02026/11/081 天前

关于 @christang/keel 的常见问题

@christang/keel 是什么?

Keel OpenSpec 执行纪律 CLI 用于 Claude Code, Codex, 和 OpenCode.

如何安装 @christang/keel?

在项目目录运行 npm install @christang/keel,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 keel,可以用 npx keel 直接运行。

@christang/keel 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/christang__keel 是否存在。

@christang/keel 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.19.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@christang/keel 可以商用吗?

@christang/keel 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@christang/keel 的下载量有多少?

根据 npm 官方下载统计,@christang/keel 在 2026/10/10 – 2026/11/08 这 30 天内被下载 18,417 次,日均约 614 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@christang/keel 还在维护吗?

最新版本 5.227.0 发布于 2026/11/08(1 天前);至今共发布 229 个版本,其中过去 12 个月发布了 229 个;首次发布于 2026/07/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。