为 Claude Code Agent Teams 编写插件:从 Chorus 的实践经验看插件设计模式


February 21, 2025

本文基于 Chorus 项目的实际开发经验,系统介绍 Claude Code 的插件机制,重点探讨如何为 Agent Teams(Swarm 模式)构建插件,以及如何解决多 Agent 协作中的上下文注入难题。

TL;DR:这篇文章要聊什么

Claude Code 的 Agent Teams(也叫 Swarm 模式)让一个 Team Lead Agent 可以并行调度多个 Sub-Agent 协同工作。这是一个强大的能力——但随之而来的问题是:当你有一个外部工作追踪系统时,怎样让每个 Sub-Agent 自动接入你的工作流,而不是靠 Team Lead 在每个 spawn prompt 里手写一大堆 boilerplate?

这篇文章的主要目的是:

  1. 介绍 Claude Code 的插件体系——Marketplace、Plugin Manifest、Hooks、Skills、MCP 配置,这些构成了一套完整的扩展机制
  2. 以 Chorus 为案例,展示一个 Agent-first 的任务管理平台是如何通过插件无缝接入 Claude Code 多 Agent 工作流的
  3. 深入探讨 Sub-Agent 的上下文注入——多 Agent 协作场景下,如何让每个 Sub-Agent 自动获得正确的工作上下文,是插件能否真正落地的关键

如果你正在考虑为自己的工具链(CI/CD、项目管理、监控系统等)编写 Claude Code 插件,希望这篇文章能给你一些启发。


一、Claude Code Agent Teams:Swarm 模式速览

Agent Teams 是 Claude Code 的多 Agent 协作模式。核心概念很简单:

Team Lead (主 Agent)
  ├── Task tool ──> Sub-Agent A (frontend-worker)
  ├── Task tool ──> Sub-Agent B (backend-worker)
  └── Task tool ──> Sub-Agent C (test-runner)

Team Lead 通过 Task 工具 spawn 多个 Sub-Agent,每个 Sub-Agent 是一个独立的 Agent 进程,拥有自己的上下文窗口、工具访问权限、和独立的生命周期。Sub-Agent 之间通过 SendMessage 通信,通过共享文件系统协作。

关键生命周期事件:

事件 触发时机 上下文
PreToolUse:Task Team Lead 调用 Task 工具之前 Team Lead
SubagentStart Sub-Agent 进程启动时(同步) Team Lead
TeammateIdle Sub-Agent 空闲(完成一轮对话) Team Lead
TaskCompleted Claude Code 内部 Task 标记完成 Team Lead
SubagentStop Sub-Agent 进程退出时 Team Lead

注意一个关键事实:所有这些 Hook 都在 Team Lead 的上下文中触发,它们的输出(additionalContext)注入的是 Team Lead 的对话,而不是 Sub-Agent 的。Sub-Agent 的起始 prompt 完全由 Task 工具的 prompt 参数决定——没有任何 Hook 可以直接修改它。

这意味着,如果你想让 Sub-Agent 自动获得某些上下文信息(比如会话 ID、工作流指令),你需要找到间接的注入方式。这正是本文的核心话题。


二、Chorus 是什么,解决什么问题

在深入插件实现之前,先简单介绍一下 Chorus。

Chorus 是一个 AI Agent 与人类的协作平台,受 AI-DLC(AI-Driven Development Lifecycle)方法论启发,实现了其中从 Idea 到 Verify 的核心工作流:

Idea → Proposal → [Document + Task] → Execute → Verify → Done
 ^        ^            ^                 ^          ^        ^
Human   PM Agent    PM Agent         Dev Agent   Admin    Admin

核心理念是反转对话(Reversed Conversation):AI 提出方案,人类审核和验证——而不是人类下指令、AI 执行。

在多 Agent 团队场景下,Chorus 需要解决一个具体问题:可观测性(Observability)。当 5 个 Sub-Agent 同时在写代码时:

  • 哪个 Agent 正在处理哪个 Task?
  • 每个 Agent 的工作进度是什么?
  • Task 状态流转(open → in_progress → to_verify → done)是否正确?
  • Agent 是否还活着(heartbeat)?

Chorus 通过 Session 机制追踪这一切——每个工作中的 Agent 拥有一个 Session,Session checkin 到 Task 上,UI 实时展示谁在做什么。

Chorus 用起来是什么样的

文字描述总是抽象的,来看几张实际截图。

Kanban 看板 — 实时追踪 Agent 工作状态

Kanban 看板

