Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .pi/extensions/superpowers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

const EXTREMELY_IMPORTANT_MARKER = "<EXTREMELY_IMPORTANT>";
const BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for pi";

const extensionDir = dirname(fileURLToPath(import.meta.url));
const packageRoot = resolve(extensionDir, "../..");
const skillsDir = resolve(packageRoot, "skills");
const bootstrapSkillPath = resolve(skillsDir, "using-superpowers", "SKILL.md");

let cachedBootstrap: string | null | undefined;

export default function superpowersPiExtension(pi: ExtensionAPI) {
let injectBootstrap = true;

pi.on("resources_discover", async () => ({
skillPaths: [skillsDir],
}));

pi.on("session_start", async () => {
injectBootstrap = true;
});

pi.on("session_compact", async () => {
injectBootstrap = true;
});

pi.on("agent_end", async () => {
injectBootstrap = false;
});

pi.on("context", async (event) => {
if (!injectBootstrap) return;
if (event.messages.some(messageContainsBootstrap)) return;

const bootstrap = getBootstrapContent();
if (!bootstrap) return;

const bootstrapMessage = {
role: "user" as const,
content: [{ type: "text" as const, text: bootstrap }],
timestamp: Date.now(),
};

const insertAt = firstNonCompactionSummaryIndex(event.messages);
return {
messages: [
...event.messages.slice(0, insertAt),
bootstrapMessage,
...event.messages.slice(insertAt),
],
};
});
}

function getBootstrapContent(): string | null {
if (cachedBootstrap !== undefined) return cachedBootstrap;

try {
const skillContent = readFileSync(bootstrapSkillPath, "utf8");
const body = stripFrontmatter(skillContent);
cachedBootstrap = `${EXTREMELY_IMPORTANT_MARKER}
${BOOTSTRAP_MARKER}

You have superpowers.

The using-superpowers skill content is included below and is already loaded for this Pi session. Follow it now. Do not try to load using-superpowers again.

${body}

${piToolMapping()}
</EXTREMELY_IMPORTANT>`;
return cachedBootstrap;
} catch {
cachedBootstrap = null;
return null;
}
}

function stripFrontmatter(content: string): string {
const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
return (match ? match[1] : content).trim();
}

function piToolMapping(): string {
return `## Pi tool mapping

Pi has native skills but does not expose Claude Code's \`Skill\` tool. When a Superpowers instruction says to invoke a skill, use Pi's native skill system instead: load the relevant \`SKILL.md\` with \`read\` when the skill applies, or let a human invoke \`/skill:name\` explicitly.

Pi's built-in coding tools are lowercase: \`read\`, \`write\`, \`edit\`, \`bash\`, plus optional \`grep\`, \`find\`, and \`ls\`. Use those for the corresponding actions: read a file, create or edit files, run shell commands, search file contents, find files by name, and list directories.

Pi does not ship a standard subagent tool. If a subagent tool such as \`subagent\` from \`pi-subagents\` is available, use it for Superpowers subagent workflows. If no subagent tool is available, do the work in this session or explain the missing capability instead of inventing \`Task\` calls.

Pi does not ship a standard task-list tool. If an installed todo/task tool is available, use it. Otherwise track work in plan files or a repo-local \`TODO.md\` when task tracking is needed. Treat older \`TodoWrite\` references as this task-tracking action.`;
}

function messageContainsBootstrap(message: unknown): boolean {
const content = (message as { content?: unknown }).content;
if (typeof content === "string") return content.includes(BOOTSTRAP_MARKER);
if (!Array.isArray(content)) return false;
return content.some((part) => {
return (
part &&
typeof part === "object" &&
(part as { type?: unknown }).type === "text" &&
typeof (part as { text?: unknown }).text === "string" &&
(part as { text: string }).text.includes(BOOTSTRAP_MARKER)
);
});
}

