From e01aa900f7e12e8a4660fb5d757ad016baeffed9 Mon Sep 17 00:00:00 2001 From: Ben Younes Date: Sun, 14 Jun 2026 17:50:02 +0200 Subject: [PATCH] feat: add Gemini CLI support (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Gemini CLI support Add a thin Gemini CLI extension adapter (closes #22). The manifest points contextFileName at the existing AGENTS.md for always-on rules and reuses the repo's commands/*.toml (/ponytail, /ponytail-review) and skills/, which Gemini CLI auto-discovers — no rule text is duplicated. Co-Authored-By: Claude Opus 4.8 (1M context) * test: read manifest inside cases for a clean RED The manifest was parsed at module scope, so reverting the adapter crashed the whole test file at load (ENOENT) and collapsed all four cases into one unreadable failure. Read it inside each case via a helper that asserts existence first, so a missing or malformed manifest surfaces as clear per-case assertion failures instead of a stack trace. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- README.md | 10 ++++- docs/agent-portability.md | 1 + gemini-extension.json | 6 +++ tests/gemini-extension.test.js | 67 ++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 gemini-extension.json create mode 100644 tests/gemini-extension.test.js 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}`); + } +});