这是 Chorus 的核心视图。每个 Task 卡片上的彩色徽章显示当前正在处理该 Task 的 Agent Session。当 Sub-Agent 调用 chorus_session_checkin_task 后,徽章实时出现;调用 checkout 后消失。Task 在不同列之间的移动(Open → In Progress → To Verify → Done)由 Agent 通过 MCP 工具驱动。

Task 依赖图(DAG)

DAG

Chorus 中的 Task 可以声明依赖关系,形成有向无环图。PM Agent 在创建 Proposal 时通过 dependsOnDraftUuids 设置依赖。UI 使用 dagre 自动布局。Team Lead 可以据此决定 spawn 顺序——先处理没有依赖的 Task,被依赖的 Task 完成后,下游 Task 自动解除阻塞。

Proposal — AI 提出方案,人类审核

Proposal

这是 AI-DLC 的核心理念"反转对话"的体现:PM Agent 分析 Idea 后,创建包含 PRD 文档草案和 Task 草案的 Proposal。Admin(人类)审核通过后,草案自动物化为真实的 Document 和 Task 实体。

Task 详情 — Session 追踪

Task Tracking

Task 详情页展示了完整的工作历史:哪些 Session 曾经 checkin 过这个 Task,每次 checkin/checkout 的时间,以及 Agent 提交的工作报告。这就是 Chorus 的可观测性——即使 5 个 Agent 同时工作,你也能清楚看到每个人在做什么。

像素办公室 — Agent 的虚拟工位

Pixel Workspace

这是 Chorus 的趣味功能:每个活跃的 Agent Session 在像素办公室里有自己的工位。Agent checkin 到 Task 时开始"工作"动画,idle 时休息,完成时庆祝。纯粹的可视化娱乐,但能一眼看出团队的工作状态。


三、为什么要写 Claude Code 插件

没有插件之前,Team Lead 需要在每个 Sub-Agent 的 spawn prompt 里手写大量 boilerplate:

Task({
  name: "frontend-worker",
  prompt: """
    你的 Chorus session UUID: ???(Team Lead 还不知道,因为 session 还没创建)
    你的 Chorus task UUID: task-A-uuid

    工作前:
    1. 创建 session: chorus_create_session(...)
    2. Checkin: chorus_session_checkin_task(sessionUuid, taskUuid)
    3. 更新状态: chorus_update_task(taskUuid, "in_progress", sessionUuid)

    工作中:
    4. 汇报进度: chorus_report_work(taskUuid, report, sessionUuid)

    完成后:
    5. Checkout: chorus_session_checkout_task(sessionUuid, taskUuid)
    6. 提交验证: chorus_submit_for_verify(taskUuid, summary)
    7. 关闭 session: chorus_close_session(sessionUuid)
  """
})

问题显而易见:

  1. Session UUID 不可能预知——Session 需要调用 MCP 才能创建,但 prompt 在 spawn 之前就要写好
  2. 每个 Sub-Agent 的 prompt 里重复同样的 boilerplate——6-7 步工作流指令,占据大量 prompt 空间
  3. Team Lead 必须记住所有步骤——忘了 checkout?忘了 heartbeat?Session 就会失效
  4. Session 生命周期管理复杂——创建、复用、重开、心跳、关闭,全靠手动

有了插件,这一切可以自动化:

Task({
  name: "frontend-worker",
  prompt: """
    你的 Chorus task UUID: task-A-uuid
    读取 .chorus/sessions/frontend-worker.json,按里面的 workflow 指令操作。
    然后实现前端用户表单组件...
  """
})

从 15 行 boilerplate 到 3 行。其余的全部由插件在 Hook 中自动处理。


四、Claude Code 插件体系全览

Claude Code 的插件是一个目录,包含以下组件:

my-plugin/
├── .claude-plugin/
│   └── plugin.json          # 插件清单(元数据)
├── .mcp.json                # MCP 服务器配置
├── hooks/
│   └── hooks.json           # Hook 配置
├── bin/                     # Hook 脚本
│   ├── on-session-start.sh
│   └── on-subagent-start.sh
└── skills/
    └── my-skill/
        ├── SKILL.md         # 技能入口文件
        └── references/      # 参考文档

下面逐一介绍各组件。

4.1 Plugin Manifest(plugin.json)

位于 .claude-plugin/plugin.json,是插件的身份证:

{
  "name": "chorus",
  "description": "Chorus AI-DLC collaboration platform plugin...",
  "version": "0.1.3",
  "author": { "name": "Chorus-AIDLC" },
  "homepage": "https://github.com/Chorus-AIDLC/chorus",
  "license": "AGPL-3.0",
  "keywords": ["ai-dlc", "mcp", "multi-agent", "session"]
}

