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:
5
.changeset/gemini-extension-package.md
Normal file
5
.changeset/gemini-extension-package.md
Normal 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)
|
||||
@@ -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
53
GEMINI.md
Normal 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>
|
||||
@@ -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:
|
||||
|
||||
@@ -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
6
gemini-extension.json
Normal 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"
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
"assets",
|
||||
"agents",
|
||||
".claude-plugin",
|
||||
"gemini-extension.json",
|
||||
"GEMINI.md",
|
||||
"hooks",
|
||||
"scripts"
|
||||
],
|
||||
|
||||
119
tests/issue-775-gemini-extension.test.cjs
Normal file
119
tests/issue-775-gemini-extension.test.cjs
Normal 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`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user