From ed41aa1355cf85517704cac2a3fc8dc32c85039e Mon Sep 17 00:00:00 2001 From: Phoen1xCode Date: Fri, 31 Jul 2026 11:48:33 +0800 Subject: [PATCH 1/2] feat: add Kimi Code plugin adapter kimi.plugin.json at the repo root makes the repo installable via /plugins install: the six skills, the shared .opencode/command/*.md files as /ponytail:, AGENTS.md via systemPromptPath, and the ponytail skill at session start. No hooks: Kimi Code's hook payload differs from the Claude/Codex event shapes the lifecycle hooks emit, so level switches live for the session. --- README.md | 13 +++- docs/agent-portability.md | 1 + kimi.plugin.json | 22 ++++++ scripts/check-versions.js | 5 +- tests/kimi-plugin.test.js | 142 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 kimi.plugin.json create mode 100644 tests/kimi-plugin.test.js diff --git a/README.md b/README.md index c2e9b0e2..2d22971b 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,16 @@ 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. +### Kimi Code + +``` +/plugins install https://github.com/DietrichGebert/ponytail +``` + +Or run `/plugins`, switch to the **Custom** tab, and paste the repo URL. Run `/reload` (or `/new`) after installing. + +The plugin manifest ([`kimi.plugin.json`](kimi.plugin.json)) ships the six skills, registers the commands as `/ponytail:ponytail`, `/ponytail:ponytail-review`, and so on, injects `AGENTS.md` into the system prompt, and session-starts the ponytail skill — always-on with zero setup. Level switches (`/ponytail:ponytail ultra`) last for the session. The adapter declares no hooks: Kimi Code's hook payload differs from the Claude/Codex event shapes ponytail's lifecycle hooks emit, and the system prompt plus session-start skill already cover always-on behavior. + ### Antigravity CLI Google is renaming Gemini CLI to Antigravity CLI (the `agy` binary); the same extension installs there: @@ -275,6 +285,7 @@ Which files map to which agent: [Agent portability](docs/agent-portability.md). | Host | Command | |------|---------| | Claude Code | `/plugin remove ponytail` | +| Kimi Code | `/plugins remove ponytail` | | Codex | `codex plugin remove ponytail` | | Devin CLI | `devin plugins remove ponytail` | | Pi agent | `pi uninstall ponytail` | @@ -293,7 +304,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). 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, Kimi Code). In Codex they're skills, invoke with `@` (`@ponytail-review`). In Kimi Code they're plugin commands, namespaced by plugin id (`/ponytail:ponytail-review`). The instruction-only adapters (Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity) load the always-on ruleset without the commands. ## Development diff --git a/docs/agent-portability.md b/docs/agent-portability.md index 2e15e133..37cccda3 100644 --- a/docs/agent-portability.md +++ b/docs/agent-portability.md @@ -28,6 +28,7 @@ to load in a given agent. | 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. | | 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`. | +| Kimi Code | `kimi.plugin.json`, `skills/`, `.opencode/command/`, `AGENTS.md` | Full plugin-tier install (`/plugins install https://github.com/DietrichGebert/ponytail`): the root manifest reuses the six `skills/`, registers the shared `.opencode/command/*.md` files as `/ponytail:`, injects `AGENTS.md` via `systemPromptPath`, and session-starts the ponytail skill. No hooks — Kimi Code's hook payload differs from the Claude/Codex event shapes ponytail's lifecycle hooks emit, so level switches live for the session. | | 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/kimi.plugin.json b/kimi.plugin.json new file mode 100644 index 00000000..d6722522 --- /dev/null +++ b/kimi.plugin.json @@ -0,0 +1,22 @@ +{ + "name": "ponytail", + "version": "4.8.4", + "description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.", + "keywords": ["yagni", "minimalism", "code-review", "productivity"], + "author": { + "name": "Dietrich Gebert", + "url": "https://github.com/DietrichGebert" + }, + "homepage": "https://github.com/DietrichGebert/ponytail", + "license": "MIT", + "interface": { + "displayName": "Ponytail", + "shortDescription": "Lazy senior dev mode: YAGNI, stdlib first, the least code that works." + }, + "skills": "./skills/", + "commands": "./.opencode/command/", + "systemPromptPath": "./AGENTS.md", + "sessionStart": { + "skill": "ponytail" + } +} diff --git a/scripts/check-versions.js b/scripts/check-versions.js index b2b0d6fc..d63012e3 100644 --- a/scripts/check-versions.js +++ b/scripts/check-versions.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -// Version-consistency guard. Ponytail declares its version in seven files across -// five host ecosystems, and every release bumps all of them by hand. +// Version-consistency guard. Ponytail declares its version in nine files across +// seven host ecosystems, and every release bumps all of them by hand. // // tests/gemini-extension.test.js already checks the four plugin manifests agree // with each other, but that can't catch the failure mode that shipped in v4.8.0: @@ -25,6 +25,7 @@ const VERSION_FILES = [ '.github/plugin/plugin.json', // Copilot plugin '.qoder-plugin/plugin.json', // Qoder plugin 'gemini-extension.json', // Gemini CLI extension + 'kimi.plugin.json', // Kimi Code plugin 'package.json', // pi-package / repo root 'ponytail-mcp/package.json', // MCP server (private, internal-only) ]; diff --git a/tests/kimi-plugin.test.js b/tests/kimi-plugin.test.js new file mode 100644 index 00000000..62ff5912 --- /dev/null +++ b/tests/kimi-plugin.test.js @@ -0,0 +1,142 @@ +#!/usr/bin/env node +// Smoke test for the Kimi Code adapter. The adapter is a single thin manifest +// (kimi.plugin.json, repo root takes precedence over .kimi-plugin/plugin.json) +// that reuses the repo's existing files: AGENTS.md via systemPromptPath for +// always-on rules, skills/ for the six agent skills, sessionStart.skill to +// activate ponytail every session, and the shared Markdown command files in +// .opencode/command/ — already the exact frontmatter + $ARGUMENTS shape Kimi +// Code registers as /ponytail:. Kimi's hook payload differs from the +// Claude/Codex event shapes ponytail's lifecycle hooks emit, so the adapter +// declares no hooks. This test fails if the manifest is removed, drifts off +// the pinned version, points at files that no longer exist, or starts carrying +// fields Kimi Code would flag as diagnostics. +// +// Manifest spec: https://www.kimi.com/code/docs/en/kimi-code-cli/customization/plugins.html + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '..'); +const MANIFEST = 'kimi.plugin.json'; +// Floating refs are a supply-chain footgun; the manifest version must be pinned. +const PINNED_SEMVER = /^\d+\.\d+\.\d+$/; +// The name field doubles as the plugin id (and the /ponytail: prefix). +const PLUGIN_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/; +// Fields the manifest spec documents. Unsupported runtime fields (tools, apps, +// inject, configFile) only surface as diagnostics; anything else is a typo. +const SUPPORTED_FIELDS = new Set([ + 'name', 'version', 'description', 'keywords', 'author', 'homepage', 'license', + 'interface', 'skills', 'agents', 'sessionStart', 'skillInstructions', + 'systemPrompt', 'systemPromptPath', 'mcpServers', 'hooks', 'commands', +]); +// systemPrompt and systemPromptPath are each capped at 32 KB; oversized +// content is silently ignored, so AGENTS.md must stay well under it. +const PROMPT_BUDGET = 32 * 1024; +const SIX_SKILLS = ['ponytail', 'ponytail-review', 'ponytail-audit', 'ponytail-debt', 'ponytail-gain', 'ponytail-help']; +// Same load-bearing phrases asserted by scripts/check-rule-copies.js: the file +// systemPromptPath points at must actually carry the rules, not just exist. +const RULE_INVARIANTS = [ + 'lazy senior', + 'input validation at trust boundaries', + 'naive heuristic', +]; + +function read(relPath) { + return fs.readFileSync(path.join(root, relPath), 'utf8'); +} + +// Read inside each test (not at module scope) so a missing or malformed manifest +// surfaces as a clean per-test assertion failure, not a load-time crash. +function loadManifest() { + assert.ok(fs.existsSync(path.join(root, MANIFEST)), `${MANIFEST} must exist`); + return JSON.parse(read(MANIFEST)); +} + +// Every file reference in the manifest is a ./ path inside the plugin root. +function referencedPaths(manifest) { + return [manifest.systemPromptPath] + .concat(manifest.skills || []) + .concat(manifest.commands || []) + .filter(Boolean); +} + +test('manifest names the ponytail plugin with a pinned version', () => { + const manifest = loadManifest(); + assert.match(manifest.name, PLUGIN_ID); + assert.equal(manifest.name, 'ponytail'); + assert.match(manifest.version, PINNED_SEMVER); +}); + +test('version stays aligned with the other plugin manifests', () => { + const manifest = loadManifest(); + const reference = JSON.parse(read('.claude-plugin/plugin.json')); + assert.equal(manifest.version, reference.version); +}); + +test('manifest only uses fields Kimi Code supports', () => { + const manifest = loadManifest(); + for (const field of Object.keys(manifest)) { + assert.ok(SUPPORTED_FIELDS.has(field), `unsupported manifest field: ${field}`); + } +}); + +test('every referenced ./ path stays inside the plugin root and exists', () => { + const manifest = loadManifest(); + const refs = referencedPaths(manifest); + assert.ok(refs.length > 0, 'manifest references no files — adapter wires nothing'); + for (const ref of refs) { + assert.ok(ref.startsWith('./'), `manifest path must be relative to the plugin root: ${ref}`); + const resolved = path.resolve(root, ref); + assert.ok(resolved.startsWith(root + path.sep), `manifest path escapes the plugin root: ${ref}`); + assert.ok(fs.existsSync(resolved), `referenced path missing: ${ref}`); + } +}); + +test('systemPromptPath carries the ponytail rules within the 32 KB budget', () => { + const manifest = loadManifest(); + assert.ok(manifest.systemPromptPath, 'systemPromptPath must be set so rules load every session'); + const context = read(manifest.systemPromptPath); + for (const phrase of RULE_INVARIANTS) { + assert.ok(context.includes(phrase), `system prompt file missing rule invariant: "${phrase}"`); + } + assert.ok(Buffer.byteLength(context, 'utf8') < PROMPT_BUDGET, 'system prompt file exceeds the 32 KB budget'); +}); + +test('sessionStart.skill resolves to a shipped skill', () => { + const manifest = loadManifest(); + const skill = manifest.sessionStart && manifest.sessionStart.skill; + assert.ok(skill, 'sessionStart.skill must activate ponytail every session'); + const skillsDirs = [].concat(manifest.skills || []); + const found = skillsDirs.some((dir) => + fs.existsSync(path.join(root, dir, skill, 'SKILL.md'))); + assert.ok(found, `sessionStart.skill "${skill}" has no SKILL.md under the declared skills path`); +}); + +test('the declared skills dir ships the six ponytail skills', () => { + const manifest = loadManifest(); + for (const skill of SIX_SKILLS) { + const rel = path.join([].concat(manifest.skills)[0], skill, 'SKILL.md'); + assert.ok(fs.existsSync(path.join(root, rel)), `missing skill: ${rel}`); + } +}); + +// The command set is canonical in pi-extension; tests/commands.test.js already +// pins one file per registered command. Kimi Code must find the same set in +// the directory its manifest points at, as parseable Markdown commands. +test('every registered command resolves in the declared commands dir', () => { + const manifest = loadManifest(); + const commandsDir = [].concat(manifest.commands)[0]; + const piSource = fs.readFileSync(path.join(root, 'pi-extension', 'index.js'), 'utf8'); + const commands = [...piSource.matchAll(/registerCommand\(["']([\w-]+)["']/g)].map((m) => m[1]); + assert.ok(commands.length > 0, 'expected pi to register at least one command'); + for (const name of commands) { + const rel = path.join(commandsDir, `${name}.md`); + assert.ok(fs.existsSync(path.join(root, rel)), `missing command file: ${rel}`); + const body = read(rel); + // Kimi Code parses frontmatter for name/description; the rest is the prompt. + assert.match(body, /^---\r?\n[\s\S]*?description:.+?\r?\n---\r?\n/, `${rel} lost its frontmatter description`); + assert.ok(body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').trim().length > 0, `${rel} has an empty prompt body`); + } +}); From 47431705f5732a978b1e0727c639ceef97dc1692 Mon Sep 17 00:00:00 2001 From: Phoen1xCode Date: Fri, 31 Jul 2026 12:55:49 +0800 Subject: [PATCH 2/2] refactor: give the Kimi Code adapter its own plugin dir Move the manifest to .kimi-plugin/plugin.json and ship dedicated command copies in .kimi-plugin/commands/ instead of pointing at .opencode/command/, so the two adapters evolve independently. tests/commands.test.js now pins one Kimi command file per registered command, and tests/kimi-plugin.test.js byte-compares the two command sets so a one-sided edit fails loudly. --- .kimi-plugin/commands/ponytail-audit.md | 5 +++ .kimi-plugin/commands/ponytail-debt.md | 5 +++ .kimi-plugin/commands/ponytail-gain.md | 5 +++ .kimi-plugin/commands/ponytail-help.md | 5 +++ .kimi-plugin/commands/ponytail-review.md | 5 +++ .kimi-plugin/commands/ponytail.md | 5 +++ kimi.plugin.json => .kimi-plugin/plugin.json | 2 +- README.md | 2 +- docs/agent-portability.md | 2 +- scripts/check-versions.js | 2 +- tests/commands.test.js | 16 +++++-- tests/kimi-plugin.test.js | 46 +++++++++++++++----- 12 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 .kimi-plugin/commands/ponytail-audit.md create mode 100644 .kimi-plugin/commands/ponytail-debt.md create mode 100644 .kimi-plugin/commands/ponytail-gain.md create mode 100644 .kimi-plugin/commands/ponytail-help.md create mode 100644 .kimi-plugin/commands/ponytail-review.md create mode 100644 .kimi-plugin/commands/ponytail.md rename kimi.plugin.json => .kimi-plugin/plugin.json (94%) diff --git a/.kimi-plugin/commands/ponytail-audit.md b/.kimi-plugin/commands/ponytail-audit.md new file mode 100644 index 00000000..4722747f --- /dev/null +++ b/.kimi-plugin/commands/ponytail-audit.md @@ -0,0 +1,5 @@ +--- +description: Audit the whole repo for over-engineering, what can be deleted +--- + +Audit the entire repository for over-engineering only, not correctness. Scan the whole tree, not a diff. One line per finding, ranked biggest cut first: . . [path]. Tags: delete (dead code/speculative feature), stdlib (reinvented standard library), native (dependency doing what the platform does), yagni (abstraction with one implementation), shrink (same logic, fewer lines). End with the net lines and dependencies removable. If nothing to cut: 'Lean already. Ship.' diff --git a/.kimi-plugin/commands/ponytail-debt.md b/.kimi-plugin/commands/ponytail-debt.md new file mode 100644 index 00000000..d853778e --- /dev/null +++ b/.kimi-plugin/commands/ponytail-debt.md @@ -0,0 +1,5 @@ +--- +description: "Harvest ponytail: comments into a tracked debt ledger" +--- + +Harvest every `ponytail:` comment in this repository into a debt ledger so deferrals do not rot into 'later means never'. Grep the whole tree for comment markers (grep -rnE '(#|//) ?ponytail:' ., skipping node_modules/.git/build output). One row per marker, grouped by file: :, . ceiling: . upgrade: . Tag any marker that names no upgrade path or trigger as no-trigger, those rot silently. End with the count of markers and how many lack a trigger. If none: 'No ponytail: debt. Clean ledger.' Report only, change nothing. diff --git a/.kimi-plugin/commands/ponytail-gain.md b/.kimi-plugin/commands/ponytail-gain.md new file mode 100644 index 00000000..9243a0e9 --- /dev/null +++ b/.kimi-plugin/commands/ponytail-gain.md @@ -0,0 +1,5 @@ +--- +description: Show ponytail's measured impact scoreboard (less code, cost, time) +--- + +Show the ponytail gain scoreboard. One shot, change nothing: do not switch mode, write flag files, or persist anything. Render the published benchmark medians (5 everyday tasks; models Haiku, Sonnet, Opus; source benchmarks/ and the README) as plain ASCII bars: Lines of code, no-skill 100% vs ponytail 6-20% (down 80-94%); Cost, no-skill 100% vs ponytail 23-53% (down 47-77%); Speed, ponytail 3-6x faster. The bar length shows the measured range, the label carries the exact figure. These are benchmark medians, not this repo. NEVER print a per-repo savings number: the unbuilt version was never written, so there is no real baseline to subtract from in a live repo. For real per-repo figures, point to /ponytail-debt (the counted shortcut ledger) and /ponytail-audit (what is still cuttable). Report only. diff --git a/.kimi-plugin/commands/ponytail-help.md b/.kimi-plugin/commands/ponytail-help.md new file mode 100644 index 00000000..d1751759 --- /dev/null +++ b/.kimi-plugin/commands/ponytail-help.md @@ -0,0 +1,5 @@ +--- +description: Quick reference for ponytail levels, skills, and commands +--- + +Show the ponytail quick reference. One shot, change nothing: do not switch mode, write flag files, or persist anything. Levels: /ponytail lite (build what's asked, name the lazier alternative in one line), /ponytail (full, the default ladder: YAGNI then stdlib then native then one line then minimum), /ponytail ultra (deletion before addition, challenges the requirement before building). Commands: /ponytail-review (over-engineering review of the current changes), /ponytail-audit (whole-repo over-engineering audit), /ponytail-debt (harvest ponytail: comments into a tracked ledger), /ponytail-gain (measured-impact scoreboard from the benchmark), /ponytail-help (this card). Deactivate with 'stop ponytail', 'normal mode', or /ponytail off; resume anytime with /ponytail. Default mode is full; change it with the PONYTAIL_DEFAULT_MODE environment variable (off|lite|full|ultra) or a config file at ~/.config/ponytail/config.json (Windows: %APPDATA%\ponytail\config.json) with {"defaultMode": "lite"}. Resolution order: env var, then config file, then full. diff --git a/.kimi-plugin/commands/ponytail-review.md b/.kimi-plugin/commands/ponytail-review.md new file mode 100644 index 00000000..119cda5f --- /dev/null +++ b/.kimi-plugin/commands/ponytail-review.md @@ -0,0 +1,5 @@ +--- +description: Review changes for over-engineering, what can be deleted +--- + +Review the current code changes for over-engineering only, not correctness. One line per finding: L: . . Tags: delete (dead code/speculative feature), stdlib (reinvented standard library), native (dependency doing what the platform does), yagni (abstraction with one implementation), shrink (same logic, fewer lines). End with the net lines removable. If nothing to cut: 'Lean already. Ship.' diff --git a/.kimi-plugin/commands/ponytail.md b/.kimi-plugin/commands/ponytail.md new file mode 100644 index 00000000..7a6892b3 --- /dev/null +++ b/.kimi-plugin/commands/ponytail.md @@ -0,0 +1,5 @@ +--- +description: Switch ponytail intensity level (lite/full/ultra/off) +--- + +Switch to ponytail $ARGUMENTS mode. If no level specified, use full. Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a ponytail: comment that names the ceiling and upgrade path. diff --git a/kimi.plugin.json b/.kimi-plugin/plugin.json similarity index 94% rename from kimi.plugin.json rename to .kimi-plugin/plugin.json index d6722522..9ac9fc21 100644 --- a/kimi.plugin.json +++ b/.kimi-plugin/plugin.json @@ -14,7 +14,7 @@ "shortDescription": "Lazy senior dev mode: YAGNI, stdlib first, the least code that works." }, "skills": "./skills/", - "commands": "./.opencode/command/", + "commands": "./.kimi-plugin/commands/", "systemPromptPath": "./AGENTS.md", "sessionStart": { "skill": "ponytail" diff --git a/README.md b/README.md index 2d22971b..7e64148d 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ For full plugin-tier support (automatic mode activation + ruleset injection on e Or run `/plugins`, switch to the **Custom** tab, and paste the repo URL. Run `/reload` (or `/new`) after installing. -The plugin manifest ([`kimi.plugin.json`](kimi.plugin.json)) ships the six skills, registers the commands as `/ponytail:ponytail`, `/ponytail:ponytail-review`, and so on, injects `AGENTS.md` into the system prompt, and session-starts the ponytail skill — always-on with zero setup. Level switches (`/ponytail:ponytail ultra`) last for the session. The adapter declares no hooks: Kimi Code's hook payload differs from the Claude/Codex event shapes ponytail's lifecycle hooks emit, and the system prompt plus session-start skill already cover always-on behavior. +The plugin manifest ([`.kimi-plugin/plugin.json`](.kimi-plugin/plugin.json)) ships the six skills, registers the commands from [`.kimi-plugin/commands/`](.kimi-plugin/commands/) as `/ponytail:ponytail`, `/ponytail:ponytail-review`, and so on, injects `AGENTS.md` into the system prompt, and session-starts the ponytail skill — always-on with zero setup. Level switches (`/ponytail:ponytail ultra`) last for the session. The adapter declares no hooks: Kimi Code's hook payload differs from the Claude/Codex event shapes ponytail's lifecycle hooks emit, and the system prompt plus session-start skill already cover always-on behavior. ### Antigravity CLI diff --git a/docs/agent-portability.md b/docs/agent-portability.md index 37cccda3..6b87c3e4 100644 --- a/docs/agent-portability.md +++ b/docs/agent-portability.md @@ -28,7 +28,7 @@ to load in a given agent. | 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. | | 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`. | -| Kimi Code | `kimi.plugin.json`, `skills/`, `.opencode/command/`, `AGENTS.md` | Full plugin-tier install (`/plugins install https://github.com/DietrichGebert/ponytail`): the root manifest reuses the six `skills/`, registers the shared `.opencode/command/*.md` files as `/ponytail:`, injects `AGENTS.md` via `systemPromptPath`, and session-starts the ponytail skill. No hooks — Kimi Code's hook payload differs from the Claude/Codex event shapes ponytail's lifecycle hooks emit, so level switches live for the session. | +| Kimi Code | `.kimi-plugin/`, `skills/`, `AGENTS.md` | Full plugin-tier install (`/plugins install https://github.com/DietrichGebert/ponytail`): the manifest in `.kimi-plugin/` reuses the six `skills/`, registers its own command copies (`.kimi-plugin/commands/*.md`, kept in sync with the OpenCode set by test) as `/ponytail:`, injects `AGENTS.md` via `systemPromptPath`, and session-starts the ponytail skill. No hooks — Kimi Code's hook payload differs from the Claude/Codex event shapes ponytail's lifecycle hooks emit, so level switches live for the session. | | 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/scripts/check-versions.js b/scripts/check-versions.js index d63012e3..dabec1d1 100644 --- a/scripts/check-versions.js +++ b/scripts/check-versions.js @@ -25,7 +25,7 @@ const VERSION_FILES = [ '.github/plugin/plugin.json', // Copilot plugin '.qoder-plugin/plugin.json', // Qoder plugin 'gemini-extension.json', // Gemini CLI extension - 'kimi.plugin.json', // Kimi Code plugin + '.kimi-plugin/plugin.json', // Kimi Code plugin 'package.json', // pi-package / repo root 'ponytail-mcp/package.json', // MCP server (private, internal-only) ]; diff --git a/tests/commands.test.js b/tests/commands.test.js index ef47a86c..94eb9071 100644 --- a/tests/commands.test.js +++ b/tests/commands.test.js @@ -1,9 +1,10 @@ #!/usr/bin/env node // Every ponytail command the pi extension registers must also ship as a // file-based command for the hosts that need one: Claude Code (commands/*.toml, -// which Gemini CLI reuses) and OpenCode (.opencode/command/*.md). /ponytail-help -// was advertised in the README and the help card but missing both files; this -// guards that drift -- a registered command with no adapter file fails here. +// which Gemini CLI reuses), OpenCode (.opencode/command/*.md), and Kimi Code +// (.kimi-plugin/commands/*.md). /ponytail-help was advertised in the README and +// the help card but missing both files; this guards that drift -- a registered +// command with no adapter file fails here. const test = require('node:test'); const assert = require('node:assert/strict'); @@ -37,3 +38,12 @@ test('every registered command ships an OpenCode .opencode/command/*.md', () => ); } }); + +test('every registered command ships a Kimi Code .kimi-plugin/commands/*.md', () => { + for (const name of commands) { + assert.ok( + fs.existsSync(path.join(root, '.kimi-plugin', 'commands', `${name}.md`)), + `missing .kimi-plugin/commands/${name}.md`, + ); + } +}); diff --git a/tests/kimi-plugin.test.js b/tests/kimi-plugin.test.js index 62ff5912..99327906 100644 --- a/tests/kimi-plugin.test.js +++ b/tests/kimi-plugin.test.js @@ -1,15 +1,17 @@ #!/usr/bin/env node -// Smoke test for the Kimi Code adapter. The adapter is a single thin manifest -// (kimi.plugin.json, repo root takes precedence over .kimi-plugin/plugin.json) -// that reuses the repo's existing files: AGENTS.md via systemPromptPath for -// always-on rules, skills/ for the six agent skills, sessionStart.skill to -// activate ponytail every session, and the shared Markdown command files in -// .opencode/command/ — already the exact frontmatter + $ARGUMENTS shape Kimi -// Code registers as /ponytail:. Kimi's hook payload differs from the -// Claude/Codex event shapes ponytail's lifecycle hooks emit, so the adapter -// declares no hooks. This test fails if the manifest is removed, drifts off -// the pinned version, points at files that no longer exist, or starts carrying -// fields Kimi Code would flag as diagnostics. +// Smoke test for the Kimi Code adapter. The adapter is a self-contained +// plugin dir (.kimi-plugin/): the manifest (plugin.json, recognized by Kimi +// Code next to the root-level kimi.plugin.json form) wires the repo's shared +// files — AGENTS.md via systemPromptPath for always-on rules, skills/ for the +// six agent skills, sessionStart.skill to activate ponytail every session — +// and registers its own copy of the Markdown commands in .kimi-plugin/commands/ +// as /ponytail:. The command copies start out identical to +// .opencode/command/*.md and a test below keeps the two adapters from silently +// drifting apart. Kimi's hook payload differs from the Claude/Codex event +// shapes ponytail's lifecycle hooks emit, so the adapter declares no hooks. +// This test fails if the manifest is removed, drifts off the pinned version, +// points at files that no longer exist, or starts carrying fields Kimi Code +// would flag as diagnostics. // // Manifest spec: https://www.kimi.com/code/docs/en/kimi-code-cli/customization/plugins.html @@ -19,7 +21,7 @@ const fs = require('fs'); const path = require('path'); const root = path.join(__dirname, '..'); -const MANIFEST = 'kimi.plugin.json'; +const MANIFEST = path.join('.kimi-plugin', 'plugin.json'); // Floating refs are a supply-chain footgun; the manifest version must be pinned. const PINNED_SEMVER = /^\d+\.\d+\.\d+$/; // The name field doubles as the plugin id (and the /ponytail: prefix). @@ -140,3 +142,23 @@ test('every registered command resolves in the declared commands dir', () => { assert.ok(body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').trim().length > 0, `${rel} has an empty prompt body`); } }); + +// The Kimi adapter keeps its own copy of the command files so the two adapters +// can evolve independently — but the copies start out identical, and a change +// to one almost always applies to the other. Byte-compare against the OpenCode +// set so a one-sided edit fails loudly instead of drifting (same convention as +// scripts/check-rule-copies.js). If a divergence is ever deliberate, this is +// the reminder to say so in the test. +test('command files stay in sync with the OpenCode set', () => { + const manifest = loadManifest(); + const kimiDir = [].concat(manifest.commands)[0]; + const opencodeDir = path.join('.opencode', 'command'); + const kimiFiles = fs.readdirSync(path.join(root, kimiDir)).filter((f) => f.endsWith('.md')).sort(); + const opencodeFiles = fs.readdirSync(path.join(root, opencodeDir)).filter((f) => f.endsWith('.md')).sort(); + assert.deepEqual(kimiFiles, opencodeFiles, 'the two command dirs ship different command sets'); + for (const file of kimiFiles) { + const kimiBody = read(path.join(kimiDir, file)).replace(/\r\n/g, '\n'); + const opencodeBody = read(path.join(opencodeDir, file)).replace(/\r\n/g, '\n'); + assert.equal(kimiBody, opencodeBody, `${file} drifted from .opencode/command/${file}`); + } +});