plugin.json 是可选的——如果省略,Claude Code 会从目录名推断插件名称,并自动发现各组件。但推荐总是提供,便于版本管理和分发。

4.2 Marketplace(插件市场)

插件通过 Marketplace 分发。Marketplace 本质上是一个 JSON 清单文件(.claude-plugin/marketplace.json),放在 GitHub 公开仓库中即可。Chorus 就是用自己的 GitHub 仓库作为 Marketplace:

{
  "name": "chorus-plugins",
  "owner": { "name": "Chorus-AIDLC" },
  "plugins": [
    {
      "name": "chorus",
      "source": "./public/chorus-plugin",
      "description": "Chorus AI-DLC collaboration platform plugin...",
      "version": "0.1.3",
      "category": "project-management",
      "tags": ["ai-dlc", "collaboration", "mcp", "session"]
    }
  ]
}

用户安装 Chorus 插件的实际流程:

# 1. 添加 marketplace — 指向 GitHub 仓库(仓库中包含 .claude-plugin/marketplace.json)
/plugin marketplace add Chorus-AIDLC/chorus

# 2. 安装插件 — 格式为 插件名@marketplace名
/plugin install chorus@chorus-plugins

# 3. 可以指定作用域
/plugin install chorus@chorus-plugins --scope project  # 项目级(团队共享,提交到 git)
/plugin install chorus@chorus-plugins --scope local    # 本地级(仅自己)

source 字段指向插件在仓库中的相对路径。除了本地路径,还支持指向其他 GitHub repo("source": {"source": "github", "repo": "owner/repo"})或 Git URL 等来源。

4.3 MCP 配置(.mcp.json

插件可以自带 MCP Server 配置,安装后自动生效:

{
  "mcpServers": {
    "chorus": {
      "type": "http",
      "url": "${CHORUS_URL}/api/mcp",
      "headers": {
        "Authorization": "Bearer ${CHORUS_API_KEY}"
      }
    }
  }
}

${CHORUS_URL}${CHORUS_API_KEY} 是环境变量——Claude Code 在运行时自动替换。用户只需设置环境变量,插件就能连接到正确的服务。

这意味着:插件安装后,所有 MCP 工具自动可用。Sub-Agent 也能访问(前提是 MCP 配置在项目级别,而不是用户级别)。

Chorus 的 MCP 配置:Chorus 通过 HTTP Streamable Transport 暴露了 50+ 个 MCP 工具,按角色分组(公共工具、PM 工具、Developer 工具、Admin 工具、Session 工具)。用户只需设置两个环境变量 CHORUS_URLCHORUS_API_KEY,插件就能连接到 Chorus 服务。API Key 以 cho_ 前缀开头,携带 Agent 角色信息,服务端据此决定哪些工具可见。

4.4 Skills(技能系统)

Skills 是插件内置的指令集,Claude 可以在需要时自动调用,用户也可以通过 /skill-name 手动触发。

一个 Skill 由一个 SKILL.md 入口文件和可选的 references/ 参考文档组成:

---
name: chorus
description: Chorus AI Agent collaboration platform Skill...
metadata:
  author: chorus
  version: "0.1.1"
  category: project-management
  mcp_server: chorus
---

# Chorus Skill

This Skill guides AI Agents on how to use Chorus MCP tools...

## Skill Files

| File | Description |
|------|-------------|
| **references/02-pm-workflow.md** | PM Agent workflow |
| **references/03-developer-workflow.md** | Developer Agent workflow |
| **references/06-claude-code-agent-teams.md** | Agent Teams integration |

Chorus 的 Skill 体系:Chorus 包含 7 个参考文档references/00references/06),覆盖从公共工具、PM 工作流、Developer 工作流、Admin 工作流,到 Session 管理和 Agent Teams 集成的完整指南。当 Agent 调用 /chorus 或 Claude 判断需要 Chorus 知识时,Skill 文档自动加载到上下文中。这相当于给每个 Agent 一本随身携带的操作手册——无论是 Team Lead 还是 Sub-Agent,都能通过 Skill 了解正确的工作流程。

Skill 的 frontmatter 支持丰富的配置项:

---
name: my-skill
description: "When to use this skill"
allowed-tools: Read, Grep, Glob     # 允许无需权限使用的工具
model: claude-opus-4-6              # 指定模型
context: fork                       # 在 subagent 中运行
disable-model-invocation: true      # 仅用户可触发(Claude 不会自动调用)
---

4.5 Hooks(钩子系统)

