Agent 方法论框架 superpowers 移植:难点不在复制文件

2026-07-29

本文基于 superpowers 仓库 commit 44c9b2d(2026-07-27)梳理,该项目仍在持续迭代,具体行为以仓库 https://github.com/obra/superpowers 最新代码与文档为准。

把 superpowers 搬到一个新的编码工具上,最不值钱的一步就是把 skills/ 目录复制过去。真正决定这次移植是成是败的,是两件事:会话一开始能不能在没有人手动操作的前提下把引导内容塞进模型上下文,以及技能里写的「派发一个子代理」在这个工具里到底叫什么名字。 官方那份 docs/porting-to-a-new-harness.md 用了大半篇幅在讲这两件事,剩下的篇幅在讲怎么证明它真的生效了。

这个仓库把承载 Agent 的运行环境统称为 harness——IDE、CLI、Agent runner 都算。它自己已经接了 Claude Code、Codex、Cursor、Copilot CLI、Gemini CLI、Kimi Code、OpenCode、pi、Antigravity 这一串。多接一个,看起来像是复制粘贴改改名,实际上文档开头就写明:新增 harness 是这个仓库里风险最高的一类贡献。

站内的 Agent 框架横向对比Cursor 与 Claude Code 的取舍 讲的是跨框架选型的通用方法论——按什么维度比、按什么标准选。本篇不做选型,只拆一个具体开源项目:当你已经有一套写好的方法论内容,要让它在另一个工具里同样自动生效,工程上到底得干哪些活。

一、先分清哪部分是共用的,哪部分是每个工具都得重写的

superpowers 的内容在所有 harness 上是同一份。变的只是那层把内容送到模型面前、并把指令翻译成宿主原生工具的薄壳。文档把它拆成三块:技能正文(与 harness 无关)、工具映射(每个 harness 一份)、引导注入(每个 harness 一份)。

其中最反直觉的一句是:引导注入就是整个集成本身。没有它,技能文件躺在磁盘上是死的——存在,但永远不会被调用。

技能正文之所以能一份通吃,是因为它们只描述动作,从不点名具体工具。技能里写的是「调用一个技能」「读一个文件」「派发一个子代理」「建一条待办」,而不是写 Taskread_file。这条约束反过来也约束了移植者:不许为了适配自己的 harness 去改技能正文里的工具名。文档里说得很直白,这类「为了兼容性」的措辞改写会被直接拒。

对着仓库目录,这些部件的位置是这样的:

组成部分它负责什么对应仓库位置你什么时候会碰到它
技能正文描述动作而不点名工具,所有 harness 共用同一份skills/ 下的 14 个目录(brainstormingtest-driven-developmentsystematic-debuggingusing-superpowers 等)基本不碰——移植过程中改它就是走错路
引导注入每次会话开始把 using-superpowers 的内容送进上下文skills/using-superpowers/SKILL.md,加上 hooks/session-start.pi/extensions/superpowers.ts 这类注入器移植的主要工作量都在这里
工具名映射把动作词翻译成宿主真实的工具名skills/using-superpowers/references/ 下的 codex-tools.mdgemini-tools.mdpi-tools.mdantigravity-tools.md模型开始调不存在的工具、或者凭空编工具名时
宿主清单让宿主认出这是插件/扩展,并找到 skills 目录.claude-plugin/.cursor-plugin/.codex-plugin/.kimi-plugin/gemini-extension.json、仓库根 package.json第一次装不上,或者装上了但技能列表是空的
跨平台包装让同一个 hook 脚本在 Windows 和 Unix 上都能跑hooks/run-hook.cmd只有走 shell hook 形态、且要支持 Windows 时
版本登记让各家清单的 version 字段跟着一起升.version-bump.json新增了一份带版本号的清单文件时
集成测试断言 hook 输出的 JSON 形状、扩展的注入与去重是否正确tests/hooks/tests/pi/tests/opencode/提 PR 之前

