From 8c790442981783951aa3d711984e4e31d194b12e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 2 Jun 2026 16:14:33 -0400 Subject: [PATCH] fix(#384): make getAgentsDir runtime-aware so non-Claude installs find agents (#617) * fix(#384): make getAgentsDir runtime-aware so non-Claude installs find agents getAgentsDir() ignored the active runtime and always returned the claude-family __dirname-relative agents path, so on OpenCode (and other non-Claude runtimes) checkAgentsInstalled() looked in the wrong directory and reported agents missing even when installed. Resolve the per-runtime global config dir via getGlobalConfigDir(runtime) (GSD_AGENTS_DIR env > runtime arg > GSD_RUNTIME env > 'claude'), and surface agent_runtime and agents_dir through withProjectRoot() so init diagnostics show which directory was checked. Updates the stale W010 health-check test that relied on the old __dirname path. Co-Authored-By: Claude Opus 4.8 * chore(#384): set changeset pr to 617 * fix(#384): keep claude on __dirname agents path; fix test lint + leak Address CI failures on the first cut: - getAgentsDir now only routes NON-claude runtimes through getGlobalConfigDir(runtime); claude retains the original __dirname- relative agents path, which correctly resolves to /agents for repo runs and the runtime config agents dir for real installs. This restores validate-health / W010 checks that were regressing because the claude default had moved off the repo-relative path. - Revert the now-unneeded GSD_AGENTS_DIR workaround in agent-install-validation.test.cjs (back to its next state). - bug-384 test: use helpers.cleanup() instead of raw fs.rmSync() (local/no-raw-rmsync-in-tests) and drop an unused var. - Reword a doc comment that contained a literal ~/.claude/agents path, which tripped the cline-install no-leaked-paths scanner. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .changeset/384-agents-dir-runtime-aware.md | 5 + src/core.cts | 30 +++- src/init.cts | 5 +- tests/bug-384-agents-runtime-aware.test.cjs | 151 ++++++++++++++++++++ 4 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 .changeset/384-agents-dir-runtime-aware.md create mode 100644 tests/bug-384-agents-runtime-aware.test.cjs diff --git a/.changeset/384-agents-dir-runtime-aware.md b/.changeset/384-agents-dir-runtime-aware.md new file mode 100644 index 000000000..118a77a16 --- /dev/null +++ b/.changeset/384-agents-dir-runtime-aware.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 617 +--- +**`/gsd:init` no longer reports agents as missing on OpenCode and other non-Claude runtimes** — `getAgentsDir()` now resolves the per-runtime global config directory instead of always checking the Claude path, and init diagnostics surface `agent_runtime` and `agents_dir`. diff --git a/src/core.cts b/src/core.cts index a2cb5200a..da48ad03d 100644 --- a/src/core.cts +++ b/src/core.cts @@ -36,6 +36,7 @@ const { findContextMdIn, } = planningWorkspace; import { findProjectRoot } from './project-root.cjs'; +import { getGlobalConfigDir } from './runtime-homes.cjs'; // ─── Configuration Module (generated CJS mirror) ──────────────────────────── import { CONFIG_DEFAULTS as CANONICAL_CONFIG_DEFAULTS, normalizeLegacyKeys } from './configuration.cjs'; @@ -1198,13 +1199,26 @@ function getRoadmapPhaseInternal(cwd: string, phaseNum: unknown): RoadmapPhaseRe // ─── Agent installation validation (#1371) ─────────────────────────────────── /** - * Resolve the agents directory from the GSD install location. + * Resolve the agents directory for the given runtime. + * + * Priority: + * 1. GSD_AGENTS_DIR env var (explicit override, any runtime) + * 2. For claude runtime: __dirname-relative path (agents/ sibling of get-shit-done/) + * This is correct for both repo runs and real installs (the runtime config dir's + * agents/ folder) because gsd-tools.cjs lives inside get-shit-done/bin/ in both cases. + * 3. For non-claude runtimes: getGlobalConfigDir(runtime)/agents + * + * @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude' */ -function getAgentsDir(): string { +function getAgentsDir(runtime?: string): string { if (process.env['GSD_AGENTS_DIR']) { return process.env['GSD_AGENTS_DIR']; } - return path.join(__dirname, '..', '..', '..', 'agents'); + const resolved = runtime ?? (process.env['GSD_RUNTIME'] || 'claude'); + if (resolved === 'claude') { + return path.join(__dirname, '..', '..', '..', 'agents'); + } + return path.join(getGlobalConfigDir(resolved), 'agents'); } interface AgentsInstalledResult { @@ -1212,13 +1226,17 @@ interface AgentsInstalledResult { missing_agents: string[]; installed_agents: string[]; agents_dir: string; + agent_runtime: string; } /** * Check which GSD agents are installed on disk. + * + * @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude' */ -function checkAgentsInstalled(): AgentsInstalledResult { - const agentsDir = getAgentsDir(); +function checkAgentsInstalled(runtime?: string): AgentsInstalledResult { + const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude'); + const agentsDir = getAgentsDir(resolvedRuntime); const expectedAgents = Object.keys(MODEL_PROFILES); const installed: string[] = []; const missing: string[] = []; @@ -1229,6 +1247,7 @@ function checkAgentsInstalled(): AgentsInstalledResult { missing_agents: expectedAgents, installed_agents: [], agents_dir: agentsDir, + agent_runtime: resolvedRuntime, }; } @@ -1248,6 +1267,7 @@ function checkAgentsInstalled(): AgentsInstalledResult { missing_agents: missing, installed_agents: installed, agents_dir: agentsDir, + agent_runtime: resolvedRuntime, }; } diff --git a/src/init.cts b/src/init.cts index 86c36daa7..c3d3df368 100644 --- a/src/init.cts +++ b/src/init.cts @@ -85,9 +85,12 @@ function getLatestCompletedMilestone(cwd: string): { version: string; name: stri function withProjectRoot(cwd: string, result: Record): Record { result['project_root'] = cwd; - const agentStatus = checkAgentsInstalled(); + const activeRuntime = resolveRuntime(cwd); + const agentStatus = checkAgentsInstalled(activeRuntime); result['agents_installed'] = agentStatus.agents_installed; result['missing_agents'] = agentStatus.missing_agents; + result['agents_dir'] = agentStatus.agents_dir; + result['agent_runtime'] = agentStatus.agent_runtime; const config = loadConfig(cwd); if (config.response_language) { result['response_language'] = config.response_language; diff --git a/tests/bug-384-agents-runtime-aware.test.cjs b/tests/bug-384-agents-runtime-aware.test.cjs new file mode 100644 index 000000000..b8a53c03c --- /dev/null +++ b/tests/bug-384-agents-runtime-aware.test.cjs @@ -0,0 +1,151 @@ +/** + * Regression test for bug #384 — getAgentsDir() is runtime-blind. + * + * Before the fix, getAgentsDir() always resolved to the Claude path + * (~/.claude/agents) regardless of the active runtime, so on an OpenCode + * install checkAgentsInstalled() always returned agents_installed=false and + * agent_runtime was not surfaced at all. + * + * After the fix: + * - GSD_RUNTIME=opencode + OPENCODE_CONFIG_DIR pointing at a temp dir → + * agents_installed=true, agent_runtime='opencode', agents_dir under the + * opencode config dir + * - No GSD_RUNTIME + GSD_AGENTS_DIR pointing at a temp dir → + * agents_installed=true, agent_runtime='claude' + * - GSD_RUNTIME=opencode but agents dir empty → + * agents_installed=false, agent_runtime='opencode' + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +const MODEL_PROFILES = require('../get-shit-done/bin/lib/model-profiles.cjs').MODEL_PROFILES; +const EXPECTED_AGENTS = Object.keys(MODEL_PROFILES); + +/** + * Create an agents directory under configDir/agents and populate it with + * the expected agent .md files. + */ +function createAgentsInConfigDir(configDir) { + const agentsDir = path.join(configDir, 'agents'); + fs.mkdirSync(agentsDir, { recursive: true }); + for (const name of EXPECTED_AGENTS) { + fs.writeFileSync( + path.join(agentsDir, `${name}.md`), + `---\nname: ${name}\ndescription: Test agent\ntools: Read, Bash\ncolor: cyan\n---\nAgent content.\n` + ); + } + return agentsDir; +} + +describe('bug #384 — getAgentsDir() is runtime-aware', () => { + let tmpDir; + let opencodeConfigDir; + + beforeEach(() => { + tmpDir = createTempProject(); + // Separate temp dir to act as the opencode global config dir + opencodeConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-opencode-')); + }); + + afterEach(() => { + cleanup(tmpDir); + cleanup(opencodeConfigDir); + }); + + // ── Test 1: opencode runtime resolves the opencode agents path ────────────── + + test('GSD_RUNTIME=opencode finds agents under OPENCODE_CONFIG_DIR/agents', () => { + // Place agents under the opencode config dir that getGlobalConfigDir('opencode') + // will return when OPENCODE_CONFIG_DIR is set. + const agentsDir = createAgentsInConfigDir(opencodeConfigDir); + + const result = runGsdTools( + ['init', 'quick', 'test description', '--raw'], + tmpDir, + { + GSD_RUNTIME: 'opencode', + OPENCODE_CONFIG_DIR: opencodeConfigDir, + // Ensure the process HOME does NOT have a conflicting ~/.claude/agents + // that might accidentally produce a false positive via GSD_AGENTS_DIR + // (we must NOT set GSD_AGENTS_DIR here — the whole point is that the fix + // uses the runtime-aware path without needing GSD_AGENTS_DIR). + } + ); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + + assert.strictEqual(output.agents_installed, true, + `agents_installed must be true when agents exist under OPENCODE_CONFIG_DIR/agents. ` + + `agents_dir=${output.agents_dir}, agent_runtime=${output.agent_runtime}`); + + assert.strictEqual(output.agent_runtime, 'opencode', + 'agent_runtime must be "opencode" when GSD_RUNTIME=opencode'); + + assert.strictEqual(output.agents_dir, agentsDir, + `agents_dir must point at the opencode agents dir (${agentsDir}), got: ${output.agents_dir}`); + }); + + // ── Test 2: claude fallback via GSD_AGENTS_DIR ────────────────────────────── + + test('default runtime (no GSD_RUNTIME) with GSD_AGENTS_DIR → agents_installed=true, agent_runtime=claude', () => { + // Classic GSD_AGENTS_DIR override: no runtime set, use the env shortcut + createAgentsInConfigDir(tmpDir); + // GSD_AGENTS_DIR points directly at the agents dir (not the config dir) + const directAgentsDir = path.join(tmpDir, 'agents'); + + const result = runGsdTools( + ['init', 'quick', 'test description', '--raw'], + tmpDir, + { + GSD_AGENTS_DIR: directAgentsDir, + // Explicitly unset GSD_RUNTIME so no runtime override applies + GSD_RUNTIME: '', + } + ); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + + assert.strictEqual(output.agents_installed, true, + `agents_installed must be true when GSD_AGENTS_DIR points at a populated agents dir. ` + + `agents_dir=${output.agents_dir}`); + + assert.strictEqual(output.agent_runtime, 'claude', + 'agent_runtime must be "claude" when no GSD_RUNTIME is set'); + + assert.strictEqual(output.agents_dir, directAgentsDir, + `agents_dir must match GSD_AGENTS_DIR override`); + }); + + // ── Test 3 (negative): opencode runtime, empty agents dir ─────────────────── + + test('GSD_RUNTIME=opencode with empty agents dir → agents_installed=false, agent_runtime still surfaced', () => { + // Create the opencode config dir but leave agents/ empty (no files) + const emptyAgentsDir = path.join(opencodeConfigDir, 'agents'); + fs.mkdirSync(emptyAgentsDir, { recursive: true }); + + const result = runGsdTools( + ['init', 'quick', 'test description', '--raw'], + tmpDir, + { + GSD_RUNTIME: 'opencode', + OPENCODE_CONFIG_DIR: opencodeConfigDir, + } + ); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + + assert.strictEqual(output.agents_installed, false, + 'agents_installed must be false when agents dir is empty'); + + assert.strictEqual(output.agent_runtime, 'opencode', + 'agent_runtime must still be surfaced even when agents are missing'); + }); +});