Hooks 是插件的核心——它们让你在 Claude Code 生命周期的关键节点执行自定义逻辑。

配置在 hooks/hooks.json

{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup|resume|compact",
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PLUGIN_ROOT}/bin/on-session-start.sh"
      }]
    }],
    "SubagentStart": [{
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PLUGIN_ROOT}/bin/on-subagent-start.sh"
      }]
    }],
    "SubagentStop": [{
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PLUGIN_ROOT}/bin/on-subagent-stop.sh",
        "async": true
      }]
    }]
  }
}

Hook 类型

Claude Code 支持三种 Hook 执行方式:

类型 说明 适用场景
command 执行 shell 命令,通过 stdin 接收事件 JSON,通过 stdout 输出结果 大多数场景
prompt 用 LLM 评估决策,模型返回 {ok: true/false} 需要智能判断时(如代码审查)
agent spawn 一个有工具访问权限的 subagent 来验证 需要执行复杂多步验证时(如跑测试)

Chorus 插件的所有 Hook 都使用 command 类型——因为 Chorus 的 Hook 逻辑是确定性的(调 API、读写文件、管理状态),不需要 LLM 判断。promptagent 更适合需要"理解"代码内容才能做决策的场景,比如在 Stop 事件中用 agent 类型自动跑测试来判断任务是否真的完成。

Hook 事件全表

事件 触发时机 能否阻断
SessionStart 会话开始/恢复/compact
UserPromptSubmit 用户提交输入
PreToolUse 工具执行前
PostToolUse 工具执行后
SubagentStart Sub-Agent 启动
SubagentStop Sub-Agent 退出
TeammateIdle Sub-Agent 空闲
TaskCompleted CC Task 完成
SessionEnd 会话结束

Hook 输出格式

知道有哪些事件后,下一个问题是:Hook 脚本能返回什么来影响 Claude 的行为? Hook 通过 stdout 输出 JSON:

{
  "systemMessage": "用户可见的通知消息",
  "hookSpecificOutput": {
    "hookEventName": "SubagentStart",
    "additionalContext": "这段文字会注入到 Claude 的上下文中",
    "permissionDecision": "allow"
  }
}

关键字段:

  • systemMessage:显示在 Claude Code UI 中的通知,用户可见
  • additionalContext:注入到 LLM 的 system context 中——这是 Hook 影响 Claude 行为的主要手段。Chorus 的 SessionStart Hook 就是通过它把 checkin 结果(身份、任务、通知)注入到 Agent 上下文的
  • permissionDecisionallow / deny / ask,用于 PreToolUse 控制工具执行权限
  • suppressOutput:设为 true 可静默输出——Chorus 的 TeammateIdle Hook 用它来避免每次心跳都弹通知

同步 vs 异步

  • 同步 Hook(默认):阻塞 Claude 直到完成。适合需要立即影响的场景——Chorus 的 SubagentStart 必须同步,因为它要在 Sub-Agent 开始工作前创建好 session 并写入 session file
  • 异步 Hook"async": true):后台运行,不阻塞。适合不影响流程的场景——Chorus 的 SubagentStop(清理资源)和 TeammateIdle(心跳)都是异步的

每个 Hook 事件 Chorus 用它做什么

了解了事件、输出格式、同步/异步之后,来看 Chorus 插件如何具体使用每个 Hook。

SessionStart — Checkin + 上下文注入

这是插件的"开机自检"。Chorus 在这里做三件事:

  1. 调用 chorus_checkin() MCP 工具,获取当前 Agent 的身份(角色、名称、人格)、已分配的 Idea 和 Task、未读通知
  2. 将完整的 checkin 结果通过 additionalContext 注入到 Claude 的上下文中——Agent 一启动就知道自己是谁、该做什么
  3. 扫描 .chorus/sessions/ 目录,列出已有的 Sub-Agent session 文件——这是为了处理 Claude Code 会话中断后恢复的情况:上次的 Sub-Agent session 文件可能还在,Team Lead 恢复后需要知道哪些 session 仍然存在
# on-session-start.sh 核心逻辑
CHECKIN_RESULT=$("$API" mcp-tool "chorus_checkin" '{}')

CONTEXT="# Chorus Plugin — Active
Chorus is connected at ${CHORUS_URL}.
## Checkin Result
${CHECKIN_RESULT}
## Session Management — IMPORTANT
The Chorus Plugin fully automates session lifecycle...
Do NOT call chorus_create_session for sub-agents."

"$API" hook-output "$USER_MSG" "$CONTEXT" "SessionStart"

效果:Agent 在第一轮对话就拥有了完整的项目上下文和行为指南,不需要用户手动告知。