看完这张表你大概能感觉到,工作量分布跟直觉相反:内容是现成的,力气全花在「怎么让它每次都出现」和「怎么让它说对工具名」上。

二、动手之前先做能力体检,缺一条就该停

文档 Part 2 给的是一张准入清单,第一条不满足就直接停手。

硬性要求只有一条:这个 harness 必须允许你在每次会话开始时自动注入文本,且不需要使用者每次手动做点什么。形式不限——可以是会话开始时跑一条 shell 命令并读它 stdout 的 hook 系统,可以是能改写消息数组的进程内插件回调,也可以是「你安装的扩展自带并声明的上下文文件」被宿主固定加载。

反面情形写得很清楚:如果唯一的办法是让人每次会话粘一段提示词、跑一条命令、或者手动开某个模式,那这个 harness 就没法被正经支持。文档管这叫「最常见的假移植」。

剩下的能力分成两类。必须有的:读写编辑文件、跑 shell 命令。可降级的:子代理派发、待办跟踪、网页抓取与搜索。「可降级」的含义很具体——技能正文里已经写好了缺这个工具时的退路,你在工具映射里要做的是有就指向真工具,没有就复用那段退路措辞。比如子代理不可用时,技能会让模型在当前会话里串行做完或者直接说明能力缺失,而不是去编一个 Task 调用出来。

还有一条很省事的判断:有些「新 harness」其实只是换了个安装器的既有集成。文档举了 Factory 的 Droid,它用自己的 plugin install 命令直接消费 Claude Code 的插件,仓库里一个新文件都不用加。这种情况下,一次移植的最终产物就是 README 里的一段话——文档明确说这是个完全合格的结果。这个判断值得先做,能省掉一整轮返工。

三、三种集成形态,区别在于引导内容怎么到模型面前

Part 4 按「引导怎么送达」把集成分成三种形态,选错了后面全错。

Shape A 是 shell hook。 宿主在会话开始跑一条命令,读它 stdout 的 JSON。仓库里 hooks/session-start 这个脚本读 SKILL.md、加上前言、转义,然后按宿主的 JSON 形状打印。坑在于字段名和嵌套层级每家都不一样:Cursor 认 additional_context,Claude Code 认 hookSpecificOutput.additionalContext,Copilot CLI 那条走 additionalContext。脚本靠环境变量分支(CURSOR_PLUGIN_ROOTCLAUDE_PLUGIN_ROOTCOPILOT_CLI)。文档专门点了一句:Claude Code 会同时读两个字段而不去重,所以两个都发就是双重注入。

hook 配置文件本身的 schema 也各不相同。对比 hooks/hooks.jsonhooks/hooks-cursor.json 就能看出来——前者用大写 SessionStart、带 matcher: "startup|clear|compact"typeasync,命令里引用 ${CLAUDE_PLUGIN_ROOT};后者只有 version: 1、小写的 sessionStart 和一条相对路径命令,那几个字段一个都没有。匹配串写错的后果是 hook 静默地永远不触发,不报错。

还有一个陷阱:hook 系统存在不等于会话开始事件存在。文档提到有个真实案例,某个 harness 的二进制里能 grep 到 SessionStart 字样,但那只是遥测,实际暴露的事件只有 pre/post-tool 和 stop。所以在押注 Shape A 之前,要确认那个具体事件真的存在、且真的能写进模型上下文。

Shape B 是进程内插件。 宿主加载一个 JS/TS 模块,你在生命周期回调里自己拼字符串、自己改消息数组。.pi/extensions/superpowers.ts 是最完整的参考:它在 resources_discover 事件里返回 skillPaths 注册技能目录,在 context 事件里往消息数组插一条 user 角色的消息。

