diff --git a/.changeset/gemini-extension-package.md b/.changeset/gemini-extension-package.md new file mode 100644 index 000000000..5c527dc5a --- /dev/null +++ b/.changeset/gemini-extension-package.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 775 +--- +**Gemini CLI extension package** — gsd-core now ships a `gemini-extension.json` manifest (plus a `GEMINI.md` context payload) at the repository root, so Gemini CLI users can install, update, and remove GSD through Gemini's own extension lifecycle: `gemini extensions install https://github.com/open-gsd/gsd-core`, `gemini extensions update gsd-core`, `gemini extensions uninstall gsd-core`, and `gemini extensions link ` for local dev. The extension is discoverable in `gemini extensions list` and loads GSD's operating context into every session. Additive — the existing `npx gsd-core --gemini` installer (which provides the `/gsd:*` slash commands) is unchanged. (#775) diff --git a/CONTEXT.md b/CONTEXT.md index 4f97160ed..7ae30f170 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -127,6 +127,9 @@ Module owning the explicit per-runtime config-mutation dispatch table for the in ### Claude Code Plugin Manifest Module Module owning the projection of gsd-core's artifact surfaces (`commands`, `agents`, hooks) onto the Claude Code plugin contract (`.claude-plugin/plugin.json` + `hooks/hooks.json`) — the plugin-contract sibling of the Runtime Artifact Layout Module (which projects the same surfaces onto filesystem placements). Defined mapping: `name`=`binName` (drives the `/gsd-core:` command namespace), `repository`/`homepage`=`repoUrl` (Package Identity Module), `version`/`description`/`license` from `package.json` (`version` is required for `claude plugin validate --strict`), `commands`=`./commands/gsd/`, agents via Claude Code's default `agents/` discovery (the explicit string form is schema-rejected), `hooks`=`./hooks/hooks.json`. The hook projection carries ONLY the always-on subset of the Installer Module's Claude `settings.json` wiring (check-update, context-monitor, prompt-guard, read-guard, worktree-path-guard, read-injection-scanner) via `${CLAUDE_PLUGIN_ROOT}`; config-gated opt-in hooks are excluded because a static manifest cannot honor per-project config gates, and plugin-shipped agents cannot carry hook frontmatter (so all plugin-path hook wiring lives in hooks.json). Additive — the file-copy path (Runtime Artifact Layout / Install Policy / Installer Modules) is unchanged. Conformance is validated by `claude plugin validate --strict` plus the in-repo drift-guard `tests/issue-766-plugin-manifest.test.cjs`. _Avoid_: "the plugin API", "the plugin file" (when you mean the seam). See ADR-766 and Runtime Artifact Layout Module. +### Gemini Extension Package +The repo-root `gemini-extension.json` + `GEMINI.md` pair that projects gsd-core onto the Gemini CLI extension contract, enabling one-step lifecycle management via `gemini extensions install ` / `update` / `remove` (and `gemini extensions link ` for dev). The Gemini-CLI sibling of the Claude Code Plugin Manifest Module — same additive idea, different runtime package format. Defined mapping: `name`=`binName` (`gsd-core`; lowercase-dashes per Gemini's extension naming rule), `version` tracks `package.json` (Gemini's `gemini extensions update` keys off the manifest `version` field), `description` (required by the manifest schema), `contextFileName`=`GEMINI.md` (the extension's context payload, loaded into every Gemini session). Intentionally minimal: no `mcpServers` (gsd-core ships no MCP server). Slash-command / agent / hook projection into the extension (which would require committing the Gemini-format TOML/agent conversions the Installer Module produces at `--gemini` install time) is deferred — the manual `npx gsd-core --gemini` path remains the way to install the `/gsd:*` commands, and is unchanged (additive, no breaking change). Conformance is guarded by the in-repo drift test `tests/issue-775-gemini-extension.test.cjs` (manifest validity, `version`↔`package.json` parity, `contextFileName` existence, `files[]` publication). _Avoid_: "the Gemini plugin" (Gemini calls them extensions, not plugins). See #775, ADR-766, Claude Code Plugin Manifest Module, and Runtime Artifact Layout Module. + ### Knowledge Graph Module Module owning the graphify integration: config gate (`isGraphifyEnabled`), disabled response (`disabledResponse`), subprocess helper (`execGraphify`, typed `GRAPHIFY_REASON` enum), presence detection (`checkGraphifyInstalled`), version checking (`checkGraphifyVersion`), query surface (`graphifyQuery` — BFS seed-expand + budget trim), status surface (`graphifyStatus` — node/edge counts, mtime staleness, commit-staleness tri-state via `built_at_commit`/`commits_behind`/`commit_stale`), diff surface (`graphifyDiff` — added/removed/changed nodes+edges), build pre-flight (`graphifyBuild`), snapshot management (`writeSnapshot`). Reads `.planning/config.json:graphify.enabled` as config gate; writes to `.planning/graphs/`. Auto-update hook (`hooks/gsd-graphify-update.sh`) triggers a detached background rebuild after HEAD-advancing git operations on the default branch when `graphify.auto_update=true`. Status file `.planning/graphs/.last-build-status.json` carries `{ ts, status, exit_code, duration_ms, head_at_build, graphify_version }`. Graph IR uses `nodes[]`, `edges[]` (or `links[]` for graphify ≥0.7 compat), `hyperedges[]`, `built_at_commit`. `commit_stale` is tri-state: `false` (known fresh), `true` (stale), `null` (unknown — no git or pre-v0.7 graph). Source: `gsd-core/bin/lib/graphify.cjs`. Skill: `commands/gsd/graphify.md`. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..0d298557d --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,53 @@ +# GSD Core — Gemini CLI context + +This context is loaded by the **gsd-core Gemini CLI extension**. It gives Gemini +the operating context for [GSD Core](https://github.com/open-gsd/gsd-core), a +meta-prompting, context-engineering, and spec-driven development system for AI +coding agents. + +## What GSD is + +GSD turns a vague goal into shipped software through an explicit, +resumable workflow: **explore → plan → execute → verify → ship**. Work is +organised into milestones and phases under a `.planning/` directory, with each +phase carrying a SPEC, a PLAN, and verification criteria. The system favours +small, atomic, test-backed commits and keeps durable context in version-tracked +files rather than in the conversation. + +## The slash commands (installed separately) + +> **This extension ships only the context above — not the slash commands.** It +> loads gsd's operating context into your Gemini sessions and is managed through +> `gemini extensions list / update / uninstall`. To install the `/gsd:*` command +> set, agents, and hooks into `~/.gemini/`, run the dedicated installer: +> +> ```bash +> npx gsd-core --gemini --global +> ``` +> +> The two paths are complementary and the manual installer remains fully +> supported. The commands below are available only once that installer has run. + +If you have installed the gsd commands, the workflow is driven by these `/gsd:*` +slash commands (Gemini registers gsd's commands under the `gsd` namespace, so the +colon form is canonical): + +- `/gsd:new-project` — initialise a project and gather deep context. +- `/gsd:progress` — the unified situational command: check progress, advance the + workflow, or dispatch a freeform intent. +- `/gsd:plan-phase ` — produce a detailed phase plan with a verification loop. +- `/gsd:execute-phase ` — execute a phase's plans with wave-based parallelism. +- `/gsd:verify-work` — validate built features through conversational UAT. +- `/gsd:ship` — open a PR, run review, and prepare for merge. +- `/gsd:help` — list every available command. + +## Working with GSD + +- Treat `.planning/` as the source of truth for project state — read it before + acting, and keep it current as work progresses. +- Prefer the smallest change that satisfies the phase's verification criteria. +- Run the project's tests and linters before declaring a phase done. +- When unsure what to do next, and the gsd commands are installed, `/gsd:progress` + is the situational entry point. + +Learn more: diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index e7af7be47..1e3eef13d 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -733,6 +733,43 @@ npx @opengsd/gsd-core --codebuddy --global npx @opengsd/gsd-core --qwen --global ``` +### Installing as a Gemini CLI extension (#775) + +GSD ships a `gemini-extension.json` extension manifest at the repository root, so +Gemini CLI users can install, update, and remove GSD through Gemini's own +extension lifecycle — and have it show up in `gemini extensions list`: + +```bash +# Install (Gemini clones the repo and copies the extension) +gemini extensions install https://github.com/open-gsd/gsd-core + +# Update to the latest released manifest version +gemini extensions update gsd-core + +# Remove +gemini extensions uninstall gsd-core +``` + +For local development against a checkout, symlink it instead of copying: + +```bash +gemini extensions link /path/to/gsd-core +``` + +**What the extension delivers today:** it loads GSD's operating context +(`GEMINI.md`) into every Gemini session in the project, and gives you the +discoverable install/update/remove lifecycle above. The `/gsd:*` slash commands, +agents, and hooks are still installed via the dedicated installer: + +```bash +npx @opengsd/gsd-core --gemini --global +``` + +The two paths are complementary and additive — installing the extension does not +change or replace the `npx gsd-core --gemini` install, and either can be used on +its own. (Slash-command/agent/hook projection into the extension package itself +is a planned follow-up.) + ### Installing for Prerelease Editions Set the runtime's `*_CONFIG_DIR` env var to the prerelease directory before running the installer: diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 1231e0336..f14046ebb 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -104,6 +104,21 @@ GEMINI_CONFIG_DIR=~/.gemini-alt npx @opengsd/gsd-core@latest --gemini --global --- +### Gemini CLI — native extension install (#775) + +GSD also ships a `gemini-extension.json` extension manifest, so you can manage GSD through Gemini's own extension lifecycle and see it in `gemini extensions list`: + +```bash +gemini extensions install https://github.com/open-gsd/gsd-core # install +gemini extensions update gsd-core # update +gemini extensions uninstall gsd-core # remove +gemini extensions link /path/to/gsd-core # dev: symlink a checkout +``` + +The extension loads GSD's operating context (`GEMINI.md`) into every session and gives you the discoverable install/update/remove lifecycle. The `/gsd:*` slash commands, agents, and hooks are installed separately by `npx @opengsd/gsd-core --gemini --global` (above). The two paths are complementary and additive — neither replaces the other, and slash-command projection into the extension is a planned follow-up. + +--- + ### OpenCode ```bash diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 000000000..31dbbdead --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,6 @@ +{ + "name": "gsd-core", + "version": "1.3.1-dev.0", + "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.", + "contextFileName": "GEMINI.md" +} diff --git a/package.json b/package.json index 01069328d..8eac5d16e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "assets", "agents", ".claude-plugin", + "gemini-extension.json", + "GEMINI.md", "hooks", "scripts" ], diff --git a/tests/issue-775-gemini-extension.test.cjs b/tests/issue-775-gemini-extension.test.cjs new file mode 100644 index 000000000..33d5caf38 --- /dev/null +++ b/tests/issue-775-gemini-extension.test.cjs @@ -0,0 +1,119 @@ +'use strict'; + +/** + * Regression tests for issue #775: additive Gemini CLI extension manifest. + * + * Asserts structural and semantic correctness of the Gemini Extension Package: + * gemini-extension.json — extension manifest (consumed by + * `gemini extensions install `) + * GEMINI.md — the extension's context-file payload, referenced + * by the manifest's `contextFileName` field. + * + * This mirrors tests/issue-766-plugin-manifest.test.cjs (the parallel Claude + * Code plugin manifest) — the Gemini extension is the same artifact-surface + * projection onto Gemini CLI's package contract. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const identity = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'package-identity.cjs')); +const pkg = require(path.join(ROOT, 'package.json')); + +const MANIFEST_PATH = path.join(ROOT, 'gemini-extension.json'); + +// ─── Section A: gemini-extension.json ──────────────────────────────────────── +describe('A: gemini-extension.json', () => { + + let manifest; + + test('exists and is valid JSON', () => { + assert.ok(fs.existsSync(MANIFEST_PATH), 'gemini-extension.json must exist at repo root'); + const raw = fs.readFileSync(MANIFEST_PATH, 'utf-8'); + manifest = JSON.parse(raw); // throws on invalid JSON + assert.ok(typeof manifest === 'object' && manifest !== null, 'manifest must be a JSON object'); + }); + + test('name equals identity.binName ("gsd-core")', (t) => { + if (!manifest) { t.skip('manifest could not be parsed'); return; } + assert.equal(manifest.name, identity.binName, `name should be "${identity.binName}"`); + }); + + test('name is lowercase/dashes only (Gemini extension naming rule)', (t) => { + if (!manifest) { t.skip('manifest could not be parsed'); return; } + // Gemini reference: "lowercase or numbers and use dashes instead of + // underscores or spaces". + assert.match( + manifest.name, + /^[a-z0-9]+(?:-[a-z0-9]+)*$/, + 'name must be lowercase letters/numbers with dashes (no underscores, spaces, or uppercase)' + ); + }); + + test('version is a non-empty string matching package.json version', (t) => { + if (!manifest) { t.skip('manifest could not be parsed'); return; } + assert.equal( + manifest.version, + pkg.version, + `gemini-extension.json version (${manifest.version}) must match package.json version (${pkg.version}). ` + + `When bumping the package version, update gemini-extension.json \`version\` to match — ` + + `Gemini CLI's \`gemini extensions update\` keys off the manifest version field. (#775)` + ); + }); + + test('description is a non-empty string (required by Gemini manifest schema)', (t) => { + if (!manifest) { t.skip('manifest could not be parsed'); return; } + assert.ok( + typeof manifest.description === 'string' && manifest.description.trim().length > 0, + 'description must be a non-empty string' + ); + }); + + test('contextFileName points to an existing repo-root file', (t) => { + if (!manifest) { t.skip('manifest could not be parsed'); return; } + assert.equal(manifest.contextFileName, 'GEMINI.md', 'contextFileName must be "GEMINI.md"'); + const ctx = path.join(ROOT, manifest.contextFileName); + assert.ok(fs.existsSync(ctx), `context file must exist on disk: ${ctx}`); + const body = fs.readFileSync(ctx, 'utf-8'); + assert.ok(body.trim().length > 0, 'context file must be non-empty'); + }); + + test('only declares schema-known top-level keys', (t) => { + if (!manifest) { t.skip('manifest could not be parsed'); return; } + // Per Gemini extension reference. We intentionally ship the minimal + // context-loading subset; this guards against typos / unknown keys that + // would fail `gemini extensions install` manifest validation. + const ALLOWED = new Set([ + 'name', 'version', 'description', 'contextFileName', + 'mcpServers', 'excludeTools', 'migratedTo', 'plan', 'settings', 'themes', + ]); + for (const key of Object.keys(manifest)) { + assert.ok(ALLOWED.has(key), `Unknown top-level manifest key "${key}" is not in the Gemini extension schema`); + } + }); + + test('does not declare mcpServers (gsd-core ships no MCP server)', (t) => { + if (!manifest) { t.skip('manifest could not be parsed'); return; } + assert.ok( + !Object.prototype.hasOwnProperty.call(manifest, 'mcpServers'), + 'manifest must NOT declare mcpServers — gsd-core has no MCP server' + ); + }); +}); + +// ─── Section B: package publication ────────────────────────────────────────── +describe('B: package.json publication', () => { + + test('files[] includes gemini-extension.json and GEMINI.md', () => { + const files = pkg.files || []; + for (const required of ['gemini-extension.json', 'GEMINI.md']) { + assert.ok( + files.includes(required), + `package.json "files" must include "${required}" so it ships to npm consumers` + ); + } + }); +});