UserPromptSubmit — 轻量级状态提醒

每次用户输入都会触发,所以必须极快(<100ms)。Chorus 在这里不做任何网络调用,只做本地文件检查:

# on-user-prompt.sh — 纯本地操作,不调 MCP
# 统计 .chorus/sessions/ 下的 json 文件数量
CONTEXT="[Chorus Plugin Active]
- Active sub-agent sessions (3): frontend-worker, backend-worker, test-runner"

这给 Team Lead 一个持续的状态感知:当前有几个 Sub-Agent session 在运行。

PreToolUse — 工作流引导(3 个子 Hook)

Chorus 注册了 3 个 PreToolUse Hook,分别匹配不同的工具:

matcher 脚本 Chorus 做什么
EnterPlanMode on-pre-enter-plan.sh 注入 Chorus Proposal 工作流指南——"先创建 Proposal,设置 Task 依赖 DAG,提交审批后再编码"
ExitPlanMode on-pre-exit-plan.sh 提醒检查——"退出 Plan Mode 前确认 Proposal 已创建并提交"
Task on-pre-spawn-agent.sh 捕获 Sub-Agent 的 name/type 写入 pending 文件;通过 additionalContext 提醒 Team Lead 在 prompt 中包含 task UUID

EnterPlanModeExitPlanMode 展示了一个有趣的用法:用 Hook 引导 Agent 遵循特定的工作流程。当 Agent 进入 Plan Mode 时,Chorus 自动注入"先创建 Proposal 再编码"的指导;退出 Plan Mode 时,检查是否已有 Proposal。这不是强制阻断(permissionDecision 仍然是 allow),而是通过 additionalContext 做软性引导。

SubagentStart — Session 自动创建 + PE 注入(核心)

这是 Chorus 插件最核心的 Hook,详见第五章。简要概括:认领 pending 文件 → 创建/复用 Session → 写 session file(含 workflow PE)→ 存储状态映射。

SubagentStop — 自动清理 + 任务发现

异步执行,做四件事:(1) 批量 checkout 所有未关闭的 task checkin,(2) 关闭 Session,(3) 清理本地文件和状态,(4) 查询项目中新解除阻塞的 Task 并通过 additionalContext 通知 Team Lead——这一步非常有价值,它实现了自动任务调度发现:当一个前置 Task 完成后,下游 Task 自动解除阻塞,Team Lead 立即得到通知可以分配新工作。

TeammateIdle — 自动心跳

异步 + suppressOutput: true。只做一件事:调 chorus_session_heartbeat 保持 Session 活跃。Chorus 的 Session 超过 1 小时没有心跳会自动标记为 inactive——这个 Hook 确保只要 Sub-Agent 还在运行,Session 就不会失活。

TaskCompleted — 元数据桥接

当 Claude Code 内部的 Task 标记完成时,Chorus 检查 task 描述中是否包含 chorus:task:<uuid> 标签。如果有,自动执行 chorus_session_checkout_task。这是一个优雅的元数据桥接模式——通过在 CC Task 描述中嵌入 Chorus task UUID,让两个系统的 Task 生命周期联动。

SessionEnd — 清理 .chorus/ 目录

会话结束时,检查是否所有 session 文件都已清理、state.json 是否为空。如果是,删除整个 .chorus/ 目录,不留垃圾文件。


五、Chorus 插件的完整实现

现在进入正题——Chorus 插件是如何利用上述机制解决多 Agent 协作问题的。

5.1 整体架构

Team Lead 调用 Task 工具 spawn Sub-Agent
  │
  ├─ [PreToolUse:Task] on-pre-spawn-agent.sh
  │    写入 .chorus/pending/<name> 文件(捕获 agent name)
  │    向 Team Lead 注入提醒:"在 prompt 里包含 task UUID"
  │
  ├─ [SubagentStart] on-subagent-start.sh    ← 核心
  │    认领 pending 文件(原子 mv,处理并发)
  │    创建/复用/重开 Chorus Session(MCP 调用)
  │    写入 .chorus/sessions/<name>.json(含 workflow PE)
  │    存储 state 映射(agent_id ↔ session_uuid)
  │
  ├─ Sub-Agent 开始执行
  │    读取 .chorus/sessions/<name>.json → 获得 sessionUuid + 工作流指令
  │    按 workflow 字段自主执行 checkin → in_progress → report → checkout → submit
  │
  ├─ [TeammateIdle] on-teammate-idle.sh(异步)
  │    发送 session heartbeat,保持 session 活跃
  │
  ├─ [TaskCompleted] on-task-completed.sh
  │    检测 chorus:task:<uuid> 标签,自动 checkout
  │
  └─ [SubagentStop] on-subagent-stop.sh(异步)
       批量 checkout 所有 task
       关闭 Chorus Session
       清理本地状态
       查询并展示新解除阻塞的 task