这里有三件必须照做的事。一是去重守卫——回调可能反复触发,注入前得先检查标记在不在。二是压缩后重注入——pi 的做法是在 session_startsession_compact 时把 injectBootstrap 置真,在 agent_end 时置假,并且把消息插在开头那些压缩摘要消息之后。三是消息对象形状必须自己去查:pi 用的是 { role, content: [{ type, text }], timestamp },OpenCode 那边操作的是 message.info.rolemessage.parts[],两者不兼容,照抄一份字面量过去会静默失败。

另外注入的是 user 消息而不是 system 消息,这是刻意的:system 消息每轮重复会撑 token,而且多条 system 消息会让某些模型出问题。文档把「别好心把它改成 system 消息」单独列进了踩坑清单。

Shape C 是指令文件。 既没有 shell hook 也没有代码插件,宿主只固定加载一个上下文文件。Gemini 那份是最干净的例子:gemini-extension.json 里声明 contextFileName: "GEMINI.md",而 GEMINI.md 全文只有两行 @-include,一行指向 skills/using-superpowers/SKILL.md,一行指向 skills/using-superpowers/references/gemini-tools.md。这里没有注入器,所以不剥 frontmatter、不拼字符串,宿主原样加载。

这个形态最容易踩的是 include 语法未必真的展开。文档警告过:某个从 Gemini 派生的 harness 接受 @./path 写法,但把它当成「模型可以选择去读的提示」,实际发出的是一次文件读取调用。这跟「每次会话必然在上下文里」是两码事。验证办法是塞一个唯一标记进去,开新会话看它在没有工具调用的情况下在不在上下文里;不在就把内容内联进去,别用 include。

贯穿三种形态的还有一条铁律:所有东西都必须通过宿主自己的安装机制交付,不许去改用户的文件。 用户家目录下的 ~/.gemini/config/AGENTS.mdsettings.jsontrustedFolders.json~/.bashrc,一个都不能碰。Gemini 那个上下文文件之所以合规,是因为它随扩展一起装进去、并由清单声明,宿主加载的是扩展自己的文件。如果安装机制实在带不动引导内容,正确做法是把这个限制说出来,而不是去写用户的配置。

关于 hook 机制本身的更多背景,可以对照读 Claude Code hooks 的用法Claude Code 技能机制

四、工具名映射:为什么这一步最容易写错

Step 4 要求映射覆盖一整组动作:读文件、创建/编辑/删除文件、跑 shell、搜内容与找文件、抓 URL 与搜网、派发子代理(含怎么传 agent 类型、以及需不需要开某个配置开关)、建与更新待办、调用技能。

文档在这里给了一条几乎可以直接抄走的实践:工具名要从 harness 本身拿,绝不能编。 如果文档没列全,权威来源就是在活的会话里让模型「一行一个,列出你能调用的每个工具的确切机器名」,然后用它报出来的名字。这个办法看着土,但它避开了移植里最隐蔽的一类 bug——工具名拼错了,模型不会报错,只会绕路或者卡住。

两份现成的映射文件放在一起看,差异一目了然。Gemini CLI 那份是一张长表,动作几乎都能对上一个真实工具名:读文件是 read_file、一次读多个是 read_many_files、写新文件是 write_file、改文件是 replace、跑命令是 run_shell_command、搜内容是 grep_search、按名字找文件是 glob、列目录是 list_directory、抓 URL 是 web_fetch、搜网是 google_web_search、调技能是 activate_skill、待办是 write_todos。子代理走 invoke_agent,传 agent_name: "generalist",同一条回复里发多个 invoke_agent 就是并行派发。

pi 那份短得多,因为它要说的主要是「什么没有」。references/pi-tools.md 里明确写着:pi 核心不带标准的子代理工具,pi-subagents 这个包提供的 subagent 工具是个可选搭档;pi 核心也不带标准的任务列表工具,没有的话就用计划文件、Markdown 清单或者仓库里的 TODO.md。并且补了一句——老文档里提到的 TodoWrite,按「任务跟踪」这个动作理解就行。