function firstNonCompactionSummaryIndex(messages: unknown[]): number {
let index = 0;
while ((messages[index] as { role?: unknown } | undefined)?.role === "compactionSummary") {
index += 1;
}
return index;
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ cp -r superpowers-zh/skills /your/project/.qoder/skills # Qoder(阿里 AI
| Claw Code | `.claw/skills/*/SKILL.md` | Rust 版 CLI agent,兼容 Claude Code 的 SKILL.md 格式 |
| Qoder | `.qoder/skills/*/SKILL.md` + `.qoder/rules/superpowers-zh.md` | 阿里 AI IDE,自动生成 `trigger: always_on` 的 bootstrap rule |

> **详细安装指南**:[Kiro](docs/README.kiro.md) · [DeerFlow](docs/README.deerflow.md) · [Trae](docs/README.trae.md) · [Antigravity](docs/README.antigravity.md) · [VS Code](docs/README.vscode.md) · [Codex](docs/README.codex.md) · [OpenCode](docs/README.opencode.md) · [OpenClaw](docs/README.openclaw.md) · [Windsurf](docs/README.windsurf.md) · [Gemini CLI](docs/README.gemini-cli.md) · [Aider](docs/README.aider.md) · [Qwen Code](docs/README.qwen.md) · [Hermes Agent](docs/README.hermes.md) · [Qoder](docs/README.qoder.md) · [Kimi Code](docs/README.kimi.md)
> **详细安装指南**:[Kiro](docs/README.kiro.md) · [DeerFlow](docs/README.deerflow.md) · [Trae](docs/README.trae.md) · [Antigravity](docs/README.antigravity.md) · [VS Code](docs/README.vscode.md) · [Codex](docs/README.codex.md) · [OpenCode](docs/README.opencode.md) · [OpenClaw](docs/README.openclaw.md) · [Windsurf](docs/README.windsurf.md) · [Gemini CLI](docs/README.gemini-cli.md) · [Aider](docs/README.aider.md) · [Qwen Code](docs/README.qwen.md) · [Hermes Agent](docs/README.hermes.md) · [Qoder](docs/README.qoder.md) · [Kimi Code](docs/README.kimi.md) · [Pi](docs/README.pi.md)

### 卸载 / 误装清理(v1.2.1+)

Expand Down
54 changes: 54 additions & 0 deletions docs/README.pi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Superpowers 中文版 · Pi 指南

在 [Pi](https://github.com/earendil-works/pi)(oh-my-pi)上使用 superpowers-zh 的完整说明。

## 安装

superpowers-zh 通过 Pi 的扩展机制集成,**直接指向仓库现有的 `skills/` 目录**——不复制 skill、不建 symlink、无额外运行时依赖。

集成由 `package.json` 里的 `pi` 字段声明:

```json
"pi": {
"skills": ["./skills"],
"extensions": ["./.pi/extensions/superpowers.ts"]
}
```

并带 `pi-package` keyword,便于 Pi 发现这是一个 Pi 包。

按 Pi 的包安装方式安装 `superpowers-zh`(参考 Pi 文档的包管理命令),Pi 会读取上述 `pi` 配置,挂载 `skills/` 并加载 `.pi/extensions/superpowers.ts` 扩展。

## 工作原理

`.pi/extensions/superpowers.ts` 注册了 Pi 的生命周期钩子:

1. **`resources_discover`** — 把仓库的 `skills/` 目录贡献给 Pi 的技能系统;
2. **`session_start` / `session_compact`** — 标记需要重新注入 bootstrap;
3. **`context`** — 在会话上下文中注入 `using-superpowers` 的内容(去除 frontmatter)+ Pi 工具映射,作为「You have superpowers」bootstrap,让 skill 在恰当时机被遵循;
4. **`agent_end`** — 一轮结束后停止重复注入。

注入带有唯一标记,已存在时不会重复注入;并且会插入到 compaction summary 之后,避免被压缩流程吞掉。

## 工具映射

Pi 有原生技能系统,但**不暴露** `Skill` 工具。skill 内容描述「动作」,在 Pi 上对应到小写工具:

- 「调用某个 skill」→ Pi 原生技能:用 `read` 加载对应 `SKILL.md`,或由人类显式 `/skill:name`
- 「读/写/改文件」→ `read` / `write` / `edit`
- 「跑 shell 命令」→ `bash`
- 「搜索文件内容」→ `grep`,「按名找文件」→ `find`,「列目录」→ `ls`
- 「分派子智能体」→ 若装了 `pi-subagents` 的 `subagent` 工具则用之;没有则在本会话内完成或说明能力缺失,**不要**臆造 `Task` 调用
- 「待办清单」→ 若装了 todo/task 工具则用之;否则用 plan 文件或仓库内 `TODO.md` 跟踪;旧的 `TodoWrite` 引用按此处理

完整映射见 [`skills/using-superpowers/references/pi-tools.md`](../skills/using-superpowers/references/pi-tools.md),扩展也会把同样的映射注入会话。

## 验证

```bash
bash tests/pi/run-tests.sh
```

该测试动态加载扩展并校验:声明了 `pi` 包配置、注册了正确的生命周期钩子(且无 pre-compaction 注入)、`resources_discover` 贡献了 `skills/` 目录、`session_start` 注入了「You have superpowers」+「Pi tool mapping」、pi-tools 参考文档存在。

> 注:扩展是 TypeScript(仅 `import type`,运行时无类型依赖)。Node 22.6–23.5 需 `--experimental-strip-types`(run-tests.sh 已带),23.6+ 默认支持。
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
".codex-plugin/",
".opencode/INSTALL.md",
".opencode/plugins/",
".pi/extensions/",
"CLAUDE.md",
"GEMINI.md",
"RELEASE-NOTES.md",
Expand Down Expand Up @@ -62,8 +63,17 @@
"中文",
"tdd",
"debugging",
"code-review"
"code-review",
"pi-package"
],
"pi": {
"skills": [
"./skills"
],
"extensions": [
"./.pi/extensions/superpowers.ts"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/jnMetaCode/superpowers-zh.git"
Expand Down
28 changes: 28 additions & 0 deletions skills/using-superpowers/references/pi-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Pi Tool Mapping

Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On Pi these resolve to the tools below.

| Action skills request | Pi equivalent |
| --- | --- |
| Invoke a skill | Pi native skills: load the relevant `SKILL.md` with `read`, or let the human use `/skill:name` |
| Read a file | `read` |
| Create a file | `write` |
| Edit a file | `edit` |
| Run a shell command | `bash` |
| Search file contents | `grep` when active; otherwise `bash` with `rg`/`grep` |
| Find files by name | `find` or `bash` with shell globs |
| List files and subdirectories | `ls` when active; otherwise `bash` with `ls` |
| Dispatch a subagent (`Subagent (general-purpose):` template) | Use an installed subagent tool such as `subagent` from `pi-subagents` if available |
| Task tracking ("create a todo", "mark complete") | Use an installed todo/task tool if available, otherwise track tasks in the plan or `TODO.md` |

## Skills

Pi discovers skills from configured skill directories and installed Pi packages. A Superpowers Pi package should expose `skills/` through its `pi.skills` manifest entry. Pi does not expose Claude Code's `Skill` tool, but the agent should still follow the Superpowers rule: when a skill applies, load and follow it before responding.

## Subagents

Pi core does not ship a standard subagent tool. The `pi-subagents` package is a strong optional companion and provides a `subagent` tool with single-agent, chain, parallel, async, forked-context, and resume/status workflows. If no subagent tool is available, do not fabricate `Task` calls; execute sequentially in the current session or explain that the optional subagent capability is not installed.

## Task lists

Pi core does not ship a standard task-list tool. If a todo/task extension is installed, use its documented tool. Otherwise use Superpowers plan files, checklists in Markdown, or a repo-local `TODO.md` for task tracking. Older Superpowers docs may refer to `TodoWrite`; treat that as the task-tracking action above.
8 changes: 8 additions & 0 deletions tests/pi/run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# 扩展是 TypeScript(仅 `import type`,运行时无类型依赖)。Node 22.6–23.5
# 需要 --experimental-strip-types 才能 import .ts;23.6+ 默认开启,该 flag 仍兼容。
node --experimental-strip-types "$SCRIPT_DIR/test-pi-extension.mjs"
128 changes: 128 additions & 0 deletions tests/pi/test-pi-extension.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import test from 'node:test';

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '../..');
const packageJsonPath = resolve(repoRoot, 'package.json');
const extensionPath = resolve(repoRoot, '.pi/extensions/superpowers.ts');
const piToolsPath = resolve(repoRoot, 'skills/using-superpowers/references/pi-tools.md');

async function readPackageJson() {
return JSON.parse(await readFile(packageJsonPath, 'utf8'));
}

async function loadExtension() {
const handlers = new Map();
const pi = {
on(event, handler) {
if (!handlers.has(event)) handlers.set(event, []);
handlers.get(event).push(handler);
},
};
const mod = await import(pathToFileURL(extensionPath).href + `?cachebust=${Date.now()}-${Math.random()}`);
mod.default(pi);
return { handlers };
}

function firstHandler(handlers, event) {
const eventHandlers = handlers.get(event) ?? [];
assert.equal(eventHandlers.length, 1, `expected one ${event} handler`);
return eventHandlers[0];
}

function textOf(message) {
if (typeof message.content === 'string') return message.content;
return message.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('\n');
}

test('package.json declares a pi package with skills and extension resources', async () => {
const pkg = await readPackageJson();

assert.equal(pkg.name, 'superpowers-zh');
assert.ok(pkg.keywords.includes('pi-package'));
assert.deepEqual(pkg.pi.skills, ['./skills']);
assert.deepEqual(pkg.pi.extensions, ['./.pi/extensions/superpowers.ts']);
});

test('extension registers lifecycle hooks without pre-compaction injection', async () => {
const { handlers } = await loadExtension();

for (const event of ['resources_discover', 'session_start', 'session_compact', 'context', 'agent_end']) {
assert.equal((handlers.get(event) ?? []).length, 1, `missing ${event} handler`);
}
assert.equal((handlers.get('session_before_compact') ?? []).length, 0);
});

test('resources_discover contributes the bundled skills directory', async () => {
const { handlers } = await loadExtension();
const discover = firstHandler(handlers, 'resources_discover');

const result = await discover({ type: 'resources_discover', cwd: repoRoot, reason: 'startup' }, {});

assert.deepEqual(result.skillPaths, [resolve(repoRoot, 'skills')]);
});

test('startup context injects the bootstrap as one user message until agent_end', async () => {
const { handlers } = await loadExtension();
const sessionStart = firstHandler(handlers, 'session_start');
const context = firstHandler(handlers, 'context');
const agentEnd = firstHandler(handlers, 'agent_end');

await sessionStart({ type: 'session_start', reason: 'startup' }, {});

const originalMessages = [
{ role: 'user', content: [{ type: 'text', text: 'Let us make a react todo list' }], timestamp: 1 },
];
const result = await context({ type: 'context', messages: originalMessages }, {});

assert.equal(result.messages.length, 2);
assert.equal(result.messages[0].role, 'user');
assert.match(textOf(result.messages[0]), /You have superpowers/);
assert.match(textOf(result.messages[0]), /Pi tool mapping/);
assert.equal(result.messages[1], originalMessages[0]);

const repeatedProviderRequest = await context({ type: 'context', messages: originalMessages }, {});
assert.equal(repeatedProviderRequest.messages.length, 2);
assert.match(textOf(repeatedProviderRequest.messages[0]), /You have superpowers/);

const alreadyInjected = await context({ type: 'context', messages: result.messages }, {});
assert.equal(alreadyInjected, undefined, 'bootstrap should not duplicate when already present');

await agentEnd({ type: 'agent_end', messages: [] }, {});
const afterEnd = await context({ type: 'context', messages: originalMessages }, {});
assert.equal(afterEnd, undefined, 'startup bootstrap should clear after agent_end');
});

test('session_compact injects bootstrap after compaction summaries, not before compaction', async () => {
const { handlers } = await loadExtension();
const sessionCompact = firstHandler(handlers, 'session_compact');
const context = firstHandler(handlers, 'context');

await sessionCompact({ type: 'session_compact', compactionEntry: {}, fromExtension: false }, {});

const summary = { role: 'compactionSummary', summary: 'Prior work summary', tokensBefore: 123, timestamp: 1 };
const user = { role: 'user', content: [{ type: 'text', text: 'Continue' }], timestamp: 2 };
const result = await context({ type: 'context', messages: [summary, user] }, {});

assert.equal(result.messages.length, 3);
assert.equal(result.messages[0], summary);
assert.equal(result.messages[1].role, 'user');
assert.match(textOf(result.messages[1]), /You have superpowers/);
assert.equal(result.messages[2], user);
});

test('pi tools reference documents pi-specific mappings', async () => {
assert.equal(existsSync(piToolsPath), true, 'pi-tools.md should exist');
const text = await readFile(piToolsPath, 'utf8');

for (const expected of ['Skill', 'Task', 'TodoWrite', 'read', 'write', 'edit', 'bash']) {
assert.match(text, new RegExp(expected));
}
});
Loading