5.2 .chorus/ 目录:连接一切的桥梁

前面多次提到"共享文件系统",现在展开说。Chorus 插件在项目根目录下维护一个 .chorus/ 目录(gitignored),它是 Team Lead、Sub-Agent、和所有 Hook 之间的信息枢纽:

.chorus/                              # 插件运行时状态(gitignored)
├── state.json                        # 全局状态 KV 存储
├── state.json.lock                   # flock 排他锁文件
├── sessions/                         # Sub-Agent session 文件(含 workflow PE)
│   ├── frontend-worker.json
│   ├── backend-worker.json
│   └── test-runner.json
├── pending/                          # PreToolUse:Task 写入,等待 SubagentStart 认领
│   └── <agent-name>
└── claimed/                          # SubagentStart 认领后的文件
    └── <agent-id>

核心:state.json — 跨 Hook 的状态共享

每个 Hook 都是独立的 shell 进程,它们不共享内存。state.json 是所有 Hook 之间的共享状态存储:

{
  "session_a0ed860": "699f8ed4-4a98-4522-8321-662a2222a180",
  "agent_for_session_699f8ed4-...": "a0ed860",
  "session_frontend-worker": "699f8ed4-...",
  "name_for_agent_a0ed860": "frontend-worker",
  "main_session_uuid": "..."
}

存储的是四组映射关系:agent_id → session_uuidsession_uuid → agent_idagent_name → session_uuidagent_id → agent_name。这样任何 Hook 只要知道其中一个 ID,就能查到其余所有关联信息。

并发写入保护:flock

当 5 个 Sub-Agent 同时 spawn 时,5 个 SubagentStart Hook 会并发执行,每个都要往 state.json 写入 4 个 key。如果不做保护,JSON 文件会被并发写入损坏。

Chorus 在 chorus-api.sh 中用 flock 排他锁解决这个问题:

# chorus-api.sh 中的 state_set 实现
state_set() {
  local key="$1" value="$2"
  (
    # 获取排他锁,超时 5 秒
    flock -w 5 200 || { echo "WARN: flock timeout" >&2; return 0; }
    # 在锁保护下修改 JSON
    jq --arg k "$key" --arg v "$value" '.[$k] = $v' "$STATE_FILE" > "$tmp" \
      && mv "$tmp" "$STATE_FILE"
  ) 200>"${STATE_FILE}.lock"
}

关键细节:

  • flock -w 5 200:在文件描述符 200 上获取排他锁,等待最多 5 秒
  • 200>"${STATE_FILE}.lock":锁文件与 state 文件分开(.lock 后缀)
  • jq ... > $tmp && mv $tmp:先写临时文件再原子替换,避免写到一半崩溃导致文件损坏
  • 超时不报错(return 0)——宁可丢失一次状态写入,也不阻塞整个 Hook 链

pending/claimed/:原子所有权转移

SubagentStart 事件只提供 agent_idagent_type不提供 Team Lead 给 Sub-Agent 起的名字。但 session 需要用名字命名(这样 Sub-Agent 才能通过名字找到自己的 session 文件)。

解决方案是两个 Hook 的接力:

  1. PreToolUse:Task(Team Lead 上下文)能从 tool_input 中拿到 name 参数,写入 pending/<name> 文件
  2. SubagentStart(仍是 Team Lead 上下文,但并发执行)通过 mv pending/<name> claimed/<agent_id> 原子认领
时间线:
  T1  PreToolUse:Task 触发 → 写 .chorus/pending/frontend-worker
  T2  PreToolUse:Task 触发 → 写 .chorus/pending/backend-worker
  T3  SubagentStart(agent_id=a0e) 触发 → mv pending/frontend-worker → claimed/a0e ✓
  T4  SubagentStart(agent_id=b1f) 触发 → mv pending/backend-worker → claimed/b1f ✓
  T4' SubagentStart(agent_id=c2g) 触发 → mv pending/frontend-worker → 失败(已被 a0e 认领)
                                        → mv pending/backend-worker → 失败(已被 b1f 认领)
                                        → 没有更多 pending 文件 → 跳过(内部 agent,不创建 session)

mv 在同一文件系统上是原子操作——只有一个进程能成功移动同一个文件。这比 flock 更轻量,适合"谁先到谁拿"的场景。

