02 启动与注入:规则如何进入会话¶
一、启动问题:技能文件存在,不代表 Agent 会使用¶
一个技能库最容易失败的方式,是把几十个 SKILL.md 放进目录,然后期待 Agent 自己发现它们。Superpowers 的第一条工程化判断是:入口规则必须在会话开始时显式出现。
在 Claude/Cursor/Copilot 风格的宿主里,会话可能经历 startup、clear 或 compact;入口技能必须在这些边界重新建立。因此 hooks/hooks.json 只做一件事:为 SessionStart 注册 hooks/run-hook.cmd session-start。
{
"hooks": {
"SessionStart": [{
"matcher": "startup|clear|compact",
"hooks": [{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"shell": "bash",
"async": false
}]
}]
}
}
这里有三个设计点:
- 事件范围明确:不仅 startup 注入,清空上下文或压缩后也要恢复入口规则。
- 同步执行:
async: false保证宿主在继续前拿到入口上下文。 - 根目录变量化:使用
${CLAUDE_PLUGIN_ROOT},让插件安装位置不影响脚本。
源码证据:hooks/hooks.json;Cursor 的同类声明在 hooks/hooks-cursor.json。
二、session-start 做了什么¶
启动脚本的主流程可以抽象为:
sequenceDiagram
participant Host as Harness
participant Hook as session-start
participant Skill as using-superpowers/SKILL.md
participant Model as Agent context
Host->>Hook: SessionStart
Hook->>Hook: 计算 PLUGIN_ROOT
Hook->>Skill: cat 完整内容
Skill-->>Hook: YAML frontmatter + 正文
Hook->>Hook: escape slash quote newline tab
Hook->>Hook: 选择宿主 JSON 字段
Hook-->>Host: JSON additional context
Host->>Model: 注入本轮会话
hooks/session-start 的实现故意使用 Bash 原生参数替换完成 JSON escaping,而不是逐字符循环。其核心是:
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
escape_for_json() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\r'/\\r}"
s="${s//$'\t'/\\t}"
printf '%s' "$s"
}
这个实现把“读取失败”也变成上下文的一部分,而不是静默失败。模型会看到错误文本,至少知道入口规则没有成功读取;对诊断和反馈来说,这比 hook 没有任何输出更好。
三、同一内容,不同的 JSON 方言¶
脚本根据环境变量选择输出字段:
| 环境 | 输出字段 | 目标宿主 |
|---|---|---|
CURSOR_PLUGIN_ROOT 存在 |
additional_context |
Cursor hook |
CLAUDE_PLUGIN_ROOT 存在且不是 Copilot |
hookSpecificOutput.additionalContext |
Claude Code |
其他情况,含 COPILOT_CLI=1 |
顶层 additionalContext |
Copilot CLI / SDK 兼容宿主 |
这段判断解决的是一个现实问题:不同宿主对“hook 返回额外上下文”的字段命名不一致,而且 Claude 同时读取多个字段会产生重复注入。源码中特别写下注释,说明它宁可只输出当前平台消费的一个字段,也不让两个字段同时存在。
这是一种兼容性边界下沉:技能正文不应该知道 additional_context 和 hookSpecificOutput 的差别;只有 transport 层知道。
四、为什么只注入 using-superpowers¶
启动时没有把所有技能正文都塞进上下文,原因有三层:
上下文成本¶
技能文件合计数千行。全量注入会让 Agent 在每个会话都承担不必要的 token 成本,还可能稀释当前任务的上下文。
触发准确性¶
using-superpowers 的职责不是指导实现,而是告诉 Agent:如果有 1% 的可能某个技能适用,就先调用 Skill 工具。其余技能的 frontmatter description 负责在后续发现阶段提供触发线索。
可替换性¶
入口规则是稳定协议,具体工作流可以持续增加、拆分或重命名。把二者分开,意味着新增技能不需要重写 hook 的字符串。
五、Manifest 如何宣告能力¶
以 .codex-plugin/plugin.json 为例:
{
"name": "superpowers",
"version": "6.2.0",
"skills": "./skills/",
"hooks": {},
"interface": {
"displayName": "Superpowers",
"capabilities": ["Interactive", "Read", "Write"],
"defaultPrompt": [
"I've got an idea for something I'd like to build.",
"Let's add a feature to this project."
]
}
}
manifest 解决“插件如何被宿主识别”,hook 解决“会话如何被初始化”,skill 解决“初始化后具体怎么工作”。三者不要混写:
六、Compact/clear 为什么也是架构事件¶
大多数 Agent 在上下文压缩后只关注“剩下哪些消息”。Superpowers 还要恢复行为规则:压缩可能丢失上一段会话中由 hook 注入的入口文本,也可能让模型忘记技能调用约束。因此 startup|clear|compact 被统一看成“需要重新建立过程边界”的事件。
这和普通系统的初始化很像:不是每个请求都重装程序,但每个新进程、恢复点或状态重置点都需要重新加载必要的不变量。
七、移植到新宿主的最小清单¶
新增一个 harness 时,先回答五个问题:
- 插件 manifest 的名称、版本和技能目录字段叫什么?
- 宿主会在什么生命周期事件运行脚本?
- 脚本输出的上下文字段是什么,是否需要 JSON escaping?
- 宿主的“调用技能、派发 Agent、向用户提问、写 todo”工具名是什么?
- 宿主是否会重复消费多个额外上下文字段?
只要这五项落在适配层,核心 SKILL.md 通常不需要改动。
小结¶
启动链路的本质是“把一个过程协议可靠地送进模型上下文”。Superpowers 用同步 hook 建立入口,用 JSON transport 兼容宿主,再用按需 Skill 调用控制上下文成本。它没有试图让每个平台行为完全相同,而是让它们共享同一个更高层的开发过程。