diff --git a/README.md b/README.md index 9334ea2..405d3a2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

Stars Release - Works with 10 agents + Works with 11 agents MIT license

@@ -101,6 +101,14 @@ Run OpenCode from a checkout of this repo (the plugin reuses its `hooks/` and `s Injects the ruleset every turn at the active level; adds `/ponytail`, `/ponytail-review`, and `/ponytail-audit`. OpenCode also auto-loads this repo's `AGENTS.md`, so the rules hold even without the plugin. The plugin adds the `lite/full/ultra/off` levels. +### Gemini CLI + +```bash +gemini extensions install https://github.com/DietrichGebert/ponytail +``` + +Loads the ruleset as always-on context every session and registers `/ponytail` and `/ponytail-review`; the `skills/` ship too, activated when a task needs them. + That was it. He'd be proud. He won't say it. Active every session. `/ponytail-review` finds what to delete in your diff, `/ponytail-audit` does the same for the whole repo. `/ponytail ultra` exists for when the codebase has wronged you personally. `/ponytail-help` explains the rest. diff --git a/docs/agent-portability.md b/docs/agent-portability.md index 6db5d17..1206031 100644 --- a/docs/agent-portability.md +++ b/docs/agent-portability.md @@ -11,6 +11,7 @@ to load in a given agent. | Claude Code | `.claude-plugin/`, `commands/`, `hooks/` | Full plugin install with session activation, mode tracking, commands, and statusline support. | | Codex | `.codex-plugin/plugin.json`, `hooks/hooks.json`, `hooks/`, `skills/` | Plugin install with the same skills plus lifecycle hooks for activation and mode tracking. | | OpenCode | `.opencode/plugins/ponytail.mjs`, `.opencode/command/`, `hooks/`, `skills/` | Server plugin injects the ruleset each turn via `experimental.chat.system.transform` and persists `/ponytail` switches; reuses the shared instruction builder. | +| Gemini CLI | `gemini-extension.json`, `AGENTS.md`, `commands/`, `skills/` | Extension manifest points `contextFileName` at `AGENTS.md` for always-on rules, and reuses the existing `commands/*.toml` (`/ponytail`, `/ponytail-review`) and `skills/`, which Gemini CLI auto-discovers. | | Cursor | `.cursor/rules/ponytail.mdc` | Always-on project rule. | | Windsurf | `.windsurf/rules/ponytail.md` | Project rule. | | Cline | `.clinerules/ponytail.md` | Project rule. | diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 0000000..5acba13 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,6 @@ +{ + "name": "ponytail", + "version": "4.2.0", + "description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.", + "contextFileName": "AGENTS.md" +} diff --git a/tests/gemini-extension.test.js b/tests/gemini-extension.test.js new file mode 100644 index 0000000..866f66d --- /dev/null +++ b/tests/gemini-extension.test.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +// Smoke test for the Gemini CLI adapter. The adapter is a single thin manifest +// (gemini-extension.json) that reuses the repo's existing files: AGENTS.md for +// always-on context, commands/*.toml for /ponytail + /ponytail-review, and +// skills/ for the agent skills. This test fails if the manifest is removed, +// loses its pinned version, or points contextFileName at a file that no longer +// carries the load-bearing rules — i.e. if the adapter stops wiring ponytail. + +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 = 'gemini-extension.json'; +const EXTENSION_NAME = 'ponytail'; +// Floating refs are a supply-chain footgun; the manifest version must be pinned. +const PINNED_SEMVER = /^\d+\.\d+\.\d+$/; +// Gemini auto-discovers these by directory; the manifest is only useful if they exist. +const REUSED_COMMANDS = ['commands/ponytail.toml', 'commands/ponytail-review.toml']; +const REUSED_SKILLS = ['skills/ponytail/SKILL.md']; +// Same load-bearing phrases asserted by scripts/check-rule-copies.js: the file +// contextFileName 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 that +// collapses every case into one unreadable stack trace. +function loadManifest() { + assert.ok(fs.existsSync(path.join(root, MANIFEST)), `${MANIFEST} must exist`); + return JSON.parse(read(MANIFEST)); +} + +test('manifest names the ponytail extension with a pinned version', () => { + const manifest = loadManifest(); + assert.equal(manifest.name, EXTENSION_NAME); + assert.match(manifest.version, PINNED_SEMVER); +}); + +test('version stays aligned with the other plugin manifests', () => { + const manifest = loadManifest(); + const claude = JSON.parse(read('.claude-plugin/plugin.json')); + assert.equal(manifest.version, claude.version); +}); + +test('contextFileName resolves to a file carrying the ponytail rules', () => { + const manifest = loadManifest(); + assert.ok(manifest.contextFileName, 'contextFileName must be set so rules load every session'); + const context = read(manifest.contextFileName); + for (const phrase of RULE_INVARIANTS) { + assert.ok(context.includes(phrase), `context file missing rule invariant: "${phrase}"`); + } +}); + +test('the commands and skills the adapter reuses are present', () => { + for (const rel of [...REUSED_COMMANDS, ...REUSED_SKILLS]) { + assert.ok(fs.existsSync(path.join(root, rel)), `reused file missing: ${rel}`); + } +});