两者不是谁比谁强,是暴露面不同:一个内建工具齐全,映射就是一张对照表;另一个把能力交给可选扩展,映射就得连「没装怎么办」一起写清楚。想深入这块,Agent 工具设计的取舍 那篇讲的是同一层问题的另一面。

映射放哪儿也跟形态有关。Shape A 放 references/<harness>-tools.md,不内联进 hook 输出;Shape B 通常内联进注入的字符串,pi 是两边都放——.pi/extensions/superpowers.ts 里有个 piToolMapping() 函数,references/pi-tools.md 里也有一份,文档提醒改的时候两边都得改,否则这次移植只做了一半;Shape C 放 references/ 然后被指令文件拉进去。

还有个特殊分支:宿主根本没有技能调用工具怎么办。using-superpowers/SKILL.md 里那句「永远不要用文件工具手动读技能文件」,本意是「别绕过平台的技能加载机制」,而不是「永远别用文件读取」。对一个没有技能工具的 harness,被认可的机制就是SKILL.md——所以在映射里要把这句话挑明,免得模型觉得自己在违规。piToolMapping() 里那段就是这么写的:pi 有原生技能但不暴露 Skill 工具,技能适用时用 read 去读对应的 SKILL.md

五、边界与代价:这套设计明确不管什么

先说清楚代价,这比列优点有用。

它让开发变慢,而且是设计上的慢。 一句「做个 react 待办列表」会先触发 brainstorming 而不是直接写代码——这正是它的验收标准。对于改个文案、调个常量这类小改动,走一遍需求澄清、写计划、TDD、完成前验证,是明确的过度设计。模型也会变啰嗦:注入的引导内容每次会话都要占上下文,技能正文本身也不短。上下文预算紧张的场景下这是实打实的成本。

它不保证技能一定被触发。 文档在讨论「靠宿主暴露的技能索引来引导」这条软路径时说得很坦白:没有 <EXTREMELY_IMPORTANT> 包裹、没有去重、压缩之后不会重注入,能不能触发取决于模型愿不愿意照着它看到的一段描述去行动。这也是验收测试在这种情况下是强制项的原因——它是唯一的保证,而且要在用户真正会用的那些模型上跑,不能只挑一个能力最好的模型跑一遍就算过。

它不替你解决模型能力问题。 移植做的只是内容送达和词汇翻译。子代理、待办、联网这些能力宿主没有就是没有,技能里能做的只是退化处理。

它不接管安装。 手工拷贝技能文件、手工改用户配置,两条都被禁掉了。唯一被支持的安装动作是跑宿主自己的安装命令。如果一个 harness 压根没有安装命令、唯一的接入面就是用户自己的配置文件,那按文档的说法,它就不满足交付规则,应该把问题提出来,而不是发一个会改用户文件的安装脚本。

它对依赖也划了线。 这是个零依赖插件,新增 harness 是贡献规则里唯一的豁免口,即便如此也只限于集成严格需要的部分——能编译掉的类型导入可以,运行时包不行。文档还提醒了一个具体情形:pi 那个 import type { ExtensionAPI } 之所以能成立,是因为宿主直接跑 .ts、加载时提供该类型、且仓库从不对这个文件做类型检查;换一个真的会做类型检查或打包的宿主,这条路就断了,该去问维护者而不是悄悄加依赖。

六、上手与避坑清单

先搜有没有人试过。 文档要求同时搜开着的和已关闭的 PR。会踩是因为没人搜过就动手,结果重复了一遍别人卡住的地方;已关闭的 PR 里往往写着当初为什么卡住。避法是把这一步放在读代码之前。

别把「有 hook 系统」当成「有会话开始事件」。 会踩是因为看到 hooks.json 就直接照抄 Shape A,甚至在二进制里 grep 到了 SessionStart 字样。避法是先确认那个具体事件真的会在会话开始时触发、并且它的 stdout 真的能进上下文,用一个唯一标记验证过再动手。

