跳转至

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
      }]
    }]
  }
}

这里有三个设计点:

  1. 事件范围明确:不仅 startup 注入,清空上下文或压缩后也要恢复入口规则。
  2. 同步执行async: false 保证宿主在继续前拿到入口上下文。
  3. 根目录变量化:使用 ${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_contexthookSpecificOutput 的差别;只有 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 解决“初始化后具体怎么工作”。三者不要混写:

manifest = 身份 + 入口 + 能力声明
hook     = transport + 时机 + 宿主方言
skill    = 触发条件 + 过程协议 + 验证门槛

六、Compact/clear 为什么也是架构事件

大多数 Agent 在上下文压缩后只关注“剩下哪些消息”。Superpowers 还要恢复行为规则:压缩可能丢失上一段会话中由 hook 注入的入口文本,也可能让模型忘记技能调用约束。因此 startup|clear|compact 被统一看成“需要重新建立过程边界”的事件。

这和普通系统的初始化很像:不是每个请求都重装程序,但每个新进程、恢复点或状态重置点都需要重新加载必要的不变量。

七、移植到新宿主的最小清单

新增一个 harness 时,先回答五个问题:

  1. 插件 manifest 的名称、版本和技能目录字段叫什么?
  2. 宿主会在什么生命周期事件运行脚本?
  3. 脚本输出的上下文字段是什么,是否需要 JSON escaping?
  4. 宿主的“调用技能、派发 Agent、向用户提问、写 todo”工具名是什么?
  5. 宿主是否会重复消费多个额外上下文字段?

只要这五项落在适配层,核心 SKILL.md 通常不需要改动。

小结

启动链路的本质是“把一个过程协议可靠地送进模型上下文”。Superpowers 用同步 hook 建立入口,用 JSON transport 兼容宿主,再用按需 Skill 调用控制上下文成本。它没有试图让每个平台行为完全相同,而是让它们共享同一个更高层的开发过程。