diff --git a/.kiro/hooks/ponytail-activate.json b/.kiro/hooks/ponytail-activate.json new file mode 100644 index 00000000..c112d3bd --- /dev/null +++ b/.kiro/hooks/ponytail-activate.json @@ -0,0 +1,14 @@ +{ + "version": "v1", + "_note": "Paths are relative to the project root. These files work when ponytail is cloned as the project. For other projects, use hooks/kiro-hooks.json as a template with absolute paths.", + "hooks": [ + { + "name": "Ponytail session activation", + "trigger": "SessionStart", + "action": { + "type": "command", + "command": "PONYTAIL_HOST=kiro node hooks/ponytail-activate.js" + } + } + ] +} diff --git a/.kiro/hooks/ponytail-mode-tracker.json b/.kiro/hooks/ponytail-mode-tracker.json new file mode 100644 index 00000000..dd4946f5 --- /dev/null +++ b/.kiro/hooks/ponytail-mode-tracker.json @@ -0,0 +1,14 @@ +{ + "version": "v1", + "_note": "Paths are relative to the project root. These files work when ponytail is cloned as the project. For other projects, use hooks/kiro-hooks.json as a template with absolute paths.", + "hooks": [ + { + "name": "Ponytail mode tracker", + "trigger": "UserPromptSubmit", + "action": { + "type": "command", + "command": "PONYTAIL_HOST=kiro node hooks/ponytail-mode-tracker.js" + } + } + ] +} diff --git a/README.md b/README.md index c57811a0..b60bb552 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,23 @@ Qoder auto-loads `AGENTS.md` from the repo root as always-on context, so running For full plugin-tier support (automatic mode activation + ruleset injection on every prompt), add the hooks from [`hooks/qoder-hooks.json`](hooks/qoder-hooks.json) to your `.qoder/settings.json`. Replace `PONYTAIL_DIR` with the path to your ponytail checkout. Qoder's `UserPromptSubmit` hook activates the default mode on first prompt and injects the ruleset every turn; `PreToolUse` with `task|Task` matcher injects the ruleset into subagents. Level switches (`/ponytail lite|full|ultra|off`) work automatically. +### Kiro CLI + +Copy the hook files from this repo into your project or globally: + +```bash +# Per-project (from a ponytail checkout): +cp -r .kiro/hooks/ /path/to/your/project/.kiro/hooks/ + +# Global (all Kiro sessions): +mkdir -p ~/.kiro/hooks +cp .kiro/hooks/ponytail-*.json ~/.kiro/hooks/ +``` + +The hooks reference the `hooks/` scripts relative to the project root, so either clone ponytail into your project or update the paths in the JSON files to point at your checkout (see [`hooks/kiro-hooks.json`](hooks/kiro-hooks.json) for a template with `PONYTAIL_DIR` placeholders). + +This gives hook support: session activation and mode switching (`/ponytail lite|full|ultra|off`). Subagent injection is not supported (Kiro's `PreToolUse` stdout is for permission decisions, not context injection; a future Kiro trigger may enable it). The steering file (`.kiro/steering/ponytail.md`) remains as an instruction-only fallback — copy it to `~/.kiro/steering/` for always-on rules without hooks. Hook commands use inline env var syntax (`VAR=val node ...`), which requires a POSIX shell (Linux/macOS); Windows support depends on Kiro adding per-platform command fields. + ### Antigravity CLI Google is renaming Gemini CLI to Antigravity CLI (the `agy` binary); the same extension installs there: @@ -273,7 +290,7 @@ While active, the ruleset is also injected into every subagent spawned via the A Cursor, Windsurf, Cline, GitHub Copilot Chat (the VS Code, JetBrains, and Visual Studio editor extension, not the standalone Copilot CLI covered under [Install](#install)), Aider, Kiro, Zed, CodeWhale, Swival, Qoder: copy the matching rules file from this repo ([`.cursor/rules/`](.cursor/rules/), [`.windsurf/rules/`](.windsurf/rules/), [`.clinerules/`](.clinerules/), [`.github/copilot-instructions.md`](.github/copilot-instructions.md), [`AGENTS.md`](AGENTS.md), [`.kiro/steering/`](.kiro/steering/), [`.qoder/rules/`](.qoder/rules/)). -Kiro: copy `.kiro/steering/ponytail.md` to `~/.kiro/steering/` (global) or `.kiro/steering/` in your project. +Kiro: copy `.kiro/steering/ponytail.md` to `~/.kiro/steering/` (global) or `.kiro/steering/` in your project. For full hook support (mode switching, subagent injection), see [Kiro CLI](#kiro-cli) under Install. GitHub Copilot CLI fallback (instruction-only mode): it reads `AGENTS.md` and `.github/copilot-instructions.md` in a project, or copy the rules into `~/.copilot/copilot-instructions.md` to run ponytail in every project. This path keeps always-on guidance, but does not add plugin mode switches or hooks. @@ -296,6 +313,7 @@ Which files map to which agent: [Agent portability](docs/agent-portability.md). | Devin CLI | `devin plugins remove ponytail` | | Grok Build | `grok plugin uninstall ponytail` | | Pi agent | `pi uninstall ponytail` | +| Kiro | Delete `.kiro/hooks/ponytail-*.json` and `.kiro/steering/ponytail.md` | | Cursor / Windsurf / Cline / Qoder / etc. | Delete the copied rule file | These remove the plugin's own files. They leave behind a small amount of state ponytail writes outside the plugin folder: the mode flag, `~/.config/ponytail/config.json`, and (if you accepted the setup nudge) a `statusLine` entry in `~/.claude/settings.json`. Run `node scripts/uninstall.js` to clean those up too. **Run it before the host remove command above** — the script is itself a plugin file, so removing the plugin first deletes it (or run it from a separate clone of this repo). It only removes the statusLine entry if it points at ponytail's own script, so a statusline you set up yourself is left untouched. @@ -311,7 +329,7 @@ These remove the plugin's own files. They leave behind a small amount of state p | `/ponytail-gain` | Show the measured impact scoreboard (less code, less cost, more speed) from the benchmark. | | `/ponytail-help` | Quick reference for the commands above. | -Commands need a skill-capable host (Claude Code, Codex, Devin CLI, OpenCode, Gemini, pi, Swival, Hermes Agent, Qoder, Grok Build). In Codex they're skills, invoke with `@` (`@ponytail-review`). The instruction-only adapters (Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity) load the always-on ruleset without the commands. +Commands need a skill-capable host (Claude Code, Codex, Devin CLI, OpenCode, Gemini, pi, Swival, Hermes Agent, Qoder, Grok Build). In Codex they're skills, invoke with `@` (`@ponytail-review`). The instruction-only adapters (Cursor, Windsurf, Cline, Copilot, Antigravity) load the always-on ruleset without the commands. Kiro with hooks supports mode switching (`/ponytail lite|full|ultra|off`) but not the slash-command skills or subagent injection. ## Development diff --git a/docs/agent-portability.md b/docs/agent-portability.md index 08adfcdf..c707357f 100644 --- a/docs/agent-portability.md +++ b/docs/agent-portability.md @@ -27,7 +27,7 @@ to load in a given agent. | JetBrains Junie | `AGENTS.md` | Junie reads `AGENTS.md` once you point it there in Settings → Tools → Junie → Project Settings → Guidelines Path (not automatic yet); this repo ships `AGENTS.md`, and `.junie/guidelines.md` is Junie's legacy path. Instruction-tier. | | Amp (Sourcegraph) | `AGENTS.md` | Amp reads `AGENTS.md` from the working directory and parent directories up to `$HOME` (plus global config like `~/.config/amp/AGENTS.md`); falls back to `AGENT.md`/`CLAUDE.md`. Instruction-tier. | | Jules (Google) | `AGENTS.md` | Jules automatically reads `AGENTS.md` from the repository root. Instruction-tier. | -| Kiro | `.kiro/steering/ponytail.md` | Steering rule; copy globally or into a project. | +| Kiro | `.kiro/steering/ponytail.md`, `.kiro/hooks/ponytail-*.json`, `hooks/kiro-hooks.json`, `hooks/` | Instruction-only (steering file) or hook support (v2 hooks for session activation and mode tracking). Copy `.kiro/hooks/` into your project or `~/.kiro/hooks/` for global activation. Hooks wire to the shared `hooks/` scripts via `PONYTAIL_HOST=kiro`. Subagent injection not supported (Kiro's PreToolUse is for permission decisions only). | | Qoder | `.qoder/rules/ponytail.md`, `.qoder-plugin/plugin.json`, `hooks/qoder-hooks.json`, `skills/`, `AGENTS.md` | Qoder auto-loads `AGENTS.md` as always-on context; `.qoder/rules/ponytail.md` provides per-project rules; the plugin manifest points at `skills/` for the six ponytail skills (invoked as `/ponytail`, `/ponytail-review`, etc. via the Skill system). Full plugin-tier: `hooks/qoder-hooks.json` template registers `UserPromptSubmit` (mode activation + ruleset injection) and `PreToolUse` with `task|Task` matcher (subagent injection). Instruction-tier works from repo root with zero setup via `AGENTS.md`. | | Zed | `AGENTS.md` | Auto-includes `AGENTS.md` from the worktree root as one of its default rule files for the Agent Panel. Instruction-tier. | | Generic agents | `AGENTS.md` or `skills/*/SKILL.md` | Copy the compact rule file or load the skill files directly. | diff --git a/hooks/kiro-hooks.json b/hooks/kiro-hooks.json new file mode 100644 index 00000000..3230010c --- /dev/null +++ b/hooks/kiro-hooks.json @@ -0,0 +1,22 @@ +{ + "_comment": "Reference template — copy the individual hook entries into your project's .kiro/hooks/ or ~/.kiro/hooks/ for global activation. Replace PONYTAIL_DIR with the absolute path to your ponytail checkout. Subagent injection is not supported: Kiro's PreToolUse stdout is for permission decisions, not context injection.", + "version": "v1", + "hooks": [ + { + "name": "Ponytail session activation", + "trigger": "SessionStart", + "action": { + "type": "command", + "command": "PONYTAIL_HOST=kiro node PONYTAIL_DIR/hooks/ponytail-activate.js" + } + }, + { + "name": "Ponytail mode tracker", + "trigger": "UserPromptSubmit", + "action": { + "type": "command", + "command": "PONYTAIL_HOST=kiro node PONYTAIL_DIR/hooks/ponytail-mode-tracker.js" + } + } + ] +} diff --git a/hooks/ponytail-activate.js b/hooks/ponytail-activate.js index d54fbe4a..3b17cab8 100644 --- a/hooks/ponytail-activate.js +++ b/hooks/ponytail-activate.js @@ -14,6 +14,7 @@ const { clearMode, isCodex, isCopilot, + isKiro, setMode, writeHookOutput, } = require('./ponytail-runtime'); @@ -26,7 +27,7 @@ const mode = getDefaultMode(); // "off" mode — skip activation entirely, don't write flag or emit rules if (mode === 'off') { clearMode(); - const hookOutput = (isCodex || isCopilot) ? '' : 'OK'; + const hookOutput = (isCodex || isCopilot || isKiro) ? '' : 'OK'; writeHookOutput('SessionStart', 'off', hookOutput); process.exit(0); } @@ -42,7 +43,7 @@ try { let output = getPonytailInstructions(mode); // 3. Detect missing statusline config — nudge Claude to help set it up -if (!isCodex && !isCopilot) try { +if (!isCodex && !isCopilot && !isKiro) try { let hasStatusline = false; if (fs.existsSync(settingsPath)) { // Strip UTF-8 BOM some editors prepend on Windows (breaks JSON.parse) diff --git a/hooks/ponytail-runtime.js b/hooks/ponytail-runtime.js index db0c4776..3bfeb9be 100644 --- a/hooks/ponytail-runtime.js +++ b/hooks/ponytail-runtime.js @@ -20,6 +20,8 @@ const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA) || isVsCodeCopilotRoot(process.env.CLAUDE_PLUGIN_ROOT); const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA); const isQoder = !isCopilot && !isCodex && Boolean(process.env.QODER_SESSION_ID); +const isKiro = !isCopilot && !isCodex && !isQoder && + (process.env.PONYTAIL_HOST || '').toLowerCase() === 'kiro'; let stateDir = getClaudeDir(); if (isCodex) stateDir = process.env.PLUGIN_DATA; @@ -27,6 +29,8 @@ if (isCodex) stateDir = process.env.PLUGIN_DATA; // getClaudeDir() rather than building a path from undefined. if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA || getClaudeDir(); if (isQoder) stateDir = path.join(os.homedir(), '.qoder'); +// Kiro: state lives under KIRO_HOME (documented profile redirect) or ~/.kiro. +if (isKiro) stateDir = process.env.KIRO_HOME || path.join(os.homedir(), '.kiro'); const statePath = path.join(stateDir, STATE_FILE); @@ -79,6 +83,12 @@ function writeHookOutput(event, mode, context = '') { process.stdout.write(JSON.stringify(output)); return; } + // Kiro: raw stdout is forwarded as context for SessionStart and + // UserPromptSubmit hooks (exit 0). Same semantics as native Claude Code. + if (isKiro) { + process.stdout.write(context); + return; + } // Native Claude: SessionStart accepts raw stdout, but SubagentStart needs the // hookSpecificOutput JSON form or the context is dropped. if (event === 'SubagentStart') { @@ -93,6 +103,7 @@ module.exports = { clearMode, isCodex, isCopilot, + isKiro, isQoder, readMode, setMode, diff --git a/tests/hooks.test.js b/tests/hooks.test.js index 44ee9222..cb13c628 100644 --- a/tests/hooks.test.js +++ b/tests/hooks.test.js @@ -489,4 +489,71 @@ try { if (prevEnvModeRev === undefined) delete process.env.PONYTAIL_DEFAULT_MODE; else process.env.PONYTAIL_DEFAULT_MODE = prevEnvModeRev; } +// --- Kiro host support --- +// isKiro detection via PONYTAIL_HOST=kiro, state at ~/.kiro/, raw stdout output. +{ + const kiroHome = path.join(temp, 'kiro-home'); + fs.mkdirSync(path.join(kiroHome, '.kiro'), { recursive: true }); + const kiroEnv = { + HOME: kiroHome, + USERPROFILE: kiroHome, + PONYTAIL_HOST: 'kiro', + PONYTAIL_DEFAULT_MODE: 'full', + }; + const kiroState = path.join(kiroHome, '.kiro', '.ponytail-active'); + + // SessionStart: activates mode, writes raw stdout (not JSON) + let r = run('ponytail-activate.js', kiroEnv); + assert.equal(r.status, 0, 'kiro activate exit: ' + r.stderr); + assert.equal(fs.readFileSync(kiroState, 'utf8'), 'full'); + assert.match(r.stdout, /PONYTAIL MODE ACTIVE — level: full/); + // Raw text, not JSON + assert.throws(() => JSON.parse(r.stdout), 'Kiro output must be raw text, not JSON'); + + // Mode switch via UserPromptSubmit + r = run('ponytail-mode-tracker.js', kiroEnv, JSON.stringify({ prompt: '/ponytail ultra' })); + assert.equal(r.status, 0, 'kiro mode switch exit: ' + r.stderr); + assert.equal(fs.readFileSync(kiroState, 'utf8'), 'ultra'); + assert.match(r.stdout, /PONYTAIL MODE CHANGED — level: ultra/); + assert.throws(() => JSON.parse(r.stdout), 'Kiro mode output must be raw text, not JSON'); + + // Mode off clears state and emits PONYTAIL MODE OFF + r = run('ponytail-mode-tracker.js', kiroEnv, JSON.stringify({ prompt: '/ponytail off' })); + assert.equal(r.status, 0, 'kiro off exit: ' + r.stderr); + assert.equal(fs.existsSync(kiroState), false); + assert.match(r.stdout, /PONYTAIL MODE OFF/); + + // "off" activation emits empty string (not 'OK') + const kiroOffEnv = { ...kiroEnv, PONYTAIL_DEFAULT_MODE: 'off' }; + r = run('ponytail-activate.js', kiroOffEnv); + assert.equal(r.status, 0, 'kiro off activate: ' + r.stderr); + assert.equal(r.stdout, '', 'Kiro off mode must emit empty stdout'); + + // KIRO_HOME override for state dir + const kiroHomeOverride = path.join(temp, 'kiro-custom'); + fs.mkdirSync(kiroHomeOverride, { recursive: true }); + const kiroCustomEnv = { ...kiroEnv, KIRO_HOME: kiroHomeOverride, PONYTAIL_DEFAULT_MODE: 'lite' }; + r = run('ponytail-activate.js', kiroCustomEnv); + assert.equal(r.status, 0, 'kiro KIRO_HOME activate: ' + r.stderr); + assert.equal(fs.readFileSync(path.join(kiroHomeOverride, '.ponytail-active'), 'utf8'), 'lite'); + + // isKiro is false when PONYTAIL_HOST is not set (falls through to native Claude) + const notKiroEnv = { HOME: kiroHome, USERPROFILE: kiroHome, PONYTAIL_DEFAULT_MODE: 'full' }; + r = run('ponytail-activate.js', notKiroEnv); + assert.equal(r.status, 0, 'non-kiro activate: ' + r.stderr); + // State goes to ~/.claude, not ~/.kiro + assert.equal( + fs.readFileSync(path.join(kiroHome, '.claude', '.ponytail-active'), 'utf8'), + 'full', + ); + + // Host-collision: PONYTAIL_HOST=kiro + PLUGIN_DATA set → isCodex wins (Codex takes precedence) + const collisionEnv = { ...kiroEnv, PLUGIN_DATA: pluginData }; + r = run('ponytail-activate.js', collisionEnv); + assert.equal(r.status, 0, 'collision exit: ' + r.stderr); + let collisionOutput; + try { collisionOutput = JSON.parse(r.stdout); } catch (e) { collisionOutput = null; } + assert.notEqual(collisionOutput, null, 'Codex must win over PONYTAIL_HOST=kiro; output must be JSON'); +} + console.log('hook compatibility checks passed');