feat(#775): ship a gemini-extension.json extension package (#818)

Add a Gemini CLI extension package so users can install, update, and
remove GSD through Gemini's own extension lifecycle and have it appear in
`gemini extensions list`:

  gemini extensions install https://github.com/open-gsd/gsd-core
  gemini extensions update gsd-core
  gemini extensions uninstall gsd-core
  gemini extensions link /path/to/gsd-core   # dev

This mirrors the additive Claude Code plugin manifest (#766): a thin,
version-stamped manifest enforced by an in-repo drift test. The extension
ships the context-file payload (GEMINI.md), loaded into every Gemini
session; slash-command/agent/hook TOML projection into the extension is a
documented follow-up. The manual `npx gsd-core --gemini` installer (which
provides the /gsd:* commands) is unchanged — purely additive, no breaking
change.

- gemini-extension.json: name=binName, version tracks package.json,
  description, contextFileName=GEMINI.md (minimal; no mcpServers — gsd
  ships no MCP server)
- GEMINI.md: Gemini-session context payload
- package.json: add both artifacts to files[] so they publish
- CONTEXT.md: add "Gemini Extension Package" glossary entry
- docs: USER-GUIDE + install-on-your-runtime how-to
- tests/issue-775-gemini-extension.test.cjs: manifest validity, version
  parity with package.json, contextFileName existence, files[] publication

Closes #775

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-07 18:54:08 -04:00
committed by GitHub
parent 2860e995b5
commit a3aa0ae142
8 changed files with 240 additions and 0 deletions

View File

@@ -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 <path>` 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)

View File

@@ -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 <git-url>` / `update` / `remove` (and `gemini extensions link <path>` 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`.

53
GEMINI.md Normal file
View File

@@ -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 <N>` — produce a detailed phase plan with a verification loop.
- `/gsd:execute-phase <N>` — 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: <https://github.com/open-gsd/gsd-core>

View File

@@ -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:

View File

@@ -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

6
gemini-extension.json Normal file
View File

@@ -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"
}

View File

@@ -13,6 +13,8 @@
"assets",
"agents",
".claude-plugin",
"gemini-extension.json",
"GEMINI.md",
"hooks",
"scripts"
],

View File

@@ -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 <git-url>`)
* 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`
);
}
});
});