别假设 fork 继承了母体的行为。 会踩是因为某个 harness 派生自 Gemini,manifest 字段和 @-include 语法看起来都在。避法是塞唯一标记做实验:标记必须在没有任何工具调用的前提下就在上下文里;只要模型是「读了一下文件」才知道,就说明它没被真正展开,该内联。

JSON 字段名和 matcher 串一个字都不能错。 会踩是因为拿 Claude Code 那份当模板套。避法是注册一个一次性的 hook,把环境变量全 dump 出来、带上标记,先观察宿主到底认哪个变量、怎么吃你的 stdout,再写真分支;如果你的 harness 也设置了前面分支要判断的变量,你的分支要排在它前面。

去重策略要跟回调频率配套。 会踩是因为把一个 harness 的做法搬到另一个上:OpenCode 的 transform 每个 agent step 都跑,pi 的 context 是每轮跑一次。避法是先搞清楚自己这个回调多久触发一次,再决定用逐次检查标记还是用生命周期标志位。顺带把引导内容在模块级缓存起来,别每次回调都重读重解析 SKILL.md

Windows 上 hook 脚本别加 .sh 后缀。 会踩是因为习惯性给 shell 脚本加扩展名。Claude Code 在 Windows 上会给任何含 .sh 的命令前置 bash,结果是双重调用。避法是保持脚本无扩展名(session-start 而不是 session-start.sh),并且不要写按操作系统分叉的多个脚本版本——一个无扩展名的 bash 脚本加上 hooks/run-hook.cmd 这个同时是批处理又是 shell 脚本的包装器,三个平台都能覆盖。

新加的带版本清单要登记。 会踩是因为 .version-bump.json 里没加你那份文件,scripts/bump-version.sh 就不会带上它,发出去的版本号是旧的。避法是先打开那个文件看看现在跟踪了哪些(当前列着 package.json、四份 *-plugin/plugin.jsonmarketplace.jsongemini-extension.json),再决定要不要加一行。如果你的 harness 是搭在已有文件上的(pi 就是靠仓库根 package.json 的字段),那就什么都不用加。

别指望读代码就能确认移植成功。 会踩是因为跳过了真实运行。避法是把宿主装成指向本地工作树、然后在 detached 的 tmux 会话里驱动它:先给足启动时间,先处理首次引导和「信任这个目录吗」这类模态弹窗(这时候发的按键会被当成菜单选择而不是输入),提示词文本和 Enter 要分两次 send-keys 发、中间留一点间隔,而且要循环 capture-pane 轮询而不是只抓一次。冒烟检查是问模型「你有哪些 superpowers」,正式验收是那句 Let's make a react todo list,通过的标准是 brainstorming 在任何代码写出来之前就触发。

收个尾

如果你要把类似的方法论内容移植到自家的工具链上,这份文档最值钱的三条结论是:把内容和送达机制分开,内容一份共用、送达每家重写;把工具词汇当成必须查证的外部事实,宁可去问活着的模型也不要靠命名习惯推断;把「一句自然语言输入能不能在写代码之前触发正确的技能」当成唯一的验收信号,其余都只是过程指标。

接下来该读哪个文件,取决于你落在哪种形态:走 shell hook 就从 hooks/session-starthooks/hooks.jsonhooks/hooks-cursor.json 三份对照着读;走进程内插件就通读 .pi/extensions/superpowers.ts,注意它的四个生命周期事件和插入位置的计算;走指令文件就看 gemini-extension.json 加那两行 GEMINI.md,它短到可以一眼看完。项目采用 MIT 许可证,代码和文档都能直接翻。文档自己有一句话适合当结束语:当指南和代码打架时,以代码为准,然后去修指南。

本文属于 superpowers 方法论专题(共 30 篇,含三篇与其它开源 Agent 项目的对照)。想看把资产铺满的另一种取向,见 ECC 开源 Agent 套件专题

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。