diff --git a/.changeset/3562-codex-install-skill-surface.md b/.changeset/3562-codex-install-skill-surface.md new file mode 100644 index 000000000..3f93b90b9 --- /dev/null +++ b/.changeset/3562-codex-install-skill-surface.md @@ -0,0 +1,6 @@ +--- +type: Fixed +pr: 3568 +--- + +**Codex global install now produces discoverable `$gsd-*` skill surface** — `npx get-shit-done-cc@latest --codex --global` was leaving Codex CLI users with `get-shit-done/workflows/*.md` and `agents/gsd-*` on disk but no `~/.codex/skills/gsd-*/SKILL.md` files, so Codex 0.130.0 silently exposed zero `$gsd-*` commands after restart. The installer had been bypassing skill generation under the assumption that Codex auto-discovers from workflow/agent files; that assumption does not hold for the current Codex CLI. Re-wired the existing `copyCommandsAsCodexSkills()` helper into the Codex install dispatch path so it produces the same skill-shape as the Claude / Copilot / Antigravity / Cursor / Windsurf / Augment / Trae installs already do (one `skills/gsd-/SKILL.md` per `commands/gsd/*.md`). Pre-existing user-owned non-`gsd-*` skill directories are preserved. Closes #3562. diff --git a/README.md b/README.md index 4275b2a0c..63766fc3a 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,8 @@ For the full configuration reference — all settings, git branching strategies, **Commands not showing up?** Restart your runtime after install. GSD installs to `~/.claude/skills/gsd-*/` (Claude Code), `~/.codex/skills/gsd-*/` (Codex), or the equivalent for your runtime. +**Codex users — minimum supported CLI version is `0.130.0`.** Codex CLI 0.130.0 ([release notes](https://github.com/openai/codex/releases/tag/rust-v0.130.0)) removed extra-skill-roots discovery via [openai/codex#21485](https://github.com/openai/codex/pull/21485); from that version onward Codex discovers skills from standard roots (including `~/.codex/skills//SKILL.md`). GSD installs there directly. Earlier Codex CLI versions may still discover additional roots, which can surface duplicate `gsd-*` entries (one from extra-roots discovery, one from `~/.codex/skills/`); restart Codex after install and either upgrade or accept the duplicate listing. + **Something broken?** Re-run the installer — it's idempotent: ```bash npx get-shit-done-cc@latest diff --git a/bin/install.js b/bin/install.js index 2e920fcb4..fc0d73711 100755 --- a/bin/install.js +++ b/bin/install.js @@ -8114,23 +8114,27 @@ function install(isGlobal, runtime = 'claude', options = {}) { failures.push('command/gsd-*'); } } else if (isCodex) { + // Codex CLI (0.130.0 at time of #3562) does NOT auto-discover commands + // from get-shit-done/workflows/*.md or agents/*.md. It only registers + // commands from skills//SKILL.md. The earlier "Codex discovers + // official skills directly" branch left users with workflows on disk and + // no $gsd-* entrypoints. Regenerate the skill surface the same way the + // other runtimes do — copyCommandsAsCodexSkills() rewrites each + // commands/gsd/*.md as ~/.codex/skills/gsd-/SKILL.md and converts + // Claude-flavored command frontmatter into Codex skill frontmatter. const skillsDir = path.join(targetDir, 'skills'); - // Codex now discovers repo/user/admin/system skills from .agents/skills and - // warns if a layer mixes redundant hook/skill representations. Legacy - // gsd-* copies under ~/.codex/skills are therefore removed and no longer - // regenerated. - let removedLegacyCodexSkills = 0; + const gsdSrc = _stageSkills(_commandsDir); + copyCommandsAsCodexSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime); if (fs.existsSync(skillsDir)) { - for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) { - if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue; - fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true }); - removedLegacyCodexSkills += 1; + const count = fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length; + if (count > 0) { + console.log(` ${green}✓${reset} Installed ${count} skills to skills/`); + } else { + failures.push('skills/gsd-*'); } - } - if (removedLegacyCodexSkills > 0) { - console.log(` ${green}✓${reset} Removed ${removedLegacyCodexSkills} legacy Codex gsd-* skill copies from skills/`); } else { - console.log(` ${dim}↳${reset} Skipped Codex skill-copy generation (Codex discovers official skills directly)`); + failures.push('skills/gsd-*'); } } else if (isCopilot) { const skillsDir = path.join(targetDir, 'skills'); diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 73c7d8a0a..6ffe77e70 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -968,6 +968,10 @@ The `dynamic_routing` block is **disabled by default** — `enabled: false` (or ### Non-Claude Runtimes (Codex, OpenCode, Gemini CLI, Kilo) +> **Codex CLI minimum supported version: `0.130.0`** (issue [#3562](https://github.com/gsd-build/get-shit-done/issues/3562)). +> +> [Codex CLI 0.130.0](https://github.com/openai/codex/releases/tag/rust-v0.130.0) (released 2026-05-08) removed extra-skills-roots discovery via [openai/codex#21485](https://github.com/openai/codex/pull/21485). From this version forward, Codex CLI only scans `~/.codex/skills//SKILL.md`, `/.codex/skills/`, and registered plugin roots for invocable skills. GSD installs the `$gsd-*` surface as `~/.codex/skills/gsd-/SKILL.md` so commands resolve after a Codex restart. Earlier Codex CLI versions can show a duplicate listing (the legacy extra-roots scan plus the user-root copies) — restart Codex and either upgrade to ≥ 0.130.0 or accept the duplicates until you do. + When GSD is installed for a non-Claude runtime, the installer automatically sets `resolve_model_ids: "omit"` in `~/.gsd/defaults.json`. This causes GSD to return an empty model parameter for all agents, so each agent uses whatever model the runtime is configured with. No additional setup is needed for the default case. If you want different agents to use different models, use `model_overrides` with fully-qualified model IDs that your runtime recognizes: diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 7d1fa0f1d..695f767dc 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -1219,6 +1219,12 @@ For the full audit, harness reference, and the composition note with `model_prof ### Using Non-Claude Runtimes (Codex, OpenCode, Gemini CLI, Kilo) +> **Codex CLI minimum supported version: `0.130.0`** (issue [#3562](https://github.com/gsd-build/get-shit-done/issues/3562)). +> +> Codex CLI [0.130.0](https://github.com/openai/codex/releases/tag/rust-v0.130.0) (released 2026-05-08) removed extra-skills-roots discovery via [openai/codex#21485](https://github.com/openai/codex/pull/21485). From that version onward, Codex only discovers commands from `~/.codex/skills//SKILL.md` (user root), `/.codex/skills/` (cwd root), and registered plugin roots. The GSD installer writes `~/.codex/skills/gsd-/SKILL.md` directly so `$gsd-help`, `$gsd-new-project`, etc. are discoverable after restart. +> +> **Earlier Codex CLI versions** (pre-0.130.0) had additional skill-root scanning that discovered the GSD agent/workflow files in alternate locations. GSD still installs the `~/.codex/skills/gsd-*` copies on those versions, which can show a duplicate listing alongside the legacy auto-discovered surface — restart Codex after install and either upgrade to ≥ 0.130.0 or accept the duplicate entries until you do. + If you installed GSD for a non-Claude runtime, the installer already configured model resolution so all agents use the runtime's default model. No manual setup is needed. Specifically, the installer sets `resolve_model_ids: "omit"` in your config, which tells GSD to skip Anthropic model ID resolution and let the runtime choose its own default model. To assign different models to different agents on a non-Claude runtime, add `model_overrides` to `.planning/config.json` with fully-qualified model IDs that your runtime recognizes: diff --git a/tests/bug-3427-3433-codex-install-shape.test.cjs b/tests/bug-3427-3433-codex-install-shape.test.cjs index 6d4c5d60e..e4fd522b8 100644 --- a/tests/bug-3427-3433-codex-install-shape.test.cjs +++ b/tests/bug-3427-3433-codex-install-shape.test.cjs @@ -10,7 +10,7 @@ const crypto = require('node:crypto'); const { execFileSync } = require('node:child_process'); const { install, uninstall, parseTomlToObject } = require('../bin/install.js'); -const { createTempDir, cleanup } = require('./helpers.cjs'); +const { createTempDir, cleanup, parseFrontmatter } = require('./helpers.cjs'); const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist'); const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js'); @@ -55,7 +55,9 @@ describe('#3427 + #3433 — Codex installer avoids duplicate skills and mixed ho cleanup(tmpRoot); }); - test('removes legacy gsd-* copies from ~/.codex/skills and does not regenerate them', () => { + test('regenerates managed gsd-* skill copies and preserves unrelated user skills (#3562 reverses prior #3427/#3433 behaviour)', () => { + // Stale legacy body — fresh install must overwrite this so Codex sees the + // current SKILL.md, not whatever was last on disk. const legacySkillBody = '# old managed\n'; fs.mkdirSync(path.join(codexHome, 'skills', 'gsd-help'), { recursive: true }); fs.writeFileSync(path.join(codexHome, 'skills', 'gsd-help', 'SKILL.md'), legacySkillBody); @@ -77,7 +79,16 @@ describe('#3427 + #3433 — Codex installer avoids duplicate skills and mixed ho ? fs.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : []; - assert.equal(entries.some((name) => name.startsWith('gsd-')), false); + // #3562: $gsd-* commands are discoverable only when skills/gsd-*/SKILL.md + // exists. The installer must regenerate (not remove) the managed gsd-* + // directories. + assert.equal(entries.includes('gsd-help'), true); + const refreshedBody = fs.readFileSync(path.join(skillsDir, 'gsd-help', 'SKILL.md'), 'utf8'); + assert.notEqual(refreshedBody, legacySkillBody, 'stale legacy body must be overwritten'); + const frontmatter = parseFrontmatter(refreshedBody); + assert.equal(frontmatter.name, 'gsd-help', 'refreshed SKILL.md frontmatter must declare name: gsd-help'); + + // Unrelated user skills are preserved — the regen scope is `gsd-*` only. assert.equal(entries.includes('custom-user-skill'), true); }); diff --git a/tests/bug-3562-codex-install-skill-surface.test.cjs b/tests/bug-3562-codex-install-skill-surface.test.cjs new file mode 100644 index 000000000..a0875922c --- /dev/null +++ b/tests/bug-3562-codex-install-skill-surface.test.cjs @@ -0,0 +1,120 @@ +'use strict'; + +process.env.GSD_TEST_MODE = '1'; + +/** + * Regression test for bug #3562 — Codex global install must create a + * discoverable $gsd-* skill surface. + * + * Codex CLI 0.130.0 (the version in the issue report) does NOT auto-discover + * commands from get-shit-done/workflows/*.md or agents/*.md. It only registers + * commands from skills//SKILL.md. Prior installer logic ("Codex now + * discovers official skills from .agents/skills") was based on an assumption + * that does not match the shipping Codex CLI behavior, leaving users with + * workflows on disk and no $gsd-* entrypoints after `npx get-shit-done-cc + * --codex --global`. + * + * Fix: re-wire copyCommandsAsCodexSkills() back into the install dispatch path + * so the same skill-shape that Claude / Copilot / Antigravity / Cursor / + * Windsurf / Augment / Trae installs produce is also produced for Codex. + */ + +const { describe, test, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); + +const { install } = require('../bin/install.js'); +const { createTempDir, cleanup, parseFrontmatter } = require('./helpers.cjs'); + +const HOOKS_DIST = path.join(__dirname, '..', 'hooks', 'dist'); +const BUILD_HOOKS_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js'); + +function withCodexHome(codexHome, fn) { + const prev = process.env.CODEX_HOME; + process.env.CODEX_HOME = codexHome; + try { + return fn(); + } finally { + if (prev == null) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = prev; + } +} + +describe('#3562 — Codex install produces discoverable $gsd-* skill surface', { concurrency: false }, () => { + let tmpRoot; + let codexHome; + + beforeEach(() => { + if (!fs.existsSync(HOOKS_DIST) || fs.readdirSync(HOOKS_DIST).length === 0) { + execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' }); + } + tmpRoot = createTempDir('gsd-3562-'); + codexHome = path.join(tmpRoot, '.codex'); + fs.mkdirSync(codexHome, { recursive: true }); + }); + + afterEach(() => { + cleanup(tmpRoot); + }); + + test('global install creates skills/gsd-help/SKILL.md', () => { + withCodexHome(codexHome, () => install(true, 'codex')); + + const skillPath = path.join(codexHome, 'skills', 'gsd-help', 'SKILL.md'); + assert.ok( + fs.existsSync(skillPath), + `Codex install must create ${skillPath} so $gsd-help is discoverable. ` + + 'Without this, Codex CLI 0.130.0 does not expose any $gsd-* command.', + ); + }); + + test('SKILL.md content has frontmatter expected by Codex skill discovery', () => { + withCodexHome(codexHome, () => install(true, 'codex')); + + const skillPath = path.join(codexHome, 'skills', 'gsd-help', 'SKILL.md'); + assert.ok(fs.existsSync(skillPath), 'precondition: SKILL.md exists'); + + const content = fs.readFileSync(skillPath, 'utf8'); + const frontmatter = parseFrontmatter(content); + assert.equal(frontmatter.name, 'gsd-help', 'SKILL.md frontmatter must declare name: gsd-help so $gsd-help resolves'); + }); + + test('multiple core $gsd-* skills are produced (not just gsd-help)', () => { + withCodexHome(codexHome, () => install(true, 'codex')); + + const skillsDir = path.join(codexHome, 'skills'); + assert.ok(fs.existsSync(skillsDir), 'skills/ directory must exist after install'); + + const gsdSkills = fs + .readdirSync(skillsDir, { withFileTypes: true }) + .filter((e) => e.isDirectory() && e.name.startsWith('gsd-')) + .map((e) => e.name); + + // Lower bound — exact count depends on the current command surface. The + // commands/gsd/ directory holds dozens of *.md files; expecting more than + // 10 generated skills is a conservative floor that catches "we generated + // nothing" or "we only generated one accidentally" regressions. + assert.ok( + gsdSkills.length >= 10, + `Expected >= 10 generated gsd-* skill directories, found ${gsdSkills.length}: ${gsdSkills.join(', ')}`, + ); + }); + + test('install preserves existing user skills (does not remove unrelated dirs)', () => { + fs.mkdirSync(path.join(codexHome, 'skills', 'custom-user-skill'), { recursive: true }); + fs.writeFileSync( + path.join(codexHome, 'skills', 'custom-user-skill', 'SKILL.md'), + '---\nname: custom-user-skill\n---\n# user skill\n', + ); + + withCodexHome(codexHome, () => install(true, 'codex')); + + const userSkill = path.join(codexHome, 'skills', 'custom-user-skill', 'SKILL.md'); + assert.ok( + fs.existsSync(userSkill), + 'Codex install must preserve existing non-gsd user skill directories', + ); + }); +}); diff --git a/tests/install-minimal-all-runtimes.test.cjs b/tests/install-minimal-all-runtimes.test.cjs index b3ea99a70..5e5495ad0 100644 --- a/tests/install-minimal-all-runtimes.test.cjs +++ b/tests/install-minimal-all-runtimes.test.cjs @@ -180,8 +180,12 @@ function expectedSkillSet() { } function expectedManifestSkillSet(runtime) { - // Codex no longer materializes gsd-* skill files in minimal mode. - if (runtime === 'codex') return new Set(); + // Codex CLI 0.130.0 does not auto-discover commands from workflow / agent + // files (#3562) — it only registers commands from skills//SKILL.md. + // Codex installs therefore materialize the same minimal-allowlist skill + // surface as the other runtimes; the prior "Codex discovers official + // skills directly" assumption (which led to an empty Codex skill set + // here) does not hold in practice. return expectedSkillSet(); } diff --git a/tests/installer-migration-install-integration.test.cjs b/tests/installer-migration-install-integration.test.cjs index f9ca22a72..9fb0aeba9 100644 --- a/tests/installer-migration-install-integration.test.cjs +++ b/tests/installer-migration-install-integration.test.cjs @@ -209,11 +209,11 @@ function assertFreshInstallContract(runtime, targetDir) { ); if (contract.surface === 'flat-skills') { - if (runtime === 'codex') { - assertNoGsdDirectoryEntries(targetDir, 'skills'); - } else { - assertHasGsdDirectory(targetDir, 'skills'); - } + // Pre-#3562: codex was special-cased to expect zero gsd-* skill dirs + // (assumption: Codex auto-discovers from workflows). That assumption + // does not hold for Codex CLI 0.130.0 — fresh installs now materialize + // the same flat-skills surface as the other runtimes. + assertHasGsdDirectory(targetDir, 'skills'); } else if (contract.surface === 'hermes-skills') { assertHasGsdDirectory(targetDir, path.join('skills', 'gsd')); assert.ok(