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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ rulesync.local.jsonc
**/.github/hooks/copilotcli-hooks.json
**/.kilo/plugins/rulesync-hooks.js
**/.opencode/plugins/rulesync-hooks.js
**/.pi/extensions/rulesync-hooks.ts
**/.factory/hooks.json
**/.agents/plugins/rulesync/hooks/hooks.json
**/.kiro/agents/default.json
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ The tables below show whether each tool supports a given feature (✅ = supporte
| Devin Desktop | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
| Warp | ✅ | ✅ | ✅ | | | ✅ | | ✅ | |
| Replit | ✅ | | | | | ✅ | | | |
| Pi Coding Agent | ✅ | | | ✅ | | ✅ | | | |
| Pi Coding Agent | ✅ | | | ✅ | | ✅ | | | |
| Zed | ✅ | ✅ | ✅ | | | ✅ | | ✅ | |

<!-- SUPPORTED_TOOLS_AI:END -->
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"dyoshikawa",
"eabi",
"eamodio",
"earendil",
"eastasianwidth",
"elicitations",
"émøjî",
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ This is Rulesync, a Node.js CLI tool that automatically generates configuration

> **JSONC variant:** hooks can also be authored as `.rulesync/hooks.jsonc` (JSON with comments and trailing commas). When both `hooks.json` and `hooks.jsonc` exist, the `.jsonc` file takes precedence.

Hooks run scripts at lifecycle events (e.g. session start, before tool use). Events use **canonical camelCase** in this file, and Rulesync translates them per tool: Cursor uses them as-is; Claude Code, Factory Droid, Codex CLI, Gemini CLI, and Goose get PascalCase (with a few tool-specific name mappings) in their settings files; OpenCode and Kilo hooks are emitted as JavaScript plugins (`.opencode/plugins/rulesync-hooks.js`, `.kilo/plugins/rulesync-hooks.js`); Copilot and Copilot CLI map event names to their own camelCase (e.g. `beforeSubmitPrompt` → `userPromptSubmitted`, `stop` → `agentStop`, `afterError` → `errorOccurred`) and use `powershell`/`bash` command fields — Copilot CLI additionally covers a wider event set and supports `prompt` and `http` hook types beyond `command`; deepagents-cli uses a dot-notation (e.g. `session.start`, `tool.error`); Kiro emits hooks into `.kiro/agents/default.json` using Kiro's CLI event names (`agentSpawn`, `userPromptSubmit`, `preToolUse`, `postToolUse`, `stop`); Qwen Code emits PascalCase events into the `hooks` key of `.qwen/settings.json` (its supported event set differs from Gemini CLI's).
Hooks run scripts at lifecycle events (e.g. session start, before tool use). Events use **canonical camelCase** in this file, and Rulesync translates them per tool: Cursor uses them as-is; Claude Code, Factory Droid, Codex CLI, Gemini CLI, and Goose get PascalCase (with a few tool-specific name mappings) in their settings files; OpenCode and Kilo hooks are emitted as JavaScript plugins (`.opencode/plugins/rulesync-hooks.js`, `.kilo/plugins/rulesync-hooks.js`); Pi Coding Agent hooks are emitted as a Rulesync-owned TypeScript extension (`.pi/extensions/rulesync-hooks.ts`, or `~/.pi/agent/extensions/rulesync-hooks.ts` in global mode) that subscribes to Pi's snake_case extension events (`sessionStart` → `session_start`, `stop` → `agent_end`, `preToolUse` → `tool_call` with the matcher tested as a regex against the tool name) and observes events only — command hooks run but cannot block or mutate Pi events; Copilot and Copilot CLI map event names to their own camelCase (e.g. `beforeSubmitPrompt` → `userPromptSubmitted`, `stop` → `agentStop`, `afterError` → `errorOccurred`) and use `powershell`/`bash` command fields — Copilot CLI additionally covers a wider event set and supports `prompt` and `http` hook types beyond `command`; deepagents-cli uses a dot-notation (e.g. `session.start`, `tool.error`); Kiro emits hooks into `.kiro/agents/default.json` using Kiro's CLI event names (`agentSpawn`, `userPromptSubmit`, `preToolUse`, `postToolUse`, `stop`); Qwen Code emits PascalCase events into the `hooks` key of `.qwen/settings.json` (its supported event set differs from Gemini CLI's).

Example:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Devin Desktop | devin | ✅ 🌏 | ✅ | ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | |
| Warp | warp | ✅ | ✅ | ✅ 🌏 | | | ✅ 🌏 | | 🌏 | |
| Replit | replit | ✅ | | | | | ✅ 🌏 | | | |
| Pi Coding Agent | pi | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | | | |
| Pi Coding Agent | pi | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Zed | zed | ✅ 🌏 | ✅ | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | |

<!-- SUPPORTED_TOOLS_DOCS:END -->
Expand Down
2 changes: 1 addition & 1 deletion skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ This is Rulesync, a Node.js CLI tool that automatically generates configuration

> **JSONC variant:** hooks can also be authored as `.rulesync/hooks.jsonc` (JSON with comments and trailing commas). When both `hooks.json` and `hooks.jsonc` exist, the `.jsonc` file takes precedence.

Hooks run scripts at lifecycle events (e.g. session start, before tool use). Events use **canonical camelCase** in this file, and Rulesync translates them per tool: Cursor uses them as-is; Claude Code, Factory Droid, Codex CLI, Gemini CLI, and Goose get PascalCase (with a few tool-specific name mappings) in their settings files; OpenCode and Kilo hooks are emitted as JavaScript plugins (`.opencode/plugins/rulesync-hooks.js`, `.kilo/plugins/rulesync-hooks.js`); Copilot and Copilot CLI map event names to their own camelCase (e.g. `beforeSubmitPrompt` → `userPromptSubmitted`, `stop` → `agentStop`, `afterError` → `errorOccurred`) and use `powershell`/`bash` command fields — Copilot CLI additionally covers a wider event set and supports `prompt` and `http` hook types beyond `command`; deepagents-cli uses a dot-notation (e.g. `session.start`, `tool.error`); Kiro emits hooks into `.kiro/agents/default.json` using Kiro's CLI event names (`agentSpawn`, `userPromptSubmit`, `preToolUse`, `postToolUse`, `stop`); Qwen Code emits PascalCase events into the `hooks` key of `.qwen/settings.json` (its supported event set differs from Gemini CLI's).
Hooks run scripts at lifecycle events (e.g. session start, before tool use). Events use **canonical camelCase** in this file, and Rulesync translates them per tool: Cursor uses them as-is; Claude Code, Factory Droid, Codex CLI, Gemini CLI, and Goose get PascalCase (with a few tool-specific name mappings) in their settings files; OpenCode and Kilo hooks are emitted as JavaScript plugins (`.opencode/plugins/rulesync-hooks.js`, `.kilo/plugins/rulesync-hooks.js`); Pi Coding Agent hooks are emitted as a Rulesync-owned TypeScript extension (`.pi/extensions/rulesync-hooks.ts`, or `~/.pi/agent/extensions/rulesync-hooks.ts` in global mode) that subscribes to Pi's snake_case extension events (`sessionStart` → `session_start`, `stop` → `agent_end`, `preToolUse` → `tool_call` with the matcher tested as a regex against the tool name) and observes events only — command hooks run but cannot block or mutate Pi events; Copilot and Copilot CLI map event names to their own camelCase (e.g. `beforeSubmitPrompt` → `userPromptSubmitted`, `stop` → `agentStop`, `afterError` → `errorOccurred`) and use `powershell`/`bash` command fields — Copilot CLI additionally covers a wider event set and supports `prompt` and `http` hook types beyond `command`; deepagents-cli uses a dot-notation (e.g. `session.start`, `tool.error`); Kiro emits hooks into `.kiro/agents/default.json` using Kiro's CLI event names (`agentSpawn`, `userPromptSubmit`, `preToolUse`, `postToolUse`, `stop`); Qwen Code emits PascalCase events into the `hooks` key of `.qwen/settings.json` (its supported event set differs from Gemini CLI's).

Example:

Expand Down
2 changes: 1 addition & 1 deletion skills/rulesync/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Devin Desktop | devin | ✅ 🌏 | ✅ | ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | |
| Warp | warp | ✅ | ✅ | ✅ 🌏 | | | ✅ 🌏 | | 🌏 | |
| Replit | replit | ✅ | | | | | ✅ 🌏 | | | |
| Pi Coding Agent | pi | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | | | |
| Pi Coding Agent | pi | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Zed | zed | ✅ 🌏 | ✅ | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | |

<!-- SUPPORTED_TOOLS_DOCS:END -->
Expand Down
3 changes: 3 additions & 0 deletions src/constants/pi-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { join } from "node:path";

export const PI_DIR = ".pi";
const PI_AGENT_DIR = join(PI_DIR, "agent");
export const PI_AGENT_EXTENSIONS_DIR_PATH = join(PI_AGENT_DIR, "extensions");
export const PI_AGENT_PROMPTS_DIR_PATH = join(PI_AGENT_DIR, "prompts");
export const PI_AGENT_SKILLS_DIR_PATH = join(PI_AGENT_DIR, "skills");
export const PI_EXTENSIONS_DIR_PATH = join(PI_DIR, "extensions");
export const PI_PROMPTS_DIR_PATH = join(PI_DIR, "prompts");
export const PI_SKILLS_DIR_PATH = join(PI_DIR, "skills");
export const PI_RULE_FILE_NAME = "AGENTS.md";
export const PI_APPEND_SYSTEM_FILE_NAME = "APPEND_SYSTEM.md";
export const PI_HOOKS_FILE_NAME = "rulesync-hooks.ts";
17 changes: 17 additions & 0 deletions src/e2e/e2e-hooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const hooksGenerateTargets = [
{ target: "cursor", outputPath: join(".cursor", "hooks.json") },
{ target: "opencode", outputPath: join(".opencode", "plugins", "rulesync-hooks.js") },
{ target: "kilo", outputPath: join(".kilo", "plugins", "rulesync-hooks.js") },
{ target: "pi", outputPath: join(".pi", "extensions", "rulesync-hooks.ts") },
{ target: "codexcli", outputPath: join(".codex", "hooks.json") },
{ target: "qwencode", outputPath: join(".qwen", "settings.json") },
{
Expand Down Expand Up @@ -103,6 +104,14 @@ describe("E2E: hooks", () => {
// so assert the canonical command paths survive rather than parsing JSON.
expect(generatedContent).toContain(".rulesync/hooks/session-start.sh");
expect(generatedContent).toContain(".rulesync/hooks/audit.sh");
} else if (target === "pi") {
// Pi emits a TypeScript extension (.pi/extensions/rulesync-hooks.ts)
// that subscribes to snake_case extension events: sessionStart →
// session_start, stop → agent_end.
expect(generatedContent).toContain('pi.on("session_start"');
expect(generatedContent).toContain('pi.on("agent_end"');
expect(generatedContent).toContain(".rulesync/hooks/session-start.sh");
expect(generatedContent).toContain(".rulesync/hooks/audit.sh");
} else {
const parsed = JSON.parse(generatedContent);

Expand Down Expand Up @@ -388,6 +397,7 @@ describe("E2E: hooks", () => {
// factorydroid now writes a dedicated .factory/hooks.json (isDeletable=true).
{ target: "cursor", orphanPath: join(".cursor", "hooks.json") },
{ target: "opencode", orphanPath: join(".opencode", "plugins", "rulesync-hooks.js") },
{ target: "pi", orphanPath: join(".pi", "extensions", "rulesync-hooks.ts") },
{ target: "codexcli", orphanPath: join(".codex", "hooks.json") },
{ target: "copilot", orphanPath: join(".github", "hooks", "copilot-hooks.json") },
{ target: "factorydroid", orphanPath: join(".factory", "hooks.json") },
Expand Down Expand Up @@ -602,6 +612,7 @@ const hooksGlobalTargets = [
},
{ target: "opencode", outputPath: join(".config", "opencode", "plugins", "rulesync-hooks.js") },
{ target: "kilo", outputPath: join(".config", "kilo", "plugins", "rulesync-hooks.js") },
{ target: "pi", outputPath: join(".pi", "agent", "extensions", "rulesync-hooks.ts") },
{ target: "factorydroid", outputPath: join(".factory", "hooks.json") },
{ target: "deepagents", outputPath: join(".deepagents", "hooks.json") },
{ target: "junie", outputPath: join(".junie", "config.json") },
Expand Down Expand Up @@ -667,6 +678,12 @@ describe("E2E: hooks (global mode)", () => {
// Kilo's JS plugin differs from OpenCode's shape; assert command paths.
expect(generatedContent).toContain(".rulesync/hooks/session-start.sh");
expect(generatedContent).toContain(".rulesync/hooks/audit.sh");
} else if (target === "pi") {
// Pi emits a TypeScript extension subscribing to snake_case events.
expect(generatedContent).toContain('pi.on("session_start"');
expect(generatedContent).toContain('pi.on("agent_end"');
expect(generatedContent).toContain(".rulesync/hooks/session-start.sh");
expect(generatedContent).toContain(".rulesync/hooks/audit.sh");
} else if (target === "copilotcli") {
// Copilot CLI does not support the `stop` hook event, so audit.sh is
// intentionally dropped during generation.
Expand Down
6 changes: 4 additions & 2 deletions src/features/hooks/hooks-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ describe("HooksProcessor", () => {
});

describe("getToolTargets", () => {
it("should return cursor, claudecode, copilot, copilotcli, opencode, kilo, factorydroid, and kiro for project mode", () => {
it("should return cursor, claudecode, copilot, copilotcli, opencode, kilo, pi, factorydroid, and kiro for project mode", () => {
const targets = HooksProcessor.getToolTargets({ global: false });
expect(targets).toEqual([
"antigravity-cli",
Expand All @@ -531,6 +531,7 @@ describe("HooksProcessor", () => {
"copilotcli",
"kilo",
"opencode",
"pi",
"factorydroid",
"goose",
"kiro",
Expand All @@ -545,7 +546,7 @@ describe("HooksProcessor", () => {
]);
});

it("should return cursor, claudecode, copilotcli, opencode, kilo, and factorydroid for global mode", () => {
it("should return cursor, claudecode, copilotcli, opencode, kilo, pi, and factorydroid for global mode", () => {
const targets = HooksProcessor.getToolTargets({ global: true });
expect(targets).toEqual([
"antigravity-cli",
Expand All @@ -556,6 +557,7 @@ describe("HooksProcessor", () => {
"copilotcli",
"kilo",
"opencode",
"pi",
"factorydroid",
"goose",
"hermesagent",
Expand Down
23 changes: 23 additions & 0 deletions src/features/hooks/hooks-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
KIRO_HOOK_EVENTS,
KIRO_IDE_HOOK_EVENTS,
OPENCODE_HOOK_EVENTS,
PI_HOOK_EVENTS,
QWENCODE_HOOK_EVENTS,
REASONIX_HOOK_EVENTS,
VIBE_HOOK_EVENTS,
Expand Down Expand Up @@ -52,6 +53,7 @@ import { KiroCliHooks } from "./kiro-cli-hooks.js";
import { KiroHooks } from "./kiro-hooks.js";
import { KiroIdeHooks } from "./kiro-ide-hooks.js";
import { OpencodeHooks } from "./opencode-hooks.js";
import { PiHooks } from "./pi-hooks.js";
import { QwencodeHooks } from "./qwencode-hooks.js";
import { ReasonixHooks } from "./reasonix-hooks.js";
import { RulesyncHooks } from "./rulesync-hooks.js";
Expand Down Expand Up @@ -254,6 +256,27 @@ export const toolHooksFactories = new Map<HooksProcessorToolTarget, ToolHooksFac
supportsMatcher: true,
},
],
[
"pi",
{
class: PiHooks,
meta: {
// Pi discovers extensions from `.pi/extensions/` (project) and
// `~/.pi/agent/extensions/` (global); rulesync generates a
// rulesync-owned TypeScript extension in both scopes. Arbitrary
// extension code cannot be parsed back into canonical hooks, so
// import is unsupported (same as opencode/kilo).
supportsProject: true,
supportsGlobal: true,
supportsImport: false,
},
supportedEvents: PI_HOOK_EVENTS,
supportedHookTypes: ["command"],
// Matchers are evaluated as regexes against `event.toolName` on Pi's
// tool_call / tool_result events.
supportsMatcher: true,
},
],
[
"factorydroid",
{
Expand Down
Loading
Loading