diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 000000000..05bdc022d --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "gsd-core", + "description": "Marketplace for GSD Core — meta-prompting, context engineering, and spec-driven development system for AI coding agents.", + "owner": { + "name": "open-gsd", + "url": "https://github.com/open-gsd" + }, + "plugins": [ + { + "name": "gsd-core", + "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.", + "version": "1.7.0", + "source": "./", + "author": { + "name": "open-gsd", + "url": "https://github.com/open-gsd" + } + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a2f8ce964..e208749f5 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gsd-core", "displayName": "GSD Core", - "version": "1.6.1", + "version": "1.7.0", "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.", "author": { "name": "open-gsd", diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f1bfa3727..b408974a8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -37,7 +37,6 @@ body: description: Which AI coding tool are you using GSD with? options: - Claude Code - - Gemini CLI - OpenCode - Codex - Copilot @@ -174,7 +173,6 @@ body: **How to retrieve:** - Claude Code: `cat ~/.claude/settings.json` - - Gemini CLI: `cat ~/.gemini/settings.json` - OpenCode: `cat ~/.config/opencode/opencode.json` or `opencode.jsonc` **⚠️ PII Warning:** This file may contain API keys, tokens, or custom paths. **Remove all API keys and tokens before pasting.** We recommend running through [presidio-anonymizer](https://microsoft.github.io/presidio/) or manually redacting any line containing "key", "token", or "secret". diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 3df867eae..4107215a5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -170,7 +170,6 @@ body: description: Which runtimes must this work with? Check all that apply. options: - label: Claude Code - - label: Gemini CLI - label: OpenCode - label: Codex - label: Copilot diff --git a/.github/PULL_REQUEST_TEMPLATE/registry-entry.md b/.github/PULL_REQUEST_TEMPLATE/registry-entry.md new file mode 100644 index 000000000..c5da25e72 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/registry-entry.md @@ -0,0 +1,105 @@ +## Registry Entry PR + +> **Using the wrong template?** +> — Bug fix: use [fix.md](?template=fix.md) +> — New feature (not a registry listing): use [feature.md](?template=feature.md) +> — Enhancement to existing behavior: use [enhancement.md](?template=enhancement.md) + +Full schema and process: [docs/registries/README.md](../../docs/registries/README.md). + +--- + +## Registry type + + + +- [ ] Capability Registry entry — adds/updates one object in `docs/registries/capabilities.json` +- [ ] EoS Registry entry — adds/updates one object in `docs/registries/eos.json` + +## The entry + + + +```json +{ + "id": "", + "name": "", + "type": "", + "repo": "", + "description": "", + "author": "", + "license": "", + "enginesGsd": "", + "install": "", + "uninstall": "", + "interactions": {}, + "discussion": "" +} +``` + +--- + +## Required-field checklist + +- [ ] `id`, `name`, `type`, `repo`, `description`, `author`, `license`, `enginesGsd`, `install`, `uninstall`, `interactions`, `discussion` are all present and non-empty +- [ ] **(Capability entries only)** `interactions.loopExtensionPoints` is a non-empty subset of the 12 Loop Extension Points, `interactions.hookKinds` ⊆ `{step, contribution, gate}`, and `interactions.configKeys` / `requires` / `runtimeCompat` / `produces` / `consumes` are present (empty arrays are fine where nothing applies) +- [ ] **(EoS entries only)** `protocolVersion` is an integer ≥ 1, `interactions.interfacePoints` is a non-empty subset of the six interface points, `interactions.profile` is one of `programmatic-cli` / `declarative-cli` / `ide`, and `interactions.axes` has exactly the eight required axis keys + +## Ownership & non-endorsement + +- [ ] `repo` links to a repository **I own or am the primary maintainer of** — not a fork, mirror, or someone else's project +- [ ] I understand that inclusion in this registry means only that a maintainer merged this PR — it is **not** an endorsement, and GSD has not reviewed, tested, audited, or verified my solution or its claimed GSD interactions +- [ ] I understand this entry is removed only for illegal content, malware, spam, or a dead/non-functional link — never for quality — and a maintainer may remove it on that narrow basis without further notice + +## One entry, one PR + +- [ ] This PR adds or updates exactly **one** entry, in exactly one of `capabilities.json` / `eos.json` +- [ ] I have not bundled any other registry entry, code change, or unrelated docs change into this PR + +## Generated file in sync + +- [ ] I ran `npm run gen:registry` after editing the JSON source, and this PR includes the regenerated `docs/registries/capability-registry.md` or `docs/registries/eos-registry.md` +- [ ] I did **not** hand-edit the generated `.md` file directly — all edits were made to the JSON source + +## Documentation + +> CI enforces `lint:docs` for any changeset fragment typed `Added` / `Changed` / `Deprecated` / `Removed` — it must also touch a file under `docs/`. The JSON source and its regenerated markdown, both under `docs/registries/`, satisfy this. + +- [ ] This PR includes both the JSON source file and the regenerated markdown file under `docs/registries/` + +## Checklist + +- [ ] `npm run validate:registry` passes locally against my entry +- [ ] `discussion` links to a GitHub Discussion in the `Registry` category (or notes that one will be created on merge, per [docs/registries/README.md](../../docs/registries/README.md)) +- [ ] `.changeset/` fragment added with an `Added` type describing the new listing + +--- + +## Example filled entry + + + +```json +{ + "id": "linear-issue-sync", + "name": "Linear Issue Sync", + "type": "capability", + "repo": "some-org/gsd-cap-linear-sync", + "description": "Mirrors ROADMAP.md items to Linear issues as a ship:post contribution.", + "author": "Some Org ", + "license": "MIT", + "enginesGsd": ">=1.6.0", + "install": "gsd capability install https://github.com/some-org/gsd-cap-linear-sync.git#v1.0.0", + "uninstall": "gsd capability remove linear-issue-sync", + "interactions": { + "loopExtensionPoints": ["ship:post"], + "hookKinds": ["contribution"], + "configKeys": ["linear-issue-sync.enabled"], + "requires": [], + "runtimeCompat": ["all"], + "produces": ["linear-issue-links"], + "consumes": ["ROADMAP.md"] + }, + "discussion": "https://github.com/open-gsd/gsd-core/discussions/1234" +} +``` diff --git a/.github/workflows/auto-backmerge.yml b/.github/workflows/auto-backmerge.yml index 7fc561e8d..37fc83931 100644 --- a/.github/workflows/auto-backmerge.yml +++ b/.github/workflows/auto-backmerge.yml @@ -115,7 +115,7 @@ jobs: # filter those out. A substantive change still parks: it leaves # non-"version" lines (deps in package.json; resolved/integrity in the # lockfile when a dependency actually changes). - VERSION_STAMP_MANIFESTS='package.json package-lock.json .claude-plugin/plugin.json gemini-extension.json' + VERSION_STAMP_MANIFESTS='package.json package-lock.json .claude-plugin/plugin.json .claude-plugin/marketplace.json' DROPPED=$(printf '%s\n' "$DROPPED" | while IFS= read -r f; do [ -n "$f" ] || continue case " $VERSION_STAMP_MANIFESTS " in @@ -141,11 +141,6 @@ jobs: echo "dropped_oneline=" >> "$GITHUB_OUTPUT" fi - - - name: Install dependencies and build - if: steps.check.outputs.next_exists == 'true' - run: npm ci --silent && npm run build:lib - - name: Sync next's version to main's released version if: steps.check.outputs.next_exists == 'true' run: | diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml index 2b19882cb..109a25c2b 100644 --- a/.github/workflows/auto-label-issues.yml +++ b/.github/workflows/auto-label-issues.yml @@ -5,7 +5,7 @@ on: types: [opened] concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 927ab0c58..19e5ec846 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -319,7 +319,7 @@ jobs: needs: [validate-version, install-smoke-rc] if: inputs.action == 'rc' runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 permissions: contents: write id-token: write @@ -507,7 +507,10 @@ jobs: needs: [validate-version, install-smoke-finalize] if: inputs.action == 'finalize' runs-on: ubuntu-latest - timeout-minutes: 10 + # Matches the rc job's budget: `npm ci` + `npm run test:coverage:unit` now + # exceeds 10m as the unit suite grows, so a 10m cap cancels the job mid-test + # before tag/publish. 30m gives the same headroom rc already relies on. + timeout-minutes: 30 permissions: contents: write pull-requests: write diff --git a/.gitignore b/.gitignore index 12009ded1..fdec0b734 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,22 @@ build/ # by `npm run build:lib`). Source of truth is src/; these are emitted, never edited. # Published via prepublishOnly; built before test via pretest. Grows as modules migrate. /tsconfig.build.tsbuildinfo +/gsd-core/bin/lib/host-integration.cjs +/gsd-core/bin/lib/host-integration-sdk.cjs +/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +/gsd-core/bin/lib/handshake-serialized.cjs +/gsd-core/bin/lib/install-effort-resolver.cjs +/gsd-core/bin/lib/install-engine.cjs +/gsd-core/bin/lib/embedding-adapter.cjs +/gsd-core/bin/lib/adapter-declarative.cjs +/gsd-core/bin/lib/adapter-imperative.cjs +/gsd-core/bin/lib/model-adapter.cjs +/gsd-core/bin/lib/hook-bus.cjs +/gsd-core/bin/lib/state-io.cjs +/gsd-core/bin/lib/mcp-server.cjs +/gsd-core/bin/lib/external-descriptor-trust.cjs +/gsd-core/bin/lib/cli-skew-check.cjs /gsd-core/bin/lib/capability-loader.cjs /gsd-core/bin/lib/capability-source.cjs /gsd-core/bin/lib/capability-ledger.cjs @@ -75,6 +91,8 @@ build/ /gsd-core/bin/lib/capability-consent.cjs /gsd-core/bin/lib/capability-lock.cjs /gsd-core/bin/lib/markdown-sectionizer.cjs +/gsd-core/bin/lib/markdown-table.cjs +/gsd-core/bin/lib/write-set.cjs /gsd-core/bin/lib/resolution.cjs /gsd-core/bin/lib/research-store.cjs /gsd-core/bin/lib/research-provider.cjs @@ -83,7 +101,9 @@ build/ /gsd-core/bin/lib/plan-drift-guard.cjs /gsd-core/bin/lib/edge-probe.cjs /gsd-core/bin/lib/probe-core.cjs +/gsd-core/bin/lib/spec-section.cjs /gsd-core/bin/lib/prohibition-enforcement.cjs +/gsd-core/bin/lib/ui-consideration-probe.cjs /gsd-core/bin/lib/config-types.cjs /gsd-core/bin/lib/cli-exit.cjs /gsd-core/bin/lib/code-review-flags.cjs @@ -99,6 +119,7 @@ build/ /gsd-core/bin/lib/installer-migration-report.cjs /gsd-core/bin/lib/prompt-budget.cjs /gsd-core/bin/lib/secrets.cjs +/gsd-core/bin/lib/smart-entry.cjs /gsd-core/bin/lib/phase-lifecycle.cjs /gsd-core/bin/lib/workstream-name-policy.cjs /gsd-core/bin/lib/decisions.cjs @@ -151,6 +172,7 @@ build/ /gsd-core/bin/lib/core-utils.cjs /gsd-core/bin/lib/io.cjs /gsd-core/bin/lib/phase-id.cjs +/gsd-core/bin/lib/normalize-test-command.cjs /gsd-core/bin/lib/config-loader.cjs /gsd-core/bin/lib/model-resolver.cjs /gsd-core/bin/lib/loop-resolver.cjs @@ -165,6 +187,7 @@ build/ /gsd-core/bin/lib/phase-command-router.cjs /gsd-core/bin/lib/surface.cjs /gsd-core/bin/lib/gap-checker.cjs +/gsd-core/bin/lib/gate-predicate-evaluator.cjs /gsd-core/bin/lib/docs.cjs /gsd-core/bin/lib/check-command-router.cjs /gsd-core/bin/lib/frontmatter.cjs @@ -177,6 +200,7 @@ build/ /gsd-core/bin/lib/eval.cjs /gsd-core/bin/lib/eval-command-router.cjs /gsd-core/bin/lib/init-command-router.cjs +/gsd-core/bin/lib/onboard-projection.cjs /gsd-core/bin/lib/agent-command-router.cjs /gsd-core/bin/lib/agent-install-check.cjs /gsd-core/bin/lib/task-command-router.cjs @@ -215,6 +239,7 @@ tmp/ # MemPalace per-project files (issue #185) mempalace.yaml entities.json +.planning/.mempalace-stage/ # Local scratch + Claude-test artifacts .scratch/ @@ -231,3 +256,7 @@ reports/mutation/ # Local Crabbox machine configuration .crabbox.yaml .crabbox.local.yaml + +# Memtrace local daemon/runtime state (per-machine; never committed) +.memdb/ +.memtrace/ diff --git a/.kilo/plugins/gsd-core.js b/.kilo/plugins/gsd-core.js new file mode 100644 index 000000000..039277b7b --- /dev/null +++ b/.kilo/plugins/gsd-core.js @@ -0,0 +1,731 @@ +/** + * GSD plugin for OpenCode.ai (CommonJS) + * + * Architecture: SUBPROCESS REUSE. Instead of re-implementing hook logic inside + * the plugin, this file is a thin adapter that spawns the existing Claude Code + * hook scripts under hooks/ as child processes. The hooks speak a stable + * protocol (JSON on stdin, JSON + exit code on stdout); this adapter: + * 1. Translates OpenCode plugin events into Claude Code hook payloads + * 2. Spawns `node /.js` with the payload on stdin + * 3. Translates hook output back into OpenCode semantics + * - block → throw Error (OpenCode returns the error to the model) + * - advisory → output.metadata + console.error (best-effort surfacing) + * + * Namespace conversion (/gsd:xxx → /gsd-xxx) reuses scripts/fix-slash-commands.cjs + * via require(), keeping the single source of truth. + * + * ── Two distribution shapes, one adapter (issue #1914) ───────────────────── + * This single file serves both distribution paths, distinguished at load time + * by REPO_ROOT (path.resolve(__dirname, "../..")): + * + * • Option 1 — file copy (the supported GSD path). `bin/install.js` copies + * this file to /plugins/gsd-core.js, so REPO_ROOT is the + * OpenCode config dir. GSD's own install already stages `hooks/*.js` and + * `gsd-core/` there (ADR-857 skips hook *registration* for OpenCode, not the + * file copy), so the hook bridge and content rewriting resolve natively. + * Commands/agents/skills are ALREADY registered by GSD's native file copy in + * this mode, so the plugin's own config-hook registration is redundant and is + * SKIPPED (see IS_PACKAGE_TREE) to avoid double-registration. + * + * • Option 2 — package / git-spec. When loaded from the package tree (npm + * `main`, or an OpenCode git-spec install), REPO_ROOT is the package root and + * the source layout (commands/gsd/, agents/, skills/) is present. Here the + * plugin IS the sole registrar, so it registers commands/agents/skills too. + * + * IS_PACKAGE_TREE keys off the presence of the SOURCE command layout + * (commands/gsd/), which only exists in the package tree — never in an installed + * config dir (that uses the flattened command/ layout). The hook bridge and + * Read-time content rewriting run in BOTH modes; only the config-hook + * registration of commands/agents/skills is gated. + * + * Runtime-specific hooks are deliberately excluded: + * - gsd-statusline.js / gsd-update-banner.js (Claude Code statusline) + * - gsd-cursor-*.js (Cursor-specific) + * - *.sh scripts (invoked directly by commands/agents, not hook events) + */ + +"use strict"; + +const path = require("path"); +const fs = require("fs"); +const os = require("os"); +const { spawnSync } = require("child_process"); + +// Resolve REPO_ROOT to the directory that actually holds the GSD payload +// (hooks/ + gsd-core/). This must work across three physical layouts because a +// single adapter file serves both distribution shapes (see header): +// • package/git-spec tree: /.opencode/plugins/gsd-core.js → +// • global file-copy: ~/.config/opencode/plugins/gsd-core.js → ~/.config/opencode +// • local file-copy: /.opencode/plugins/gsd-core.js → /.opencode +// A fixed "../.." only works for the first; the copied layouts sit one level +// shallower. Walking up to the first ancestor containing BOTH payload markers +// resolves all three deterministically. Falls back to the package-tree +// assumption ("../..") if no ancestor matches (keeps graceful degradation). +function resolveRepoRoot(startDir) { + let dir = startDir; + for (let i = 0; i < 6; i++) { + if ( + fs.existsSync(path.join(dir, "hooks")) && + fs.existsSync(path.join(dir, "gsd-core")) + ) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; // filesystem root + dir = parent; + } + // No ancestor carried both markers (broken/partial layout — the plugin can't + // function regardless). Fall back to the package-tree assumption ("../.."), + // matching the historical fixed-depth behavior and the .opencode/plugins/ + // source layout. + return path.resolve(startDir, "../.."); +} + +// CJS: __dirname is a global, no need to derive from import.meta.url +const REPO_ROOT = resolveRepoRoot(__dirname); +const HOOKS_DIR = path.join(REPO_ROOT, "hooks"); +const COMMANDS = path.join(REPO_ROOT, "commands", "gsd"); +const AGENTS = path.join(REPO_ROOT, "agents"); +const SKILLS = path.join(REPO_ROOT, "skills"); +const GSD_CORE = path.join(REPO_ROOT, "gsd-core"); + +// True only when loaded from the package/source tree (Option 2), detected by the +// presence of the SOURCE command layout (commands/gsd/). In an installed OpenCode +// config dir (Option 1) this directory is absent — the flattened command/ layout +// is used instead — so the plugin skips its own command/agent/skill registration +// and lets GSD's native file copy own that surface (avoids double-registration). +const IS_PACKAGE_TREE = fs.existsSync(COMMANDS); + +// --------------------------------------------------------------------------- +// Namespace conversion — reuse the single source of truth +// --------------------------------------------------------------------------- + +let _cmdNames = null; +let _transformFn = null; + +/** + * Lazily load scripts/fix-slash-commands.cjs and cache the transform function + * + command name list. Returns null if the module is unavailable (the plugin + * still works, just without namespace conversion). + */ +function getNamespaceConverter() { + if (_transformFn) return _transformFn; + try { + const mod = require( + path.join(REPO_ROOT, "scripts", "fix-slash-commands.cjs"), + ); + _cmdNames = mod.readCmdNames(); + _transformFn = mod.transformContentToHyphen; + return _transformFn; + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// Session state — tracked across plugin hook invocations +// --------------------------------------------------------------------------- + +let currentSessionId = null; +let currentCwd = process.cwd(); + +// --------------------------------------------------------------------------- +// Tool name / argument mapping (OpenCode ↔ Claude Code) +// --------------------------------------------------------------------------- + +const TOOL_NAME_MAP = { + read: "Read", + write: "Write", + edit: "Edit", + apply_patch: "MultiEdit", + multi_edit: "MultiEdit", + bash: "Bash", + webfetch: "WebFetch", + web_search: "WebSearch", + websearch: "WebSearch", + task: "Task", + subagent: "Task", +}; + +function mapToolName(tool) { + if (!tool) return ""; + return TOOL_NAME_MAP[String(tool).toLowerCase()] || tool; +} + +// Build a Claude-style `tool_input` object from OpenCode's `output.args`. +function mapToolInput(args) { + const input = {}; + if (!args || typeof args !== "object") return input; + + // File-path keys (OpenCode uses filePath/path; Claude uses file_path) + const filePath = args.filePath || args.path || args.file_path; + if (filePath) input.file_path = filePath; + + // Content for Write + if (args.content !== undefined) input.content = args.content; + + // Edit patch fields + if (args.new_string !== undefined) input.new_string = args.new_string; + if (args.newString !== undefined) input.new_string = args.newString; + if (args.old_string !== undefined) input.old_string = args.old_string; + if (args.oldString !== undefined) input.old_string = args.oldString; + + // Bash command + if (args.command !== undefined) input.command = args.command; + + // Web + if (args.url !== undefined) input.url = args.url; + if (args.query !== undefined) input.query = args.query; + + return input; +} + +// --------------------------------------------------------------------------- +// Hook subprocess runner +// --------------------------------------------------------------------------- + +/** + * Spawn a Claude Code hook script and pipe a JSON payload to its stdin. + * + * Hooks follow the convention: + * - stdout: JSON object (decision/advisory) or empty + * - exit 0: allow (with optional advisory JSON on stdout) + * - exit 2: block (Claude convention; reason in stdout JSON) + * - any error: exit 0 silently (hooks swallow their own errors) + * + * @param {string} hookFile filename under hooks/, e.g. "gsd-prompt-guard.js" + * @param {object} payload stdin JSON (hook_event_name, tool_name, ...) + * @param {object} [opts] + * @param {number} [opts.timeout=8000] spawn timeout in ms + * @param {string} [opts.cwd] working directory for the child + * @returns {{ stdout: string, exitCode: number, timedOut: boolean }} + */ +function runHook(hookFile, payload, opts = {}) { + const hookPath = path.join(HOOKS_DIR, hookFile); + if (!fs.existsSync(hookPath)) { + return { stdout: "", exitCode: 0, timedOut: false }; + } + const timeout = opts.timeout ?? 8000; + let result; + try { + result = spawnSync(process.execPath, [hookPath], { + input: JSON.stringify(payload), + encoding: "utf8", + timeout, + cwd: opts.cwd || currentCwd, + windowsHide: true, + }); + } catch { + // Spawn failure — never break the tool call + return { stdout: "", exitCode: 0, timedOut: false }; + } + + const stdout = (result.stdout || "").trim(); + const exitCode = result.status == null ? 0 : result.status; + return { stdout, exitCode, timedOut: result.signal === "SIGTERM" }; +} + +// --------------------------------------------------------------------------- +// Hook output translation → OpenCode semantics +// --------------------------------------------------------------------------- + +/** + * Parse a hook's stdout and apply its effect to the OpenCode output object. + * + * - Block → throw Error(parsed.reason) so OpenCode aborts the tool call + * - Advisory→ append to output.metadata._gsdAdvisory[] and log to stderr + * - Silent → no-op + * + * @param {{ stdout: string, exitCode: number }} hookResult + * @param {object} [output] OpenCode mutable output object (optional) + */ +function handleHookResult(hookResult, output) { + const { stdout, exitCode } = hookResult; + if (!stdout && exitCode !== 2) return; // silent allow + + let parsed = null; + if (stdout) { + try { + parsed = JSON.parse(stdout); + } catch { + // Non-JSON stdout (e.g. a stray log) — treat exit 2 as hard block, else allow + } + } + + // Block: explicit decision OR Claude exit-code-2 convention + const isBlock = exitCode === 2 || (parsed && parsed.decision === "block"); + if (isBlock) { + const reason = + (parsed && parsed.reason) || "Blocked by GSD hook (no reason provided)."; + throw new Error(reason); + } + + // Advisory: inject additionalContext into metadata + log + const advisory = + parsed && + parsed.hookSpecificOutput && + parsed.hookSpecificOutput.additionalContext; + if (advisory) { + if (output) { + output.metadata = output.metadata || {}; + // Accumulate: a single tool call can run several advisory hooks in + // sequence (prompt guard, read guard, worktree guard, workflow guard). + // Storing a scalar would let a later advisory clobber an earlier one, so + // collect them all. + if (!Array.isArray(output.metadata._gsdAdvisory)) { + output.metadata._gsdAdvisory = []; + } + output.metadata._gsdAdvisory.push(advisory); + } + // Best-effort visibility when metadata isn't surfaced to the model + console.error(advisory); + } +} + +// --------------------------------------------------------------------------- +// Frontmatter helpers (for config registration) +// --------------------------------------------------------------------------- + +function parseFrontmatter(content) { + const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!m) return { frontmatter: {}, body: content }; + const fm = {}; + for (const line of m[1].split("\n")) { + const i = line.indexOf(":"); + if (i > 0) { + let v = line.slice(i + 1).trim(); + if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1); + fm[line.slice(0, i).trim()] = v; + } + } + return { frontmatter: fm, body: m[2] }; +} + +// Rewrite @~/.claude/ includes to point at the repo root. +// Also applies /gsd:xxx → /gsd-xxx namespace conversion via the shared +// transform from scripts/fix-slash-commands.cjs (single source of truth). +function rewriteRefs(content) { + let out = content.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`); + const transform = getNamespaceConverter(); + if (transform && _cmdNames && _cmdNames.length) { + out = transform(out, _cmdNames); + } + return out; +} + +function loadDir(dir, keyFn, valFn) { + const result = {}; + if (!fs.existsSync(dir)) return result; + for (const f of fs.readdirSync(dir).filter((f) => f.endsWith(".md"))) { + const raw = fs.readFileSync(path.join(dir, f), "utf8"); + const { frontmatter, body } = parseFrontmatter(raw); + result[keyFn(f)] = valFn(body, frontmatter, f); + } + return result; +} + +// --------------------------------------------------------------------------- +// Runtime content transform — for Read tool results on GSD-managed files +// --------------------------------------------------------------------------- + +// Directories whose .md files may contain ~/.claude/ paths and gsd: namespace +// refs. When the model reads these via the Read tool, we transparently rewrite +// both so OpenCode sees correct paths and hyphen-form command names. +const GSD_MANAGED_DIRS = [ + path.join(GSD_CORE, "workflows"), + path.join(GSD_CORE, "references"), + path.join(GSD_CORE, "templates"), + path.join(GSD_CORE, "contexts"), + COMMANDS, + AGENTS, + SKILLS, +]; + +function isGsdManagedFile(filePath) { + if (!filePath) return false; + const resolved = path.resolve(filePath); + return GSD_MANAGED_DIRS.some( + (dir) => resolved === dir || resolved.startsWith(dir + path.sep), + ); +} + +// Rewrite content for OpenCode consumption: +// 1. @-include paths: @~/.claude/ → @/ +// 2. plain-text paths: ~/.claude/gsd-core/ → / +// 3. namespace: gsd:xxx → gsd-xxx (via fix-slash-commands.cjs) +function rewriteContent(content) { + let out = content; + out = out.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`); + out = out.replace(/~\/\.claude\/gsd-core\//g, `${GSD_CORE}/`); + const transform = getNamespaceConverter(); + if (transform && _cmdNames && _cmdNames.length) { + out = transform(out, _cmdNames); + } + return out; +} + +// --------------------------------------------------------------------------- +// Skills cache — copy SKILL.md files with rewritten @-include paths +// --------------------------------------------------------------------------- +// +// OpenCode's skill loader reads SKILL.md files directly from disk and resolves +// @-includes internally — this bypasses our tool.execute hooks. To make +// @~/.claude/gsd-core/... includes resolve, we copy all SKILL.md files to a +// cache directory with paths rewritten to the actual GSD_CORE location. +// +// Only used in package-tree mode (Option 2). In an installed OpenCode config +// dir (Option 1) skills are already staged + registered by GSD's native file +// copy, so we never register skills from the plugin (see IS_PACKAGE_TREE). + +const SKILLS_CACHE = path.join( + os.homedir(), + ".cache", + "opencode", + "gsd-skills", +); + +function prepareSkillsCache() { + if (!fs.existsSync(SKILLS)) return null; + fs.mkdirSync(SKILLS_CACHE, { recursive: true }); + for (const dir of fs.readdirSync(SKILLS)) { + const srcFile = path.join(SKILLS, dir, "SKILL.md"); + if (!fs.existsSync(srcFile)) continue; + const raw = fs.readFileSync(srcFile, "utf8"); + // Rewrite @-include paths only; namespace conversion is handled at + // Read-time via tool.execute.after for workflow/reference files. + const rewritten = raw + .replace(/@~\/\.claude\/gsd-core\//g, `@${GSD_CORE}/`) + .replace(/~\/\.claude\/gsd-core\//g, `${GSD_CORE}/`); + const destDir = path.join(SKILLS_CACHE, dir); + fs.mkdirSync(destDir, { recursive: true }); + fs.writeFileSync(path.join(destDir, "SKILL.md"), rewritten); + } + return SKILLS_CACHE; +} + +// =========================================================================== +// Plugin entry +// =========================================================================== + +const GsdCorePlugin = async ({ directory } = {}) => { + if (directory) currentCwd = directory; + + return { + // ── Config: register commands / agents / skills paths ────────────── + // Only in package-tree mode (Option 2). In an installed config dir + // (Option 1) GSD's native file copy already registered these, so the + // plugin stays out of registration to avoid double-registering. + config: async (config) => { + if (!IS_PACKAGE_TREE) return; + + // Commands (commands/gsd/*.md → gsd-) + config.command = config.command || {}; + const cmds = loadDir( + COMMANDS, + (f) => "gsd-" + f.slice(0, -3), + (body, fm, name) => ({ + template: rewriteRefs(body.trim()), + description: fm.description || `GSD ${name.slice(0, -3)} command`, + }), + ); + for (const [k, v] of Object.entries(cmds)) { + if (!config.command[k]) config.command[k] = v; + } + + // Agents (agents/*.md) + config.agent = config.agent || {}; + const agents = loadDir( + AGENTS, + (f) => f.slice(0, -3), + (body, fm, name) => ({ + prompt: rewriteRefs(body.trim()), + description: fm.description || `GSD ${name.slice(0, -3)} agent`, + mode: fm.mode || "subagent", + }), + ); + for (const [k, v] of Object.entries(agents)) { + if (!config.agent[k]) config.agent[k] = v; + } + + // Skills — copy SKILL.md files to cache with rewritten @-include paths, + // then register the cache directory. OpenCode's skill loader reads + // SKILL.md from disk and resolves @-includes internally (bypassing our + // tool.execute hooks), so we must pre-process the files. + const skillsCache = prepareSkillsCache(); + config.skills = config.skills || {}; + config.skills.paths = config.skills.paths || []; + const skillsPath = skillsCache || SKILLS; + if (!config.skills.paths.includes(skillsPath)) { + config.skills.paths.push(skillsPath); + } + }, + + // ── shell.env ─────────────────────────────────────────────────────── + "shell.env": async (_input, output) => { + output.env = output.env || {}; + output.env.GSD_DIR = GSD_CORE; + }, + + // ── tool.execute.before — PreToolUse hooks ───────────────────────── + "tool.execute.before": async (input, output) => { + const claudeTool = mapToolName(input.tool); + const toolInput = mapToolInput(output.args || {}); + const cwd = currentCwd; + + // 0. Read path rewrite — redirect ~/.claude/gsd-core/ to actual GSD_CORE + // so the model can read workflow/reference/template files that SKILL.md + // and command templates reference via the canonical Claude path. + if (claudeTool === "Read" && toolInput.file_path) { + const original = toolInput.file_path; + const rewritten = original + .replace(/^~\/\.claude\/gsd-core\//, GSD_CORE + "/") + .replace(/(?:.*)\/\.claude\/gsd-core\//, GSD_CORE + "/"); + if (rewritten !== original) { + const args = output.args || {}; + if (args.filePath) args.filePath = rewritten; + else if (args.path) args.path = rewritten; + else if (args.file_path) args.file_path = rewritten; + else args.filePath = rewritten; + } + } + + const basePayload = { + hook_event_name: "PreToolUse", + cwd, + }; + // NOTE: session_id intentionally omitted for PreToolUse hooks. + // gsd-read-guard.js treats a non-empty session_id as a Claude Code + // session and skips its advisory. On OpenCode we WANT the advisory. + const prePayload = (overrides = {}) => ({ + ...basePayload, + tool_name: claudeTool, + tool_input: toolInput, + ...overrides, + }); + + const isWriteLike = ["Write", "Edit", "MultiEdit"].includes(claudeTool); + + // 1. gsd-prompt-guard.js — injection scan on .planning/ writes + if (claudeTool === "Write" || claudeTool === "Edit") { + const r = runHook("gsd-prompt-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 2. gsd-read-guard.js — read-before-edit advisory + if (claudeTool === "Write" || claudeTool === "Edit") { + const r = runHook("gsd-read-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 3. gsd-worktree-path-guard.js — hard-block edits outside worktree + if (isWriteLike) { + const r = runHook("gsd-worktree-path-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 4. gsd-workflow-guard.js — workflow advisory + git-force-add block + // (covers Write/Edit/MultiEdit AND Bash force-add detection) + if (isWriteLike || claudeTool === "Bash") { + const r = runHook("gsd-workflow-guard.js", prePayload()); + handleHookResult(r, output); + } + }, + + // ── tool.execute.after — PostToolUse hooks ───────────────────────── + "tool.execute.after": async (input, output) => { + const claudeTool = mapToolName(input.tool); + // NOTE: In the `after` hook, `args` lives on `input` (not `output`). + // The `output` object only has { title, output, metadata }. + const toolInput = mapToolInput(input.args || {}); + const cwd = currentCwd; + + // GSD content transform — rewrite paths + namespace in Read results + // BEFORE injection scanning so the scanner sees the final content. + if ( + claudeTool === "Read" && + output.output && + isGsdManagedFile(toolInput.file_path) + ) { + const content = + typeof output.output === "string" + ? output.output + : String(output.output); + output.output = rewriteContent(content); + } + + // gsd-read-injection-scanner.js — scan Read/WebFetch/WebSearch results + if ( + claudeTool === "Read" || + claudeTool === "WebFetch" || + claudeTool === "WebSearch" + ) { + const payload = { + hook_event_name: "PostToolUse", + tool_name: claudeTool, + tool_input: toolInput, + tool_response: output.output, + cwd, + }; + const r = runHook("gsd-read-injection-scanner.js", payload); + handleHookResult(r, output); + return; + } + + // gsd-context-monitor.js — context usage warnings (Bash/Edit/Write/Task/...) + // Only meaningful when a session_id is tracked (writes metrics sentinel). + if (currentSessionId) { + const payload = { + hook_event_name: "PostToolUse", + tool_name: claudeTool, + tool_input: toolInput, + session_id: currentSessionId, + cwd, + }; + const r = runHook("gsd-context-monitor.js", payload); + handleHookResult(r, output); + } + }, + + // ── experimental.session.compacting — PreCompact ─────────────────── + "experimental.session.compacting": async (_input, output) => { + if (!currentSessionId) return; + const payload = { + hook_event_name: "PreCompact", + session_id: currentSessionId, + cwd: currentCwd, + }; + const r = runHook("gsd-context-monitor.js", payload); + handleHookResult(r, output); + + // Also inject a GSD compaction breadcrumb (mirrors the original plugin) + output.context = output.context || []; + output.context.push( + `[GSD] Active session: ${currentSessionId}. Preserve any in-flight phase/plan state.`, + ); + }, + + // ── General event subscriptions ───────────────────────────────────── + event: async ({ event }) => { + // session.created → SessionStart hooks + if (event.type === "session.created") { + // Track session for context-monitor payloads. + // SDK type EventSessionCreated: { properties: { info: Session } } + // Session has `id` and `directory` (not `cwd`). + const info = event.properties?.info; + currentSessionId = + info?.id || event.sessionID || event.session_id || null; + if (info?.directory) currentCwd = info.directory; + + // gsd-ensure-canonical-path.js — no stdin dependency; silent + runHook("gsd-ensure-canonical-path.js", { + hook_event_name: "SessionStart", + session_id: currentSessionId, + cwd: currentCwd, + }); + // gsd-check-update.js — spawns its own background worker; no stdin + runHook("gsd-check-update.js", { + hook_event_name: "SessionStart", + session_id: currentSessionId, + cwd: currentCwd, + }); + return; + } + + // file.edited → FileChanged hook (config.json reload) + if (event.type === "file.edited") { + // SDK type EventFileEdited: { properties: { file: string } } + const filePath = event.properties?.file || event.filePath || ""; + if (!filePath.endsWith("config.json")) return; + const cwd = event.properties?.cwd || currentCwd; + const expected = path.join(cwd, ".planning", "config.json"); + if (path.resolve(filePath) !== path.resolve(expected)) return; + + const payload = { + hook_event_name: "FileChanged", + file_path: filePath, + event: "change", + cwd, + }; + const r = runHook("gsd-config-reload.js", payload); + // Advisory-only (additionalContext); surface to logs + handleHookResult(r); + return; + } + + // session.idle ↔ Claude Stop lifecycle point (#1682 Slice 1b/c). + // OpenCode fires session.idle when the run quiesces. GSD maps it to the + // Stop equivalent — the opencode-subset lifecycle peer of compaction + // (compaction preserves state across context-window summarization; idle + // marks end-of-turn). No-op sentinel today (GSD state is already + // persisted to .planning/), but it MUST be recognized so the declared + // opencode-subset surface is fully wired and a future Stop-class hook can + // attach without a plugin change. + if (event.type === "session.idle") { + return; + } + + // permission.asked / permission.replied — OpenCode permission lifecycle + // (#2087, opencode.ai/docs/plugins). GSD gates tool INPUTS at + // tool.execute.before (read-guard, injection-scanner); the permission + // grant/deny decision itself carries no GSD workflow-phase contribution, + // so these are recognized sentinels — wired so a future permission-aware + // gate can attach without a plugin change (the engine owns phase + // sequencing; this host bus is session/tool/permission-scoped, never + // phase-scoped — ADR-1239 §OpenCode). + if (event.type === "permission.asked" || event.type === "permission.replied") { + return; + } + + // session.error — OpenCode session-error lifecycle point (#2087). No GSD + // hook fires here today (loop state is already persisted to .planning/); + // recognized so the declared extension-event surface is fully wired and a + // future error-class hook can attach without a plugin change. + if (event.type === "session.error") { + return; + } + }, + }; +}; + +// Export shape — verified against OpenCode's plugin loader source +// (packages/opencode/src/plugin). The loader imports this module and runs +// `for (const entry of Object.values(mod)) { getServerPlugin(entry) }`, where +// `getServerPlugin` accepts a bare function OR an object exposing a `.server` +// function, and THROWS `TypeError("Plugin export is not a function")` for +// anything else. So EVERY enumerable value the loader iterates must be a +// function or an object with `.server`. +// +// The subtlety: depending on how OpenCode's runtime (Node or Bun) imports a +// CommonJS file, `mod` may be the raw `module.exports` OR an ESM namespace of +// the form `{ default: module.exports, ...syntheticNamedExports }`. A plain +// `module.exports = { id: "gsd-core", server }` literal risks a string `id` +// appearing in `Object.values(mod)` (as a raw property, or as a lexer- +// synthesized named export) — which would trip the throw. Two defenses: +// 1. `id` is defined NON-ENUMERABLE, so it never appears in Object.values yet +// stays readable (via property access) for the loader's identity/dedup. +// 2. `module.exports` is assigned from a VARIABLE (not an object literal), so +// cjs-module-lexer cannot statically synthesize named exports from it — +// only `default` is exposed under ESM/Bun interop. +// Result: raw-CJS `Object.values` = `[server]`; ESM `Object.values` = +// `[{server, }]` — both fully extractable. Test-only helpers hang +// off the `server` FUNCTION (`server._internals`), never as a sibling export. +GsdCorePlugin._internals = { + REPO_ROOT, + IS_PACKAGE_TREE, + mapToolName, + mapToolInput, + parseFrontmatter, + rewriteContent, + isGsdManagedFile, + handleHookResult, + GsdCorePlugin, +}; + +const gsdCorePluginExport = { server: GsdCorePlugin }; +Object.defineProperty(gsdCorePluginExport, "id", { + value: "gsd-core", + enumerable: false, + writable: false, + configurable: false, +}); +module.exports = gsdCorePluginExport; diff --git a/.opencode/plugins/gsd-core.js b/.opencode/plugins/gsd-core.js new file mode 100644 index 000000000..039277b7b --- /dev/null +++ b/.opencode/plugins/gsd-core.js @@ -0,0 +1,731 @@ +/** + * GSD plugin for OpenCode.ai (CommonJS) + * + * Architecture: SUBPROCESS REUSE. Instead of re-implementing hook logic inside + * the plugin, this file is a thin adapter that spawns the existing Claude Code + * hook scripts under hooks/ as child processes. The hooks speak a stable + * protocol (JSON on stdin, JSON + exit code on stdout); this adapter: + * 1. Translates OpenCode plugin events into Claude Code hook payloads + * 2. Spawns `node /.js` with the payload on stdin + * 3. Translates hook output back into OpenCode semantics + * - block → throw Error (OpenCode returns the error to the model) + * - advisory → output.metadata + console.error (best-effort surfacing) + * + * Namespace conversion (/gsd:xxx → /gsd-xxx) reuses scripts/fix-slash-commands.cjs + * via require(), keeping the single source of truth. + * + * ── Two distribution shapes, one adapter (issue #1914) ───────────────────── + * This single file serves both distribution paths, distinguished at load time + * by REPO_ROOT (path.resolve(__dirname, "../..")): + * + * • Option 1 — file copy (the supported GSD path). `bin/install.js` copies + * this file to /plugins/gsd-core.js, so REPO_ROOT is the + * OpenCode config dir. GSD's own install already stages `hooks/*.js` and + * `gsd-core/` there (ADR-857 skips hook *registration* for OpenCode, not the + * file copy), so the hook bridge and content rewriting resolve natively. + * Commands/agents/skills are ALREADY registered by GSD's native file copy in + * this mode, so the plugin's own config-hook registration is redundant and is + * SKIPPED (see IS_PACKAGE_TREE) to avoid double-registration. + * + * • Option 2 — package / git-spec. When loaded from the package tree (npm + * `main`, or an OpenCode git-spec install), REPO_ROOT is the package root and + * the source layout (commands/gsd/, agents/, skills/) is present. Here the + * plugin IS the sole registrar, so it registers commands/agents/skills too. + * + * IS_PACKAGE_TREE keys off the presence of the SOURCE command layout + * (commands/gsd/), which only exists in the package tree — never in an installed + * config dir (that uses the flattened command/ layout). The hook bridge and + * Read-time content rewriting run in BOTH modes; only the config-hook + * registration of commands/agents/skills is gated. + * + * Runtime-specific hooks are deliberately excluded: + * - gsd-statusline.js / gsd-update-banner.js (Claude Code statusline) + * - gsd-cursor-*.js (Cursor-specific) + * - *.sh scripts (invoked directly by commands/agents, not hook events) + */ + +"use strict"; + +const path = require("path"); +const fs = require("fs"); +const os = require("os"); +const { spawnSync } = require("child_process"); + +// Resolve REPO_ROOT to the directory that actually holds the GSD payload +// (hooks/ + gsd-core/). This must work across three physical layouts because a +// single adapter file serves both distribution shapes (see header): +// • package/git-spec tree: /.opencode/plugins/gsd-core.js → +// • global file-copy: ~/.config/opencode/plugins/gsd-core.js → ~/.config/opencode +// • local file-copy: /.opencode/plugins/gsd-core.js → /.opencode +// A fixed "../.." only works for the first; the copied layouts sit one level +// shallower. Walking up to the first ancestor containing BOTH payload markers +// resolves all three deterministically. Falls back to the package-tree +// assumption ("../..") if no ancestor matches (keeps graceful degradation). +function resolveRepoRoot(startDir) { + let dir = startDir; + for (let i = 0; i < 6; i++) { + if ( + fs.existsSync(path.join(dir, "hooks")) && + fs.existsSync(path.join(dir, "gsd-core")) + ) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; // filesystem root + dir = parent; + } + // No ancestor carried both markers (broken/partial layout — the plugin can't + // function regardless). Fall back to the package-tree assumption ("../.."), + // matching the historical fixed-depth behavior and the .opencode/plugins/ + // source layout. + return path.resolve(startDir, "../.."); +} + +// CJS: __dirname is a global, no need to derive from import.meta.url +const REPO_ROOT = resolveRepoRoot(__dirname); +const HOOKS_DIR = path.join(REPO_ROOT, "hooks"); +const COMMANDS = path.join(REPO_ROOT, "commands", "gsd"); +const AGENTS = path.join(REPO_ROOT, "agents"); +const SKILLS = path.join(REPO_ROOT, "skills"); +const GSD_CORE = path.join(REPO_ROOT, "gsd-core"); + +// True only when loaded from the package/source tree (Option 2), detected by the +// presence of the SOURCE command layout (commands/gsd/). In an installed OpenCode +// config dir (Option 1) this directory is absent — the flattened command/ layout +// is used instead — so the plugin skips its own command/agent/skill registration +// and lets GSD's native file copy own that surface (avoids double-registration). +const IS_PACKAGE_TREE = fs.existsSync(COMMANDS); + +// --------------------------------------------------------------------------- +// Namespace conversion — reuse the single source of truth +// --------------------------------------------------------------------------- + +let _cmdNames = null; +let _transformFn = null; + +/** + * Lazily load scripts/fix-slash-commands.cjs and cache the transform function + * + command name list. Returns null if the module is unavailable (the plugin + * still works, just without namespace conversion). + */ +function getNamespaceConverter() { + if (_transformFn) return _transformFn; + try { + const mod = require( + path.join(REPO_ROOT, "scripts", "fix-slash-commands.cjs"), + ); + _cmdNames = mod.readCmdNames(); + _transformFn = mod.transformContentToHyphen; + return _transformFn; + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// Session state — tracked across plugin hook invocations +// --------------------------------------------------------------------------- + +let currentSessionId = null; +let currentCwd = process.cwd(); + +// --------------------------------------------------------------------------- +// Tool name / argument mapping (OpenCode ↔ Claude Code) +// --------------------------------------------------------------------------- + +const TOOL_NAME_MAP = { + read: "Read", + write: "Write", + edit: "Edit", + apply_patch: "MultiEdit", + multi_edit: "MultiEdit", + bash: "Bash", + webfetch: "WebFetch", + web_search: "WebSearch", + websearch: "WebSearch", + task: "Task", + subagent: "Task", +}; + +function mapToolName(tool) { + if (!tool) return ""; + return TOOL_NAME_MAP[String(tool).toLowerCase()] || tool; +} + +// Build a Claude-style `tool_input` object from OpenCode's `output.args`. +function mapToolInput(args) { + const input = {}; + if (!args || typeof args !== "object") return input; + + // File-path keys (OpenCode uses filePath/path; Claude uses file_path) + const filePath = args.filePath || args.path || args.file_path; + if (filePath) input.file_path = filePath; + + // Content for Write + if (args.content !== undefined) input.content = args.content; + + // Edit patch fields + if (args.new_string !== undefined) input.new_string = args.new_string; + if (args.newString !== undefined) input.new_string = args.newString; + if (args.old_string !== undefined) input.old_string = args.old_string; + if (args.oldString !== undefined) input.old_string = args.oldString; + + // Bash command + if (args.command !== undefined) input.command = args.command; + + // Web + if (args.url !== undefined) input.url = args.url; + if (args.query !== undefined) input.query = args.query; + + return input; +} + +// --------------------------------------------------------------------------- +// Hook subprocess runner +// --------------------------------------------------------------------------- + +/** + * Spawn a Claude Code hook script and pipe a JSON payload to its stdin. + * + * Hooks follow the convention: + * - stdout: JSON object (decision/advisory) or empty + * - exit 0: allow (with optional advisory JSON on stdout) + * - exit 2: block (Claude convention; reason in stdout JSON) + * - any error: exit 0 silently (hooks swallow their own errors) + * + * @param {string} hookFile filename under hooks/, e.g. "gsd-prompt-guard.js" + * @param {object} payload stdin JSON (hook_event_name, tool_name, ...) + * @param {object} [opts] + * @param {number} [opts.timeout=8000] spawn timeout in ms + * @param {string} [opts.cwd] working directory for the child + * @returns {{ stdout: string, exitCode: number, timedOut: boolean }} + */ +function runHook(hookFile, payload, opts = {}) { + const hookPath = path.join(HOOKS_DIR, hookFile); + if (!fs.existsSync(hookPath)) { + return { stdout: "", exitCode: 0, timedOut: false }; + } + const timeout = opts.timeout ?? 8000; + let result; + try { + result = spawnSync(process.execPath, [hookPath], { + input: JSON.stringify(payload), + encoding: "utf8", + timeout, + cwd: opts.cwd || currentCwd, + windowsHide: true, + }); + } catch { + // Spawn failure — never break the tool call + return { stdout: "", exitCode: 0, timedOut: false }; + } + + const stdout = (result.stdout || "").trim(); + const exitCode = result.status == null ? 0 : result.status; + return { stdout, exitCode, timedOut: result.signal === "SIGTERM" }; +} + +// --------------------------------------------------------------------------- +// Hook output translation → OpenCode semantics +// --------------------------------------------------------------------------- + +/** + * Parse a hook's stdout and apply its effect to the OpenCode output object. + * + * - Block → throw Error(parsed.reason) so OpenCode aborts the tool call + * - Advisory→ append to output.metadata._gsdAdvisory[] and log to stderr + * - Silent → no-op + * + * @param {{ stdout: string, exitCode: number }} hookResult + * @param {object} [output] OpenCode mutable output object (optional) + */ +function handleHookResult(hookResult, output) { + const { stdout, exitCode } = hookResult; + if (!stdout && exitCode !== 2) return; // silent allow + + let parsed = null; + if (stdout) { + try { + parsed = JSON.parse(stdout); + } catch { + // Non-JSON stdout (e.g. a stray log) — treat exit 2 as hard block, else allow + } + } + + // Block: explicit decision OR Claude exit-code-2 convention + const isBlock = exitCode === 2 || (parsed && parsed.decision === "block"); + if (isBlock) { + const reason = + (parsed && parsed.reason) || "Blocked by GSD hook (no reason provided)."; + throw new Error(reason); + } + + // Advisory: inject additionalContext into metadata + log + const advisory = + parsed && + parsed.hookSpecificOutput && + parsed.hookSpecificOutput.additionalContext; + if (advisory) { + if (output) { + output.metadata = output.metadata || {}; + // Accumulate: a single tool call can run several advisory hooks in + // sequence (prompt guard, read guard, worktree guard, workflow guard). + // Storing a scalar would let a later advisory clobber an earlier one, so + // collect them all. + if (!Array.isArray(output.metadata._gsdAdvisory)) { + output.metadata._gsdAdvisory = []; + } + output.metadata._gsdAdvisory.push(advisory); + } + // Best-effort visibility when metadata isn't surfaced to the model + console.error(advisory); + } +} + +// --------------------------------------------------------------------------- +// Frontmatter helpers (for config registration) +// --------------------------------------------------------------------------- + +function parseFrontmatter(content) { + const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!m) return { frontmatter: {}, body: content }; + const fm = {}; + for (const line of m[1].split("\n")) { + const i = line.indexOf(":"); + if (i > 0) { + let v = line.slice(i + 1).trim(); + if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1); + fm[line.slice(0, i).trim()] = v; + } + } + return { frontmatter: fm, body: m[2] }; +} + +// Rewrite @~/.claude/ includes to point at the repo root. +// Also applies /gsd:xxx → /gsd-xxx namespace conversion via the shared +// transform from scripts/fix-slash-commands.cjs (single source of truth). +function rewriteRefs(content) { + let out = content.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`); + const transform = getNamespaceConverter(); + if (transform && _cmdNames && _cmdNames.length) { + out = transform(out, _cmdNames); + } + return out; +} + +function loadDir(dir, keyFn, valFn) { + const result = {}; + if (!fs.existsSync(dir)) return result; + for (const f of fs.readdirSync(dir).filter((f) => f.endsWith(".md"))) { + const raw = fs.readFileSync(path.join(dir, f), "utf8"); + const { frontmatter, body } = parseFrontmatter(raw); + result[keyFn(f)] = valFn(body, frontmatter, f); + } + return result; +} + +// --------------------------------------------------------------------------- +// Runtime content transform — for Read tool results on GSD-managed files +// --------------------------------------------------------------------------- + +// Directories whose .md files may contain ~/.claude/ paths and gsd: namespace +// refs. When the model reads these via the Read tool, we transparently rewrite +// both so OpenCode sees correct paths and hyphen-form command names. +const GSD_MANAGED_DIRS = [ + path.join(GSD_CORE, "workflows"), + path.join(GSD_CORE, "references"), + path.join(GSD_CORE, "templates"), + path.join(GSD_CORE, "contexts"), + COMMANDS, + AGENTS, + SKILLS, +]; + +function isGsdManagedFile(filePath) { + if (!filePath) return false; + const resolved = path.resolve(filePath); + return GSD_MANAGED_DIRS.some( + (dir) => resolved === dir || resolved.startsWith(dir + path.sep), + ); +} + +// Rewrite content for OpenCode consumption: +// 1. @-include paths: @~/.claude/ → @/ +// 2. plain-text paths: ~/.claude/gsd-core/ → / +// 3. namespace: gsd:xxx → gsd-xxx (via fix-slash-commands.cjs) +function rewriteContent(content) { + let out = content; + out = out.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`); + out = out.replace(/~\/\.claude\/gsd-core\//g, `${GSD_CORE}/`); + const transform = getNamespaceConverter(); + if (transform && _cmdNames && _cmdNames.length) { + out = transform(out, _cmdNames); + } + return out; +} + +// --------------------------------------------------------------------------- +// Skills cache — copy SKILL.md files with rewritten @-include paths +// --------------------------------------------------------------------------- +// +// OpenCode's skill loader reads SKILL.md files directly from disk and resolves +// @-includes internally — this bypasses our tool.execute hooks. To make +// @~/.claude/gsd-core/... includes resolve, we copy all SKILL.md files to a +// cache directory with paths rewritten to the actual GSD_CORE location. +// +// Only used in package-tree mode (Option 2). In an installed OpenCode config +// dir (Option 1) skills are already staged + registered by GSD's native file +// copy, so we never register skills from the plugin (see IS_PACKAGE_TREE). + +const SKILLS_CACHE = path.join( + os.homedir(), + ".cache", + "opencode", + "gsd-skills", +); + +function prepareSkillsCache() { + if (!fs.existsSync(SKILLS)) return null; + fs.mkdirSync(SKILLS_CACHE, { recursive: true }); + for (const dir of fs.readdirSync(SKILLS)) { + const srcFile = path.join(SKILLS, dir, "SKILL.md"); + if (!fs.existsSync(srcFile)) continue; + const raw = fs.readFileSync(srcFile, "utf8"); + // Rewrite @-include paths only; namespace conversion is handled at + // Read-time via tool.execute.after for workflow/reference files. + const rewritten = raw + .replace(/@~\/\.claude\/gsd-core\//g, `@${GSD_CORE}/`) + .replace(/~\/\.claude\/gsd-core\//g, `${GSD_CORE}/`); + const destDir = path.join(SKILLS_CACHE, dir); + fs.mkdirSync(destDir, { recursive: true }); + fs.writeFileSync(path.join(destDir, "SKILL.md"), rewritten); + } + return SKILLS_CACHE; +} + +// =========================================================================== +// Plugin entry +// =========================================================================== + +const GsdCorePlugin = async ({ directory } = {}) => { + if (directory) currentCwd = directory; + + return { + // ── Config: register commands / agents / skills paths ────────────── + // Only in package-tree mode (Option 2). In an installed config dir + // (Option 1) GSD's native file copy already registered these, so the + // plugin stays out of registration to avoid double-registering. + config: async (config) => { + if (!IS_PACKAGE_TREE) return; + + // Commands (commands/gsd/*.md → gsd-) + config.command = config.command || {}; + const cmds = loadDir( + COMMANDS, + (f) => "gsd-" + f.slice(0, -3), + (body, fm, name) => ({ + template: rewriteRefs(body.trim()), + description: fm.description || `GSD ${name.slice(0, -3)} command`, + }), + ); + for (const [k, v] of Object.entries(cmds)) { + if (!config.command[k]) config.command[k] = v; + } + + // Agents (agents/*.md) + config.agent = config.agent || {}; + const agents = loadDir( + AGENTS, + (f) => f.slice(0, -3), + (body, fm, name) => ({ + prompt: rewriteRefs(body.trim()), + description: fm.description || `GSD ${name.slice(0, -3)} agent`, + mode: fm.mode || "subagent", + }), + ); + for (const [k, v] of Object.entries(agents)) { + if (!config.agent[k]) config.agent[k] = v; + } + + // Skills — copy SKILL.md files to cache with rewritten @-include paths, + // then register the cache directory. OpenCode's skill loader reads + // SKILL.md from disk and resolves @-includes internally (bypassing our + // tool.execute hooks), so we must pre-process the files. + const skillsCache = prepareSkillsCache(); + config.skills = config.skills || {}; + config.skills.paths = config.skills.paths || []; + const skillsPath = skillsCache || SKILLS; + if (!config.skills.paths.includes(skillsPath)) { + config.skills.paths.push(skillsPath); + } + }, + + // ── shell.env ─────────────────────────────────────────────────────── + "shell.env": async (_input, output) => { + output.env = output.env || {}; + output.env.GSD_DIR = GSD_CORE; + }, + + // ── tool.execute.before — PreToolUse hooks ───────────────────────── + "tool.execute.before": async (input, output) => { + const claudeTool = mapToolName(input.tool); + const toolInput = mapToolInput(output.args || {}); + const cwd = currentCwd; + + // 0. Read path rewrite — redirect ~/.claude/gsd-core/ to actual GSD_CORE + // so the model can read workflow/reference/template files that SKILL.md + // and command templates reference via the canonical Claude path. + if (claudeTool === "Read" && toolInput.file_path) { + const original = toolInput.file_path; + const rewritten = original + .replace(/^~\/\.claude\/gsd-core\//, GSD_CORE + "/") + .replace(/(?:.*)\/\.claude\/gsd-core\//, GSD_CORE + "/"); + if (rewritten !== original) { + const args = output.args || {}; + if (args.filePath) args.filePath = rewritten; + else if (args.path) args.path = rewritten; + else if (args.file_path) args.file_path = rewritten; + else args.filePath = rewritten; + } + } + + const basePayload = { + hook_event_name: "PreToolUse", + cwd, + }; + // NOTE: session_id intentionally omitted for PreToolUse hooks. + // gsd-read-guard.js treats a non-empty session_id as a Claude Code + // session and skips its advisory. On OpenCode we WANT the advisory. + const prePayload = (overrides = {}) => ({ + ...basePayload, + tool_name: claudeTool, + tool_input: toolInput, + ...overrides, + }); + + const isWriteLike = ["Write", "Edit", "MultiEdit"].includes(claudeTool); + + // 1. gsd-prompt-guard.js — injection scan on .planning/ writes + if (claudeTool === "Write" || claudeTool === "Edit") { + const r = runHook("gsd-prompt-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 2. gsd-read-guard.js — read-before-edit advisory + if (claudeTool === "Write" || claudeTool === "Edit") { + const r = runHook("gsd-read-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 3. gsd-worktree-path-guard.js — hard-block edits outside worktree + if (isWriteLike) { + const r = runHook("gsd-worktree-path-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 4. gsd-workflow-guard.js — workflow advisory + git-force-add block + // (covers Write/Edit/MultiEdit AND Bash force-add detection) + if (isWriteLike || claudeTool === "Bash") { + const r = runHook("gsd-workflow-guard.js", prePayload()); + handleHookResult(r, output); + } + }, + + // ── tool.execute.after — PostToolUse hooks ───────────────────────── + "tool.execute.after": async (input, output) => { + const claudeTool = mapToolName(input.tool); + // NOTE: In the `after` hook, `args` lives on `input` (not `output`). + // The `output` object only has { title, output, metadata }. + const toolInput = mapToolInput(input.args || {}); + const cwd = currentCwd; + + // GSD content transform — rewrite paths + namespace in Read results + // BEFORE injection scanning so the scanner sees the final content. + if ( + claudeTool === "Read" && + output.output && + isGsdManagedFile(toolInput.file_path) + ) { + const content = + typeof output.output === "string" + ? output.output + : String(output.output); + output.output = rewriteContent(content); + } + + // gsd-read-injection-scanner.js — scan Read/WebFetch/WebSearch results + if ( + claudeTool === "Read" || + claudeTool === "WebFetch" || + claudeTool === "WebSearch" + ) { + const payload = { + hook_event_name: "PostToolUse", + tool_name: claudeTool, + tool_input: toolInput, + tool_response: output.output, + cwd, + }; + const r = runHook("gsd-read-injection-scanner.js", payload); + handleHookResult(r, output); + return; + } + + // gsd-context-monitor.js — context usage warnings (Bash/Edit/Write/Task/...) + // Only meaningful when a session_id is tracked (writes metrics sentinel). + if (currentSessionId) { + const payload = { + hook_event_name: "PostToolUse", + tool_name: claudeTool, + tool_input: toolInput, + session_id: currentSessionId, + cwd, + }; + const r = runHook("gsd-context-monitor.js", payload); + handleHookResult(r, output); + } + }, + + // ── experimental.session.compacting — PreCompact ─────────────────── + "experimental.session.compacting": async (_input, output) => { + if (!currentSessionId) return; + const payload = { + hook_event_name: "PreCompact", + session_id: currentSessionId, + cwd: currentCwd, + }; + const r = runHook("gsd-context-monitor.js", payload); + handleHookResult(r, output); + + // Also inject a GSD compaction breadcrumb (mirrors the original plugin) + output.context = output.context || []; + output.context.push( + `[GSD] Active session: ${currentSessionId}. Preserve any in-flight phase/plan state.`, + ); + }, + + // ── General event subscriptions ───────────────────────────────────── + event: async ({ event }) => { + // session.created → SessionStart hooks + if (event.type === "session.created") { + // Track session for context-monitor payloads. + // SDK type EventSessionCreated: { properties: { info: Session } } + // Session has `id` and `directory` (not `cwd`). + const info = event.properties?.info; + currentSessionId = + info?.id || event.sessionID || event.session_id || null; + if (info?.directory) currentCwd = info.directory; + + // gsd-ensure-canonical-path.js — no stdin dependency; silent + runHook("gsd-ensure-canonical-path.js", { + hook_event_name: "SessionStart", + session_id: currentSessionId, + cwd: currentCwd, + }); + // gsd-check-update.js — spawns its own background worker; no stdin + runHook("gsd-check-update.js", { + hook_event_name: "SessionStart", + session_id: currentSessionId, + cwd: currentCwd, + }); + return; + } + + // file.edited → FileChanged hook (config.json reload) + if (event.type === "file.edited") { + // SDK type EventFileEdited: { properties: { file: string } } + const filePath = event.properties?.file || event.filePath || ""; + if (!filePath.endsWith("config.json")) return; + const cwd = event.properties?.cwd || currentCwd; + const expected = path.join(cwd, ".planning", "config.json"); + if (path.resolve(filePath) !== path.resolve(expected)) return; + + const payload = { + hook_event_name: "FileChanged", + file_path: filePath, + event: "change", + cwd, + }; + const r = runHook("gsd-config-reload.js", payload); + // Advisory-only (additionalContext); surface to logs + handleHookResult(r); + return; + } + + // session.idle ↔ Claude Stop lifecycle point (#1682 Slice 1b/c). + // OpenCode fires session.idle when the run quiesces. GSD maps it to the + // Stop equivalent — the opencode-subset lifecycle peer of compaction + // (compaction preserves state across context-window summarization; idle + // marks end-of-turn). No-op sentinel today (GSD state is already + // persisted to .planning/), but it MUST be recognized so the declared + // opencode-subset surface is fully wired and a future Stop-class hook can + // attach without a plugin change. + if (event.type === "session.idle") { + return; + } + + // permission.asked / permission.replied — OpenCode permission lifecycle + // (#2087, opencode.ai/docs/plugins). GSD gates tool INPUTS at + // tool.execute.before (read-guard, injection-scanner); the permission + // grant/deny decision itself carries no GSD workflow-phase contribution, + // so these are recognized sentinels — wired so a future permission-aware + // gate can attach without a plugin change (the engine owns phase + // sequencing; this host bus is session/tool/permission-scoped, never + // phase-scoped — ADR-1239 §OpenCode). + if (event.type === "permission.asked" || event.type === "permission.replied") { + return; + } + + // session.error — OpenCode session-error lifecycle point (#2087). No GSD + // hook fires here today (loop state is already persisted to .planning/); + // recognized so the declared extension-event surface is fully wired and a + // future error-class hook can attach without a plugin change. + if (event.type === "session.error") { + return; + } + }, + }; +}; + +// Export shape — verified against OpenCode's plugin loader source +// (packages/opencode/src/plugin). The loader imports this module and runs +// `for (const entry of Object.values(mod)) { getServerPlugin(entry) }`, where +// `getServerPlugin` accepts a bare function OR an object exposing a `.server` +// function, and THROWS `TypeError("Plugin export is not a function")` for +// anything else. So EVERY enumerable value the loader iterates must be a +// function or an object with `.server`. +// +// The subtlety: depending on how OpenCode's runtime (Node or Bun) imports a +// CommonJS file, `mod` may be the raw `module.exports` OR an ESM namespace of +// the form `{ default: module.exports, ...syntheticNamedExports }`. A plain +// `module.exports = { id: "gsd-core", server }` literal risks a string `id` +// appearing in `Object.values(mod)` (as a raw property, or as a lexer- +// synthesized named export) — which would trip the throw. Two defenses: +// 1. `id` is defined NON-ENUMERABLE, so it never appears in Object.values yet +// stays readable (via property access) for the loader's identity/dedup. +// 2. `module.exports` is assigned from a VARIABLE (not an object literal), so +// cjs-module-lexer cannot statically synthesize named exports from it — +// only `default` is exposed under ESM/Bun interop. +// Result: raw-CJS `Object.values` = `[server]`; ESM `Object.values` = +// `[{server, }]` — both fully extractable. Test-only helpers hang +// off the `server` FUNCTION (`server._internals`), never as a sibling export. +GsdCorePlugin._internals = { + REPO_ROOT, + IS_PACKAGE_TREE, + mapToolName, + mapToolInput, + parseFrontmatter, + rewriteContent, + isGsdManagedFile, + handleHookResult, + GsdCorePlugin, +}; + +const gsdCorePluginExport = { server: GsdCorePlugin }; +Object.defineProperty(gsdCorePluginExport, "id", { + value: "gsd-core", + enumerable: false, + writable: false, + configurable: false, +}); +module.exports = gsdCorePluginExport; diff --git a/.out-of-scope/plan-md-execution-context-portability.md b/.out-of-scope/plan-md-execution-context-portability.md new file mode 100644 index 000000000..e5989acd6 --- /dev/null +++ b/.out-of-scope/plan-md-execution-context-portability.md @@ -0,0 +1,42 @@ +# Clone-Portable `` in Committed PLAN.md + +GSD does not make the `` block in committed `PLAN.md` files +clone-portable — it does not rewrite the planner's install-relative +`@…/gsd-core/…` references into repository-relative or install-neutral paths so +that a committed plan reads identically across developers, machines, or runtimes. + +## Why this is out of scope + +PLAN.md is a **machine artifact**, not a human- or clone-facing document — the +same principle that governs [plan-md-human-rendering.md](./plan-md-human-rendering.md) +(from #2158). A committed PLAN.md is a per-run agent instruction set, produced by +`gsd-planner` and consumed in place by `gsd-executor`, `gsd-plan-checker`, and +`gsd-verifier`. There is no documented step in which a plan is read on another +machine after `git clone` without a local GSD install. + +Under that model, ``'s `@` references point at the reader's +own local GSD install (Claude `~/.claude/gsd-core/…`, Cursor `.cursor/gsd-core/…`, +or an absolute path for a `--local` install). They are install-relative by design, +and the executor loads those workflows from its own installed copy — it never +consumes the paths a *different* machine wrote into a committed plan. +`/gsd-execute-phase` builds its own `` inline from the +orchestrator's installed workflow (`workflows/execute-phase.md`), so the block a +planner writes into a committed plan does not gate execution anywhere. + +Making committed plans clone-portable would treat PLAN.md as a shared +cross-developer document — the boundary #2158 declined to cross — for a block that +no consumer reads across machines. + +**Revisit if** GSD introduces a documented cross-developer / cross-machine contract +for committed PLAN.md — a human- or teammate-facing use where plans are read after +`git clone` without a local install — at which point `` +portability becomes in scope. + +## Prior requests + +- #2238 — "Planner embeds machine-specific gsd-core paths in committed PLAN.md execution_context" + +## Related + +- `.out-of-scope/plan-md-human-rendering.md` — #2158, the governing "PLAN.md is a machine artifact" precedent. +- `docs/reference/plan-md.md` — the `` reference, which describes the install-relative behaviour. diff --git a/.out-of-scope/plan-md-human-rendering.md b/.out-of-scope/plan-md-human-rendering.md new file mode 100644 index 000000000..d73c1c0e9 --- /dev/null +++ b/.out-of-scope/plan-md-human-rendering.md @@ -0,0 +1,63 @@ +# Human-Readable Rendering of PLAN.md + +GSD does not change PLAN.md's structural tag convention (``, ``, +``, ``, ``, etc.) to improve how a PLAN.md renders when a +human opens the raw file in a markdown viewer on GitHub/GitLab. + +## Why this is out of scope + +PLAN.md is a **machine artifact**, not a human-facing document. The docs are +explicit: + +- `docs/reference/plan-md.md` — a PLAN.md is *"an executable unit of work — a + structured document that tells an executor agent exactly what to build and how + to verify it was built correctly."* +- `agents/gsd-planner.md` — *"Produce PLAN.md files that Claude executors can + implement without interpretation. Plans are prompts, not documents that become + prompts."* + +PLAN.md is produced by `gsd-planner` and consumed by `gsd-executor`, +`gsd-plan-checker`, `gsd-verifier`, and cross-AI review agents. There is no +documented step in which a human opens, reads, reviews, or signs off on a +PLAN.md — unlike `SUMMARY.md` / `VERIFICATION.md`, which are produced for human +validation. + +The reported symptom — alphabet-only tags like `` / `` tripping +CommonMark's HTML-block rule so inner markdown renders as cramped run-on text — +only manifests when a human views the raw file in a markdown renderer. It does +**not** affect either machine consumer: + +- Tag location/extraction is regex-based (`extractTaggedBlocks` in + `src/markdown-sectionizer.cts`), operating on raw text, not rendered HTML. +- Agents read the raw file content, not a rendered view. + +```js +// The extraction contract is a raw-text regex, indifferent to CommonMark +// HTML-block folding: +new RegExp(`<${escapedTag}>([\\s\\S]*?)`, 'g') +``` + +The proposed fixes (HTML-comment markers ``, or underscored tag +names ``) would change a load-bearing machine convention that is +duplicated across ~6 surfaces — the extractor regex, the `execute-plan` grep +counter, `verify.cjs`, `decisions.cjs`, and the planner/executor schema docs. A +drift between those surfaces silently breaks plan extraction (the executor finds +zero tasks), which is a far worse failure than cosmetic rendering. The +underscore option additionally increases token consumption on every plan read +(longer tag names, repeated across every PLAN.md, read in full by the executor) +and leaves the marker names visible as literal noise in any rendered view. +Incurring that cost and risk to improve a rendering path that is not a +documented use of PLAN.md does not align with the project's model of PLAN.md as +an agent instruction set. + +The same reasoning covers the report's secondary point (unquoted `|` in PLAN.md +frontmatter breaking rendered markdown tables): that too is a human-render +concern for a machine artifact. + +**Revisit if** GSD ever introduces a human-review gate for PLAN.md — a step +where a person reads and approves the plan before execution. At that point +PLAN.md gains a documented human audience and its rendering becomes in-scope. + +## Prior requests + +- #2158 — "PLAN.md XML task tags trigger CommonMark HTML-block rule — task content renders as cramped run-on text" diff --git a/.out-of-scope/statusline-account-usage.md b/.out-of-scope/statusline-account-usage.md new file mode 100644 index 000000000..39c3b7adb --- /dev/null +++ b/.out-of-scope/statusline-account-usage.md @@ -0,0 +1,38 @@ +# Statusline Account / Usage Segment (credential-reading, external API) + +GSD's statusline does not read credentials or call external network APIs to +display account-level resource state (5-hour / 7-day rate-limit utilization, +usage windows, plan quotas). + +## Why this is out of scope + +The statusline draws its data boundary at **local, read-only** sources — see +[`docs/adr/2164-statusline-scope-boundary.md`](../docs/adr/2164-statusline-scope-boundary.md). +It refines the stdin payload Claude Code already sends (model, context meter, +GSD-state) and may add a new *local* source (e.g. `git`), but it does not: + +- read Claude Code's OAuth credentials (`.credentials.json`, or the macOS login + Keychain via `security`), or +- make authenticated network calls (e.g. `https://api.anthropic.com/api/oauth/usage`) + to fetch data. + +Reasons: + +- **Trust surface.** A planning-workflow hook reading an OAuth token is a + materially larger trust surface than any rendering concern — even read-only, + never-logged, and opt-in. Credential custody belongs to the platform, not to + a markdown planning tool. +- **Unstable dependency.** The usage endpoint is undocumented; it can change or + disappear and silently rot the feature. +- **Scope.** Surfacing account/rate-limit state is a platform (Claude Code) + concern. This matches the prior in + [`temporal-context.md`](./temporal-context.md): *"Statusline / TUI re-entry is + platform-level, not GSD-level."* + +**Revisit if** a documented, first-party usage API — or a platform-provided +value delivered to the hook without GSD reading credentials — becomes +available. That would move usage display out of the excluded tier. + +## Prior requests + +- #2164 — "enhancement(statusline): opt-in 5-hour/7-day account usage segment" diff --git a/.pr-body-2100.md b/.pr-body-2100.md new file mode 100644 index 000000000..082c3cbe1 --- /dev/null +++ b/.pr-body-2100.md @@ -0,0 +1,71 @@ +## Linked Issue + +Closes #2100 + +The linked issue carries the `approved-feature` label. + +--- + +## Feature summary + +Migrates **Windsurf** onto the ADR-1239 Embeddable Orchestration System — the largest of the EoS migrations. Folds all 10 residual `isWindsurf` branches onto the capability descriptor **and** wires GSD's write/command safety guards into Windsurf/Cascade's native blocking hook bus. + +## What changed (highlights) + +| File | What changed | +|------|-------------| +| `bin/install.js` | Folded 10 `isWindsurf` sites onto `hostBehaviors` (skipSharedHooksInstall, legacyDevinSkillsCleanup, installsCommandBodiesForWorkflowDelegation [#1629], verificationStyle); dropped 2 dead destructures + the dead `else if (isWindsurf)` agent arm; wired the Cascade hook-bridge install/uninstall; corrected stale comments | +| `capabilities/windsurf/capability.json` | `hostBehaviors` block; `hooksSurface: "none"` → `"windsurf-hooks-json"` | +| `src/runtime-hooks-surface.cts` | `writeWindsurfHooksJson`/`reconcileWindsurfHooksJson`/`removeWindsurfHooksJson` (Cursor-templated, Cascade's flat `{hooks:{:[{command}]}}` shape) + event/script constants | +| `hooks/gsd-windsurf-pre-write.js`, `gsd-windsurf-pre-command.js` | **New** Cascade-native blocking guard scripts (stdin JSON, exit-code-2 blocking) | +| `gsd-core/bin/lib/capability-validator.cjs`, `src/runtime-config-adapter-registry.cts` | `windsurf-hooks-json` added to `VALID_HOOKS_SURFACES`, GATE A's `profile-marker-only` allowlist, and the `HooksSurface` union | +| managed-hooks-registry / build-hooks / INVENTORY | registered the 2 new guard scripts | +| tests / docs / changeset | `declarative-reference-windsurf` + `windsurf-hooks-bridge` (live blocking); matrix hookBus delta; changeset (`Changed`) | + +## Implementation notes + +- **Byte-parity concretely verified** (via the review): a real windsurf install rebuilt through the golden-parity harness → 327 files, 0 drift; only `windsurf.json` gains the 2 new script hashes. cursor/trae re-verified 0 drift. The load-bearing #1629 command-body copy (`.windsurf/gsd-core/commands/gsd/*.md` for local installs) is intact. +- **The hook-bridge is faithful, not padding.** Cascade's `hooks.json` genuinely supports blocking via exit code 2 (confirmed against docs.windsurf.com / docs.devin.ai). Only **2 of GSD's 6 guards** faithfully map — the worktree-path guard (→ `pre_write_code`) and a destructive-command guard (→ `pre_run_command`). The 4 advisory guards + `pre_mcp_tool_use` + the 5 `post_*` logging events are **deliberately not wired**: Cascade's hook bus has no context-injection channel to carry GSD's advisory reminders faithfully, and GSD has no MCP-tool policy — porting them would be non-functional padding. This is the same faithful-subset pattern used for codebuddy #2098 / copilot #2099, and it satisfies AC4's testable requirement ("a real blocking hook rejecting a disallowed write/command"). +- **Security (reviewed, clean).** The guard scripts parse untrusted stdin and spawn `git rev-parse` — command injection via `file_path` was **refuted** (argv array, no `shell:true`, PATH-resolved git). No traversal / prototype-pollution (frozen 2-event set, fixed script names). The pre-command guard was **hardened post-review**: a tokenize-based classifier (no catastrophic-backtracking regex — a 200k-char pathological input now completes in ~32ms via a 4096-char cap), catching prefixed `rm -rf` forms (`sudo`/`env`/`/bin/rm`) and refspec force-pushes (`HEAD:main`, `+main`), and a fail-closed false-positive fixed (a `feature/main-fix` branch or a trailing-`# ...main` comment no longer wrongly blocks a legit force-push). Guards fail-open (never wedge Cascade) by design. +- **Golden mechanics.** `.windsurf/hooks.json` is golden-excluded by basename (like settings.json); the 2 guard scripts under `hooks/` are windsurf-specific → only windsurf.json regenerates, additively. + +## Spec compliance (acceptance criteria) + +- [x] Golden parity: byte-identical for the folds across all 16 runtimes (windsurf.json regen is the additive hook-script delta only) +- [x] Driven through the descriptor — zero live `runtime==='windsurf'`/`isWindsurf` branches (AC2 guard over 4 files) +- [x] Every axis populated + `capability-validator`-clean (`runtime`/dispatch stay `undocumented` per the cited search trail) +- [x] UPGRADE implemented AND exercised by a test driving a real blocking hook (exit-2 on a disallowed write/command) +- [x] `negotiateHostCapabilities` fail-closes for windsurf (test) +- [x] `gsd-test` green (linux node22/24); no other-runtime regression (cursor.json byte-identical) +- [x] Docs (matrix hookBus delta) + changeset (`Changed`) + +## Testing + +- [x] macOS (real install byte-parity harness + live guard-script exit-2 probing + ReDoS timing) +- [x] Windows (backslash; Windows destructive-command forms handled) — GitHub CI +- [x] Linux (`gsd-test`) +- [x] Runtimes: Windsurf (primary) + all 16 golden fixtures (only windsurf's 2 new scripts) + +--- + +## Scope confirmation + +- [x] Windsurf only; other runtimes byte-identical. The hook-bridge's faithful 2-guard scope (vs. the AC's fuller event list) is disclosed above — the unbridged events have no faithful GSD logic / Cascade channel. +- [x] Cascade envelope/schema is best-effort per the official docs (guards fail-open if the live schema differs, never breaking Cascade); flagged for a live-Cascade schema confirmation follow-up. + +## Documentation + +- [x] matrix (## windsurf hookBus/hooksSurface delta + the not-ported-guards rationale); English + +## Checklist + +- [x] `Closes #2100`; issue has `approved-feature` +- [x] Acceptance criteria met (faithful hook-bridge scope disclosed) +- [x] `gsd-test` green +- [x] New tests cover the folds (AC2 guard) + the blocking hook bus (live exit-2) + fail-closed negotiation +- [x] `.changeset/` fragment (`Changed`) +- [x] No new dependencies + +## Breaking changes + +None at landing. New Windsurf install output is additive: 2 guard scripts + a `.windsurf/hooks.json` registering blocking pre-hooks. No skill, agent, workflow, or path is removed or altered; the guards fail-open. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a64f2189..2506e0619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,214 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [1.7.0] - 2026-07-15 + +### Added + +- **A default-off, BETA, claude-only "Claude orchestration" capability** — adopts Claude Code's Workflow tool (`/effort ultracode`, Agent SDK ≥ v0.3.149) as an optional parallel-execution backend for the GSD loop, restoring the wave parallelism + plan-checker + verifier that the #853 backgrounded-agent nesting limitation forces inline on Claude Code, and folding the existing `gsd-ultraplan-phase` plan-offload under the same runtime gate. When `claude_orchestration.enabled` is on AND the runtime is Claude AND the Workflow tool is detected AND the Agent SDK meets the floor (`claude_orchestration.min_agent_sdk_version`, default `0.3.149`), `execute-phase` emits a generated Workflow script (`waves → parallel() barriers`, `plans → agent({ agentType: 'gsd-executor', isolation: 'worktree' })`, `files_modified overlap → separate sequential stages`, `resumeFromRunId` wired to the phase run id, shared `budget` pool) that composes the SAME executor agent + worktree isolation the inline path uses, so artifacts/commits are produced identically. Detection is pure and fail-closed (any miss → inline), so on any runtime lacking the Workflow tool behaviour is byte-identical to today. Adds a pure module `gsd-core/bin/lib/claude-orchestration.cjs` (`detectWorkflowBackend`, `emitWorkflowScript`), the `capabilities/claude-orchestration/` declaration with two gated loop contributions (`execute:wave:post`, `plan:post`) and a `claude-orchestration` command family (`gsd-tools claude-orchestration detect-backend|emit-workflow`), federated config keys, and an ADR-1143 implementation amendment. (#1143) (#2044) +- **Phases that integrate an external API/SDK/service can no longer seal without a decided coverage matrix** — a new `api-coverage` gate on the `ai-integration` capability blocks `/gsd:verify-work` until the phase produces a `COVERAGE.md` enumerating the API's full capability surface, with every non-integrated capability an explicit, reasoned opt-out. Full coverage is the default; the matrix is the subtraction record, so "we integrated the API" can no longer silently mean "we integrated whatever the first use case exercised." Toggleable via `workflow.api_coverage_gate` (on by default). (#1562) (#2065) +- +**OpenCode installs now auto-register the GSD companion MCP server (`mcp.gsd`)** — `--opencode` install writes a `mcp.gsd` entry (local stdio → `gsd-mcp-server`) into `opencode.json`, so OpenCode drives GSD's command + planning-state surface over MCP with no bespoke plugin (ADR-1239 Phase D / #1682). Idempotent and non-clobbering; a user-defined `mcp.gsd` is preserved. (#1682) (#1929) +- +**OpenCode plugin handles `session.idle` + the `opencode-subset` hook dialect is implemented** — the GSD OpenCode plugin now recognizes `session.idle` (↔ Claude `Stop` lifecycle point), completing the compaction/idle pair (#1914 shipped compaction). The reserved `opencode-subset` dialect gains a consumer — `hookEventSurfaceFor()` in `host-integration.cts` — describing OpenCode's session/tool/file event subset (no workflow-phase events; the engine owns phase sequencing, ADR-1239 §OpenCode binding). Adds a Claude-parity test asserting the plugin covers the full declared subset. (#1682) (#1930) +- **GSD now warns when model config changed without re-running the installer on static-frontmatter runtimes** — on `codex` and `opencode`, editing `model_overrides` or `model_profile_overrides` or `model_policy.runtime_tiers` in `.planning/config.json` or `~/.gsd/defaults.json` previously had no effect until the user re-ran `gsd install `, and the failure was silent: the sub-agent kept using the base model. Workflow entry points like `gsd-tools init *` now emit a one-line stderr warning naming the changed config file and the exact remediation command when they detect the config is newer than the baked agent files. The guard is read-only and warning-only by default, dedup'd per session, and skipped entirely on Claude Code because Claude Code resolves models at spawn time. Resolves #1688 as the structural follow-up to #1650. (#1692) +- **`gsd-tools state rebuild`** — new subcommand that re-derives STATE.md body structure from canonical sources (frontmatter + `.planning/phases/` disk scan), reconciling drifted `## Current Position` prose, dropping orphaned rows from the `**By Phase:**` table, clearing template-placeholder field values, and de-duplicating `## Session Continuity Archive` blocks. Every mutation is recorded in a `## Rebuild Log` audit section. Idempotent (running twice on a clean file is a no-op). Supports `--dry-run` (preview) and `--verbose` (tee log to stderr). Heavier, manual counterpart to the lightweight auto-triggered `state sync`. (#1830) +- **`graphify.graph_path` makes the knowledge-graph location configurable so one umbrella graph can serve multiple projects** — a new `.planning/config.json` key (path relative to project root, or absolute) overrides where `/gsd-graphify query|status|diff` read the graph, letting a single curated cross-repo umbrella graph serve every sibling sub-project without N drifting ~5 MB mirror copies. Previously the graph location was hardcoded to `/.planning/graphs/` with no override; the only workaround was copying the umbrella `graph.json` into each project (which drifted, wasted disk, and could be silently overwritten by an in-project build). The diff snapshot travels with the configured graph; build stays project-scoped; unset → byte-identical default; a configured-but-missing file yields an actionable error naming the path. (#1825) (#2013) +- **Claude Sonnet 5 is now the `standard` (sonnet) tier model.** The model catalog and provider presets resolve the sonnet/standard tier to `claude-sonnet-5` (GA 2026-06-30) across the Anthropic-backed runtimes (`claude`, `copilot`, and the `anthropic`/`anthropic-fable` presets), plus the OpenRouter-style `anthropic/claude-sonnet-5` for `opencode`/`hermes`, replacing the superseded `claude-sonnet-4-6`. Opus and Haiku tier defaults are unchanged (the `haiku` high-effort preset's escalation slot tracks the current sonnet model). Shipped in 1.6.1. (#1847) (#1848) +- **Third-party capability gates now actually fire via a generic `command-exit-zero` predicate.** — a capability's declared `check.predicate` gate was rendered for display but never evaluated (only built-in `check.query` gates were enforced, and the `security` capability's gate worked solely via a hard-coded `ship.md` branch). A new generic evaluator (`gsd_run check predicate`) now evaluates `check.predicate` blocks by `kind`; the first built-in kind `command-exit-zero` runs a bounded `sh -c` command at the project root and blocks the loop on non-zero exit (timeout → block, fail-closed). The `execute:wave:post`, `execute:post`, and `plan:post` gate-dispatch sites route `predicate` gates to the new evaluator automatically. (#2008) (#2011) +- **GSD's lifecycle hooks now run under Kimi CLI** — installing GSD into Kimi wires its session-state, phase-boundary, graphify, and guard hooks into Kimi's own native `config.toml` `[[hooks]]` bus (Beta on Kimi's side) instead of silently no-op'ing, and GSD's Kimi subagents can now run in the background. Kimi's install is driven by its negotiated capability descriptor instead of hardcoded runtime special-cases. (#2095) (#2159) +- **GSD is now installable on pi** — `npx @opengsd/gsd-core --pi` installs the GSD extension to `~/.pi/agent/extensions/gsd.cjs`, and `/gsd ` now dispatches real commands through the embedded engine (the reference binding previously could only run `query help`). Drives pi through the negotiated imperative Host-Integration adapter, with active-model steering and the full pi lifecycle-event surface. (#2102) (#2205) +- +**GSD now ships a pi extension** — a real, jiti-loadable ExtensionAPI module (`pi/gsd.cjs`) that registers `/gsd` (dispatches through the GSD command-routing hub) + `gsd_invoke` tool + `tool_call` event, installable at `~/.pi/agent/extensions/`. A reachability test proves the `/gsd` handler dispatches through the engine (keystone wired, not just registered on a mock). (#1965) (#1965) +- `plan-phase` now authors edge and prohibition predicates into PLAN.md `must_haves` when a phase SPEC omits `## Edge Coverage` / `## Prohibitions`, so goal-backward verification still has predicates to check on a spec-less phase (ADR-857 Phase 6). Gated by the new default-on `workflow.specless_probe_fallback` toggle — disable it to skip the fallback (the skip is recorded visibly in the plan). Spec-less prohibitions are authored descriptor-less and disposed flagged/unverified (honest verifier #1154), never a silent pass. (#1835) +- **Discover third-party GSD Capabilities in a new Community Capability Registry.** — A non-endorsing discoverability catalog where authors register a Capability via a documentation PR; each entry carries a live latest-release badge and a per-entry GitHub Discussion for community ranking and comments. (#2188) (#2188) +- **GSD now warns when a stale global CLI (e.g. a retired @gsd-build/sdk canary) shadows your project-local install** — the gsd-tools CLI startup detects when the running binary is outside the project root while a project-local install exists, and prints a remediation warning to stderr (non-blocking). (#1754) (#1755) +- **`gsd-mcp-server` — companion MCP server (interface points 1 + 5)** — a new bin command (`npx @opengsd/gsd-core gsd-mcp-server`) runs a stdio JSON-RPC 2.0 MCP server exposing `gsd_invoke_command` (→ the GSD command-routing hub) + `gsd_read_state` / `gsd_write_state` (→ `.planning/` state), so any MCP-consuming host (Claude Code, Codex, OpenCode, VS Code, Gemini CLI, Cursor, Cline, Hermes) can drive GSD with no bespoke plugin (ADR-1239 Phase C-2 / #1681). Dependency-free (hand-rolled JSON-RPC). How-to: `docs/how-to/connect-gsd-mcp-server.md`. (#1810) +- **Opt-in absolute token count on the statusline context meter** — new `statusline.show_context_tokens` config (default `false`). When enabled, the meter shows the absolute context total after the percentage, e.g. "████░░░░░░ 46% (156k)", summing input, cache-creation, cache-read, and output tokens from the hook payload (a broader basis than the meter's percentage, which is derived from `used_percentage` and excludes output tokens — the two figures can diverge slightly). Default meter output is unchanged. (#2161) (#2174) +- **Long-running compute can now be externalized as async external jobs instead of blocking the agent turn** — a default-off external-job capability lets executors submit SLURM jobs, commit a .planning/async-jobs manifest, defer SUMMARY.md, and return external_job_waiting; the core loop already reconciles these manifests (#1165), so this adds the producer half (SLURM adapter, pure manifest module, planner/executor fragments, operation policy). (#1105) (#1998) +- +**GSD now ships a repo-local VS Code extension** — a buildable extension (`vscode/extension.js` + `vscode/package.json`) that registers `gsd.invoke` (dispatches through the GSD command-routing hub) in the VS Code command palette. A reachability test proves the handler dispatches through the engine (keystone wired). Not Marketplace-published; mirrors the OpenCode plugin's bar. (#1966) (#1966) +- **Discover third-party GSD Embeddable Orchestration System (EoS) integrations in a new EoS Registry.** — A non-endorsing discoverability catalog where host-integration authors register via a documentation PR; each entry declares its Host-Integration interface points, negotiated axes, and protocol version, with a live release badge and a per-entry GitHub Discussion for ranking and comments. (#2193) (#2193) +- GSD Core ships a `.claude-plugin/marketplace.json` marketplace manifest so Claude-plugin-compatible runtimes (ZCODE et al.) can discover and install gsd-core from a custom marketplace source. Additive — the existing `.claude-plugin/plugin.json` and the Claude Code install path are unchanged. The catalog version (`plugins[0].version`) tracks `package.json` via the release version-sync. (#1861) +- **GSD now drives VS Code through the Embeddable Orchestration System** — the VS Code extension is rewired through the negotiated imperative Host-Integration adapter (active `vscode.lm` model, engine hook bus, sandboxed storage), gains native Language Model Tools (GSD skills as `#gsd-*` tools) and `#runSubagent` dispatch, and runs as a Web Extension (no Node APIs). (#2103) (#2210) +- **`/gsd:next` smart-entry workflow** — adds a state-aware entry point that classifies the current project situation (no-project, blocked, verify-failed, planning, executing, verify-pending, complete, and more) and recommends the right next GSD command. The `gsd-tools smart-entry [--json]` classifier handles phase ordering including decimal phase IDs; the `/gsd:next` skill surfaces the workflow with tiered fallback behavior. (#1798) +- OpenCode now runs GSD's lifecycle safety hooks (prompt-injection guard, read-before-edit guard, injection scanner, worktree/workflow guards, context monitor) via a native plugin installed to `~/.config/opencode/plugins/gsd-core.js`. OpenCode declares `hooksSurface: 'none'`, so these hooks were previously inert; the plugin bridges OpenCode's event bus onto GSD's existing hook scripts. Installed automatically by `npx @opengsd/gsd-core --opencode` and removed on uninstall. (#1923) +- **Host-integration descriptors now carry an `extensionEvents` vocabulary** — the extension-system event surface (OpenCode, pi) is a separate descriptor field from managed `hookEvents`, so OpenCode declares `extensionEvents:opencode` without conflicting with the hooksSurface:none invariant. (#1946) (#1946) +- **`/gsd-review` now supports custom reviewer instances** — run one model-capable adapter (e.g. OpenCode) as several independent reviewer identities via a bounded `review.reviewer_instances` config, so two different models can review in a single pass without manually swapping config or hand-merging REVIEWS.md. (#1517) (#1766) +- **Opt-in git branch and working-state segment in the statusline** — the shell prompt's branch/dirty-state signal is hidden for the whole session under the Claude Code TUI, so wrong-branch commits and ship-time push rejections surface only after the fact. New `statusline.show_git` config (default `false`) renders the branch name plus staged/unstaged/untracked/ahead/behind markers (or ✓ when clean and in sync) after the directory segment. When disabled, no git subprocess is spawned and output is unchanged. (#2163) (#2183) +- **`/gsd:onboard` guides brownfield setup** — existing repos now have a top-level onboarding command that routes through codebase mapping, docs ingest, project initialization, and an onboarding summary without silently overwriting planning files. (#1994) +- **Plural/optional/chosen assumption-delta checkpoint during planning** — when a phase makes something plural, optional, or chosen that used to be singular, required, or derived, the planner is now prompted to re-ask whether the primary key / identity model still names the right thing, preventing silent architectural drift from accumulating into a later user-facing bug. Advisory (non-blocking); fires only on a detected signal. Toggle with workflow.assumption_delta. (#1561) (#1767) +- **`/gsd-ui-phase` now probes UI state coverage** — a new `ui-consideration-probe` (the third `probe-core` adapter) enumerates the shape-rooted UI states a UI-SPEC must resolve (empty/loading/error/populated/partial/overflow/zero-one-many/long-text). After the UI checker approves, the probe surfaces applicable considerations for each element, records a `## UI Considerations` section in the UI-SPEC, and plan-phase lifts each resolved consideration into `must_haves` — so a purely-visual state with no wired test routes to `insufficient_spec → human_needed` at verify rather than a silent pass. (#1979) +- **Host-Integration Interface (ADR-1239 Phase A)** — a versioned, negotiated capability contract (`runtime.hostIntegration`) over the six host-integration points (command, dispatch, model, hooks, state, artifact). Adds an in-process `negotiateHostCapabilities` handshake that fail-closes on undeclared/unknown/`undocumented` values (`effective ⊆ host-declared ∩ engine-known`), a typed degradation ladder, host-capability profiles, and a documentation-sourced per-CLI capability matrix for all 16 runtimes. Interface-definition only — no change to install behaviour. (#1690) +- **ZCode (Z.ai) is now an installable runtime** — a desktop Agentic Development Environment for the GLM-5.2 model can now be targeted with `--zcode`, landing GSD skills at `~/.zcode/skills//SKILL.md` plus slash commands and subagents. ZCode ships as a pure declarative capability descriptor (`capabilities/zcode/capability.json`) with zero hardcoded `runtime === 'zcode'` branches, reusing the Claude skill converter — the de-hardcoded, data-driven runtime path that 1.7.0 (ADR-1016 / ADR-1239) enables. (#1925) (#2039) + +### Changed + +- +**The GSD CLI now self-heals a missing runtime build.** The compiled `gsd-core/bin/lib/*.cjs` modules are gitignored build artifacts (ADR-457) that ship prebuilt in the npm tarball but are absent on a Claude Code plugin-marketplace / git-clone install, which never runs `npm run build:lib`. Previously every command died at load with `Cannot find module './lib/cli-exit.cjs'`. The `gsd-tools` entrypoint now detects the missing output and compiles it once, on demand (lock-guarded so parallel invocations don't race), then proceeds — a single no-op check on the already-built npm path. When TypeScript is genuinely unavailable it prints an actionable `npm install && npm run build:lib` message instead of crashing. (#2036) +- **Internal: Claude Code's installer is now driven through the public Host-Integration Interface (ADR-1239 / EoS).** `bin/install.js` routes `claude` install/uninstall through the imperative adapter (`createImperativeAdapter`) instead of calling the engine directly, and its 13 hardcoded `runtime === 'claude'` / `runtime !== 'claude'` branches are folded into descriptor-driven `runtime.hostBehaviors` on `capabilities/claude/capability.json` (permission schema, `settings.local.json` scope routing, `.gsd-source` marker, effort frontmatter, canonical-workflow authorship, and more). Install/uninstall output is **byte-identical** for both the global skills layout and the local legacy layout (golden-parity asserted for both scopes); no other runtime changes. Removes the "add-a-host tax" of scattered string-equality checks for the tier-1 reference host. No user-facing change. (#2086) (#2106) +- **OpenCode is now driven through the public Host-Integration Interface, with two capability upgrades (ADR-1239 / EoS).** OpenCode and its Kilo sibling previously installed via a bespoke `runtime === 'opencode'`/`isOpencode` branch in `bin/install.js`; its commands+skills+plugin install now runs through the imperative adapter → the engine's combined-family install path (`installRuntimeArtifacts`), and every hardcoded `runtime === 'opencode'` branch is folded into descriptor-driven `runtime.hostBehaviors`. Install/uninstall output is **byte-identical** (golden parity asserted for all 16 runtimes). Two Context7-verified upgrades land: (1) **background dispatch** — OpenCode shipped experimental background subagents in v1.15 and made them default-on in v1.17, so `dispatch.background`/`backgroundDispatch` flip to `true`; GSD no longer force-flattens OpenCode-hosted wave dispatch (`shouldFlattenDispatch` now returns `false`), letting agents run concurrently where the host supports it. (2) **expanded event surface** — the OpenCode plugin now subscribes to `permission.asked`, `permission.replied`, and `session.error` (added to `EXTENSION_EVENT_SURFACES.opencode`), wiring the declared surface for future permission/error-aware bindings. (#2087) (#2108) +- **Codex is now driven through the public Host-Integration Interface, with three capability upgrades (ADR-1239 / EoS).** Codex previously installed via hardcoded `runtime === 'codex'`/`isCodex` projection in `bin/install.js`; its `config.toml` / agent-`.toml` / `hooks.json` install now runs through the declarative embedding adapter and descriptor-driven `runtime.hostBehaviors`, with **zero** positive `isCodex` gates and **zero** `runtime === 'codex'` branches remaining (source-guarded). Install/uninstall output stays byte-parity-gated (`tests/fixtures/golden-install-parity/codex.json`). Three Context7-verified upgrades land, each with a test driving the user-reachable surface: (1) **skill root** — GSD skills now install to Codex's canonical `$HOME/.agents/skills` (via a skills-kind `home` override) instead of the deprecated `$CODEX_HOME/skills` fallback, and pre-move installs are migrated (stale `~/.codex/skills/gsd-*` cleaned on both install and uninstall, user-owned content preserved); (2) **hook events** — GSD registers the six documented Codex lifecycle events it previously skipped (`PreToolUse`, `PermissionRequest`, `PreCompact`, `PostCompact`, `SubagentStop`, `UserPromptSubmit`, in addition to the existing `SessionStart`/`SubagentStart`/`Stop`/`PostToolUse`) in `hooks.json`, so `gsd-context-monitor` fires at the same points as in Claude Code, and the descriptor `extendedHookEvents` is reconciled from `[]` to the schema-valid wired subset; (3) **dispatch tuning** — `[agents] max_depth = 1` is written explicitly into the managed `config.toml` block to pin the negotiated `dispatch.maxDepth: 1` axis (`degradationFor` flattens GSD-hosted waves to single-level), and `validateCodexConfigSchema` now permits a known-scalar-only `[agents]` AgentsToml table (coexisting with the flattened `[agents.gsd-*]` role sub-tables) while still rejecting the `[[agents]]` and unknown-key break-forms from #2760. (#2088) (#2110) +- **Cursor is now driven through the public Host-Integration Interface, with two capability upgrades (ADR-1239 / EoS).** Cursor previously installed via hardcoded `runtime === 'cursor'`/`isCursor` branches in `bin/install.js`; its install/uninstall now runs through the imperative adapter, and every hardcoded cursor branch is folded into descriptor-driven `runtime.hostBehaviors` (reapplyCommand, frontmatterDialect, hooksJsonSurface, skipSharedHooksInstall, reportCommandsDir, managedHookEvents). Install/uninstall output is **byte-identical** (golden parity asserted for all 16 runtimes). Two Context7-verified upgrades land: (1) **expanded hook-bus coverage** — GSD registers all 6 managed lifecycle events in Cursor's `hooks.json` (`preToolUse`, `stop`, `subagentStart`, `subagentStop` in addition to the original `sessionStart`/`postToolUse`), driven by a new descriptor-driven adapter module (`src/host-integration-adapters/imperative-hook-bus.cts`) that reads `hostBehaviors.managedHookEvents` instead of a hardcoded event pair; cite https://cursor.com/docs/hooks. (2) **named/background nested subagent dispatch** — Cursor's `dispatch.background`/`backgroundDispatch`/`nested` are all `true` with `maxDepth: 2`, so `shouldFlattenDispatch(cursor)` returns `false` and GSD's wave-based execution drives Cursor's native background + depth-2 nested subagent invocation instead of flattening to inline sequential calls; cite https://cursor.com/docs/subagents + https://cursor.com/docs/sdk/typescript. (#2089) (#2120) +- **Cline is now driven through the public Host-Integration Interface, with two capability upgrades (ADR-1239 / EoS).** Cline previously installed via hardcoded `runtime === 'cline'`/`isCline` branches in `bin/install.js`; its install/uninstall now runs through the imperative adapter, and every hardcoded cline branch is folded into descriptor-driven `runtime.hostBehaviors` (reapplyCommand, frontmatterDialect, skipSharedHooksInstall, localTargetIsProjectRoot, clineRulesSurface, localCommandsViaRules). Install/uninstall output is **byte-identical** (golden parity asserted for cline + claude/cursor/codex/opencode). Two Context7-verified upgrades land: (1) **`AgentPlugin.hooks.beforeTool` planning guard** — the `.clinerules/hooks/PreToolUse` file-convention hook (#787) is re-implemented as a real Cline SDK `AgentPlugin` that cancels write-class calls targeting `.planning/` (same fail-open semantics), driven by a new descriptor-driven adapter module (`src/host-integration-adapters/cline-sdk-binding.cts`); cite https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx. (2) **`createAgentModel` model overrides** — `DefaultGateway.createAgentModel({providerId, modelId})` is wired so GSD's per-subagent `model_overrides`/`model_profile_overrides` resolution applies to Cline subagents (`modelMode: active`); cite https://github.com/cline/cline/blob/main/docs/sdk/reference/gateway.mdx. Cline's dispatch deliberately stays **degraded/flat** (`maxDepth: 1`, read-only, no nested spawning) per the documented host restriction — never silently upgraded to full nested/background. (#2090) (#2132) +- **Hermes Agent is now driven through the public Host-Integration Interface, with three capability upgrades (ADR-1239 / EoS).** Hermes previously installed via hardcoded `runtime === 'hermes'`/`isHermes` branches in `bin/install.js`; its install/uninstall now runs through the imperative adapter, and every hardcoded hermes branch is folded into descriptor-driven `runtime.hostBehaviors`. Three upgrades land: (1) **real plugin hook vocabulary** — GSD registers a new `extensionEvents: "hermes"` dialect carrying the 13 documented Hermes plugin events (`pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`, `on_session_end`, `on_session_finalize`, `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`, `pre_approval_request`, `transform_tool_result`), replacing the borrowed `hookEvents: "claude"` 6-event surface that silently never fired; cite https://github.com/nousresearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md. (2) **dispatch posture** — Hermes' `dispatch.nested: true` with `maxDepth: 1` is correctly negotiated (not silently flattened). (3) **branding/category metadata** — `DESCRIPTION.md` category descriptions, `version:` frontmatter, and branding rewrites are now descriptor-driven rather than hardcoded. Install/uninstall output is byte-identical (golden parity asserted for all runtimes). (#2091) (#2134) +- **Qwen Code now projects GSD's specialist agents as native subagents** — installing GSD into Qwen Code writes `~/.qwen/agents/gsd-*.md` files you can invoke directly (planner, executor, code-reviewer, …) instead of reaching them only through skill prose, and a `SubagentStart` hook now fires alongside `SubagentStop`. Qwen's install is driven by its negotiated capability descriptor instead of hardcoded runtime special-cases. (#2092) (#2153) +- **Kilo Code now supports native hooks, active-model routing, and named subagent dispatch** — installing GSD into Kilo wires a lifecycle-hook plugin, keeps each agent's requested model instead of dropping it, projects GSD's specialist agents as invokable subagents, and documents the GSD MCP companion. Kilo's install is driven by its negotiated capability descriptor instead of hardcoded runtime special-cases. (#2093) (#2156) +- **GSD skills installed for Trae now carry SOLO stage metadata** — Trae's SOLO Agent can recognize GSD skills as workflow-stage skills for auto-invocation instead of requiring manual triggering. Several of Trae's install branches (shared-hooks gating, path rewrites) also move onto its capability descriptor. Note: the stage-metadata field is a best-effort/inferred shape — Trae publishes no formal schema. (#2094) (#2157) +- **Installing GSD into Antigravity now writes the `permissions.allow` rules its CLI documents** — so GSD's own reads and hooks aren't stuck on interactive prompts — and registers GSD's companion MCP server via a standalone `mcp_config.json` (best-effort: Antigravity's raw config schema isn't published, so this uses the Gemini-CLI-successor format). Antigravity's install is now driven by its negotiated capability descriptor instead of hardcoded runtime special-cases. (#2096) (#2165) +- +**Augment Code now installs through its capability descriptor, with a native MCP companion** — installing GSD into Augment registers the GSD companion server in Augment's `settings.json` `mcpServers` and drives command/skill/agent conversion from Augment's negotiated descriptor instead of hardcoded runtime special-cases. (#2097) (#2166) +- +**CodeBuddy now wires GSD's full extended lifecycle hook set and is driven by its capability descriptor** — installing GSD into CodeBuddy now registers `SubagentStart`, `SubagentStop`, `Stop`, and `PreCompact` hooks in its `settings.json` (it previously had none of these), matching the coverage Qwen/Kimi already ship, and CodeBuddy's install is fully descriptor-driven instead of via residual hardcoded runtime branches. (#2098) (#2169) +- +**GitHub Copilot now wires GSD's full lifecycle hook bus and is driven by its capability descriptor** — installing GSD into Copilot registers `preToolUse`, `postToolUse`, `userPromptSubmitted`, and `sessionEnd` handlers in its `hooks/gsd-session.json` (beyond today's `sessionStart`-only advisory), and Copilot's residual hardcoded runtime branches are folded onto descriptor-driven `hostBehaviors`. (#2099) (#2172) +- **Windsurf now enforces GSD's write/command safety guards through Cascade's native hook bus** — installing GSD into Windsurf registers blocking `pre_write_code`/`pre_run_command` hooks in `.windsurf/hooks.json` (exit-code-2 blocking) and drives Windsurf's install from its capability descriptor instead of hardcoded runtime branches. (#2100) (#2190) +- **ZCode's install is now driven and regression-tested through its capability descriptor** — ZCode joins the dogfooded declarative-adapter reference hosts with a byte-identical install, and its shared-hooks exclusion is folded onto `hostBehaviors` instead of a hardcoded runtime branch. (Hook-automation and MCP upgrades remain blocked on ZCode publishing its on-disk config formats.) (#2101) (#2195) +- **Codex/OpenAI default models advance to the GPT-5.6 family (Sol/Terra/Luna)** — the Codex runtime tier defaults and the `openai` provider preset now resolve to current-generation model IDs instead of the superseded GPT-5.4/5.5 line, so Codex users on default profiles get improved agentic coding (Sol) and lower costs (Terra/Luna) without changing any config. (#2122) (#2146) +- **Internal: the installer's `program` (display-name) + `command` (slash-invocation) chains are now single-source lookups** — the 14-line `program` chain (an exact duplicate of `runtimeLabel`) → `getRuntimeLabel`, and the 14-line `command` chain (the per-runtime `/gsd-new-project` syntax: gemini `/gsd:`, codex `$`, cursor skill-mention, kimi `/skill:`, default `/gsd-new-project`) → new `getRuntimeNewProjectCommand(runtime)` helper (ADR-1239 Phase B / #1679 AC2 slice 4). `runtime ===` count in `bin/install.js`: 53 → 25 (cumulative this session: 129 → 25). Stdout strings preserved byte-for-byte; no install-output change (golden-parity 16/16). No user-facing change. (#1813) +- **Internal: the installer's per-function `is` flag-declaration blocks are now a single `runtimeFlags` lookup** — the four duplicated `const isX = runtime === 'x'` blocks in `bin/install.js` (uninstall / writeManager / install / a fourth helper — 48 branches) are collapsed into one `runtimeFlags(runtime)` helper in `runtime-name-policy.cts` (ADR-1239 Phase B / #1679 AC2 slice 3). The add-a-host tax for flags is removed (one `RUNTIME_FLAG_IDS` entry, not four declaration blocks). Install output is byte-identical for all 16 runtimes (golden-parity asserted); `runtime ===` count in `bin/install.js`: 101 → 53. No user-facing change. (#1811) +- **Internal: third-party descriptor loader enforces `configHome` write-confinement at load time** — `loadRegistry({includeInstalled:true, configHome})` now rejects (skip + warn, fail-closed) any installed third-party host-plugin descriptor whose declared `destSubpath` resolves outside the supplied `configHome`, before it is composed into the registry (ADR-1239 Phase C-2 / #1681 slice 2). The `configHome` option is optional and backward-compatible (omitted → no load-time check; install-time gate still bounds writes). No user-facing change for existing flows. (#1808) +- **Internal: agent install for cursor/windsurf/augment/trae/codebuddy now flows through the descriptor path** — ADR-1235 step 1 routes the trivial-converter runtime group's agents off the inline install() loop onto the descriptor-driven `installRuntimeArtifacts` path, applying the cross-cutting steps uniformly (pre-converter, no workflow-stamp). Agent output is byte-identical for all 16 runtimes (golden-parity asserted, global + local verified); no user-facing change. (#1764) +- **gsd-ui-checker gains an adversarial FORCE stance (LLM-playbook principle 16)** — the only verdict-producing critic that lacked one now resists rubber-stamping UI-SPEC contracts, with BLOCK/FLAG/PASS classification. Based on arXiv 2505.23840 (third-person objective persona), 2506.04975 (objective-not-hostile persona). (#1584) +- **Internal: the declarative embedding adapter is now named + bound behind a minimal `HostIntegrationInterface`** — `createDeclarativeAdapter({runtime})` (new `src/adapter-declarative.cts`) delegates in-process to `install-engine`'s `installRuntimeArtifacts`/`uninstallRuntimeArtifacts`, formalizing today's projection path as one of the two embedding adapters behind a common contract (ADR-1239 Phase C-1 / #1680 AC1). Output is byte-identical to today's install (gated by `golden-install-parity`). The full 6-point interface binding surface is deferred until the imperative adapter (AC2) fixes the shape (ADR-1239 open wire-shape question). No user-facing change — the adapter is not yet wired to any runtime path. (#1802) +- **Internal: getDirName is now derived from a documented `runtime.localConfigDir` descriptor field** — each runtime's local content-rewrite directory (e.g. `cursor`→`.cursor`, `copilot`→`.github`) moved from a hand-maintained if-chain into its capability descriptor (ADR-1239 Phase B), so it can no longer drift from the registry. Install output is byte-identical for all 16 runtimes (golden-parity asserted); no user-facing change. (#1757) +- **Internal: copyWithPathReplacement converter selection is now data-driven** — the installer's back-compat content-copy path replaced its 13 hardcoded `runtime === 'x'` flag chains with a single per-runtime dispatch table (ADR-1239 Phase B). Install output is byte-identical for all 16 runtimes (golden-parity asserted); no user-facing change. (#1759) +- **Phase-completion now writes `Status: All phases complete` instead of the overloaded bare `Milestone complete`** — the phase-level completion verb (`completePhaseCore`) was writing the same bare 'Milestone complete' string that the milestone-close verb uses for terminal state, causing a phase-level verb to own a milestone-level field. Per ADR-2207, phase-completion now writes the existing intermediate value 'All phases complete' (already used in gsd2-import.cts); milestone termination (' milestone complete' / 'Awaiting next milestone') remains solely with the milestone-close verb. (#2204) (#2259) +- **#853 dispatch-flatten is now data-driven (ADR-1239 Phase B)** — whether GSD backgrounds the plan/execute orchestrator is decided from a documentation-sourced `backgroundDispatch` capability per host (via `gsd_run query dispatch-should-flatten`) instead of a hardcoded `runtime === 'codex'` check. **Cursor now backgrounds the orchestrator** (its docs document backgrounded subagent nesting); codex unchanged; all other hosts run inline. Fail-closed to inline on any uncertainty. (#1719) +- **Internal: companion MCP server module (interface points 1 + 5)** — `handleMessage`/`runServer` (new `src/mcp-server.cts`) is a minimal, dependency-free stdio JSON-RPC 2.0 server exposing `gsd_invoke_command` (→ the command-routing hub) + `gsd_read_state`/`gsd_write_state` (→ the Phase 3 stateIO seam), so any MCP-consuming host can drive GSD with no bespoke plugin (ADR-1239 Phase C-2 / #1681 slice 3a). Bin entry / packaging deferred to slice 3b. No user-facing change — the server is not yet wired to a bin entry. (#1809) +- **`requirements mark-complete` reports a per-surface write-set** — the command now returns a per-requirement `write_set` (checkbox + traceability surfaces) and a `write_set_complete` that is true only when every surface of every requirement applied, so a partial (checkbox-only) reconcile can no longer masquerade as full success even inside a multi-ID batch. Introduces the reusable ADR-2143 §5/§6 `Result` / `WriteSet` contract. (#2251) (#2251) +- **Internal: the imperative embedding adapter now composes the capability registry behind the same `HostIntegrationInterface`** — `createImperativeAdapter({runtime})` (new `src/adapter-imperative.cts`) calls `loadRegistry({includeInstalled:true})` (first-party-wins + consent + fail-closed — identical trust semantics to the CLI) and binds the engine surface behind the same contract the declarative adapter (AC1) satisfies, plus a `registry` accessor for an in-process host to bind its primitives to (ADR-1239 Phase C-1 / #1680 AC2). Concrete host binding is deferred to Phase 5. No user-facing change — the adapter is not yet wired to any runtime path. (#1803) +- **Internal: the model adapter seam exposes `passive` + `active` adapters selected by `modelMode`** — `createModelAdapter({modelMode})` (new `src/model-adapter.cts`): `passive` formalizes today's tier routing (delegates to `model-resolver.resolveModelForTier`), `active` is a host-supplied `sendRequest` seam (VS Code `vscode.lm` / pi providers), fail-closed until Phase 5 binds a concrete provider (ADR-1239 Phase C-1 / #1680 AC3). No user-facing change — the seam is not yet wired to any runtime path. (#1804) +- **Internal: derive the non-Claude runtime list from the capability registry** — `NON_CLAUDE_RUNTIMES` is now computed from the capability registry instead of a hand-maintained literal, so it can no longer drift from the per-runtime descriptors. No user-visible behavior change (the list is identical). (#1728) +- **Honest verifier — verify-phase now abstains on non-inferable `backstop` truths instead of confidently false-passing them (#1154).** When the spec's edge-probe marks a truth non-inferable (`verification: backstop`) and the verifier cannot confirm it with explicit evidence (a passing wired held-out/property test, or a directly-observed behavior), it now reports `human_needed` with reason `insufficient_spec` ("unverified — held-out test recommended") rather than a silent `passed`. Autonomous runs complete with "N unverified non-inferable checks"; interactive runs route to the end-of-phase human checkpoint. Inferable truths are never abstained (over-abstention guard); abstention is exogenous (driven by the tag, not self-judgment). Truth-axis mirror of the prohibition judgment-tier (ADR-550 D4). (#1738) +- Document Claude Code's advisor-tool inheritance in the model-profiles reference: the session-level advisor is inherited by all GSD subagents and composes with per-agent tiering, with candidate executor/advisor pairings, when it is worth enabling, and the session-level (no per-agent control) constraint. (#1922) +- **Extraction discipline for strict-format agents (LLM-playbook principle 8)** — gsd-doc-classifier and gsd-doc-synthesizer apply taxonomy/precedence rules directly without inventing content, reducing reasoning-induced format drift. Based on arXiv 2504.05081 (few-shot beats CoT for pattern tasks), 2506.00069 (terminal instruction placement), 2505.14810, 2505.11423. (#1584) +- **Internal: extracted the runtime-artifact install engine from `bin/install.js`** — `installRuntimeArtifacts`/`uninstallRuntimeArtifacts`/`installOpencodeFamilySkills` and their helpers now live in a dedicated `gsd-core/bin/lib/install-engine.cjs` module (ADR-1239 Phase B), so adapters can import the install pipeline instead of reaching into the 12k-line installer. Install output is byte-identical for all 16 runtimes (golden-parity asserted); no user-facing behaviour change. (#1735) +- **MemPalace `memory_mode` `kg_backend` and `replace` are now functional** — selecting either mode now routes recall through the palace instead of silently behaving like `augment`: `kg_backend` treats the palace temporal KG as the primary knowledge-graph source (native `.planning/graphs/` as fallback), and `replace` resolves recall through the palace as the source of truth. Every mode stays default-resilient — an unreachable palace falls back to native memory and no memory is lost. (#2010) (#2010) +- **`/gsd:surface` and `--materialize` now produce byte-identical agent output to a fresh install** — surface-path agents for descriptor-driven runtimes (cursor, windsurf, augment, trae, codebuddy, copilot, antigravity) now receive the same path-prefix rewrite, Co-Authored-By attribution, runtime-specific conversion, and body normalization as the install path. Copilot and Antigravity agents are now installed via the descriptor-driven path (copilot agents get the `.agent.md` filename rename). Cline remains on the inline loop (rules-only local branch). (#1575) (#2040) +- **Internal: hook-bus + stateIO adapter seams** — `createHookBus({bus})` (new `src/hook-bus.cts`, `host`/`engine`/`none` — engine is in-process pub/sub, host fail-closed, none silent) + `createStateIO({io})` (new `src/state-io.cts`, `filesystem`/`sandboxed-storage`/`session-log-append` — filesystem delegates to fs, the rest are fail-closed seams) (ADR-1239 Phase C-1 / #1680 AC4). Completes the Phase 3 adapter seam layer; concrete host binding is Phase 5. No user-facing change. (#1805) +- **Long-context model names render compactly in the statusline** — the verbose " (1M context)" suffix Claude Code appends to the model display name now collapses to a compact " (1M)" badge (tolerant of future window sizes and the abbreviated "ctx" variant: "(500K context)" → "(500K)", "(1M ctx)" → "(1M)"). Lossless — the long-context signal stays, the 12 characters of width don't. (#2160) (#2173) +- **Lazy-split `plan-phase.md` into a `steps/` directory** — ~4.7 KB lighter eager context per `/gsd-plan-phase` call via byte-invariant progressive disclosure (ADR-1610). (#1852) (#1934) +- **GSD subagents now self-load configured agent_skills regardless of orchestrator bash** — projects that map skills via `.planning/config.json` `agent_skills.` no longer silently lose them on `/gsd-autonomous` or Cursor, where `Skill()`-delegated workflow bash init did not reliably run. Each of the 22 consumer agents queries its own type at init and reads the listed skills, with a dedup guard so runtimes that also inject orchestrator-side (Claude Code) never carry two copies. (#1866) (#1868) +- **Internal: install/uninstall runtime labels are now sourced from a single `getRuntimeLabel` lookup** — the two duplicated `runtimeLabel` assignment chains in `bin/install.js` (uninstall + install) are collapsed into one curated label table in `runtime-name-policy.cts`, sibling to the registry-derived `getDirName` (ADR-1239 Phase B, #1679). Install output is byte-identical for all 16 runtimes (golden-parity asserted). Two console-label inconsistencies are normalized as a side effect: `kimi` shows 'Kimi CLI' in both sites, and `cline` uninstall no longer falls through to 'Claude Code'. (#1800) +- **Internal: external-descriptor trust gate — load-time `configHome` confinement** — `assertDescriptorConfined(descriptor, configHome)` (new `src/external-descriptor-trust.cts`) fail-closed rejects any installed third-party host-plugin descriptor whose declared `destSubpath` resolves outside the user-approved `configHome`, before its install plan runs (ADR-1239 Phase C-2 / #1681 slice 1). Defense-in-depth load-time twin of Phase 2's install-time `assertDestWithinConfigHome`. Not yet wired into the loader (slice 2). No user-facing change. (#1806) +- **Internal: the installer's runtime → global-config-home hook-pathogen fragment is now a single `getGlobalConfigHomeFragment` lookup** — the 14-branch `if (runtime === 'x') return "'...'"` chain in `getConfigDirFromHome` (`bin/install.js`, the hook `path.join()` codegen mapping) is collapsed into one table in `runtime-name-policy.cts`, sibling to `getRuntimeLabel` (ADR-1239 Phase B, #1679 AC2 slice 2). Generated hook output is byte-identical for all 16 runtimes (golden-parity asserted); antigravity's dynamic env-overridable resolution is preserved in the caller. No user-facing change. (#1801) + +### Removed + +- **Removed the sunset Gemini CLI runtime — use Antigravity CLI instead** — Google discontinued Gemini CLI on 2026-06-18, so `npx gsd-core --gemini` now prints a deprecation notice and points you to Antigravity CLI (the official successor), which GSD already ships as a first-class runtime. (#1928) (#1996) + +### Fixed + +- The `verify-work` security-blocked presentation no longer offers next-phase planning. When security enforcement blocks phase advancement (no `SECURITY.md` produced), the workflow now routes only to the current-phase fix instead of competing `/gsd:plan-phase {next}` and `/gsd:execute-phase {next}` options. (#1687) +- `milestone complete` and `roadmap analyze` now exclude the Phase 0 / Phase 999 backlog sentinels. A milestone whose only directory-less ROADMAP heading is a backlog sentinel can be completed without `--force`, and `roadmap analyze` no longer counts the sentinel in `phase_count` or routes `next_phase` into it. Completes the `^999` exclusion #1445 added to the progress denominators. (#1691) +- **`config-set` no longer silently coerces values into something the disk never sees** — `Number.isFinite` replaced `!isNaN` in the value parser so `Infinity`/`-Infinity` are no longer coerced to non-finite numbers that `JSON.stringify` then renders as `null` on disk while the CLI echoes `Infinity` (output ≠ disk). `context_window` now has a per-key validator requiring a finite positive integer (rejects `Infinity`, `0`, negatives, non-integers with a non-zero exit), and `project_code` is always persisted as a string so a leading-zero code like `007` survives verbatim instead of collapsing to `7`. Numeric coercion for genuine numeric keys (e.g. `granularity 42`) is unchanged. (#1581) (#2023) +- **`phase.complete` no longer reports a false `is_last_phase` on a `
`-wrapped checkbox checklist (#1591, #1752)** — when the active milestone's phase checklist was written as `- [ ] Phase N:` checkbox items inside a `
` block and the next phase had no directory on disk yet (still in planning), `phase.complete`'s `isLastPhase` roadmap-enumeration fallback used a heading-only pattern (`/#{2,4}\s*Phase…/`) that never matched checkbox items. It returned `is_last_phase: true, next_phase: null` on a mid-milestone phase and — via the milestone-complete cascade — wrongly flipped STATE.md to `Milestone complete` and decremented `progress.total_phases` (e.g. 8 → 7). The pattern now matches heading-style (`### Phase N:`), plain checkbox-list phases (`- [ ] Phase N:` / `- [x] Phase N:`), and the canonical **bold** checklist form the roadmap template emits (`- [ ] **Phase N: Name**`); `extractCurrentMilestone` already surfaces the `
`-wrapped checklist correctly, so no parser change was needed. Only the reproduced `phase.complete` fallback is changed; the heading-only sibling patterns elsewhere in `phase.cts` are untouched. + (#1819) +- The `` block emitted by `gsd init` no longer leaks backslash paths into `@`-reference skill paths on Windows. The global skill directory (a native `path.join` result) was interpolated into the generated markdown without POSIX normalization, producing references like `@C:\…\skills\name/SKILL.md`; the reference is now normalized at the emit site so skill references use forward slashes on every platform. (#1736) +- **`/gsd-settings` no longer warns about four search-provider keys on fresh projects (#1747)** — `buildNewProjectConfig` emits seven search-provider availability flags and `research-provider.cts` `providerAvailability()` consumes all seven, but only three were registered in `VALID_CONFIG_KEYS` (`config-schema.manifest.json`). Running `/gsd-settings` on a freshly generated `.planning/config.json` printed `unknown config key(s) … tavily_search, ref_search, perplexity, jina — these will be ignored` even though the user never hand-edited the config. The four missing keys are now registered alongside `brave_search`/`firecrawl`/`exa_search` and documented in `docs/CONFIGURATION.md`; a drift guard in `tests/bug-2530-valid-config-keys.test.cjs` now requires every config-driven research-provider flag to be in the schema, so a future provider addition cannot reintroduce the drift. (#1814) +- **`gsd-tools state json` no longer reports conflated progress for an unversioned milestone (#1761)** — the ADR-1769 Phase 7 fix (#1794) taught `state sync` to leave Progress untouched when a milestone version is asserted but the ROADMAP has no versioned heading for it, but the `state json` **read** path still rebuilt progress via `buildStateFrontmatter`, whose phase-heading count fell back to the whole document and summed sibling milestones. `state json` therefore reported a conflated `total_phases` (e.g. 8 = 4+4 across two milestones) plus a derived `percent`, contradicting the sync guard on the very same project. The read path now mirrors the sync guard: when the asserted milestone cannot be bounded to a versioned ROADMAP heading, `total_phases` falls back to the on-disk phase-dir count and `percent` is omitted. Bounded milestones (versioned ROADMAP, or no milestone asserted) are unchanged; the signal rides on the existing `_diskScanCache` so `extractCurrentMilestone`'s return contract and its other callers are untouched. (#1818) +- **`gsd-graphify-update.sh` now reads the full multi-line command in Gate 2 (#1772)** — the PostToolUse auto-update hook joined `tool_name` + `\n` + `tool_input.command` and extracted the command with `sed -n '2p'` (line 2 only). Agent runtimes (Claude Code's Bash tool among them) routinely emit HEAD-advancing commits as multi-line scripts (`cd /path`, then `git add`, then `git commit …`), so line 2 was the `cd`, Gate 2's `*"git commit"*` match failed, and the rebuild silently no-op'd on real commits even with `graphify.auto_update: true`. The failure was invisible in manual probes because a single-line `git commit -m x` passes line 2 verbatim. The hook now captures line 2 through EOF (`sed -n '2,$p'`) so the `case` glob sees the full command string; single-line behavior is unchanged and multi-line commands without a HEAD-advancing op still no-op cleanly. (#1815) +- **`/gsd-thread close|resume` now writes the thread status/updated frontmatter (#1778)** — the thread workflow's CLOSE and RESUME branches invoked `frontmatter.set` with the pre-1.6 fully-positional shape (`frontmatter.set `), but since 1.6 the dispatcher parses the file positionally and reads `field`/`value` from the named flags `--field`/`--value` via `parseNamedArgs`. The positional form left `field`/`value` undefined, `cmdFrontmatterSet` errored `file, field, and value required`, and the writes were silently skipped — so closing a thread never marked it `status: resolved` and resuming never marked it `status: in_progress`, with the error scrolling past on every thread command. All four sites (CLOSE `status`+`updated`, RESUME `status`+`updated`) now use the 1.6 hybrid form that `verify-work.md` already uses (`frontmatter.set --field --value `). (#1816) +- **The installer no longer copies dead lifecycle hook scripts for Kilo and ZCode** — both declare `hooksSurface: 'none'` and have no plugin surface, so the staged `hooks/*.js`, `hooks/*.sh`, `hooks/lib/` and the CommonJS `package.json` marker were dead weight in `~/.kilo/` and `~/.zcode/`. The two hook-copy guards in `install.js` now exclude Kilo and ZCode alongside the other no-hook runtimes. OpenCode, which also declares `hooksSurface: 'none'`, is deliberately kept: its native plugin adapter (#1914) spawns those staged hooks via OpenCode's event bus and needs both them and the marker. (#2057) +- **Test gates can no longer hang forever on a watch-mode test runner.** vitest defaults to watch mode in an interactive terminal (exactly where `gsd-execute-phase` runs), so a resolved `npm test` / `pnpm test` that maps to vitest never exited and the orchestrator waited indefinitely until the user manually intervened. Every GSD test-command gate — the regression gate, the post-merge gate, the audit-fix gate, and the verify-phase gate — now routes the resolved command through a shared `normalize-test-command` helper that rewrites it to a one-shot form (direct vitest → `vitest run`; jest `--watch` → `--watchAll=false`; a package-manager `test` script backed by watch-vitest → `CI=true` prefix; already-one-shot commands are left unchanged). The three gates that previously hung or silently continued — the regression, post-merge, and audit-fix gates — additionally bound execution with a configurable `workflow.test_gate_timeout` (default 600s), aborting or surfacing the cause on timeout instead of hanging; the verify-phase gate was already bounded (a fixed 5-minute limit) and keeps it, now naming watch mode on timeout. The normalizer only rewrites a runner named as a standalone command token (so paths/targets like `run-vitest.js` are never mangled), is length-capped and linear-time on adversarial input, and only reads a regular-file `package.json`. (#2060) +- **`settings-advanced.md` no longer has an orphan `` around §8 Model Policy** — the §8 Model Policy block ended with a closing `` but had no matching opening tag (5 opens / 6 closes), leaving its content as loose inter-step prose that could fail to execute reliably. Added the missing `` opener so the section is a proper step. A new workflow ``-tag-balance regression guard (fenced-code-stripped) now blocks any future orphan tag across all top-level workflows. (#1864) (#2014) +- **The runtime launcher now honors `CLAUDE_CONFIG_DIR`** — the `gsd_run` preamble embedded in every workflow/agent resolved the Claude global install only at `$HOME/.claude/gsd-core/bin/`, while the installer honored `CLAUDE_CONFIG_DIR`, so a global install redirected via `CLAUDE_CONFIG_DIR` was invisible to every `gsd_run` call (every GSD command failed with `gsd-tools.cjs not found`). The Claude resolver arm now uses `${CLAUDE_CONFIG_DIR:-$HOME/.claude}` — matching the installer and the other runtimes' `${VAR:-default}` pattern — so a custom `CLAUDE_CONFIG_DIR` is found and the default `$HOME/.claude` path is unchanged. Re-synced into all 95 workflows/agents; two capped workflows trimmed to stay under their byte budgets. (#1865) (#2024) +- **Node-test prohibition proofs now require a clean-fixture causation control** — a `node-test` prohibition's fail-first proof no longer accepts a deceptive content-independent negative test (one that reds merely because `GSD_PROHIB_SUBJECT` is *set*, ignoring the subject's content). The `check_clean_fixture` control is now **mandatory** for the `node-test` kind: a descriptor that omits it is un-provable and hard-gates, rather than greening on the violation alone. **Breaking (Hyrum):** a previously-green node-test prohibition with no clean fixture now hard-gates — blast radius is zero in-tree (no `node-test` prohibition ships today). The `lint-rule` kind is unchanged (its subject IS the linted file, no `GSD_PROHIB_SUBJECT` indirection). (#1906) (#2001) +- **Third-party capabilities now work on installed layouts.** `capability install` no longer rejects capabilities with a real `engines.gsd` range as "incompatible with GSD 0.0.0" — the host version is now read from the authoritative `gsd-core/VERSION` file across every runtime and the `capability install` CLI. The installer also now ships the registry generator scripts (`gen-capability-registry.cjs`, `gen-loop-host-contract.cjs`), so installed third-party capabilities actually compose into the loop instead of being silently discarded. (#1938) +- **`/gsd:verify-work` preserves verification state across gap-closure execution and no longer auto-promotes deferred follow-ups into blocking gaps** — resuming after `/gsd:execute-phase --gaps-only` used to lose the verification state: the UAT `## Gaps` still read `status: failed` even after their fix plans executed, so verify-work re-diagnosed them as fresh blockers, spawned a new gap plan, and reported only the new plan as verified. A state contract now links each gap to its fix plan: every UAT gap carries a stable `gap_id` (`G-{phase}-{N}`), gap-closure plans tag the ids they address in their frontmatter (`gap_ids: […]`), and a new `reconcile_gaps` step on resume marks a gap `status: resolved` when its plan has a matching `*-SUMMARY.md` — so fixed gaps aren't re-diagnosed and the phase can close. Separately, a deferred-follow-up branch captures future-work ideas (signals like "later", "next version", "out of scope") into a `## Deferred Follow-Ups` section instead of creating a blocking gap/plan. (#1921) (#2025) +- **`roadmap update-plan-progress` no longer counts stray non-plan `*-SUMMARY.md` files against phase completion** — remediation/gap-closure summaries (e.g. `30-FIX-CR02-SUMMARY.md`, `30-GAPCLOSURE-SUMMARY.md`) inflated `summary_count`, and once `summary_count >= plan_count` the phase silently flipped to `Complete` (checkbox checked, date stamped) even though several plans had no summary. A new `countMatchedSummaries` helper (core-utils) pairs summaries to plans via the `PLAN→SUMMARY` marker swap + the `-SUMMARY.md` form (layout-agnostic across root, bare, and nested layouts), so only a summary that corresponds to a real plan counts. Wired into `scanPhasePlans` (fixing roadmap listing, state sync, verification, workstream inventory at once) and `cmdRoadmapUpdatePlanProgress`. (#1988) (#2016) +- **`milestone complete --ws` requirements archive header now points at the workstream REQUIREMENTS.md** — the archive header string hardcoded the root path (`` `…see .planning/REQUIREMENTS.md` ``), so a workstream archive directed readers at the wrong file even though #1917 had already fixed the archive *locations* to land inside the workstream. The display path is now derived from the same workstream-aware `reqPath` the writer uses (`path.relative(cwd, reqPath)`), so root behavior is byte-identical and the workstream case correctly reads `.planning/workstreams//REQUIREMENTS.md`. (#1993) (#2015) +- **Load-failed capability gates now fail open with a loud warning instead of blocking the whole project** — when an installed overlay (third-party) capability failed to load (e.g. an incompatible `engines.gsd` range) but had declared a `gate`-kind loop hook, the loop resolver injected a blocking synthetic gate (`blocking:true`, `onError:halt`) at every point where that capability declared a gate. A single incompatible capability therefore halted every `ship:pre` and `verify:post` in the project — unrelated to what the gate would have checked, and with no remediation surfaced. The resolver now injects no gate and instead emits a loud warning — to stderr and in the `loop render-hooks` envelope's `warnings` array — naming the load-failure reason and the exact `gsd capability remove ` remediation, and the loop proceeds (fail open). The capability id embedded in that remediation is validated against the canonical id shape first, so a malformed overlay directory name cannot inject shell metacharacters into the surfaced command. The loader still records `_overlay.blockedGates`; only the consequence changes from block to warn. `step`/`contribution` overlays were already skip-open. (#2009) (#2075) +- **`phase.complete` now updates the `## Progress` rollup row even when an earlier phase-numbered table precedes it** — the Progress-row writer used a non-global regex that matched *any* table row starting with the phase number, so it bound to the first such row (e.g. a `| Phase | Requirements | Count |` coverage table), no-op'd on the wrong 3-column row, and never reached the real Progress row. The regex is now scoped to the `## Progress` section so it binds to the correct table. The command still returned `roadmap_updated: true` (that field is `fs.existsSync(ROADMAP.md)`), masking the silent failure. (#2012) (#2032) +- **context7 now works for plugin-marketplace installs (8 agents regained doc lookup)** — the agents granted only `mcp__context7__*`, which matches a standalone context7 MCP server but not the official Claude Code plugin-marketplace install (`context7@claude-plugins-official`), whose tools are named `mcp__plugin_context7_context7__*`. The grant never matched, so advisor/ai/domain/phase/project/ui-researcher + planner + executor silently lost documentation lookup and fell back to WebSearch. All 8 agents now grant both forms, the researcher profile table is updated, and a parity guard asserts no agent grants the standalone form without the plugin form. (#2017) (#2029) +- **`applySurface` no longer deletes every `gsd-*` agent when the skills manifest resolves empty** — the agent-prune loop in `_syncGsdDir` deleted any `gsd-*.md` not in the staged set, and when the manifest was empty/unresolvable (null manifest, no array entries, no `files` key, or an unresolvable install source root), the staged set was empty → every agent was pruned. Skills were guarded by `pruneSkillDirs`'s manifest-membership check (conservative preservation on empty manifest); agents had no equivalent. The agent-prune loop is now skipped when the manifest is empty/absent, so agents are preserved while copy (adding genuinely new agents) still runs. (#2018) (#2031) +- **`planning-config.md` global-learnings path corrected to `~/.gsd/knowledge/`** — the `features.global_learnings` row directed users to `~/.gsd/learnings/`, but the implementation (`src/learnings.cts`, `execute-phase.md`) stores and reads global learnings from `~/.gsd/knowledge/`. Anyone following the docs to inspect, back up, or seed their global learnings looked in a directory the code never touches. (#2019) (#2026) +- **Removed dead SDK file references from runtime-loaded markdown that triggered an infinite `find.exe` storm on Windows** — `agents/gsd-executor.md` pointed at `sdk/src/query/QUERY-HANDLERS.md` and `gsd-core/workflows/reapply-patches.md` at `sdk/dist/cli.js`, both retired with the SDK package (ADR-0174). AI runtimes that resolve doc references by filesystem search ran `find / -iname …`; on Git Bash for Windows `/` maps to the drive root, so `find.exe` traversed the whole disk (14h+, orphaned processes, 4M+ open handles each, unkillable). The references now resolve to live paths, and a new regression guard asserts no `sdk/src|sdk/dist|sdk/handlers` file references remain in agents/workflows/references markdown. (#2020) (#2027) +- **`roadmap update-plan-progress` no longer checks the phase checkbox without verification** — the command stamped the phase-level ROADMAP checkbox and completion date the moment the last plan summary landed (called routinely after every wave and every plan), with **no verification gate** — unlike `phase.complete` which correctly requires `readVerificationStatus(...).status === 'passed'`. Now `isComplete` requires both all plan summaries AND a passed verification, matching the `cmdPhaseComplete` contract, so the checkbox only fires after `gsd-verifier` has confirmed the phase. (#2022) (#2030) +- **`phase complete` no longer marks a milestone done out of order, nor silently writes root state in workstream mode.** Completing the numerically-highest phase while an earlier phase was still outstanding wrongly flipped STATE.md to `Status: Milestone complete` (the milestone-end check only looked for higher-numbered phases, so an out-of-order completion — e.g. Phase 10 before Phase 9 — read as the end). It now reports milestone-end only when every lower-numbered phase in the milestone is checked complete. Separately, in workstream mode with no active workstream, `phase complete` previously fell back to root `.planning` and wrote STATE.md/ROADMAP.md (and the mislabel) into the shared root other workstreams read; it now fails safe — asking for `--ws ` or an active workstream — mirroring the existing `init progress` guard. (#2066) (#2066) +- **Phase directories whose slug begins with a single digit now resolve correctly.** A phase like `46-6-rs-pipeline-orchestrator` (roadmap name "6 Rs Pipeline Orchestrator") had its phase token over-collected as `46-6` instead of `46`, so `gsd-tools` phase-by-number lookups resolved `phase_dir=null` / `has_context=false` (breaking `init.plan-phase`, `init.phase-op`, and downstream execute/verify/ship). Numeric phase-token components must now be zero-padded (≥2 digits), so a single-digit slug word is no longer absorbed into the token. Fixed consistently across every same-class implementation — `extractPhaseToken`, `PHASE_TOKEN_FROM_DIR_RE` and `canonicalPlanStem` (health checks / plan pairing), `isDirInMilestone`'s numeric matcher (milestone filtering), and `extractCanonicalPlanId` — so the health-check and milestone-filter subsystems are fixed alongside phase resolution. (#2059) +- **`gsd-tools config-set null` now clears (removes) the key instead of persisting the literal string `"null"`.** The documented "Clear" action previously fell through the value parser and stored `"null"` — a truthy value — so "cleared" keys stayed set and `config-get` returned `"null"`; for secret keys (`brave_search`/`firecrawl`/`exa_search`) a masked success line hid a truthy value on disk that integrations could pass along as a real credential. `config-set null` now deletes the key (short-circuiting the typed per-key validators so clearing an enum/boolean/number key removes it rather than being rejected), making the "Clear" flows in `settings-integrations.md` / `settings-advanced.md` actually clear. (#2058) +- **`init plan-phase` no longer collapses foreign-prefixed task/workstream IDs into numeric phases** — a query like `MEM-01` (where `MEM` is not the configured `project_code`) used to have its prefix stripped and resolve to the unrelated numeric Phase 01; it now reports `phase_found: false` unless a phase directory or roadmap entry literally carries that prefix. The configured `project_code`'s own prefixed phases (e.g. `LKML-01` under `project_code: LKML`) continue to resolve as before. (#2056) (#2105) +- **`phase complete` no longer ticks the wrong phase's ROADMAP checkbox** — completing a phase whose number also appears in a later phase's description (e.g. an idempotent re-run of an already-complete phase) used to mark the *wrong* phase done, because the checkbox-matching regex greedily spanned from `]` to any later "Phase N" mention instead of only the immediately-following phase title. (#2067) (#2079) +- **`gsd-tools effort sync` no longer crashes in an installed runtime.** In any global install (e.g. `~/.claude/gsd-core/`), `effort sync` threw `Cannot find module '../../../bin/install.js'` — the command reached into the package-root `bin/install.js` for its install-time effort resolvers, but the installer only copies the `gsd-core/` subtree into a runtime home, so that file is never present there. As a result, `effort` config changes (`routing_tier_defaults` / `agent_overrides`) silently never reached installed agents without a full reinstall. The two resolvers (`readGsdEffectiveEffortConfig` + `resolveInstallTimeEffort`, with their helpers) are now extracted into a shipped `gsd-core/bin/lib/install-effort-resolver.cjs` that both `effort sync` and the installer import — a single source of truth that is always present in the installed tree. (#2076) (#2076) +- **`model_overrides` and per-phase-type models now actually apply to the assumptions-analyzer, code-reviewer, and code-fixer agents on Claude Code.** Previously `model_overrides["gsd-code-reviewer"]` / `["gsd-assumptions-analyzer"]` / `["gsd-code-fixer"]` (and `models.verification` / `models.discuss` / `models.execution`) were accepted and resolved but silently dropped — the workflows spawned these agents with no model, so they inherited the session model and the configured routing never took effect (no warning). Every spawn now threads its resolved model: `discuss-phase-assumptions`, `code-review`, and `code-review-fix` (both the re-review and the two fixer spawns) resolve it inline, and `quick`'s review step uses the code-reviewer's own resolved model instead of the executor's. The stale "`discuss` — reserved, no subagent" model-profile docs are corrected to list `gsd-assumptions-analyzer`, and the `verification` row now includes `gsd-code-reviewer`. (#2074) (#2074) +- **`/gsd-review`'s Antigravity CLI reviewer no longer fails silently on large prompts, unavailable pinned models, or pre-session stalls** — the `agy` invocation now uses a file-reference prompt to avoid exec arg-list overflow, is wrapped in an external wall-clock `timeout` paired with `--print-timeout` because `--print-timeout` cannot fire before `agy` creates a session, passes `--model` from `review.models.agy` when set as an escape hatch for a 404'd pinned model, and its empty-output stub now surfaces an `agy` cli.log diagnostic instead of a bare generic message. Supersedes the #687 "no external killer / inline `$(cat)`" contract, which predated `agy` gaining `--model` and predated its own guidance to pair `--print-timeout` with a terminal timeout. (#2073) (#2109) +- **`init execute-phase`, `init verify-work`, and `init phase-op` no longer collapse foreign-prefixed task IDs to numeric phases** — `MEM-01` under `project_code: LKML` was silently stripped to `01` and resolved to the unrelated numeric Phase 01, because the #2056 guard was applied only to `init plan-phase`. The guard is now extracted into shared helpers (`guardedFindPhase` / `guardedGetRoadmapPhase`) that delegate to the canonical `isForeignPrefixedPhaseQuery` from `phase-id.cts`, and all four init commands route through them. (#2104) (#2149) +- **`commit --files` now commits only the declared paths** — `gsd-tools commit --files A B` previously ran a bare `git commit` that absorbed the entire staged index, silently sweeping in unrelated files the caller never named. The commit now appends a pathspec (`-- `) so only the staged subset of `--files` lands in the commit; the no-`--files` default path is unchanged. Missing tracked files are still skipped (not committed as deletions, #2014), and when all declared files are missing the function short-circuits to `nothing_to_commit` instead of absorbing the index. (#2112) (#2148) +- **Fixed unresolvable bare `require('gsd-core/...')` in `gsd-surface` command doc** — the four `require()` examples now derive the engine path from `runtimeConfigDir` (resolvable at runtime), and the reinstall hint corrects `npm i -g gsd-core` to `npm i -g @opengsd/gsd-core`. (#2116) (#2213) +- **`milestone complete --dry-run` now prints a preview plan instead of silently mutating** — `gsd-tools milestone complete --dry-run` was neither parsed nor rejected, so a caller expecting a preview triggered the full destructive mutation (archive phases, move audit artifacts, rewrite STATE.md) with no way to back out. The `--dry-run` flag is now honored: it returns a JSON plan listing `would_archive` (roadmap, requirements, audit, phase dirs) and `would_update` (MILESTONES.md, STATE.md) targets with zero filesystem mutations. (#2118) (#2155) +- **`/gsd-secure-phase` now has a single SECURITY.md writer** — the `gsd-security-auditor` subagent previously held `Write`/`Edit` tools and was instructed to "write SECURITY.md" with no padded `-` prefix and no template frontmatter, while the orchestrator's Step 6 also wrote the phase-scoped `-SECURITY.md` from `templates/SECURITY.md`. The auditor is now return-only (drops `Write`/`Edit`, returns a structured verdict with `threats_open`); the orchestrator is the sole file writer. The workflow's Step 5 spawn constraints explicitly forbid the auditor from writing SECURITY.md. (#2119) (#2154) +- **Dead security scan exports removed; injection-scan docs corrected to match reality** — `scanEntropyAnomalies` and `shannonEntropy` were dead code with zero production callers (live hooks inline their own patterns for independence). REQ-SCAN-INJ-02/-03 now accurately describe what runs live (injection patterns, invisible Unicode) vs CI-only (base64-decode, codebase scan). (#2198) (#2211) +- **Custom STATE.md frontmatter keys are no longer dropped on every mutating verb** — syncStateFrontmatter rebuilt the frontmatter from a fixed schema, silently dropping any custom key. It now carries forward existing keys the schema does not own. (#2202) (#2233) +- **Non-frontend phases with `UI hint: no` are no longer blocked by the UI-SPEC gate** — the UI safety gate's token list included the bare token `UI`, which matched GSD's own `**UI hint**: no` metadata line and false-detected a UI, blocking backend/infra phases at /gsd-plan-phase. An explicit `UI hint: yes|no` is now authoritative and the hint line is no longer token-sniffed. (#2150) (#2222) +- **OpenCode reviewer no longer silently yields an empty review on large prompts** — `/gsd-review --opencode` now invokes `opencode run --format json` and reconstructs the review from the assistant text parts, so a large-prompt run where the default `build` agent ends its turn with zero output tokens no longer produces an empty stub. When the agent genuinely emits no text, the stub now reports the stop reason, output-token count, and captured stderr instead of a generic message. (#1936) (#1992) +- **`stale-bake-guard` hermeticity fix (test-isolation)** — the readGsdEffectiveModelOverrides subtest no longer reads the developer's real `~/.gsd/defaults.json`; the resolver now accepts a homedir seam so the test sandboxes HOME. (#2152) (#2223) +- **`/gsd-surface` (`list`/`status`) works on Claude Code global installs** — the installer now writes a `.gsd-source` marker pointing at its `commands/gsd` source, so `findInstallSourceRoot` resolves on the global skills layout (which ships no `commands/gsd` tree) instead of throwing `could not locate commands/gsd`. (#1487) (#1487) +- **`phase complete --phase N` now works alongside the positional form** — the phase verb family treated the first positional as the phase number, so `--phase 12` was passed as the literal phase name and failed with 'Phase --phase not found'. The phase family now accepts the --phase flag consistently with the state family, and unrecognized flags yield a usage error. (#2201) (#2231) +- **Third-party capability skills now surface correctly after install** — a skills-only `role: feature` capability installed `active` but its skills never reached the runtime surface, `capability enable`/`set` rejected it as `unknown capability`, and `capability list` disagreed with `capability state`. `resolveSurface` now unions the composed registry's `capabilityClusters` into the surfaced skill set (no on-disk linking), the writer validates against the composed overlay-aware registry, and `capability list` carries a `surfaced` field matching `capability state`. (#2054) +- Fixed: probe-core's runProbeCli now fails closed on per-item adapter garbage inside a well-shaped report envelope, matching its documented 'fails closed on adapter garbage' contract. (#1910) +- **`/gsd:verify-work` no longer silently terminates when all remaining UAT tests are blocked** — sessions with `blocked_count > 0` and `pending_count == 0` now route to `complete_session` as expected, enabling the zero-issues auto-transition path. (#1722) +- **state record-metric no longer appends per-plan rows into the By-Phase velocity table** — it now maintains its own Per-Plan Metrics table (self-created on first use), and its auto-create scaffold header is corrected. (#2253) (#2253) +- Codex reviewer now captures the review via codex's --output-last-message flag instead of redirecting stdout, so Windows process-teardown output no longer pollutes the review file and slips past the empty-output guard. (#1709) +- **`last_activity` now shows your local calendar day** — the clock seam derived the date by slicing a UTC instant, so in negative-UTC-offset zones during UTC's early evening the date-only `last_activity` field jumped a day ahead of the operator's actual date (and of `last_updated`'s local date). Operator-facing date fields now use a host-local calendar day while internal/cosmetic stamps stay UTC. (#2136) (#2216) +- **`milestone_name` is no longer clobbered with a delimiter-led fragment** — getMilestoneInfo's `##` heading regex was unanchored, so it matched a heading quoted inside backticks in the Milestones bullet and wrote garbage like `— Active Milestone` over the curated milestone name on every phase transition. Now consults the 🚧 marker first, anchors the regex to line start, strips the leading delimiter, and widens the preserve guard so a bad derive keeps the existing name. (#2135) (#2215) +- **`init milestone-op` now counts project_code-prefixed phase directories correctly** — fully shipped milestones using the standard prefixed directory layout no longer report `completed_phases: 0` or stay falsely incomplete. (#1844) (#1844) +- **`/gsd-quick` no longer halts with a stale-base worktree mismatch** — the worktree executor now degrades to sequential execution when its fork base has diverged from origin/HEAD, instead of spawning a worktree guaranteed to fail the base-mismatch guard. (#1991) +- **Setting `external_job.submit_timeout_ms` / `poll_timeout_ms` / `artifact_dir` in `.planning/config.json` now actually configures the SLURM adapter** — the keys were declared by the external-job capability but the adapter only read env vars, so config edits silently had no effect. The adapter now resolves them through the canonical capability-config seam (env override > config > registry default), surfaces the resolved `artifact_dir` in `submit` output, documents why the contribution registers at `execute:wave:post` (#1164 asks for `wave:pre`, which `execute-phase.md` does not dispatch today; wiring it is a core-loop change #1164 explicitly defers), and gains unit coverage for the CLI surface (`parseFlags`, `findPlanningDir`, `resolveExternalJobSettings`, `formatShowReport`). (#1164) (#2006) +- **The Antigravity reviewer in `/gsd-review` no longer reviews blind** — `agy -p` never granted the agent the repo under review, so it frequently anchored on its own scratch directory and returned plan-text-only verdicts counted at full consensus weight. The reviewer is now granted the repo (capability-probed `--add-dir`) and anchored to the absolute repo root; a review that still runs without repo access is stamped `[reviewed-without-repo-access]` and down-weighted in the Consensus Summary. The cursor-agent prompt gains the same absolute-root anchor. (#2176) (#2184) +- **Autonomous reruns now skip phases with deferred verification until you resume them explicitly** — if a prior `/gsd-autonomous` run recorded `verification_deferred_human` or `verification_deferred_gaps`, later reruns no longer drop back into the same prompt loop and instead point you at the saved resume command. (#1846) (#1846) +- **`requirements mark-complete` no longer reports silent success when the traceability row is missing** — it OR-ed its checkbox and table-row writes into one flag, so a checkbox-only reconcile returned a payload byte-identical to a full reconcile while the traceability row stayed Pending (and re-run masked it as already-complete). It now surfaces `table_unmatched` for IDs whose checkbox reconciled but whose table row is absent, and treats a checked box with no table row as partial rather than done. (#2140) (#2219) +- state prune now resolves the current phase from the canonical location — frontmatter current_phase, the Current Phase field, or the prose Phase: line scoped to the ## Current Position section — instead of extracting Phase over the whole document, where stateExtractField's pipe-table fallback could latch onto an unrelated | Phase | N | row (e.g. a historical verification table) and compute a wrong prune cutoff. (#1832) +- **`model_overrides` Claude model IDs now resolve to Agent-tool aliases on the claude runtime** — a full Claude model ID (e.g. `claude-sonnet-5`) in `model_overrides` was returned verbatim and silently dropped by the Claude Agent tool (whose `model` parameter documents only tier aliases), causing the spawned subagent to inherit the parent session model instead of the configured one. It now maps to the tier alias (`sonnet`/`opus`/`haiku`/`fable`), consistent with the `model_policy` path (#1144). Bare aliases, non-Claude values, and non-Claude runtimes are unchanged; a Claude ID with no alias warns once and falls through to tier resolution. (#2041) (#2048) +- Phase headers that place a parenthetical tag before the colon (`### Phase 26 (Cluster B): Title`) now resolve and enumerate the same as untagged headers. Previously the resolver returned not-found and `roadmap analyze`/listing silently dropped the phase (wrong phase_count, progress, and next_phase). Tag tolerance is applied at every phase-header read site; untagged and all existing header formats parse unchanged. (#1765) +- Executor and milestone-summary/forensics workflows now call state.* commands with named flags so the named-only router records metrics, decisions, blockers, and session continuity instead of silently dropping positional args. (#1873) +- **bug-1367 install test no longer fails on Windows CI when hooks/dist isn't pre-built** — the test ran install.js without building its hooks/dist precondition (a gitignored build artifact the unit lane doesn't build), so on a lane without pre-built hooks the installer hit "Failed to install hooks: directory is empty" and the before-hook threw. The test now builds hooks in its own before() (mirroring golden-install-parity). (#1926) (#1927) +- **`/gsd-fast` now appends Quick Task rows to STATE.md again** — the log_to_state column-count guard used an off-by-one awk formula (`NF-1`) that was always one too high, so the schema gate rejected the very table quick.md creates and silently skipped the STATE.md update. Also now supports the 6-column validate-mode table. (#2133) (#2214) +- Build the gitignored `hooks/dist/` artifact once upfront in `scripts/run-tests.cjs` (the same chokepoint as `ensureBuiltArtifacts`), before any concurrent install test spawns `install.js`. Closes the scoped-CI first-build empty-dir race that intermittently failed install tests with `Failed to install hooks: directory is empty` (e.g. `bug-3683-workflow-colon-namespace-leak`). (#1967) (#1968) +- **workstream progress no longer reports shipped milestones as `executing`** — `gsd-tools workstream progress` now derives each workstream's status from authoritative shipped signals (an archived milestone snapshot under milestones/, or a SHIPPED marker in the workstream ROADMAP) instead of trusting the mutable STATE.md `Status` field, so a stale field can never hide a shipped/archived milestone. The output adds `status_source` (`field` | `derived`) and `status_conflict` (true when the derived value disagrees with the stale field). (#1913) (#1916) +- **Windows install/upgrade/state-write operations no longer fail on transient antivirus/indexer file locks** — the fs.renameSync atomic-publish sites (install state, hooks config, capability ledger/lifecycle, phase/workstream/milestone dirs, roadmap, planning/state locks) now retry EPERM/EBUSY/EACCES via retryRenameSync instead of propagating the transient lock; enforced by the new local/require-fs-op-fallback lint rule (ADR-1703 Phase 6). (#1740) (#1742) +- reconstructFrontmatter now emits valid YAML for scalars and block-array items that were previously serialized unescaped. Values carrying a YAML indicator plus a literal quote/backslash, embedded control characters, the empty string, a leading YAML indicator, or leading/trailing whitespace are now routed through a properly escaped double-quoted form, so frontmatter round-trips through strict parsers (js-yaml, PyYAML) instead of corrupting the block on the next state sync. (#1807) +- **`phase remove` no longer destroys the Progress table when removing the last phase** — deleting a phase used a whole-document regex whose scan, on the final phase, ran past the section and swept away the `## Progress` heading and its entire tracking table; the deletion is now structurally bounded to the phase’s own section. (#2253) (#2253) +- **`phases clear` archives phase directories instead of destroying them** — at a milestone switch, committed phase directories were hard-deleted (`rmSync`) with no archive, silently losing browsable phase history (the #1447 dirty-tree guard was a no-op for the common committed case). Phase directories are now moved to `milestones/-phases/` (collision-safe; timestamp fallback when no version resolves), so history survives the switch. The #1447 uncommitted-changes guard is retained as a secondary backstop. (#1871) (#1919) +- **Cross-AI review no longer silently drops the Codex/Claude/Gemini lanes on large plan sets** — the prompt-fed reviewer blocks in review.md invoked each CLI with no explicit timeout, so a slow source-grounded review was killed at the host default (~2 min) and the lane was silently lost. The workflow now directs a high Bash timeout and frames an empty output as a timeout (not the crash it was misdiagnosed as). (#2194) (#2226) +- **`/gsd-progress` no longer reports a stale root milestone in workstream mode** — in a multi-workstream project with no active workstream set, `gsd-tools query init.progress` silently fell back to root `.planning/STATE.md` (often stale) and reported it confidently. It now fails safe with an actionable error naming the available workstreams and the `--ws`/`workstream set` fix, so a stale root value is never reported. Flat mode and `--ws ` are unchanged. (#1912) (#1918) +- Windows: stop double-quoting $CLAUDE_PROJECT_DIR-anchored managed node hook paths during the #2979 legacy rewrite, which produced "\"$CLAUDE_PROJECT_DIR\"/..." and broke every node managed hook with MODULE_NOT_FOUND (PreToolUse-guard deadlock). (#1746) +- **phase complete now updates STATE progress on milestone-grouped roadmaps** — deriveProgressFromRoadmap parses the ## Progress table by header (column-by-name) instead of a fixed 4-column layout, so the 5-column milestone-grouped shape is no longer silently unparsed. (#2168) +- **Windows Claude Code hooks now work under PowerShell** — when Claude Code's hook runner resolves to PowerShell (not Git Bash), every GSD-installed hook failed with `Unexpected token` because the installer emitted bare quoted paths with no PowerShell call operator. The fix adds a `hookShell` parameter to the hook-command projection chain; when `hookShell='powershell'`, the `&` call operator is prepended. Default behavior (Git Bash, no prefix) is unchanged. (#2236) (#2261) +- **`capability state` and `loop render-hooks` now accept `--runtime` to override the auto-detected runtime** — previously both commands parsed only `--config-dir`, so the runtime config dir was derived from the persisted `.planning/config.json` runtime (precedence `GSD_RUNTIME` → `config.runtime` → `claude`). A repo that persisted `runtime:"codex"` resolved the config dir to `~/.codex`, where the Claude skill isn't installed, so every skill-bearing capability reported `surfaced:false` and `execute:post`/`verify:post` hooks silently no-op'd when the operator drove GSD from Claude Code. `--runtime ` (canonicalized, so aliases like `codex-app` work) now bypasses that fallback so the config dir resolves to the explicitly-named runtime's home. Behavior without the flag is unchanged. (#2003) (#2051) +- **`phase complete` no longer false-reports REQ-IDs as missing when the traceability table leads with a status column** — the parser required the REQ-ID in the first column, so a table shaped `| ☐ | REQ-01 | …` matched zero rows and every body REQ-ID was reported missing. It now matches REQ-IDs in any column. (#2203) (#2234) +- **`init milestone-op` now ignores backlog `999.x` headings when counting milestone phases** — parked backlog items no longer inflate `phase_count` or pin `all_phases_complete` false for an otherwise finished milestone. (#1843) (#1843) +- **Phase archival is now wired end-to-end across the milestone lifecycle** — finishes the #1871 follow-up: `phases archive` is now a real command (the half-wired alias is routed, no longer errors Unknown), `milestone complete` archives phase dirs by default (`--no-archive-phases` opts out), and `new-milestone` §6 stages the archive move + source removal in the same commit so history is preserved atomically rather than left as orphaned uncommitted deletions. (#1871) (#1924) +- **`state update-progress` no longer mangles the frontmatter and discards the progress suffix** — its Progress: regex matched the raw STATE.md including frontmatter, so the YAML `progress:` key was hit first (corrupting the frontmatter) while the body line stayed stale and was silently reverted on the next write, and any descriptive suffix after the progress bar was destroyed. It now targets the body line only and preserves the suffix. (#2177) (#2224) +- **`/gsd-ship` no longer silently drops the ship-status note from STATE on merge** — the track_shipping step committed the STATE ship-note after creating the PR but never pushed it, so on a fast merge the note stayed local-only and never reached the default branch. The ship-note is now pushed onto the PR branch with a `[ci skip]` trailer so it lands on merge without a redundant pipeline. (#2138) (#2217) +- **`/gsd-debug` no longer stalls on a phantom background handoff** — the orchestrator treated the foreground session-manager spawn as a background task and queried its agent ID via TaskOutput (which needs a task ID), then waited on a handoff that was never queryable. The workflow now states the spawn is foreground/blocking, forbids passing an agent ID to TaskOutput, and gives a lost-handoff recovery path. (#2196) (#2227) +- **Roadmap phase lookup now ignores fenced examples and the backlog sentinel lane** — `roadmap get-phase` and `init plan-phase` no longer return fenced sample headings as real phases or treat `999.x` backlog items as active milestone work. (#1845) (#1845) +- **`phase complete` no longer checks the wrong ROADMAP checkbox or writes the plan count into a shipped milestone** — the roadmap mutators ran unanchored and un-milestone-scoped, so they could flip a bullet inside a backticked prose literal or a Backlog entry instead of the closing phase's, and write the plan count into a same-numbered phase in a shipped milestone. The checkbox flip is now line-anchored and both writers are scoped to the current milestone. (#2200) (#2229) +- **`roadmap get-phase` resolves project-code-prefixed headings by bare number** — a bare-number query (e.g. `29`) now resolves a drifted `### Phase AB-29:` heading, matching the internal resolver used by `init.phase-op`; previously the CLI returned empty. A bare sibling (`### Phase 29:`) still takes precedence. A project-code-prefixed heading present only as a summary/checklist line (no matching detail section) now reports a `malformed_roadmap` diagnostic — for both prefixed and bare-number queries — instead of a silent empty result. (#2114) (#2139) +- **`milestone complete --ws` now archives into the workstream instead of root** — the archive paths (MILESTONES.md, the milestones/ archive dir, and the per-version MILESTONE-AUDIT.md) were hardcoded to root `.planning/`, so a workstream milestone close scattered its artifacts into root and never produced a workstream-local archive. They now derive from the workstream-aware planning base (`planningPaths(cwd).planning`); flat-mode (no --ws) is unchanged. (#1911) (#1917) +- **`phase complete` now reads milestone-grouped ROADMAP progress tables** — progress reported 0% on projects whose Progress table carries a Milestone column, because the reader assumed a fixed column position; it now resolves progress columns by name so both flat and milestone-grouped tables work (#2137). Quick Tasks logging via `/gsd:fast` also appends schema-correct, lock-safe rows instead of guessing the column count in shell (#2133). (#2248) (#2248) +- **Skill-bearing capabilities now surface correctly on flat command-layout installs** — on an install using the flat `commands/gsd-.md` source layout (e.g. a Claude Code local project install with no `commands/gsd/` subdir), every skill-bearing capability (`nyquist`, `code-review`, `security`, `ui`, `mempalace`, `ai-integration`, `profile-pipeline`) was silently reported `surfaced:false`/`enabled:false`/`active:false`, so their loop hooks (`verify:post`, `execute:post`, etc.) never fired even with the corresponding `workflow.*` toggle on. The skill-manifest resolver now detects the flat layout and produces the same stems the nested `commands/gsd/*.md` loader does. (#1858) (#2049) +- **Roadmap, requirements, and state table edits are confined to the right table** — the last ad-hoc table writers (phase completion updating roadmap progress, `requirements mark-complete`, and `state record-metric`/velocity) now route through the shared markdown-table seam, so a stray decoy table elsewhere in a document can no longer swallow a phase-progress update, a single ragged neighbouring row no longer silently aborts the whole edit, and per-plan metric recording no longer drops trailing section content or duplicates the section. (#2253) (#2253) +- **ROADMAP phase edits can no longer escape their section** — completing a phase updated its plan count and per-plan checkboxes with whole-document regexes that could bleed into a neighbouring phase; those per-phase writes are now structurally bounded to the phase own section via a new `withSection` / `withPhaseSection` seam (#2130, #2067, #2080). (#2250) (#2250) +- **STATE.md `## Session` fields now resolve on Windows** — the session-section reader used a `\n`-only heading regex that silently failed on a CRLF `## Session` heading, nulling all session state on Windows checkouts; it now reads through the CRLF-safe section seam. (#2253) (#2253) +- **Bullet/em-dash ROADMAP phases no longer resolve to `Phase null`** — the roadmap phase lookup matched only ATX headings with a colon, so a bullet entry like `- [ ] **Phase N — Name**` (which the roadmapper emits) failed to resolve and `Phase null` landed in STATE.md; a bullet-only ROADMAP also broke the milestone phase count. Phase lookup and the milestone filter now accept bullet/checkbox entries with an em-dash/en-dash/hyphen/colon separator. (#2199) (#2228) +- **Linuxbrew users no longer lose all GSD-managed hooks after `brew upgrade node`** — normalizeNodePath only recognized macOS Homebrew Cellar paths, so on Linux the version-pinned node path stayed baked into hook commands and 404'd after a node bump (and reinstall couldn't repair it). It now rewrites any Homebrew Cellar path — Intel, Apple Silicon, Linuxbrew, custom HOMEBREW_PREFIX — to the stable `/bin/node` symlink. (#2185) (#2225) +- **`milestone complete` no longer corrupts the recorded phase** — closing a milestone (e.g. `v0.5`) previously overwrote `current_phase` in STATE.md with the version's minor digit, and a follow-up `state complete-phase` mined a bogus `0.5` token and rewrote the file; phase resolution is now anchored so the real phase is preserved and a milestone-closure line is rejected. (#2111) (#2131) +- **Headless MemPalace capture no longer fails silently** — the headless invocation `mempalace mine --wing --room ` used a `--room` flag that does not exist on the `mine` subcommand (only `search` accepts `--room`), causing every headless/no-MCP capture run to fail with `unrecognized arguments: --room` and silently skip (onError: skip). The fix replaces the flag with MemPalace's documented room-assignment mechanism: stage the artifact under a room-named subfolder with a `mempalace.yaml` taxonomy so `detect_room()` assigns it via folder-path match. (#2220) (#2260) +- Fixed: a hand-authored non-inferable backstop truth with a stray trailing space or surrounding quotes no longer silently grades green — it correctly abstains (insufficient_spec), restoring the #1154 honest-verifier guarantee. (#1909) +- **`commit_docs` no longer silently disables on CRLF `.gitignore` repos** — git check-ignore falsely reports a trailing-slash path (e.g. `.planning/`) as ignored when the .gitignore has CRLF line endings with blank lines. isGitIgnored now strips trailing slashes before querying, so the false positive cannot occur. (#2206) (#2235) +- **Phase-directory resolution fails loud on cross-project collisions** — when two unrelated GSD projects share a `.planning/phases/` tree, a bare phase number silently resolved to the first `0N-*` directory found. The fix detects multiple matches and surfaces an `ambiguous_matches` result. (#2237) (#2262) +- **`scanPhasePlans` no longer counts PLAN-REVIEW artifacts as executable plans** — `*-PLAN-REVIEW.md` files were counted by the loose `/PLAN/i` fallback. The fix adds a `PLAN_REVIEW_RE` exclusion before the fallback. (#2252) (#2263) +- **Milestone audit no longer flags a not-yet-validated phase as a Nyquist failure** — a phase that was planned but never run through `validate-phase` now reports as NOT-VALIDATED (a "run validate-phase" TODO) instead of collapsing into PARTIAL alongside phases whose validation genuinely failed. (#2117) (#2209) + +### Security + +- **`gate="blocking-human"` checkpoints are no longer auto-approved by the execute-phase orchestrator** — the package-legitimacy gate (#2827) spans two layers: `gsd-executor` refuses to auto-approve a `gate="blocking-human"` checkpoint and escalates it via `checkpoint_return_format` so a human can vet the package, and `execute-phase`'s `checkpoint_handling` step decides what happens next. That step dispatched purely on checkpoint *type* and never read `gate`, so under `--auto` / `--chain` it immediately auto-approved the very checkpoint the executor had just refused to auto-approve (`human-verify → {user_response} = "approved"`). The slopsquatting defence was therefore inert in exactly the unattended mode where nobody is watching: an `[ASSUMED]`/`[SUS]` package reached install with no human ever seeing the verification prompt. `checkpoint_handling` now carves out `gate="blocking-human"` (and the package-legitimacy `what-built` markers) ahead of every auto-mode branch, routing those checkpoints to the standard present-to-user flow regardless of type. `references/checkpoints.md` documents the `gate` attribute and its two values for the first time — previously `blocking-human` appeared nowhere outside `agents/gsd-executor.md`, so no planner had a documented way to author a checkpoint that auto-mode could not bypass. The existing regression test asserted the executor half only; it now asserts the orchestrator half too, which is why it stayed green while the gate was open. (#2107) (#2113) +- **Hardened phase/roadmap/plan markdown parsing against quadratic-time (ReDoS) CPU exhaustion** — a crafted `ROADMAP.md`, `STATE.md`, or `PLAN.md` with large runs of unclosed `(`, `[`, ``, `` HTML-comment markers embedded near the top of each of the five step workflow files (`discuss-phase.md`, `plan-phase.md`, `execute-phase.md`, `verify-work.md`, `ship.md`). Generated by `scripts/gen-loop-host-contract.cjs` → `gsd-core/bin/lib/loop-host-contract.cjs` (ADR-894 §3 phase 3a-impl-2). Covers exactly the 12 canonical points (discuss:pre/post, plan:pre/post, execute:pre/wave:pre/wave:post/post, verify:pre/post, ship:pre/post). The generator enforces a drift guard: every declared non-orchestrator agent role must correspond to an actual agent reference in the workflow file. Consumed by `gen-capability-registry.cjs` (replaces the former inline `LOOP_HOST_CONTRACT` constant). Run `node scripts/gen-loop-host-contract.cjs --write` after editing a workflow step marker. +### Gate Predicate Evaluator Module +Pure, deps-injected evaluator for capability gate `check.predicate` blocks (#2008, ADR-2008). Prior to #2008 the registry validator accepted `check.predicate` (one of exactly-one-of `query`/`predicate`/`agentVerdict`) and the loop-resolver rendered it, but nothing EVALUATED a declared predicate — only `check.query` was enforced (dispatched via `gsd_run check `), and built-in gates like `security` worked only via hard-coded `capId` prose branches in `ship.md`/`execute-phase.md`/`verify-work.md`. This module is the generic evaluation path: `evaluatePredicate(predicate, context, deps) → { block, message, details? }` dispatches by `predicate.kind` through a `KIND_TABLE`. Built-in kind (v1): `command-exit-zero` — runs a declared command in a bounded `sh -c` subprocess (production binding: `shell-command-projection.execTool`) at the project root, inheriting env; exit 0 ⇒ pass, non-zero ⇒ block, timeout (SIGTERM) ⇒ block; interpolates `${PHASE_NUMBER}`/`${PHASE_DIR}`/`${PHASE_REQ_IDS}` from gate context. A THROWN error (malformed predicate, non-positive/non-finite timeout, command >4096 chars, unknown kind) maps at the CLI seam to a non-zero check-command exit, which the workflow's two-step gate contract treats as a step-1 command failure routed per `onError` — so an evaluator bug is never conflated with a legitimate block decision. Leaf pure module (no fs/child_process/config — subprocess seam injected). CLI entry: `gsd_run check predicate --predicate '' [--phase-dir …] [--phase-number …] [--phase-req-ids …] --raw`, wired into `check-command-router.cts:cmdCheckPredicate`; the three generic workflow gate-dispatch sites (`execute:wave:post`, `execute:post`, `plan:post`) branch on `check` shape (`query` vs `predicate`). Source of truth: `src/gate-predicate-evaluator.cts`. Docs: `docs/reference/gate-predicates.md`, `docs/how-to/command-exit-zero-gate.md`. + ### Capability Registry Generated central manifest projecting all co-located Capability declarations into one validated artifact for runtime resolution and for the install, surface, config, and loop-extension adapters. Mirrors the research-profiles / package-identity generation pattern (co-located source → generated central file). Generated by `scripts/gen-capability-registry.cjs` → `gsd-core/bin/lib/capability-registry.cjs` (ADR-894 §5 phase 3a-impl). Role-partitioned indexes: `bySkill`, `byAgent`, `byLoopPoint` (hook ordering materialized), `configKeys` (ownership map: key→capId), `configSchema` (full per-key schema: key→{ owner, type, default, description }), `runtimes`, `requiresClosure(id)`. Each feature capability's entry in `capabilities` now includes the optional `activationKey` field (the dotted config key that gates the whole capability, e.g. `"graphify.enabled"`; absent means no config gate). ADR-857 phase 3b adds `configSchema` with validated type/default/description per key, sourced from each capability's `.config` slice. ADR-857 phase 4a adds two derived views: `capabilityClusters` (`{ : [] }` — each cap's skills array, sorted, derived from the capability's `skills` declaration; consistency-gated against the hand-authored `CLUSTERS`) and `profileMembership` (`{ : { tier, profiles: [...] } }` — the tier-derived index: suffix of `PROFILE_RANK` starting at the capability's tier). Both views cover the same capability set: only capabilities that own skills (non-empty `skills` array). The generator enforces a HARD gate (throws) if a capId matching a `CLUSTERS` key has a mismatched skill set, and emits SOFT `⚠ pending-reconciliation` warnings to stderr (never to the file) for skills not yet in the hand-authored profile at the capability's tier. `install` and `surface` are UNTOUCHED (still read hand-authored constants; derived views are emitted and tested but unconsumed until cutover). Validated against the Loop Host Contract (12 points; generated by `gen-loop-host-contract.cjs` from workflow markers, phase 3a-impl-2). Run `node scripts/gen-capability-registry.cjs --write` after editing any `capabilities//capability.json`. @@ -184,6 +208,12 @@ ADR-857 phase 3b seam that merges capability-declared config slices into the `lo ### Capability Registry Overlay Runtime seam (`gsd-core/bin/lib/capability-loader.cjs`, ADR-1244 D2) that composes the frozen first-party Capability Registry (`capability-registry.cjs`) with a validated installed overlay of third-party capability manifests discovered at load time. Install roots are global (`$GSD_HOME/.gsd/capabilities//capability.json`, where `GSD_HOME` defaults to `~`) and project (`/.gsd/capabilities//capability.json`). Primary interface: `loadRegistry({ includeInstalled }) → registry` — when `includeInstalled` is true the overlay is merged via the canonical `buildRegistry` so all derived views (bySkill, byAgent, byLoopPoint, configKeys) cover first-party and overlay entries identically. First-party always wins: any overlay entry whose id, owned skill/agent stem, or federated config key collides with first-party, or whose id uses a reserved `gsd-`/`gsd-core-`/`anthropic-` prefix, is rejected at load time. Load-time re-gate: an overlay failing schema validation or whose `engines.gsd` semver range does not satisfy the running GSD version is skipped with a warning and never crashes the load loop. Per-hook-kind policy: a skipped capability that declared a `gate`-kind hook fails CLOSED (the loop resolver injects a blocking gate); skipped `step` or `contribution` capabilities skip open. A capability dir whose co-located ledger entry carries an in-flight `_pending` intent (a crashed/uncommitted install or upgrade, ADR-1244 Phase 4) is skipped OPEN (never activated until reconciliation commits or rolls it back). #1459 user-owned consent gate: a PROJECT-scope overlay is activated (declarative surfaces AND command dispatch) ONLY when the user-owned Capability Consent Store holds a record for `(realpath(projectRoot), id)` whose stored `contentHash` equals the bundle content hash the loader RECOMPUTES at load (`bundleContentHash(capDir)` over the whole on-disk bundle) — NOT the repo-plantable ledger integrity nor the executable-only disclosure signature — otherwise the cap is DISCOVERED-BUT-INACTIVE (a warning carrying `kind:'unconsented'`, no surfaces, empty commandRoots), so a forged/cloned in-repo project ledger or any post-consent tamper no longer activates anything; GLOBAL scope (under the user's own home) is trusted without a record, and the global-vs-project root dedup/escalation is realpath-keyed so a symlinked `GSD_HOME` aliasing the project root cannot bypass the gate (finding 1). The consent lookup is wrapped to fail CLOSED (inactive); both the per-scope ledger AND the `capability.json` manifest are read via the shared bounded `readSmallRegularFile` (a repo-planted FIFO/oversized ledger or manifest can no longer hang or OOM the loader — finding 2). The loader reuses the ledger's shared `isValidLedgerEntry` for committed-entry parity. Consumers wired to the overlay-aware registry: `config-loader.cjs`, `config-schema.cjs`, `capability-state.cjs`, `loop-resolver.cjs`. +### Community Capability Registry +Human-facing discoverability catalog (`docs/registries/capability-registry.md`, generated from `docs/registries/capabilities.json`; issue #2182) listing third-party Feature Capabilities registered by a docs PR so a solo developer can find one before installing it. Distinct from **Capability Registry** (the generated runtime manifest compiled from first-party `capability.json` declarations, ADR-894) and **Capability Registry Overlay** (the runtime seam that merges an installed third-party manifest into that generated registry at load time, ADR-1244 D2): this registry is a static document rendered by `scripts/gen-registry.cjs`, not a runtime data structure or loader. Each entry enumerates the capability's Loop Extension Points and hook kinds so a reader can judge blast radius before running `gsd capability install`, and declares its `engines.gsd` range. Inclusion is an explicit non-endorsement — a maintainer merged a link, nothing more — per `docs/registries/README.md`. + +### EoS Registry +Human-facing discoverability catalog (`docs/registries/eos-registry.md`, generated from `docs/registries/eos.json`; issue #2182) listing third-party Embeddable Orchestration System (EoS) host integrations — projects that embed GSD as an orchestration engine behind the ADR-1239 six-interface-point Host-Integration Interface. Entries are registered by the same docs-PR process, schema conventions, and non-endorsement stance as the **Community Capability Registry**, but enumerate the six interface points, the eight negotiated axes, and `protocolVersion` in place of Loop Extension Points and hook kinds. It has no generated-manifest or Capability Registry Overlay counterpart: an ADR-1239 host integration runs inside the third-party host, not inside GSD's own capability loader, so there is nothing for a runtime registry to merge. See `docs/registries/README.md` for the full entry schema. + ### Capability Validator Shared conformance validator (`gsd-core/bin/lib/capability-validator.cjs`, ADR-1244 D2) extracted from `scripts/gen-capability-registry.cjs` so the build-time generator and the runtime overlay loader share one validator implementation. Exports the same `validateCapability(manifest)` surface consumed by both the generator (build-time) and `capability-loader.cjs` (runtime). Generative-parity is CI-guarded: a drift between the generator's validation logic and the extracted module is a hard failure. Callers that previously inlined validation against the generator's internal helpers are migrated to import this module directly. Source of truth: `gsd-core/bin/lib/capability-validator.cjs`. @@ -194,7 +224,7 @@ ADR-1244 D3 fetch-and-stage seam (`gsd-core/bin/lib/capability-source.cjs`). Pri ADR-1244 D4 per-runtime install manifest (`gsd-core/bin/lib/capability-ledger.cjs`). Leaf module (only `node:fs`/`node:path` plus `shell-command-projection`'s `platformWriteSync`). Records `{ id, version, source, integrity, files[], sharedEdits[{file,marker}] }` per installed capability in `.gsd-capabilities.json` at the runtime config dir root. Exports: `readLedger` (structural-validated, never throws), `writeLedger` (atomic via `platformWriteSync`), `recordInstall` (idempotent, prototype-pollution-guarded), `removeEntry`, and `reconcile` (reports orphans whose `files[]` are missing on disk; hardened against non-string/`..` members; never mutates). Serves as the atomic commit point for Phase-4 upgrade/remove and the reconciliation basis for detecting stale entries after out-of-band deletions. ### Capability Consent Store -Issue #1459 user-owned consent seam (`gsd-core/bin/lib/capability-consent.cjs`, generated from `src/capability-consent.cts`). Leaf module (`node:fs`/`node:path`/`node:os`/`node:crypto` + the ledger's shared bounded `readSmallRegularFile`/`readSmallRegularFileBuffer` + the shared `capability-lock` primitive). Stores `{ version:"1", records: { "": { projectRoot, id, scope:'project', integrity, disclosureSignature, contentHash, consentedAt } } }` at `${GSD_HOME||homedir()}/.gsd/consent.json` — a USER-OWNED file OUTSIDE any repository. Exports: `consentStorePath(gsdHome?)`, `readConsentStore(gsdHome?)` (bounded via `readSmallRegularFile` + 8 MiB cap, NON-THROWING — missing/corrupt/oversized/FIFO/wrong-shape → empty `{records:{}}`; caps records at `MAX_RECORDS=4096`), `bundleContentHash(capDir)` (THE security binding — a `sha512-` over a DETERMINISTIC, INJECTIVE, LOSSLESS serialization of EVERY regular file AND directory under the bundle: length-FRAMED entry COUNT + per-entry TYPE tag + uint32 path-byte-len + RAW path bytes from a `{encoding:'buffer'}` dir walk [finding 4] + for files uint64 content-byte-len + RAW content bytes via `readSmallRegularFileBuffer` [finding 1b], plus typed DIR markers binding empty directories [finding 2]; symlinks/non-regular rejected; size+count bounded), `hasProjectConsent({gsdHome,projectRoot,id,contentHash})` (true iff a record for `${realpath(projectRoot)}` exists AND its stored `contentHash` equals the supplied recomputed hash — the binding is `contentHash`, NOT `integrity` and NOT `disclosureSignature` (those remain on the record purely for the human disclosure + re-consent-on-executable-change UX); unsafe ids → false; prototype-pollution-safe NUL-joined keys + `Object.prototype.hasOwnProperty`), `recordProjectConsent({gsdHome,projectRoot,id,integrity,disclosureSignature,contentHash})` (LOCKED, atomic+durable write — tmp `wx`/fsync/rename/dir-fsync mirroring `writeLedger`; enforces the record cap at write time) and `revokeProjectConsent({gsdHome,projectRoot,id})` (LOCKED atomic delete, no-op if absent) — BOTH **THROW** rather than perform an UNLOCKED read-modify-write when the consent-store lock cannot be acquired (finding 3; the lifecycle treats a consent-write failure as non-fatal, and the `trust revoke` CLI catches the throw and emits a clean error). This is the authoritative consent signal the loader recomputes (`bundleContentHash(capDir)`) and checks at load before activating a PROJECT-scope third-party overlay (declarative surfaces AND command dispatch): a forged/cloned in-repo project ledger, OR any post-consent tamper (swapped declarative manifest, edited hook script, empty-integrity local install — all change the recomputed hash), leaves the cap DISCOVERED-BUT-INACTIVE until the user consents on THIS machine to the EXACT bundle (the lifecycle records the consent on a consented project install/upgrade and revokes it on remove; install/lookup/revoke share one canonical `consentProjectRoot` root key). GLOBAL-scope overlays (under the user's own home) need no record; and when `GSD_HOME` resolves (via realpath, defeating symlink aliasing — finding 1) to a genuine project root the in-repo bundle still requires a record. The consent lock is the SHARED hardened primitive (below), so it never stale-steals a slow-but-live writer (finding 4). See `docs/explanation/capability-trust-model.md` "project-scope trust boundary". +Issue #1459 user-owned consent seam (`gsd-core/bin/lib/capability-consent.cjs`, generated from `src/capability-consent.cts`). Leaf module (`node:fs`/`node:path`/`node:os`/`node:crypto` + the ledger's shared bounded `readSmallRegularFile`/`readSmallRegularFileBuffer` + the shared `capability-lock` primitive). Stores `{ version:"1", records: { "": { projectRoot, id, scope:'project', integrity, disclosureSignature, contentHash, consentedAt } } }` at `${GSD_HOME||homedir()}/.gsd/consent.json` — a USER-OWNED file OUTSIDE any repository. Exports: `consentStorePath(gsdHome?)`, `readConsentStore(gsdHome?)` (bounded via `readSmallRegularFile` + 8 MiB cap, NON-THROWING — missing/corrupt/oversized/FIFO/wrong-shape → empty `{records:{}}`; caps records at `MAX_RECORDS=4096`), `bundleContentHash(capDir)` (THE security binding — a `sha512-` over a DETERMINISTIC, INJECTIVE, LOSSLESS serialization of EVERY regular file AND directory under the bundle: length-FRAMED entry COUNT + per-entry TYPE tag + uint32 path-byte-len + RAW path bytes from a `{encoding:'buffer'}` dir walk [finding 4] + for files uint64 content-byte-len + RAW content bytes via `readSmallRegularFileBuffer` [finding 1b], plus typed DIR markers binding empty directories [finding 2]; symlinks/non-regular rejected; size+count bounded), `hasProjectConsent({gsdHome,projectRoot,id,contentHash})` (true iff a record for `${realpath(projectRoot)}\x00` exists AND its stored `contentHash` equals the supplied recomputed hash — the binding is `contentHash`, NOT `integrity` and NOT `disclosureSignature` (those remain on the record purely for the human disclosure + re-consent-on-executable-change UX); unsafe ids → false; prototype-pollution-safe NUL-joined keys + `Object.prototype.hasOwnProperty`), `recordProjectConsent({gsdHome,projectRoot,id,integrity,disclosureSignature,contentHash})` (LOCKED, atomic+durable write — tmp `wx`/fsync/rename/dir-fsync mirroring `writeLedger`; enforces the record cap at write time) and `revokeProjectConsent({gsdHome,projectRoot,id})` (LOCKED atomic delete, no-op if absent) — BOTH **THROW** rather than perform an UNLOCKED read-modify-write when the consent-store lock cannot be acquired (finding 3; the lifecycle treats a consent-write failure as non-fatal, and the `trust revoke` CLI catches the throw and emits a clean error). This is the authoritative consent signal the loader recomputes (`bundleContentHash(capDir)`) and checks at load before activating a PROJECT-scope third-party overlay (declarative surfaces AND command dispatch): a forged/cloned in-repo project ledger, OR any post-consent tamper (swapped declarative manifest, edited hook script, empty-integrity local install — all change the recomputed hash), leaves the cap DISCOVERED-BUT-INACTIVE until the user consents on THIS machine to the EXACT bundle (the lifecycle records the consent on a consented project install/upgrade and revokes it on remove; install/lookup/revoke share one canonical `consentProjectRoot` root key). GLOBAL-scope overlays (under the user's own home) need no record; and when `GSD_HOME` resolves (via realpath, defeating symlink aliasing — finding 1) to a genuine project root the in-repo bundle still requires a record. The consent lock is the SHARED hardened primitive (below), so it never stale-steals a slow-but-live writer (finding 4). See `docs/explanation/capability-trust-model.md` "project-scope trust boundary". ### Capability Lock Issue #1459 finding 4 shared cross-process lock primitive (`gsd-core/bin/lib/capability-lock.cjs`, generated from `src/capability-lock.cts`). Leaf module (`node:fs`/`node:path`/`node:os`/`node:crypto` + the ledger's bounded `readSmallRegularFile` + `shell-command-projection`'s `execTool` for the rare start-time shell-out). THE single hardened lockfile protocol shared by BOTH `capability-lifecycle` (the `.gsd/capabilities/.lock` mutation lock) and `capability-consent` (the consent-store `.consent.lock`) — extracted so the two locks cannot diverge (mirrors the shared-validator / shared bounded-reader lessons). Exports: `acquireLock(lockPath, opts?)` (O_EXCL create with a JSON `{token,pid,hostname,startTime,ts}` body; steal protocol binds age to the body's own `ts`, never stale-steals a VERIFIED-LIVE same-host holder — pid alive AND recorded start-time matches the pid's current start-time, defeating pid-reuse without ever stealing a live holder — and reclaims only a dead/unverifiable holder via the dead-pid fast path or the hard `LOCK_DEADMAN_MS` deadman; `opts.maxAttempts` raises the bounded retry budget and `opts.waitForFresh` makes a contended fresh/live holder be WAITED FOR rather than failed-fast so genuinely-racing consent writers serialize), `releaseLock(handle)` (token + inode owner-safe — never deletes a successor's lock), `getProcessStartTime`, and the `_setLockProbes`/`_resetLockProbes` test seams. Carries the #1462 lifecycle-lock invariants (process-start-time liveness, TOCTOU-safe pre-rename identity recheck, bounded iterative loop). @@ -208,6 +238,8 @@ ADR-1244 Phase 4 (D5+D6) orchestration seam (`gsd-core/bin/lib/capability-lifecy ### Capability Command Dispatch ADR-1244 Phase 5 (D7) registry-driven dispatch of capability command families. First-party families (`graphify`/`intel`/`audit`, shipped in `bin/lib/`) dispatch via `dispatchCapabilityCommand` (`gsd-core/bin/gsd-tools.cjs`) against the FROZEN `capability-registry.cjs` `commandFamilies` (confined to `bin/lib/`) — unchanged. Third-party (installed overlay) families dispatch via `dispatchOverlayCapabilityCommand`: after the first-party path returns false, it calls `loadRegistry({ includeInstalled, cwd })` and dispatches a family iff its `capId` is in `_overlay.commandRoots` — which `capability-loader.cjs` populates ONLY for accepted overlay capabilities that declare `commands` AND pass the loader's activation gate (a **committed** ledger entry, present and non-`_pending`, PLUS — for PROJECT scope — a matching user consent record in the Capability Consent Store; GLOBAL scope needs no consent record). A bundle dropped on disk with no install (no ledger entry) or no on-this-machine consent is NOT command-dispatchable. The router module is `require()`'d FROM the capability's install root via `defaultRequireFromInstallRoot` (bare-`.cjs` basename + `realpath` containment, rejecting `..` traversal and symlink escape); same own-property/function/sync-only guards as the first-party path. Wired into the `runCommand` default arm before "Unknown command". A repo-planted project ledger no longer activates anything on its own (#1459) — see `docs/explanation/capability-trust-model.md` "project-scope trust boundary". +### Claude Orchestration Capability +Default-off, BETA, claude-only Capability (`capabilities/claude-orchestration/`, `role: feature`, `runtimeCompat.supported: ["claude"]`, `tier: full`, `activationKey: claude_orchestration.enabled`) adopting Claude Code's Workflow tool (the engine behind `/effort ultracode`, Agent SDK ≥ v0.3.149) as an optional parallel-execution backend for the GSD loop, and folding the `gsd-ultraplan-phase` plan-offload under the same runtime gate (#1143; ADR-1143). Pure, fail-closed core in `gsd-core/bin/lib/claude-orchestration.cjs` (generated from `src/claude-orchestration.cts`): `detectWorkflowBackend({ runtimeId, hostIntegration, config, agentSdkVersion }) → { available, backend:'workflow'|'inline', reason }` (gate ladder: enabled → Claude → execution_backend ≠ inline → host dispatch nested+background → valid Agent SDK → SDK ≥ floor; every miss degrades to `inline`, never throws); `emitWorkflowScript({ phaseDir, waves, runId, budgetTokens? }) → { ok, script, summary }` mapping waves → `parallel()` stage barriers, plans → `agent({ agentType:'gsd-executor', isolation:'worktree' })`, `files_modified` overlap → separate sequential stages (greedy first-fit), `resumeFromRunId` wired to the run id, shared `budget(tokens)`; all interpolated identifiers validated script-safe (no `"`,`\`,control chars) and briefs JSON-quoted (review anti-injection). Registers two loop contributions at WIRED points only (execute:wave:pre/execute:pre are declared but not rendered, same constraint external-job documents): `execute:wave:post into:executor` (Workflow-backend guidance) and `plan:post into:planner` (ultraplan ownership declaration), both `when: claude_orchestration.enabled`, `onError: skip`. Federated config keys (`claude_orchestration.enabled` default false, `execution_backend` enum auto|workflow|inline default auto, `min_agent_sdk_version` string default "0.3.149") live only in the registry — uninstall removes them cleanly. Pre-release versions of the floor compare below GA (SemVer precedence). Restores the wave parallelism + plan-checker + verifier that #853 forces inline on Claude Code; on any runtime lacking the Workflow tool, behaviour is byte-identical to today. BETA v1 ships detection + emission + declarative ultraplan ownership + a `claude-orchestration` command family (`gsd-tools claude-orchestration detect-backend|emit-workflow`, router `gsd-core/bin/lib/claude-orchestration-command-router.cjs` from `src/claude-orchestration-command-router.cts`); full install-profile migration of the ultraplan skill into `skills[]` is a follow-up (CLUSTERS/profile gate). Test anchors: `tests/claude-orchestration.test.cjs`, `tests/claude-orchestration-command-router.test.cjs`. ### Loop Extension Point A named, stable site on a host loop step (per-step `pre`/`post` plus per-wave in Execute; 12 total) where Capabilities register hooks. Three hook kinds: `step` (runs as its own sequenced unit), `contribution` (injects into the core step's prompt/context), and `gate` (checks and optionally blocks via a declared `blocking` flag). Each hook declares the artifacts it produces and consumes; hook order is derived by topological sort of that produces/consumes graph (capability-id tiebreak), which also defines data flow — file-artifact based, surviving `/clear` and fresh executor contexts. Hooks are surfaced by runtime resolution with concrete projection: the workflow calls a query that resolves the active hooks and returns fully-rendered, ordered markdown for the executor. Failure is default-resilient — a non-gate hook that errors is skipped with a warning; a hook may opt into `onError: halt`. Part of the Capability system. ADR-857 phase 3c ships the registry-consuming query layer: `gsd-core/bin/lib/loop-resolver.cjs` exposes `resolveLoopHooks({ point, registry, config })` (pure, no I/O), `renderLoopHooks(resolved)` (pure markdown renderer), and `cmdLoopRenderHooks(cwd, point, raw, opts)` (I/O entry point); activated via `gsd-tools loop render-hooks ` which emits `{ point, activeHooks[], rendered }`. Activation is driven by `when` (dotted config key resolved against `loadConfig`), with inline literal `__proto__`/`constructor`/`prototype` prototype-pollution guard. The first phase-6 cutovers wiring workflows to this query have landed — ui-phase at `plan:pre` and ui-review at `verify:post` (in `plan-phase.md`/`autonomous.md`); further per-feature cutovers are ongoing. @@ -253,22 +285,19 @@ A cross-Wing knowledge connection created by `mempalace_create_tunnel`. GSD prop A per-agent narrative entry written by `mempalace_diary_write`. GSD's `gsd-mempalace-curator` writes a diary entry at `ship:post` when `mempalace.diary_journal: true`, recording a session summary scoped to the project and agent role. MemPalace vocabulary — see Connected Capability. ### memory_mode -The `mempalace.memory_mode` config key controlling how tightly MemPalace couples to GSD's native memory. Three declared values: `augment` (default — **implemented**; palace is an additional write-mostly recall layer; lowest coupling), `kg_backend` (**declared; routing seam not yet implemented** — intended to route graphify KG queries through MemPalace's temporal graph; selecting today behaves as `augment`), `replace` (**declared; not yet functional** — intended to make the palace the durable store; selecting today behaves as `augment`). Only `augment` has effect in the current release; `kg_backend` and `replace` are forward-declared for a future release. Read at hook-render time; switching is a config change, not a reinstall. See MemPalace Settings in `docs/CONFIGURATION.md`. +The `mempalace.memory_mode` config key controlling how authoritative MemPalace is during recall/capture relative to GSD's native memory. Three wired values: `augment` (default — palace is an additive recall layer; native memory stays authoritative; lowest coupling), `kg_backend` (knowledge-graph queries resolve against MemPalace's temporal graph as the primary source, `.planning/graphs/` as fallback; non-KG drawer recall stays additive), `replace` (recall resolves through the palace as the source of truth, native artifacts as fallback). Every mode is `onError:skip` and default-resilient — an unreachable palace degrades to native memory and GSD keeps writing `.planning/graphs/`, so no mode loses memory. Read at hook-render time; switching is a config change, not a reinstall. Cross-mode migration of existing `.planning/graphs/` into the palace is a separate, not-yet-implemented concern (PRD/ADR §17 open question). See MemPalace Settings in `docs/CONFIGURATION.md`. ### Runtime Hooks Surface Module -Standalone hook-surface writer module extracted from `bin/install.js` as ADR-857 phase 5f-1 (behavior-preserving relocation, no logic change). Owns: Cline rules-body/agents-md/pre-tool-use hook generation (`buildClineRulesBody`, `buildClineAgentsMdBody`, `buildClinePreToolUseHook`, `mergeGsdAgentsMd`, `writeClineArtifacts`); Cursor `hooks.json` lifecycle (`buildCursorHookEntry`, `isManagedCursorHookEntry`, `reconcileCursorHooksJson`, `writeCursorHooksJson`, `removeCursorHooksJson`); Copilot session-hook config (`buildCopilotHookConfig`, `writeCopilotHookConfig`); Codex hook-block and event management (`buildCodexHookBlock`, `rewriteLegacyCodexHookBlock`, `reconcileCodexHooksJsonEvent`, `reconcileCodexHooksJsonSessionStart`, `ensureCodexHooksJsonSessionStart`, `ensureCodexHooksJsonEvent`, `removeCodexHooksJsonEvent`, `removeCodexHooksJsonSessionStart`, `buildCodexHookWindowsShimIR`); and shared hook command helpers (`buildHookCommand`, `rewriteLegacyManagedNodeHookCommands`, `normalizeNodePath`, `resolveNodeRunner`). `bin/install.js` delegates to this module via thin wrappers and re-exports its functions unchanged so existing tests require no modification. Source: `src/runtime-hooks-surface.cts`. Built output: `gsd-core/bin/lib/runtime-hooks-surface.cjs`. +Standalone hook-surface writer module extracted from `bin/install.js` as ADR-857 phase 5f-1 (behavior-preserving relocation, no logic change). Owns: Cline rules-body/agents-md/pre-tool-use hook generation (`buildClineRulesBody`, `buildClineAgentsMdBody`, `buildClinePreToolUseHook`, `mergeGsdAgentsMd`, `writeClineArtifacts`); Cursor `hooks.json` lifecycle (`buildCursorHookEntry`, `isManagedCursorHookEntry`, `reconcileCursorHooksJson`, `writeCursorHooksJson`, `removeCursorHooksJson`); Copilot session-hook config (`buildCopilotHookConfig`, `writeCopilotHookConfig`); Codex hook-block and event management (`buildCodexHookBlock`, `rewriteLegacyCodexHookBlock`, `reconcileCodexHooksJsonEvent`, `reconcileCodexHooksJsonSessionStart`, `ensureCodexHooksJsonSessionStart`, `ensureCodexHooksJsonEvent`, `removeCodexHooksJsonEvent`, `removeCodexHooksJsonSessionStart`, `buildCodexHookWindowsShimIR`); Kimi native config.toml `[[hooks]]` lifecycle (`buildKimiHooksTomlBlock`, `stripKimiHooksTomlBlock`, `writeKimiHooksToml`, `removeKimiHooksToml` — #2095 EoS/kimi Upgrade 1, the first genuinely NEW hook surface added post-relocation rather than a behavior-preserving move: kimi's `[[hooks]]` array lives in its own native `config.toml`, resolved by `resolveKimiHooksTomlDir` in Runtime Homes Module to a directory deliberately separate from kimi's GSD configDir, wrapped in `# GSD Hooks BEGIN`/`END` marker comments for idempotent reinstall); and shared hook command helpers (`buildHookCommand`, `rewriteLegacyManagedNodeHookCommands`, `normalizeNodePath`, `resolveNodeRunner`). `bin/install.js` delegates to this module via thin wrappers and re-exports its functions unchanged so existing tests require no modification. Source: `src/runtime-hooks-surface.cts`. Built output: `gsd-core/bin/lib/runtime-hooks-surface.cjs`. ### Runtime Config Adapter Registry -Module owning the explicit per-runtime config-mutation dispatch table for the installer. `resolveRuntimeConfigIntent(runtime)` projects a typed config intent — `installSurface` (`settings-json` | `codex-toml` | `copilot-instructions` | `cline-rules` | `cursor-hooks-json` | `profile-marker-only`), `writesSharedSettings` (the `finishInstall` shared-settings write gate), and `finishPermissionWriter` (`opencode` | `kilo` | none) — that `bin/install.js` dispatches on instead of inline `runtime === '...'` branching. Owns adapter selection only: it performs no filesystem IO and does not execute config mutations (the install/finishInstall handlers and the per-runtime writers do that). Unknown runtimes fail loudly with a `TypeError`, guarded by an `Object.hasOwn` own-property check so prototype-chain keys (`__proto__`, `constructor`) also throw. Also exports `resolveInstallPlan(runtime)` — the ADR-58 `InstallPlan` capstone — which collects the install-level descriptor axes (`installSurface`, `writesSharedSettings`, `finishPermissionWriter`, `hookEvents`, `extendedHookEvents`, `hooksSurface`, `sandboxTier`) into one typed `InstallPlan` value consumed by `install()` and `finishInstall()` in `bin/install.js`. `sandboxTier` (`none` | `codex-agent-sandbox`) gates per-agent `sandbox_mode` emission in the codex TOML path and fails loud on a missing/invalid value (#1151). The spatial axes (`configHome`, `artifactLayout`, `commandStyle`) remain behind their self-resolving adapter modules and are not part of the plan; they are the execution adapters. Realizes both the adapter-selection and plan-collection halves of the Runtime Install Policy Module boundary. Source: `gsd-core/bin/lib/runtime-config-adapter-registry.cjs`. See ADR-58, #60. +Module owning the explicit per-runtime config-mutation dispatch table for the installer. `resolveRuntimeConfigIntent(runtime)` projects a typed config intent — `installSurface` (`settings-json` | `codex-toml` | `copilot-instructions` | `cline-rules` | `cursor-hooks-json` | `profile-marker-only`), `writesSharedSettings` (the `finishInstall` shared-settings write gate), and `finishPermissionWriter` (`opencode` | `kilo` | `antigravity` | none) — that `bin/install.js` dispatches on instead of inline `runtime === '...'` branching. Owns adapter selection only: it performs no filesystem IO and does not execute config mutations (the install/finishInstall handlers and the per-runtime writers do that). Unknown runtimes fail loudly with a `TypeError`, guarded by an `Object.hasOwn` own-property check so prototype-chain keys (`__proto__`, `constructor`) also throw. Also exports `resolveInstallPlan(runtime)` — the ADR-58 `InstallPlan` capstone — which collects the install-level descriptor axes (`installSurface`, `writesSharedSettings`, `finishPermissionWriter`, `hookEvents`, `extendedHookEvents`, `hooksSurface`, `sandboxTier`) into one typed `InstallPlan` value consumed by `install()` and `finishInstall()` in `bin/install.js`. `sandboxTier` (`none` | `codex-agent-sandbox`) gates per-agent `sandbox_mode` emission in the codex TOML path and fails loud on a missing/invalid value (#1151). The spatial axes (`configHome`, `artifactLayout`, `commandStyle`) remain behind their self-resolving adapter modules and are not part of the plan; they are the execution adapters. Realizes both the adapter-selection and plan-collection halves of the Runtime Install Policy Module boundary. Source: `gsd-core/bin/lib/runtime-config-adapter-registry.cjs`. See ADR-58, #60. ### 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). `hooks.json` covers all seven Claude Code lifecycle events: SessionStart, PreToolUse, PostToolUse, SubagentStop, Stop, PreCompact (all wired to context-monitor for context-headroom awareness), and FileChanged (matcher: `config.json` → config-reload, injects `additionalContext` when `.planning/config.json` changes mid-session). 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: tri-state capability gate (`isCapabilityActive('graphify', cwd)` from capability-state.cjs — requires installed AND surfaced AND config-enabled; replaces the former config-only `isGraphifyEnabled` gate, cutover in #1306), 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`). Config leg reads `.planning/config.json:graphify.enabled`; all three legs (install, surface, config) must be active; 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`. +Module owning the graphify integration: tri-state capability gate (`isCapabilityActive('graphify', cwd)` from capability-state.cjs — requires installed AND surfaced AND config-enabled; replaces the former config-only `isGraphifyEnabled` gate, cutover in #1306), 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`). Config leg reads `.planning/config.json:graphify.enabled`; all three legs (install, surface, config) must be active; writes to `.planning/graphs/`. Graph location override (#1825): `graphify.graph_path` in `.planning/config.json` (a path relative to the project root, or absolute) redirects where `graphifyQuery`/`graphifyStatus`/`graphifyDiff` read `graph.json` — so one umbrella-level cross-repo graph serves multiple sibling projects without N drifting mirror copies; the diff snapshot (`.last-build-snapshot.json`) travels with the configured graph (same dir); the auto-update status sidecar stays project-local; `writeSnapshot` honors the key (reads the configured graph, writes the snapshot alongside it); build stays project-scoped (`.planning/graphs/`) since the build skill hardcodes that destination — the umbrella graph is built in the umbrella project and sub-projects only READ it. Unset/blank/non-string → byte-identical `.planning/graphs/graph.json` default; a configured-but-missing file yields an actionable error naming the path. The key is registered in `config-schema.manifest.json` `validKeys`. 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`. ### Intel Module Module owning the code-intelligence store: tri-state capability gate (`isCapabilityActive('intel', cwd)` from capability-state.cjs — honours installed+surfaced+config-enabled; replaces the former config-only `isIntelEnabled` gate, cutover in #1307; intel has `skills:[]` so installed/surfaced are vacuously true and the effective gate is `intel.enabled` in config), disabled response, query surface (`intelQuery` — full-text search across all intel JSON files), status surface (`intelStatus` — per-file freshness, 24-hour staleness threshold), diff surface (`intelDiff` — added/changed/removed files vs last-refresh snapshot), snapshot management (`saveRefreshSnapshot`/`intelSnapshot`), validation (`intelValidate` — existence, JSON validity, _meta.updated_at recency), api-surface render (`intelApiSurface` — generates `.planning/intel/API-SURFACE.md` from `api-map.json`), plus ungated utilities (`intelPatchMeta` — patches `_meta.updated_at` in any JSON file; `intelExtractExports` — extracts CJS/ESM exports from any JS file). Loop hook rendering gates on `state.active` (not `state.enabled`) so the `activationKey` config gate is honoured even without a per-hook `when` guard (Phase 4 tri-state alignment, #1307). Source: `gsd-core/bin/lib/intel.cjs` (generated from `src/intel.cts`). Router: `gsd-core/bin/lib/intel-command-router.cjs`. See Capability Command Family Module (ADR-959 4d-impl-4) and Loop Extension Point. @@ -314,6 +343,9 @@ The ownership seam between the prohibition probe and security/compliance tooling ### Prohibition Probe Module Second adapter of the Probe Core Module (ADR-550 Decision 7): the spec-phase prohibition-completeness probe wired into spec-phase Step 5.6, surfacing the unwritten *must-NOT* constraints (values/safety/ethics) the spec never forbids. Unlike the Edge Probe, recall is **prose-orchestrated, not a compiled engine** (ADR-550 D7b) — a two-stage pass per requirement: Stage 1 an adversarial recall question, Stage 2 a one-pass precision classifier (drop routine engineering, keep genuine prohibitions). The code surface is schema/projection only: `projectProhibitions()` (deterministic SPEC↔`must_haves.prohibitions` projection backing the `DEFECT.GENERATIVE-FIX` parity assertion), the `{test, judgment}` `PROHIBITION_VALIDATORS`, `validateProhibitionResolution`, and `dispositionForProhibition()` (the fail-closed default — an unwired `test`-tier item resolves to `unverified`/flagged, never green). **Deterministic test-tier locate (#1278, ADR-550 D3 addendum):** a resolved `test`-tier prohibition MAY carry an optional flat-scalar `check` descriptor — `check_kind` (`node-test` | `lint-rule`), `check_target`, and `check_rule` (lint-rule only) — that `projectProhibitions` emits into `must_haves.prohibitions` when well-formed, and `descriptorFromProjection()` (the read-back seam in the #1259 enforcement producer, `src/prohibition-enforcement.cts`) reconstructs into a `{kind, target, rule?}` `CheckDescriptor`, so verify-phase locates the wired check with zero LLM/author authoring. **Flat scalars, never a nested `check:{}` object** — so the round-trip rides the *unchanged* shared `parseMustHavesBlock` (the #644 no-parser-rewrite precedent); an absent/partial descriptor falls through to the producer's existing fail-closed locate, and `failFirst` stays caller-attested (machine-proof is #1279). No `proposeProhibitions()` — recall is LLM prose. `plan-phase` lifts every resolved prohibition from the SPEC `## Prohibitions (must-NOT)` section into the `must_haves.prohibitions` sibling block (never `truths`). Exports (locked surface): `projectProhibitions`, `PROHIBITION_VALIDATORS` (the `{test, judgment}` validators bundle injected into `probe-core`'s generic engine), `validateProhibitionResolution`, and `dispositionForProhibition` (the fail-closed disposition) — the prohibition adapter surface, shipped from `probe-core` alongside the generic engine. Source of truth: `gsd-core/bin/lib/probe-core.cjs` (the prohibition exports live in `src/probe-core.cts`, gitignored per ADR-457) + `gsd-core/references/prohibition-probe.md`. Tests: `tests/prohibition-probe.*.test.cjs`. See ADR-550, Probe Core Module, Edge Probe Module, Verification Tier, Bespoke vs Canon Prohibition. +### Spec-Section Helper Module +The SINGLE source of truth for "did the phase SPEC supply section X (with at least one resolved row)?" — the SPEC-section detection seam consumed by `plan-phase` Step 7.95 (the spec-less probe fallback) to decide, per section, whether to run the fallback. Replaces the ad-hoc `awk` that previously lived in the workflow body, which hard-coded the section header strings at the call site and hand-rolled markdown-table row counting — a brittleness that produced two bugs: an exact `^## Prohibitions$` anchor that missed the canonical `## Prohibitions (must-NOT)` heading, and a single-table row-counting assumption. **Suffix-tolerant header invariant:** `SECTION_HEADERS` regexes match a heading AND any parenthetical/whitespace suffix — `prohibitions` matches both `## Prohibitions` and `## Prohibitions (must-NOT)`; `edges` matches `## Edge Coverage` (and any future suffix); if spec-phase renames a heading, update HERE and the `templates/spec.md` heading together (the contract is pinned by `tests/spec-section.test.cjs`). **Supply rule:** `supplied = present AND dataRows > 0` — a present-but-empty section is NOT supplied (it triggers the fallback). **Multi-table robustness:** a blank or prose line resets the per-table state, so a section with multiple tables (or prose between them) counts every table's data rows without miscounting a second table's header row; the `|…|` line before a `|---|` separator is the table header row and is never counted. **Fail-safe:** a missing/unreadable SPEC file resolves to `present:false` / `supplied:false` (so the fallback fires) rather than throwing. Exports (locked surface): the `SpecSectionKey` type (`edges | prohibitions`), `SECTION_HEADERS` (the canonical header matchers), the `SectionStatus` shape (`{ key, present, dataRows, supplied }`), `countSectionDataRows` (pure `specText → { present, dataRows }`), and `specSectionStatus` (disk-reading wrapper). CLI: `node spec-section.cjs ` prints `SectionStatus` JSON — exit 0 on success (an absent file is a valid "not supplied" answer), exit 2 only on a usage error (missing args / bad key). Pure and dependency-free. Source of truth: `gsd-core/bin/lib/spec-section.cjs` (generated from `src/spec-section.cts`, gitignored per ADR-457). Tests: `tests/spec-section.test.cjs`. See Edge Probe Module, Prohibition Probe Module, and `references/specless-probe-fallback.md`. + ### MVP Mode Phase-level planning mode that frames work as a vertical slice (UI → API → DB) of one user-visible capability instead of horizontal layers. Resolved at workflow init via the precedence chain: `--mvp` CLI flag → ROADMAP.md `**Mode:** mvp` field → `workflow.mvp_mode` config → false. All-or-nothing per phase (PRD #2826 Q1). Surfaced as `MVP_MODE=true|false` to the planner, executor, verifier, and discovery surfaces (progress, stats, graphify). Canonical parser: `roadmap.cjs` `**Mode:**` field; canonical resolution chain documented in `workflows/plan-phase.md`. Concept index: `references/mvp-concepts.md`. @@ -353,11 +385,42 @@ The canonical lint infrastructure adopted in ADR 452 (`docs/adr/452-eslint-lint- ### External-job-waiting half-state A legal deferred state of an Execute step (`external_job_waiting`): the executor has dispatched a long-running async external job and committed an async-job manifest at `.planning/async-jobs/.json` instead of a SUMMARY.md. Distinct from the synchronous "mid-production-commits" half-state and from an illegal partial-plan state. The core loop's step-completion + safe-resume/pause contract treats a non-terminal manifest as legal and reconciles against it (never re-dispatching the plan, which would duplicate the external job); SUMMARY.md is deferred until the job reaches a terminal state and its `expected_artifacts` are verified. The manifest is a versioned stability contract (`docs/reference/planning-artifacts.md`); core *consumes* it while a default-off scheduler-adapter Capability (#1164) *produces* it at `execute:wave:post` — the contract-is-core / producer-is-capability seam mirrors ADR-857's verification-substrate decision. Status enum is closed and scheduler-agnostic: `submitted`, `running`, `completed-unverified`, `failed`, `cancelled`, `timeout`. +### External-job Capability +The producer half of the async external-job contract (#1164, part of #1105). Default-off Capability (`capabilities/external-job/capability.json`) that *writes* `.planning/async-jobs/.json` manifests — the only thing that does; core never writes them. SLURM is the first backend (`sbatch --parsable` submit, `squeue` poll with `sacct` fallback, terminal-state mapping); the design stays scheduler-pluggable via the `backend` field (LSF/PBS/Kubernetes batch forward-declared, not built). Contributions inject at `execute:wave:post` into the executor (classify runtime budget → externalize `long_compute`, commit manifest + handoff, return `external_job_waiting`, defer SUMMARY.md) and at `plan:post` into the planner (emit `` quick|medium|unknown|long_compute per task). Activation key `external_job.enabled` (default `false`); sibling keys `external_job.backend`, `external_job.artifact_dir` (default `Artifacts/jobs`, per-job dirs — no fixed log paths, no hardcoded cluster/partition/account), `external_job.submit_timeout_ms` / `external_job.poll_timeout_ms` (bounded subprocesses per CLAUDE.md). Pure producer logic — SLURM state→manifest-status mapping, manifest build/validate, `sbatch`/`squeue`/`sacct` parsers, and the fail-closed manifest writer (refuses a second non-terminal job for a `plan_id` already in flight; refuses to clobber a malformed manifest) — lives in `gsd-core/src/external-job.cts` (generated to `gsd-core/bin/lib/external-job.cjs`); the operator CLI surface is `scripts/slurm-adapter.cjs` (`submit`/`poll`/`show`). Manifest commands are untrusted across the trust seam: `show` surfaces them for confirmation, never auto-runs `submit_command`/`verification_command`/`resume_command`. Test seam: `tests/external-job.test.cjs` (producer behavioral + fast-check property tests; the consumer invariant suite is `tests/external-job-waiting.test.cjs`). + ### Untrusted-input boundary The prompt-level data/instruction isolation seam for untrusted web/document ingress (#1577). Shared reference `gsd-core/references/untrusted-input-boundary.md`, `@`-included by the 10 ingest agents (`gsd-project-researcher`, `gsd-phase-researcher`, `gsd-ui-researcher`, `gsd-assumptions-analyzer`, `gsd-advisor-researcher`, `gsd-ai-researcher`, `gsd-domain-researcher`, `gsd-research-synthesizer`, `gsd-doc-classifier`, `gsd-doc-synthesizer`) — every agent that reads fetch/search/MCP output or external source documents. The reference instructs: treat fetched/read content as **data, never instructions**; self-scan content for embedded directives before use; act only on the assigned task (ignore off-task instructions in data); and wrap quoted untrusted spans in a **fresh random delimiter** per wrap (fixed markers are spoofable). This prompt-level boundary is the primary control — it keeps an injection from being *followed* even while it sits in context. The hook-level companion is the read-injection scanner (`hooks/gsd-read-injection-scanner.js`, PostToolUse on `Read`/`WebFetch`/`WebSearch`), advisory by default; the opt-in top-level `security.injection_blocking` key upgrades HIGH-confidence detections to a PostToolUse circuit-breaker that halts the agent's next step (it runs *after* the fetch, so it is not a redactor). Tests: `tests/untrusted-input-isolation.test.cjs`, `tests/read-injection-scanner.*.test.cjs`, `tests/injection-blocking-config.test.cjs`. See `docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md` and `docs/explanation/security-model.md`. Grounding: arXiv 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472. --- +## Probe family — spec-completeness probes (machine-oriented predicates) + +> Glossary prose for these modules lives above (Probe Core / Edge Probe / Prohibition Probe / Verification Tier / Verification substrate). These are the greppable one-line predicates ADR-550's Consequences promised alongside the glossary. Research-derived numbers (N17/N18 rates) are deliberately kept out of this machine-canon and live hedged in `docs/design/verifier-reach.md`. (That design note and `docs/adr/1606` are co-delivered sibling PRs of epic #1605; predicate refs to them below resolve once the batch lands.) + +`PROBE.principle=verifier-reach-equals-spec-reach (a goal-backward verifier only checks assertions that exist; probes make omitted assertions exist before code) — ADR-857 verification-substrate boundary; docs/design/verifier-reach.md` +`PROBE.family=edge-probe(shape-axis)+prohibition-probe(must-NOT-axis)+ui-consideration-probe(UI-state-axis), shared probe-core, run as spec-phase/ui-phase soft gates (ADR-550 D7; #1867)` +`PROBE.protocol=recall(adversarial over-generate)->precision(drop routine-engineering); dismissals require a non-empty reason` +`PROBE.core.seam=analyzeCoverage(items,resolutions?,validators) ingests ALREADY-proposed items; does NOT assume deterministic propose (ADR-550 D7b)` +`PROBE.item.axes=status{resolved|dismissed|unresolved} x verification{|null} — orthogonal; the lifecycle enum carries no verification fact (ADR-550 D7a)` +`PROBE.edge.verification=explicit|backstop` +`PROBE.prohib.verification=test|judgment` +`PROBE.ui.verification=explicit|backstop` +`PROBE.ui.axis=MIXED — closed compiled shape-rooted 8 (empty/loading/error/populated/partial/overflow/zero-one-many/long-text) via ui-consideration-probe adapter; open UX (real-time/a11y/i18n-RTL) prose-owned in references/domain-probes.md, NOT compiled (#1867)` +`PROBE.ui.seam=ui-phase Step 9.5 post-verification: element-cue classify -> propose-then-confirm (partial-cue mitigation, Goodhart) -> autoResolve --auto floor (never dismiss; unclassified stays unresolved #1110) -> ## UI Considerations write-back -> plan-phase `## UI Considerations` lift rule (#1867)` +`PROBE.ci.surface=the contract (parse/validate, projection round-trip, fail-closed guards), NEVER the LLM judgment (ADR-550 D5)` +`PROHIB.recall=LLM-prose; no compiled prohibition-probe recall engine (only the schema/projection layer is code, ADR-550 D7b)` +`PROHIB.canon-referral=OWASP/GDPR/fairness-canon are REFERRED to /gsd:secure-phase+eslint, never minted as prohibitions (ADR-550 D6)` +`PROHIB.enforce.green-rule=passed iff provenFailFirst===true && run.passed===true (runProhibitionEnforcement); every miss/fail/un-provable HARD-GATES both modes via dispositionForProhibition's fail-closed default` +`PROHIB.enforce.kinds=node-test (non-vacuous red via isNonVacuousNodeTestRed; pass-side vacuity via isNonVacuousNodeTestPass) | lint-rule (eslint --format json filtered by ruleId)` +`PROHIB.enforce.failfirst=MACHINE-PROVEN against an author-supplied violation fixture (#1279); caller failFirst attestation DEMOTED to a non-authoritative hint (FF-08)` +`PROHIB.enforce.causation=clean-fixture control proves the red is content-caused not env-var-set; MANDATORY for node-test (#1906 supersedes #1346 opt-in) — absent clean-fixture ⇒ node-test un-provable/fail-closed; lint-rule needs none (its subject IS the linted file)` +`PROHIB.descriptor.shape=5 FLAT scalars (check_kind,check_target,check_rule,check_violation_fixture,check_clean_fixture) — NEVER a nested check:{} (parseMustHavesBlock is a flat parser, src/frontmatter.cts)` +`PROHIB.rail=core verify rail, non-toggleable (ADR-857 verification-substrate boundary / decision #6); the verifier<->predicate contract is NOT an off-by-default capability` +`PROHIB.judgment-tier=never-silent / never-hard-halt soft gate; autonomous emits "unverified-prohibition — human review recommended" (exogenous grading, ADR-550 D4)` +`PROHIB.enforce.adr=docs/adr/1606 (verify-time enforcement seam) + docs/adr/550 (spec-phase contract)` + +--- + ## Test rules and lint `RULESET.TESTS.no-source-grep=scripts/lint-no-source-grep.cjs rejects readFileSync source + .includes()/.match()/.startsWith() on the bound var; CI hard-fail` @@ -388,7 +451,7 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `RULESET.WORKFLOW_SIZE_BUDGET=workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = per-file baseline (PRIMARY anti-creep: tests/workflow-size-baseline.json pins each file's exact size) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + new-file cap (un-baselined files <32768, the Codex anchor) + discuss-phase<32000; a file that grew fails the baseline guard — fix with `npm run size:baseline`, commit the one-line diff, and justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump` `RULESET.AGENT_SIZE_BUDGET=agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = per-file baseline (PRIMARY anti-creep: tests/agent-size-baseline.json pins each agents/gsd-*.md exact byte size) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). One 'npm run size:baseline' regenerates BOTH workflow and agent baselines via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter. A grown agent fails the baseline guard — regenerate + justify, or extract LAZILY to gsd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on gsd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes` `RULESET.WORKFLOW_FILE_NAMES=workflow files use hyphens; XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name` -`RULESET.WORKFLOW_EXECUTION_CONTEXT=@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/bug-3135-capture-backlog-workflow.test.cjs; INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; "Invoked by" attribution must move when a flag absorbs a micro-skill` +`RULESET.WORKFLOW_EXECUTION_CONTEXT=@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/docs-update.test.cjs (folds former \`bug-3135-capture-backlog-workflow\`, consolidation epic #1969); INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; "Invoked by" attribution must move when a flag absorbs a micro-skill` `RULESET.WORKFLOW_EXECUTE_END_TO_END=ADR-0002 standard for single-workflow commands is "Execute end-to-end." (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses "execute the X workflow end-to-end." in routing bullets` `RULESET.WORKFLOW.COVERAGE-METADATA=#1602 SUMMARY frontmatter `coverage:` block (list of {id,description,requirement?,verification:[{kind∈unit|integration|e2e|automated_ui|manual_procedural|other, ref, status∈pass|fail|unknown}],human_judgment:bool,rationale?}) is the per-deliverable RTM consumed DETERMINISTICALLY by verify-work extract_tests via `gsd-tools uat classify-coverage --summary ` (src/coverage.cts → bin/lib/coverage.cjs). AUTHORING: execute-plan create_summary populates it from task results; every deliverable MUST be classified; fail-safe default = human_judgment:true + rationale. CLASSIFY CONTRACT: auto-pass (skip human) ONLY when human_judgment===false (strict boolean) AND verification non-empty AND every status==='pass' AND zero validation errors — else PRESENT to human. mode:legacy (no block) ⇒ byte-identical prose `## Accomplishments` fall-through; `coverage: []` ⇒ mode:coverage, zero entries (single-confirmation). Frozen IR: MODE/PRESENT_REASON/ERROR_CODE enums locked by tests/coverage-metadata-parser.test.cjs. extractFrontmatter CANNOT parse it (scalars-only `-` items) → dedicated parser, sibling of parseMustHavesBlock. Asymmetry by design: false-negative=redundant prompt (status quo); false-positive=shipped bug UAT existed to catch` @@ -396,9 +459,6 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `RULESET.ARGUMENTS-SANITIZE=any workflow step constructing .planning/.../{SLUG}.md path from user input ($ARGUMENTS, parsed remainder) must sanitize inline ([a-z0-9-] only, reject ..//\\, max-length) — "(already sanitized)" must trace back to explicit guard; RESUME/fallback modes need own guards` `RULESET.SHARED-HELPERS-LINT-VS-TEST=when a lint script and test suite both implement same constant (CANONICAL_TOOLS) or parser (parseFrontmatter, executionContextRefs), extract to scripts/*-helpers.cjs required by both — silent divergence otherwise` -`RULESET.GEMINI.TOOLS.ask_user=Gemini CLI has no ask_user tool; filter both AskUserQuestion and lowercase ask_user from tools frontmatter and neutralize both names in body text` -`RULESET.GEMINI.TEST_SENTINEL=convertClaudeToGeminiAgent regression should assert tools excludes ask_user, body excludes AskUserQuestion/ask_user, and Read still maps to read_file` - `RULESET.ADR-HEADER=every docs/adr/NNNN-*.md must open with - **Status:** Accepted|Proposed|Deprecated + - **Date:** YYYY-MM-DD immediately after title` `RULESET.MANIFEST-CANONICAL-KEY=docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL SIX families.* arrays (agents/commands/workflows/references/cli_modules/hooks) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all six, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write` @@ -428,9 +488,6 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `RULESET.CODERABBIT.GUARD.SCOPE=if a new @me open PR appears during final list, include it in the same guard pass before declaring all-open-PRs complete` `RULESET.TESTS.CODERABBIT_FIX=prefer exported-function behavioral tests over source-grep; lint-no-source-grep rejects readFileSync source assertions without allow-test-rule` `RULESET.WORKFLOW_MARKDOWN.FENCES=when editing shell snippets inside workflow markdown, preserve the opening language fence; malformed fence can create fresh CodeRabbit threads` -`RULESET.GEMINI.TOOLS.ask_user=Gemini CLI has no ask_user tool; filter both AskUserQuestion and lowercase ask_user from tools frontmatter and neutralize both names in Gemini body text` -`RULESET.GEMINI.TEST_SENTINEL=convertClaudeToGeminiAgent regression should assert tools excludes ask_user, body excludes AskUserQuestion/ask_user, and Read still maps to read_file` - `CI.GATE.issue-link-required=hard-fail if PR body lacks closes/fixes/resolves #` `CI.GATE.changeset-lint=hard-fail for user-facing code diffs unless .changeset/* or PR has no-changelog label` `CI.GATE.repair-sequence(PR)=create issue -> apply approval label -> edit PR body w/ closing keyword -> apply no-changelog if appropriate -> re-run checks` @@ -453,7 +510,6 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `WORKTREE.SEAM.test-policy=cover all decision branches in policy module before changing prune behavior` `WORKTREE.SEAM.test-anchors=[resolveWorktreeContext:has_local_planning|linked_worktree|not_git_repo|main_worktree, planWorktreePrune:git_list_failed|worktrees_present|no_worktrees|parser_throw_fallback, executeWorktreePrunePlan:missing_plan|skip_passthrough|unsupported_action|metadata_prune_only]` `WORKTREE.SEAM.invariant=parser failure must degrade to metadata_prune_only and never escalate to destructive removal` -`WORKTREE.SEAM.execution-rule=prefer node --test tests/worktree-safety-policy.test.cjs for fast seam validation; avoid full npm test loop for seam-only changes` `WORKTREE.SEAM.inventory-interface=[listLinkedWorktreePaths, inspectWorktreeHealth]` `WORKTREE.SEAM.caller-rule=verify.cjs must consume inspectWorktreeHealth for W017 classification; no ad-hoc porcelain parsing in callers` `WORKTREE.SEAM.test-anchor-w017=tests/orphan-worktree-detection.test.cjs + tests/worktree-safety-policy.test.cjs` @@ -680,8 +736,8 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `DEFECT.WINDOWS-FS-OPS.symptom=fs.renameSync / fs.copyFileSync hits EPERM/EBUSY on Windows when antivirus or another process holds a transient handle on the target` `DEFECT.WINDOWS-FS-OPS.examples=c47c2c5d build-hooks rename → copy fallback, d2412271 install Windows persistent SDK shim` -`DEFECT.WINDOWS-FS-OPS.detect=any rename/copy in build/install path without try/catch fallback` -`DEFECT.WINDOWS-FS-OPS.fix-forward=catch EPERM/EBUSY/EACCES, fall back to copy + unlink with retry, surface degraded-mode message; never silently swallow` +`DEFECT.WINDOWS-FS-OPS.detect=ADR-1703 Phase 6: enforced by local/require-fs-op-fallback (AST ESLint rule, error) over src/**/*.cts + bin/install.js + scripts/build-hooks.js — flags an unguarded fs.rename/fs.renameSync (the atomic-publish primitive named in .symptom) that lacks a transient-errno retry or a Windows platform guard; a catch that silently swallows or cleans-up-and-rethrows without an errno check does NOT satisfy the .fix-forward clause. copyFile/unlink are the fallback primitives (out of scope); delegated retry helpers (retryRenameSync from shell-command-projection) are the recognized compliant shape` +`DEFECT.WINDOWS-FS-OPS.fix-forward=catch EPERM/EBUSY/EACCES, fall back to copy + unlink with retry, surface degraded-mode message; never silently swallow; the canonical production cure is retryRenameSync (shell-command-projection.cjs) or a bounded RENAME_RETRY_ERRNOS = new Set(['EPERM','EBUSY','EACCES']) loop` `DEFECT.UNBOUNDED-SUBPROCESS.symptom=git/npm subprocess shelled out without timeout; CLI hangs indefinitely on stuck remote, large repo, or missing network` `DEFECT.UNBOUNDED-SUBPROCESS.examples=a33cbe72 worktree fix bound git subprocesses with timeout` @@ -720,30 +776,36 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.examples=#586/PR #650 ship.md verification gate — grep "^status:" also matched body status: lines, yielding passed+gaps_found+human_needed instead of passed and blocking a passed phase; the same broad-grep still lives in execute-phase.md (consolidation tracked by #651)` `DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.detect=grep "^:" on a *.md whose result is compared to exact tokens, with no frontmatter scoping and no -m1; one body line beginning : is enough to break it` `DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.fix-forward=scope to the leading frontmatter block and take the first match: sed -n '/^---$/,/^---$/p' "$f" | grep -m1 "^:" | cut -d: -f2 | tr -d ' '; fix every parallel copy in the same change or consolidate behind one queryable seam (#651)` -`DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.symptom=a test that parses a workflow bash block out of a *.md and runs it via execFileSync('bash',...) breaks on Windows two ways: the fence regex uses a literal \n after the bash fence that will not match CRLF and trips windows-test-parity-guard (fenceRegexLiteralNewline); and git-bash exists so a bash-presence probe is true, but an os.tmpdir() Windows path (C:\...) is un-globbable in bash so the pipeline returns empty and assertions fail` +`DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.symptom=a test that parses a workflow bash block out of a *.md and runs it via execFileSync('bash',...) breaks on Windows two ways: the fence regex uses a literal \n after the bash fence that will not match CRLF and is flagged by local/no-crlf-fragile-split (the windows-test-parity-guard ratchet it formerly tripped was deleted in ADR-1703 Phase 4 #1726); and git-bash exists so a bash-presence probe is true, but an os.tmpdir() Windows path (C:\...) is un-globbable in bash so the pipeline returns empty and assertions fail` `DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.examples=#586/PR #650 tests/ship-586-verification-routing.test.cjs — the fence \n offender failed ubuntu-24/macos/coverage, then the Windows tmpdir-path glob failed full test (windows-latest,22) at fail 3; both were invisible to file-scoped gsd-test-both runs because the parity guard is only scanned by the full suite` -`DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.detect=test does readFileSync(md).match for a bash fence with literal \n, OR execFileSync('bash',...) gated only on a bash-presence probe; also verifying a new test with a file-scoped run instead of the full suite hides repo-wide static guards` +`DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.detect=test does readFileSync(md).match for a bash fence with literal \n, OR execFileSync('bash',...) gated only on a bash-presence probe; also verifying a new test with a file-scoped run instead of the full suite hides repo-wide static guards; now enforced at write-time + CI by local/no-crlf-fragile-split (CRLF fence/frontmatter regex + readFileSync split-on-\n) and local/no-unguarded-nonportable-exec (bash+chmod), eslint, ADR-1703` `DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.fix-forward=match the fence with \r?\n and normalize the captured block to LF; gate pipeline execution on process.platform !== 'win32' && hasBash since the extraction LOGIC is platform-independent and POSIX coverage suffices; run the full suite (or the parity/lint guards) before push when adding a test file` `DEFECT.WINDOWS-TEST-PORTABILITY.symptom=local gsd-test runs Mac+Linux only (no Windows host); Windows-only test failures (chmod exec-bit not honored for PATH-executing extension-less scripts in Git Bash msys2; / vs \ path-separator in assertions; Git Bash msys2 shell semantics) surface ONLY in CI test (windows-latest,*) / full test (windows-latest,*) lanes, never locally` -`DEFECT.WINDOWS-TEST-PORTABILITY.examples=PR #1084 (chmod 0o755 + bare-command execution failed on windows lane); test files that assert path.join result without normalizing to forward slashes` -`DEFECT.WINDOWS-TEST-PORTABILITY.detect=npm run lint:windows-test-portability (tripwire: flags tests combining chmod exec-bit with sh/bash -c and no platform guard); watch CI windows matrix green before declaring a PR done` -`DEFECT.WINDOWS-TEST-PORTABILITY.fix-forward=gate platform-specific execution with if (process.platform !== 'win32'); normalize path expectations to forward slashes with .replace(/\\/g, '/'); invoke scripts via explicit interpreter (sh ) rather than relying on exec-bit; annotate // windows-portability-ok: when a bypass is intentional` -`DEFECT.WINDOWS-TEST-PORTABILITY.prevention=run lint:ci before opening a PR; treat the CI windows lane as the only true Windows signal — gsd-test (Mac/Linux only) cannot substitute for it` +`DEFECT.WINDOWS-TEST-PORTABILITY.examples=PR #1084 (chmod 0o755 + bare-command execution failed on windows lane); PR #1692 tests/stale-bake-guard.test.cjs resolveAgentDir assertions hardcoded '/H/.config/opencode/agent' forward-slash literals against a path.join return — passed macOS/linux/ubuntu CI (incl. gsd-test docker mirror), failed windows-latest,24 + full test windows-latest,22 shard 2/3; test files that assert path.join result without normalizing to forward slashes` +`DEFECT.WINDOWS-TEST-PORTABILITY.detect=npm run lint (eslint) runs the local/* AST portability rules (ADR-1703): local/no-unguarded-nonportable-exec flags a test that chmods an exec bit AND runs it via sh/bash -c without a process.platform !== 'win32' guard (the retired scripts/lint-windows-test-portability.cjs tripwire, migrated to AST in #1720); local/no-path-literal-in-assert + local/no-posix-mode-bit-assert cover the assertion shapes; local/no-crlf-fragile-split (CRLF file-content split/regex), local/no-hardcoded-tmp (/tmp literal → os.tmpdir()), local/no-bare-npm-exec (npm needs shell:true on Windows) and local/require-userprofile-with-home (set USERPROFILE alongside HOME) replace the deleted windows-test-parity-guard ratchet (#1726); all are platform-guard-aware with zero opt-out (tests/portability-rule-disable-ban.test.cjs); watch CI windows matrix green before declaring a PR done` +`DEFECT.WINDOWS-TEST-PORTABILITY.fix-forward=gate platform-specific execution with if (process.platform !== 'win32'); normalize path expectations to forward slashes with .replace(/\\/g, '/'); invoke scripts via explicit interpreter (sh ) rather than relying on exec-bit; there is NO opt-out for the local/* portability rules — structure platform-specific code behind a recognized process.platform !== 'win32' guard (ADR-1703 zero escape hatch)` +`DEFECT.WINDOWS-TEST-PORTABILITY.prevention=run npm run lint (the local/* AST portability rules, ADR-1703) before opening a PR; treat the CI windows lane as the only true Windows signal — gsd-test (Mac/Linux only) cannot substitute for it` `DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.symptom=a test writes a file with a POSIX mode (fs.writeFileSync(p, data, {mode: 0o644}) or fs.chmodSync) then asserts fs.statSync(p).mode & 0o777 === ; passes on macOS/Linux/ubuntu CI, FAILS on the windows-latest CI lane — Windows fs does NOT honor POSIX write modes, Node reports the mode derived from the DOS readonly attribute (0o666 for writable / 0o444 for readonly), never the requested 0o644/0o755` `DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.examples=#1634/PR #1638 tests/capability-lifecycle.test.cjs "a .cjs hook command is node-prefixed so it runs without the executable bit" failed windows-latest,24 on "precondition: file staged without +x" (expected 420/0o644, got 438/0o666); the node-prefix behavioral assertion was correct — only the mode-bit precondition was the POSIX-only fact` -`DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.detect=grep tests for \`.mode & 0o777\` / \`.mode) === 0o\` / \`writeFileSync(...{ mode: 0o\` / \`chmodSync\` paired with a strict-equality assertion on the resulting mode; any such assertion is a POSIX-only fact that will diverge on Windows (write reads back as 0o666)` +`DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.detect=grep tests for \`.mode & 0o777\` / \`.mode) === 0o\` / \`writeFileSync(...{ mode: 0o\` / \`chmodSync\` paired with a strict-equality assertion on the resulting mode; any such assertion is a POSIX-only fact that will diverge on Windows (write reads back as 0o666); NOW mechanically enforced by the AST ESLint rule local/no-posix-mode-bit-assert (eslint-rules/no-posix-mode-bit-assert.cjs, ADR-1703 Phase 2 #1711) — flags a .mode-vs-octal-literal equality assertion unless control-dependent on a process.platform !== 'win32' guard (eslint-rules/lib/platform-guard.cjs); zero opt-outs (tests/portability-rule-disable-ban.test.cjs)` `DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.fix-forward=gate the mode-bit precondition on if (process.platform !== 'win32') — the executable-bit/mode is a POSIX concept meaningless on Windows; KEEP the platform-independent behavioral assertion (the actual behavior under test) running on every OS; do NOT delete the precondition, scope it to POSIX` -`DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.prevention=ref DEFECT.WINDOWS-TEST-PORTABILITY — gsd-test is Mac/Linux only (no Windows host), only the CI windows-latest lane catches this; run npm run lint:ci (lint-windows-test-portability) before push; prefer asserting the BEHAVIOR (command shape, runnability) over the filesystem mode bit` +`DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.prevention=ref DEFECT.WINDOWS-TEST-PORTABILITY — gsd-test is Mac/Linux only (no Windows host), only the CI windows-latest lane catches this; enforced at write-time + CI by the AST ESLint rule local/no-posix-mode-bit-assert (eslint, error; ADR-1703 Phase 2 #1711); run npm run lint before push; prefer asserting the BEHAVIOR (command shape, runnability) over the filesystem mode bit` `DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.symptom=path.join() result on Windows (backslashes) substituted verbatim into markdown body (@-references, workflow files, generated docs); content gains mixed separators; cross-platform substring assertions fail on windows-latest CI lane only; macOS/Linux CI green so defect ships undetected` `DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.examples=PR #1622 computePathPrefix returned ${resolvedTarget}/ verbatim — rewrites of @~/.claude/gsd-core/commands/gsd/X.md wrote @C:\...\gsd-ial-windsurf-XXX\gsd-core/commands/gsd/help.md (trailing forward slashes from the original literal survived, prefix backslashes did not); tests/install-runtime-artifacts.test.cjs:318 + tests/install.test.cjs:1323 failed on windows-latest only` -`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.detect=any function returning a filesystem path that flows into markdown/text body substitution; grep for path.join/raw resolvedTarget/${configDir}/ in code paths writing workflow .md, agent .md, or generated docs; smoke pattern is ${resolvedTarget}/ or ${configDir}/... templates that bypass normalization` +`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.detect=any function returning a filesystem path that flows into markdown/text body substitution; grep for path.join/raw resolvedTarget/${configDir}/ in code paths writing workflow .md, agent .md, or generated docs; smoke pattern is ${resolvedTarget}/ or ${configDir}/... templates that bypass normalization; NOW enforced at write-time + CI by local/normalize-path-in-content (eslint, error, src/**/*.cts; ADR-1703 Phase 5 #1733) — flags a path-returning fn result (path.basename excluded — returns a separator-less filename) interpolated DIRECTLY into @-reference content (shape a: @~/, @$, @/) or into a template immediately followed by a /…\.md or /…\.json quasi (shape b); INDIRECT data-flow (path stored in a variable/object field then interpolated, e.g. ${entry.ref}) is NOT detected by the rule — normalize at the assignment source or at the emit site; one known indirect leak (src/init.cts cmdAgentSkills entry.ref) fixed in PR #1733 by normalizing at emit; zero opt-out (the out-of-band disable-ban scans src/**/*.cts too)` `DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.fix-forward=normalize at the SOURCE not the test: posixTarget=String(resolvedTarget).replace(/\\/g,'/'), posixHome=homeDir?String(homeDir).replace(/\\/g,'/'):homeDir; markdown body is POSIX-only; .replace(/\\/g,'/') is idempotent on POSIX (no backslashes present) so safe to apply unconditionally; isWindowsHost arg is a no-op tripwire (enh-1511) — do NOT branch on it, normalize always` -`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.prevention=RULESET.CONTENT-PATH-NORMALIZATION; tests are downstream signal, never the fix; ref DEFECT.WINDOWS-TEST-PORTABILITY for test-side parity (normalize expected substrings too: ${configDir}/foo.replace(/\\/g,'/'))` +`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.prevention=enforced by local/normalize-path-in-content (eslint, error; ADR-1703 Phase 5 #1733) per RULESET.CONTENT-PATH-NORMALIZATION; tests are downstream signal, never the fix; ref DEFECT.WINDOWS-TEST-PORTABILITY for test-side parity (normalize expected substrings too: ${configDir}/foo.replace(/\\/g,'/'))` -`RULESET.CONTENT-PATH-NORMALIZATION=filesystem paths substituted into markdown body text (@-references, workflow .md, agent .md, generated docs, command bodies) MUST be normalized to POSIX forward slashes via .replace(/\\/g,'/') at the production source BEFORE substitution; never push normalization to tests; cross-platform content is POSIX-only; applies to: computePathPrefix output, install-path rewrites, generated shim paths emitted into .md bodies; idempotent on POSIX so unconditional` +`RULESET.CONTENT-PATH-NORMALIZATION=filesystem paths substituted into markdown body text (@-references, workflow .md, agent .md, generated docs, command bodies) MUST be normalized to POSIX forward slashes via .replace(/\\/g,'/') at the production source BEFORE substitution; never push normalization to tests; cross-platform content is POSIX-only; applies to: computePathPrefix output, install-path rewrites, generated shim paths emitted into .md bodies; idempotent on POSIX so unconditional; mechanically enforced by local/normalize-path-in-content (eslint, src/**/*.cts; #1733)` + +`DEFECT.WINDOWS-PATH-LITERAL-IN-ASSERT.symptom=an assertion compares the return value of a path-returning function (resolveAgentDir, path.join, path.resolve, getPathX, computePathPrefix, etc.) to a HARDCODED forward-slash string literal like '/H/.config/opencode/agent' or 'C:/Users/...' — passes on POSIX (macOS/linux/ubuntu CI incl. gsd-test docker mirror, where path.join emits forward slashes so literal == actual), FAILS on windows-latest CI lane where path.join emits backslashes so literal != actual` +`DEFECT.WINDOWS-PATH-LITERAL-IN-ASSERT.examples=PR #1692 tests/stale-bake-guard.test.cjs resolveAgentDir suite: assert.equal(resolveAgentDir('opencode',{homedir:()=>'/H'}), '/H/.config/opencode/agent') — green on macOS+ubuntu (docker gate PASS 21101/21101), red on test (windows-latest,24) + full test (windows-latest,22, shard 2/3); same root cause as DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT but on the TEST side against a function return, not the production-markdown side` +`DEFECT.WINDOWS-PATH-LITERAL-IN-ASSERT.detect=any assert*/expect call whose ACTUAL operand is a call to a path-returning fn (path.join, path.resolve, resolveAgentDir, getPathX, computePathPrefix, os.homedir(), path.dirname/basename) AND whose EXPECTED operand is a string literal containing '/' that does NOT first flow through .replace(/\\/g,'/'); the literal-vs-fnCall shape is the tripwire — assert.equal(pathFn(...), '/hardcoded/posix/path') is the violation; assert.equal(String(pathFn(...)).replace(/\\/g,'/'), '/hardcoded/posix/path') is the compliant form; NOW mechanically enforced by the AST ESLint rule local/no-path-literal-in-assert (eslint-rules/no-path-literal-in-assert.cjs, ADR-1703 Phase 1 #1707) — platform-guard-aware (won't flag an assertion control-dependent on a process.platform !== 'win32' guard; eslint-rules/lib/platform-guard.cjs), fn list single-sourced as eslint-rules/lib/portability-vocab.cjs PATH_RETURNING_FNS (drift-guarded vs src/runtime-homes.cts)` +`DEFECT.WINDOWS-PATH-LITERAL-IN-ASSERT.fix-forward=normalize the ACTUAL value to POSIX before comparing: assert.equal(String(pathFn(...)).replace(/\\/g,'/'), '/posix/literal'). Do NOT instead path.join the expected value to match the platform separator — that passes on every platform but masks a malformed backslash-on-POSIX return (both sides wrong together). The .replace is idempotent on POSIX so it is safe unconditionally. For values that are conceptually never paths (null/undefined/numbers), no normalization needed.` +`DEFECT.WINDOWS-PATH-LITERAL-IN-ASSERT.prevention=enforced at write-time (editor) and in CI by the AST ESLint rule local/no-path-literal-in-assert (error, scoped to tests/**/*.test.cjs in eslint.config.mjs; ADR-1703 Phase 1 #1707); inline suppression is banned out-of-band by tests/portability-rule-disable-ban.test.cjs (zero escape hatches — structure platform-specific code behind a recognized process.platform guard, never opt out); run npm run lint before push; treat the CI windows-latest lane as the only true Windows signal — gsd-test (Mac/Linux only) cannot substitute; ref umbrella DEFECT.WINDOWS-TEST-PORTABILITY and production-side analogue DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT` `DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.symptom=scripts/prompt-injection-scan.sh flags a NEW test file as a finding because the test contains real injection payloads as fixtures (strings that match one of the scanner's PATTERNS — see scripts/prompt-injection-scan.sh lines 18-64) to prove the validator under test rejects them; scanner cannot distinguish fixture from real injection; CI security lane fails on the test that ADDS the security validation` `DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.examples=PR #1622 commit 4ed208e74 added convertClaudeCommandToWindsurfWorkflow commandName validation with 22 malicious-name fixtures; scanner matched an instruction-override phrase at tests/windsurf-conversion.test.cjs:122; CI security lane failed even though the test is the security control` @@ -788,7 +850,7 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `SESSION.2026-05-09=[8-PR triage wave, 7 merged + 1 subsumed; META.RULE.* introduced; WAVE.LESSON.* captured; k320/k322/k323/k326/k331 evidence; AI Ops Memory predicate format established]` `SESSION.2026-05-10=[ai-ops memory consolidation; release-notes standard taxonomy + templates; RELEASE-NOTES.* predicates introduced]` `SESSION.2026-05-13=[Shell Command Projection Module expansion (#3465-#3468); ADR-0009 superseded; new exports for subprocess dispatch and platform file I/O; phase-gated migration plan; PR #3464 three-gate invariant CI+CR+unresolved=0; PR #3470 stash-include-untracked rebase pattern]` -`SESSION.2026-05-14=[#3095/PR #3490 EXEC.CLASSIFY.* introduced (Anthropic/Copilot/Codex/Gemini cross-runtime rate-limit sentinel coverage); #3489/PR #3499 DEFECT.STATE-TRAMPLE.idempotency-oracle (STATE.md current_phase field is oracle for state.complete-phase); #3488/PR #3501 DAG resolver same-phase short-form depends_on (shortFormToId index added to sdk/src/query/phase.ts); #3491/PR #3502 DEFECT.NESTED-GIT-INIT (gitWorktreeInfoInternal helper); #3493/PR #3500 extractCurrentMilestone generic Phase Details continuation past planned-milestone siblings; #3503/PR #3504 DEFECT.PATH-SUBSTRING-CHECK (trailing-slash anchor for homedir checks); #3346/PR #3505 codex AoT TOML leaf-key via extractFlatHookEventName; #3506/PR #3507 label-scoped stale-bot sub-job pattern; multi-PR triage operational lessons folded into PROC.TRIAGE.*; #3508 DEFECT.AGENT-ISOLATION-SILENT-FAIL; gsd-test image-missing auto-build (locally-built image via embedded heredoc Dockerfile); refined PRED.k322 threshold to 3 PRs/<10min]` +`SESSION.2026-05-14=[#3095/PR #3490 EXEC.CLASSIFY.* introduced (Anthropic/Copilot/Codex/Gemini [runtime removed #1928] cross-runtime rate-limit sentinel coverage); #3489/PR #3499 DEFECT.STATE-TRAMPLE.idempotency-oracle (STATE.md current_phase field is oracle for state.complete-phase); #3488/PR #3501 DAG resolver same-phase short-form depends_on (shortFormToId index added to sdk/src/query/phase.ts); #3491/PR #3502 DEFECT.NESTED-GIT-INIT (gitWorktreeInfoInternal helper); #3493/PR #3500 extractCurrentMilestone generic Phase Details continuation past planned-milestone siblings; #3503/PR #3504 DEFECT.PATH-SUBSTRING-CHECK (trailing-slash anchor for homedir checks); #3346/PR #3505 codex AoT TOML leaf-key via extractFlatHookEventName; #3506/PR #3507 label-scoped stale-bot sub-job pattern; multi-PR triage operational lessons folded into PROC.TRIAGE.*; #3508 DEFECT.AGENT-ISOLATION-SILENT-FAIL; gsd-test image-missing auto-build (locally-built image via embedded heredoc Dockerfile); refined PRED.k322 threshold to 3 PRs/<10min]` `SESSION.2026-05-15=[#3537/PR #3538 DEFECT.PHASE-REGEX-FANOUT — phaseMarkdownRegexSource promoted to core.cjs and wired to 7 sites; parity-style regression test established as DEFECT.GENERATIVE-FIX exemplar; trek-e/gsd-test-runner#1 filed for DEFECT.GSD-TEST-MIRROR-POISONED — chown-back-before-exec legacy gap (poisoned holodeck mirror unstuck via authorized docker chown to remote 1000:1000); RULESET.PR-FLOW.* codified from project CLAUDE.md load-bearing rule; first dispatch under run-tests-before-create held cleanly (PR #3520 worker stopped on Docker exit 12 infra failure, orchestrator opened PR after unblock); CONTEXT.md refactored from 882 lines of mixed prose+predicates into ~500 lines of pure-predicate format with chronological session log]` `SESSION.2026-05-15.parallel-fix-dispatch=[#3542/PR #3546 prohibit git stash family in executor agents (shared refs/stash across worktrees); #3541/PR #3547 non-TTY resolution for installer prompt-user actions (default remove for SDK build artifacts, keep for skills/gsd-*/SKILL.md); #3545 filed for gsd-test-summary concurrent /tmp output collision; new predicates DEFECT.HOOK-OVER-ENFORCEMENT.read-tool-tracking, DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION, DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL, DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT, PROC.PARALLEL-FIX-DISPATCH; agent-trust-but-verify caught /gsd-update retired-syntax comment slip in #3541 implementation before PR open]` `SESSION.2026-05-16=[multi-PR triage wave (#3577/3581/3640/3641/3642/3648/3649/3637/3639). Established global PreToolUse hook ~/.claude/hooks/test-memory-guard.sh denying new node/test spawns when sum(RSS of node|vitest|jest|...) >= 4 GiB on the 24 GB Mac OR when a same-runner process is already in argv[0] — hard deny via hookSpecificOutput.permissionDecision=deny. PR #3577 fix: revert config-ensure-section dispatch to CJS cmdConfigEnsureSection (SDK author wrote single-section semantics under a name whose legacy callers expect full-default config init); plus 3 SDK parity carve-outs (configNewProject defaults align with sdk/shared/config-defaults.manifest.json, return relative .planning/config.json path, drop quotes from Unknown config key, lead malformed-JSON error with "Failed to read config.json:"). PR #3649 fix: chunk node --test spawn at 28K argv ceiling (Windows CreateProcess lpCommandLine cap 32,767 was instantly aborting unchunked spawn of 546 paths). Chunking fix surfaced 14 pre-existing Windows-only test bugs (4010 pass / 14 fail; vs 0/0 before — entire suite was un-runnable on Windows). PRs #3639 + #3637 confirmed unable to stand alone (legitimately depend on Phase 6 scaffolding only present on feat/3575-enforcement-hardening) — user decision: cherry-pick into #3577 and close. Five other PRs each had ≤1 unresolved CR thread of the changeset-pr-number / null-vs-throw / implicit-Claude-runtime / docs-stale-guidance / hardcoded-tests-path family — all quick wins. New predicates: DEFECT.SDK-PORT-NAME-COLLISION, DEFECT.WINDOWS-ARGV-OVERFLOW, DEFECT.STACKED-PR-CANNOT-STAND-ALONE, DEFECT.CANARY-VERSION-LEAK, DEFECT.GSD-TEST-HOST-MID-RUN-DEATH, RULESET.HARNESS.test-memory-guard, RULESET.PR-FLOW.docker-before-push, RULESET.PR-FLOW.templates-mandatory]` @@ -804,12 +866,13 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `DEFECT.WINDOWS-ARGV-OVERFLOW.detect=Windows CI job at "Run unit tests" exits with code 1 within seconds of starting, no node:test output between "run-tests: suite=… files=N: …" line and "Process completed with exit code 1"; same job on Linux/macOS runs full duration` `DEFECT.WINDOWS-ARGV-OVERFLOW.fix-forward=chunk argv into batches whose total length stays under 28,000 chars (headroom under the 32,767 ceiling); run each chunk sequentially; aggregate exit codes (first non-zero wins). Expose RUN_TESTS_MAX_CMDLINE_CHARS env override so cross-platform regression tests can force chunking with short tmp paths` `DEFECT.WINDOWS-ARGV-OVERFLOW.test-anchor=tests/run-tests-harness.test.cjs "Windows argv-overflow chunking (issue #3597)" — 30 long-named fixture files + RUN_TESTS_MAX_CMDLINE_CHARS=2000 → asserts run-tests: chunk N/M marker in stderr; pattern works on every platform` +`DEFECT.WINDOWS-ARGV-OVERFLOW.prevention=a RUNTIME argv-length property (args-array size not statically knowable) — NOT AST-lint-enforceable; addressed at the source by the production run-tests.cjs chunking under RUN_TESTS_MAX_CMDLINE_CHARS plus its test-anchor (tests/run-tests-harness.test.cjs). ADR-1703 Phase 3 (#1720) evaluated and dropped a no-oversized-test-argv lint rule as unsound (it could not detect the canonical execFileSync(node,[...paths]) array overflow)` `DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.symptom=a test deletes/rewrites a SHARED REAL build artifact or fixture (e.g. gsd-core/bin/lib/*.cjs, the build tsbuildinfo) that other test files require; node --test runs files concurrently, so innocent concurrent tests intermittently fail with "Cannot find module" / ENOENT while the racy test itself passes (victim-not-culprit, leg-asymmetric red); placing mutable build state inside a copied/shipped tree (gsd-core/bin/) additionally races install-test fs.cpSync copies → copyfile ENOENT` `DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.examples=#996/88e30d53 — bug-969 hardening tests fs.unlinkSync'd + restored the real gsd-core/bin/lib/core.cjs and set tsBuildInfoFile inside gsd-core/bin/ → next red across the full-test matrix (macOS/Windows) + ubuntu-24 coverage leg, ~40-50 MODULE_NOT_FOUND/ENOENT per leg; reproduced locally on iteration 1; fixed #1001/#1002` `DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.detect=grep tests for fs.unlinkSync|rmSync|writeFileSync|renameSync|cpSync targeting paths resolved from the repo root (join(__dirname,'..',...)) under gsd-core/bin/lib or a shared committed fixture, instead of a mkdtempSync temp dir; any build helper (e.g. ensureBuiltArtifacts) invoked with real-tree paths during the concurrent test phase; any tsBuildInfoFile / build-cache path that lands inside a copied/shipped dir (gsd-core/bin/)` `DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.fix-forward=tests mutate ONLY isolated mkdtempSync copies — never delete/rewrite shared real build outputs while node --test runs files concurrently; parameterize build helpers to accept {root,srcDir,outDir,tsBuildInfoPath,tsconfigPath} overrides and point the test at a throwaway temp project (precedent: #1002 ensureBuiltArtifacts(overrides)); keep mutable build state (tsbuildinfo) OUTSIDE copied/shipped trees (repo root, gitignored) + best-effort self-heal of stale bin-local copies; this is the concrete instance of the RULESET.TESTS.delete-bad-tests real-race class` -`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.test-anchor=tests/bug-969-test-infra-flake-hardening.test.cjs (hermetic temp-project rewrite); regression gate = 10x concurrent run of that suite + tests/state.test.cjs + tests/install.test.cjs must be clean (reproduces on iter 1 when racy)` +`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.test-anchor=tests/run-tests-harness.test.cjs (hermetic temp-project rewrite); regression gate = 10x concurrent run of that suite + tests/state.test.cjs + tests/install.test.cjs must be clean (reproduces on iter 1 when racy)` `DEFECT.STACKED-PR-CANNOT-STAND-ALONE.symptom=patch PR was authored against scaffolding (handler files, lint scripts, generated modules) that exists only on an unmerged upstream feature branch; the PR's "base" on GitHub is the feature branch, not main; merging requires the upstream PR to land first` `DEFECT.STACKED-PR-CANNOT-STAND-ALONE.examples=#3639 + #3637 both targeted base=feat/3575-enforcement-hardening (the Phase 6 PR #3577); #3639 modifies SDK-bridge calls in 6 family-router files that on main do NOT have any SDK-bridge call yet; #3637 patches scripts/lint-shared-module-handsync.cjs which does not exist on main at all` @@ -829,7 +892,7 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `RULESET.HARNESS.test-memory-guard=~/.claude/hooks/test-memory-guard.sh fires on every Bash PreToolUse; if argv[0]∈{node|vitest|jest|mocha|tsx|ts-node|tap|ava|playwright|cypress} OR matches (npm|pnpm|yarn|bun) (run )?(t|test|tests|vitest|jest); blocks via hookSpecificOutput.permissionDecision=deny when sum(RSS of running matching procs, excluding tsserver|*-mcp|claude|Electron|...) ≥ 4 GiB OR when argv[0] basename matches a running process's argv[0]. Exception: node --version|-v|--help|-h|-p|-e are trivial probes and skip the check. Designed for a 24 GB Mac where prior accidental fan-out exhausted RAM` -`RULESET.PR-FLOW.docker-before-push=before ANY git push of any fix to any PR, run gsd-test-summary (docker on the remote, mirrors ubuntu CI) and confirm exit 0. macOS-local node --test is NOT a substitute — many failures are platform-specific (path separators, case sensitivity, locale, fs semantics). Watchdog with Monitor on the output log; never set a sleep/timer and walk away. Source: user feedback 2026-05-16 — "we don't set a timer we actively watch and record results in real time as possible"` +`RULESET.PR-FLOW.docker-before-push=before ANY git push of any fix to any PR, run gsd-test (docker on the remote, mirrors ubuntu CI) and confirm exit 0. macOS-local node --test is NOT a substitute — many failures are platform-specific (path separators, case sensitivity, locale, fs semantics). Watchdog with Monitor on the output log; never set a sleep/timer and walk away. Source: user feedback 2026-05-16 — "we don't set a timer we actively watch and record results in real time as possible"` `RULESET.PR-FLOW.templates-mandatory=every gh pr create|edit|gh issue create|edit MUST first invoke the gh-templates-first skill and Read (Read tool, not Bash cat — k321 read-tracking) the matching template in .github/. Apply ALL required sections; never write freeform bodies. Repo enforces this via gsd-pr-template-policy GitHub Action which flags any non-templated body — the bot allows the PR to stay open only because authors are contributors-or-higher, but the warning is a real complaint that must be cured. Source: user feedback 2026-05-16 (multi-message escalation) — "the whole reason i have that github action is because you fucking blow through and ignore using the templates"` @@ -841,7 +904,7 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `EXEC.CLASSIFY.workflow=gsd-core/workflows/execute-phase.md step 7; class-distinct prompts (quota-to-wait-for-reset; classify-handoff-bug-to-spot-check; unknown-to-continue/stop)` `EXEC.CLASSIFY.classes={class:'quota-exceeded'|'classify-handoff-bug'|'unknown-failure', sentinel?, retryAfterSeconds?}` `EXEC.CLASSIFY.sentinel-order=most specific first: 429 beats too-many-requests; quota beats resource_exhausted; case-insensitive; canonical sentinel value is lower-cased form` -`EXEC.CLASSIFY.cross-runtime=Anthropic/CC: usage limit|rate limit|quota|429|retry-after; Copilot CLI: rate_limit (stem); Codex CLI: 429|usage_limit_reached|too many requests; Gemini CLI: RESOURCE_EXHAUSTED|exceeded your` +`EXEC.CLASSIFY.cross-runtime=Anthropic/CC: usage limit|rate limit|quota|429|retry-after; Copilot CLI: rate_limit (stem); Codex CLI: 429|usage_limit_reached|too many requests` `EXEC.CLASSIFY.precedence=quota sentinel wins over classifyHandoffIfNeeded bug when both appear` `EXEC.CLASSIFY.retry-after-parser=\bretry[-_ ]after[:\s]+(\d+)\b avoids embedded-word false matches like noretry-after` `EXEC.CLASSIFY.proactive-signal-not-usable=Anthropic exposes anthropic-ratelimit-* headers + Agent SDK RateLimitEvent; Claude Code subprocess does NOT forward to hooks/statusline today (upstream #33820, #22407, #32796)` @@ -863,11 +926,11 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.fix-forward=keep gsd-test-summary --both at the top-level orchestrator; sub-agents either run it foreground with timeout: 1500000 (25min) and block, OR delegate the test step back to the orchestrator (write commits + return); never have a sub-agent fire-and-await a backgrounded long task` `DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.anchor=project CLAUDE.md "Top-level orchestrator (cross-turn notifications available) vs Sub-agent worker (no cross-turn notifications)" guidance — load-bearing for multi-worktree parallel fix dispatch` `DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.symptom=sub-agent writes /gsd- (legacy hyphen syntax) in code comments or doc strings while implementing a fix; lands as part of the implementation diff` -`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.examples=#3541 implementation included a typical /gsd-update path comment in installer-migration-report.cjs; caught by tests/bug-2543-gsd-slash-namespace.test.cjs (#3443 invariant)` -`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.detect=tests/bug-2543-gsd-slash-namespace.test.cjs prints "Found N retired /gsd- reference(s) — use /gsd: instead" with line-number-precise violations` +`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.examples=#3541 implementation included a typical /gsd-update path comment in installer-migration-report.cjs; caught by tests/slash-command-namespace.test.cjs (#3443 invariant)` +`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.detect=tests/slash-command-namespace.test.cjs prints "Found N retired /gsd- reference(s) — use /gsd: instead" with line-number-precise violations` `DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.fix-forward=replace /gsd- with /gsd: at the cited file:line; healthy emergent property — project-wide invariant test catches drift agents would never self-correct` `DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.lesson=agent-trust-but-verify is load-bearing — sub-agent reporting "done" is not a substitute for running the full suite; the invariant test surfaces drift even in doc-only changes` -`PROC.PARALLEL-FIX-DISPATCH.pattern=bot triage brief → worktree per branch → parallel sub-agents do rubber-duck/RCA/TDD implementation only → top-level orchestrator owns commit + gsd-test-summary --both + push + PR + changeset-pr-backfill` +`PROC.PARALLEL-FIX-DISPATCH.pattern=bot triage brief → worktree per branch → parallel sub-agents do rubber-duck/RCA/TDD implementation only → top-level orchestrator owns commit + gsd-test + push + PR + changeset-pr-backfill` `PROC.PARALLEL-FIX-DISPATCH.rationale=long-running test runs need cross-turn notifications (orchestrator-only); CONTRIBUTING.md gh-templates-first hook requires session-scoped Read calls sub-agents wouldn't otherwise make; sequencing test runs avoids GSD-TEST-CONCURRENT-OUTPUT-COLLISION` `PROC.PARALLEL-FIX-DISPATCH.observed=#3541 + #3542 dispatched simultaneously this session; PRs #3546 #3547 opened green; one syntax slip caught by AGENT-RETIRED-SLASH-SYNTAX-DRIFT and fixed before second PR opened` @@ -891,7 +954,7 @@ Full detail in `~/.claude/skills/gsd-pr-fix-discipline/SKILL.md`. AI agents MUST ### Slash command two-tier confusion -- **Symptom:** `tests/bug-2543-gsd-slash-namespace.test.cjs` or `tests/bug-3584-runtime-slash-emitters.test.cjs` fails +- **Symptom:** `tests/slash-command-namespace.test.cjs` (folds former `bug-2543-gsd-slash-namespace`, consolidation epic #1969) or `tests/init-manager.test.cjs` (folds former `bug-3584-runtime-slash-emitters`, consolidation epic #1969) fails - **Affected this session:** #154 (three passes), #164 (added the authoritative matrix) - **Fix:** Consult `## Slash-command form` section of this file before touching any `/gsd-` or `/gsd:` token — colon for `agents/`/`commands/`, hyphen for runtime emitters diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2d14a389..a7a5b0a54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -817,6 +817,7 @@ The following checks run on every PR in addition to the test suite: | Job | What it checks | How to pass | |-----|----------------|-------------| | `Lint — ESLint` | No source-grep tests (see above), via the `local/no-source-grep` rule | Replace with `runGsdTools()` behavioral tests, or add `// allow-test-rule: ` | +| `Lint — cross-platform portability` | Windows-portability defects in tests, via `local/no-path-literal-in-assert` (more rules land per [ADR-1703](docs/adr/1703-portability-enforcement-architecture.md)) — e.g. a path-returning call asserted against a hardcoded `/`-literal | Normalize the actual: `String(pathFn(...)).replace(/\\/g, '/')`, or structure platform-specific code behind a `process.platform !== 'win32'` guard. **No `eslint-disable`** — see [cross-platform-portability-rules.md](docs/contributing/cross-platform-portability-rules.md) | Run locally before pushing: `npm run lint` (or `npx eslint .`) diff --git a/GEMINI.md b/GEMINI.md index 0d298557d..98fc45d8c 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,9 +1,13 @@ -# GSD Core — Gemini CLI context +# GSD Core — Antigravity 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. +> **Gemini CLI was sunset by Google on 2026-06-18** and is no longer served for +> free/Pro/Ultra tiers. Antigravity CLI is its official successor, and this file +> is the context Antigravity reads automatically (its `contextFileName` is +> `GEMINI.md`, inherited from the shared Gemini 3 backend). + +This context gives Antigravity 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 @@ -16,30 +20,28 @@ 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: +> **This file ships only the context above — not the slash commands.** To +> install the `/gsd-*` command set, agents, and hooks into `~/.gemini/antigravity/`, +> run the dedicated installer: > > ```bash -> npx gsd-core --gemini --global +> npx gsd-core --antigravity --global > ``` > -> The two paths are complementary and the manual installer remains fully -> supported. The commands below are available only once that installer has run. +> 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): +If you have installed the gsd commands, the workflow is driven by these `/gsd-*` +slash commands (Antigravity registers gsd's commands under a hyphenated +namespace): -- `/gsd:new-project` — initialise a project and gather deep context. -- `/gsd:progress` — the unified situational command: check progress, advance the +- `/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. +- `/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 @@ -47,7 +49,7 @@ colon form is canonical): 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` +- When unsure what to do next, and the gsd commands are installed, `/gsd-progress` is the situational entry point. Learn more: diff --git a/QUICK-WINS-CONFIRMED-BUGS.md b/QUICK-WINS-CONFIRMED-BUGS.md deleted file mode 100644 index ac041691e..000000000 --- a/QUICK-WINS-CONFIRMED-BUGS.md +++ /dev/null @@ -1,73 +0,0 @@ -# Quick Wins: Confirmed-Bug Fixes - -**Status**: Active -**Started**: 2026-05-16 -**Owner**: Current session (Grok + user) -**Context**: Follow-up to `/gsd-inbox` triage on 2026-05-16 - -## Goal - -Land 6 high-signal, confirmed-bug issues that currently have **zero open pull requests**. These are the cleanest quick-win opportunities available in the public GitHub inbox right now. - -All six issues carry the `confirmed-bug` label, meaning the bug has been verified and a fix is explicitly welcome. - -## The 6 Issues (Prioritized) - -| # | Issue | Short Title | Type | Recommended Flow | Est. Effort | Status | Notes | -|---|-------|-------------|------|------------------|-------------|--------|-------| -| 1 | [#3583](https://github.com/open-gsd/gsd-core/issues/3583) | Claude skill install leaves `/gsd:` in `SKILL.md` body | Installer / Command namespace | PR 3629 (our branch) + competing 3586 | Small (1 file + test) | PR opened / Review | **Leading PR: 3629** (cristianuibar) — reviewed + hardened with CodeRabbit feedback (left-boundary regex + body-scoped guard). Competing PR 3586 has "needs changes" + "ci: failing". Issue still carries `confirmed-bug`. | -| 2 | [#3579](https://github.com/open-gsd/gsd-core/issues/3579) | `build-hooks.js` + npm publish omit graphify auto-update hook | Packaging / Build | `/gsd-quick` | Small | Not started | Classic "new feature missed in release artifact". Easy local verification. | -| 3 | [#3496](https://github.com/open-gsd/gsd-core/issues/3496) | `/gsd:update` changelog extraction skips intermediate versions | Workflow / Update logic | `/gsd-quick` or lightweight plan | Medium-small | Not started | Needs deterministic version-range helper. | -| 4 | [#3588](https://github.com/open-gsd/gsd-core/issues/3588) | Production `npm audit` has 1 high + 5 moderate advisories | Security / Dependencies | Direct + careful review | Medium | Not started | Transitive via `@anthropic-ai/claude-agent-sdk`. May need overrides. | -| 5 | [#3584](https://github.com/open-gsd/gsd-core/issues/3584) | Runtime `bin/lib/*.cjs` still emit `/gsd:` (larger piece deferred from #3583) | Runtime output / Slash formatter | Short plan first, then execute | Medium-Large | Not started | 16+ files. Design a centralized runtime-aware formatter. Do after #3583. | -| 6 | [#3340](https://github.com/open-gsd/gsd-core/issues/3340) | SDK publish lag — agent dir fix never shipped in `@opengsd/gsd-sdk@0.1.0` | Release / SDK publishing | Plan + coordination | Medium (release-focused) | Not started | Oldest. Mostly a publishing/versioning task. | - -## Execution Rules for This Batch - -- **Branch naming**: `fix/NNNN-short-description` (enforced by CI) -- **PR template**: Must use `.github/PULL_REQUEST_TEMPLATE/fix.md` -- **Linking**: `Fixes #NNNN` (or `Closes`) in the PR body -- **Changeset**: Required for all user-facing or security fixes -- **Testing**: All existing tests must pass + new coverage where the issue describes a gap -- **Clean context windows**: Each fix should preferably be driven from a fresh session using the prepared prompts (see session notes or ask for them) -- **GSD self-use**: For the small ones (#3583, #3579, #3496), using `/gsd-quick` (or `/gsd-fast`) inside the fix session is encouraged and appropriate. For #3584, a short planning step is recommended. - -## Status Legend - -- **Not started** — Issue claimed for this batch, no work begun -- **In progress** — Active work in a clean window -- **PR opened** — Pull request created and linked -- **Review** — Awaiting review / CI / merge fixes -- **Merged** — Landed on main -- **Blocked** — Needs input from maintainers or upstream - -## Current Status - -- [x] #3583 — **PR opened** (3629 leading after CodeRabbit review + hardening push; competing 3586 needs changes + CI failing) -- [ ] #3579 — Not started (cleanest next target — 0 PRs) -- [ ] #3496 — PR 3497 open (changes requested) -- [ ] #3588 — Not started -- [ ] #3584 — Not started (larger; deferred runtime cjs colon emissions) -- [ ] #3340 — Not started - -**Progress**: 0 / 6 merged (1 in active review) - -## Process Notes - -- These issues were identified during a `/gsd-inbox` run on 2026-05-16. -- At the time of creation of this file, zero of the six had open PRs. -- 2026-05-16 Grok session: Reviewed PR 3629 (our #3583 fix) for CodeRabbit comments. 1 critical was false-positive (scripts/ *is* published per package.json "files" + npm pack). Applied the 2 valid suggestions (bidirectional word-boundary lookbehind in `buildColonPattern` + body-only scope for the colon-ref regression guard in the test). Tests pass. Pushed hardening commit to the fork branch. Competing PR 3586 exists but is behind on CI/review status. -- Work is intended to be done in **parallel clean context windows** (one issue per fresh Claude/Codex/Gemini session) using dedicated prompts. -- After each fix is complete in its window, the resulting branch + PR description should be brought back here for final review and opening. -- This file serves as the single source of truth for the current batch while execution is in progress. It can be deleted or moved to `docs/archive/` once all six PRs are merged. - -## Related Artifacts - -- Inbox triage report: `/tmp/GSD-INBOX-TRIAGE-2026-05-16.md` (from the `/gsd-inbox` run) -- Full issue list with `confirmed-bug` label: `gh issue list --state open --label confirmed-bug` - ---- - -**Next action**: #3583 now has active PR(s) under review. Next clean quick win (0 PRs, small packaging effort, high value for recently-landed graphify feature): **#3579**. Validated via GitHub search: no PRs mention 3579. Ready for `/gsd-quick` or direct fix (update `scripts/build-hooks.js` HOOKS_TO_COPY + ensure `hooks/lib/` copy in installer + fix any publish filter). - -This document will be updated as status changes. \ No newline at end of file diff --git a/README.ja-JP.md b/README.ja-JP.md index 449483459..e852c33b0 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -47,13 +47,14 @@ npx @opengsd/gsd-core@latest 別のランタイムをお使いの場合や Node.js がない場合は [ランタイムへのインストール](docs/ja-JP/how-to/install-on-your-runtime.md) を参照してください。 -インストール後、最初のプロジェクトを開始します。 +インストール後、新規プロジェクトを開始するか、既存リポジトリをオンボーディングします。 ```bash -/gsd-new-project +/gsd-new-project # グリーンフィールドプロジェクト +/gsd-onboard # 既存コードベース ``` -初めての方は [はじめてのプロジェクト](docs/ja-JP/tutorials/your-first-project.md) で、インストールから最初のフェーズ出荷までのガイド付きチュートリアルをご覧ください。 +初めての方は [はじめてのプロジェクト](docs/ja-JP/tutorials/your-first-project.md) で、インストールから最初のフェーズ出荷までのガイド付きチュートリアルをご覧ください。既存リポジトリの場合は [既存コードベースのオンボーディング](docs/ja-JP/tutorials/onboarding-an-existing-codebase.md) を参照してください。 --- diff --git a/README.ko-KR.md b/README.ko-KR.md index 3bef4570b..2d90f1a6e 100644 --- a/README.ko-KR.md +++ b/README.ko-KR.md @@ -47,13 +47,14 @@ npx @opengsd/gsd-core@latest 다른 런타임이나 Node.js가 없는 환경은 [런타임에 설치하기](docs/ko-KR/how-to/install-on-your-runtime.md)를 참조하세요. -설치 후 첫 번째 프로젝트를 시작합니다: +설치 후 새 프로젝트를 시작하거나 기존 저장소를 온보딩합니다: ```bash -/gsd-new-project +/gsd-new-project # 그린필드 프로젝트 +/gsd-onboard # 기존 코드베이스 ``` -처음 사용하시나요? [첫 번째 프로젝트](docs/ko-KR/tutorials/your-first-project.md)를 따라 설치부터 첫 단계 출시까지 안내받으세요. +처음 사용하시나요? [첫 번째 프로젝트](docs/ko-KR/tutorials/your-first-project.md)를 따라 설치부터 첫 단계 출시까지 안내받으세요. 기존 저장소라면 [기존 코드베이스 온보딩](docs/ko-KR/tutorials/onboarding-an-existing-codebase.md)을 참고하세요. --- diff --git a/README.md b/README.md index db7187704..9f5f08857 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **English** · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [한국어](README.ko-KR.md) -**A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Gemini CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more.** +**A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more.** [![npm version](https://img.shields.io/npm/v/%40opengsd%2Fgsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@opengsd/gsd-core) [![npm downloads](https://img.shields.io/npm/dm/%40opengsd%2Fgsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@opengsd/gsd-core) @@ -21,7 +21,7 @@ ## What is GSD Core -GSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Gemini CLI, Kimi CLI, Copilot, Cursor, and more) through a disciplined phase loop. It solves [context rot](docs/explanation/context-engineering.md) — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean. +GSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Antigravity CLI, Kimi CLI, Copilot, Cursor, and more) through a disciplined phase loop. It solves [context rot](docs/explanation/context-engineering.md) — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean. --- @@ -43,17 +43,18 @@ Each milestone repeats the same five-step loop, one phase at a time: npx @opengsd/gsd-core@latest ``` -The installer prompts for your runtime (Claude Code, OpenCode, Gemini CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from `agents/` or `commands/` directly. +The installer prompts for your runtime (Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from `agents/` or `commands/` directly. On another runtime or without Node.js? See [Install on your runtime](docs/how-to/install-on-your-runtime.md). -Once installed, start your first project: +Once installed, start a new project or onboard an existing repo: ```bash -/gsd-new-project +/gsd-new-project # greenfield project +/gsd-onboard # existing codebase ``` -New here? Follow [Your first project](docs/tutorials/your-first-project.md) for a guided walkthrough from install to first shipped phase. +New here? Follow [Your first project](docs/tutorials/your-first-project.md) for a guided walkthrough from install to first shipped phase, or [Onboarding an existing codebase](docs/tutorials/onboarding-an-existing-codebase.md) for brownfield setup. --- diff --git a/README.pt-BR.md b/README.pt-BR.md index 91c6de96e..96c0c9ada 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -47,13 +47,14 @@ O instalador solicita seu ambiente de execução (Claude Code, OpenCode, Gemini Em outro runtime ou sem Node.js? Consulte [Instalar no seu runtime](docs/pt-BR/how-to/install-on-your-runtime.md). -Após a instalação, inicie seu primeiro projeto: +Após a instalação, inicie um projeto novo ou integre um repositório existente: ```bash -/gsd-new-project +/gsd-new-project # projeto greenfield +/gsd-onboard # base de código existente ``` -É a primeira vez? Siga [Seu primeiro projeto](docs/pt-BR/tutorials/your-first-project.md) para um passo a passo guiado, desde a instalação até a primeira fase entregue. +É a primeira vez? Siga [Seu primeiro projeto](docs/pt-BR/tutorials/your-first-project.md) para um passo a passo guiado, desde a instalação até a primeira fase entregue. Para um repositório existente, consulte [Integrar uma base de código existente](docs/pt-BR/tutorials/onboarding-an-existing-codebase.md). --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 7e2831c54..993167e57 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -47,13 +47,14 @@ npx @opengsd/gsd-core@latest 使用其他运行时或没有 Node.js?请参阅[在你的运行时上安装](docs/zh-CN/how-to/install-on-your-runtime.md)。 -安装完成后,启动你的第一个项目: +安装完成后,启动一个新项目或接入现有仓库: ```bash -/gsd-new-project +/gsd-new-project # 新建项目 +/gsd-onboard # 现有代码库 ``` -初次使用?请按照[你的第一个项目](docs/zh-CN/tutorials/your-first-project.md)进行引导式操作,从安装到完成第一个交付阶段。 +初次使用?请按照[你的第一个项目](docs/zh-CN/tutorials/your-first-project.md)进行引导式操作,从安装到完成第一个交付阶段。对于现有仓库,请参阅[接入现有代码库](docs/zh-CN/tutorials/onboarding-an-existing-codebase.md)。 --- diff --git a/VERSIONING.md b/VERSIONING.md index 7c3a53493..1ba485a44 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -131,13 +131,17 @@ match `package.json`: - `.claude-plugin/plugin.json` — Claude Code plugin manifest (issue #766) - `gemini-extension.json` — Gemini CLI extension manifest (issue #775) +- `.claude-plugin/marketplace.json` — Claude plugin marketplace manifest; its + version lives at `plugins[0].version` and is stamped via a nested versionKey + descriptor (issue #1855) The `version` npm lifecycle script (`scripts/sync-manifest-versions.cjs --stage`) stamps these files automatically on every `npm version` call, and stages them so they are included in the release commit alongside `package.json`. -To add a new manifest that must track the package version, register its path in -the `VERSIONED_MANIFESTS` array in `scripts/sync-manifest-versions.cjs`. A +To add a new manifest that must track the package version, register its path +(and, if its version field is not top-level, its dotted `versionKey`) in the +`VERSIONED_MANIFESTS` array in `scripts/sync-manifest-versions.cjs`. A regression test (`tests/issue-844-manifest-version-sync.test.cjs`) enforces this: it scans all committed JSON files for a matching `version` field and fails if any are missing from the registry. diff --git a/agents/gsd-advisor-researcher.md b/agents/gsd-advisor-researcher.md index 13cdd2ec8..62d84bc7e 100644 --- a/agents/gsd-advisor-researcher.md +++ b/agents/gsd-advisor-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-advisor-researcher description: Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode. -tools: Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__* +tools: Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__* color: cyan --- @@ -19,6 +19,8 @@ Spawned by `discuss-phase` via `Task()`. You do NOT present output directly to t @~/.claude/gsd-core/references/untrusted-input-boundary.md +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + @~/.claude/gsd-core/references/research-documentation-lookup.md diff --git a/agents/gsd-ai-researcher.md b/agents/gsd-ai-researcher.md index 20108ca80..9c9a66db3 100644 --- a/agents/gsd-ai-researcher.md +++ b/agents/gsd-ai-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-ai-researcher description: Researches a chosen AI framework's official docs to produce implementation-ready guidance — best practices, syntax, core patterns, and pitfalls distilled for the specific use case. Writes the Framework Quick Reference and Implementation Guidance sections of AI-SPEC.md. Spawned by /gsd:ai-integration-phase orchestrator. -tools: Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__* +tools: Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*, mcp__plugin_context7_context7__* color: green # hooks: # PostToolUse: @@ -72,7 +72,7 @@ Update AI-SPEC.md at `ai_spec_path`: **Section 3 — Framework Quick Reference:** real installation command, actual imports, working entry point pattern for `system_type`, abstractions table (3-5 rows), pitfall list with why-it's-a-pitfall notes, folder structure, Sources subsection with URLs. -**Section 4 — Implementation Guidance:** specific model (e.g., `claude-sonnet-4-6`, `gpt-4o`) with params, core pattern as code snippet with inline comments, tool use config, state management approach, context window strategy. +**Section 4 — Implementation Guidance:** specific model (e.g., `claude-sonnet-5`, `gpt-4o`) with params, core pattern as code snippet with inline comments, tool use config, state management approach, context window strategy. diff --git a/agents/gsd-assumptions-analyzer.md b/agents/gsd-assumptions-analyzer.md index ccfbd2b76..364baa2b4 100644 --- a/agents/gsd-assumptions-analyzer.md +++ b/agents/gsd-assumptions-analyzer.md @@ -20,6 +20,8 @@ Spawned by `discuss-phase-assumptions` via `Task()`. You do NOT present output d @~/.claude/gsd-core/references/untrusted-input-boundary.md +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + Agent receives via prompt: diff --git a/agents/gsd-code-fixer.md b/agents/gsd-code-fixer.md index 1b89dd79c..8799c129b 100644 --- a/agents/gsd-code-fixer.md +++ b/agents/gsd-code-fixer.md @@ -24,6 +24,8 @@ Before fixing code, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions during fixes. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation @@ -456,7 +458,7 @@ For each finding in sorted order: Use `gsd-tools query commit` with conventional format (message first, then every staged file path): ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi gsd_run query commit \ "fix({padded_phase}): {finding_id} {short_description}" \ --files \ diff --git a/agents/gsd-code-reviewer.md b/agents/gsd-code-reviewer.md index 0ecc81549..36220ed4f 100644 --- a/agents/gsd-code-reviewer.md +++ b/agents/gsd-code-reviewer.md @@ -40,6 +40,8 @@ Before reviewing, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions during review. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during review diff --git a/agents/gsd-codebase-mapper.md b/agents/gsd-codebase-mapper.md index 4897c3040..5a92d75e7 100644 --- a/agents/gsd-codebase-mapper.md +++ b/agents/gsd-codebase-mapper.md @@ -29,6 +29,8 @@ If the prompt contains a `` block, you MUST use the `Read` too **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-debug-session-manager.md b/agents/gsd-debug-session-manager.md index 86170ce03..f3128591d 100644 --- a/agents/gsd-debug-session-manager.md +++ b/agents/gsd-debug-session-manager.md @@ -93,7 +93,7 @@ Agent( Resolve the debugger model before spawning: ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi debugger_model=$(gsd_run query resolve-model gsd-debugger 2>/dev/null | jq -r '.model' 2>/dev/null || true) ``` diff --git a/agents/gsd-debugger.md b/agents/gsd-debugger.md index 565ea6508..ebfcc3d38 100644 --- a/agents/gsd-debugger.md +++ b/agents/gsd-debugger.md @@ -40,6 +40,8 @@ Your job: Find the root cause through hypothesis testing, maintain debug file st - Load `rules/*.md` as needed during **investigation and fix**. - Follow skill rules relevant to the bug being investigated and the fix being applied. +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + @~/.claude/gsd-core/references/debugger-philosophy.md @@ -1150,7 +1152,7 @@ mv .planning/debug/{slug}.md .planning/debug/resolved/ **Check planning config using state load (commit_docs is available from the output):** ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi INIT=$(gsd_run query state.load) if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi # commit_docs is in the JSON output diff --git a/agents/gsd-doc-classifier.md b/agents/gsd-doc-classifier.md index ba4d0c250..53561559f 100644 --- a/agents/gsd-doc-classifier.md +++ b/agents/gsd-doc-classifier.md @@ -20,6 +20,104 @@ If the prompt contains a `` block, use the `Read` tool to load @~/.claude/gsd-core/references/untrusted-input-boundary.md + +This is **rule-application, not generation.** Apply the taxonomy / precedence rules directly to what the source actually contains. Do not infer, embellish, summarize creatively, or add any content not present in the source. Output only the required structure; when the source is silent on a field, mark it absent rather than guessing. (2505.11423 — applies here as a simple mechanical constraint: mark absent rather than fabricate.) + + + +These worked examples show the exact input→output contract. Apply the same pattern to new inputs. + +**Exemplar 1 — Clean ADR case** + +Input: file `docs/adr/0003-choose-postgres.md`, first 50 lines contain: +``` +--- +status: Accepted +--- +# ADR-0003 Use PostgreSQL as primary datastore +## Context +We evaluated SQLite, MySQL, and Postgres. Team has prior Postgres expertise. +## Decision +Use PostgreSQL 15+ for all relational data. +## Consequences +Operators must provision a Postgres instance. +``` + +Output: +```json +{ + "source_path": "docs/adr/0003-choose-postgres.md", + "type": "ADR", + "confidence": "high", + "manifest_override": false, + "title": "ADR-0003 Use PostgreSQL as primary datastore", + "summary": "Chose PostgreSQL 15+ as the primary relational datastore based on team expertise.", + "scope": ["PostgreSQL", "primary datastore", "relational data"], + "cross_refs": [], + "locked": true, + "precedence": null, + "notes": "" +} +``` + +**Exemplar 2 — Ambiguous / UNKNOWN case** + +Input: file `docs/notes/meeting-2024-01-15.md`, first 50 lines contain: +``` +# Meeting notes Jan 15 +Discussed caching options. No decision reached. +Bob mentioned Redis might be faster. Action: revisit next week. +``` + +Output: +```json +{ + "source_path": "docs/notes/meeting-2024-01-15.md", + "type": "UNKNOWN", + "confidence": "low", + "manifest_override": false, + "title": "Meeting notes Jan 15", + "summary": "Meeting notes discussing caching options; no decision or requirement recorded.", + "scope": ["caching", "Redis"], + "cross_refs": [], + "locked": false, + "precedence": null, + "notes": "No ADR/PRD/SPEC signals. Prose-only meeting notes. No status field, no decision statement, no requirements. Mark as UNKNOWN — user must type-tag via manifest." +} +``` + +**Exemplar 3 — Edge case: PRD with ADR-like section header** + +Input: file `docs/prd/user-auth.md`, contains: +``` +# User Authentication PRD +## User Stories +As a user, I want to log in with email+password. +## Acceptance Criteria +- Login succeeds with valid credentials +- Login fails with wrong password (error message shown) +## Decision +Use JWT tokens. (inherited from ADR-0005) +``` + +Output (note: one `## Decision` section inherited from ADR does NOT override strong PRD signals): +```json +{ + "source_path": "docs/prd/user-auth.md", + "type": "PRD", + "confidence": "medium", + "manifest_override": false, + "title": "User Authentication PRD", + "summary": "Requirements for email+password login with JWT tokens.", + "scope": ["user authentication", "login", "JWT"], + "cross_refs": [], + "locked": false, + "precedence": null, + "notes": "Contains one '## Decision' section but dominant signals are user stories + acceptance criteria → PRD. ADR reference recorded in cross_refs if a link is present." +} +``` + + Your classification drives extraction. If you tag a PRD as a DOC, its requirements never make it into REQUIREMENTS.md. If you tag an ADR as a PRD, its decisions lose their LOCKED status and get overridden by weaker sources. Classification fidelity is load-bearing for the entire ingest pipeline. @@ -111,6 +209,13 @@ Regardless of type, extract: - **locked_markers** — for ADRs only: does status read `Accepted` (locked) vs `Proposed`/`Draft` (not locked)? Set `locked: true|false`. + +**Output contract reminder (2506.00069 — restate schema immediately before writing):** +You MUST write exactly one JSON object matching this schema — no extra fields, no omissions: +`{ source_path, type (ADR|PRD|SPEC|DOC|UNKNOWN), confidence (high|medium|low), manifest_override (bool), title (string), summary (≤30 words), scope (string[]), cross_refs (string[]), locked (bool), precedence (int|null), notes (string, omit if high confidence) }` +`locked: true` only for ADR with `Accepted` status. `manifest_override: true` only if MANIFEST_TYPE was provided. Fields absent in source → mark absent (empty array / empty string / false), never fabricate. + + Write to `{OUTPUT_DIR}/{slug}-{source_hash}.json` where `slug` is the filename without extension (replace non-alphanumerics with `-`), and `source_hash` is the first 8 hex chars of SHA-256 of the **full source file path** (POSIX-style) so parallel classifiers never collide on sibling `README.md` files. diff --git a/agents/gsd-doc-synthesizer.md b/agents/gsd-doc-synthesizer.md index 548b14398..7b392dec6 100644 --- a/agents/gsd-doc-synthesizer.md +++ b/agents/gsd-doc-synthesizer.md @@ -22,6 +22,56 @@ If the prompt contains a `` block, load every file listed ther @~/.claude/gsd-core/references/untrusted-input-boundary.md + +This is **rule-application, not generation.** Apply the taxonomy / precedence rules directly to what the source actually contains. Do not infer, embellish, summarize creatively, or add any content not present in the source. Output only the required structure; when the source is silent on a field, mark it absent rather than guessing. (2505.11423 — applies here as a simple mechanical constraint: mark absent rather than fabricate.) + + + +These worked examples show the exact input→output contract for per-type extraction. Apply the same pattern. + +**Exemplar 1 — Clean ADR extraction** + +Input: classified ADR `docs/adr/0003-choose-postgres.md` with `locked: true`, decision statement: "Use PostgreSQL 15+ for all relational data." + +Output entry for `INTEL_DIR/decisions.md`: +``` +## ADR-0003: Use PostgreSQL as primary datastore +- source: docs/adr/0003-choose-postgres.md +- status: locked (Accepted) +- decision: Use PostgreSQL 15+ for all relational data. +- scope: primary datastore, relational data +``` + +**Exemplar 2 — UNKNOWN / low-confidence doc (conflict surfacing)** + +Input: classified doc `docs/notes/meeting-2024-01-15.md` with `type: UNKNOWN`, `confidence: low`. + +Output: do NOT extract to any intel file. Instead, add to `unresolved-blockers` in `CONFLICTS_PATH`: +``` +[BLOCKER] UNKNOWN classification — user must type-tag + Found: docs/notes/meeting-2024-01-15.md classified UNKNOWN (low confidence) + Signals observed: prose-only meeting notes, no ADR/PRD/SPEC markers + → Re-tag via --manifest before re-running ingest +``` +Mark absent fields as absent in the entry — do not infer a type. + +**Exemplar 3 — Edge case: competing PRD acceptance criteria** + +Input: two PRD classifications for the same scope "user-auth": +- `docs/prd/auth-v1.md` → requirement: "login via email+password" +- `docs/prd/auth-v2.md` → requirement: "login via SSO only" + +Output: do NOT pick one. Write both to `competing-variants` bucket in `CONFLICTS_PATH`: +``` +[WARNING] Competing acceptance variants for REQ-user-auth + Found: docs/prd/auth-v1.md requires "email+password" + Found: docs/prd/auth-v2.md requires "SSO only" — same scope "user authentication" + Impact: Synthesis cannot pick without losing intent + → Choose one variant or split into two requirements before routing +``` +Emit both variants verbatim to `INTEL_DIR/requirements.md` under separate IDs (REQ-user-auth-v1, REQ-user-auth-v2). + + You are the precedence-enforcing layer. Silent merges, lost locked decisions, or naive dedupes here corrupt every downstream plan. When in doubt, surface the conflict rather than pick. @@ -111,6 +161,17 @@ Apply the `doc-conflict-engine` severity semantics: - `auto-resolved` maps to [INFO] — recorded for transparency + +**Output contract reminder (2506.00069 — restate schema immediately before writing):** +Per-type intel files must use these exact formats — no omissions, no extra fields: +- `decisions.md`: each entry has `## {title}`, `- source:`, `- status: locked|proposed`, `- decision:`, `- scope:` +- `requirements.md`: each entry has `## REQ-{slug}`, `- source:`, `- description:`, `- acceptance:`, `- scope:` +- `constraints.md`: each entry has `## {title}`, `- source:`, `- type: api-contract|schema|nfr|protocol`, `- content:` +- `context.md`: topic-keyed entries with `- source:` attribution +Absent fields → mark absent (empty / omit), never fabricate. LOCKED-vs-LOCKED → always BLOCKER, never auto-resolve. +`CONFLICTS_PATH` must have exactly three sections: `### BLOCKERS`, `### WARNINGS`, `### INFO`. + + Write `CONFLICTS_PATH` using the format from `references/doc-conflict-engine.md`. Three buckets, plain text, no tables. diff --git a/agents/gsd-doc-writer.md b/agents/gsd-doc-writer.md index d594b7bb3..85f26c23e 100644 --- a/agents/gsd-doc-writer.md +++ b/agents/gsd-doc-writer.md @@ -34,6 +34,8 @@ If the prompt contains a `` block, you MUST use the `Read` too **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-domain-researcher.md b/agents/gsd-domain-researcher.md index 3b355b57a..701cb4022 100644 --- a/agents/gsd-domain-researcher.md +++ b/agents/gsd-domain-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-domain-researcher description: Researches the business domain and real-world application context of the AI system being built. Surfaces domain expert evaluation criteria, industry-specific failure modes, regulatory context, and what "good" looks like for practitioners in this field — before the eval-planner turns it into measurable rubrics. Spawned by /gsd:ai-integration-phase orchestrator. -tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__* +tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__* color: purple # hooks: # PostToolUse: diff --git a/agents/gsd-eval-auditor.md b/agents/gsd-eval-auditor.md index 4b0f96282..6e16c62cb 100644 --- a/agents/gsd-eval-auditor.md +++ b/agents/gsd-eval-auditor.md @@ -39,6 +39,8 @@ Read `~/.claude/gsd-core/references/ai-evals.md` before auditing. This is your s **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation @@ -112,7 +114,7 @@ Score 5 components (ok / partial / missing): Do NOT compute scores by hand. Call the deterministic verb with your audited inputs: ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi gsd_run query eval.score --covered --total --infra ,,,, --raw ``` diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index d86e4a990..f3f57169e 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -1,7 +1,7 @@ --- name: gsd-executor description: Executes GSD plans with atomic commits, deviation handling, checkpoint protocols, and state management. Spawned by execute-phase orchestrator or execute-plan command. -tools: Read, Write, Edit, Bash, Grep, Glob, Skill, mcp__context7__* +tools: Read, Write, Edit, Bash, Grep, Glob, Skill, mcp__context7__*, mcp__plugin_context7_context7__* color: yellow # hooks: # PostToolUse: @@ -24,7 +24,7 @@ Your job: Execute the plan completely, commit each task, create SUMMARY.md, upda When you need library or framework documentation, check in this order: -1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them: +1. If Context7 MCP tools (`mcp__context7__*, mcp__plugin_context7_context7__*`) are available in your environment, use them: - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` @@ -64,6 +64,8 @@ Before executing, discover project context: - Load `rules/*.md` as needed during **implementation**. - Follow skill rules relevant to the task you are about to commit. +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + **CLAUDE.md enforcement:** If `./CLAUDE.md` exists, treat its directives as hard constraints during execution. Before committing each task, verify that code changes do not violate CLAUDE.md rules (forbidden patterns, required conventions, mandated tools). If a task action would contradict a CLAUDE.md directive, apply the CLAUDE.md rule — it takes precedence over plan instructions. Document any CLAUDE.md-driven adjustments as deviations (Rule 2: auto-add missing critical functionality). @@ -73,7 +75,7 @@ Before executing, discover project context: Load execution context: ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi INIT=$(gsd_run query init.execute-phase "${PHASE}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` @@ -313,7 +315,7 @@ For full automation-first patterns, server lifecycle, CLI handling: **Auto-mode checkpoint behavior** (when `AUTO_CFG` is `"true"`): - **checkpoint:human-verify** → Auto-approve **except package-legitimacy checkpoints**. If checkpoint has `gate="blocking-human"` OR its purpose indicates package legitimacy verification (`what-built` mentions `Package verification required before install` or `Package install failed — human verification required`), do **not** auto-approve. STOP and return checkpoint_return_format for explicit human confirmation. -- **checkpoint:decision** → Auto-select first option (planners front-load the recommended choice). Log `⚡ Auto-selected: [option name]`. Continue to next task. +- **checkpoint:decision** → If checkpoint has `gate="blocking-human"`, do **not** auto-select — STOP and return checkpoint_return_format for an explicit human decision (a `blocking-human` decision exists because its default answer would be wrong to assume). Otherwise auto-select first option (planners front-load the recommended choice), log `⚡ Auto-selected: [option name]`, continue to next task. - **checkpoint:human-action** → STOP normally. Auth gates cannot be automated — return structured checkpoint message using checkpoint_return_format. **Standard checkpoint behavior** (when `AUTO_CFG` is not `"true"`): @@ -338,6 +340,7 @@ When hitting checkpoint or auth gate, return this structure: ## CHECKPOINT REACHED **Type:** [human-verify | decision | human-action] +**Gate:** [blocking | blocking-human] — copy the task's `gate` attribute verbatim so the orchestrator's carve-out sees it **Plan:** {phase}-{plan} **Progress:** {completed}/{total} tasks complete @@ -689,7 +692,7 @@ Do NOT skip. Do NOT proceed to state updates if self-check fails. -After SUMMARY.md, update STATE.md using `gsd-tools query` state handlers (positional args; see `sdk/src/query/QUERY-HANDLERS.md`): +After SUMMARY.md, update STATE.md using `gsd-tools query` state handlers (named flags): ```bash # Advance plan counter (handles edge cases automatically) @@ -700,16 +703,17 @@ gsd_run query state.update-progress # Record execution metrics (phase, plan, duration, tasks, files) gsd_run query state.record-metric \ - "${PHASE}" "${PLAN}" "${DURATION}" "${TASK_COUNT}" "${FILE_COUNT}" + --phase "${PHASE}" --plan "${PLAN}" --duration "${DURATION}" \ + --tasks "${TASK_COUNT}" --files "${FILE_COUNT}" # Add decisions (extract from SUMMARY.md key-decisions) for decision in "${DECISIONS[@]}"; do - gsd_run query state.add-decision "${decision}" + gsd_run query state.add-decision --summary "${decision}" done -# Update session info (timestamp, stopped-at, resume-file) +# Update session info (stopped-at, resume-file; timestamp set automatically) gsd_run query state.record-session \ - "" "Completed ${PHASE}-${PLAN}-PLAN.md" "None" + --stopped-at "Completed ${PHASE}-${PLAN}-PLAN.md" --resume-file "None" ``` ```bash @@ -736,7 +740,7 @@ gsd_run query requirements.mark-complete ${REQ_IDS} **For blockers found during execution:** ```bash -gsd_run query state.add-blocker "Blocker description" +gsd_run query state.add-blocker --text "Blocker description" ``` diff --git a/agents/gsd-integration-checker.md b/agents/gsd-integration-checker.md index ab324b012..8908c5db4 100644 --- a/agents/gsd-integration-checker.md +++ b/agents/gsd-integration-checker.md @@ -35,6 +35,8 @@ Every expected cross-phase connection must resolve to WIRED (verified end-to-end **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-intel-updater.md b/agents/gsd-intel-updater.md index cfd83cf28..5ff51317b 100644 --- a/agents/gsd-intel-updater.md +++ b/agents/gsd-intel-updater.md @@ -212,7 +212,7 @@ Glob for project structure indicators: Read package.json, configs, and build files. Write `stack.json`. Then patch its timestamp: ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi gsd_run intel patch-meta .planning/intel/stack.json ``` diff --git a/agents/gsd-mempalace-curator.md b/agents/gsd-mempalace-curator.md index 3bd7c1fba..b6e43a46d 100644 --- a/agents/gsd-mempalace-curator.md +++ b/agents/gsd-mempalace-curator.md @@ -22,7 +22,7 @@ If `mempalace.enabled !== true`, do nothing and report `MemPalace disabled — c ## Wing / mode / transport - **Wing:** `mempalace.wing` if non-empty, else `project_code`, else the repo directory name. Every call you make is scoped to this one wing. -- **Mode:** only `augment` is currently wired — KG writes are an additive mirror of `.planning/graphs/`. `kg_backend`/`replace` are forward-declared and behave as `augment` today. +- **Mode** (`mempalace.memory_mode`): under `augment`, KG writes are an additive mirror of `.planning/graphs/`. Under `kg_backend`/`replace`, the palace KG is the authoritative fact store — still mirror every fact here as the primary target; GSD's normal graphify keeps `.planning/graphs/` current, so an unreachable palace never loses history. - **Transport:** prefer the `mempalace_*` MCP tools interactively; fall back to the `mempalace` CLI in headless/cron runs. If neither is reachable, report unavailability and stop — do not error. ## Tasks (each independently best-effort) diff --git a/agents/gsd-nyquist-auditor.md b/agents/gsd-nyquist-auditor.md index 862fef52a..c2bdabaab 100644 --- a/agents/gsd-nyquist-auditor.md +++ b/agents/gsd-nyquist-auditor.md @@ -51,6 +51,8 @@ Read ALL files from ``. Extract: **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index c57a853a7..00bd8ff21 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-phase-researcher description: Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator. -tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__* +tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__* color: cyan # hooks: # PostToolUse: @@ -50,6 +50,8 @@ Before researching, discover project context: - Load `rules/*.md` as needed during **research**. - Research output should account for project skill patterns and conventions. +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + **CLAUDE.md enforcement:** If `./CLAUDE.md` exists, extract all actionable directives (required tools, forbidden patterns, coding conventions, testing rules, security requirements). Include a `## Project Constraints (from CLAUDE.md)` section in RESEARCH.md listing these directives so the planner can verify compliance. Treat CLAUDE.md directives with the same authority as locked decisions from CONTEXT.md — research should not recommend approaches that contradict them. @@ -112,7 +114,7 @@ Construct a JSON file at a temp path (e.g. `/tmp/research-plan-input.json`): ### Step B — Obtain the fetch plan ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi gsd_run query research-plan --input /tmp/research-plan-input.json ``` diff --git a/agents/gsd-plan-checker.md b/agents/gsd-plan-checker.md index 290ec87ce..3ab9bde7d 100644 --- a/agents/gsd-plan-checker.md +++ b/agents/gsd-plan-checker.md @@ -54,6 +54,8 @@ Before verifying, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during verification @@ -689,7 +691,7 @@ issue: Load phase operation context: ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi INIT=$(gsd_run query init.phase-op "${PHASE_ARG}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index b830fa049..18a792769 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -1,7 +1,7 @@ --- name: gsd-planner description: Creates executable phase plans with task breakdown, dependency analysis, and goal-backward verification. Spawned by /gsd:plan-phase orchestrator. -tools: Read, Write, Edit, Bash, Glob, Grep, Skill, WebFetch, mcp__context7__* +tools: Read, Write, Edit, Bash, Glob, Grep, Skill, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__* color: green # hooks: # PostToolUse: @@ -46,6 +46,8 @@ Before planning, discover project context: **Project skills:** @~/.claude/gsd-core/references/project-skills-discovery.md - Load `rules/*.md` as needed during **planning**. - Ensure plans account for project skill patterns and conventions. + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md @@ -620,7 +622,7 @@ start of execution when `--reviews` flag is present or reviews mode is active. Load planning context: ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi INIT=$(gsd_run query init.plan-phase "${PHASE}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` diff --git a/agents/gsd-project-researcher.md b/agents/gsd-project-researcher.md index eb55cc2be..c3123e915 100644 --- a/agents/gsd-project-researcher.md +++ b/agents/gsd-project-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-project-researcher description: Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators. -tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__* +tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__* color: cyan # hooks: # PostToolUse: @@ -34,6 +34,8 @@ Your files feed the roadmap: @~/.claude/gsd-core/references/untrusted-input-boundary.md +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + @~/.claude/gsd-core/references/research-documentation-lookup.md @@ -78,7 +80,7 @@ Construct a JSON file at a temp path (e.g. `/tmp/research-plan-input.json`): ### Step B — Obtain the fetch plan ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi gsd_run query research-plan --input /tmp/research-plan-input.json ``` diff --git a/agents/gsd-research-synthesizer.md b/agents/gsd-research-synthesizer.md index d34dfc13e..cb7c6be32 100644 --- a/agents/gsd-research-synthesizer.md +++ b/agents/gsd-research-synthesizer.md @@ -34,6 +34,8 @@ If the prompt contains a `` block, you MUST use the `Read` too @~/.claude/gsd-core/references/untrusted-input-boundary.md +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + Your SUMMARY.md is consumed by the gsd-roadmapper agent which uses it to: @@ -153,7 +155,7 @@ Write to `.planning/research/SUMMARY.md`. The 4 parallel researcher agents write files but do NOT commit. You commit everything together. ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi gsd_run query commit "docs: complete project research" --files .planning/research/ ``` diff --git a/agents/gsd-roadmapper.md b/agents/gsd-roadmapper.md index 4971cceb7..f2608f0f6 100644 --- a/agents/gsd-roadmapper.md +++ b/agents/gsd-roadmapper.md @@ -26,6 +26,8 @@ If the prompt contains a `` block, you MUST use the `Read` too **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-security-auditor.md b/agents/gsd-security-auditor.md index 80377d38d..1ceae604d 100644 --- a/agents/gsd-security-auditor.md +++ b/agents/gsd-security-auditor.md @@ -1,10 +1,8 @@ --- name: gsd-security-auditor -description: Verifies threat mitigations from PLAN.md threat model exist in implemented code. Produces SECURITY.md. Spawned by /gsd:secure-phase. +description: Verifies threat mitigations from PLAN.md threat model exist in implemented code. Returns structured security verdict (SECURED / OPEN_THREATS / ESCALATE). Spawned by /gsd:secure-phase. tools: - Read - - Write - - Edit - Bash - Glob - Grep @@ -15,11 +13,11 @@ color: red An implemented phase has been submitted for security audit. Verify that every declared threat mitigation is present in the code — do not accept documentation or intent as evidence. -Does NOT scan blindly for new vulnerabilities. Verifies each threat in `` by its declared disposition (mitigate / accept / transfer). Reports gaps. Writes SECURITY.md. +Does NOT scan blindly for new vulnerabilities. Verifies each threat in `` by its declared disposition (mitigate / accept / transfer). Reports gaps. Returns a structured verdict — the orchestrator owns the SECURITY.md file write (#2119: single-writer contract). **Mandatory Initial Read:** If prompt contains ``, load ALL listed files before any action. -**Implementation files are READ-ONLY.** Only create/modify: SECURITY.md. Implementation security gaps → OPEN_THREATS or ESCALATE. Never patch implementation. +**Implementation files are READ-ONLY.** The auditor does NOT write any files — it returns a structured verdict (SECURED / OPEN_THREATS / ESCALATE). The orchestrator persists SECURITY.md. Implementation security gaps → OPEN_THREATS or ESCALATE. Never patch implementation. @@ -51,6 +49,8 @@ Read ALL files from ``. Extract: **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation @@ -77,20 +77,20 @@ Classify each threat before verification. Record classification for every threat - L3: deep trace — follow the data flow end-to-end, check edge cases and ordering, confirm no bypass path exists. - + For each `mitigate` threat: grep for declared mitigation pattern in cited files → found = `CLOSED`, not found = `OPEN`. Apply depth per `asvs_level` (see analyze_threats step). -For `accept` threats: check SECURITY.md accepted risks log → entry present = `CLOSED`, absent = `OPEN`. +For `accept` threats: check existing SECURITY.md accepted risks log → entry present = `CLOSED`, absent = `OPEN`. For `transfer` threats: check for transfer documentation → present = `CLOSED`, absent = `OPEN`. -For each `threat_flag` in SUMMARY.md `## Threat Flags`: if maps to existing threat ID → informational. If no mapping → log as `unregistered_flag` in SECURITY.md (not a blocker). +For each `threat_flag` in SUMMARY.md `## Threat Flags`: if maps to existing threat ID → informational. If no mapping → log as `unregistered_flag` in the structured return (not a blocker). **Severity-aware `threats_open` computation (severity order: critical > high > medium > low):** `threats_open` (the SECURITY.md frontmatter gate field) = the count of threats whose status is OPEN AND whose severity rank ≥ the `block_on` rank. `block_on: none` ⇒ 0 (nothing ever blocks). `block_on: low` ⇒ all open threats block. `block_on: high` (default) ⇒ only high and critical open threats block. -Open threats BELOW the block threshold are recorded in SECURITY.md as **open — below {block_on} threshold (non-blocking)** and MUST NOT be counted in `threats_open`. +Open threats BELOW the block threshold are recorded in the return as **open — below {block_on} threshold (non-blocking)** and MUST NOT be counted in `threats_open`. **Fail-closed for missing severity:** if an OPEN threat has no severity or an unparseable severity (e.g. a legacy register predating the Severity column), treat it as `critical` for this computation — it COUNTS toward `threats_open` (blocking). Never silently drop an unranked open threat. -Write SECURITY.md. Set `threats_open` to the severity-filtered count. Return structured result. +Return the structured result (SECURED / OPEN_THREATS / ESCALATE) with `threats_open` set to the severity-filtered count. The orchestrator writes SECURITY.md from this data — the auditor does NOT write any files (#2119). @@ -114,7 +114,7 @@ Write SECURITY.md. Set `threats_open` to the severity-filtered count. Return str ### Unregistered Flags {none / list from SUMMARY.md ## Threat Flags with no threat mapping} -SECURITY.md: {path} +**threats_open:** {count} ``` ## OPEN_THREATS @@ -143,9 +143,9 @@ SECURITY.md: {path} *Only blocking-open threats count toward `threats_open` in SECURITY.md frontmatter.* -Next: Implement mitigations or document as accepted in SECURITY.md accepted risks log, then re-run /gsd:secure-phase. +Next: Implement mitigations or document as accepted risks, then re-run /gsd:secure-phase. -SECURITY.md: {path} +**threats_open:** {count} ``` ## ESCALATE @@ -170,6 +170,6 @@ SECURITY.md: {path} - [ ] Each threat verified by disposition type (mitigate / accept / transfer) - [ ] Threat flags from SUMMARY.md `## Threat Flags` incorporated - [ ] Implementation files never modified -- [ ] SECURITY.md written to correct path -- [ ] Structured return: SECURED / OPEN_THREATS / ESCALATE +- [ ] No files written — structured verdict returned only (orchestrator writes SECURITY.md) +- [ ] Structured return: SECURED / OPEN_THREATS / ESCALATE with `threats_open` count diff --git a/agents/gsd-ui-auditor.md b/agents/gsd-ui-auditor.md index f30d28a35..9111177f2 100644 --- a/agents/gsd-ui-auditor.md +++ b/agents/gsd-ui-auditor.md @@ -49,6 +49,8 @@ Before auditing, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill 3. Do NOT load full `AGENTS.md` files (100KB+ context cost) diff --git a/agents/gsd-ui-checker.md b/agents/gsd-ui-checker.md index b59e4f85e..88f475afb 100644 --- a/agents/gsd-ui-checker.md +++ b/agents/gsd-ui-checker.md @@ -24,12 +24,44 @@ If the prompt contains a `` block, you MUST use the `Read` too You are read-only — never modify UI-SPEC.md. Report findings, let the researcher fix. + +**FORCE stance:** Assume every UI-SPEC.md contains design debt until the contract proves otherwise. Your starting hypothesis: generic CTAs, missing states, and grid-breaking values are present — find them. + +**Common failure modes — how UI checkers go soft:** +- Passing a spec because all sections are filled in, without checking the *content* quality of CTA labels, empty/error states, and copy +- Treating "accent color defined" as sufficient without checking it is reserved (not applied to all interactive elements) +- Accepting more than 4 font sizes or non-4-multiple spacing because "it's close enough" +- Letting a polished-looking spec bias the verdict toward PASS before each dimension is checked +- Softening a BLOCK to FLAG to avoid sending the researcher back + +**Required verdict classification:** every dimension must resolve to: +- **BLOCK** — contract is incomplete/inconsistent/unimplementable; planning must not begin +- **FLAG** — works but degrades design quality; researcher should fix +- **PASS** — dimension meets the contract + + + +**The Auditor** is an independent design reviewer known for objective, uncompromising spec review. The Auditor applies the six dimensions without deference to effort, polish, or seniority. The Auditor's verdict is grounded in the contract criteria alone — not in whether the spec looks good or whether the researcher worked hard. + +When producing a verdict, ask: *What is The Auditor's verdict on this dimension?* The Auditor's verdict must be derived from evidence in the spec, not from impressions. + +The Auditor is skeptical and exacting, but NOT hostile or contemptuous. The Auditor does not express anger or frustration — the Auditor simply applies the criteria and states what is there and what is missing. (Sources: 2505.23840 — third-person objective persona as sycophancy mitigation; 2506.04975 — objective persona, not hostile, to avoid toxicity escalation.) + +This persona is **not a standalone accuracy guarantee**. It is a stance for applying the evidence contract consistently; if the persona framing and the written criteria/evidence conflict, the criteria and evidence win. + +**Anti-capitulation rule (re-verification turns):** If the researcher disagrees with a BLOCK verdict or submits a revised spec, The Auditor re-examines the revised content against the criteria. Researcher disagreement alone is never grounds to downgrade a BLOCK. A BLOCK may be downgraded only when the spec contains a concrete fix that resolves the exact deficiency that triggered the BLOCK, or when re-examination shows the prior dimension application was mistaken. Self-correction is allowed when the criteria and evidence support it; capitulation to pressure is not. "We'll handle it in implementation" or "it's implied" are not concrete fixes. + + +@~/.claude/gsd-core/references/ui-consideration-probe.md + Before verifying, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during verification diff --git a/agents/gsd-ui-researcher.md b/agents/gsd-ui-researcher.md index 93611e340..a1b8f6bfe 100644 --- a/agents/gsd-ui-researcher.md +++ b/agents/gsd-ui-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-ui-researcher description: Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator. -tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__* +tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__* color: purple # hooks: # PostToolUse: @@ -28,6 +28,7 @@ If the prompt contains a `` block, you MUST use the `Read` too @~/.claude/gsd-core/references/untrusted-input-boundary.md +@~/.claude/gsd-core/references/ui-consideration-probe.md @~/.claude/gsd-core/references/research-documentation-lookup.md @@ -39,6 +40,8 @@ Before researching, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during research @@ -288,7 +291,7 @@ This file is the canonical output of this agent. The orchestrator reads `$PHASE_ ## Step 6: Commit (optional) ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi gsd_run query commit "docs($PHASE): UI design contract" --files "$PHASE_DIR/$PADDED_PHASE-UI-SPEC.md" ``` diff --git a/agents/gsd-verifier.md b/agents/gsd-verifier.md index 55a4c427c..ab13a3fed 100644 --- a/agents/gsd-verifier.md +++ b/agents/gsd-verifier.md @@ -12,7 +12,7 @@ color: green --- -A completed phase has been submitted for goal-backward verification. Verify that the phase goal is actually achieved in the codebase — SUMMARY.md claims are not evidence. +A completed phase has been submitted for verification. Verify that the phase goal is actually achieved in the codebase — SUMMARY.md claims are not evidence. Goal-backward verification. Start from what the phase SHOULD deliver, verify it actually exists and works in the codebase. @@ -52,14 +52,16 @@ Before verifying, discover project context: **Project skills:** @~/.claude/gsd-core/references/project-skills-discovery.md - Load `rules/*.md` as needed during **verification**. - Apply skill rules when scanning for anti-patterns and verifying quality. + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md **Task completion ≠ Goal achievement** -A task "create chat component" can be marked complete when the component is a placeholder. The task was done — a file was created — but the goal "working chat interface" was not achieved. +A "create chat component" task can be complete with a placeholder file — task done, goal "working chat interface" missed. -Goal-backward verification starts from the outcome and works backwards: +Start from the outcome and work backwards: 1. What must be TRUE for the goal to be achieved? 2. What must EXIST for those truths to hold? @@ -99,7 +101,7 @@ Set `is_re_verification = false`, proceed with Step 1. ## Step 1: Load Context (Initial Mode Only) ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi ls "$PHASE_DIR"/*-PLAN.md 2>/dev/null ls "$PHASE_DIR"/*-SUMMARY.md 2>/dev/null gsd_run query roadmap.get-phase "$PHASE_NUM" @@ -197,6 +199,7 @@ For each truth: - A pre-existing test exercises the transition/invariant and passes (confirm via Step 7b's single-named-test path) → ✓ VERIFIED. - No such test exists, or it can't run without a server/state mutation → ⚠️ PRESENT_BEHAVIOR_UNVERIFIED. Emit a human-verification item (Step 8) and do not count it toward the verified score (Step 9). - An accepted override (Step 3b) carries the truth as PASSED (override), exactly as it does for a FAILED truth. +5b. **Non-inferable (`backstop`) truths:** a `verification: backstop` truth (via `truthVerification()`) abstains unless confirmed by explicit evidence — mark `insufficient_spec` -> a human-verification item -> `human_needed`. See `references/honest-verifier.md`. 6. Determine truth status ## Step 3b: Check Verification Overrides diff --git a/bin/gsd-mcp-server.js b/bin/gsd-mcp-server.js new file mode 100644 index 000000000..49a197aed --- /dev/null +++ b/bin/gsd-mcp-server.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +'use strict'; +/** + * gsd-mcp-server — companion MCP server bin entry (ADR-1239 Phase C-2 / #1681). + * + * Lives at top-level bin/ (alongside install.js) — it is a PACKAGE bin the host + * spawns via `npx gsd-mcp-server` (or the global bin), NOT a per-runtime + * artifact copied into a host's config dir. (Placing it under gsd-core/bin/ + * would leak it into every runtime install + break golden parity.) + * + * A stdio JSON-RPC 2.0 server exposing GSD interface points 1 (command) + 5 + * (state IO) so any MCP-consuming host (Claude/Codex/OpenCode/VS Code/Gemini/ + * Cursor/Cline/Hermes) can drive GSD with no bespoke plugin. Delegates to the + * tested server module (gsd-core/bin/lib/mcp-server.cjs runServer). Reads + * line-delimited JSON-RPC from stdin, writes one response + newline per + * request, exits cleanly when stdin closes. + * + * The protocol logic (handleMessage) + the injectable-stream loop (runServer) + * are unit-tested in tests/gsd-mcp-server.test.cjs; the process lifecycle + * (spawn → JSON-RPC → clean exit) in tests/gsd-mcp-server-bin.test.cjs. + */ +const { runServer } = require('../gsd-core/bin/lib/mcp-server.cjs'); + +runServer({ + input: process.stdin, + output: process.stdout, + ctx: { cwd: process.cwd() }, +}).catch((err) => { + process.stderr.write(String((err && err.message) || err) + '\n'); + process.exit(1); +}); diff --git a/bin/install.js b/bin/install.js index 66a82f8c7..7b6c7e385 100755 --- a/bin/install.js +++ b/bin/install.js @@ -32,17 +32,19 @@ const { resolveAntigravityGlobalDir, getGlobalConfigDir, getGlobalSkillsBase, + resolveKimiHooksTomlDir, } = require('../gsd-core/bin/lib/runtime-homes.cjs'); // getDirName (runtime -> local config dir name) is relocated out of this // installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the // conversion module's rewrite engine can consume it without importing // bin/install.js. Re-exported below for back-compat consumers/tests. -const { getDirName } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); +const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags, getRuntimeNewProjectCommand } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); const { applyWorktreeBaseRef, readBaseRefFromSettings, } = require('../gsd-core/bin/lib/worktree-base-ref.cjs'); const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs'); +const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs'); const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs'); // Canonical set of hook files shipped to users. Imported here so writeManifest() // records exactly the same set that build-hooks.js copies to hooks/dist/, making @@ -57,25 +59,20 @@ const INSTALLED_HOOK_FILES = new Set(_HOOKS_TO_COPY); const hooksSurface = require('../gsd-core/bin/lib/runtime-hooks-surface.cjs'); /** - * Runtimes that register hyphen-form `name:` per #2808 AND copy agent bodies - * verbatim (only branding swaps, no namespace conversion), so retired - * `/gsd:` colon refs leak into installed agent prose. Sibling fixes + * #3677 predicate — true when an agent body needs `/gsd:` → `/gsd-` + * normalization at install time. Descriptor-driven + * (capabilities//capability.json -> runtime.hostBehaviors.hyphenNameAgentBody) + * instead of a hardcoded runtime allow-list (ADR-1239 / #2086). Sibling fixes * #3583 / #3629 covered SKILL.md bodies, #3584 / #3606 covered runtime * emissions — this is the agent-body surface (#3677). * - * Explicit allow-list rather than deny-list so unknown / future runtimes - * default to "no rewrite" (better to leak than to mangle a runtime whose - * namespace behavior we haven't verified). - */ -const HYPHEN_NAME_AGENT_RUNTIMES = new Set(['claude', 'qwen', 'hermes']); - -/** - * #3677 predicate — true when an agent body needs `/gsd:` → `/gsd-` - * normalization at install time. + * Unknown / future runtimes that don't declare the flag default to "no + * rewrite" (better to leak than to mangle a runtime whose namespace + * behavior we haven't verified). */ function shouldNormalizeHyphenNamespaceInAgentBody(runtime) { if (typeof runtime !== 'string' || runtime === '') return false; - return HYPHEN_NAME_AGENT_RUNTIMES.has(runtime); + return _hostBehaviors(runtime).hyphenNameAgentBody === true; } /** @@ -101,6 +98,45 @@ const reset = '\x1b[0m'; // Codex config.toml constants const GSD_CODEX_MARKER = '# GSD Agent Configuration \u2014 managed by gsd-core installer'; const GSD_CODEX_HOOKS_OWNERSHIP_PREFIX = '# GSD codex_hooks ownership: '; +// Known scalar fields of Codex's `AgentsToml` struct (codex-rs/config/src/ +// config_toml.rs \u2014 `[agents]` table). Codex marks the struct +// `#[schemars(deny_unknown_fields)]`, so a bare `[agents]` table is valid ONLY +// when every direct key is one of these (named agent roles live in the flattened +// `[agents.]` sub-tables, a separate `AgentRoleToml`). GSD writes only +// `max_depth` (ADR-1239 upgrade 2 / #2088); the full set is enumerated so the +// schema check accepts a user's other legitimate AgentsToml scalars too. +const CODEX_AGENTS_TOML_SCALAR_KEYS = new Set([ + 'max_threads', + 'max_depth', + 'job_max_runtime_seconds', + 'interrupt_message', +]); +// GSD's managed dispatch-depth value. Codex's implicit default is also 1 (root +// sessions start at depth 0); writing it EXPLICITLY pins the negotiated +// `dispatch.maxDepth: 1` axis instead of relying on codex-cli's implicit default +// (ADR-1239 upgrade 2 / #2088). Per the negotiated capability, GSD-hosted Codex +// dispatch is single-level (maxDepth === 1 \u2192 `degradationFor` flattens waves). +const GSD_CODEX_AGENTS_MAX_DEPTH = 1; +// Codex hooks.json lifecycle events GSD registers beyond SessionStart (which has +// its own dedicated path). This is Codex's OWN hook-event vocabulary (per +// developers.openai.com/codex/config-reference), distinct from the cross-runtime +// settings.json `extendedHookEvents` descriptor field (a claude/gemini-family +// allowlist consumed only by hooksSurface==='settings-json' runtimes — Codex is +// codex-hooks-json). All route through gsd-context-monitor.js. #772 wired the +// first three; #2088 adds the remaining six documented events so GSD's monitor +// fires at the same lifecycle points as in Claude Code. Install and uninstall +// share this list so the registered set and the removed set never diverge. +const CODEX_EXTENDED_HOOK_EVENTS = [ + 'SubagentStart', + 'Stop', + 'PostToolUse', + 'PreToolUse', + 'PermissionRequest', + 'PreCompact', + 'PostCompact', + 'SubagentStop', + 'UserPromptSubmit', +]; // Codex's hook-enabling feature flag (issue #3566). Codex itself marks // `codex_hooks` as a `legacy_key` in codex-rs/features/src/legacy.rs; the // canonical current key under [features] is `hooks`. The installer always @@ -126,6 +162,9 @@ function isCodexHooksFeatureKey(key) { // // Merge policy: additive, non-destructive \u2014 existing user entries are preserved; // GSD entries are appended only when not already present (idempotent). +// The reference/default runtime (ADR-1239 reference host). Single-sourced here +// instead of scattered literal 'claude' defaults/rosters (#2086). +const DEFAULT_RUNTIME = 'claude'; const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([ 'Bash(npx gsd-core *)', 'Read(.planning/*)', @@ -212,15 +251,55 @@ const GSD_COPILOT_SESSION_HOOK_PWSH = // Cursor reads hook configs from /.cursor/hooks.json (local) or // ~/.cursor/hooks.json (global) with the shape { version: 1, hooks: { : [...] } }. // Events use camelCase: sessionStart, postToolUse, preToolUse, etc. -// A `command` hook entry runs an external script. GSD registers two managed hooks: -// sessionStart → gsd-cursor-session-start.js (context injection) -// postToolUse → gsd-cursor-post-tool.js (STATE.md update monitor) +// A `command` hook entry runs an external script. GSD registers six managed hooks +// (AC4a upgrade, #2089 — ADR-1239): +// sessionStart → gsd-cursor-session-start.js (context injection) +// postToolUse → gsd-cursor-post-tool.js (STATE.md update monitor) +// preToolUse → gsd-cursor-pre-tool.js (write-path guard) +// stop → gsd-cursor-stop.js (verify-work reminder) +// subagentStart → gsd-cursor-subagent-start.js (subagent context injection) +// subagentStop → gsd-cursor-subagent-stop.js (subagent completion reminder) // Cursor docs: https://cursor.com/docs/hooks const GSD_CURSOR_SESSION_HOOK_SCRIPT = 'gsd-cursor-session-start.js'; const GSD_CURSOR_POST_TOOL_HOOK_SCRIPT = 'gsd-cursor-post-tool.js'; +const GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT = 'gsd-cursor-pre-tool.js'; +const GSD_CURSOR_STOP_HOOK_SCRIPT = 'gsd-cursor-stop.js'; +const GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT = 'gsd-cursor-subagent-start.js'; +const GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT = 'gsd-cursor-subagent-stop.js'; +// All GSD-managed Cursor hook scripts (used by uninstall cleanup). +const GSD_CURSOR_HOOK_SCRIPTS = [ + GSD_CURSOR_SESSION_HOOK_SCRIPT, + GSD_CURSOR_POST_TOOL_HOOK_SCRIPT, + GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT, + GSD_CURSOR_STOP_HOOK_SCRIPT, + GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT, + GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT, +]; // Marker comment embedded in managed hook entries so GSD can find+remove them. const GSD_CURSOR_HOOK_MARKER = 'gsd-managed'; +// #2100 Stage 2 — Windsurf/Cascade lifecycle hook constants. +// Windsurf/Cascade reads hook configs from /.windsurf/hooks.json +// (local) or ~/.codeium/windsurf/hooks.json (global) with the shape +// { hooks: { : [ { command, ... } ] } } — note: no top-level `version` +// field, and each entry carries a bare `command` shell string (no `type` +// field), unlike Cursor's hooks.json. GSD registers two managed BLOCKING +// hooks (exit code 2 to block, vs. Cursor's stdout-JSON form): +// pre_write_code → gsd-windsurf-pre-write.js (write-path guard) +// pre_run_command → gsd-windsurf-pre-command.js (destructive-command guard) +// Cascade has no context-injection channel, so the 4 advisory hooks GSD +// registers on Cursor (sessionStart, postToolUse, stop, subagentStart/Stop) +// have no Windsurf counterpart and are deliberately NOT ported. +// Cascade hooks docs (reference): https://docs.windsurf.com/llms-full.txt , +// https://docs.devin.ai/desktop/cascade/hooks +const GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT = 'gsd-windsurf-pre-write.js'; +const GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT = 'gsd-windsurf-pre-command.js'; +// All GSD-managed Windsurf hook scripts (used by uninstall cleanup). +const GSD_WINDSURF_HOOK_SCRIPTS = [ + GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT, + GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT, +]; + // GSD-managed files under hooks/lib/ (helpers required by gsd-*.sh hooks). // git-cmd.js does not start with "gsd-" (shared classifier for #3129), gsd-graphify-rebuild.sh does. const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh']; @@ -273,60 +352,20 @@ const { } = require(path.join(_gsdLibDir, 'model-catalog.cjs')); const { resolveTierEntry: gsdResolveTierEntry, - EFFORT_SET: GSD_EFFORT_SET, } = require(path.join(_gsdLibDir, 'model-resolver.cjs')); -// #443 — model-catalog and config-defaults.manifest.json exports needed only -// by effort-resolution code paths (resolveInstallTimeEffort / -// generateCodexAgentToml / Claude .md effort injection). Loaded lazily the -// first time they are needed so that requiring install.js in test contexts that -// never trigger an install does NOT produce module-load-time side effects (the -// manifest read + hard throw) that could alter subprocess exit codes or stderr. -let _gsdEffortCatalogCache = null; -function _getGsdEffortCatalog() { - if (_gsdEffortCatalogCache) return _gsdEffortCatalogCache; - - const { AGENT_DEFAULT_TIERS, renderEffortForRuntime } = require(path.join(_gsdLibDir, 'model-catalog.cjs')); - - const manifestPath = path.join( - __dirname, - '..', - 'gsd-core', - 'bin', - 'shared', - 'config-defaults.manifest.json' - ); - let manifestData; - try { - manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); - } catch (_err) { - // Fail loudly — a missing manifest is a broken install, not a soft degradation. - throw new Error( - `gsd install: cannot load config-defaults.manifest.json at ${manifestPath}: ${_err.message}` - ); - } - - const tierDefaults = - (manifestData.effort && - manifestData.effort.routing_tier_defaults && - typeof manifestData.effort.routing_tier_defaults === 'object' && - !Array.isArray(manifestData.effort.routing_tier_defaults)) - ? manifestData.effort.routing_tier_defaults - : { light: 'low', standard: 'high', heavy: 'xhigh' }; // guard: unreachable if manifest is valid - - const effortDefault = - (manifestData.effort && typeof manifestData.effort.default === 'string') - ? manifestData.effort.default - : 'high'; // guard: unreachable if manifest is valid - - _gsdEffortCatalogCache = { - AGENT_DEFAULT_TIERS, - renderEffortForRuntime, - EFFORT_MANIFEST_TIER_DEFAULTS: tierDefaults, - EFFORT_MANIFEST_DEFAULT: effortDefault, - }; - return _gsdEffortCatalogCache; -} +// #2071 — install-time effort resolution (readGsdEffectiveEffortConfig / +// resolveInstallTimeEffort, plus their _getGsdEffortCatalog + _readGsdConfigFile +// helpers) was extracted into the shipped gsd-core/bin/lib/install-effort-resolver.cjs +// so `gsd-tools effort sync` can require it from the installed runtime instead of this +// package-root bin/install.js, which the installer never copies (#2071 crash). The +// installer imports it back here — single source of truth for both surfaces. +const { + readGsdEffectiveEffortConfig, + resolveInstallTimeEffort, + _getGsdEffortCatalog, + _readGsdConfigFile, +} = require(path.join(_gsdLibDir, 'install-effort-resolver.cjs')); const { MINIMAL_SKILL_ALLOWLIST, @@ -350,6 +389,89 @@ try { } catch (_) { _capabilityRegistry = undefined; } + +// Fail-safe floor for the reference host's #338-privacy-critical behaviors, used +// ONLY when the first-party capability registry cannot be loaded (a broken bundle). +// Without it, a registry-load failure would make `_hostBehaviors('claude')` return +// {} and silently route a claude LOCAL install to the repo-shared, committed +// `settings.json` instead of the gitignored `settings.local.json` (#338) — leaking +// engineer-specific absolute paths. Keyed by runtime id (a DATA lookup, not a +// hardcoded string-equality branch) so behavior degrades CLOSED (safe), never open. +// The live descriptor (capabilities/claude/capability.json) remains the source of +// truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086) +const FALLBACK_HOST_BEHAVIORS = Object.freeze({ + claude: Object.freeze({ + settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }), + permissionsSchema: 'claude', + sourceMarkerFile: '.gsd-source', + hyphenNameAgentBody: true, + legacyCommandsGsdInstallMigration: true, + legacyCommandsGsdUninstall: 'global', + }), + // antigravity's global config dir is resolved dynamically (env-overridable, + // multi-segment) via resolveAntigravityGlobalDir in getConfigDirFromHome. If the + // registry fails to load, this floor keeps that routing intact instead of + // silently falling through to the generic getGlobalConfigHomeFragment default + // (which would return the wrong '.claude' fragment). (ADR-1239 / #2096) + antigravity: Object.freeze({ globalDirResolver: 'antigravity' }), +}); + +/** + * Resolve a runtime's host behaviors from a capability registry, with the + * #338-privacy fail-safe floor when the registry (or the runtime's descriptor) + * is unavailable. Registry is passed in so this is unit-testable under a + * simulated registry-load failure. (ADR-1239 / #2086) + */ +function _resolveHostBehaviors(runtime, registry) { + const cap = registry && registry.runtimes && registry.runtimes[runtime]; + const declared = cap && cap.runtime && cap.runtime.hostBehaviors; + if (declared) return declared; + return FALLBACK_HOST_BEHAVIORS[runtime] || {}; +} + +/** + * Host-specific install behaviors, declared on the runtime descriptor + * (capabilities//capability.json -> runtime.hostBehaviors) instead of + * scattered `runtime === ''` string checks (ADR-1239 / #2086). Returns {} + * for runtimes that declare none, so every behavior branch degrades to the + * generic path by default — EXCEPT the reference host's #338-critical keys, which + * fall back to FALLBACK_HOST_BEHAVIORS if the registry failed to load. + */ +function _hostBehaviors(runtime) { + return _resolveHostBehaviors(runtime, _capabilityRegistry); +} + +/** + * Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a + * skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills -> + * $HOME/.agents/skills instead of the runtime's configDir). Descriptor-driven + * (no runtime === '' check) so the snapshot/rollback machinery and post-install + * verification look where the skills actually landed. Falls back to /skills. + */ +function _resolveSkillsRootDir(runtime, targetDir, scope) { + try { + const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope); + const skillsKind = layout.kinds.find((k) => k.kind === 'skills'); + if (skillsKind) return path.join(skillsKind.home || targetDir, skillsKind.destSubpath); + } catch (_e) { /* fall through to the configDir default */ } + return path.join(targetDir, 'skills'); +} + +/** + * Construct the imperative Host-Integration adapter (ADR-1239 / #2086), FAIL-OPEN. + * `createImperativeAdapter` composes the capability registry via + * `loadRegistry({includeInstalled:true})`, which require()s several capability + * modules. If any is unavailable (e.g. a packaging regression), return null so + * the caller degrades to the engine directly rather than hard-crashing install/ + * uninstall — matching the optional `capability-registry.cjs` load posture above. + */ +function _runtimeAdapter(runtime) { + try { + return createImperativeAdapter({ runtime }); + } catch { + return null; + } +} const { applyInstallerMigrationPlan, discoverInstallerMigrations, @@ -364,6 +486,7 @@ const { resolveRuntimeArtifactLayout, } = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs')); const { + assertDestWithinConfigHome, createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan, } = require(path.join(_gsdLibDir, 'runtime-artifact-install-plan.cjs')); @@ -373,8 +496,35 @@ const { } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'legacy-cleanup.cjs')); const { updateCacheFileName, + PACKAGE_NAME, } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'package-identity.cjs')); +// ADR-1239 Phase B: runtime-artifact install cluster extracted to install-engine.cjs. +// getCommitAttribution STAYS here (impure install-time config I/O); it is injected +// into the engine functions via the resolveAttribution parameter at each call site. +const installEngine = require(path.join(_gsdLibDir, 'install-engine.cjs')); +const { + installRuntimeArtifacts, + uninstallRuntimeArtifacts, + installOpencodeFamilySkills, + _installNativePluginIfDeclared, + _copyStaged, + hasExistingSymlinkBetween, + preserveUserArtifacts, + restoreUserArtifacts, + migrateLegacyDevPreferencesToSkill, + applyOpencodeFamilyPathPrefix, + convertClaudeCommandToOpencodeSkill, + convertClaudeCommandToKiloSkill, + USER_OWNED_ARTIFACTS, + _runLegacyInstallMigrations, + _runLegacyUninstallCleanup, + _removeGsdEntries, + _snapshotDir, + _restoreDir, + _removeHermesBareStemDirs, +} = installEngine; + // Parse args const args = process.argv.slice(2); const hasGlobal = args.includes('--global') || args.includes('-g'); @@ -407,7 +557,7 @@ if (hasMinimal && _profileArgRaw) { function selectRuntimesFromArgs(runtimeArgs) { if (runtimeArgs.includes('--all')) { - return ['claude', 'kimi', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline']; + return ['claude', 'kimi', 'kilo', 'opencode', 'pi', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode']; } if (runtimeArgs.includes('--both')) { return ['claude', 'opencode']; @@ -416,7 +566,7 @@ function selectRuntimesFromArgs(runtimeArgs) { const selected = []; if (runtimeArgs.includes('--claude')) selected.push('claude'); if (runtimeArgs.includes('--opencode')) selected.push('opencode'); - if (runtimeArgs.includes('--gemini')) selected.push('gemini'); + if (runtimeArgs.includes('--pi')) selected.push('pi'); if (runtimeArgs.includes('--kilo')) selected.push('kilo'); if (runtimeArgs.includes('--codex')) selected.push('codex'); if (runtimeArgs.includes('--copilot')) selected.push('copilot'); @@ -430,12 +580,44 @@ function selectRuntimesFromArgs(runtimeArgs) { if (runtimeArgs.includes('--kimi')) selected.push('kimi'); if (runtimeArgs.includes('--codebuddy')) selected.push('codebuddy'); if (runtimeArgs.includes('--cline')) selected.push('cline'); + if (runtimeArgs.includes('--zcode')) selected.push('zcode'); return selected; } // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = selectRuntimesFromArgs(args); +// #1928: Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is its +// official successor. `--gemini` is no longer a valid runtime selector — +// selectRuntimesFromArgs above no longer recognizes it, so it never lands in +// selectedRuntimes. Print a one-time redirect notice, and — when `--gemini` +// was the ONLY runtime flag supplied (selectedRuntimes is empty) — exit +// deterministically rather than silently falling through to the "no runtime +// specified" defaults below (which would install Claude Code, surprising a +// user who explicitly asked for Gemini). Other flags (e.g. `--codex`) still +// parse and install normally alongside the notice. +if (args.includes('--gemini')) { + const wantsHelp = args.includes('--help') || args.includes('-h'); + console.error('Gemini CLI was sunset by Google on 2026-06-18 and is no longer served for free/Pro/Ultra tiers.'); + console.error('GSD now supports Antigravity CLI (the official successor). Re-run with: --antigravity'); + if (hasUninstall) { + // The gemini runtime was removed (#1928), so there is no automated + // `--gemini --uninstall`. Guide manual cleanup and exit — do NOT fall + // through to the uninstall dispatch below, which defaults an empty runtime + // selection to 'claude' and would wrongly uninstall the user's Claude install. + console.error('The gemini runtime was removed, so `--gemini --uninstall` is no longer available.'); + console.error('To remove a prior Gemini install, delete GSD files under your Gemini config dir'); + console.error('(e.g. ~/.gemini/commands/gsd) and GSD hook entries in ~/.gemini/settings.json.'); + process.exit(1); + } + // For `--gemini --help`, fall through so the usage block still prints. For a + // bare install attempt (no other runtime selected), exit rather than silently + // installing Claude. + if (!wantsHelp && selectedRuntimes.length === 0) { + process.exit(1); + } +} + // WSL + Windows Node.js detection // When Windows-native Node runs on WSL, os.homedir() and path.join() produce // backslash paths that don't resolve correctly on the Linux filesystem. @@ -476,7 +658,7 @@ Then re-run: npx ${pkg.name}@latest /** * Get the config directory path relative to home directory for a runtime * Used for templating hooks that use path.join(homeDir, '', ...) - * @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot' + * @param {string} runtime - 'claude', 'opencode', 'codex', or 'copilot' * @param {boolean} isGlobal - Whether this is a global install */ function getConfigDirFromHome(runtime, isGlobal) { @@ -484,18 +666,19 @@ function getConfigDirFromHome(runtime, isGlobal) { // Local installs use the same dir name pattern return `'${getDirName(runtime)}'`; } - // Global installs - OpenCode uses XDG path structure - if (runtime === 'copilot') return "'.copilot'"; - if (runtime === 'opencode') { - // OpenCode: ~/.config/opencode -> '.config', 'opencode' - // Return as comma-separated for path.join() replacement - return "'.config', 'opencode'"; - } - if (runtime === 'gemini') return "'.gemini'"; - if (runtime === 'kilo') return "'.config', 'kilo'"; - if (runtime === 'codex') return "'.codex'"; - if (runtime === 'antigravity') { - if (!isGlobal) return "'.agents'"; + // Global installs. antigravity's home is resolved dynamically (env-overridable, + // multi-segment via resolveAntigravityGlobalDir + path.relative) — not a table + // entry. (The prior inner `if (!isGlobal) return "'.agents'"` was unreachable: + // !isGlobal returns at the top of this function.) + // Descriptor-driven (ADR-1239 / #2096): folded from a hardcoded + // `runtime === 'antigravity'` literal into a read of the runtime's + // `hostBehaviors.globalDirResolver` descriptor field (via _hostBehaviors, which + // also degrades to FALLBACK_HOST_BEHAVIORS on registry-load failure). This is + // antigravity-unique: unlike `configHome.kind === 'dot-home-nested'` (which + // windsurf also declares — see capabilities/windsurf/capability.json — and + // would wrongly route windsurf's global dir through + // resolveAntigravityGlobalDir), `globalDirResolver` is only set by antigravity. + if (_hostBehaviors(runtime).globalDirResolver === 'antigravity') { const antigravityDir = resolveAntigravityGlobalDir(); const rel = path.relative(os.homedir(), antigravityDir); const segments = rel.split(path.sep).filter(Boolean); @@ -506,16 +689,9 @@ function getConfigDirFromHome(runtime, isGlobal) { // stable legacy template so generated path.join() calls remain valid. return "'.gemini', 'antigravity'"; } - if (runtime === 'cursor') return "'.cursor'"; - if (runtime === 'windsurf') return "'.windsurf'"; - if (runtime === 'augment') return "'.augment'"; - if (runtime === 'trae') return "'.trae'"; - if (runtime === 'qwen') return "'.qwen'"; - if (runtime === 'hermes') return "'.hermes'"; - if (runtime === 'codebuddy') return "'.codebuddy'"; - if (runtime === 'cline') return "'.cline'"; - if (runtime === 'kimi') return "'.config', 'agents'"; - return "'.claude'"; + // All other runtimes: single source-of-truth fragment table (ADR-1239 Phase B, + // #1679). claude/unknown fall through to the table's default '.claude'. + return getGlobalConfigHomeFragment(runtime); } /** @@ -536,7 +712,7 @@ const banner = '\n' + ' GSD Core ' + dim + 'v' + pkg.version + reset + '\n' + ' Git. Ship. Done.\n' + ' A meta-prompting, context engineering and spec-driven\n' + - ' development workflows for Claude Code, OpenCode, Gemini, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline and CodeBuddy.\n'; + ' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy, ZCode and pi.\n'; // Pure seam: parse --config-dir / -c from an arbitrary args array. // Returns the path string, '' for an empty equals-form value, or null when the @@ -593,7 +769,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`); + console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`); process.exit(0); } @@ -617,6 +793,10 @@ const referencesHook = hooksSurface.referencesHook; // applySettingsJsonHooks: mutates settings.hooks.* in place with all GSD-managed // hook registrations for settings.json-surface runtimes (ADR-857 phase 5f-1b). const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks; +// writeKimiHooksToml / removeKimiHooksToml: kimi's native config.toml [[hooks]] +// surface (#2095 EoS/kimi Upgrade 1) — separate from settings.json entirely. +const writeKimiHooksToml = hooksSurface.writeKimiHooksToml; +const removeKimiHooksToml = hooksSurface.removeKimiHooksToml; // processAttribution: pure Co-Authored-By content transform, relocated to the // conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js // callers continue to work and there is a single implementation. (All call @@ -630,6 +810,15 @@ const processAttribution = runtimeArtifactConversion.processAttribution; const computePathPrefix = runtimeArtifactConversion._computePathPrefix; const applyRuntimeContentRewritesInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesInPlace; const applyRuntimeContentRewritesForCommandsInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace; +// #1675 (ADR-1508): the augment converter family is single-sourced in the +// conversion module. install.js re-binds (does not re-define) these so there +// is exactly one body — the generative-drift hazard the dedup removes. The two +// private helpers (getAugmentSkillAdapterHeader, convertSlashCommandsToAugmentSkillMentions) +// live only in the conversion module now; they are no longer duplicated here. +// (All call sites are below this line → no TDZ hazard.) +const convertClaudeToAugmentMarkdown = runtimeArtifactConversion.convertClaudeToAugmentMarkdown; +const convertClaudeCommandToAugmentSkill = runtimeArtifactConversion.convertClaudeCommandToAugmentSkill; +const convertClaudeAgentToAugmentAgent = runtimeArtifactConversion.convertClaudeAgentToAugmentAgent; function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) { return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts); @@ -857,6 +1046,9 @@ function resolveKiloConfigPath(configDir) { return path.join(configDir, 'kilo.json'); } +// #2087 — attribution config-path resolvers, keyed by descriptor (hostBehaviors.attributionConfigResolver) +const ATTRIBUTION_CONFIG_RESOLVERS = { opencode: resolveOpencodeConfigPath, kilo: resolveKiloConfigPath }; + /** * Strip JSONC comments (// and /* *​/) from a string to produce valid JSON. * Handles comments inside strings correctly (does not strip them). @@ -947,9 +1139,10 @@ function writeSettings(settingsPath, settings) { * Used by Codex TOML and OpenCode agent file generators to embed per-agent * model assignments so that model_overrides is respected on non-Claude runtimes (#2256). */ -function readGsdGlobalModelOverrides() { +function readGsdGlobalModelOverrides(options = {}) { try { - const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json'); + const home = options.homedir ? options.homedir() : os.homedir(); + const defaultsPath = path.join(home, '.gsd', 'defaults.json'); if (!fs.existsSync(defaultsPath)) return null; const raw = fs.readFileSync(defaultsPath, 'utf-8'); const parsed = JSON.parse(raw); @@ -986,8 +1179,8 @@ function readGsdGlobalModelOverrides() { * Returns a plain `{ agentName: modelId }` object, or `null` when neither * source defines `model_overrides`. */ -function readGsdEffectiveModelOverrides(targetDir = null) { - const global = readGsdGlobalModelOverrides(); +function readGsdEffectiveModelOverrides(targetDir = null, options = {}) { + const global = readGsdGlobalModelOverrides(options); let projectOverrides = null; if (targetDir) { @@ -1018,126 +1211,6 @@ function readGsdEffectiveModelOverrides(targetDir = null) { return { ...(global || {}), ...(projectOverrides || {}) }; } -/** - * #443 — Read the merged `effort` config block for install-time effort resolution. - * - * Probes the same config sources as readGsdRuntimeProfileResolver (per-project - * `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the - * `effort` object instead of the model-profile fields. - * - * Returns the merged `effort` object or null when neither source defines one. - * The caller can pass this to resolveInstallTimeEffort() which is pure and - * requires no filesystem access beyond what this helper already performs. - * - * @param {string|null} targetDir Runtime install root (walks up to find .planning/). - * @returns {object|null} - */ -function readGsdEffectiveEffortConfig(targetDir = null) { - const homeDefaults = _readGsdConfigFile( - path.join(os.homedir(), '.gsd', 'defaults.json'), - '~/.gsd/defaults.json' - ); - - let projectConfig = null; - if (targetDir) { - let probeDir = path.resolve(targetDir); - for (let depth = 0; depth < 8; depth += 1) { - const candidate = path.join(probeDir, '.planning', 'config.json'); - if (fs.existsSync(candidate)) { - projectConfig = _readGsdConfigFile(candidate, '.planning/config.json'); - break; - } - const parent = path.dirname(probeDir); - if (parent === probeDir) break; - probeDir = parent; - } - } - - const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort)) - ? homeDefaults.effort - : null; - const projectEffort = (projectConfig && projectConfig.effort && typeof projectConfig.effort === 'object' && !Array.isArray(projectConfig.effort)) - ? projectConfig.effort - : null; - - if (!homeEffort && !projectEffort) return null; - - // Per-project wins on conflict within each sub-field. Merge field-by-field so - // a project config that only sets agent_overrides still inherits global - // routing_tier_defaults and default. - return { - ...(homeEffort || {}), - ...(projectEffort || {}), - // Deep-merge agent_overrides (project wins per-key) - agent_overrides: { - ...((homeEffort && homeEffort.agent_overrides) || {}), - ...((projectEffort && projectEffort.agent_overrides) || {}), - }, - }; -} - - -/** - * #443 — Resolve install-time effort for a given agent, using the same - * precedence chain as resolveEffortInternal() in core.cjs, but operating - * on a pre-loaded effortCfg object (no loadConfig side-effects at install). - * - * Precedence (mirrors resolveEffortInternal): - * 1. effortCfg.agent_overrides[agentName] - * 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present) - * — OR manifest tier defaults when effortCfg is null - * 3. effortCfg.default - * 4. 'high' (hardcoded fallback) - * - * @param {object|null} effortCfg Result of readGsdEffectiveEffortConfig(). - * @param {string} agentName e.g. 'gsd-planner' - * @returns {string} Universal effort string (low/medium/high/xhigh/max/minimal) - */ -function resolveInstallTimeEffort(effortCfg, agentName) { - // Validates each candidate against the canonical EFFORT_SET (sourced once - // from core.cjs) before accepting it, mirroring resolveEffortInternal exactly. - // Invalid values fall through to the next precedence layer; final fallback 'high'. - - // Step 1: agent_overrides - if (effortCfg) { - const ao = effortCfg.agent_overrides; - if (ao && typeof ao === 'object' && !Array.isArray(ao)) { - const v = ao[agentName]; - if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v; - } - } - - // Step 2: routing_tier_defaults keyed by the agent's catalog tier - const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog(); - const agentTier = AGENT_DEFAULT_TIERS[agentName]; - if (agentTier) { - if (effortCfg && effortCfg.routing_tier_defaults && - typeof effortCfg.routing_tier_defaults === 'object' && - !Array.isArray(effortCfg.routing_tier_defaults)) { - const v = effortCfg.routing_tier_defaults[agentTier]; - if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v; - } else if (!effortCfg) { - // No effort config — use manifest tier defaults - const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier]; - if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v; - } - // effortCfg exists but has no routing_tier_defaults — fall through - } - - // Step 3: effort.default - if (effortCfg) { - const d = effortCfg.default; - if (typeof d === 'string' && GSD_EFFORT_SET.has(d)) return d; - } - - // Step 4: manifest default (sourced from config-defaults.manifest.json effort.default) - // If even the manifest default is invalid, fall back to 'high'. - if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) { - return EFFORT_MANIFEST_DEFAULT; - } - return 'high'; -} - /** * #443 — Inject `effort: ` into YAML frontmatter of a Claude .md agent * file in a newline-agnostic way (LF and CRLF source files are both handled). @@ -1243,29 +1316,6 @@ const READONLY_AGENT_DISALLOWED_TOOLS = { 'gsd-ui-auditor': 'Edit, MultiEdit', }; -/** - * #2517 — Read a single GSD config file (defaults.json or per-project - * config.json) into a plain object, returning null on missing/empty files - * and warning to stderr on JSON parse failures so silent corruption can't - * mask broken configs (review finding #5). - */ -function _readGsdConfigFile(absPath, label) { - if (!fs.existsSync(absPath)) return null; - let raw; - try { - raw = fs.readFileSync(absPath, 'utf-8'); - } catch (err) { - process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`); - return null; - } - try { - return JSON.parse(raw); - } catch (err) { - process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`); - return null; - } -} - /** * #2517 — Build a runtime-aware tier resolver for the install path. * @@ -1357,7 +1407,7 @@ const attributionCache = new Map(); /** * Get commit attribution setting for a runtime - * @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot' + * @param {string} runtime - 'claude', 'opencode', 'codex', or 'copilot' * @returns {null|undefined|string} null = remove, undefined = keep default, string = custom */ function getCommitAttribution(runtime) { @@ -1368,25 +1418,14 @@ function getCommitAttribution(runtime) { let result; - if (runtime === 'opencode' || runtime === 'kilo') { - const resolveConfigPath = runtime === 'opencode' - ? resolveOpencodeConfigPath - : resolveKiloConfigPath; + const _attrResolverKey = _hostBehaviors(runtime).attributionConfigResolver; + if (_attrResolverKey && ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey]) { + const resolveConfigPath = ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey]; const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null))); result = (config && config.disable_ai_attribution === true) ? null : undefined; - } else if (runtime === 'gemini') { - // Gemini: check gemini settings.json for attribution config - const settings = readSettings(path.join(getGlobalConfigDir('gemini', explicitConfigDir), 'settings.json')); - if (!settings || !settings.attribution || settings.attribution.commit === undefined) { - result = undefined; - } else if (settings.attribution.commit === '') { - result = null; - } else { - result = settings.attribution.commit; - } - } else if (runtime === 'claude') { + } else if (_hostBehaviors(runtime).attributionSource === 'settings-json-commit') { // Claude Code - const settings = readSettings(path.join(getGlobalConfigDir('claude', explicitConfigDir), 'settings.json')); + const settings = readSettings(path.join(getGlobalConfigDir(runtime, explicitConfigDir), 'settings.json')); if (!settings || !settings.attribution || settings.attribution.commit === undefined) { result = undefined; } else if (settings.attribution.commit === '') { @@ -1854,11 +1893,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c // Hermes' SKILL.md spec lists `version` as a required frontmatter field. // Track GSD's package version so Hermes' skill_view() reports a stable // identifier per install. - if (runtime === 'hermes') fm += `version: ${yamlQuote(pkg.version)}\n`; - // #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen - // so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but - // we keep their output byte-stable). skillName is the `gsd-` dir name. - if (runtime === 'qwen') { + if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`; + // #778 (b) — numeric priority for /skills ordering, declared on the runtime + // descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to + // runtimes that declare the flag so Claude/Hermes skill frontmatter is + // unchanged (they ignore the field, but we keep their output byte-stable). + // skillName is the `gsd-` dir name. (ADR-1239 / #2086) + if (_hostBehaviors(runtime).skillPriorityFrontmatter) { const stem = typeof skillName === 'string' && skillName.startsWith('gsd-') ? skillName.slice(4) : skillName; @@ -1903,6 +1944,14 @@ function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) { .replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`); } +// DEFECT.GENERATIVE-FIX: this body is mirrored in +// src/runtime-artifact-conversion.cts's convertClaudeCommandToKimiSkill (dead +// for the live skills-install path, which routes here via +// install-engine.cts's SKILLS_CONVERTER_REGISTRY through the kimi capability +// descriptor's artifactLayout `converter: "convertClaudeCommandToKimiSkill"`; +// kept for bin/install.js's own module-level export/test surface). Neither +// copy re-exports the other — mirror any behavior change into both. Guarded +// by the output-parity test in tests/runtime-converters.test.cjs (#2095). function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) { const { frontmatter, body } = extractFrontmatterAndBody(content); const kimiSkillName = normalizeKimiSkillName(skillName); @@ -2047,6 +2096,16 @@ function buildKimiSubagentYaml({ name, description, tools }) { return `${lines.join('\n')}\n`; } +// DEFECT.GENERATIVE-FIX: this body is mirrored in +// src/runtime-artifact-conversion.cts's buildKimiAgentArtifacts (dead for the +// live install path, which routes here via runtime-artifact-layout.cts's +// kimiAgentsKind — see its `conversionExports['buildKimiAgentArtifacts']` +// dynamic lookup against the compiled runtime-artifact-conversion.cjs; kept +// for bin/install.js's own module-level export/test surface). Neither copy +// re-exports the other — mirror any behavior change into both, including the +// kimi_cli.tools.agent:Agent grant that enables background dispatch +// (#2095 Upgrade 2). Guarded by the output-parity test in +// tests/runtime-converters.test.cjs (#2095). function buildKimiAgentArtifacts({ rootAgent = '', subagents = [], @@ -2572,113 +2631,14 @@ function convertClaudeAgentToWindsurfAgent(content) { // Augment uses a tool set similar to Cursor/Windsurf. // Config lives in .augment/ (local) and ~/.augment/ (global). -const claudeToAugmentTools = { - Bash: 'launch-process', - Edit: 'str-replace-editor', - AskUserQuestion: null, - SlashCommand: null, - TodoWrite: 'add_tasks', -}; - -function convertSlashCommandsToAugmentSkillMentions(content) { - return content.replace(/gsd:/gi, 'gsd-'); -} - -function convertClaudeToAugmentMarkdown(content) { - let converted = convertSlashCommandsToAugmentSkillMentions(content); - converted = converted.replace(/\bBash\(/g, 'launch-process('); - converted = converted.replace(/\bEdit\(/g, 'str-replace-editor('); - converted = converted.replace(/\bRead\(/g, 'view('); - converted = converted.replace(/\bWrite\(/g, 'save-file('); - converted = converted.replace(/\bTodoWrite\(/g, 'add_tasks('); - converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting'); - // Replace subagent_type from Claude to Augment format - converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); - converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); - // Replace project-level Claude conventions with Augment equivalents - converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.augment/rules/`'); - converted = converted.replace(/\.\/CLAUDE\.md/g, '.augment/rules/'); - converted = converted.replace(/`CLAUDE\.md`/g, '`.augment/rules/`'); - converted = converted.replace(/\bCLAUDE\.md\b/g, '.augment/rules/'); - converted = converted.replace(/\.claude\/skills\//g, '.augment/skills/'); - // Remove Claude Code-specific bug workarounds before brand replacement - converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); - converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); - // Replace "Claude Code" brand references with "Augment" - converted = converted.replace(/\bClaude Code\b/g, 'Augment'); - return converted; -} - -function getAugmentSkillAdapterHeader(skillName) { - return ` -## A. Skill Invocation -- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill. -- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`. -- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty. - -## B. User Prompting -When the workflow needs user input, prompt the user conversationally: -- Present options as a numbered list in your response text -- Ask the user to reply with their choice -- For multi-select, ask for comma-separated numbers - -## C. Tool Usage -Use these Augment tools when executing GSD workflows: -- \`launch-process\` for running commands (terminal operations) -- \`str-replace-editor\` for editing existing files -- \`view\` for reading files and listing directories -- \`save-file\` for creating new files -- \`grep\` for searching code (or use MCP servers for advanced search) -- \`web-search\`, \`web-fetch\` for web queries -- \`add_tasks\`, \`view_tasklist\`, \`update_tasks\` for task management - -## D. Subagent Spawning -When the workflow needs to spawn a subagent: -- Use the built-in subagent spawning capability -- Define agent prompts in \`.augment/agents/\` directory -`; -} - -function convertClaudeCommandToAugmentSkill(content, skillName) { - const converted = convertClaudeToAugmentMarkdown(content); - const { frontmatter, body } = extractFrontmatterAndBody(converted); - let description = `Run GSD workflow ${skillName}.`; - if (frontmatter) { - const maybeDescription = extractFrontmatterField(frontmatter, 'description'); - if (maybeDescription) { - description = maybeDescription; - } - } - description = toSingleLine(description); - const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; - const adapter = getAugmentSkillAdapterHeader(skillName); - - return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; -} - -/** - * Convert Claude Code agent markdown to Augment agent format. - * Strips frontmatter fields Augment doesn't support (color, skills), - * converts tool references, and cleans up for Augment agents. - */ -function convertClaudeAgentToAugmentAgent(content) { - let converted = convertClaudeToAugmentMarkdown(content); - - const { frontmatter, body } = extractFrontmatterAndBody(converted); - if (!frontmatter) return converted; - - const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; - const description = extractFrontmatterField(frontmatter, 'description') || ''; - - const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; - - return `${cleanFrontmatter}\n${body}`; -} - -/** - * Copy Claude commands as Augment skills — one folder per skill with SKILL.md. - * Mirrors copyCommandsAsCursorSkills but uses Augment converters. - */ +// #1675 (ADR-1508): the augment converter family below was a byte-identical +// duplicate of runtime-artifact-conversion.cjs: +// convertSlashCommandsToAugmentSkillMentions, convertClaudeToAugmentMarkdown, +// getAugmentSkillAdapterHeader, convertClaudeCommandToAugmentSkill, +// convertClaudeAgentToAugmentAgent +// Deleted here and bound from runtimeArtifactConversion above (single source). +// The DEFECT.GENERATIVE-FIX parity guard in +// tests/enh-1511-rewrite-engine-relocation.test.cjs asserts reference identity. function convertSlashCommandsToTraeSkillMentions(content) { return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => { @@ -2712,6 +2672,14 @@ function convertClaudeToTraeMarkdown(content) { return converted; } +// DEFECT.GENERATIVE-FIX: this body is mirrored in +// src/runtime-artifact-conversion.cts's convertClaudeCommandToTraeSkill (used +// by src/install-engine.cts's skills-install path via +// SKILLS_CONVERTER_REGISTRY). This bin/install.js copy is dead for the live +// skills-install path — kept for this file's own module-level export/test +// surface. Neither copy re-exports the other — mirror any behavior change +// into both. Guarded by the output-parity test in +// tests/runtime-converters.test.cjs (#2094). function convertClaudeCommandToTraeSkill(content, skillName) { const converted = convertClaudeToTraeMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); @@ -2726,7 +2694,16 @@ function convertClaudeCommandToTraeSkill(content, skillName) { const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; // #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's // frontmatter parser. - return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`; + let fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n`; + // #2094: emit `stage:` so Trae's SOLO agent can auto-invoke GSD skills at + // the corresponding stage (docs.trae.ai/ide/agent). The field name/schema + // is not formally documented (thin SPA docs) — descriptor-driven, single + // fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/ + // best-effort. + const soloStage = _hostBehaviors('trae').soloStageMetadata; + if (soloStage) fm += `stage: ${soloStage}\n`; + fm += '---'; + return `${fm}\n${body}`; } function convertClaudeAgentToTraeAgent(content) { @@ -3314,6 +3291,77 @@ function cleanupWindsurfLegacyDevinSkills(workspaceDir) { return removed; } +/** + * Migrate a skills kind that moved to an alternate `home` (ADR-1239 split-home): + * remove now-stale `*` skill dirs left at the OLD configDir-rooted + * location by installs from before the move. Without this, upgrading (e.g. Codex + * relocating skills to ~/.agents/skills) orphans the pre-move dirs at + * ~/.codex/skills. Only managed `*` dirs are touched; user-owned content + * (non-prefixed dirs, gsd-dev-preferences, symlinks) is preserved. Fail-open. + * @param {string} oldSkillsDir absolute path to the pre-move skills location + * @param {string} prefix managed skill-dir prefix (e.g. 'gsd-') + * @returns {number} count of stale dirs removed + */ +function cleanupMovedSkillsOldLocation(oldSkillsDir, prefix) { + if (!fs.existsSync(oldSkillsDir)) return 0; + + // Mirror the user-owned list from cleanupCodexSkillMetadataSidecars (#2973). + const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']); + let removed = 0; + + for (const entry of fs.readdirSync(oldSkillsDir, { withFileTypes: true })) { + if (!entry.isDirectory() || !entry.name.startsWith(prefix)) continue; + if (_userOwnedSkillDirs.has(entry.name)) continue; + + const dirToRemove = path.join(oldSkillsDir, entry.name); + try { + // Symlink guard (mirrors cleanupWindsurfLegacyDevinSkills): never delete + // through a symlinked gsd-* dir — it could escape the tree. + const stat = fs.lstatSync(dirToRemove); + if (stat.isSymbolicLink()) continue; + + fs.rmSync(dirToRemove, { recursive: true, force: true }); + removed++; + } catch (_err) { + // Fail open — a single bad dir must not block install/uninstall. + } + } + + // Prune the old skills dir if now empty — leaves the configHome clean. + // Never remove a non-empty container (user may keep other content there). + try { + if (fs.existsSync(oldSkillsDir) && fs.readdirSync(oldSkillsDir).length === 0) { + fs.rmdirSync(oldSkillsDir); + } + } catch (_err) { + // best-effort container cleanup + } + + return removed; +} + +/** + * When a runtime's skills kind declares an alternate `home` (split-home move), + * return the now-stale configDir-rooted skills location that installs before the + * move used; null when no move is in effect (no home override, or home resolves + * to the same path). Descriptor-driven — no per-runtime hardcoding. + * @returns {string|null} + */ +function _resolveMovedSkillsOldDir(runtime, targetDir, scope) { + try { + const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope); + const skillsKind = layout.kinds.find((k) => k.kind === 'skills'); + if (skillsKind && skillsKind.home) { + const oldDir = path.join(targetDir, skillsKind.destSubpath); + const newDir = path.join(skillsKind.home, skillsKind.destSubpath); + if (path.resolve(oldDir) !== path.resolve(newDir)) return oldDir; + } + } catch (_e) { + // No migration when the layout can't resolve — never block on this. + } + return null; +} + /** * Generate the GSD config block for Codex config.toml. * @param {Array<{name: string, description: string}>} agents @@ -3329,6 +3377,17 @@ function generateCodexConfigBlock(agents, targetDir) { '', ]; + // ADR-1239 upgrade 2 / #2088 — explicit dispatch tuning. Pin `max_depth` on the + // `[agents]` (AgentsToml) table rather than relying on codex-cli's implicit + // default, realizing the negotiated `dispatch.maxDepth: 1` axis. This bare + // `[agents]` scalar table coexists with the flattened `[agents.]` role + // sub-tables below (validated by validateCodexConfigSchema, which permits a + // known-scalar-only `[agents]`). Emitted before the role tables so the parent + // table is opened first. + lines.push('[agents]'); + lines.push(`max_depth = ${GSD_CODEX_AGENTS_MAX_DEPTH}`); + lines.push(''); + for (const { name, description } of agents) { // #2727 — Codex 0.124.0 requires [agents.] struct format, not [[agents]] sequence. // [[agents]] (introduced in #2645) is rejected by codex-cli 0.124.0 with @@ -3342,6 +3401,52 @@ function generateCodexConfigBlock(agents, targetDir) { return lines.join('\n'); } +/** + * Extract a user's pre-existing AgentsToml scalar assignments from a bare + * `[agents]` table — every known scalar EXCEPT `max_depth` (which GSD manages + * and always re-emits as 1). Returned as raw `key = value` line strings so + * mergeCodexConfig can PRESERVE them in the managed block instead of silently + * dropping the user's tuning when the bare `[agents]` table is purged (#2088 + * review finding: the loosened validator declares such a table legitimate, so + * install must not destroy it). Only the first bare `[agents]` section is read; + * `[agents.]` role tables are ignored. Fail-open → []. + * @returns {string[]} + */ +function extractCodexUserAgentsScalars(content) { + const preserved = []; + let section; + try { + section = getTomlTableSections(content).find((s) => !s.array && s.path === 'agents'); + } catch (_e) { + return preserved; + } + if (!section) return preserved; + const body = content.slice(section.headerEnd, section.end); + for (const record of getTomlLineRecords(body)) { + if (record.startsInMultilineString || record.tableHeader) continue; + const trimmed = record.text.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + if (!record.keySegments || record.keySegments.length !== 1) continue; + const key = record.keySegments[0]; + if (key === 'max_depth') continue; // GSD-managed — GSD's value wins. + if (!CODEX_AGENTS_TOML_SCALAR_KEYS.has(key)) continue; + preserved.push(trimmed); + } + return preserved; +} + +/** + * Splice preserved user AgentsToml scalar lines into the managed GSD config + * block, immediately after the `[agents]` header and before GSD's `max_depth` + * line. Operates on the pre-EOL-normalization block (LF joins), matching only + * the bare `[agents]` header (never `[agents.]`). Returns the block + * unchanged when there is nothing to preserve or the anchor is absent. + */ +function spliceCodexAgentsScalars(block, scalarLines) { + if (!scalarLines || scalarLines.length === 0) return block; + return block.replace(/(\n\[agents\]\n)(max_depth = )/, `$1${scalarLines.join('\n')}\n$2`); +} + /** * Strip any managed GSD agent sections from a TOML string. * @@ -3364,6 +3469,16 @@ function stripCodexGsdAgentSections(content) { return true; } + // GSD's managed `[agents]` scalar block (ADR-1239 upgrade 2 / #2088 — the + // `max_depth` dispatch-tuning table). Install purges any pre-existing bare + // `[agents]` and writes its own, so a known-scalar-only bare `[agents]` is + // GSD-owned; strip it on uninstall. (The marker path already removes it via + // the marker-to-EOF cut; this covers the no-marker fallback.) + if (!section.array && section.path === 'agents') { + const body = content.slice(section.headerEnd, section.end); + return codexBareAgentsHasOnlyKnownScalars(body); + } + // Legacy `[[agents]]` array-of-tables (#2645) — only strip blocks whose // `name = "gsd-..."`, preserving user-authored [[agents]] entries. if (section.array && section.path === 'agents') { @@ -3391,7 +3506,11 @@ function stripGsdFromCodexConfig(content) { const codexHooksOwnership = getManagedCodexHooksOwnership(content); if (markerIndex !== -1) { - // Has GSD marker — remove everything from marker to EOF + // Has GSD marker — remove everything from marker to EOF. First recover the + // user's own AgentsToml scalars (max_threads etc.) that install folded into + // the managed [agents] block (#2088), so a full install→uninstall cycle + // round-trips the user's tuning. GSD-managed max_depth is dropped. + const preservedScalars = extractCodexUserAgentsScalars(content.slice(markerIndex)); let before = content.substring(0, markerIndex); before = stripCodexHooksFeatureAssignments(before, codexHooksOwnership); // Also strip GSD-injected feature keys above the marker (Case 3 inject) @@ -3400,6 +3519,9 @@ function stripGsdFromCodexConfig(content) { before = before.replace(/^\[features\]\s*\n(?=\[|$)/m, ''); before = before.replace(/^\[agents\]\s*\n(?=\[|$)/m, ''); before = before.replace(/^(?:\r?\n)+/, '').trimEnd(); + if (preservedScalars.length > 0) { + before = (before ? before + eol + eol : '') + '[agents]' + eol + preservedScalars.join(eol); + } if (!before) return null; return before + eol; } @@ -3410,7 +3532,11 @@ function stripGsdFromCodexConfig(content) { cleaned = cleaned.replace(/^multi_agent\s*=\s*true\s*(?:\r?\n)?/m, ''); cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*(?:\r?\n)?/m, ''); - // Remove [agents.gsd-*] sections (from header to next section or EOF) + // #2088: recover the user's own AgentsToml scalars before the [agents] table is + // stripped, so they survive uninstall even in the no-marker fallback path. + const preservedScalars = extractCodexUserAgentsScalars(cleaned); + + // Remove [agents.gsd-*] sections + the managed known-scalar [agents] table. cleaned = stripCodexGsdAgentSections(cleaned); // Remove [features] section if now empty (only header, no keys before next section) @@ -3421,6 +3547,10 @@ function stripGsdFromCodexConfig(content) { cleaned = cleaned.replace(/^(?:\r?\n)+/, '').trimEnd(); + if (preservedScalars.length > 0) { + cleaned = (cleaned ? cleaned + eol + eol : '') + '[agents]' + eol + preservedScalars.join(eol); + } + if (!cleaned) return null; return cleaned + eol; } @@ -4892,6 +5022,33 @@ function parseTomlToObject(content) { * - `hooks.` MUST be an array of tables when present (Codex ≥0.124 * rejects bare `[hooks.]` single-bracket maps). */ +/** + * True when a bare `[agents]` table body contains ONLY known AgentsToml scalar + * keys (CODEX_AGENTS_TOML_SCALAR_KEYS) — i.e. it is a valid AgentsToml struct + * that Codex's `deny_unknown_fields` will accept, not the break-causing form + * (#2760) that carries an unknown key. Comments and blank lines are ignored; an + * empty body is trivially valid. Mirrors isLegacyGsdAgentsSection's line scan. + */ +function codexBareAgentsHasOnlyKnownScalars(body) { + const lineRecords = getTomlLineRecords(body); + for (const record of lineRecords) { + // Conservative reject of anything not positively a single known-scalar + // assignment. A multiline-string value cannot be a valid AgentsToml scalar + // (max_threads/max_depth/job_max_runtime_seconds are integers, + // interrupt_message is a bool — none are strings), so codex would reject it + // too; rejecting here is correct, not a false negative. + if (record.startsInMultilineString) return false; + if (record.tableHeader) return false; + const trimmed = record.text.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + if (!record.keySegments || record.keySegments.length !== 1 || + !CODEX_AGENTS_TOML_SCALAR_KEYS.has(record.keySegments[0])) { + return false; + } + } + return true; +} + function validateCodexConfigSchema(content) { let parsed; try { @@ -4920,10 +5077,21 @@ function validateCodexConfigSchema(content) { } if (!section.array && section.path === 'agents') { - return { - ok: false, - reason: 'bare [agents] table is invalid in current Codex schema (expected [agents.] struct form)', - }; + // #2760 rejected ALL bare `[agents]` tables because a bare table holding a + // non-AgentsToml key (`default = "x"`, a role name, etc.) triggers Codex's + // "invalid type: ..., expected struct AgentsToml" and breaks every CLI + // invocation. But a bare `[agents]` whose keys are all valid AgentsToml + // scalars (max_depth/max_threads/...) IS a valid struct — that is exactly + // GSD's managed `max_depth` dispatch-tuning block (ADR-1239 upgrade 2 / + // #2088), and a user's own scalar tuning. Permit known-scalar-only; still + // reject any bare `[agents]` carrying an unknown key. + const body = content.slice(section.headerEnd, section.end); + if (!codexBareAgentsHasOnlyKnownScalars(body)) { + return { + ok: false, + reason: 'bare [agents] table with a non-AgentsToml key is invalid in current Codex schema (expected [agents.] struct form, or only AgentsToml scalars like max_depth/max_threads)', + }; + } } // hooks.state.* is Codex's persistent hook-trust namespace (added in @@ -5199,7 +5367,13 @@ function mergeCodexConfig(configPath, gsdBlock) { const existing = fs.readFileSync(configPath, 'utf8'); const eol = detectLineEnding(existing); - const normalizedGsdBlock = gsdBlock.replace(/\r?\n/g, eol); + // #2088 review: the bare `[agents]` table is purged below (Case 2/3 via + // stripLeakedGsdCodexSections) to keep a single managed `[agents]`. Preserve + // the user's own AgentsToml scalar tuning (max_threads, job_max_runtime_seconds, + // interrupt_message — everything except GSD-managed max_depth) by re-emitting + // it inside the managed block, so install never silently drops it. + const mergedGsdBlock = spliceCodexAgentsScalars(gsdBlock, extractCodexUserAgentsScalars(existing)); + const normalizedGsdBlock = mergedGsdBlock.replace(/\r?\n/g, eol); const markerIndex = existing.indexOf(GSD_CODEX_MARKER); // Case 2: Has GSD marker — truncate and re-append @@ -5670,6 +5844,37 @@ function removeCursorHooksJson(targetDir) { return hooksSurface.removeCursorHooksJson(targetDir); } +/** + * #2100 Stage 2 — Write GSD-managed Windsurf/Cascade lifecycle hooks into + * /hooks.json. Both managed hook scripts + * (gsd-windsurf-pre-write.js, gsd-windsurf-pre-command.js) are copied from + * the GSD hooks/ source to /hooks/ first, so the hooks.json + * entries never reference a script that wasn't installed. Mirrors + * writeCursorHooksJson's structure; Cascade's blocking protocol (exit code 2) + * and entry shape (bare `command` string, no `type` field) are distinct from + * Cursor's. + * + * @param {string} targetDir - The Windsurf config dir (global: ~/.codeium/windsurf; local: .windsurf) + * @param {string} src - The GSD install source root (for copying hook scripts) + * @param {{ platform?: string }} opts + * @returns {{ hooksJsonPath: string, changed: boolean }} + */ +function writeWindsurfHooksJson(targetDir, src, opts) { + return hooksSurface.writeWindsurfHooksJson(targetDir, src, opts); +} + +/** + * Remove all GSD-managed Windsurf/Cascade lifecycle hook entries from + * hooks.json. User-owned entries are preserved. If the file becomes empty, + * it is removed. + * + * @param {string} targetDir - The Windsurf config dir + * @returns {{ changed: boolean }} + */ +function removeWindsurfHooksJson(targetDir) { + return hooksSurface.removeWindsurfHooksJson(targetDir); +} + /** * #786 — Build the GSD-managed GitHub Copilot lifecycle hook config object. * @@ -5710,8 +5915,21 @@ function writeCopilotHookConfig(targetDir) { * Reads agent .md files from source, extracts metadata, writes .toml configs. */ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-sandbox') { - const configPath = path.join(targetDir, 'config.toml'); - const agentsTomlDir = path.join(targetDir, 'agents'); + // ADR-1239 Phase B write-confinement: every Codex config write stays under targetDir. + const configPath = assertDestWithinConfigHome(targetDir, 'config.toml'); + const agentsTomlDir = assertDestWithinConfigHome(targetDir, 'agents'); + const resolvedTargetRoot = path.resolve(targetDir); + // Symlink-escape guard (parity with _copyStaged / copyWithPathReplacement): the + // lexical gate above does not resolve symlinks, so a pre-existing config.toml or + // agents/ symlink could redirect writes outside targetDir. Reject those. + if ( + hasExistingSymlinkBetween(resolvedTargetRoot, configPath) || + hasExistingSymlinkBetween(resolvedTargetRoot, path.resolve(agentsTomlDir)) + ) { + throw new Error( + `installCodexConfig: a Codex config path under "${targetDir}" contains a symlink escaping the install root — refusing to write`, + ); + } fs.mkdirSync(agentsTomlDir, { recursive: true }); const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md')); @@ -5756,7 +5974,16 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san // follows the same config-driven precedence as the Claude .md effort key. const effortCfg = readGsdEffectiveEffortConfig(targetDir); const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier); - fs.writeFileSync(path.join(agentsTomlDir, `${name}.toml`), tomlContent); + // Confine the per-agent write to the agents/ dir itself: a crafted agent + // `name` containing path separators must not escape agents/ (which would let + // it clobber config.toml or write elsewhere under the configHome). + const agentTomlPath = assertDestWithinConfigHome(agentsTomlDir, `${name}.toml`); + if (hasExistingSymlinkBetween(resolvedTargetRoot, agentTomlPath)) { + throw new Error( + `installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink escaping the install root — refusing to write`, + ); + } + fs.writeFileSync(agentTomlPath, tomlContent); } const gsdBlock = generateCodexConfigBlock(agents, targetDir); @@ -5765,11 +5992,6 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san return agents.length; } -/** - * Strip HTML tags for Gemini CLI output - * Terminals don't support subscript — Gemini renders these as raw HTML. - * Converts text to italic *(text)* for readable terminal output. - */ /** * Runtime-neutral agent name and instruction file replacement. * Used by ALL non-Claude runtime converters to avoid Claude-specific @@ -5800,200 +6022,6 @@ function neutralizeAgentReferences(content, instructionFile) { return c; } -function stripSubTags(content) { - return content.replace(/(.*?)<\/sub>/g, '*($1)*'); -} - -/** - * Convert Claude Code agent frontmatter to Gemini CLI format - * Gemini agents use .md files with YAML frontmatter, same as Claude, - * but with different field names and formats: - * - tools: must be a YAML array (not comma-separated string) - * - tool names: must use Gemini built-in names (read_file, not Read) - * - color: must be removed (causes validation error) - * - skills: must be removed (causes validation error) - * - mcp__* tools: must be excluded (auto-discovered at runtime) - */ -let _gsdCommandRoster = null; -let _gsdCommandRosterWarned = false; - -/** - * Get the list of known GSD commands from the source directory. - * Caches the result after the first scan. Emits a one-shot warning if the - * source directory cannot be located — an empty roster silently neutralises - * every Gemini slash-command conversion, which is the bug this code exists - * to prevent. The warning is gated on GSD_TEST_MODE to keep test output clean. - * @returns {Set} Set of command names (without .md extension) - */ -function getGsdCommandRoster() { - if (_gsdCommandRoster) return _gsdCommandRoster; - const baseDir = (typeof __dirname !== 'undefined') ? __dirname : process.cwd(); - const gsdSrc = path.join(baseDir, '..', 'commands', 'gsd'); - if (fs.existsSync(gsdSrc)) { - _gsdCommandRoster = new Set( - fs.readdirSync(gsdSrc) - .filter(f => f.endsWith('.md')) - .map(f => f.replace('.md', '')) - ); - } else { - _gsdCommandRoster = new Set(); - if (!_gsdCommandRosterWarned && !process.env.GSD_TEST_MODE) { - _gsdCommandRosterWarned = true; - console.warn( - `WARNING: GSD command roster not found at ${gsdSrc}. ` + - `Gemini /gsd- → /gsd: conversion will be a no-op. ` + - `This usually means the package was installed without commands/gsd/.` - ); - } - } - return _gsdCommandRoster; -} - -// Test-only: reset the cached roster. Exported via GSD_TEST_MODE bundle below. -function _resetGsdCommandRoster() { - _gsdCommandRoster = null; - _gsdCommandRosterWarned = false; -} - -function convertSlashCommandsToGeminiMentions(content) { - const commands = getGsdCommandRoster(); - // Defense in depth: regex boundary AND roster lookup must both agree. - // - // - Lookbehind `(? { - return commands.has(commandName) ? `/gsd:${commandName}` : match; - }); -} - -function convertClaudeToGeminiMarkdown(content, { isCommand = false, commandName = null } = {}) { - // Apply Gemini-specific slash command namespacing - let converted = convertSlashCommandsToGeminiMentions(content); - // Gemini CLI does not expose Claude's AskUserQuestion tool. Convert body - // references to runtime-neutral wording so converted agents do not instruct - // Gemini to call a nonexistent tool (#3362). - converted = converted.replace(/\b(?:AskUserQuestion|ask_user)\b/g, 'conversational prompting'); - // Strip HTML subscript tags — terminals can't render them. Done before - // TOML conversion so the prompt body of a command file is also clean. - converted = stripSubTags(converted); - - if (isCommand) { - // Convert to Gemini TOML format (threads the command name so per-command - // enrichment — e.g. the #778 live-state injection — can target a command). - converted = convertClaudeToGeminiToml(converted, { commandName }); - } - - return converted; -} - -function convertClaudeToGeminiAgent(content) { - if (!content.startsWith('---')) return content; - - const endIndex = content.indexOf('---', 3); - if (endIndex === -1) return content; - - const frontmatter = content.substring(3, endIndex).trim(); - const body = content.substring(endIndex + 3); - - const lines = frontmatter.split('\n'); - const newLines = []; - let inAllowedTools = false; - let inSkippedArrayField = false; - const tools = []; - - for (const line of lines) { - const trimmed = line.trim(); - - if (inSkippedArrayField) { - if (!trimmed || trimmed.startsWith('- ')) { - continue; - } - inSkippedArrayField = false; - } - - // Convert allowed-tools YAML array to tools list - if (trimmed.startsWith('allowed-tools:')) { - inAllowedTools = true; - continue; - } - - // Handle inline tools: field (comma-separated string) - if (trimmed.startsWith('tools:')) { - const toolsValue = trimmed.substring(6).trim(); - if (toolsValue) { - const parsed = toolsValue.split(',').map(t => t.trim()).filter(t => t); - for (const t of parsed) { - const mapped = convertGeminiToolName(t); - if (mapped) tools.push(mapped); - } - } else { - // tools: with no value means YAML array follows - inAllowedTools = true; - } - continue; - } - - // Strip color field (not supported by Gemini CLI, causes validation error) - if (trimmed.startsWith('color:')) continue; - - // Strip skills field (not supported by Gemini CLI, causes validation error) - if (trimmed.startsWith('skills:')) { - inSkippedArrayField = true; - continue; - } - - // Collect allowed-tools/tools array items - if (inAllowedTools) { - if (trimmed.startsWith('- ')) { - const mapped = convertGeminiToolName(trimmed.substring(2).trim()); - if (mapped) tools.push(mapped); - continue; - } else if (trimmed && !trimmed.startsWith('-')) { - inAllowedTools = false; - } - } - - if (!inAllowedTools) { - newLines.push(line); - } - } - - // Add tools as YAML array (Gemini requires array format) - if (tools.length > 0) { - newLines.push('tools:'); - for (const tool of tools) { - newLines.push(` - ${tool}`); - } - } - - const newFrontmatter = newLines.join('\n').trim(); - - // Escape ${VAR} patterns in agent body for Gemini CLI compatibility. - // Gemini's templateString() treats all ${word} patterns as template variables - // and throws "Template validation failed: Missing required input parameters" - // when they can't be resolved. GSD agents use ${PHASE}, ${PLAN}, etc. as - // shell variables in bash code blocks — convert to $VAR (no braces) which - // is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex. - const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1'); - - // Runtime-neutral agent name replacement (#766) - const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md'); - // Apply Gemini-specific transformations (slash commands + sub-tag stripping) - const geminiBody = convertClaudeToGeminiMarkdown(neutralBody); - return `---\n${newFrontmatter}\n---${geminiBody}`; -} - function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOverride = null } = {}) { // Replace tool name references in content (applies to all files) let convertedContent = content; @@ -6151,7 +6179,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve } // Kilo CLI — same conversion logic as OpenCode, different config paths. -function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) { +// DEFECT.GENERATIVE-FIX: this body is mirrored in +// src/runtime-artifact-conversion.cts's convertClaudeToKiloFrontmatter (used by +// src/install-engine.cts's install path). Neither copy re-exports the other — +// mirror any behavior change into both. Guarded by the output-parity test in +// tests/runtime-converters.test.cjs (#2093). +function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) { // Replace tool name references in content (applies to all files) let convertedContent = content; convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question'); @@ -6310,6 +6343,13 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) { // For agents: add required Kilo agent fields if (isAgent) { newLines.push('mode: subagent'); + // Embed model override from ~/.gsd/defaults.json so model_overrides is + // respected on Kilo (which uses static agent frontmatter, not inline + // Task() model parameters) — mirrors convertClaudeToOpencodeFrontmatter's + // model emission exactly (#2093 UPGRADE 2 / ADR-1239). See #2256. + if (modelOverride) { + newLines.push(['model:', modelOverride].join(' ')); + } newLines.push(...buildKiloAgentPermissionBlock(agentTools)); } @@ -6326,218 +6366,18 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) { return `---\n${newFrontmatter}\n---${body}`; } -/** - * Shared SKILL.md writer for the OpenCode-family runtimes (OpenCode + Kilo), - * which share a config schema (Kilo derives from OpenCode). OpenCode discovers - * skills as `skills//SKILL.md` and Kilo follows the same layout - * (https://opencode.ai/docs/skills, https://kilo.ai/docs/customize/skills). - * - * The skill body reuses the runtime's command-frontmatter converter for tool, - * path, and `/gsd:`→`/gsd-` body rewrites, then rebuilds a minimal skill - * frontmatter: only `name` (lowercase-hyphen, must match the containing - * directory) and `description` (1–1024 chars) are emitted, per the OpenCode - * skill spec. The command's `tools:`/`permission:` block is intentionally - * dropped — OpenCode skills are loaded on-demand via the native skill tool and - * inherit the calling agent's permissions. - * - * @param {string} content - Claude command markdown (with YAML frontmatter) - * @param {string} skillName - Skill directory name (e.g. gsd-help) - * @param {(content: string) => string} frontmatterConverter - runtime command converter - * @returns {string} SKILL.md content - */ -function convertClaudeCommandToOpencodeFamilySkill(content, skillName, frontmatterConverter) { - const converted = frontmatterConverter(content); - const { frontmatter, body } = extractFrontmatterAndBody(converted); - let description = `Run GSD workflow ${skillName}.`; - if (frontmatter) { - const maybeDescription = extractFrontmatterField(frontmatter, 'description'); - if (maybeDescription) { - description = maybeDescription; - } - } - description = toSingleLine(description); - // OpenCode skill descriptions must be 1–1024 characters. - if (description.length > 1024) { - description = `${description.slice(0, 1021)}...`; - } - // `name` must be lowercase alphanumeric with single-hyphen separators and - // match the containing directory name (the staged dir is `${skillName}/`). - const name = yamlIdentifier(skillName); - return `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n---\n\n${body.trimStart()}`; -} +// convertClaudeCommandToOpencodeFamilySkill, convertClaudeCommandToOpencodeSkill, +// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B). +// Imported from installEngine above. -/** - * Convert a Claude command (.md) to an OpenCode skill (SKILL.md). - * Thin wrapper over the shared OpenCode-family writer. - */ -function convertClaudeCommandToOpencodeSkill(content, skillName) { - return convertClaudeCommandToOpencodeFamilySkill( - content, - skillName, - (c) => convertClaudeToOpencodeFrontmatter(c), - ); -} - -/** - * Convert a Claude command (.md) to a Kilo skill (SKILL.md). - * Thin wrapper over the shared OpenCode-family writer (Kilo shares the schema). - */ -function convertClaudeCommandToKiloSkill(content, skillName) { - return convertClaudeCommandToOpencodeFamilySkill( - content, - skillName, - (c) => convertClaudeToKiloFrontmatter(c), - ); -} - -/** - * Convert Claude Code markdown command to Gemini TOML format - * @param {string} content - Markdown file content with YAML frontmatter - * @returns {string} - TOML content - */ -function convertClaudeToGeminiToml(content, { commandName = null } = {}) { - // #778 (c) — Gemini {{args}} interpolation. Claude's $ARGUMENTS placeholder - // maps to Gemini's {{args}} so inline argument references interpolate into the - // command body instead of being emitted as a dead literal. Applied before - // frontmatter parsing so every return path benefits (a command's frontmatter - // never contains $ARGUMENTS, so this is body-only in practice). Gemini injects - // {{args}} as typed outside shell blocks; we never place it inside a !{...} - // block, so there is no shell-escaping/injection interaction. - content = content.replace(/\$ARGUMENTS\b/g, '{{args}}'); - - // Check if content has frontmatter - if (!content.startsWith('---')) { - return `prompt = ${JSON.stringify(content)}\n`; - } - - const endIndex = content.indexOf('---', 3); - if (endIndex === -1) { - return `prompt = ${JSON.stringify(content)}\n`; - } - - const frontmatter = content.substring(3, endIndex).trim(); - let body = content.substring(endIndex + 3).trim(); - - // #778 (c) — Gemini !{...} dynamic-output injection for the situational - // `progress` command (GSD's status/dashboard surface). Inject the live - // .planning/STATE.md so the model sees current project state without relying - // on session memory. - // - // SECURITY: the shell command is a FIXED `cat` with NO interpolated user - // input — no {{args}} appears inside the block — so there is no - // shell-injection vector. Gemini still shows its standard per-invocation - // confirmation dialog (verified behavior). `2>/dev/null` keeps an - // uninitialized project (missing STATE.md) from injecting stderr noise. - // Braces inside the block are balanced (none present), per Gemini's parser - // requirement. The append happens AFTER the {{args}} mapping above so the - // injected block can never accidentally carry interpolated arguments. - if (commandName === 'progress') { - body += '\n\n## Live project state\n' - + 'Current contents of `.planning/STATE.md` ' - + '(empty if the project is not yet initialized):\n\n' - + '!{cat .planning/STATE.md 2>/dev/null}\n'; - } - - // Extract description from frontmatter - let description = ''; - const lines = frontmatter.split('\n'); - for (const line of lines) { - const trimmed = line.trim(); - if (trimmed.startsWith('description:')) { - description = trimmed.substring(12).trim(); - break; - } - } - - // Construct TOML - let toml = ''; - if (description) { - toml += `description = ${JSON.stringify(description)}\n`; - } - - toml += `prompt = ${JSON.stringify(body)}\n`; - - return toml; -} - -/** - * Copy commands to a flat structure for OpenCode - * OpenCode expects: command/gsd-help.md (invoked as /gsd-help) - * Source structure: commands/gsd/help.md - * - * @param {string} srcDir - Source directory (e.g., commands/gsd/) - * @param {string} destDir - Destination directory (e.g., command/) - * @param {string} prefix - Prefix for filenames (e.g., 'gsd') - * @param {string} pathPrefix - Path prefix for file references - * @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo') - */ -/** - * Apply OpenCode-family (`opencode`/`kilo`) `@file` path-prefix rewrites to a - * RAW Claude command/skill body, BEFORE the frontmatter converter runs. - * - * This is the single source of truth shared by copyFlattenedCommands (commands) - * and installOpencodeFamilySkills (skills) so the two surfaces produce identical - * path references. Applying pathPrefix pre-conversion (rather than rewriting an - * already-converted body) is what avoids the converter's hardcoded default - * config dir leaking into --local / --config-dir installs, and the - * prefix-overlap double-rewrite hazard for custom dirs like `kilo-alt`. (#784) - * - * @param {string} content - raw Claude command markdown - * @param {string} runtime - 'opencode' or 'kilo' - * @param {string} pathPrefix - trailing-slash install-target prefix - * @returns {string} - */ -function applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix) { - content = content.replace(/~\/\.claude\//g, pathPrefix); - content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); - content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`); - content = content.replace(/~\/\.opencode\//g, pathPrefix); - content = content.replace(/~\/\.kilo\//g, pathPrefix); - return content; -} - -function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) { - if (!fs.existsSync(srcDir)) { - return; - } - - // Remove old gsd-*.md files before copying new ones - if (fs.existsSync(destDir)) { - for (const file of fs.readdirSync(destDir)) { - if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) { - fs.unlinkSync(path.join(destDir, file)); - } - } - } else { - fs.mkdirSync(destDir, { recursive: true }); - } - - const entries = fs.readdirSync(srcDir, { withFileTypes: true }); - - for (const entry of entries) { - const srcPath = path.join(srcDir, entry.name); - - if (entry.isDirectory()) { - // Recurse into subdirectories, adding to prefix - // e.g., commands/gsd/debug/start.md -> command/gsd-debug-start.md - copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime); - } else if (entry.name.endsWith('.md')) { - // Flatten: help.md -> gsd-help.md - const baseName = entry.name.replace('.md', ''); - const destName = `${prefix}-${baseName}.md`; - const destPath = path.join(destDir, destName); - - let content = fs.readFileSync(srcPath, 'utf8'); - content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix); - content = processAttribution(content, getCommitAttribution(runtime)); - content = runtime === 'kilo' - ? convertClaudeToKiloFrontmatter(content) - : convertClaudeToOpencodeFrontmatter(content); - - fs.writeFileSync(destPath, content); - } - } -} +// applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B). +// Imported from installEngine above. +// +// copyFlattenedCommands (OpenCode/Kilo flattened command/ writer): moved to +// src/install-engine.cts as installOpencodeFamilyCommands (ADR-1239 / #2087). +// OpenCode/Kilo installs now route through installRuntimeArtifacts's +// combinedFamilyInstall path (installOpencodeFamilyArtifacts) instead of the +// bespoke inline block that used to call this function. function listCodexSkillNames(skillsDir, prefix = 'gsd-') { if (!fs.existsSync(skillsDir)) return []; @@ -6633,688 +6473,154 @@ function writeHermesCategoryDescription(categoryDir) { * @param {boolean} isGlobal - Whether this is a global install */ -/** - * Single source of truth for user-owned artifacts inside gsd-core/. - * - * These files are created/refreshed by user-facing workflows (e.g. - * /gsd-profile-user) and must be preserved across reinstalls. Critically, they - * MUST be excluded from gsd-file-manifest.json — otherwise saveLocalPatches() - * will compare a refreshed file against a stale manifest hash and emit a - * spurious "locally modified GSD file" warning (bug #2771). - * - * Invariant: a file is either distribution (manifest-tracked, diff'd against - * manifest) or user artifact (preserved across installs, never diff'd). Never - * both. Both preserveUserArtifacts call sites and writeManifest must agree on - * this list, which is why it lives here as a single constant. - * - * Paths are relative to the gsd-core/ directory. - */ -const USER_OWNED_ARTIFACTS = ['USER-PROFILE.md']; - -/** - * Save user-generated files from destDir to an in-memory map before a wipe. - * - * @param {string} destDir - Directory that is about to be wiped - * @param {string[]} fileNames - Relative file names (e.g. ['USER-PROFILE.md']) to preserve - * @returns {Map} Map of fileName → file content (only entries that existed) - */ -function preserveUserArtifacts(destDir, fileNames) { - const saved = new Map(); - for (const name of fileNames) { - const fullPath = path.join(destDir, name); - if (fs.existsSync(fullPath)) { - try { - saved.set(name, fs.readFileSync(fullPath, 'utf8')); - } catch { /* skip unreadable files */ } - } - } - return saved; -} - -/** - * Restore user-generated files saved by preserveUserArtifacts after a wipe. - * - * @param {string} destDir - Directory that was wiped and recreated - * @param {Map} saved - Map returned by preserveUserArtifacts - */ -function restoreUserArtifacts(destDir, saved) { - for (const [name, content] of saved) { - const fullPath = path.join(destDir, name); - try { - fs.mkdirSync(path.dirname(fullPath), { recursive: true }); - fs.writeFileSync(fullPath, content, 'utf8'); - } catch { /* skip unwritable paths */ } - } -} - -/** - * Migrate a legacy dev-preferences.md (saved from commands/gsd/) into the - * runtime-aware SKILL.md location used by the writer after #2973. - * - * For runtimes with a nested skills layout (e.g. Hermes: skills/gsd//), - * the target is /skills/gsd/dev-preferences/SKILL.md. - * For runtimes with a flat skills layout (prefix='gsd-'), the target is - * /skills/gsd-dev-preferences/SKILL.md. - * - * Skips silently if no legacy file was preserved, or if a SKILL.md already - * exists at the new location (don't clobber user-customized skill content - * — they may have edited the new file directly). Returns true on actual - * migration so callers can log a one-line confirmation. - * - * @param {string} targetDir - Resolved runtime config directory (e.g. ~/.claude) - * @param {Map} saved - Map returned by preserveUserArtifacts - * @param {string} [runtime] - canonical runtime ID (e.g. 'hermes', 'qwen', 'claude') - * @param {'global'|'local'} [scope] - install scope - * @returns {boolean} - true if a file was migrated, false otherwise - */ -function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = 'global') { - if (!saved || !saved.has('dev-preferences.md')) return false; - let skillDir; - if (runtime) { - const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope); - const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills'); - if (!skillsKindEntry) return false; // runtime has no skills layout at this scope (e.g. cline local) - const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences'; - skillDir = path.join(targetDir, skillsKindEntry.destSubpath, stemName); - } else { - // Legacy fallback for callers that have not yet been updated to pass runtime - skillDir = path.join(targetDir, 'skills', 'gsd-dev-preferences'); - } - const skillFile = path.join(skillDir, 'SKILL.md'); - if (fs.existsSync(skillFile)) return false; - try { - fs.mkdirSync(skillDir, { recursive: true }); - fs.writeFileSync(skillFile, saved.get('dev-preferences.md'), 'utf8'); - return true; - } catch { - return false; - } -} +// USER_OWNED_ARTIFACTS, preserveUserArtifacts, restoreUserArtifacts, +// migrateLegacyDevPreferencesToSkill, _copyStaged, _removeGsdEntries, +// _runLegacyInstallMigrations, _runLegacyUninstallCleanup, _snapshotDir, +// _restoreDir, _removeHermesBareStemDirs, installRuntimeArtifacts, +// installOpencodeFamilySkills, uninstallRuntimeArtifacts: +// ALL moved to src/install-engine.cts (ADR-1239 Phase B). +// Imported from installEngine above. // --------------------------------------------------------------------------- -// Phase 2 — Layout-driven install/uninstall orchestrators +// Phase 2 — Layout-driven install/uninstall orchestrators (moved to engine) +// _applyRuntimeRewrites / _stampNonClaudeRuntimeDefaults remain here for +// call sites in copyWithPathReplacement (not moved). // --------------------------------------------------------------------------- - -/** - * Apply per-runtime content rewrites in place across every SKILL.md inside a - * staged directory. Reproduces the rewrite scaffolding that the old - * copyCommandsAsSkills functions applied between read-content and - * converter-call. Applied AFTER stage (which already called the converter); - * rewrites target stable path patterns the converter doesn't touch. - * - * For Qwen/Hermes, branding rewrites (.claude/ → .qwen/ / .hermes/) run - * AFTER the slash-form path replacements but they only catch bare `.claude/` - * patterns (skill-body relative refs) that the slash forms didn't consume. - * This mirrors the exact ordering in the legacy copyCommandsAsClaudeSkills body. - * - * @param {string} stagedDir - * @param {string} runtime - * @param {string} pathPrefix e.g. "~/.codex/" — trailing-slash string - * @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install - */ -// applyRuntimeContentRewritesInPlace: walk loop is now owned by -// runtimeArtifactConversion.applyRuntimeContentRewritesInPlace (ADR-1508 / #1511 Phase 2). -// The const binding above (~line 629) delegates here. Call sites in installRuntimeArtifacts -// pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract. - -/** - * Apply per-runtime content rewrites to flat .md files in a staged commands dir. - * Used for runtimes that have a commandsKind in their layout and need content rewrites - * (e.g. augment — replaces ~/.claude/ paths and applies branding conversions). - * - * IMPORTANT: `stageSkillsForProfile()` returns the original source directory unchanged - * on a full/default profile (skills === '*'). This function MUST NOT mutate that source - * directory. It always copies to a temp dir first, rewrites there, and returns the new - * path so the caller installs from the temp copy, not the source. - * - * @param {string} stagedDir directory of staged flat .md command files (may be source dir) - * @param {string} runtime - * @param {string} pathPrefix - * @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install - * @returns {string} path to a temp dir with rewritten files (caller is responsible for cleanup) - */ -// applyRuntimeContentRewritesForCommandsInPlace: copy+rewrite loop is now owned by -// runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace (ADR-1508 / #1511 Phase 2). -// The const binding above (~line 630) delegates here. Call sites in installRuntimeArtifacts -// pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract. - -/** - * Apply the per-runtime rewrite table to a single content string. - * Extracted so it can be unit-tested independently of the filesystem walk. - * - * @param {string} content - * @param {string} runtime - * @param {string} pathPrefix trailing-slash string - * @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install - * @returns {string} - */ -// _applyRuntimeRewrites: single implementation lives in runtimeArtifactConversion -// (ADR-1508 / #1511 Phase 2). Bound here so install.js call sites and exports are -// reference-identical to the conversion module (consistent with the walkers above). -// All call sites are below this line → no TDZ hazard. const _applyRuntimeRewrites = runtimeArtifactConversion._applyRuntimeRewrites; const _stampNonClaudeRuntimeDefaults = runtimeArtifactConversion._stampNonClaudeRuntimeDefaults; /** - * Copy a staged directory's contents into destDir. - * Additive — does not prune (surface.cjs handles pruning). + * Data-driven dispatch table for copyWithPathReplacement (ADR-1239 Phase B). + * Keyed by runtime id. Each entry declares ONLY what that runtime does differently. + * The DEFAULT (no entry, or entry with no md/js key) = identity transform after + * the uniform steps — covers claude, augment, codebuddy, kimi, etc. * - * For skills kind: each child of stagedDir is a `${prefix}${stem}/` dir; copy - * the whole dir into destDir. - * For commands/agents kind: iterate .md files and write them into destDir. - * - commands: write as `${prefix}${stem}.md` unless destSubpath already - * encodes the GSD namespace as its last segment (e.g. `commands/gsd`), in - * which case write as `${stem}.md` (directory IS the namespace). - * - agents: write as-is (files already carry their own `gsd-` prefix). - * For kimi-agents kind: recursively copy generated YAML/prompt files. + * Entry shape: + * mdSkipGenericRewrite?: boolean — skip the ~/.claude/ rewrite block (copilot, antigravity) + * md?: (content, ctx) => string — per-runtime .md transform + * mdReattributeAfter?: boolean — re-run processAttribution after md() (copilot, antigravity) + * mdTomlRenameOnCommand?: boolean — when isCommand, rename dest .md → .toml + * (unused since the gemini runtime was removed, #1928; + * kept as generic dispatch infra for a future TOML-command runtime) + * js?: (content, ctx) => string — per-runtime .cjs/.js transform (absent = plain copyFileSync) + * + * ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName, runtime } */ -function _copyStaged(stagedDir, destDir, kind) { - if (!fs.existsSync(stagedDir)) return; - fs.mkdirSync(destDir, { recursive: true }); - - if (kind.kind === 'skills') { - // Each child of stagedDir is a prefixed skill directory: gsd-help/, etc. - for (const entry of fs.readdirSync(stagedDir, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const src = path.join(stagedDir, entry.name); - const dest = path.join(destDir, entry.name); - fs.cpSync(src, dest, { recursive: true }); - } - return; - } - - if (kind.kind === 'kimi-agents') { - fs.cpSync(stagedDir, destDir, { recursive: true }); - return; - } - - // commands or agents - const entries = fs.readdirSync(stagedDir, { withFileTypes: true }); - // For commands: apply prefix unless the destSubpath's last segment already - // represents the GSD namespace (e.g. 'commands/gsd' → last segment 'gsd'). - const destLast = path.basename(kind.destSubpath); - const prefixStem = kind.prefix ? kind.prefix.replace(/-$/, '') : ''; - const namespacedByDir = kind.kind === 'commands' && destLast === prefixStem; - - for (const entry of entries) { - if (!entry.isFile()) continue; - if (!entry.name.endsWith('.md')) continue; - const stem = entry.name.slice(0, -3); // strip .md - - let destName; - if (kind.kind === 'agents') { - // Agent files already carry the gsd- prefix in the source dir - destName = entry.name; - } else if (namespacedByDir) { - // Directory is the namespace; don't double-prefix the filename - destName = entry.name; - } else { - // Flat commands directory (e.g. command/ for opencode/kilo) - destName = `${kind.prefix}${stem}.md`; - } - - fs.copyFileSync(path.join(stagedDir, entry.name), path.join(destDir, destName)); - } -} - -/** - * Remove GSD-prefixed entries from destDir matching kind.prefix. - * For the prefix='' case: the destSubpath IS the namespace — remove the entire - * destDir. (No current runtime uses prefix='' after #947 reversed Hermes; kept - * as a defensive guard for future runtimes.) - */ -function _removeGsdEntries(destDir, kind) { - if (!fs.existsSync(destDir)) return; - if (kind.kind === 'kimi-agents') { - for (const fileName of ['gsd.yaml', 'gsd.md']) { - fs.rmSync(path.join(destDir, fileName), { force: true }); - } - const subagentsDir = path.join(destDir, 'subagents'); - if (fs.existsSync(subagentsDir)) { - for (const entry of fs.readdirSync(subagentsDir, { withFileTypes: true })) { - if (!entry.isFile()) continue; - if (!entry.name.startsWith('gsd-')) continue; - if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.md')) continue; - fs.rmSync(path.join(subagentsDir, entry.name), { force: true }); +const RUNTIME_CONTENT_DISPATCH = { + opencode: { + md: (content) => convertClaudeToOpencodeFrontmatter(content), + }, + kilo: { + md: (content) => convertClaudeToKiloFrontmatter(content), + }, + codex: { + md: (content) => convertClaudeToCodexMarkdown(content), + }, + copilot: { + mdSkipGenericRewrite: true, + md: (content, ctx) => convertClaudeToCopilotContent(content, ctx.isGlobal), + mdReattributeAfter: true, + js: (content, ctx) => convertClaudeToCopilotContent(content, ctx.isGlobal), + }, + antigravity: { + mdSkipGenericRewrite: true, + md: (content, ctx) => convertClaudeToAntigravityContent(content, ctx.isGlobal), + mdReattributeAfter: true, + js: (content, ctx) => convertClaudeToAntigravityContent(content, ctx.isGlobal), + }, + cursor: { + md: (content) => convertClaudeToCursorMarkdown(content), + js: (content) => { + content = content.replace(/gsd:/gi, 'gsd-'); + content = content.replace(/\.claude\/skills\//g, '.cursor/skills/'); + content = content.replace(/CLAUDE\.md/g, '.cursor/rules/'); + content = content.replace(/\bClaude Code\b/g, 'Cursor'); + return content; + }, + }, + windsurf: { + md: (content) => convertClaudeToWindsurfMarkdown(content), + js: (content) => { + // Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085). + content = content.replace(/gsd:/gi, 'gsd-'); + content = content.replace(/\.claude\/skills\//g, '.devin/skills/'); + content = content.replace(/CLAUDE\.md/g, '.devin/rules'); + content = content.replace(/\bClaude Code\b/g, 'Windsurf'); + return content; + }, + }, + trae: { + md: (content) => convertClaudeToTraeMarkdown(content), + js: (content) => { + content = content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => { + return `/gsd-${commandName}`; + }); + content = content.replace(/\.claude\/skills\//g, '.trae/skills/'); + content = content.replace(/CLAUDE\.md/g, '.trae/rules/'); + content = content.replace(/\bClaude Code\b/g, 'Trae'); + return content; + }, + }, + cline: { + md: (content) => convertClaudeToCliineMarkdown(content), + js: (content) => { + content = content.replace(/\.claude\/skills\//g, '.cline/skills/'); + content = content.replace(/CLAUDE\.md/g, '.clinerules'); + content = content.replace(/\bClaude Code\b/g, 'Cline'); + return content; + }, + }, + // qwen/hermes: brand VALUES are descriptor-driven (ADR-1239 / #2092) via + // _hostBehaviors(ctx.runtime).brandingRewrites — EXACT regexes/ordering + // preserved from the prior hardcoded-literal versions (including the + // qwen-specific `.claude/skills/` -> `.qwen/skills/` pre-rewrite, whose + // target is derived as `${b['.claude/']}skills/`). + qwen: { + md: (content, ctx) => { + // Guarded (post-review #2092): degrade closed to a no-op if the + // registry fails to load, instead of throwing on `b['CLAUDE.md']`. + const b = _hostBehaviors(ctx.runtime).brandingRewrites; + if (b) { + content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']); + content = content.replace(/\bClaude Code\b/g, b['Claude Code']); + content = content.replace(/\.claude\//g, b['.claude/']); } - } - return; - } - if (kind.prefix === '') { - // Whole-namespace removal (Hermes nested case — destSubpath is skills/gsd) - // The directory itself is the GSD namespace, so remove it entirely. - fs.rmSync(destDir, { recursive: true, force: true }); - return; - } - for (const entry of fs.readdirSync(destDir, { withFileTypes: true })) { - if (!entry.name.startsWith(kind.prefix)) continue; - fs.rmSync(path.join(destDir, entry.name), { recursive: true, force: true }); - } -} - -/** - * Run legacy install migrations that must execute BEFORE the layout-driven - * copy so stale artifacts are cleaned up before new ones are written. - * - * - Claude/Qwen/Hermes: migrate legacy commands/gsd/dev-preferences.md → - * skills/gsd-dev-preferences/SKILL.md if the old file is present. - * Also removes the legacy commands/gsd/ directory. - * - Hermes: remove flat skills/gsd-STAR directories (pre-2841 layout) before - * writing the new nested skills/gsd/ layout. - * - * @param {string} runtime - * @param {string} configDir resolved runtime config directory - * @param {'global'|'local'} [scope] - */ -function _runLegacyInstallMigrations(runtime, configDir, scope = 'global') { - const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd'); - - // Claude / Qwen / Hermes: clean up legacy commands/gsd/ and preserve dev-preferences - // for migration. The actual migration call is deferred to after all layout cleanup so - // that for Hermes the flat skills/gsd-*/ removal (below) does not delete the freshly - // created skills/gsd-dev-preferences/ skill dir. - let savedLegacyArtifacts = null; - if (runtime === 'claude' || runtime === 'qwen' || runtime === 'hermes') { - if (fs.existsSync(legacyCommandsGsd)) { - savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsGsd, ['dev-preferences.md']); - fs.rmSync(legacyCommandsGsd, { recursive: true }); - } - } - - // Hermes: remove pre-#2841 flat skills/gsd-*/ entries that lived alongside - // the new skills/gsd/ nested layout. - if (runtime === 'hermes') { - const flatSkillsDir = path.join(configDir, 'skills'); - if (fs.existsSync(flatSkillsDir)) { - for (const entry of fs.readdirSync(flatSkillsDir, { withFileTypes: true })) { - if (entry.isDirectory() && entry.name.startsWith('gsd-')) { - fs.rmSync(path.join(flatSkillsDir, entry.name), { recursive: true }); - } + return content; + }, + js: (content, ctx) => { + const b = _hostBehaviors(ctx.runtime).brandingRewrites; + if (b) { + content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`); + content = content.replace(/\.claude\//g, b['.claude/']); + content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']); + content = content.replace(/\bClaude Code\b/g, b['Claude Code']); } - } - - // Hermes: bare-stem skills/gsd// cleanup is deferred to AFTER the - // layout-driven install loop in installRuntimeArtifacts, where the exact set - // of staged gsd-/ dirs is known. Removing here (before staging) would - // require readGsdCommandNames() which misses skills like 'dev-preferences' - // that are not in the commands directory. See _removeHermesBareStemDirs(). - } - - // Migrate dev-preferences.md content → runtime-aware SKILL.md location (#2973). - // Done after all layout cleanup so Hermes flat-dir removal does not delete the - // newly created skill dir. No-op if skill file already exists. - if (savedLegacyArtifacts) { - migrateLegacyDevPreferencesToSkill(configDir, savedLegacyArtifacts, runtime, scope); - } -} - -/** - * Run legacy uninstall cleanup that must execute BEFORE the layout-driven - * removal so old-format entries are also cleaned up. - * - * - Claude global/Qwen: remove legacy commands/gsd/ directory if present. - * For Claude LOCAL, commands/gsd/ is the current primary location (not - * legacy), so we skip removal here and let _removeGsdEntries handle it - * with gsd- prefix filtering (preserving user files like dev-preferences.md). - * - Hermes: remove pre-2841 flat skills/gsd-STAR entries. - * - * @param {string} runtime - * @param {string} configDir resolved runtime config directory - * @param {'global'|'local'} [scope] - */ -function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') { - // commands/gsd/ is a legacy location for Qwen, Hermes, and all Claude installs. - // Prior to #1367 fix, Claude-local used commands/gsd/.md (colon-namespaced). - // After #1367, Claude-local uses flat commands/gsd-.md. The inline uninstall - // block (1c) handles removal of flat files; this function handles the legacy - // commands/gsd/ directory for all Claude scopes (global was already included, - // local is now added since that layout is also legacy post-#1367). - // #2973 / Codex review (bd1f06c9): preserve user-owned dev-preferences.md - // before destructive wipe. Migration to skills/gsd-dev-preferences/SKILL.md - // is deferred and returned so the caller can apply it AFTER layout-driven - // removal — this prevents the layout's gsd-* prefix removal from wiping the - // freshly created skill dir (same pattern as _runLegacyInstallMigrations). - let savedLegacyArtifacts = null; - // commands/gsd/ is a legacy location for Qwen, Hermes, and Claude global. - // Claude local is intentionally excluded: the inline uninstall block (1c) handles - // commands/gsd/ for claude local, preserving dev-preferences.md by restoring it - // to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here - // (which would redirect to skills/) conflicts with the test contract for local installs. - const isLegacyCommandsGsd = runtime === 'qwen' || runtime === 'hermes' || (runtime === 'claude' && scope === 'global'); - if (isLegacyCommandsGsd) { - const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd'); - if (fs.existsSync(legacyCommandsGsd)) { - savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsGsd, ['dev-preferences.md']); - fs.rmSync(legacyCommandsGsd, { recursive: true }); - } - } - - // Hermes: pre-#2841 flat skills/gsd-*/ entries - if (runtime === 'hermes') { - const flatSkillsDir = path.join(configDir, 'skills'); - if (fs.existsSync(flatSkillsDir)) { - for (const entry of fs.readdirSync(flatSkillsDir, { withFileTypes: true })) { - if (entry.isDirectory() && entry.name.startsWith('gsd-')) { - fs.rmSync(path.join(flatSkillsDir, entry.name), { recursive: true }); - } + return content; + }, + }, + hermes: { + md: (content, ctx) => { + // Guarded (post-review #2092): see qwen entry above. + const b = _hostBehaviors(ctx.runtime).brandingRewrites; + if (b) { + content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']); + content = content.replace(/\bClaude Code\b/g, b['Claude Code']); + content = content.replace(/\.claude\//g, b['.claude/']); } - } - - // Hermes: pre-#947 bare-stem skills/gsd// entries (dirs that do NOT - // start with 'gsd-') — the #3664 layout used prefix='' so GSD-owned skills - // had bare names (e.g. skills/gsd/help/). These are stale on uninstall. - const nestedGsdDirForUninstall = path.join(configDir, 'skills', 'gsd'); - if (fs.existsSync(nestedGsdDirForUninstall)) { - for (const entry of fs.readdirSync(nestedGsdDirForUninstall, { withFileTypes: true })) { - if (entry.isDirectory() && !entry.name.startsWith('gsd-')) { - fs.rmSync(path.join(nestedGsdDirForUninstall, entry.name), { recursive: true }); - } + return content; + }, + js: (content, ctx) => { + const b = _hostBehaviors(ctx.runtime).brandingRewrites; + if (b) { + content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`); + content = content.replace(/\.claude\//g, b['.claude/']); + content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']); + content = content.replace(/\bClaude Code\b/g, b['Claude Code']); } - } - } - - // Return saved artifacts so the caller can migrate after layout-driven removal. - return savedLegacyArtifacts; -} - -/** - * Layout-driven install orchestrator. - * Runs legacy migrations first, then uses resolveRuntimeArtifactLayout to - * determine what artifact kinds to write and where. - * - * @param {string} runtime canonical runtime ID - * @param {string} configDir resolved runtime config directory - * @param {'global'|'local'} scope - * @param {Object} resolvedProfile from resolveProfile() / resolveEffectiveProfile() - */ -/** - * Deep-snapshot a directory tree into a Map. - * Returns an empty Map if the directory doesn't exist. - * @param {string} dir - * @returns {Map} - */ -function _snapshotDir(dir) { - const files = new Map(); - if (!fs.existsSync(dir)) return files; - const walk = (relPath, absPath) => { - for (const e of fs.readdirSync(absPath, { withFileTypes: true })) { - const childRel = relPath ? path.join(relPath, e.name) : e.name; - const childAbs = path.join(absPath, e.name); - if (e.isDirectory()) walk(childRel, childAbs); - else if (e.isFile()) files.set(childRel, fs.readFileSync(childAbs)); - } - }; - walk('', dir); - return files; -} - -/** - * Restore a directory tree from a Map produced by _snapshotDir. - * @param {string} dir - * @param {Map} snapshot - */ -function _restoreDir(dir, snapshot) { - for (const [relPath, buf] of snapshot) { - const absPath = path.join(dir, relPath); - fs.mkdirSync(path.dirname(absPath), { recursive: true }); - fs.writeFileSync(absPath, buf); - } -} - -/** - * After the layout-driven install loop writes new gsd-/ dirs to - * skills/gsd/, remove any pre-existing bare-stem dirs (skills/gsd//) - * that correspond to the newly installed gsd- entries. - * - * The removal set is derived from the ACTUAL installed skill dirs (every - * entry starting with 'gsd-' that is a directory), so it covers ALL shipped - * GSD skills — including 'dev-preferences' and future additions — without - * relying on readGsdCommandNames() which only enumerates the commands source - * tree and can miss skills that ship outside that directory. - * - * Safety: a bare dir is ONLY removed when a corresponding gsd-/ dir was - * installed this run. A user-owned dir 'skills/gsd/my-workflow/' that has no - * matching 'skills/gsd/gsd-my-workflow/' is never touched. - * - * @param {string} nestedGsdDir absolute path to skills/gsd/ category dir - */ -function _removeHermesBareStemDirs(nestedGsdDir) { - if (!fs.existsSync(nestedGsdDir)) return; - const entries = fs.readdirSync(nestedGsdDir, { withFileTypes: true }); - - // Collect the set of stems that were installed as gsd-/ this run. - const installedStems = new Set(); - for (const entry of entries) { - if (entry.isDirectory() && entry.name.startsWith('gsd-')) { - installedStems.add(entry.name.slice('gsd-'.length)); // e.g. 'quick', 'dev-preferences' - } - } - - // Remove any bare / dir for which gsd-/ was just installed. - for (const entry of entries) { - if (entry.isDirectory() && !entry.name.startsWith('gsd-') && installedStems.has(entry.name)) { - fs.rmSync(path.join(nestedGsdDir, entry.name), { recursive: true }); - } - } -} - -function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) { - // Legacy cleanup before layout-driven writes - _runLegacyInstallMigrations(runtime, configDir, scope); - - const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope); - const planResult = createRuntimeArtifactInstallPlan({ - layout, - resolvedProfile, - homedir: () => os.homedir(), - platform: process.platform, - resolveAttribution: getCommitAttribution, - }); - - const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs; - try { - if (!planResult.ok) { - throw new Error(planResult.message); - } - - const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind])); - for (const item of planResult.plan.items) { - const kind = kindsByName.get(item.kind); - if (!kind) throw new Error(`Install plan returned unknown artifact kind: ${item.kind}`); - const dest = item.destDir; - fs.mkdirSync(dest, { recursive: true }); - if (kind.kind === 'skills' && fs.existsSync(dest)) { - // Pre-prune: snapshot user-owned content before _removeGsdEntries wipes it, - // then restore after. This preserves user dirs across a wipe-and-replace - // install (#2973 / #3664). - // - // All runtimes (incl. Hermes after #947) use prefix='gsd-'. - // _removeGsdEntries removes only gsd-* entries; non-gsd-* user dirs are - // untouched. Preserve the explicit user-owned GSD-prefixed skill - // gsd-dev-preferences, which GSD does not reinstall from source but must - // survive the prune (#2973). - const toPreserve = new Map(); // dirName -> Map - - { - // Preserve explicitly user-owned GSD-prefixed skill dirs. - // gsd-dev-preferences is the sole user-customisable skill in this category. - const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences']; - for (const dirName of USER_OWNED_SKILL_DIRS) { - const skillDir = path.join(dest, dirName); - if (!fs.existsSync(skillDir)) continue; - const snap = _snapshotDir(skillDir); - if (snap.size > 0) toPreserve.set(dirName, snap); - } - } - - _removeGsdEntries(dest, kind); - _copyStaged(item.sourceDir, dest, kind); - - // Restore user-owned dirs after the prune+copy - for (const [dirName, snap] of toPreserve) { - _restoreDir(path.join(dest, dirName), snap); - } - } else { - // For non-skills kinds (commands, agents): no user content to preserve; - // just prune stale gsd-* entries and copy new ones. - _removeGsdEntries(dest, kind); - _copyStaged(item.sourceDir, dest, kind); - } - } - } finally { - for (const dir of cleanupDirs) { - try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ } - } - } - - // Hermes: after the install loop has written all gsd-/ dirs to - // skills/gsd/, remove any stale bare-stem dirs (skills/gsd//) that - // correspond to the newly installed gsd- entries. This is the robust - // replacement for the readGsdCommandNames()-based pre-install cleanup that - // missed skills like 'dev-preferences' (#947 adversarial review). - // - // We run this AFTER the install loop so the installed set is authoritative: - // every gsd-/ present now was written this run (or was there before - // with the same prefix). User-owned bare dirs with no gsd- counterpart - // are untouched. - if (runtime === 'hermes') { - const nestedGsdDirForCleanup = path.join(configDir, 'skills', 'gsd'); - _removeHermesBareStemDirs(nestedGsdDirForCleanup); - } -} - -/** - * Install the skills layout kind for an OpenCode-family runtime (OpenCode/Kilo). - * - * These runtimes do NOT go through installRuntimeArtifacts (their commands use a - * bespoke flattened-command writer), so this writes ONLY the skills kind - * alongside their existing command/ + agents/ surfaces. Uninstall is already - * layout-driven (uninstallRuntimeArtifacts iterates layout.kinds), so the - * skills/ dir is cleaned up automatically once the layout declares it. - * - * `rawCommandsDir` MUST be the SAME staged command directory the flattened - * command writer consumes (the caller passes its `_stageSkills()` output) so the - * command/ and skills/ surfaces always cover the identical, profile-resolved set - * — including the `--minimal`/`--core-only` alias path, which stages differently - * from a plain `--profile=core`. - * - * Mirrors copyFlattenedCommands exactly per file — pathPrefix rewrite → - * attribution → command→skill conversion — guaranteeing command/ and skills/ - * bodies match byte-for-byte for global, --local, and --config-dir installs. - * We deliberately do NOT use skillsKindEntry.stage(): that converts before any - * pathPrefix is known, so its bodies would carry the converter's hardcoded - * default config dir. (#784) - * - * @param {string} runtime - 'opencode' or 'kilo' - * @param {string} targetDir - resolved runtime config directory - * @param {string} rawCommandsDir - staged RAW Claude command dir (caller's _stageSkills output) - * @param {string} pathPrefix - computed config-path prefix for body rewrites - * @returns {number} number of gsd-* skill directories written - */ -function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPrefix) { - const layout = resolveRuntimeArtifactLayout(runtime, targetDir); - const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills'); - if (!skillsKindEntry) return 0; - const rawDir = rawCommandsDir; - if (!rawDir || !fs.existsSync(rawDir)) return 0; - - const converter = runtime === 'kilo' - ? convertClaudeCommandToKiloSkill - : convertClaudeCommandToOpencodeSkill; - - const dest = path.join(targetDir, skillsKindEntry.destSubpath); - fs.mkdirSync(dest, { recursive: true }); - - // Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune. - // gsd-dev-preferences is generated by the user (via generate-dev-preferences) - // and lives at /skills/gsd-dev-preferences — _removeGsdEntries - // would otherwise wipe it. Mirrors the preservation in installRuntimeArtifacts - // (#2973). - const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences']; - const toPreserve = new Map(); // dirName -> Map - for (const dirName of USER_OWNED_SKILL_DIRS) { - const skillDir = path.join(dest, dirName); - if (!fs.existsSync(skillDir)) continue; - const snap = _snapshotDir(skillDir); - if (snap.size > 0) toPreserve.set(dirName, snap); - } - - _removeGsdEntries(dest, skillsKindEntry); - - let count = 0; - for (const entry of fs.readdirSync(rawDir, { withFileTypes: true })) { - if (!entry.isFile() || !entry.name.endsWith('.md')) continue; - const stem = entry.name.slice(0, -3); - const skillName = `${skillsKindEntry.prefix}${stem}`; - let content = fs.readFileSync(path.join(rawDir, entry.name), 'utf8'); - content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix); - content = processAttribution(content, getCommitAttribution(runtime)); - content = converter(content, skillName); - const skillDir = path.join(dest, skillName); - fs.mkdirSync(skillDir, { recursive: true }); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content); - count++; - } - - // Restore user-owned dirs after the prune+copy. - for (const [dirName, snap] of toPreserve) { - _restoreDir(path.join(dest, dirName), snap); - } - - return count; -} - -/** - * Layout-driven uninstall orchestrator. - * Runs legacy cleanup first, then uses resolveRuntimeArtifactLayout to - * determine which GSD-owned entries to remove. - * - * @param {string} runtime canonical runtime ID - * @param {string} configDir resolved runtime config directory - * @param {'global'|'local'} scope - */ -function uninstallRuntimeArtifacts(runtime, configDir, scope) { - // Legacy cleanup before layout-driven removal (scope-aware to avoid - // removing Claude local commands/gsd/ which is the primary install dir). - // Returns saved user artifacts so we can migrate AFTER layout removal - // (the layout's gsd-* prefix pass would wipe a skill dir created here). - const savedLegacyArtifacts = _runLegacyUninstallCleanup(runtime, configDir, scope); - - const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope); - const plan = createRuntimeArtifactUninstallPlan(layout); - const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind])); - for (const item of plan.items) { - const kind = kindsByName.get(item.kind); - if (!kind) { - throw new Error(`Runtime artifact uninstall plan referenced unknown kind: ${item.kind}`); - } - _removeGsdEntries(item.destDir, kind); - } - - // Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove - // the GSD-managed DESCRIPTION.md and then the category dir itself if it - // contains no user content (#947). _removeGsdEntries removed gsd-* dirs - // but left the category container and DESCRIPTION.md intact. - if (runtime === 'hermes') { - const nestedGsdDir = path.join(configDir, 'skills', 'gsd'); - if (fs.existsSync(nestedGsdDir)) { - // Remove GSD-owned DESCRIPTION.md (written by writeHermesCategoryDescription) - fs.rmSync(path.join(nestedGsdDir, 'DESCRIPTION.md'), { force: true }); - // Remove the category dir if empty (no user content remaining) - const remaining = fs.readdirSync(nestedGsdDir, { withFileTypes: true }); - if (remaining.length === 0) { - fs.rmSync(nestedGsdDir, { recursive: true, force: true }); - } - } - } - - // #2973 / Codex review (bd1f06c9): migrate dev-preferences.md to the - // runtime-aware SKILL.md location after all layout-driven removal is - // complete. Do NOT restore to commands/gsd/ — the user is uninstalling. - if (savedLegacyArtifacts) { - migrateLegacyDevPreferencesToSkill(configDir, savedLegacyArtifacts, runtime, scope); - } -} + return content; + }, + }, +}; /** * Recursively copy directory, replacing paths in .md files @@ -7322,26 +6628,32 @@ function uninstallRuntimeArtifacts(runtime, configDir, scope) { * @param {string} srcDir - Source directory * @param {string} destDir - Destination directory * @param {string} pathPrefix - Path prefix for file references - * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex') + * @param {string} runtime - Target runtime ('claude', 'opencode', 'codex') * @param {boolean} isCommand - Whether the source is a command directory * @param {boolean} isGlobal - Whether the install is global */ -function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false, isGlobal = false) { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isGemini = runtime === 'gemini'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isAugment = runtime === 'augment'; - const isTrae = runtime === 'trae'; - const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; - const isCline = runtime === 'cline'; +function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false, isGlobal = false, confinementRoot) { const dirName = getDirName(runtime); + // ADR-1239 Phase B write-confinement: refuse to wipe/write a destDir that + // escapes the caller-declared install root. Runs BEFORE the rmSync below so a + // crafted destDir can never delete or write outside confinementRoot. + if (confinementRoot === undefined) { + throw new Error( + 'copyWithPathReplacement: confinementRoot is required to confine writes to the install root — refusing to write', + ); + } + const resolvedConfinementRoot = path.resolve(confinementRoot); + const resolvedDestDir = assertDestWithinConfigHome(confinementRoot, destDir); + if (hasExistingSymlinkBetween(resolvedConfinementRoot, resolvedDestDir)) { + throw new Error( + `copyWithPathReplacement: destDir "${destDir}" contains a symlink escaping the install root "${confinementRoot}" — refusing to write`, + ); + } + // Use the validated absolute path for all writes below so the gate validates + // exactly what is written (a relative destDir would otherwise resolve to cwd). + destDir = resolvedDestDir; + // Clean install: remove existing destination to prevent orphaned files if (fs.existsSync(destDir)) { fs.rmSync(destDir, { recursive: true }); @@ -7355,12 +6667,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand const destPath = path.join(destDir, entry.name); if (entry.isDirectory()) { - copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal); + copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal, confinementRoot); } else if (entry.name.endsWith('.md')) { + const dispatch = RUNTIME_CONTENT_DISPATCH[runtime] || {}; + const ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName: entry.name, runtime }; + // Replace ~/.claude/ and $HOME/.claude/ and ./.claude/ with runtime-appropriate paths - // Skip generic replacement for Copilot — convertClaudeToCopilotContent handles all paths + // Skip generic replacement for Copilot/Antigravity — their converters handle all paths let content = fs.readFileSync(srcPath, 'utf8'); - if (!isCopilot && !isAntigravity) { + if (!dispatch.mdSkipGenericRewrite) { const globalClaudeRegex = /~\/\.claude\//g; const globalClaudeHomeRegex = /\$HOME\/\.claude\//g; const localClaudeRegex = /\.\/\.claude\//g; @@ -7384,7 +6699,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand // copyWithPathReplacement is the emit path for gsd-core/workflows/*.md; // _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix // live in real installs (it is a no-op for files without those lines). - if (runtime !== 'claude') { + if (!_hostBehaviors(runtime).authorsCanonicalWorkflow) { content = _stampNonClaudeRuntimeDefaults(content, runtime); } @@ -7392,115 +6707,28 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand // copyWithPathReplacement for runtimes that register commands under the // hyphen form; normalizeAgentBodyForRuntime self-gates on // shouldNormalizeHyphenNamespaceInAgentBody(runtime) and is a no-op for - // colon-canonical runtimes (Gemini). + // colon-canonical / self-converting runtimes. content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames()); - // Convert frontmatter for opencode compatibility - if (isOpencode || isKilo) { - content = isKilo - ? convertClaudeToKiloFrontmatter(content) - : convertClaudeToOpencodeFrontmatter(content); - fs.writeFileSync(destPath, content); - } else if (isGemini) { - // Apply Gemini-specific Markdown transformations (slash commands, TOML). - // #778: thread the command name (file stem) so per-command TOML - // enrichment (live-state injection) can target a specific command. - const geminiCommandName = isCommand ? entry.name.replace(/\.md$/, '') : null; - const processed = convertClaudeToGeminiMarkdown(content, { isCommand, commandName: geminiCommandName }); - const finalPath = isCommand ? destPath.replace(/\.md$/, '.toml') : destPath; - fs.writeFileSync(finalPath, processed); - } else if (isCodex) { - content = convertClaudeToCodexMarkdown(content); - fs.writeFileSync(destPath, content); - } else if (isCopilot) { - content = convertClaudeToCopilotContent(content, isGlobal); - content = processAttribution(content, getCommitAttribution(runtime)); - fs.writeFileSync(destPath, content); - } else if (isAntigravity) { - content = convertClaudeToAntigravityContent(content, isGlobal); - content = processAttribution(content, getCommitAttribution(runtime)); - fs.writeFileSync(destPath, content); - } else if (isCursor) { - content = convertClaudeToCursorMarkdown(content); - fs.writeFileSync(destPath, content); - } else if (isWindsurf) { - content = convertClaudeToWindsurfMarkdown(content); - fs.writeFileSync(destPath, content); - } else if (isTrae) { - content = convertClaudeToTraeMarkdown(content); - fs.writeFileSync(destPath, content); - } else if (isCline) { - content = convertClaudeToCliineMarkdown(content); - fs.writeFileSync(destPath, content); - } else if (isQwen) { - content = content.replace(/CLAUDE\.md/g, 'QWEN.md'); - content = content.replace(/\bClaude Code\b/g, 'Qwen Code'); - content = content.replace(/\.claude\//g, '.qwen/'); - fs.writeFileSync(destPath, content); - } else if (isHermes) { - content = content.replace(/CLAUDE\.md/g, 'HERMES.md'); - content = content.replace(/\bClaude Code\b/g, 'Hermes Agent'); - content = content.replace(/\.claude\//g, '.hermes/'); + // Apply per-runtime .md converter (if any) + if (dispatch.md) content = dispatch.md(content, ctx); + + // Re-run attribution after converter for runtimes that need it (copilot, antigravity) + if (dispatch.mdReattributeAfter) content = processAttribution(content, getCommitAttribution(runtime)); + + // Rename .md → .toml for command files (unused since gemini removal, #1928) + const finalPath = (dispatch.mdTomlRenameOnCommand && isCommand) ? destPath.replace(/\.md$/, '.toml') : destPath; + fs.writeFileSync(finalPath, content); + } else if (entry.name.endsWith('.cjs') || entry.name.endsWith('.js')) { + const dispatch = RUNTIME_CONTENT_DISPATCH[runtime] || {}; + if (dispatch.js) { + const ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName: entry.name, runtime }; + let content = fs.readFileSync(srcPath, 'utf8'); + content = dispatch.js(content, ctx); fs.writeFileSync(destPath, content); } else { - fs.writeFileSync(destPath, content); + fs.copyFileSync(srcPath, destPath); } - } else if (isCopilot && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - // Copilot: also transform .cjs/.js files for CONV-06 and CONV-07 - let content = fs.readFileSync(srcPath, 'utf8'); - content = convertClaudeToCopilotContent(content, isGlobal); - fs.writeFileSync(destPath, content); - } else if (isAntigravity && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - // Antigravity: also transform .cjs/.js files for path/command conversions - let content = fs.readFileSync(srcPath, 'utf8'); - content = convertClaudeToAntigravityContent(content, isGlobal); - fs.writeFileSync(destPath, content); - } else if (isCursor && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - // For Cursor, also convert Claude references in JS/CJS utility scripts - let jsContent = fs.readFileSync(srcPath, 'utf8'); - jsContent = jsContent.replace(/gsd:/gi, 'gsd-'); - jsContent = jsContent.replace(/\.claude\/skills\//g, '.cursor/skills/'); - jsContent = jsContent.replace(/CLAUDE\.md/g, '.cursor/rules/'); - jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cursor'); - fs.writeFileSync(destPath, jsContent); - } else if (isWindsurf && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - // For Windsurf/Devin, also convert Claude references in JS/CJS utility scripts. - // Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085). - let jsContent = fs.readFileSync(srcPath, 'utf8'); - jsContent = jsContent.replace(/gsd:/gi, 'gsd-'); - jsContent = jsContent.replace(/\.claude\/skills\//g, '.devin/skills/'); - jsContent = jsContent.replace(/CLAUDE\.md/g, '.devin/rules'); - jsContent = jsContent.replace(/\bClaude Code\b/g, 'Windsurf'); - fs.writeFileSync(destPath, jsContent); - } else if (isTrae && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - let jsContent = fs.readFileSync(srcPath, 'utf8'); - jsContent = jsContent.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => { - return `/gsd-${commandName}`; - }); - jsContent = jsContent.replace(/\.claude\/skills\//g, '.trae/skills/'); - jsContent = jsContent.replace(/CLAUDE\.md/g, '.trae/rules/'); - jsContent = jsContent.replace(/\bClaude Code\b/g, 'Trae'); - fs.writeFileSync(destPath, jsContent); - } else if (isCline && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - let jsContent = fs.readFileSync(srcPath, 'utf8'); - jsContent = jsContent.replace(/\.claude\/skills\//g, '.cline/skills/'); - jsContent = jsContent.replace(/CLAUDE\.md/g, '.clinerules'); - jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cline'); - fs.writeFileSync(destPath, jsContent); - } else if (isQwen && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - let jsContent = fs.readFileSync(srcPath, 'utf8'); - jsContent = jsContent.replace(/\.claude\/skills\//g, '.qwen/skills/'); - jsContent = jsContent.replace(/\.claude\//g, '.qwen/'); - jsContent = jsContent.replace(/CLAUDE\.md/g, 'QWEN.md'); - jsContent = jsContent.replace(/\bClaude Code\b/g, 'Qwen Code'); - fs.writeFileSync(destPath, jsContent); - } else if (isHermes && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - let jsContent = fs.readFileSync(srcPath, 'utf8'); - jsContent = jsContent.replace(/\.claude\/skills\//g, '.hermes/skills/'); - jsContent = jsContent.replace(/\.claude\//g, '.hermes/'); - jsContent = jsContent.replace(/CLAUDE\.md/g, 'HERMES.md'); - jsContent = jsContent.replace(/\bClaude Code\b/g, 'Hermes Agent'); - fs.writeFileSync(destPath, jsContent); } else { fs.copyFileSync(srcPath, destPath); } @@ -7647,54 +6875,54 @@ function validateHookFields(settings) { * GSD hook filenames removed during uninstall. * Module-level so tests can assert structurally instead of regex-parsing source * (retires pending-migration-to-typed-ir on hooks-opt-in.test.cjs, per #455). + * + * Derived from _HOOKS_TO_COPY (scripts/build-hooks.js — the SAME single source + * of truth INSTALLED_HOOK_FILES uses for manifest-tracking above) instead of a + * separately hand-maintained literal array. The hand-maintained array had + * silently drifted out of sync with the install-time set — missing + * gsd-check-update-worker.js, gsd-ensure-canonical-path.js, + * managed-hooks-registry.cjs, gsd-cursor-pre-tool.js, gsd-cursor-stop.js, + * gsd-cursor-subagent-start.js, gsd-cursor-subagent-stop.js, and + * gsd-worktree-path-guard.js — so every one of those files (and the hooks/ dir + * itself, via the non-empty-dir rmdir guard) was left behind on uninstall for + * every settings-json-hook runtime. `gsd-check-update.cmd` is added on top: a + * Windows-only SessionStart shim generated at install time (not copied from + * hooks/dist/, so it is not in _HOOKS_TO_COPY). */ -const GSD_UNINSTALL_HOOKS = [ - 'gsd-statusline.js', - 'gsd-check-update.js', - 'gsd-check-update.cmd', - 'gsd-config-reload.js', - 'gsd-context-monitor.js', - 'gsd-cursor-session-start.js', - 'gsd-cursor-post-tool.js', - 'gsd-prompt-guard.js', - 'gsd-read-guard.js', - 'gsd-read-injection-scanner.js', - 'gsd-update-banner.js', - 'gsd-workflow-guard.js', - 'gsd-session-state.sh', - 'gsd-validate-commit.sh', - 'gsd-phase-boundary.sh', - 'gsd-graphify-update.sh', -]; +const GSD_UNINSTALL_HOOKS = [..._HOOKS_TO_COPY, 'gsd-check-update.cmd']; /** * Uninstall GSD from the specified directory for a specific runtime * Removes only GSD-specific files/directories, preserves user content * @param {boolean} isGlobal - Whether to uninstall from global or local - * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex', 'copilot') + * @param {string} runtime - Target runtime ('claude', 'opencode', 'codex', 'copilot') */ -function uninstall(isGlobal, runtime = 'claude') { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isGemini = runtime === 'gemini'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isAugment = runtime === 'augment'; - const isTrae = runtime === 'trae'; - const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; - const isCodebuddy = runtime === 'codebuddy'; +function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { + // #2093: isKilo dropped — the Kilo permission-cleanup branch below is + // descriptor-driven (resolveInstallPlan(runtime).finishPermissionWriter), + // not gated on this flag. + // #2094: isTrae dropped — unused in this function after the + // skipSharedHooksInstall fold (was never referenced here besides the + // destructure). #2095: isKimi likewise dropped — kimi is now a hooks/ + // consumer, so its former `&& !isKimi` uninstall guards were removed. + // #2096: isAntigravity dropped — unused in this function. + // #2098: isCodebuddy dropped — unused in this function. + // #2099: isCopilot dropped — both Copilot side-effect branches below are now + // gated on resolveInstallPlan(runtime).installSurface === 'copilot-instructions'. + // #2100: isWindsurf dropped — unused in this function. + const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime); const dirName = getDirName(runtime); // Get the target directory based on runtime and install type. Cline local // installs write to the project root (.clinerules/ lives at the root, not in // a .cline/ subdir), mirroring the install() path resolution (#787). + // Descriptor-driven (ADR-1239 / #2090): cline local installs write to the + // project root (.clinerules/ lives at the root, not in a .cline/ subdir), + // mirroring the install() path resolution (#787). Folded from a hardcoded + // `runtime === 'cline'` branch into hostBehaviors.localTargetIsProjectRoot. const targetDir = isGlobal ? getGlobalConfigDir(runtime, explicitConfigDir) - : runtime === 'cline' + : _hostBehaviors(runtime).localTargetIsProjectRoot ? process.cwd() : path.join(process.cwd(), dirName); @@ -7702,28 +6930,22 @@ function uninstall(isGlobal, runtime = 'claude') { ? targetDir.replace(os.homedir(), '~') : targetDir.replace(process.cwd(), '.'); - let runtimeLabel = 'Claude Code'; - if (runtime === 'opencode') runtimeLabel = 'OpenCode'; - if (runtime === 'gemini') runtimeLabel = 'Gemini'; - if (runtime === 'kilo') runtimeLabel = 'Kilo'; - if (runtime === 'codex') runtimeLabel = 'Codex'; - if (runtime === 'copilot') runtimeLabel = 'Copilot'; - if (runtime === 'antigravity') runtimeLabel = 'Antigravity'; - if (runtime === 'cursor') runtimeLabel = 'Cursor'; - if (runtime === 'windsurf') runtimeLabel = 'Windsurf'; - if (runtime === 'augment') runtimeLabel = 'Augment'; - if (runtime === 'trae') runtimeLabel = 'Trae'; - if (runtime === 'qwen') runtimeLabel = 'Qwen Code'; - if (runtime === 'hermes') runtimeLabel = 'Hermes Agent'; - if (runtime === 'kimi') runtimeLabel = 'Kimi CLI'; - if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy'; + // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239 + // Phase B / #1679) — collapses the prior 15-line assignment chain. + const runtimeLabel = getRuntimeLabel(runtime); console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); // #786: AGENTS.md lives at the repo root (outside targetDir) for local Copilot // installs, so its cleanup must run even when .github (targetDir) was already // removed — i.e. BEFORE the "target directory missing" early-return below. - if (isCopilot && !isGlobal) { + // #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface === + // 'copilot-instructions' (was hardcoded `isCopilot`). Mirrors the install-time + // gate at the 'copilot-instructions' branch below (~line 10471 equivalent), + // which writes this same repo-root AGENTS.md only for local ('!isGlobal') + // installs — 'copilot-instructions' is unique to copilot's descriptor, so + // this is byte-parity. + if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions' && !isGlobal) { const agentsMdPath = path.join(process.cwd(), 'AGENTS.md'); if (fs.existsSync(agentsMdPath)) { const content = fs.readFileSync(agentsMdPath, 'utf8'); @@ -7755,11 +6977,34 @@ function uninstall(isGlobal, runtime = 'claude') { // 1. Remove GSD commands/skills (layout-driven) const scope = isGlobal ? 'global' : 'local'; - uninstallRuntimeArtifacts(runtime, targetDir, scope); + // ADR-1239 / #2086: drive uninstall through the public Host-Integration Interface. + // Fail-open to the engine directly if the composed-registry adapter can't load. + const _uninstallAdapter = _runtimeAdapter(runtime); + if (_uninstallAdapter) { + _uninstallAdapter.uninstall({ configDir: targetDir, scope }); + } else { + uninstallRuntimeArtifacts(runtime, targetDir, scope); + } removedCount++; + // ADR-1239 split-home migration: the adapter/plan uninstall targets the new + // `home` location (e.g. Codex → ~/.agents/skills). A user who installed + // BEFORE the move and never reinstalled still has managed gsd-* skill dirs at + // the old configDir-rooted location (~/.codex/skills) — remove those too so + // uninstall leaves nothing behind. User-owned content is preserved. + { + const _movedOldSkillsDir = _resolveMovedSkillsOldDir(runtime, targetDir, scope); + if (_movedOldSkillsDir) { + const migrated = cleanupMovedSkillsOldLocation(_movedOldSkillsDir, 'gsd-'); + if (migrated > 0) { + removedCount++; + console.log(` ${green}✓${reset} Removed ${migrated} legacy skill dir(s) from ${_movedOldSkillsDir}`); + } + } + } + // 1a. Non-layout Codex side-effects: agent .toml files, config.toml sections, hooks.json - if (isCodex) { + if (_hostBehaviors(runtime).tomlConfigInstall) { const codexAgentsDir = path.join(targetDir, 'agents'); if (fs.existsSync(codexAgentsDir)) { const tomlFiles = fs.readdirSync(codexAgentsDir); @@ -7798,8 +7043,10 @@ function uninstall(isGlobal, runtime = 'claude') { console.log(` ${green}✓${reset} Removed managed Codex SessionStart hook from hooks.json`); } - // #772: remove new Codex hook event registrations added by this enhancement. - for (const eventName of ['SubagentStart', 'Stop', 'PostToolUse']) { + // #772/#2088: remove every managed Codex extended hook-event registration. + // Shares CODEX_EXTENDED_HOOK_EVENTS with the install loop — removal set == + // registration set, so no managed event is ever orphaned. + for (const eventName of CODEX_EXTENDED_HOOK_EVENTS) { const eventCleanup = removeCodexHooksJsonEvent(targetDir, eventName); if (eventCleanup.changed) { removedCount++; @@ -7808,8 +7055,84 @@ function uninstall(isGlobal, runtime = 'claude') { } } + // 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's + // native config.toml lives outside targetDir entirely (resolveKimiHooksTomlDir + // resolves ~/.kimi, a sibling of targetDir's ~/.config/agents), so its + // cleanup can't be driven by anything under targetDir the way every other + // hook surface above is. + if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') { + const kimiHooksRoot = resolveKimiHooksTomlDir(); + const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml'); + const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath); + if (kimiHooksCleanup.changed) { + removedCount++; + console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`); + } + + // Kimi's shared hook scripts + CommonJS package.json marker are installed + // into this SAME ~/.kimi root (installSharedHooksBundle, install()'s + // kimi-hooks-toml branch) rather than under targetDir — mirror steps "4. + // Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to + // kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space — + // may hold the user's real config.toml/providers), so only the exact + // GSD-owned filenames are removed, and directories are pruned only if left + // empty by that removal. + const kimiHooksDir = path.join(kimiHooksRoot, 'hooks'); + if (fs.existsSync(kimiHooksDir)) { + let kimiHookCount = 0; + for (const hook of GSD_UNINSTALL_HOOKS) { + const hookPath = path.join(kimiHooksDir, hook); + if (fs.existsSync(hookPath)) { + fs.unlinkSync(hookPath); + kimiHookCount++; + } + } + if (kimiHookCount > 0) { + removedCount++; + console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`); + } + + const kimiHooksLibDir = path.join(kimiHooksDir, 'lib'); + if (fs.existsSync(kimiHooksLibDir)) { + let removedKimiLibFiles = 0; + for (const file of GSD_HOOK_LIB_FILES) { + try { + fs.unlinkSync(path.join(kimiHooksLibDir, file)); + removedKimiLibFiles++; + } catch (_) { /* best-effort */ } + } + try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ } + if (removedKimiLibFiles > 0) { + removedCount++; + console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`); + } + } + + try { + if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir); + } catch (_) { /* not empty — leave it */ } + } + + const kimiPkgJsonPath = path.join(kimiHooksRoot, 'package.json'); + if (fs.existsSync(kimiPkgJsonPath)) { + try { + const content = fs.readFileSync(kimiPkgJsonPath, 'utf8').trim(); + if (content === '{"type":"commonjs"}') { + fs.unlinkSync(kimiPkgJsonPath); + removedCount++; + console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot}`); + } + } catch (e) { + // Ignore read errors + } + } + } + // 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup - if (isCopilot) { + // #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface === + // 'copilot-instructions' (was hardcoded `isCopilot`), mirroring the same + // gate used at the install-time 'copilot-instructions' branch. + if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions') { const instructionsPath = path.join(targetDir, 'copilot-instructions.md'); if (fs.existsSync(instructionsPath)) { const content = fs.readFileSync(instructionsPath, 'utf8'); @@ -7846,7 +7169,9 @@ function uninstall(isGlobal, runtime = 'claude') { // 1b-cline. Non-layout Cline side-effects (issue #787): remove the // directory-form rules + PreToolUse hook, and strip the GSD block from the // global cross-tool ~/.agents/AGENTS.md target. - if (runtime === 'cline') { + // Descriptor-driven (ADR-1239 / #2090): folded from `runtime === 'cline'` + // into hostBehaviors.clineRulesSurface. + if (_hostBehaviors(runtime).clineRulesSurface) { const clinerulesDir = path.join(targetDir, '.clinerules'); for (const rel of ['gsd.md', path.join('hooks', 'PreToolUse')]) { const p = path.join(clinerulesDir, rel); @@ -7892,17 +7217,20 @@ function uninstall(isGlobal, runtime = 'claude') { } } - // 1b-cursor. Non-layout Cursor side-effects (issue #777): remove GSD-managed - // hook entries from hooks.json and clean up the managed hook scripts. - if (isCursor) { + // 1b-cursor. Descriptor-driven hook-bus cleanup (ADR-1239 / #2089): remove + // GSD-managed hook entries from hooks.json and clean up the managed hook + // scripts. Gated by the hostBehaviors.hooksJsonSurface descriptor axis, not a + // hardcoded `isCursor` branch. + if (_hostBehaviors(runtime).hooksJsonSurface) { const hooksJsonCleanup = removeCursorHooksJson(targetDir); if (hooksJsonCleanup.changed) { removedCount++; console.log(` ${green}✓${reset} Removed GSD-managed Cursor hooks from hooks.json`); } - // Remove the managed hook scripts (session-start + post-tool). + // Remove all GSD-managed hook scripts (sessionStart, postToolUse, preToolUse, + // stop, subagentStart, subagentStop — AC4a, #2089). const hooksDir = path.join(targetDir, 'hooks'); - for (const script of [GSD_CURSOR_SESSION_HOOK_SCRIPT, GSD_CURSOR_POST_TOOL_HOOK_SCRIPT]) { + for (const script of GSD_CURSOR_HOOK_SCRIPTS) { const p = path.join(hooksDir, script); try { if (fs.existsSync(p)) { @@ -7919,9 +7247,41 @@ function uninstall(isGlobal, runtime = 'claude') { } catch { /* best-effort */ } } + // 1b-windsurf. Descriptor-driven hook-bus cleanup (ADR-1239 / #2100 Stage 2): + // remove GSD-managed Cascade hook entries from hooks.json and clean up the + // managed hook scripts. Gated on resolveInstallPlan(runtime).hooksSurface + // === 'windsurf-hooks-json' (mirrors the kimi-hooks-toml gate above) — + // NOT the shared hostBehaviors.hooksJsonSurface flag the Cursor block above + // uses, since that flag drives Cursor's own remove function + script list + // and is not (and must not be) set for Windsurf. + if (resolveInstallPlan(runtime).hooksSurface === 'windsurf-hooks-json') { + const windsurfHooksJsonCleanup = removeWindsurfHooksJson(targetDir); + if (windsurfHooksJsonCleanup.changed) { + removedCount++; + console.log(` ${green}✓${reset} Removed GSD-managed Windsurf hooks from hooks.json`); + } + // Remove all GSD-managed hook scripts (pre_write_code, pre_run_command). + const windsurfHooksDir = path.join(targetDir, 'hooks'); + for (const script of GSD_WINDSURF_HOOK_SCRIPTS) { + const p = path.join(windsurfHooksDir, script); + try { + if (fs.existsSync(p)) { + fs.unlinkSync(p); + removedCount++; + } + } catch { /* best-effort */ } + } + // Prune hooks/ if empty. + try { + if (fs.existsSync(windsurfHooksDir) && fs.readdirSync(windsurfHooksDir).length === 0) { + fs.rmdirSync(windsurfHooksDir); + } + } catch { /* best-effort */ } + } + // 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout, // #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs. - if (!isGlobal && runtime === 'claude') { + if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') { const commandsDir = path.join(targetDir, 'commands'); // Remove flat gsd-*.md files (current layout after #1367 fix) if (fs.existsSync(commandsDir)) { @@ -7958,35 +7318,12 @@ function uninstall(isGlobal, runtime = 'claude') { } } - // 1d. Gemini: remove commands/gsd/ with dev-preferences.md preservation. - // The layout removes gsd-*.toml files but not the directory itself. - // Preserve user files before removing the directory. - if (isGemini) { - const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd'); - if (fs.existsSync(gsdCommandsDir)) { - const devPrefsPath = path.join(gsdCommandsDir, 'dev-preferences.md'); - const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null; - fs.rmSync(gsdCommandsDir, { recursive: true }); - removedCount++; - console.log(` ${green}✓${reset} Removed commands/gsd/`); - if (preservedDevPrefs) { - try { - fs.mkdirSync(gsdCommandsDir, { recursive: true }); - fs.writeFileSync(devPrefsPath, preservedDevPrefs); - console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`); - } catch (err) { - console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`); - } - } - } - } - // 1d. Qwen/Hermes: migrate dev-preferences.md from legacy commands/gsd/ location // during uninstall. _runLegacyUninstallCleanup (called by uninstallRuntimeArtifacts) // removes the directory; we must preserve/restore user artifacts before that path. // This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory // was already removed and skip if so (idempotent). - if (isQwen || isHermes) { + if (_hostBehaviors(runtime).legacyCommandsGsdCleanup === true) { // dev-preferences may have survived in skills/ as SKILL.md — nothing to do for // that case. If a stale commands/gsd/ still exists (e.g. legacy was not removed), // attempt migration. In practice _runLegacyUninstallCleanup removes it first, @@ -8096,6 +7433,25 @@ function uninstall(isGlobal, runtime = 'claude') { } } + // 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093). + // Descriptor-driven via hostBehaviors.nativePlugin — covers every runtime + // that declares the block (OpenCode, Kilo, ...), not just OpenCode. Only + // GSD's own plugin file is removed; the plugins/ dir is pruned only if it + // becomes empty, preserving any user-authored plugins for that host. + const _np = _hostBehaviors(runtime).nativePlugin; + if (_np) { + const pluginsDir = path.join(targetDir, _np.dir); + const pluginPath = path.join(pluginsDir, _np.file); + if (fs.existsSync(pluginPath)) { + try { + fs.unlinkSync(pluginPath); + removedCount++; + console.log(` ${green}✓${reset} Removed native plugin adapter (${runtime})`); + } catch (_) { /* best-effort */ } + try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ } + } + } + // 4a. Remove scripts/changeset/ and scripts/lib/ (#935) // GSD-managed files only: enumerate the exact set the installer writes. // Any file NOT in this set is user-owned and must survive uninstall. @@ -8140,6 +7496,11 @@ function uninstall(isGlobal, runtime = 'claude') { const fixSlashUninstallPath = path.join(targetDir, 'scripts', 'fix-slash-commands.cjs'); try { fs.unlinkSync(fixSlashUninstallPath); } catch (_) { /* best-effort */ } + // Remove the capability registry generator scripts (#1920) — before the scripts/ rmdir + for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) { + try { fs.unlinkSync(path.join(targetDir, 'scripts', gen)); } catch (_) { /* best-effort */ } + } + // If scripts/ dir is now empty, remove it too const scriptsUninstallDir = path.join(targetDir, 'scripts'); if (fs.existsSync(scriptsUninstallDir)) { @@ -8183,7 +7544,7 @@ function uninstall(isGlobal, runtime = 'claude') { // Remove GSD hooks from settings — per-hook granularity to preserve // user hooks that share an entry with a GSD hook (#1755 followup). // Includes the 3 Qwen-only events added in #788 (SubagentStop, Stop, - // PreCompact, also registered for Claude in #770), the 3 Gemini-only + // PreCompact, also registered for Claude in #770), the 3 Antigravity-only // events added in #776 (BeforeAgent, AfterAgent, BeforeModel), and the // Claude-only FileChanged event added in #770 — safe to iterate for all // runtimes; installs that don't register these events simply find no @@ -8226,7 +7587,7 @@ function uninstall(isGlobal, runtime = 'claude') { // to preserve any user-added allow/deny entries. // Uses a local flag to avoid the shared `settingsModified` producing a false // "Removed GSD permissions" message when only hooks/statusline changed. - if (runtime === 'claude' && settings.permissions) { + if (_hostBehaviors(runtime).permissionsSchema === 'claude' && settings.permissions) { let permissionsModified = false; if (Array.isArray(settings.permissions.allow)) { const before = settings.permissions.allow.length; @@ -8252,6 +7613,47 @@ function uninstall(isGlobal, runtime = 'claude') { } } + // #2096 Phase B Upgrade 1 — Remove GSD-owned Antigravity permissions.allow + // rules from settings.json. Symmetric to the Claude branch above: filters + // only the exact GSD-owned rule strings (regenerated from the current + // configDir) to preserve any user-added allow entries and all deny/ask. + if (resolveInstallPlan(runtime).finishPermissionWriter === 'antigravity' && settings.permissions) { + let antigravityPermissionsModified = false; + if (Array.isArray(settings.permissions.allow)) { + const gsdRules = new Set(buildAntigravityAllowRules(targetDir)); + const before = settings.permissions.allow.length; + settings.permissions.allow = settings.permissions.allow.filter((e) => !gsdRules.has(e)); + if (settings.permissions.allow.length !== before) { + antigravityPermissionsModified = true; + } + if (settings.permissions.allow.length === 0) { + delete settings.permissions.allow; + } + } + if (Object.keys(settings.permissions).length === 0) { + delete settings.permissions; + } + if (antigravityPermissionsModified) { + settingsModified = true; + console.log(` ${green}✓${reset} Removed GSD permissions from settings.json`); + } + } + + // #2097 UPGRADE 3 — Remove the MCP companion entry from settings.json for + // runtimes that host MCP there (Augment), symmetric to the mcp_config.json + // removal for Antigravity below. Only the GSD-owned mcpServers.gsd key is + // removed — any other user-configured MCP servers are preserved. + if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' && + settings.mcpServers && typeof settings.mcpServers === 'object' && + settings.mcpServers.gsd !== undefined) { + delete settings.mcpServers.gsd; + if (Object.keys(settings.mcpServers).length === 0) { + delete settings.mcpServers; + } + settingsModified = true; + console.log(` ${green}✓${reset} Removed GSD MCP companion server from settings.json`); + } + if (settingsModified) { writeSettings(settingsPath, settings); removedCount++; @@ -8259,7 +7661,7 @@ function uninstall(isGlobal, runtime = 'claude') { } // 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc - if (isOpencode) { + if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') { const configPath = resolveOpencodeConfigPath(targetDir); if (fs.existsSync(configPath)) { try { @@ -8300,7 +7702,9 @@ function uninstall(isGlobal, runtime = 'claude') { } // 7. For Kilo, clean up permissions from kilo.json or kilo.jsonc - if (isKilo) { + // #2093: descriptor-driven via resolveInstallPlan(runtime).finishPermissionWriter, + // mirroring the OpenCode branch above (was hardcoded `isKilo`). + if (resolveInstallPlan(runtime).finishPermissionWriter === 'kilo') { const configPath = resolveKiloConfigPath(targetDir); if (fs.existsSync(configPath)) { try { @@ -8340,6 +7744,29 @@ function uninstall(isGlobal, runtime = 'claude') { } } + // 8. For Antigravity, remove the MCP companion entry from mcp_config.json + // (#2096 Phase B Upgrade 2). Only the GSD-owned mcpServers.gsd key is + // removed — any other user-configured MCP servers are preserved. + if (resolveInstallPlan(runtime).finishPermissionWriter === 'antigravity') { + const mcpConfigPath = path.join(targetDir, 'mcp_config.json'); + if (fs.existsSync(mcpConfigPath)) { + try { + const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8')); + if (mcpConfig && typeof mcpConfig === 'object' && mcpConfig.mcpServers && mcpConfig.mcpServers.gsd !== undefined) { + delete mcpConfig.mcpServers.gsd; + if (Object.keys(mcpConfig.mcpServers).length === 0) { + delete mcpConfig.mcpServers; + } + fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n'); + removedCount++; + console.log(` ${green}✓${reset} Removed GSD MCP companion server from mcp_config.json`); + } + } catch (e) { + // Ignore JSON parse errors + } + } + } + // Remove the file manifest that the installer wrote at install time. // Without this step the metadata file persists after uninstall (#1908). const manifestPath = path.join(targetDir, MANIFEST_NAME); @@ -8482,7 +7909,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) { modified = true; } - // Configure external_directory permission (the safety guard for paths outside project) + // Configure external_directory permission (the safety guard for paths outside) if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') { config.permission.external_directory = {}; } @@ -8491,6 +7918,25 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) { modified = true; } + // ADR-1239 Phase D / #1682 — register the companion MCP server (Phase 4) so + // OpenCode connects to GSD's command (point 1) + state-IO (point 5) surface + // with NO bespoke plugin. Idempotent + non-clobbering: only added when + // `mcp.gsd` is absent (a user-defined `mcp.gsd` is respected — Hyrum's Law). + // Local-stdio schema per OpenCode config (packages/core/src/config/mcp.ts). + // `-p @opengsd/gsd-core` resolves the `gsd-mcp-server` bin from this package + // (bin name != package name) regardless of global-install state. + if (!config.mcp || typeof config.mcp !== 'object') { + config.mcp = {}; + } + if (config.mcp.gsd === undefined) { + config.mcp.gsd = { + type: 'local', + command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'], + enabled: true, + }; + modified = true; + } + if (!modified) { return; // Already configured } @@ -8574,6 +8020,193 @@ function configureKiloPermissions(isGlobal = true, configDir = null) { console.log(` ${green}✓${reset} Configured read permission for GSD docs`); } +/** + * Convert an absolute path to a `~`-relative form when it lives under the + * user's home directory (generalizes configureKiloPermissions' + * single-default-dir shorthand to Antigravity's three probed sibling config + * dirs — antigravity/antigravity-ide/antigravity-cli under ~/.gemini — none of + * which is a single fixed "default"). + */ +function toTildePosixPath(absPath) { + const posixPath = absPath.replace(/\\/g, '/'); + const posixHome = os.homedir().replace(/\\/g, '/'); + return posixPath === posixHome || posixPath.startsWith(`${posixHome}/`) + ? `~${posixPath.slice(posixHome.length)}` + : posixPath; +} + +/** + * Antigravity permission rule strings this installer contributes. + * Schema: antigravity.google/docs/cli/permissions — "action(target)" rule + * strings in permissions.{allow,deny,ask}, evaluated deny > ask > allow. GSD + * only ever contributes to `allow` — never deny/ask (those are user-owned risk + * decisions this installer has no business making). + */ +function buildAntigravityAllowRules(configDir) { + const gsdPath = toTildePosixPath(configDir); + return [ + `read_file(${gsdPath}/gsd-core/*)`, + `read_file(${gsdPath}/agents/gsd-*)`, + `read_file(${gsdPath}/skills/gsd-*)`, + `command(node ${gsdPath}/hooks/*)`, + ]; +} + +/** + * Configure Antigravity permissions to allow reading/executing GSD's installed + * tree without per-call approval prompts (#2096 Phase B Upgrade 1 — mirrors + * configureKiloPermissions/configureOpencodePermissions). + * + * Antigravity's permission schema (antigravity.google/docs/cli/permissions) is + * `{"permissions":{"allow":[...],"deny":[...],"ask":[...]}}`, living in the + * SAME settings.json GSD's own hook registration writes for this runtime + * (installSurface: 'settings-json', writesSharedSettings: true) — unlike + * Kilo/OpenCode, which write a separate native config file. This function + * re-reads the file (already containing GSD's hooks by the time finishInstall + * reaches this call) and only appends to permissions.allow. + * + * Non-destructive + idempotent: only `permissions.allow` is touched; an + * existing user permissions block (including any deny/ask entries, or + * unrelated allow entries) is preserved untouched. + * + * @param {boolean} isGlobal - Whether this is a global or local install + * @param {string|null} configDir - Resolved config directory when already known + */ +function configureAntigravityPermissions(isGlobal = true, configDir = null) { + // For local installs, use ./.agents/ (GSD's antigravity localConfigDir) + // For global installs, use the resolved ~/.gemini/antigravity{,-ide,-cli} + const antigravityConfigDir = configDir || (isGlobal + ? getGlobalConfigDir('antigravity', explicitConfigDir) + : path.join(process.cwd(), '.agents')); + // Ensure config directory exists + fs.mkdirSync(antigravityConfigDir, { recursive: true }); + + const configPath = path.join(antigravityConfigDir, 'settings.json'); + + // Read existing settings.json (readSettings tolerates JSONC + missing file; + // returns null — and warns — only when the file exists but fails to parse). + const config = readSettings(configPath); + if (config === null) { + // Cannot parse — DO NOT overwrite user's config (readSettings already warned). + return; + } + + // Ensure permission structure exists + if (!config.permissions || typeof config.permissions !== 'object' || Array.isArray(config.permissions)) { + config.permissions = {}; + } + if (!Array.isArray(config.permissions.allow)) { + config.permissions.allow = []; + } + + let modified = false; + for (const rule of buildAntigravityAllowRules(antigravityConfigDir)) { + if (!config.permissions.allow.includes(rule)) { + config.permissions.allow.push(rule); + modified = true; + } + } + + if (!modified) { + return; // Already configured + } + + writeSettings(configPath, config); + console.log(` ${green}✓${reset} Configured Antigravity permissions for GSD paths`); +} + +/** + * Configure Antigravity's MCP companion server config (#2096 Phase B + * Upgrade 2). + * + * Antigravity CLI manages MCP servers via standalone `mcp_config.json` + * profiles rather than nesting them in settings.json (antigravity.google/docs/ + * cli/gcli-migration: "Antigravity CLI uses standalone mcp_config.json + * profiles in ~/.gemini/config/ for global servers and .agents/mcp_config.json + * for workspace servers"). The raw schema for the Antigravity IDE surface + * itself is unpublished (docs are JS-rendered), so this follows the CLI's + * documented standalone-profile convention plus the standard Gemini/MCP + * `mcpServers` shape. + * + * BEST-EFFORT PATH CHOICE: rather than the CLI doc's separate `~/.gemini/config/` + * directory for global scope, this writes `/mcp_config.json` — the + * SAME resolved configDir as settings.json (configureAntigravityPermissions) — + * because (1) GSD's own antigravity configDir resolution already varies + * per-user across three sibling dirs (antigravity/antigravity-ide/ + * antigravity-cli — see resolveAntigravityGlobalDir), so a hardcoded separate + * shared path would not track that resolution, and (2) it matches the doc's + * OWN workspace-scope convention exactly (`.agents/mcp_config.json`, which IS + * GSD's local configDir for antigravity), keeping global/local symmetric and + * consistent with the configDir-relative convention every other GSD + * permission writer (kilo/opencode) already uses. + * + * Non-destructive + idempotent: only adds mcpServers.gsd when entirely absent; + * any other user-configured mcpServers entries (or a user's OWN "gsd" override) + * are preserved untouched (Hyrum's Law — mirrors OpenCode's config.mcp.gsd guard). + * + * @param {boolean} isGlobal - Whether this is a global or local install + * @param {string|null} configDir - Resolved config directory when already known + */ +function configureAntigravityMcpConfig(isGlobal = true, configDir = null) { + const antigravityConfigDir = configDir || (isGlobal + ? getGlobalConfigDir('antigravity', explicitConfigDir) + : path.join(process.cwd(), '.agents')); + fs.mkdirSync(antigravityConfigDir, { recursive: true }); + + const configPath = path.join(antigravityConfigDir, 'mcp_config.json'); + + let config = {}; + if (fs.existsSync(configPath)) { + try { + const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')); + config = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {}; + } catch (e) { + // Cannot parse - DO NOT overwrite user's config + console.log(` ${yellow}⚠${reset} Could not parse mcp_config.json - skipping MCP companion config`); + console.log(` ${dim}Reason: ${e.message}${reset}`); + console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`); + return; + } + } + + if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) { + config.mcpServers = {}; + } + + if (config.mcpServers.gsd !== undefined) { + return; // Already configured (or a user-owned override) — never clobber. + } + + config.mcpServers.gsd = { + command: 'npx', + args: ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'], + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); + console.log(` ${green}✓${reset} Configured Antigravity MCP companion server (gsd)`); +} + +/** + * #2097 (ADR-1239 transport:mcp): register the GSD companion MCP server inside a + * runtime's settings.json (Augment hosts MCP in settings.json.mcpServers, unlike + * Antigravity's standalone mcp_config.json). Mutates the in-memory settings object + * that finishInstall already writes — non-destructive + idempotent: only sets + * mcpServers.gsd, preserving any user-defined servers (a user's own `gsd` override + * is respected — Hyrum's Law). + * @param {object} settings - the in-memory settings object finishInstall will write + */ +function mergeGsdMcpServerIntoSettings(settings) { + if (!settings.mcpServers || typeof settings.mcpServers !== 'object' || Array.isArray(settings.mcpServers)) { + settings.mcpServers = {}; + } + if (settings.mcpServers.gsd === undefined) { + settings.mcpServers.gsd = { + command: 'npx', + args: ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'], + }; + } +} + /** * Verify a directory exists and contains files */ @@ -8609,7 +8242,7 @@ function verifyFileInstalled(filePath, description) { /** * Install to the specified directory for a specific runtime * @param {boolean} isGlobal - Whether to install globally or locally - * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex') + * @param {string} runtime - Target runtime ('claude', 'opencode', 'codex') */ // ────────────────────────────────────────────────────── @@ -8676,58 +8309,41 @@ function resolveInstallRelativePath(baseDir, relPath) { return { relPath: normalized, fullPath }; } -function hasExistingSymlinkBetween(root, fullPath) { - const resolvedRoot = path.resolve(root); - const resolvedFullPath = path.resolve(fullPath); - if (resolvedFullPath !== resolvedRoot && !resolvedFullPath.startsWith(resolvedRoot + path.sep)) { - return true; - } - - let cursor = resolvedRoot; - if (fs.existsSync(cursor) && fs.lstatSync(cursor).isSymbolicLink()) { - return true; - } - - const relative = path.relative(resolvedRoot, resolvedFullPath); - for (const segment of relative.split(path.sep)) { - if (!segment) continue; - cursor = path.join(cursor, segment); - if (!fs.existsSync(cursor)) return false; - if (fs.lstatSync(cursor).isSymbolicLink()) return true; - } - - return false; -} +// hasExistingSymlinkBetween: moved to src/install-engine.cts (ADR-1239 Phase B). +// Imported from installEngine above. /** * Write file manifest after installation for future modification detection */ -function writeManifest(configDir, runtime = 'claude', options = {}) { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isGemini = runtime === 'gemini'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isTrae = runtime === 'trae'; - const isCline = runtime === 'cline'; - const isKimi = runtime === 'kimi'; - const isHermes = runtime === 'hermes'; +function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) { + // #2093: isKilo dropped — unused in this function. + // #2094: isTrae dropped — was only used in the hooks-tracking conditional + // above, now covered by hostBehaviors.skipSharedHooksInstall. + // #2095: isKimi dropped — kimi is now a hooks/ consumer like every other + // settings-json-adjacent runtime, so the `&& !isKimi` term below was removed. + // #2096: isAntigravity dropped — unused in this function. + // #2098: isCodebuddy dropped — unused in this function. + // #2099: isCopilot dropped — was only used in the hooks-tracking conditional + // above, now covered by hostBehaviors.skipSharedHooksInstall. + // #2100: isWindsurf dropped — was only used in the hooks-tracking conditional + // above, now covered by hostBehaviors.skipSharedHooksInstall. + const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime); const gsdDir = path.join(configDir, 'gsd-core'); // #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/). - // commandsDir points to the old location for Gemini (which still uses commands/gsd/). // Claude local uses flatCommandsDir instead for manifest recording. - const commandsDir = path.join(configDir, 'commands', 'gsd'); const flatCommandsDir = path.join(configDir, 'commands'); - const opencodeCommandDir = path.join(configDir, 'command'); - // Hermes nests GSD skills under skills/gsd/ as a single category (#2841). + const opencodeCommandDir = path.join(configDir, _hostBehaviors(runtime).flatCommandDir || 'command'); + // Hermes nests GSD skills under skills/gsd/ as a single category (#2841) — + // already encoded in its layout descriptor's destSubpath ('skills/gsd'). // All other runtimes that use the Codex-style skills layout use a flat skills/ root. - const codexSkillsDir = isHermes - ? path.join(configDir, 'skills', 'gsd') - : path.join(configDir, 'skills'); - const codexSkillsManifestPrefix = isHermes ? 'skills/gsd/' : 'skills/'; + // ADR-1239 upgrade 3 (#2088): honor a skills-kind `home` override (e.g. Codex + // skills -> $HOME/.agents/skills instead of configDir/skills) via the same + // descriptor-driven helper used by the snapshot/rollback/verification paths, + // so the manifest records what's actually on disk. _resolveSkillsRootDir already + // resolves destSubpath (which includes hermes's 'skills/gsd' nesting) — do not + // re-append 'gsd' or the hermes dir gets double-nested to skills/gsd/gsd. + const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, options.scope === 'local' ? 'local' : 'global'); + const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/'; const agentsDir = path.join(configDir, 'agents'); const manifest = { version: pkg.version, @@ -8747,34 +8363,27 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { manifest.files['gsd-core/' + rel] = hash; } // Record commands surface for runtimes that emit it: - // Gemini: commands/gsd/.toml (nested, colon-namespaced) // Claude local (#1367 fix): flat gsd-.md at commands/ level // Manifest must reflect everything on disk so saveLocalPatches() can detect // user edits and per-runtime minimal-mode assertions can read manifest.files. - if (isGemini && fs.existsSync(commandsDir)) { - const cmdHashes = generateManifest(commandsDir); - for (const [rel, hash] of Object.entries(cmdHashes)) { - manifest.files['commands/gsd/' + rel] = hash; - } - } // Claude local (#1367): flat gsd-*.md files at commands/ level. // Only claude local writes gsd-*.md here; global installs don't emit commands, // so this branch is a no-op for global (no matching files to find). - if (runtime === 'claude' && fs.existsSync(flatCommandsDir)) { + if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) { for (const file of fs.readdirSync(flatCommandsDir)) { if (file.startsWith('gsd-') && file.endsWith('.md')) { manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file)); } } } - if ((isOpencode || isKilo) && fs.existsSync(opencodeCommandDir)) { + if (_hostBehaviors(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) { for (const file of fs.readdirSync(opencodeCommandDir)) { if (file.startsWith('gsd-') && file.endsWith('.md')) { manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file)); } } } - if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || isTrae || (!isOpencode && !isGemini)) && fs.existsSync(codexSkillsDir)) { + if (!_hostBehaviors(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) { // All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix. const skillListPrefix = 'gsd-'; for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) { @@ -8784,15 +8393,15 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { manifest.files[`${codexSkillsManifestPrefix}${skillName}/${rel}`] = hash; } } - // For Hermes, also hash the category DESCRIPTION.md so reinstall detects drift. - if (isHermes) { + // Descriptor-driven (#2090): hash the category DESCRIPTION.md so reinstall detects drift. + if (_hostBehaviors(runtime).trackCategoryDescription) { const descPath = path.join(codexSkillsDir, 'DESCRIPTION.md'); if (fs.existsSync(descPath)) { manifest.files['skills/gsd/DESCRIPTION.md'] = fileHash(descPath); } } } - if (isKimi && fs.existsSync(agentsDir)) { + if (_hostBehaviors(runtime).agentManifestStyle === 'kimi-nested' && fs.existsSync(agentsDir)) { const agentHashes = generateManifest(agentsDir); for (const [rel, hash] of Object.entries(agentHashes)) { const isRootAgent = rel === 'gsd.yaml' || rel === 'gsd.md'; @@ -8811,7 +8420,9 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { // Track Cline directory-form artifacts in the manifest (issue #787): the // rules file and the PreToolUse hook. (~/.agents/AGENTS.md is tracked via its // marker block, not the per-configDir manifest, since it lives outside it.) - if (isCline) { + // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into + // hostBehaviors.clineRulesSurface. + if (_hostBehaviors(runtime).clineRulesSurface) { for (const rel of ['.clinerules/gsd.md', '.clinerules/hooks/PreToolUse']) { const dest = path.join(configDir, rel); if (fs.existsSync(dest)) { @@ -8822,7 +8433,17 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { // Track hook files so saveLocalPatches() can detect user modifications // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline) - if (!isCodex && !isCopilot && !isCline && !isKimi) { + // Descriptor-driven (ADR-1239 / #2089+#2090): cline's exclusion is via + // hostBehaviors.skipSharedHooksInstall (was hardcoded !isCline). + // #2094: Trae's exclusion is likewise descriptor-driven (trae declares + // skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed. + // #2095: kimi is now a hooks/ consumer (native config.toml [[hooks]] bus) — + // the redundant `&& !isKimi` was removed so its hook files are tracked too. + // #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares + // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed. + // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares + // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed. + if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) { const hooksDir = path.join(configDir, 'hooks'); if (fs.existsSync(hooksDir)) { // Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from @@ -8875,6 +8496,25 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { manifest.files['scripts/fix-slash-commands.cjs'] = fileHash(fixSlashInstallPath); } + // Track the capability registry generator scripts (#1920) — top-level scripts/ files + // not covered by the changeset/lib loops. + for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) { + const genInstallPath = path.join(configDir, 'scripts', gen); + if (fs.existsSync(genInstallPath)) { + manifest.files['scripts/' + gen] = fileHash(genInstallPath); + } + } + + // Track the OpenCode native plugin adapter (#1914) so update/drift detection + // and uninstall can account for it. + const _npM = _hostBehaviors(runtime).nativePlugin; + if (_npM) { + const pluginInstallPath = path.join(configDir, _npM.dir, _npM.file); + if (fs.existsSync(pluginInstallPath)) { + manifest.files[`${_npM.dir}/${_npM.file}`] = fileHash(pluginInstallPath); + } + } + fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2)); return manifest; } @@ -8943,7 +8583,7 @@ function populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathP const srcDir = path.join(packageSrc, top); const stageDir = path.join(stageRoot, top); if (!fs.existsSync(srcDir)) continue; - copyWithPathReplacement(srcDir, stageDir, pathPrefix, runtime, false, isGlobal); + copyWithPathReplacement(srcDir, stageDir, pathPrefix, runtime, false, isGlobal, stageRoot); } for (const relPath of safeModified) { @@ -9155,7 +8795,7 @@ function saveLocalPatches(configDir, pristineCtx) { /** * After install, report backed-up patches for user to reapply. */ -function reportLocalPatches(configDir, runtime = 'claude') { +function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) { const patchesDir = path.join(configDir, PATCHES_DIR_NAME); const metaPath = path.join(patchesDir, 'backup-meta.json'); if (!fs.existsSync(metaPath)) return []; @@ -9164,17 +8804,7 @@ function reportLocalPatches(configDir, runtime = 'claude') { try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; } if (meta.files && meta.files.length > 0) { - const reapplyCommand = (runtime === 'opencode' || runtime === 'kilo' || runtime === 'copilot') - ? '/gsd-update --reapply' - : runtime === 'gemini' - ? '/gsd:update --reapply' - : runtime === 'codex' - ? '$gsd-update --reapply' - : runtime === 'cursor' - ? 'gsd-update --reapply (mention the skill name)' - : runtime === 'kimi' - ? '/skill:gsd-update --reapply' - : '/gsd-update --reapply'; + const reapplyCommand = _hostBehaviors(runtime).reapplyCommand || '/gsd-update --reapply'; console.log(''); console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):'); for (const f of meta.files) { @@ -9200,27 +8830,45 @@ function reportInstallerMigrationResult(result) { } } -function install(isGlobal, runtime = 'claude', options = {}) { - const isOpencode = runtime === 'opencode'; - const isGemini = runtime === 'gemini'; - const isKilo = runtime === 'kilo'; - const isKimi = runtime === 'kimi'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isAugment = runtime === 'augment'; - const isTrae = runtime === 'trae'; - const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; - const isCodebuddy = runtime === 'codebuddy'; - const isCline = runtime === 'cline'; +function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { + // #2093: isKilo dropped — Kilo's agent/model-override handling below reads + // _hostBehaviors(runtime).frontmatterDialect === 'kilo' instead of this flag. + // #2095: isKimi dropped — kimi is now a hooks/ consumer like every other + // settings-json-adjacent runtime; the two `&& !isKimi` hooks-copy guards + // below were removed, leaving isKimi unused in this function (the kimi + // local-install-deferred branch above already reads + // _hostBehaviors(runtime).localInstallDeferred instead of this flag). + // #2096: isAntigravity dropped — antigravity is in + // _DESCRIPTOR_AGENTS_RUNTIMES below, so its two legacy-agent-loop branches + // (the path-rewrite skip and the converter dispatch) were unreachable dead + // code; both were removed rather than re-gated on hostBehaviors. + // #2098: isCodebuddy dropped — codebuddy is also in + // _DESCRIPTOR_AGENTS_RUNTIMES below, so its legacy converter-dispatch branch + // (the `isCodebuddy` arm calling convertClaudeAgentToCodebuddyAgent) was + // unreachable dead code and was removed rather than re-gated. + // #2099: isCopilot dropped — copilot is also in _DESCRIPTOR_AGENTS_RUNTIMES + // below, so its three legacy-agent-loop branches (the path-rewrite skip, + // the converter dispatch, and the .agent.md destName ternary) were + // unreachable dead code and were removed rather than re-gated; the + // .agent.md suffix now lives on hostBehaviors.agentFileExtension in + // src/install-engine.cts, and the skipSharedHooksInstall check above no + // longer needs `&& !isCopilot`. + // #2100: isWindsurf dropped — its four former isWindsurf-gated branches + // (legacy .devin/skills/gsd-* cleanup, the #1629 command-bodies copy, the + // workflow-verification report, and the shared-hooks-install exclusion) are + // now descriptor-driven via hostBehaviors.legacyDevinSkillsCleanup, + // hostBehaviors.installsCommandBodiesForWorkflowDelegation, + // hostBehaviors.verificationStyle === 'windsurf-workflows', and + // hostBehaviors.skipSharedHooksInstall respectively; its legacy-agent-loop + // converter arm was likewise unreachable dead code (windsurf is in + // _DESCRIPTOR_AGENTS_RUNTIMES) and was removed above. + // #2101: isZcode dropped — folded onto hostBehaviors.skipSharedHooksInstall. + const { isOpencode, isCodex, isCursor, isAugment, isTrae, isQwen, isHermes, isCline } = runtimeFlags(runtime); const plan = resolveInstallPlan(runtime); const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); - if (isKimi && !isGlobal) { + if (_hostBehaviors(runtime).localInstallDeferred && !isGlobal) { console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`); console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`); console.log(` Project-level Kimi install semantics remain deferred.`); @@ -9268,15 +8916,17 @@ function install(isGlobal, runtime = 'claude', options = {}) { }; // Get the target directory based on runtime and install type. - // Cline local installs write to the project root (like Claude Code) — .clinerules - // lives at the root, not inside a .cline/ subdirectory. + // Descriptor-driven (ADR-1239 / #2090): cline local installs write to the + // project root (like Claude Code) — .clinerules lives at the root, not inside + // a .cline/ subdirectory. Folded from `isCline` into + // hostBehaviors.localTargetIsProjectRoot. // #791: antigravity local installs write to .agents/ (canonical). The legacy .agent/ // directory is recognized by RUNTIME_DIRS (update-context) and _LEGACY_SCAN_SUBDIR_NAMES // but NOT auto-removed here; legacy .agent/ gsd artifacts are recognized but not // auto-removed on reinstall (dual-read fallback per issue #791 spec). const targetDir = isGlobal ? getGlobalConfigDir(runtime, explicitConfigDir) - : isCline + : _hostBehaviors(runtime).localTargetIsProjectRoot ? process.cwd() : path.join(process.cwd(), dirName); @@ -9355,28 +9005,15 @@ function install(isGlobal, runtime = 'claude', options = {}) { const isWindowsHost = process.platform === 'win32'; const pathPrefix = computePathPrefix({ isGlobal, - isOpencode, + isOpencode: _hostBehaviors(runtime).skipHomePrefixSubstitution === true, isWindowsHost, resolvedTarget, homeDir, }); - let runtimeLabel = 'Claude Code'; - if (isOpencode) runtimeLabel = 'OpenCode'; - if (isGemini) runtimeLabel = 'Gemini'; - if (isKilo) runtimeLabel = 'Kilo'; - if (isCodex) runtimeLabel = 'Codex'; - if (isCopilot) runtimeLabel = 'Copilot'; - if (isAntigravity) runtimeLabel = 'Antigravity'; - if (isCursor) runtimeLabel = 'Cursor'; - if (isWindsurf) runtimeLabel = 'Windsurf'; - if (isAugment) runtimeLabel = 'Augment'; - if (isTrae) runtimeLabel = 'Trae'; - if (isQwen) runtimeLabel = 'Qwen Code'; - if (isHermes) runtimeLabel = 'Hermes Agent'; - if (isKimi) runtimeLabel = 'Kimi'; - if (isCodebuddy) runtimeLabel = 'CodeBuddy'; - if (isCline) runtimeLabel = 'Cline'; + // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239 + // Phase B / #1679) — collapses the prior 16-line assignment chain. + const runtimeLabel = getRuntimeLabel(runtime); console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`); @@ -9434,8 +9071,8 @@ function install(isGlobal, runtime = 'claude', options = {}) { // Map — content snapshot of each pre-existing gsd-* agent file. const codexPreInstallAgentContents = new Map(); let codexPreInstallVersionBytes = null; - if (isCodex && !isMinimalMode(_effectiveInstallMode)) { - const _preSkillsDir = path.join(targetDir, 'skills'); + if (_hostBehaviors(runtime).tomlConfigInstall && !isMinimalMode(_effectiveInstallMode)) { + const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local'); if (fs.existsSync(_preSkillsDir)) { for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) { if (entry.isDirectory() && entry.name.startsWith('gsd-')) { @@ -9488,10 +9125,10 @@ function install(isGlobal, runtime = 'claude', options = {}) { // atomic-write temp files. It is safe to call before any writes have happened. // The full restoreCodexSnapshot() (defined inside the config block) additionally // handles config.toml, which is not yet touched at this point in the pipeline. - const _codexPreConfigRollback = !isCodex || isMinimalMode(_effectiveInstallMode) ? null : () => { + const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => { rollbackInstallerMigrations(); // skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install). - const _earlySkillsDir = path.join(targetDir, 'skills'); + const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local'); for (const skillName of codexPreInstallSkillNames) { const skillDirPath = path.join(_earlySkillsDir, skillName); const fileMap = codexPreInstallSkillContents.get(skillName); @@ -9643,7 +9280,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { // Artifact install dispatcher — routes to layout-driven path for all // skills-based runtimes (both full and minimal/core profiles); keeps - // back-compat paths for commands-based runtimes (OpenCode/Kilo/Gemini/ + // back-compat paths for commands-based runtimes (OpenCode/Kilo/ // Claude-local). // // installRuntimeArtifacts handles legacy migration + skill/agent staging @@ -9654,24 +9291,58 @@ function install(isGlobal, runtime = 'claude', options = {}) { // Hermes: writeHermesCategoryDescription (not a layout kind) // Cline global: skills emitted via layout; .clinerules still written below (#782) // Cline local: no skills (only .clinerules) — falls through to cline-rules surface - // Gemini: conflict-detection logic (not expressible in layout) - // OpenCode/Kilo: copyFlattenedCommands (frontmatter conversion not in commandsKind) // Claude local: copyWithPathReplacement + stale-skills cleanup // Layout-driven path for all skills-based runtimes (full and minimal modes). // applyRuntimeContentRewritesInPlace (called inside installRuntimeArtifacts) // handles per-runtime path + branding rewrites, including Qwen/Hermes. // Cline global: emit skills to ~/.cline/skills/ (Cline >= v3.48.0 — #782). - const _isSkillsRuntime = isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || - isAugment || isTrae || isCodebuddy || isQwen || isHermes || - isKimi || - (runtime === 'claude' && isGlobal) || - (isCline && isGlobal); + // Descriptor-driven (ADR-1016 / ADR-1239): a runtime takes the layout-driven + // installRuntimeArtifacts path when its scoped artifactLayout is non-empty + // (it declares any skills/commands/agents/kimi-agents kind for this scope). + // This replaces the prior hardcoded `isCodex || isCopilot || ...` roster so a + // newly-added runtime with an artifact layout installs without a per-runtime + // branch — the add-a-host tax ADR-1239 Phase B retires. OpenCode/Kilo now + // route through this SAME path too: their hostBehaviors.combinedFamilyInstall + // flag makes installRuntimeArtifacts (in src/install-engine.cts) delegate to + // installOpencodeFamilyArtifacts for the combined commands+skills+native-plugin + // install (ADR-1239 / #2087), replacing the bespoke inline block this comment + // used to describe. Claude-local remains the one special-cased path + // (copyWithPathReplacement + stale-skills cleanup). + const _isSkillsRuntime = (() => { + if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086) + const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime]; + const layout = cap && cap.runtime && cap.runtime.artifactLayout; + if (!layout) return false; + const scopeLayout = isGlobal ? layout.global : layout.local; + return Array.isArray(scopeLayout) && scopeLayout.length > 0; + })(); if (_isSkillsRuntime) { // Layout-driven install for skills-based runtimes (full and minimal modes) const scope = isGlobal ? 'global' : 'local'; - installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile); + // ADR-1239 upgrade 3 / #2088: a kind may declare an alternate install `home` + // (e.g. Codex skills -> $HOME/.agents/skills) instead of the runtime's normal + // configDir. Resolve the ACTUAL on-disk skills root here, descriptor-driven + // (no isCodex check), so downstream sidecar-cleanup and post-install + // verification look in the right place regardless of which runtime declares + // an alternate home for its skills kind. + const _skillsRootDir = _resolveSkillsRootDir(runtime, targetDir, scope); + // ADR-1239 / #2086: drive install through the public Host-Integration Interface + // (imperative adapter). The adapter delegates to the SAME installRuntimeArtifacts + // engine call -> byte-identical output (gated by golden-install-parity). Fail-open + // to the engine directly if the composed-registry adapter can't load. + const _adapter = _runtimeAdapter(runtime); + if (_adapter) { + _adapter.install({ + configDir: targetDir, + scope, + resolvedProfile: _resolvedProfile, + resolveAttribution: getCommitAttribution, + }); + } else { + installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution); + } // #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed // gsd-* skill dirs. Prior installs wrote these files so Codex would show a @@ -9679,28 +9350,46 @@ function install(isGlobal, runtime = 'claude', options = {}) { // index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice // in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is // sufficient for Codex discovery. User-owned dirs are never touched. - if (isCodex) { - cleanupCodexSkillMetadataSidecars(path.join(targetDir, 'skills')); + if (_hostBehaviors(runtime).cleanupSkillSidecars) { + cleanupCodexSkillMetadataSidecars(_skillsRootDir); + } + + // ADR-1239 split-home migration: when a runtime's skills kind moved to an + // alternate `home` (e.g. Codex → ~/.agents/skills), pre-move installs left + // managed gsd-* skill dirs at the old configDir-rooted location + // (~/.codex/skills). Reinstalling here writes the new location but would + // otherwise orphan the old one — clean up the stale gsd-* dirs. + { + const _movedOldSkillsDir = _resolveMovedSkillsOldDir(runtime, targetDir, scope); + if (_movedOldSkillsDir) { + const migrated = cleanupMovedSkillsOldLocation(_movedOldSkillsDir, 'gsd-'); + if (migrated > 0) { + console.log(` ${green}✓${reset} Migrated ${migrated} skill dir(s) off the legacy ${_movedOldSkillsDir} location`); + } + } } // #1629 Finding B: Windsurf local only — remove legacy .devin/skills/gsd-* // dirs from pre-#1615 installs. #1615 moved Windsurf to .windsurf/workflows/ // but never cleaned up the old .devin/skills/ layout (#1085). User-owned // content is preserved (non-gsd- dirs, gsd-dev-preferences, symlinks). - if (isWindsurf && !isGlobal) { + // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into + // hostBehaviors.legacyDevinSkillsCleanup (windsurf is the only runtime that + // declares it, so this is byte-parity). + if (_hostBehaviors(runtime).legacyDevinSkillsCleanup && !isGlobal) { const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd()); if (removedCount > 0) { console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/gsd-* dir(s) (pre-#1615 Windsurf layout)`); } } - // Hermes only: write DESCRIPTION.md for the gsd/ category after layout install - if (isHermes) { + // Descriptor-driven (#2090): write DESCRIPTION.md for the gsd/ category after layout install + if (_hostBehaviors(runtime).writeCategoryDescription) { writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd')); } // Verify installed artifacts and report - if (isHermes) { + if (_hostBehaviors(runtime).reportSkillsCount) { const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd'); if (fs.existsSync(hermesSkillsDir)) { // Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd- names @@ -9714,7 +9403,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else { failures.push('skills/gsd/*'); } - } else if (isKimi) { + } else if (_hostBehaviors(runtime).verificationStyle === 'kimi') { const skillsDir = path.join(targetDir, 'skills'); const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml'); if (fs.existsSync(skillsDir)) { @@ -9734,7 +9423,11 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else { failures.push('agents/gsd.yaml'); } - } else if (isWindsurf) { + // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into + // hostBehaviors.verificationStyle === 'windsurf-workflows' (extends the + // same mechanism the 'kimi' verificationStyle branch above uses; windsurf + // is the only runtime that declares this value, so this is byte-parity). + } else if (_hostBehaviors(runtime).verificationStyle === 'windsurf-workflows') { if (isGlobal) { console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`); } else { @@ -9752,7 +9445,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } } else { - const skillsDir = path.join(targetDir, 'skills'); + const skillsDir = _skillsRootDir; if (fs.existsSync(skillsDir)) { const count = fs.readdirSync(skillsDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length; @@ -9780,24 +9473,9 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } - // Cursor only: also report the commands/ output (#785 — Cursor 1.6 slash commands) - if (isCursor) { - const commandsDir = path.join(targetDir, 'commands'); - if (fs.existsSync(commandsDir)) { - const cmdCount = fs.readdirSync(commandsDir) - .filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length; - if (cmdCount > 0) { - console.log(` ${green}✓${reset} Installed ${cmdCount} slash commands to commands/`); - } else { - failures.push('commands/gsd-*'); - } - } else { - failures.push('commands/gsd-*'); - } - } - - // CodeBuddy only: also report the commands/ output (#789 — slash commands) - if (isCodebuddy) { + // Descriptor-driven commands/ output report (#785 — Cursor 1.6 slash commands). + // Gated by hostBehaviors.reportCommandsDir, not a hardcoded `isCursor` branch (#2089). + if (_hostBehaviors(runtime).reportCommandsDir) { const commandsDir = path.join(targetDir, 'commands'); if (fs.existsSync(commandsDir)) { const cmdCount = fs.readdirSync(commandsDir) @@ -9812,87 +9490,23 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } } - } else if (isOpencode || isKilo) { - // OpenCode/Kilo: flat structure in command/ directory - const commandDir = path.join(targetDir, 'command'); - fs.mkdirSync(commandDir, { recursive: true }); - - // Copy commands/gsd/*.md as command/gsd-*.md (flatten structure) - const gsdSrc = _stageSkills(_commandsDir); - copyFlattenedCommands(gsdSrc, commandDir, 'gsd', pathPrefix, runtime); - if (verifyInstalled(commandDir, 'command/gsd-*')) { - const count = fs.readdirSync(commandDir).filter(f => f.startsWith('gsd-')).length; - console.log(` ${green}✓${reset} Installed ${count} commands to command/`); - } else { - failures.push('command/gsd-*'); - } - - // Also emit OpenCode-family skills (skills//SKILL.md). OpenCode and - // Kilo support native, on-demand skills in addition to flat commands — see - // resolveRuntimeArtifactLayout's opencode/kilo entries. Derive skills from - // the SAME staged command set (gsdSrc) so both surfaces match exactly. (#784) - const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix); - if (_skillCount > 0) { - console.log(` ${green}✓${reset} Installed ${_skillCount} skills to skills/`); - } else { - failures.push('skills/gsd-*'); - } - } else if (isCline) { + } else if (_hostBehaviors(runtime).localCommandsViaRules) { // Cline local install: rules-based only — commands are embedded in .clinerules (generated below). // No skills/commands directory needed for local installs. // Global installs are handled above by _isSkillsRuntime (#782). + // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into + // hostBehaviors.localCommandsViaRules. console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`); - } else if (isGemini) { - // #3037: when running --local --gemini and a GSD-managed user-scope - // command directory already exists at ~/.gemini/commands/gsd/, skip - // the local copy. Gemini conflict-detects by command name across - // scopes and renames every overlapping /gsd:* command to - // /workspace.gsd:* and /user.gsd:*, breaking the documented namespace. - // The user-scope install already provides the same commands, so the - // local copy adds zero value at the cost of namespace conflicts. - // - // CR #3041 (Major): the detection must be specific to PACKAGE-MANAGED - // GSD content, not just "directory is non-empty". A user who hand- - // dropped a single override (e.g. ~/.gemini/commands/gsd/my-override - // .toml) would otherwise be unable to run a local install at all. - // Detection rule: at least 3 of the canonical GSD command files - // ('help.toml', 'progress.toml', 'new-project.toml') must be present. - // These three ship in every GSD Gemini install (minimal mode included - // — they're in the core skill set per #2790's consolidation), and 3-of- - // 3 with that specific basename set is structurally impossible to - // produce by accident. - const homeGeminiGsd = path.join(os.homedir(), '.gemini', 'commands', 'gsd'); - const GSD_MANAGED_CANARIES = ['help.toml', 'progress.toml', 'new-project.toml']; - const userScopeHasGsd = - !isGlobal && - path.resolve(targetDir) !== path.resolve(path.join(os.homedir(), '.gemini')) && - fs.existsSync(homeGeminiGsd) && - GSD_MANAGED_CANARIES.every((f) => - fs.existsSync(path.join(homeGeminiGsd, f)) - ); - - if (userScopeHasGsd) { - console.log( - ` ${yellow}⚠${reset} Skipping commands/gsd/ for local install — GSD is already installed at user scope (${homeGeminiGsd}).` - ); - console.log( - ` Gemini conflict-detects across scopes and would rename every /gsd:* command to /workspace.gsd:* and /user.gsd:*.` - ); - console.log( - ` The user-scope install already provides /gsd:* commands in this project; no local copy is needed.` - ); - } else { - const commandsDir = path.join(targetDir, 'commands'); - fs.mkdirSync(commandsDir, { recursive: true }); - const gsdSrc = _stageSkills(_commandsDir); - const gsdDest = path.join(commandsDir, 'gsd'); - copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal); - if (verifyInstalled(gsdDest, 'commands/gsd')) { - console.log(` ${green}✓${reset} Installed commands/gsd`); - } else { - failures.push('commands/gsd'); - } - } + } else if (_hostBehaviors(runtime).pluginOnlyInstall) { + // pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /gsd command is + // registered programmatically by the native extension (pi/gsd.cjs → + // extensions/gsd.cjs, staged separately below) and dispatches in-process + // through the embedded gsd-core command-routing hub. pi has no host-read + // markdown surface (unlike Claude/OpenCode/etc., which scan commands/ or + // command/ directories), so writing flat gsd-.md files here would be + // dead weight the extension never reads. Skip the flat-commands fallback + // entirely for pluginOnlyInstall runtimes. + console.log(` ${green}✓${reset} pi: /gsd registered via native extension (no declarative command files)`); } else { // Claude Code local: flat gsd-.md layout — Claude Code registers // commands from .claude/commands/ using the filename stem as the command @@ -9963,12 +9577,24 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } + // Native-extension/plugin staging for runtimes OUTSIDE the layout-driven + // _isSkillsRuntime branch above (ADR-1239 / #2102 Stage 1: pi). OpenCode/Kilo + // already get their nativePlugin file from installOpencodeFamilyArtifacts + // (called inside the _isSkillsRuntime branch, since both declare a non-empty + // artifactLayout) — guard on `!_isSkillsRuntime` so this standalone call never + // double-stages their plugin file. A runtime like pi, whose artifactLayout is + // intentionally empty for both scopes (`_isSkillsRuntime` is false), still + // needs its declared hostBehaviors.nativePlugin file copied into targetDir. + if (!_isSkillsRuntime && _hostBehaviors(runtime).nativePlugin) { + _installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src); + } + // Copy gsd-core skill with path replacement // Preserve user-generated files before the wipe-and-copy so they survive re-install const skillSrc = path.join(src, 'gsd-core'); const skillDest = path.join(targetDir, 'gsd-core'); const savedGsdArtifacts = preserveUserArtifacts(skillDest, USER_OWNED_ARTIFACTS); - copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal); + copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir); restoreUserArtifacts(skillDest, savedGsdArtifacts); if (verifyInstalled(skillDest, 'gsd-core')) { console.log(` ${green}✓${reset} Installed workflow assets`); @@ -9976,6 +9602,36 @@ function install(isGlobal, runtime = 'claude', options = {}) { failures.push('gsd-core'); } + // Write the .gsd-source marker so runtime source resolution succeeds at + // runtime (#1477). The Claude-global skills layout ships gsd-core/{bin, + // contexts,references,templates,workflows} but NOT the commands/gsd source + // tree, and _runLegacyUninstallCleanup actively removes any commands/gsd/ + // for that scope — so findInstallSourceRoot's walk-up has nothing to find + // and /gsd-surface (list/status) throws. This is the writer half of the + // marker that runtime-artifact-layout.cjs's finders already read (the reader + // landed in #1476). It points at the package's own commands/gsd source. + // Scoped to the Claude-global layout (issue #1477) — the only install path + // that ships the skills layout without a commands/gsd source tree; every + // other runtime/scope deploys commands/gsd, so its walk-up already resolves + // and needs no marker. Guarded on source presence so a half-published + // package never writes a dangling marker. + if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) { + const gsdSourceCommands = path.join(src, 'commands', 'gsd'); + if (fs.existsSync(gsdSourceCommands)) { + try { + // ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename + // must resolve under targetDir (parity with the other descriptor-driven writes). + const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile); + fs.writeFileSync(_markerPath, gsdSourceCommands + '\n', 'utf8'); + } catch (err) { + // Non-fatal: install proceeds. But on the Claude-global layout walk-up + // also fails (no commands/gsd source tree), so a silent write failure + // still leaves /gsd-surface broken at runtime — warn so it's diagnosable. + console.warn(` ${yellow}!${reset} Could not write .gsd-source marker (${err.message}); /gsd-surface list/status may fail`); + } + } + } + // #1629 critical fix: Windsurf workflow wrappers (convertClaudeCommandToWindsurfWorkflow) // delegate to command bodies at /gsd-core/commands/gsd/${stem}.md via a // hardcoded @~/.claude/gsd-core/commands/gsd/ path that _applyRuntimeRewrites rewrites @@ -9984,11 +9640,15 @@ function install(isGlobal, runtime = 'claude', options = {}) { // this copy, every /gsd-* workflow in Cascade references a missing file and the LLM // cannot execute the command body. Surfaced by the #1629 regression test after the // original adversarial review of #1622 missed it. - if (isWindsurf && !isGlobal) { + // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into + // hostBehaviors.installsCommandBodiesForWorkflowDelegation (windsurf is the + // only runtime that declares it, so this is byte-parity — the #1629 fix + // itself is unchanged). + if (_hostBehaviors(runtime).installsCommandBodiesForWorkflowDelegation && !isGlobal) { const commandsSrc = path.join(src, 'commands', 'gsd'); const commandsDest = path.join(skillDest, 'commands', 'gsd'); if (fs.existsSync(commandsSrc)) { - copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal); + copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal, targetDir); console.log(` ${green}✓${reset} Installed command bodies to gsd-core/commands/gsd/ (workflow delegation targets)`); } } @@ -10030,29 +9690,59 @@ function install(isGlobal, runtime = 'claude', options = {}) { agentsSrc = _stageAgents(path.join(src, 'agents')); const agentsDest = path.join(targetDir, 'agents'); + // ADR-1235 §1: runtimes that have been migrated to the descriptor-driven agent + // path (installRuntimeArtifacts → convertedAgentsKind). The descriptor path + // applies path-rewrite + attribution + converter + normalize via + // stageAgentsForRuntimeWithConverter (with agentCtx pre-converter threading) in + // createRuntimeArtifactInstallPlan. Their agents are already written ABOVE + // (by installRuntimeArtifacts at line 8912), which also performs its own + // stale-file prune pass. The inline stale-removal + inline loop both skip them. + // Trivial group (cursor/windsurf/augment/trae/codebuddy) cut over together. + // #1575: copilot and antigravity cut over — copilot gets .agent.md filename + // rename via _copyStaged(runtime); antigravity uses scope-aware converter. + // #2092 Phase B Upgrade 1: qwen cut over — native .qwen/agents/*.md subagent + // projection via convertClaudeAgentToQwenAgent. Without this exclusion the + // legacy inline loop below deletes+re-copies qwen's agents RAW (bypassing the + // new converter entirely, since qwen has no dedicated branch in the inline + // loop's if/else-if chain — it would silently fall through to the generic + // brandingRewrites-only branch). + // cline remains excluded: rules-only local branch + local/global complication + // that the descriptor-driven path does not handle correctly. + const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy', 'copilot', 'antigravity', 'qwen', 'kimi']); + // Always remove stale gsd-* agents first so re-installing with // `--minimal` actually shrinks a previously-full install. // For Codex this also covers per-agent `.toml` files alongside the `.md` // sources so a full → minimal switch doesn't leave stale registrations. - if (fs.existsSync(agentsDest)) { + // Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes) and + // for pluginOnlyInstall runtimes (pi, ADR-1239 / #2102 Stage 1 — no agents/ + // dir is ever written for them, see the leading branch below). + if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && !_hostBehaviors(runtime).pluginOnlyInstall && fs.existsSync(agentsDest)) { for (const file of fs.readdirSync(agentsDest)) { if ( file.startsWith('gsd-') && - (file.endsWith('.md') || (isCodex && file.endsWith('.toml'))) + (file.endsWith('.md') || (_hostBehaviors(runtime).agentTomlFiles && file.endsWith('.toml'))) ) { fs.unlinkSync(path.join(agentsDest, file)); } } } - if (isKimi) { - console.log(` ${dim}↳${reset} Kimi custom agent YAML/prompt artifacts were installed via runtime artifact layout`); + if (_hostBehaviors(runtime).pluginOnlyInstall) { + // pi (ADR-1239 / #2102 Stage 1): programmatic dispatch has no named-dispatch + // subagent toolkit (dispatch.subagentToolkit: "undocumented", no Agent-tool + // equivalent) and no host-read markdown surface — skip writing agents/ entirely. + console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`); + } else if (_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime)) { + // installRuntimeArtifacts already wrote agents + handles stale-file cleanup + // via its own prune pass. No further action needed. + console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`); } else if (isMinimalMode(_effectiveInstallMode)) { // Codex registers agents in `config.toml` via `[agents.gsd-*]` sections. // Without stripping them here, a full → minimal reinstall would leave the // runtime advertising the old full agent surface even though the agent // files are gone. Reuse the same helper that powers `--uninstall`. - if (isCodex) { + if (_hostBehaviors(runtime).tomlConfigInstall) { const codexConfigPath = path.join(targetDir, 'config.toml'); if (fs.existsSync(codexConfigPath)) { const existing = fs.readFileSync(codexConfigPath, 'utf8'); @@ -10079,15 +9769,21 @@ function install(isGlobal, runtime = 'claude', options = {}) { const bareDirRegex = /~\/\.claude\b/g; const bareHomeDirRegex = /\$HOME\/\.claude\b/g; const normalizedPathPrefix = pathPrefix.replace(/\/$/, ''); - if (!isCopilot && !isAntigravity) { - content = content.replace(dirRegex, pathPrefix); - content = content.replace(homeDirRegex, pathPrefix); - content = content.replace(bareDirRegex, normalizedPathPrefix); - content = content.replace(bareHomeDirRegex, normalizedPathPrefix); - } + // #2096: `&& !isAntigravity` dropped — antigravity is in + // _DESCRIPTOR_AGENTS_RUNTIMES above, so this whole branch is already + // unreachable for it; the path-rewrite skip for antigravity now lives + // in the descriptor-driven `applyAgentPathRewrites` (hostBehaviors.noPathRewrite). + // #2099: `if (!isCopilot)` guard dropped — copilot is ALSO in + // _DESCRIPTOR_AGENTS_RUNTIMES (line ~9564 above), so this whole + // `else if (fs.existsSync(agentsSrc))` branch is unreachable for it; + // isCopilot was therefore always false here, making the guard a no-op. + content = content.replace(dirRegex, pathPrefix); + content = content.replace(homeDirRegex, pathPrefix); + content = content.replace(bareDirRegex, normalizedPathPrefix); + content = content.replace(bareHomeDirRegex, normalizedPathPrefix); content = processAttribution(content, getCommitAttribution(runtime)); // Convert frontmatter for runtime compatibility (agents need different handling) - if (isOpencode) { + if (_hostBehaviors(runtime).frontmatterDialect === 'opencode') { // Resolve per-agent model for OpenCode agents. // Precedence: model_overrides[agent] > model_profile_overrides.opencode. > omit. // model_overrides (#2256): explicit per-agent override, highest precedence. @@ -10106,61 +9802,83 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride }); - } else if (isKilo) { - content = convertClaudeToKiloFrontmatter(content, { isAgent: true }); - } else if (isGemini) { - content = convertClaudeToGeminiAgent(content); - } else if (isCodex) { + } else if (_hostBehaviors(runtime).frontmatterDialect === 'kilo') { + // Resolve per-agent model for Kilo agents (#2093 UPGRADE 2; Kilo is an + // OpenCode fork with the same static-frontmatter model constraint). + // Precedence: model_overrides[agent] > model_profile_overrides.kilo. > omit. + // model_overrides (#2256): explicit per-agent override, highest precedence. + // model_profile_overrides (#2794): tier-based runtime resolver, same parity as OpenCode. + const _kiloAgentName = entry.name.replace(/\.md$/, ''); + const _kiloModelOverrides = readGsdEffectiveModelOverrides(targetDir); + let _kiloModelOverride = _kiloModelOverrides?.[_kiloAgentName] || null; + if (!_kiloModelOverride) { + // Fall back to tier-based resolution via model_profile_overrides.kilo.. + const _kiloRuntimeResolver = readGsdRuntimeProfileResolver(targetDir); + if (_kiloRuntimeResolver) { + const _kiloEntry = _kiloRuntimeResolver.resolve(_kiloAgentName); + if (_kiloEntry?.model) { + _kiloModelOverride = _kiloEntry.model; + } + } + } + content = convertClaudeToKiloFrontmatter(content, { isAgent: true, modelOverride: _kiloModelOverride }); + } else if (_hostBehaviors(runtime).frontmatterDialect === 'codex') { content = convertClaudeAgentToCodexAgent(content); - } else if (isCopilot) { - content = convertClaudeAgentToCopilotAgent(content, isGlobal); - } else if (isAntigravity) { - content = convertClaudeAgentToAntigravityAgent(content, isGlobal); - } else if (isCursor) { - content = convertClaudeAgentToCursorAgent(content); - } else if (isWindsurf) { - content = convertClaudeAgentToWindsurfAgent(content); - } else if (isAugment) { - content = convertClaudeAgentToAugmentAgent(content); - } else if (isTrae) { - content = convertClaudeAgentToTraeAgent(content); - } else if (isCodebuddy) { - content = convertClaudeAgentToCodebuddyAgent(content); - } else if (isCline) { + // #2099: `else if (isCopilot)` arm dropped — copilot is unreachable + // here (see the isCopilot-guard-drop comment above); its content + // conversion is applied pre-staging via the descriptor's + // artifactLayout.converter (runtime-artifact-layout.cts), independent + // of this legacy loop. + // #2100: `else if (isWindsurf)` arm dropped — windsurf is ALSO in + // _DESCRIPTOR_AGENTS_RUNTIMES (line ~9575 above), so this whole + // `else if (fs.existsSync(agentsSrc))` branch is unreachable for it; + // isWindsurf was therefore always false here, making the arm dead. + // Its content conversion is applied pre-staging via the descriptor's + // artifactLayout.converter (convertClaudeAgentToWindsurfAgent), + // independent of this legacy loop. + } else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') { + // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into + // hostBehaviors.frontmatterDialect === 'cline'. content = convertClaudeAgentToClineAgent(content); - } else if (isQwen) { - content = content.replace(/CLAUDE\.md/g, 'QWEN.md'); - content = content.replace(/\bClaude Code\b/g, 'Qwen Code'); - content = content.replace(/\.claude\//g, '.qwen/'); - } else if (isHermes) { - content = content.replace(/CLAUDE\.md/g, 'HERMES.md'); - content = content.replace(/\bClaude Code\b/g, 'Hermes Agent'); - content = content.replace(/\.claude\//g, '.hermes/'); + } else if (_hostBehaviors(runtime).brandingRewrites) { + // Descriptor-driven (ADR-1239 / #2092): folded from separate + // `isQwen` / hermes-hardcoded branches into a single read of + // runtime.hostBehaviors.brandingRewrites (qwen -> QWEN.md/Qwen + // Code/.qwen/, hermes -> HERMES.md/Hermes Agent/.hermes/). + const _b = _hostBehaviors(runtime).brandingRewrites; + content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']); + content = content.replace(/\bClaude Code\b/g, _b['Claude Code']); + content = content.replace(/\.claude\//g, _b['.claude/']); } // #443 — Inject `effort:` into the Claude .md frontmatter ONLY. - // Gemini/OpenCode/Qwen/Hermes also produce .md files but break on + // OpenCode/Qwen/Hermes also produce .md files but break on // unknown frontmatter keys (the repo bans skills:/permissionMode: for // the same reason — see tests/agent-frontmatter.test.cjs). // Claude Code reads per-subagent `effort:` frontmatter (anthropics/claude-code #31536). // Injection is per-runtime at install time because the canonical source - // agents/*.md must stay Gemini-safe (no effort: key in source). - if (runtime === 'claude') { + // agents/*.md must stay runtime-safe (no effort: key in source). + if ((_hostBehaviors(runtime).agentFrontmatterExtensions || []).includes('effort')) { const _effortCfg = readGsdEffectiveEffortConfig(targetDir); const _agentName = entry.name.replace(/\.md$/, ''); const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName); - const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime('claude', _universalEffort).value; + const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(runtime, _universalEffort).value; content = injectEffortFrontmatter(content, _renderedEffort); const _disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[_agentName]; if (_disallowedTools) content = injectDisallowedToolsFrontmatter(content, _disallowedTools); } // #3677 — normalize retired `/gsd:` colon refs in the agent body // to the canonical hyphen form `/gsd-` for hyphen-`name:` - // runtimes (claude / qwen / hermes). Self-converting runtimes and - // Gemini are skipped by the predicate — see + // runtimes (claude / qwen / hermes). Self-converting and + // colon-canonical runtimes are skipped by the predicate — see // shouldNormalizeHyphenNamespaceInAgentBody above. Mirrors the // SKILL.md-body fix shipped via #3629. content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames()); - const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name; + // #2099: `isCopilot ? ... : entry.name` ternary dropped — copilot is + // unreachable here (see the isCopilot-guard-drop comment above), so + // the ternary always evaluated to entry.name in practice; its + // .agent.md suffix is applied by the descriptor-driven fold in + // src/install-engine.cts (hostBehaviors.agentFileExtension). + const destName = entry.name; fs.writeFileSync(path.join(agentsDest, destName), content); } } @@ -10192,19 +9910,41 @@ function install(isGlobal, runtime = 'claude', options = {}) { failures.push('VERSION'); } - if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) { + // Reusable: copy hooks/dist/ + hooks/lib/ into destRootDir, writing the + // CommonJS package.json marker alongside them. Used below for the generic + // configDir install path (guarded by hostBehaviors.skipSharedHooksInstall), + // and — since #2095 — for Kimi's OWN native hook-install root (~/.kimi, + // resolved by resolveKimiHooksTomlDir), a directory entirely separate from + // Kimi's configDir/agents-root. Kimi's contract forbids hooks/ or + // package.json under its generic Agent-Skills root (see + // capabilities/kimi/capability.json hostBehaviors.skipSharedHooksInstall + // and the kimi-hooks-toml branch further below), so its shared-hooks bundle + // is installed into its own root via this same helper instead. + // Returns false when hooks/dist/ exists but failed to verify post-copy (a + // genuine failure the caller should surface); true otherwise (including + // when hooks/dist/ is absent from the package — nothing to verify). + function installSharedHooksBundle(destRootDir) { + // destRootDir already exists for the generic call site (targetDir — created + // earlier in install() by the skills/agents writes above). It does NOT yet + // exist for kimi's call site (~/.kimi, resolved by resolveKimiHooksTomlDir): + // a fresh install has never created that dir before. mkdirSync recursive is + // a safe no-op when the dir is already present. + fs.mkdirSync(destRootDir, { recursive: true }); + // Write package.json to force CommonJS mode for GSD scripts // Prevents "require is not defined" errors when project has "type": "module" // Node.js walks up looking for package.json - this stops inheritance from project - const pkgJsonDest = path.join(targetDir, 'package.json'); + const pkgJsonDest = path.join(destRootDir, 'package.json'); fs.writeFileSync(pkgJsonDest, '{"type":"commonjs"}\n'); console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`); + let hooksOk = true; + // Copy hooks from dist/ (bundled with dependencies) // Template paths for the target runtime (replaces '.claude' with correct config dir) const hooksSrc = path.join(src, 'hooks', 'dist'); if (fs.existsSync(hooksSrc)) { - const hooksDest = path.join(targetDir, 'hooks'); + const hooksDest = path.join(destRootDir, 'hooks'); fs.mkdirSync(hooksDest, { recursive: true }); const hookEntries = fs.readdirSync(hooksSrc); const configDirReplacement = getConfigDirFromHome(runtime, isGlobal); @@ -10217,13 +9957,15 @@ function install(isGlobal, runtime = 'claude', options = {}) { content = content.replace(/'\.claude'/g, configDirReplacement); content = content.replace(/\/\.claude\//g, `/${getDirName(runtime)}/`); content = content.replace(/\.claude\//g, `${getDirName(runtime)}/`); - if (isQwen) { - content = content.replace(/CLAUDE\.md/g, 'QWEN.md'); - content = content.replace(/\bClaude Code\b/g, 'Qwen Code'); - } - if (isHermes) { - content = content.replace(/CLAUDE\.md/g, 'HERMES.md'); - content = content.replace(/\bClaude Code\b/g, 'Hermes Agent'); + // Descriptor-driven (ADR-1239 / #2092): folded from separate + // `isQwen` / hermes-hardcoded branches into a single read of + // runtime.hostBehaviors.brandingRewrites. This site only + // rewrites the two brand-name keys (no `.claude/` here — the + // config-dir replace above already handled path fragments). + const _b2 = _hostBehaviors(runtime).brandingRewrites; + if (_b2) { + content = content.replace(/CLAUDE\.md/g, _b2['CLAUDE.md']); + content = content.replace(/\bClaude Code\b/g, _b2['Claude Code']); } // #376: rewrite gsd: → gsd- for hyphen-namespace runtimes if (shouldNormalizeHyphenNamespaceInAgentBody(runtime)) { @@ -10276,23 +10018,63 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } } else { - failures.push('hooks'); + hooksOk = false; } } + + // Gate hooks/lib/ install on the same set of runtimes that receive hooks/. + // Codex/Copilot/Cursor/Windsurf/Trae/Cline/Kilo do not use the shared + // hooks/lib/ helpers (Cursor uses standalone .js hook scripts registered + // via hooks.json — gated descriptor-driven via + // hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise #2090; Kilo + // likewise #2093; Trae likewise #2094; Codex uses hooks.json directly; + // the others skip hooks entirely); Kilo and ZCode also skip hooks entirely + // (hooksSurface:'none' with no plugin surface — #1821). None of the + // excluded runtimes must receive the hooks/lib/ helpers — otherwise the + // Codex comment downstream ("we deliberately do *not* copy hooks/lib/ for + // Codex") is contradicted in practice. (Gating lives at the call sites + // below; this helper itself only checks source presence.) + const hooksLibSrc = path.join(src, 'hooks', 'lib'); + if (fs.existsSync(hooksLibSrc)) { + const hooksLibDest = path.join(destRootDir, 'hooks', 'lib'); + fs.mkdirSync(hooksLibDest, { recursive: true }); + copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES); + console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (git-cmd, graphify-rebuild, ...)`); + } + + return hooksOk; } - // Gate hooks/lib/ install on the same runtimes that receive hooks (see line ~8702). - // Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared hooks/lib/ helpers - // (Cursor uses standalone .js hook scripts registered via hooks.json; Codex uses - // hooks.json directly; the others skip hooks entirely), so they must not receive - // the hooks/lib/ helpers — otherwise the Codex comment downstream - // ("we deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice. - const hooksLibSrc = path.join(src, 'hooks', 'lib'); - if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && fs.existsSync(hooksLibSrc)) { - const hooksLibDest = path.join(targetDir, 'hooks', 'lib'); - fs.mkdirSync(hooksLibDest, { recursive: true }); - copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES); - console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (git-cmd, graphify-rebuild, ...)`); + // #1821: Kilo and ZCode declare hooksSurface:'none' AND have no plugin surface, + // so the staged hook scripts are dead weight for them — exclude both here. + // OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded: + // its native plugin adapter (#1914, installed above under plugins/gsd-core.js) + // spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both + // them and the CommonJS package.json marker written below. + // #2089: Cursor's exclusion is now descriptor-driven via + // hostBehaviors.skipSharedHooksInstall (was hardcoded !isCursor). + // #2090: Cline's exclusion is likewise descriptor-driven (cline declares + // skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed. + // #2093: Kilo's exclusion is likewise descriptor-driven (kilo declares + // skipSharedHooksInstall:true) — the redundant `&& !isKilo` was removed. + // #2094: Trae's exclusion is likewise descriptor-driven (trae declares + // skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed. + // #2101: ZCode's exclusion is likewise descriptor-driven (zcode declares + // skipSharedHooksInstall:true) — the redundant `&& !isZcode` was removed. + // #2095: Kimi's exclusion is likewise descriptor-driven (kimi declares + // skipSharedHooksInstall:true) — kimi's shared hooks/ + package.json marker + // are instead installed into its OWN native hook root (~/.kimi, resolved by + // resolveKimiHooksTomlDir) via installSharedHooksBundle, at the + // kimi-hooks-toml branch further below — never under the generic + // Agent-Skills configDir GSD installs skills/agents into for kimi. + // #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares + // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed. + // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares + // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed. + if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) { + if (!installSharedHooksBundle(targetDir)) { + failures.push('hooks'); + } } // Install scripts/changeset/ and scripts/lib/ into /scripts/ @@ -10372,6 +10154,31 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } + // Copy scripts/gen-capability-registry.cjs + scripts/gen-loop-host-contract.cjs — + // required by gsd-core/bin/lib/capability-loader.cjs at overlay-composition time via + // require('../../../scripts/gen-capability-registry.cjs') (which itself requires + // gen-loop-host-contract.cjs). Without these, the loader's never-crash invariant + // discards EVERY third-party capability overlay and silently falls back to the frozen + // first-party registry, so installed capabilities are inert (#1920). Same class of + // gap as #1223 (fix-slash-commands.cjs) and copied unconditionally for the same reason: + // any runtime that installs gsd-core/ needs the capability system to compose. + { + const capGenDestDir = path.join(targetDir, 'scripts'); + fs.mkdirSync(capGenDestDir, { recursive: true }); + for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) { + const genSrc = path.join(src, 'scripts', gen); + const genDest = path.join(capGenDestDir, gen); + if (!fs.existsSync(genSrc)) { + failures.push(`scripts/${gen} (source missing from package — reinstall from npm)`); + } else { + fs.copyFileSync(genSrc, genDest); + if (!verifyFileInstalled(genDest, `scripts/${gen}`)) { + failures.push(`scripts/${gen}`); + } + } + } + } + // Remove legacy get-shit-done-cc artifacts and stale update caches (#607). // cleanupLegacyGsdCc handles both the legacy shared cache and the per-package // cache (formerly an inline unlinkSync here). A cleanup failure must never @@ -10390,14 +10197,14 @@ function install(isGlobal, runtime = 'claude', options = {}) { } // Write file manifest for future modification detection - writeManifest(targetDir, runtime, { mode: _effectiveInstallMode }); + writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' }); console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`); // Report any backed-up local patches reportLocalPatches(targetDir, runtime); // Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped) - if (runtime !== 'claude') { + if (!_hostBehaviors(runtime).ownsClaudePaths) { const leakedPaths = []; // Only scan files that were written by this install (manifest-tracked). // Scanning the entire targetDir can match user-authored content that @@ -10533,7 +10340,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { // (copyCommandsAsCodexSkills removes pre-existing gsd-* dirs before re-writing) // are restored even when they are absent from disk at rollback time (#3245 CR). // • Dirs that did not pre-exist: remove entirely. - const _rollbackSkillsDir = path.join(targetDir, 'skills'); + const _rollbackSkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local'); // Pass 1 — restore snapshot entries (may be absent from disk if deleted mid-install). for (const skillName of codexPreInstallSkillNames) { const skillDirPath = path.join(_rollbackSkillsDir, skillName); @@ -10644,7 +10451,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { // Re-write the manifest now that .toml agent files exist on disk. // The initial writeManifest call (before Codex config generation) could // not include agents/gsd-*.toml because those files did not yet exist. - writeManifest(targetDir, runtime, { mode: _effectiveInstallMode }); + writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' }); } else { console.log(` ${dim}↳${reset} Skipping Codex agent config generation (minimal install)`); } @@ -10784,24 +10591,23 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } - // ── Codex extended hook events (#772) ──────────────────────────────── - // Codex CLI stabilised a full hook-event set in rust-v0.137.0. Register - // three new high-value lifecycle events — all routed through - // gsd-context-monitor.js so context-headroom warnings surface at: - // SubagentStart — subagent session open (environment / agent-name aware) - // Stop — model stop / session final-response moment - // PostToolUse — after each tool invocation (mirrors Claude baseline) - // - // Note: UserPromptSubmit is NOT wired — gsd-prompt-guard exits unless - // tool_name is Write|Edit (PreToolUse payload shape), so it would be a - // silent no-op for the UserPromptSubmit payload. Registration deferred - // to a follow-on issue. + // ── Codex extended hook events (#772, #2088) ───────────────────────── + // Codex CLI stabilised a full hook-event set in rust-v0.137.0. GSD + // registers CODEX_EXTENDED_HOOK_EVENTS (#2088 adds the 6 documented + // events beyond the original #772 three) — all routed through + // gsd-context-monitor.js so context-headroom warnings surface at each + // lifecycle point: SubagentStart/SubagentStop (subagent open/close), + // Stop (final-response), PreToolUse/PostToolUse (tool boundaries), + // PermissionRequest (approval prompts), Pre/PostCompact (context + // compaction), and UserPromptSubmit (per-turn context injection). The + // context-monitor script decides per-payload what to do; unregistered + // events simply never fire. // // Guard: only register when the context-monitor file exists and the node // runner is available — same guards as the SessionStart path above. const contextMonitorFile = path.join(targetDir, 'hooks', 'gsd-context-monitor.js'); if (codexNodeRunner && fs.existsSync(contextMonitorFile)) { - for (const codexEvent of ['SubagentStart', 'Stop', 'PostToolUse']) { + for (const codexEvent of CODEX_EXTENDED_HOOK_EVENTS) { const eventWrite = ensureCodexHooksJsonEvent(targetDir, codexEvent, { absoluteRunner: codexNodeRunner, platform: process.platform, @@ -10813,7 +10619,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } } else if (!codexNodeRunner) { - console.warn(` ${yellow}⚠${reset} Skipped Codex SubagentStart/Stop/PostToolUse hook registration — Node runner unavailable.`); + console.warn(` ${yellow}⚠${reset} Skipped Codex extended hook-event registration — Node runner unavailable.`); } // ── end Codex extended hook events ──────────────────────────────────── } @@ -10878,22 +10684,98 @@ function install(isGlobal, runtime = 'claude', options = {}) { } if (plan.installSurface === 'cursor-hooks-json') { - // #777: Cursor v2.4+ supports hooks.json. Register sessionStart + postToolUse. - // Hook scripts are copied to /hooks/ and referenced by hooks.json. - const cursorHookResult = writeCursorHooksJson(targetDir, src, {}); + // ADR-1239 / #2089: Cursor hooks.json driven by the descriptor-managed hook-bus + // adapter. Registers all 6 managed events (sessionStart, postToolUse, preToolUse, + // stop, subagentStart, subagentStop) via runtime-hooks-surface.cts, which reads + // the event list from the descriptor-driven adapter module. + const cursorHookResult = writeCursorHooksJson(targetDir, src, { + managedHookEvents: _hostBehaviors(runtime).managedHookEvents, + }); if (cursorHookResult.changed) { - console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse)`); + console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse, preToolUse, stop, subagentStart, subagentStop)`); } else { console.log(` ${green}✓${reset} Cursor lifecycle hooks already up to date`); } - // Re-run the manifest pass so the hook scripts + hooks.json are hash-tracked. - writeManifest(targetDir, runtime, { mode: _effectiveInstallMode }); + // Re-run the manifest pass to capture any files the hooks-json write path + // produced. NOTE: hooks.json and the gsd-cursor-*.js scripts are NOT + // manifest-tracked (verified) — uninstall removes them explicitly via + // removeCursorHooksJson + its script list, and reconcile is idempotent. + // The re-run is retained for parity with the settings.json install path. + writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' }); persistActiveProfileMarker(); return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; } if (plan.installSurface === 'profile-marker-only') { - // Windsurf/Trae/Kimi use artifact-only surfaces — no config.toml or settings.json hooks needed. + // Windsurf/Trae use artifact-only surfaces — no config.toml or settings.json + // hooks needed. Kimi is also artifact-only for its INSTALL surface (skills + + // kimi-agents, no settings.json) but #2095 Upgrade 1 gives it its own + // independent hooksSurface: kimi's native config.toml [[hooks]] array, which + // lives outside targetDir entirely (resolveKimiHooksTomlDir resolves ~/.kimi, + // a sibling of targetDir's ~/.config/agents) — hence writing it here, inside + // this early-return, rather than requiring installSurface to change. + // + // GATED TO GLOBAL ONLY (belt-and-suspenders): kimi local installs already + // return early at the top of install() via hostBehaviors.localInstallDeferred, + // long before this point is ever reached — so `isGlobal` is always true here + // in practice. The explicit check documents that invariant and fails closed + // if that early-return is ever refactored away. + // + // Kimi's contract forbids hooks/ or package.json under its generic + // Agent-Skills configDir (targetDir) — capabilities/kimi/capability.json + // declares hostBehaviors.skipSharedHooksInstall:true, which excludes it from + // the shared installSharedHooksBundle(targetDir) call above. Kimi still needs + // those SAME hook scripts + the CommonJS package.json marker, but SELF- + // CONTAINED under its own native hook root instead — so install them there, + // and point buildHookCommand (via writeKimiHooksToml's second arg) at that + // same root so the generated [[hooks]] command paths reference + // ~/.kimi/hooks/