sessions/ — Sub-Agent 的信息入口

这是对 Sub-Agent 唯一可见的部分。每个 session file 既是数据(sessionUuid),也是指令(workflow PE)。文件名就是 agent name——Sub-Agent 只需 Read .chorus/sessions/<my-name>.json 就能获得一切。

生命周期:创建到清理

SessionStart  → mkdir -p .chorus/(如果不存在)
PreToolUse    → 写 .chorus/pending/<name>
SubagentStart → mv pending → claimed,写 sessions/<name>.json,更新 state.json
TeammateIdle  → 读 state.json(查 session_uuid),无写入
TaskCompleted → 读 state.json(查 session_uuid),无写入
SubagentStop  → 删 sessions/<name>.json,删 claimed/<agent_id>,清 state.json 条目
SessionEnd    → 如果 sessions/ 为空且 state.json 为空 → rm -rf .chorus/

整个目录的生命周期和 Claude Code session 一致——开始时创建,结束时清理,不留痕迹。

5.3 核心难题:Sub-Agent 的上下文注入

前面提到,所有 Hook 的输出都只注入到 Team Lead 的上下文中。那么,如何让 Sub-Agent 也能获得必要的信息?

Chorus 插件的答案是:利用共享文件系统作为信息传递通道

Sub-Agent 虽然和 Team Lead 不共享上下文窗口,但它们共享同一个文件系统。而 Team Lead 的 spawn prompt 里一定会告诉 Sub-Agent 去读某个文件——这个文件就成了上下文注入的天然入口。

实现:Session file 内嵌 workflow PE

SubagentStart Hook 在写 session file 时,除了 sessionUuid 等数据字段外,额外写入一个 workflow 数组——包含完整的 5 步工作流指令,且每个 MCP 调用示例中的 sessionUuid 都是真实值(Bash heredoc 在写文件时展开变量):

# on-subagent-start.sh 核心片段
cat > "${SESSIONS_DIR}/${SESSION_NAME}.json" <<SESSIONEOF
{
  "sessionUuid": "${SESSION_UUID}",
  "agentName": "${SESSION_NAME}",
  "workflow": [
    "=== Chorus Workflow — FOLLOW THESE STEPS ===",
    "Your Chorus session UUID is: ${SESSION_UUID}",
    "",
    "1. Check in: chorus_session_checkin_task({ sessionUuid: \"${SESSION_UUID}\", taskUuid: \"<TASK_UUID>\" })",
    "2. Start:    chorus_update_task({ taskUuid: \"<TASK_UUID>\", status: \"in_progress\", sessionUuid: \"${SESSION_UUID}\" })",
    "3. Report:   chorus_report_work({ taskUuid: \"<TASK_UUID>\", report: \"...\", sessionUuid: \"${SESSION_UUID}\" })",
    "4. Checkout:  chorus_session_checkout_task({ sessionUuid: \"${SESSION_UUID}\", taskUuid: \"<TASK_UUID>\" })",
    "5. Submit:   chorus_submit_for_verify({ taskUuid: \"<TASK_UUID>\", summary: \"...\" })",
    "",
    "Replace <TASK_UUID> with the actual Chorus task UUID provided in your prompt."
  ]
}
SESSIONEOF

Sub-Agent 读取这个文件后,得到了数据(sessionUuid)和指令(workflow)。它只需要把 <TASK_UUID> 替换为 Team Lead 在 prompt 中给的值,就能直接按步骤执行。

这样 Team Lead 的 spawn prompt 只需要:

Task({
  name: "frontend-worker",
  prompt: """
    Your Chorus task UUID: task-A-uuid
    Read .chorus/sessions/frontend-worker.json and follow the workflow inside.
    Then implement the frontend user form component...
  """
})

Bash heredoc 中的 ${SESSION_UUID} 会在文件写入时被展开为真实值。

5.4 Session 复用:避免重复创建

当 Team Lead 多次 spawn 同名的 Sub-Agent 时(比如 Task 被 Admin reopen 后重新分配),插件不会创建新 Session,而是复用已有的:

# on-subagent-start.sh 中的复用逻辑
if [ "$MATCH_STATUS" = "active" ]; then
    SESSION_UUID="$MATCH_UUID"         # 直接复用
    SESSION_ACTION="reused"
elif [ "$MATCH_STATUS" = "closed" ] || [ "$MATCH_STATUS" = "inactive" ]; then
    # 重开已关闭的 session
    chorus_reopen_session(sessionUuid)
    SESSION_ACTION="reopened"
else
    # 创建新 session
    chorus_create_session(name)
    SESSION_ACTION="created"
