0. 二十个 Index 的终点
这是 Cat-Code 复刻 Claude Code 20 个核心机制的最后一个。s01 的消息循环到 s19 的 MCP 插件,20 个独立子系统已经各自闭环;s20 做的是两件「收口」的事:把 s17 自治认领 和 s18 worktree 隔离这两个「本该连在一起却刻意留到最后的」机制焊上,再给整台机器装 一个「一眼看全」的仪表盘。
阶段一 最小闭环 s01 Agent Loop ── s02 Tool Use ── s03 Permission ── s04 Hooks
阶段二 智能体能力 s05 TodoWrite ── s06 Subagent ── s07 Skill ── s08 Compact ── s09 Memory
阶段三 系统可靠性 s10 SystemPrompt ── s11 Recovery ── s12 Task ── s13 Background ── s14 Cron
阶段四 多智能体协作 s15 Teams ── s16 Protocols ── s17 Autonomous ── s18 Worktree ── s19 MCP
└──────────────► s20 Comprehensive Agent ◄──────────────┘
(把散件焊成一台机器)1. 目标
- 打通 s17 自治认领 ↔ s18 worktree 隔离:自治循环派发任务时创建隔离目录、 完成时释放——这是 s18 spec §7 白纸黑字留给 s20 的接线。
- 提供
/status统一视图:五个子系统(任务/团队/worktree/MCP/自治)一个命令看全。
完成后效果:
> /status
Tasks: 2 ready · 1 in progress · 3 completed (6 total)
Team: 1 idle · 1 running (2 total)
Worktrees: 1 active · 3 released
MCP: 1 connected server · 1 tool registered
Autonomous: 1 active claim在此之前,自治循环派发任务时,队友和主 agent 挤在同一个工作目录里;在此之后,每个 自治任务有自己的隔离目录 + 分支,且整台机器的状态一处可见。
2. 为什么需要
2.1 现实问题:两块拼图没拼上
s01-s19 每个机制都独立建好了,但有两块拼图始终悬着:
-
s17 不认识 s18。
AutoClaimer.tick()派发任务时只发一条文本消息 ("Autonomous task t1: ...\nWork on this task..."),队友和主 agent 在同一个 目录里干活。s18 建好了隔离目录的能力(WorktreeManager.create/release),但 自治循环从没用过它——隔离能力是「死」的,没有被任何生产路径触发。 -
没有全局视图。排查一个自治任务从创建到完成,现在要横跨
/task、/team、/worktree、/mcp、/auto五个命令拼图。缺一个「一眼看到全貌」的入口——这既是 运维痛点,也是「这 20 个机制到底是不是一台机器」的验收标准。
2.2 设计原则(取舍依据)
-
默认参数向后兼容。
AutoClaimer加worktreeManager: WorktreeManager? = null(AutoClaimer.kt:52),不传则行为与 s17 完全一致,现有 8 个 s17 测试零改动。 worktree 是可插拔增强,不是硬依赖。这延续了 s18「本 Index 不改 AutoClaimer」留下的 边界——s20 是唯一被授权改它的 Index(spec 里事先声明过)。 -
隔离生命周期三阶段语义。派发
create(幂等)、完成release、回滚到 PENDING 保留(任务可能被重新认领,s18 幂等 create 直接复用)。worktree 的存活期 = 任务从 IN_PROGRESS 到终态的窗口。这是把「隔离目录」从一次性资源升级为「任务级 工作区」的关键语义。 -
ComprehensiveAgent 是协调者,不是重写者。它组合既有的 TaskStore/TeamStore/ WorktreeManager/McpToolPool/AutoClaimer,做两件没人做的事:富化的周期摘要 (claimed→worktree path)+ 跨子系统统一状态。不重写 ReplLoop、不复制 s17 的 tick 逻辑——它调用
AutoClaimer而非替代之(ComprehensiveAgent.kt:36)。 -
状态视图只读。
/status只读快照,不触发任何副作用(不 tick、不 connect、 不 create)。命令层的铁律:查询命令不改变系统状态——这反过来源自 s14 cron 查询、 s15 team 查询一路建立起来的惯例。 -
单一数据源。
status()的每个数字都来自对应子系统的既有查询方法 (TaskStore.loadAll()、TeamStore.all()、WorktreeManager.all()、McpToolPool.status()),不另建缓存/计数——避免双份真相,这是 s18 博客里 「RELEASED 记录不删除」原则的延伸:审计数据只该有一份。
3. 核心设计与实现
3.1 架构全景
AutoClaimer.tick() ──(s20 接线)──► WorktreeManager.create / release
▲ ▲
│ 组合 │ 注入
ComprehensiveAgent ──────────────────────────┤
├─ runAutonomousCycle() → ComprehensiveCycle(claimed→worktree path)
└─ status() → ComprehensiveStatus(五维统一视图)
▲
│ ReplLoop 注入 + /status 命令依赖方向仍是单向:comprehensive 依赖 autonomous/task/team/worktree/mcp,
后者都不反向依赖它。这是 20 个 Index 一路坚持的包依赖单向原则(CLAUDE.md 关键设计
决策 #5)在最后一站的收口体现。
3.2 AutoClaimer 的 worktree 接线(改 autonomous/AutoClaimer.kt)
改动分三处,全部是「注入 + 分支」,不触碰原有逻辑的骨架:
① 构造注入(AutoClaimer.kt:46-53):
class AutoClaimer(
private val taskStore: TaskStore,
private val teamStore: TeamStore,
private val messageBus: MessageBus,
private val tracker: ClaimTracker = ClaimTracker(),
private val config: AutonomousConfig = AutonomousConfig(),
private val worktreeManager: WorktreeManager? = null // s20 新增,默认 null
) {
private val worktrees = ConcurrentHashMap<String, WorktreeRecord>()worktrees 是「在途任务 → worktree 绑定」的内存映射,线程安全(ConcurrentHashMap,
与 s17 ClaimTracker 同款——IdleLoop 后台 tick 写,/auto tasks 主线程读)。
② 派发创建(AutoClaimer.kt:103-109):
// s20: 派发前创建 worktree(幂等;失败 warn 不阻断派发,消息退化为无路径)
val worktree = worktreeManager?.let { wm ->
try { wm.create(task.id).also { worktrees[task.id] = it } }
catch (e: Exception) { logger.warn(e) { "worktree create failed for ${task.id}" }; null }
}关键语义:create 失败不阻断派发。worktree 是隔离增强,不是任务执行的前提——
非 git 目录、磁盘满等场景下,任务照常派发,只是消息里没有 Worktree: 行(退化为
s17 行为)。这是 s11 fail-open 哲学在 s17↔s18 边界上的延续。
③ 完成释放(AutoClaimer.kt:75-78):
// s20: 任务完成 → 释放隔离目录(s18 release 自愈优先,几乎不失败)
worktreeManager?.release(taskId)
worktrees.remove(taskId)回滚分支(队友被停 → 任务回 PENDING)不动 worktrees——保留映射,下次认领时
幂等 create 复用同一目录/分支(s18 的 rev-parse --verify 分支复用路径)。
消息正文(AutoClaimer.kt:125-129):
private fun formatTask(task: TaskRecord, worktree: WorktreeRecord? = null): String {
val base = "Autonomous task ${task.id}: ${task.title}\nWork on this task and report your results."
return if (worktree != null) "$base\nWorktree: ${worktree.path}" else base
}队友收到消息后,文件工具以 Worktree: 行里的路径为工作根——协议不用变,一行文本
完成「隔离目录的传递」。这是 s18 博客里「派发消息携带 worktree 路径」衔接设计的具体
兑现。
新增观测点 worktreeOf(taskId)(AutoClaimer.kt:122)——供 /auto tasks 与
ComprehensiveAgent 查询在途任务的隔离目录。
3.3 ComprehensiveStatus —— 五维统一快照(ComprehensiveStatus.kt)
data class ComprehensiveStatus(
val tasks: TaskSummary, // s12
val team: TeamSummary, // s15
val worktrees: WorktreeSummary, // s18
val mcp: McpSummary, // s19
val autonomous: AutonomousSummary // s17
)每个 summary 都是「计数 + 就绪」的纯数据类。字段名直接对应五个 Index——这是 20 机制 的「目录页」,读一遍就知道各子系统是谁。
ComprehensiveCycle + ClaimedTask 把 s17 的 ClaimResult(claimed: List<String>)
富化为 List<ClaimedTask(taskId, worktreePath)>——claimed 从「一个 id」变成「一个
id + 它在哪个隔离目录里干活」。
3.4 ComprehensiveAgent —— 协调者(ComprehensiveAgent.kt:27-73)
class ComprehensiveAgent(
val taskStore: TaskStore,
val teamStore: TeamStore,
private val messageBus: MessageBus,
private val worktreeManager: WorktreeManager,
private val mcpPool: McpToolPool,
private val autoClaimer: AutoClaimer
) {
fun runAutonomousCycle(): ComprehensiveCycle {
val result = autoClaimer.tick()
val claimedTasks = result.claimed.map { id ->
ClaimedTask(id, autoClaimer.worktreeOf(id)?.path)
}
return ComprehensiveCycle(claimedTasks, result.completed)
}
fun status(): ComprehensiveStatus { /* 五维聚合,见下 */ }
}taskStore/teamStore 是 public——供测试与集成方预置状态(这是测试驱动出来的决策:
ComprehensiveAgentTest 需要直接塞任务/队友才能驱动 runAutonomousCycle)。其余四个
依赖 private,暴露面最小化。
status() 的五维聚合(ComprehensiveAgent.kt:45-73)——每一维都是「既有查询 + 计数」:
val ready = TaskScheduler.readyTasks(tasks).count { it.status == TaskStatus.PENDING }
val mcp = mcpPool.status()
return ComprehensiveStatus(
tasks = TaskSummary(ready, inProgressCount, completedCount, total),
team = TeamSummary(idleCount, runningCount, total),
worktrees = WorktreeSummary(activeCount, releasedCount),
mcp = McpSummary(connectedServersCount, mcpPool.registeredTools().size),
autonomous = AutonomousSummary(autoClaimer.claims().size)
)一个值得注意的设计:TaskSummary.ready 用的是 TaskScheduler.readyTasks(...)(s12
的「依赖已满足」语义),而不是简单的 PENDING 计数——PENDING 但被阻塞的任务不算
就绪。这个语义精度是 s12 埋下的,s20 只是正确地复用它。
3.5 /status 命令(StatusCommand.kt)
class StatusCommand : ReplCommand {
override val name = "/status"
override val description = "Show unified status across all subsystems"
override suspend fun execute(args: String, ctx: ReplContext): ReplCommandResult {
val s = ctx.comprehensive.status()
println("Tasks: ${s.tasks.ready} ready · ${s.tasks.inProgress} in progress · ...")
println("Team: ${s.team.idle} idle · ${s.team.running} running (...)")
println("Worktrees: ${s.worktrees.active} active · ${s.worktrees.released} released")
println("MCP: ${s.mcp.connectedServers} connected server(s) · ${s.mcp.registeredTools} tool(s) registered")
println("Autonomous: ${s.autonomous.activeClaims} active claim(s)")
return ReplCommandResult.Continue
}
}无子命令、无状态、无副作用——三行 execute,纯粹读取 + 打印。这是 20 个 Index 里
最简单的命令之一,但它的价值在于是唯一把五个子系统并排呈现的入口。
3.6 ReplLoop 接线(两处顺序调整 + 一处注入)
ReplLoop 的改动最微妙的部分是构造顺序:原代码 autoClaimer 在 worktreeManager
之前构造(AutoClaimer(taskStore, teamStore, messageBus) 早于 worktree 的 init)。s20
把 worktree 构造提前到 AutoClaimer 之前(ReplLoop.kt s18 段移到 s17 段前),然后:
autoClaimer = AutoClaimer(taskStore, teamStore, messageBus, worktreeManager = worktreeManager)
...
comprehensiveAgent = ComprehensiveAgent(
taskStore, teamStore, messageBus, worktreeManager, mcpPool, autoClaimer
)这个顺序调整本身就是一个设计信号:s20 的接线让 s17 开始依赖 s18,而这是 20 个 Index 里第一次出现「阶段四内部的前后依赖」——s15→s16→s17→s18 本是一路往前的,s20 把它们最后两环往回焊了一下,形成闭环。
3.7 端到端:一个自治任务的完整隔离生命周期
在真实临时 git 仓库里,走一遍「任务 → 隔离目录 → 完成 → 释放」:
① 准备:仓库 master 分支,一个 PENDING 任务 t1,一个空闲队友 r1。
② 认领 tick:AutoClaimer.tick() 匹配 t1 → r1:
worktreeManager.create("t1")→git worktree add .cat-code/worktrees/t1 -b task/t1 HEAD- 队友
r1收到消息:Autonomous task t1: ...\nWork on this task...\nWorktree: /repo/.cat-code/worktrees/t1
③ 队友工作:r1 在隔离目录里改文件、提交到 task/t1 分支。主仓库 master 分支
零感知(s18 的 .git/info/exclude 保证 git status 干净)。
④ 完成 tick:队友回到 IDLE,AutoClaimer.tick() 完成检测:
taskStore.update(t1 → COMPLETED)worktreeManager.release("t1")→ 删目录 + 删task/t1分支worktrees.remove("t1")→ 观测点清除
⑤ 异常路径(队友被停):队友 markCompleted → tick 检测到终态 → 任务回 PENDING,
但 worktrees 保留——下次认领时 create("t1") 走幂等分支复用同一目录/分支(s18 的
rev-parse --verify 探测到分支已存在,不带 -b 直接检出)。
⑥ 全程可观测:任一时点 /status 都能看到 t1 在哪个状态、worktree active 数、
自治 claim 数。
这就是「一个任务从就绪到完成」在 cat-code 里的完整闭环——s12 的持久化任务、s15 的 团队派发、s17 的自治循环、s18 的隔离目录、s19 的 MCP 工具,全部在这一条链路里各司 其职,s20 把它们焊成一台机器。
3.8 一个任务穿过 20 个机制(全景追踪)
「全部机制归到一个循环」到底是什么意思?用一个自治任务 t1 从创建到完成的完整
旅程,把 20 个 Index 逐一点名——这就是 s20 的收口验收:
| 步骤 | 触发 | 涉及的 Index |
|---|---|---|
任务被 TaskStore 持久化 | 用户 /task add t1 或 s05 todo 提升 | s05 TodoWrite · s12 Task |
| 任务依赖被解析(就绪判定) | TaskScheduler.readyTasks 查 blockedBy | s12 Task |
| 空闲循环检测到就绪 | IdleLoop.tick(后台协程,忙态门控) | s13 Background · s17 Autonomous |
| 权限/审批管线就绪 | 派发前无人类干预(自治),但工具调用走管线 | s03 Permission · s16 Protocols |
| worktree 创建 | WorktreeManager.create(git worktree add) | s18 Worktree |
| 任务派发消息携带路径 | MessageBus.send → teammate 收件箱 | s15 Teams |
| teammate 消费消息续跑 | AgentLoop 一轮 run(子智能体上下文隔离) | s01 AgentLoop · s06 Subagent |
| teammate 调用工具 | ToolRegistry 分派(内置 + MCP 适配) | s02 Tool · s19 MCP |
| 工具调用前后 Hook 触发 | PreToolUse/PostToolUse | s04 Hooks |
| 上下文超限压缩 | 四层压缩(摘要/记忆/截断) | s08 Compact · s09 Memory |
| System Prompt 分段拼接 | base + skills + memory | s07 Skill · s10 SystemPrompt |
| 工具失败重试/降级 | 网络抖动自动重试、fallback 模型 | s11 Recovery |
| teammate 回到 IDLE | 消费协程结束,状态机 markIdle | s15 Teams |
| 完成检测 → 释放 worktree | worktreeManager.release | s17 Autonomous · s18 Worktree |
| 任务置 COMPLETED | taskStore.update | s12 Task |
| 统一状态可见 | /status 五维快照 | s20 Comprehensive |
这一张表就是 s20 存在的意义:20 个机制不是并列的 20 个功能,而是一条链上的 20 个 齿轮。s20 让这条链第一次完整转动起来,且每一环都有观测点。
3.9 隔离生命周期的三阶段语义(详解)
worktree 在自治任务里的存活期,是 s20 最微妙的设计点之一。三个阶段:
| 阶段 | 触发 | worktree 动作 | 理由 |
|---|---|---|---|
| 创建 | 认领(PENDING → IN_PROGRESS) | create(幂等) | 隔离目录 + 分支就绪 |
| 保留 | 队友 RUNNING / 回滚 PENDING | 不释放 | 任务还在进行 / 可能重新认领 |
| 释放 | 完成(teammate IDLE) | release(删目录+分支) | 隔离不再需要,收尾 |
「回滚保留」是三个语义里最容易想错的:队友被停(终态)时任务回 PENDING,为什么不
释放 worktree?因为任务还没死——它只是换了/还没找到执行者。如果此时释放,下次
认领时 create 要重新建目录和分支,丢掉之前队友在隔离目录里留下的半成品。保留它,
s18 的幂等 create(rev-parse --verify 探测分支已存在 → 不带 -b 检出)直接复用,
半成品不丢。
这个语义把「隔离目录」从「一次性资源」升级为「任务级工作区」——worktree 的生命周期 绑定的是任务的生命周期,不是「一次派发的生命周期」。这是 s20 对 s18 语义的一次 精确化,而非简单接线。
3.10 终端实操:before/after 对比
s20 之前,排查自治任务要横跨五个命令,且看不到 worktree 隔离:
> /task list
t1 IN_PROGRESS fix login ← 任务在跑
> /team
r1 IDLE ← 队友闲了?
> /worktree list
No active worktrees. ← 但没有任何隔离目录!三个命令拼起来才看出一个矛盾:任务 IN_PROGRESS、队友 IDLE、却没有 worktree——
意味着队友在主工作区里干的活,隔离能力根本没被用上。
s20 之后,一个命令看全,且隔离目录真正生效:
> /status
Tasks: 0 ready · 1 in progress · 0 completed (1 total)
Team: 0 idle · 0 running (1 total) ← r1 处理完回到 IDLE 之前
Worktrees: 1 active · 0 released
MCP: 0 connected server(s) · 0 tool(s) registered
Autonomous: 1 active claim
> /auto tasks
Active claims:
t1 -> r1 (worktree .cat-code/worktrees/t1) ← 隔离目录路径直接可见任务完成后:
> /status
Tasks: 0 ready · 0 in progress · 1 completed (1 total)
Worktrees: 0 active · 1 released
Autonomous: 0 active claimworktree 从 1 active 到 1 released——隔离目录随任务生命周期闭合。这就是 s20 的
验收:一处看全 + 隔离真正生效。
4. 错误处理总表
| 场景 | 行为 | 代码位置 |
|---|---|---|
| worktreeManager 为 null(未接线) | 消息不带 Worktree 行,行为同 s17 | AutoClaimer.kt:105 |
| 派发时 create 失败(非 git 目录等) | warn,任务仍派发(消息无路径),不阻断自治 | AutoClaimer.kt:105-108 |
| 完成时 release 失败 | warn,任务仍 COMPLETED(s18 release 自愈优先,几乎不失败) | AutoClaimer.kt:75 |
| 回滚到 PENDING | 保留 worktree,仅映射不清除(复用留给下次认领) | AutoClaimer.kt:79-83(else 分支不动 worktrees) |
| status() 查询 | 纯只读,无副作用,不抛(各子系统查询本就 fail-open) | ComprehensiveAgent.kt:45 |
5. 测试策略
| 测试类 | 用例数 | 覆盖点 |
|---|---|---|
| AutoClaimerWorktreeTest | 4 | 派发创建 + 消息含路径 / 完成释放 / 回滚保留 / 无 manager 行为同 s17 |
| ComprehensiveAgentTest | 2 | runAutonomousCycle 富化 claimed→path / status 五维数字正确 |
| StatusCommandTest | 1 | 五维打印 |
worktree 相关测试复用 s18 的 WorktreeManager + 真实临时 git 仓库(initRepo() helper
与 s18 同款);无 worktree 场景复用 s17 的 TaskStore+TeamStore+MessageBus 直接
构造。合计新增 7 个用例,加上为 ReplContext.comprehensive 字段修补的 9 个既有命令
测试,全量 ./gradlew test 绿。
测试设计上的一个决策记录
ComprehensiveAgentTest 需要驱动 runAutonomousCycle() 触发认领——这意味着
ComprehensiveAgent 必须暴露 taskStore/teamStore(测试要预置 PENDING 任务和 IDLE
队友)。于是 taskStore/teamStore 从 private 改为 public val。这是一个「测试驱动
出 API」的例子:与其写一个复杂的内部 setter,不如把「这个协调者持有这些子系统」这一
事实本身暴露出来——它既是真的(协调者确实持有它们),也是集成方(测试、未来的 s20
使用者)真正需要的。
6. 开发过程记录
-
worktree 构造顺序是唯一的「隐形改动」。ReplLoop 原代码 AutoClaimer 在 worktreeManager 之前构造,s20 必须把 worktree 构造提前。这个顺序调整看起来是 机械的,但它标志着 s17 开始依赖 s18——20 个 Index 里第一次出现阶段四内部的前后 依赖,闭环由此开始。
-
测试断言
fix-login撞上t1的教训。AutoClaimerWorktreeTest 第一条用例 最初断言消息含fix-login,但 worktree 目录名来自任务 id(t1)而非标题 (fix login)——s18 的sanitize(taskId)用的是 id。改法是把任务 id 设为fix-login,让断言匹配。这是一个「实现语义优先于测试直觉」的再次验证(s18 的 sanitize 空串也有同款)。 -
ComprehensiveAgent 的 public taskStore 是测试驱动的。见 5.1——把「协调者持有 子系统」这一事实从 private 改为 public,比造内部 setter 诚实。
-
TDD 节奏:3 个实现 commit(AutoClaimer 接线 → ComprehensiveAgent → /status + 接线),全部先红后绿。spec/plan 2 个 docs commit。最终全量测试无回归。
6.5 设计原则逐条复盘
把 2.2 的五个原则在实现后逐条对账——这是收口 Index 该有的诚实:
原则 1「默认参数向后兼容」 —— 兑现。AutoClaimer 的 worktreeManager 默认 null,
8 个 s17 测试零改动。这条原则的可贵在于:它让「给旧代码加增强」不必「改旧代码的
所有调用方」。这是 20 个 Index 里反复出现、却总被低估的工程纪律。
原则 2「三阶段生命周期」 —— 兑现,且在 3.9 展开。最难的是「回滚保留」,它的
语义依据是 s18 幂等 create 的 rev-parse --verify 分支复用。如果 s18 没有做幂等
create,s20 的「回滚保留」就无从谈起——这是前序 Index 的设计为后续 Index 铺路的
又一个例子。
原则 3「协调者不重写」 —— 兑现。ComprehensiveAgent 只有 74 行,其中一半是
status() 的五维聚合。它调用 AutoClaimer.tick(),不复制 tick 逻辑。如果它重写了
tick,就会产生「两处 tick 逻辑」的维护债务——这是 20 个 Index 一路用「组合优于继承、
调用优于复制」换来的干净边界。
原则 4「只读状态」 —— 兑现。/status 三行 execute,无副作用。这条原则反过
来源自 s14 cron 查询、s15 team 查询、s19 /mcp 一路建立的惯例——查询命令不改变状态,
是命令层的一贯铁律。
原则 5「单一数据源」 —— 兑现。status() 的每个数字来自对应子系统的既有查询。
代价是 5 次独立的查询调用(而非一次 join),但换来「永无双份真相」。这是 s18 博客
「RELEASED 记录不删除」原则的延伸:审计数据只该有一份,查询只是读它,不是复制它。
6.6 测试替身的二十 Index 演进(回顾)
s20 的测试只有 7 个用例,但它背后是 20 个 Index 积累下来的测试替身方法论。回顾这条 演进线,能看到项目测试哲学的成熟:
| Index | 测试替身 | 解决的问题 |
|---|---|---|
| s01-s02 | FakeLLMProvider / MockEngine | LLM API 与工具注册的确定性 |
| s06 | 子智能体 Fake store | 上下文隔离不用真起协程 |
| s08-s09 | 手写 fake(不引 mock 框架) | 压缩/记忆的纯逻辑 |
| s12-s14 | 临时目录 + 真磁盘文件 | 持久化/后台/cron 的真 IO |
| s15-s17 | TeamStore + MessageBus 直连 | 团队/自治的状态机 |
| s18 | 真实 git 临时仓库 | worktree 行为的真验证 |
| s19 | bash fixture / MockEngine / fake transport | 三种替身对应三层 |
| s20 | 复用 s17 Fake + s18 真 git | 接线语义的复合验证 |
这条线的共同原则(CLAUDE.md 测试规范):纯逻辑用 Fake,真 IO 用临时文件/临时仓库, 外部协议用 MockEngine,进程用真子进程。s20 的测试是这条原则的自然终点——它不需要 发明新替身,只需复用前 19 站造好的轮子。
7. 二十个 Index 的收口声明
7.1 二十机制清单与它在 s20 循环里的位置
| # | Index | 机制 | 在 s20 闭环里的角色 |
|---|---|---|---|
| s01 | Agent Loop | 消息循环 + LLM 抽象 | teammate 消费任务消息的运行时 |
| s02 | Tool Use | 工具注册表 + 并发调用 | 派发后 teammate 干活的工具箱 |
| s03 | Permission | 审批管线 | 工具调用前的允许/拒绝/询问 |
| s04 | Hooks | Pre/PostToolUse 扩展点 | 工具调用前后的横切面 |
| s05 | TodoWrite | 先计划后执行 | 任务标题的来源(提升) |
| s06 | Subagent | 上下文隔离子智能体 | teammate 的运行时基础 |
| s07 | Skill Loading | 按需注入 | System Prompt 的技能片段 |
| s08 | Context Compact | 四层压缩 | 长任务不爆上下文 |
| s09 | Memory | 跨会话记忆管线 | 压缩摘要 + 记忆注入 |
| s10 | System Prompt | 运行时分段拼接 | base+skills+memory 组装 |
| s11 | Error Recovery | 重试/fallback | 工具失败不中断任务 |
| s12 | Task System | 磁盘持久化 | 任务的持久化 + 依赖调度 |
| s13 | Background Tasks | 线程执行 | IdleLoop 后台协程 + 通知队列 |
| s14 | Cron Scheduler | 定时调度 | (未直接进闭环,独立时间轴) |
| s15 | Agent Teams | MessageBus + 收件箱 | 任务派发与消息路由 |
| s16 | Team Protocols | 关机握手/计划审批 | 派发前的人类审批门 |
| s17 | Autonomous Agents | 空闲循环 | 就绪任务的自动认领 |
| s18 | Worktree Isolation | 任务-目录绑定 | 隔离目录 + 分支 |
| s19 | MCP Plugin | 多传输工具池 | 外部工具注入注册表 |
| s20 | Comprehensive Agent | 全机制集成 | 收口:接线 + 统一视图 |
s14 cron 是唯一「不在自治闭环里」的机制——它有自己的时间轴(定时触发),不经过 任务认领/派发链路。这是正确的:不是所有机制都要塞进同一条链,s20 收口的是自治 任务的闭环,cron 是并行的另一条时间轴。收口不是大一统,是「该连的连上,该独立的 独立」。
7.2 四阶段交付总览
s20 完成后,四阶段全部交付:
| 阶段 | 交付 | 关键产出 |
|---|---|---|
| 一 最小闭环 | s01-s04 | AgentLoop / ToolRegistry / Permission / Hooks |
| 二 智能体能力 | s05-s09 | Todo / Subagent / Skill / Compact / Memory |
| 三 系统可靠性 | s10-s14 | SystemPrompt / Recovery / Task / Background / Cron |
| 四 多智能体协作 | s15-s20 | Teams / Protocols / Autonomous / Worktree / MCP / Comprehensive |
Cat-Code 从 s01 的一个消息循环,长成了一台完整的多智能体 CLI 平台:它能派生团队 (s15)、审批计划(s16)、自治派发(s17)、隔离工作(s18)、外接工具(s19),最后 用一张仪表盘(s20)把这一切收束进一个可观测的闭环。
贯穿 20 个 Index 的三条主线在此清晰可见:
- 包依赖单向(s01 埋下,s20 收官)——每个新包只依赖已有包,从不反向;
- fail-open 哲学(s08 压缩到 s20 接线)——局部失败不拖垮全局,warn 后继续;
- 接口隔离的回报(s02 Tool / s01 LLMProvider)——s19 适配 MCP 工具、s20 组合 协调者,都零侵入既有代码。
这是 Cat-Code 的终点,也是一个完整可扩展智能体平台的起点。
8. 三条主线:贯穿 20 个 Index 的工程哲学
收官之际,回看 20 个 Index,有三条主线清晰可见——它们不是某个 Index 的设计决策, 而是整个项目反复出现、彼此强化的工程哲学。逐条用具体例子复盘:
主线一:包依赖单向(s01 埋下 → s20 收官)
CLAUDE.md 关键设计决策 #5:「agent 包不依赖 permission/hooks/task/team,增强层可插拔」。
这条原则在 20 个 Index 里被反复验证:
- s02 的
Tool接口让 s19 能适配 MCP 工具而不改一行内置工具代码; - s06 让
ToolRegistry从全局单例改为 class,因为子智能体需要独立实例——这是 「依赖单向」第一次被迫调整; - s18 刻意不 import
task包(taskId 只做 String 引用),让 worktree 独立于任务系统; - s20 的
comprehensive依赖五个子系统,五个子系统都不反向依赖它。
依赖单向的回报在 s20 达到顶点:ComprehensiveAgent 组合五个子系统时,不需要理解
它们任何一个的内部,只需要理解它们暴露的查询接口——这是「能改内部而不破坏消费者」
的教科书级体现。
主线二:fail-open 哲学(s08 压缩 → s20 接线)
「局部失败不拖垮全局,warn 后继续」贯穿全程:
- s08 压缩失败时降级为不压缩(宁可超限也不崩);
- s11 网络抖动自动重试、降级 fallback 模型;
- s12/s18/s19 存储损坏一律 fail-open 为空(文件损坏不阻止启动);
- s17 单个 teammate 失败任务回 PENDING 可重新认领;
- s20 worktree create 失败不阻断任务派发(消息退化为无路径)。
这条哲学在 s20 的 AutoClaimer.kt:105-108 得到最精炼的表达:try { create } catch { warn; null }——增强失败就退化为不增强,而不是让增强绑架核心功能。
主线三:接口隔离的复利(s01 LLMProvider → s19 MCP → s20 组合)
「面向接口编程」不是一次性的抽象练习,是贯穿 20 站的复利投资:
- s01 的
LLMProvider接口让所有后续测试用FakeLLMProvider注入; - s02 的
Tool接口让 s19 用McpToolAdapter把远端工具适配成本地工具; - s19 的
McpTransport接口让 stdio/HTTP 两种传输可替换、可独立测试; - s20 的
ComprehensiveAgent组合的是「接口背后的既有实现」,而非复制逻辑。
复利的本质:早期花 10 分钟定义的接口,后期省下 10 次「改所有调用方」的重构。 s20 之所以能 74 行收口,正是因为前 19 站的接口边界都画在了正确的位置。
9. 已知限制与未来
收口不代表完美。s20 的诚实清单:
- MCP 异步注册的竞态(s19 已知):REPL 启动后首条消息发出时,MCP 远端工具 可能尚未就绪。当前用「下一轮重试」缓解,未做「等待注册完成」的启动门。s19 spec 已声明为可接受竞态。
- SSE 响应不支持(s19 非目标):Streamable HTTP 只做请求-响应,服务器流式 推送(如进度、sampling)未实现。
- worktree 合并不自动(s18 预留):任务完成释放目录后,
task/<id>分支默认 删除,未做「merge 回主干」的策略(ff-only?人工审?)——WorktreeRecord.branch为它留了钩子,s20 未启用。 - cron 未进自治闭环(见 7.1):定时任务是并行时间轴,未与任务认领/派发链路 打通——这是有意为之的边界,不是遗漏。
这些限制的共性:它们都是「能力边界」,不是「实现缺陷」——每个都在对应的 spec 里被明确列为非目标或已知竞态。收口 Index 的职责是画清边界,不是假装没有边界。
10. 结语
从 s01 的一个 AgentLoop,到 s20 的一台完整机器,Cat-Code 走完了复刻 Claude Code
20 个核心机制的旅程。这台机器现在能:派生团队、审批计划、自治派发、隔离工作、
外接工具、一张仪表盘看全局——而支撑这一切的,是 20 个 Index 里反复出现的三条
工程主线:包依赖单向、fail-open 哲学、接口隔离的复利。
s20 是终点,但机器的边界就是它未来的起点——worktree 合并策略、SSE 流式、MCP 注册 启动门,都在等下一个「Index 21」。