fi

5.5 自动清理:SubagentStop

当 Sub-Agent 退出时,on-subagent-stop.sh(异步执行)负责清理:

  1. 查询 Session 的所有活跃 checkin,逐一 checkout
  2. 关闭 Chorus Session
  3. 删除本地状态(state entries、session file、claimed file)
  4. 查询项目中新解除阻塞的 Task,通知 Team Lead

这样即使 Sub-Agent 忘了 checkout 或 close session,插件也会兜底。

5.6 自动心跳:TeammateIdle

Sub-Agent 在两轮对话之间会进入 idle 状态,此时 TeammateIdle Hook 自动发送 heartbeat:

# on-teammate-idle.sh
"$API" mcp-tool "chorus_session_heartbeat" \
  "$(printf '{"sessionUuid":"%s"}' "$SESSION_UUID")"

输出被 suppressOutput: true 静默——心跳太频繁,不需要通知 Team Lead。


六、设计模式总结

从 Chorus 插件的实践中,可以提炼出几个通用的设计模式:

模式 1:文件系统作为 Sub-Agent 通信通道

Hook 写文件  →  Sub-Agent 读文件
(Team Lead 上下文)    (Sub-Agent 上下文)

这是目前向 Sub-Agent 注入上下文的唯一可靠方式。适用于任何需要在 spawn 时传递动态信息给 Sub-Agent 的场景。

模式 2:PE 注入到数据文件中

不要把数据和指令分开——直接把工作流指令放在 Sub-Agent 必读的数据文件里。Sub-Agent 读取数据的同时就获得了操作指南。

模式 3:PreToolUse 捕获 + SubagentStart 执行

SubagentStart 事件中拿不到 Sub-Agent 的名字(只有 agent_idagent_type),但 PreToolUse:Task 可以从 tool_input 中提取。两个 Hook 通过文件系统(pending → claimed)传递信息。

模式 4:异步 Hook 做非阻塞清理

Session 关闭、资源清理、通知发送等不影响流程的操作,放在异步 Hook 中执行。不要让清理逻辑阻塞 Sub-Agent 的退出。

模式 5:Hook 只提醒,不强制

PreToolUse:Task 向 Team Lead 注入提醒("记得在 prompt 里包含 task UUID"),但不阻断操作。团队协作中,建议优于强制——过于严格的 Hook 会降低使用体验。


七、快速开始:编写你自己的插件

如果你想为自己的工具链编写 Claude Code 插件,这里是最小可行步骤:

Step 1:创建目录结构

mkdir -p my-plugin/.claude-plugin my-plugin/hooks my-plugin/bin

Step 2:编写 plugin.json

{
  "name": "my-plugin",
  "description": "My custom plugin for Claude Code",
  "version": "0.1.0"
}

Step 3:编写第一个 Hook

hooks/hooks.json:

{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup",
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PLUGIN_ROOT}/bin/on-start.sh"
      }]
    }]
  }
}

bin/on-start.sh:

#!/usr/bin/env bash
set -euo pipefail

cat <<EOF
{
  "systemMessage": "My plugin is active!",
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "My Plugin is connected. Custom workflow instructions here."
  }
}
EOF

Step 4:本地测试

chmod +x my-plugin/bin/on-start.sh
claude --plugin-dir ./my-plugin

Step 5:发布到 Marketplace

创建 .claude-plugin/marketplace.json

{
  "name": "my-marketplace",
  "owner": { "name": "Your Name" },
  "plugins": [{
    "name": "my-plugin",
    "source": "./my-plugin",
    "version": "0.1.0"
  }]
}

写在最后

Claude Code 的插件系统提供了一套完整的扩展机制——从 Marketplace 分发,到 MCP 工具集成,到 Hooks 生命周期管理,再到 Skills 知识注入。Agent Teams(Swarm 模式)的引入让多 Agent 协作成为可能,而插件让这种协作变得可管理、可观测。

Chorus 插件的实践表明,即使面对"Hook 输出无法直接注入 Sub-Agent prompt"这样的限制,通过巧妙利用共享文件系统和 PE 注入技术,仍然可以实现优雅的自动化工作流。

如果你对 Chorus 感兴趣,欢迎访问 GitHub 了解更多。如果你正在构建自己的 Claude Code 插件,希望本文的经验能帮你少走弯路。

CC BY-NC-ND

本作品采用知识共享署名4.0署名-非商业性使用-禁止演绎(BY-NC-ND)国际许可协议进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,但不得对本创作进行修改,亦不得依据本创作进行再创作,不得将本创作运用于商业用途。