diff --git a/CHANGELOG.md b/CHANGELOG.md index 5be565265..1d82af3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **`/gsd-ingest-docs` command** — Scan a repo containing mixed ADRs, PRDs, SPECs, and DOCs and bootstrap or merge the full `.planning/` setup from them in a single pass. Parallel classification (`gsd-doc-classifier`), synthesis with precedence rules and cycle detection (`gsd-doc-synthesizer`), three-bucket conflicts report (`INGEST-CONFLICTS.md`: auto-resolved, competing-variants, unresolved-blockers), and hard-block on LOCKED-vs-LOCKED ADR contradictions in both new and merge modes. Supports directory-convention discovery and `--manifest ` YAML override with per-doc precedence. v1 caps at 50 docs per invocation; `--resolve interactive` is reserved. Extracts shared conflict-detection contract into `references/doc-conflict-engine.md` which `/gsd-import` now also consumes (#2387) ### Fixed +- **SDK `checkAgentsInstalled` is now runtime-aware** — `sdk/src/query/init.ts::checkAgentsInstalled` only knew where Claude Code put agents (`~/.claude/agents`). Users running GSD on Codex, OpenCode, Gemini, Kilo, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen, CodeBuddy, or Cline got `agents_installed: false` even with a complete install, which hard-blocked any workflow that gates subagent spawning on that flag. `sdk/src/query/helpers.ts` now resolves the right directory via three-tier detection (`GSD_RUNTIME` env → `config.runtime` → `claude` fallback) and mirrors `bin/install.js::getGlobalDir()` for all 14 runtimes. `GSD_AGENTS_DIR` still short-circuits the chain. `init-runner.ts` stays Claude-only by design (#2402) - **`init` query agents-installed check looks at the correct directory** — `checkAgentsInstalled` in `sdk/src/query/init.ts` defaulted to `~/.claude/get-shit-done/agents/`, but the installer writes GSD agents to `~/.claude/agents/`. Every init query therefore reported `agents_installed: false` on clean installs, which made workflows refuse to spawn `gsd-executor` and other parallel subagents. The default now matches `sdk/src/init-runner.ts` and the installer (#2400) - **Installer now installs `@gsd-build/sdk` automatically** so `gsd-sdk` lands on PATH. Resolves `command not found: gsd-sdk` errors that affected every `/gsd-*` command after a fresh install or `/gsd-update` to 1.36+. Adds `--no-sdk` to opt out and `--sdk` to force reinstall. Implements the `--sdk` flag that was previously documented in README but never wired up (#2385) diff --git a/sdk/src/query/helpers.test.ts b/sdk/src/query/helpers.test.ts index 320971dc1..8d8a86952 100644 --- a/sdk/src/query/helpers.test.ts +++ b/sdk/src/query/helpers.test.ts @@ -18,7 +18,13 @@ import { planningPaths, normalizeMd, resolvePathUnderProject, + resolveAgentsDir, + getRuntimeConfigDir, + detectRuntime, + SUPPORTED_RUNTIMES, + type Runtime, } from './helpers.js'; +import { homedir } from 'node:os'; // ─── escapeRegex ──────────────────────────────────────────────────────────── @@ -252,3 +258,156 @@ describe('resolvePathUnderProject', () => { await expect(resolvePathUnderProject(tmpDir, '../../etc/passwd')).rejects.toThrow(GSDError); }); }); + +// ─── Runtime-aware agents dir resolution (#2402) ─────────────────────────── + +const RUNTIME_ENV_VARS = [ + 'GSD_AGENTS_DIR', 'GSD_RUNTIME', 'CLAUDE_CONFIG_DIR', 'OPENCODE_CONFIG_DIR', + 'OPENCODE_CONFIG', 'KILO_CONFIG_DIR', 'KILO_CONFIG', 'XDG_CONFIG_HOME', + 'GEMINI_CONFIG_DIR', 'CODEX_HOME', 'COPILOT_CONFIG_DIR', 'ANTIGRAVITY_CONFIG_DIR', + 'CURSOR_CONFIG_DIR', 'WINDSURF_CONFIG_DIR', 'AUGMENT_CONFIG_DIR', 'TRAE_CONFIG_DIR', + 'QWEN_CONFIG_DIR', 'CODEBUDDY_CONFIG_DIR', 'CLINE_CONFIG_DIR', +] as const; + +describe('getRuntimeConfigDir', () => { + const saved: Record = {}; + beforeEach(() => { + for (const k of RUNTIME_ENV_VARS) { saved[k] = process.env[k]; delete process.env[k]; } + }); + afterEach(() => { + for (const k of RUNTIME_ENV_VARS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + }); + + const defaults: Record = { + claude: join(homedir(), '.claude'), + opencode: join(homedir(), '.config', 'opencode'), + kilo: join(homedir(), '.config', 'kilo'), + gemini: join(homedir(), '.gemini'), + codex: join(homedir(), '.codex'), + copilot: join(homedir(), '.copilot'), + antigravity: join(homedir(), '.gemini', 'antigravity'), + cursor: join(homedir(), '.cursor'), + windsurf: join(homedir(), '.codeium', 'windsurf'), + augment: join(homedir(), '.augment'), + trae: join(homedir(), '.trae'), + qwen: join(homedir(), '.qwen'), + codebuddy: join(homedir(), '.codebuddy'), + cline: join(homedir(), '.cline'), + }; + + for (const runtime of SUPPORTED_RUNTIMES) { + it(`resolves default path for ${runtime}`, () => { + expect(getRuntimeConfigDir(runtime)).toBe(defaults[runtime]); + }); + } + + const envOverrides: Array<[Runtime, string, string]> = [ + ['claude', 'CLAUDE_CONFIG_DIR', '/x/claude'], + ['gemini', 'GEMINI_CONFIG_DIR', '/x/gemini'], + ['codex', 'CODEX_HOME', '/x/codex'], + ['copilot', 'COPILOT_CONFIG_DIR', '/x/copilot'], + ['antigravity', 'ANTIGRAVITY_CONFIG_DIR', '/x/antigravity'], + ['cursor', 'CURSOR_CONFIG_DIR', '/x/cursor'], + ['windsurf', 'WINDSURF_CONFIG_DIR', '/x/windsurf'], + ['augment', 'AUGMENT_CONFIG_DIR', '/x/augment'], + ['trae', 'TRAE_CONFIG_DIR', '/x/trae'], + ['qwen', 'QWEN_CONFIG_DIR', '/x/qwen'], + ['codebuddy', 'CODEBUDDY_CONFIG_DIR', '/x/codebuddy'], + ['cline', 'CLINE_CONFIG_DIR', '/x/cline'], + ['opencode', 'OPENCODE_CONFIG_DIR', '/x/opencode'], + ['kilo', 'KILO_CONFIG_DIR', '/x/kilo'], + ]; + for (const [runtime, envVar, value] of envOverrides) { + it(`${runtime} honors ${envVar}`, () => { + process.env[envVar] = value; + expect(getRuntimeConfigDir(runtime)).toBe(value); + }); + } + + it('opencode uses XDG_CONFIG_HOME when direct vars unset', () => { + process.env.XDG_CONFIG_HOME = '/xdg'; + expect(getRuntimeConfigDir('opencode')).toBe(join('/xdg', 'opencode')); + }); + + it('opencode OPENCODE_CONFIG uses dirname', () => { + process.env.OPENCODE_CONFIG = '/cfg/opencode.json'; + expect(getRuntimeConfigDir('opencode')).toBe('/cfg'); + }); + + it('kilo uses XDG_CONFIG_HOME when direct vars unset', () => { + process.env.XDG_CONFIG_HOME = '/xdg'; + expect(getRuntimeConfigDir('kilo')).toBe(join('/xdg', 'kilo')); + }); +}); + +describe('detectRuntime', () => { + const saved: Record = {}; + beforeEach(() => { + for (const k of RUNTIME_ENV_VARS) { saved[k] = process.env[k]; delete process.env[k]; } + }); + afterEach(() => { + for (const k of RUNTIME_ENV_VARS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + }); + + it('defaults to claude with no signals', () => { + expect(detectRuntime()).toBe('claude'); + }); + + it('uses GSD_RUNTIME when set to a known runtime', () => { + process.env.GSD_RUNTIME = 'codex'; + expect(detectRuntime()).toBe('codex'); + }); + + it('falls back to config.runtime when GSD_RUNTIME unset', () => { + expect(detectRuntime({ runtime: 'gemini' })).toBe('gemini'); + }); + + it('GSD_RUNTIME wins over config.runtime', () => { + process.env.GSD_RUNTIME = 'codex'; + expect(detectRuntime({ runtime: 'gemini' })).toBe('codex'); + }); + + it('unknown GSD_RUNTIME falls through to config then claude', () => { + process.env.GSD_RUNTIME = 'bogus'; + expect(detectRuntime({ runtime: 'gemini' })).toBe('gemini'); + expect(detectRuntime()).toBe('claude'); + }); + + it('unknown config.runtime falls through to claude', () => { + expect(detectRuntime({ runtime: 'bogus' })).toBe('claude'); + }); +}); + +describe('resolveAgentsDir (runtime-aware)', () => { + const saved: Record = {}; + beforeEach(() => { + for (const k of RUNTIME_ENV_VARS) { saved[k] = process.env[k]; delete process.env[k]; } + }); + afterEach(() => { + for (const k of RUNTIME_ENV_VARS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + }); + + it('defaults to Claude agents dir with no args', () => { + expect(resolveAgentsDir()).toBe(join(homedir(), '.claude', 'agents')); + }); + + it('GSD_AGENTS_DIR short-circuits regardless of runtime', () => { + process.env.GSD_AGENTS_DIR = '/explicit/agents'; + expect(resolveAgentsDir('codex')).toBe('/explicit/agents'); + expect(resolveAgentsDir('claude')).toBe('/explicit/agents'); + }); + + it('appends /agents to the per-runtime config dir', () => { + process.env.CODEX_HOME = '/codex'; + expect(resolveAgentsDir('codex')).toBe(join('/codex', 'agents')); + }); +}); diff --git a/sdk/src/query/helpers.ts b/sdk/src/query/helpers.ts index 6ff012175..2246a3116 100644 --- a/sdk/src/query/helpers.ts +++ b/sdk/src/query/helpers.ts @@ -17,27 +17,106 @@ * ``` */ -import { join, relative, resolve, isAbsolute, normalize } from 'node:path'; +import { join, dirname, relative, resolve, isAbsolute, normalize } from 'node:path'; import { realpath } from 'node:fs/promises'; import { homedir } from 'node:os'; import { GSDError, ErrorClassification } from '../errors.js'; -// ─── resolveAgentsDir ────────────────────────────────────────────────────── +// ─── Runtime-aware agents directory resolution ───────────────────────────── /** - * Resolve the Claude Code agents directory using the same precedence as the - * installer (`bin/install.js` getGlobalDir for Claude): - * 1. GSD_AGENTS_DIR — explicit SDK override - * 2. CLAUDE_CONFIG_DIR/agents — matches installer's --config-dir / env - * 3. ~/.claude/agents — default - * - * Must mirror `bin/install.js:389-396` for Claude. Used by both - * `query/init.ts:checkAgentsInstalled` and `init-runner.ts`. + * Supported GSD runtimes. Kept in sync with `bin/install.js:getGlobalDir()`. */ -export function resolveAgentsDir(): string { +export const SUPPORTED_RUNTIMES = [ + 'claude', 'opencode', 'kilo', 'gemini', 'codex', 'copilot', 'antigravity', + 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'codebuddy', 'cline', +] as const; + +export type Runtime = (typeof SUPPORTED_RUNTIMES)[number]; + +function expandTilde(p: string): string { + return p.startsWith('~/') || p === '~' ? join(homedir(), p.slice(1)) : p; +} + +/** + * Resolve the per-runtime config directory, mirroring + * `bin/install.js:getGlobalDir()`. Agents live at `/agents`. + */ +export function getRuntimeConfigDir(runtime: Runtime): string { + switch (runtime) { + case 'claude': + return process.env.CLAUDE_CONFIG_DIR + ? expandTilde(process.env.CLAUDE_CONFIG_DIR) + : join(homedir(), '.claude'); + case 'opencode': + if (process.env.OPENCODE_CONFIG_DIR) return expandTilde(process.env.OPENCODE_CONFIG_DIR); + if (process.env.OPENCODE_CONFIG) return dirname(expandTilde(process.env.OPENCODE_CONFIG)); + if (process.env.XDG_CONFIG_HOME) return join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode'); + return join(homedir(), '.config', 'opencode'); + case 'kilo': + if (process.env.KILO_CONFIG_DIR) return expandTilde(process.env.KILO_CONFIG_DIR); + if (process.env.KILO_CONFIG) return dirname(expandTilde(process.env.KILO_CONFIG)); + if (process.env.XDG_CONFIG_HOME) return join(expandTilde(process.env.XDG_CONFIG_HOME), 'kilo'); + return join(homedir(), '.config', 'kilo'); + case 'gemini': + return process.env.GEMINI_CONFIG_DIR ? expandTilde(process.env.GEMINI_CONFIG_DIR) : join(homedir(), '.gemini'); + case 'codex': + return process.env.CODEX_HOME ? expandTilde(process.env.CODEX_HOME) : join(homedir(), '.codex'); + case 'copilot': + return process.env.COPILOT_CONFIG_DIR ? expandTilde(process.env.COPILOT_CONFIG_DIR) : join(homedir(), '.copilot'); + case 'antigravity': + return process.env.ANTIGRAVITY_CONFIG_DIR ? expandTilde(process.env.ANTIGRAVITY_CONFIG_DIR) : join(homedir(), '.gemini', 'antigravity'); + case 'cursor': + return process.env.CURSOR_CONFIG_DIR ? expandTilde(process.env.CURSOR_CONFIG_DIR) : join(homedir(), '.cursor'); + case 'windsurf': + return process.env.WINDSURF_CONFIG_DIR ? expandTilde(process.env.WINDSURF_CONFIG_DIR) : join(homedir(), '.codeium', 'windsurf'); + case 'augment': + return process.env.AUGMENT_CONFIG_DIR ? expandTilde(process.env.AUGMENT_CONFIG_DIR) : join(homedir(), '.augment'); + case 'trae': + return process.env.TRAE_CONFIG_DIR ? expandTilde(process.env.TRAE_CONFIG_DIR) : join(homedir(), '.trae'); + case 'qwen': + return process.env.QWEN_CONFIG_DIR ? expandTilde(process.env.QWEN_CONFIG_DIR) : join(homedir(), '.qwen'); + case 'codebuddy': + return process.env.CODEBUDDY_CONFIG_DIR ? expandTilde(process.env.CODEBUDDY_CONFIG_DIR) : join(homedir(), '.codebuddy'); + case 'cline': + return process.env.CLINE_CONFIG_DIR ? expandTilde(process.env.CLINE_CONFIG_DIR) : join(homedir(), '.cline'); + } +} + +/** + * Detect the invoking runtime using issue #2402 precedence: + * 1. `GSD_RUNTIME` env var + * 2. `config.runtime` field (from `.planning/config.json` when loaded) + * 3. Fallback to `'claude'` + * + * Unknown values fall through to the next tier rather than throwing, so + * stale env values don't hard-block workflows. + */ +export function detectRuntime(config?: { runtime?: unknown }): Runtime { + const envValue = process.env.GSD_RUNTIME; + if (envValue && (SUPPORTED_RUNTIMES as readonly string[]).includes(envValue)) { + return envValue as Runtime; + } + const configValue = config?.runtime; + if (typeof configValue === 'string' && (SUPPORTED_RUNTIMES as readonly string[]).includes(configValue)) { + return configValue as Runtime; + } + return 'claude'; +} + +/** + * Resolve the GSD agents directory for a given runtime. + * + * Precedence: + * 1. `GSD_AGENTS_DIR` — explicit SDK override (wins over runtime selection) + * 2. `/agents` — installer-parity default + * + * Defaults to Claude when no runtime is passed, matching prior behavior + * (see `init-runner.ts`, which is Claude-only by design). + */ +export function resolveAgentsDir(runtime: Runtime = 'claude'): string { if (process.env.GSD_AGENTS_DIR) return process.env.GSD_AGENTS_DIR; - if (process.env.CLAUDE_CONFIG_DIR) return join(process.env.CLAUDE_CONFIG_DIR, 'agents'); - return join(homedir(), '.claude', 'agents'); + return join(getRuntimeConfigDir(runtime), 'agents'); } // ─── Types ────────────────────────────────────────────────────────────────── diff --git a/sdk/src/query/init-complex.ts b/sdk/src/query/init-complex.ts index 3438e98cf..a15112ad7 100644 --- a/sdk/src/query/init-complex.ts +++ b/sdk/src/query/init-complex.ts @@ -162,7 +162,7 @@ export const initNewProject: QueryHandler = async (_args, projectDir) => { project_path: '.planning/PROJECT.md', }; - return { data: withProjectRoot(projectDir, result) }; + return { data: withProjectRoot(projectDir, result, config as Record) }; }; // ─── initProgress ───────────────────────────────────────────────────────── @@ -309,7 +309,7 @@ export const initProgress: QueryHandler = async (_args, projectDir) => { config_path: toPosixPath(relative(projectDir, paths.config)), }; - return { data: withProjectRoot(projectDir, result) }; + return { data: withProjectRoot(projectDir, result, config as Record) }; }; // ─── initManager ───────────────────────────────────────────────────────── @@ -574,5 +574,5 @@ export const initManager: QueryHandler = async (_args, projectDir) => { manager_flags: managerFlags, }; - return { data: withProjectRoot(projectDir, result) }; + return { data: withProjectRoot(projectDir, result, config as Record) }; }; diff --git a/sdk/src/query/init.test.ts b/sdk/src/query/init.test.ts index 427c34b85..06d6610c8 100644 --- a/sdk/src/query/init.test.ts +++ b/sdk/src/query/init.test.ts @@ -181,6 +181,109 @@ describe('withProjectRoot', () => { } }); + // #2402 — runtime-aware resolution: GSD_RUNTIME selects which runtime's + // config-dir env chain to consult, so non-Claude installs stop + // false-negating. + it('GSD_RUNTIME=codex resolves agents under CODEX_HOME/agents', async () => { + const { MODEL_PROFILES } = await import('./config-query.js'); + const codexHome = join(tmpDir, 'codex-home'); + const agentsDir = join(codexHome, 'agents'); + await mkdir(agentsDir, { recursive: true }); + for (const name of Object.keys(MODEL_PROFILES)) { + await writeFile(join(agentsDir, `${name}.md`), '# stub'); + } + const prevAgents = process.env.GSD_AGENTS_DIR; + const prevRuntime = process.env.GSD_RUNTIME; + const prevCodex = process.env.CODEX_HOME; + delete process.env.GSD_AGENTS_DIR; + process.env.GSD_RUNTIME = 'codex'; + process.env.CODEX_HOME = codexHome; + try { + const enriched = withProjectRoot(tmpDir, {}) as Record; + expect(enriched.agents_installed).toBe(true); + expect(enriched.missing_agents).toEqual([]); + } finally { + if (prevAgents === undefined) delete process.env.GSD_AGENTS_DIR; + else process.env.GSD_AGENTS_DIR = prevAgents; + if (prevRuntime === undefined) delete process.env.GSD_RUNTIME; + else process.env.GSD_RUNTIME = prevRuntime; + if (prevCodex === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = prevCodex; + } + }); + + it('config.runtime drives detection when GSD_RUNTIME is unset', async () => { + const { MODEL_PROFILES } = await import('./config-query.js'); + const geminiHome = join(tmpDir, 'gemini-home'); + const agentsDir = join(geminiHome, 'agents'); + await mkdir(agentsDir, { recursive: true }); + for (const name of Object.keys(MODEL_PROFILES)) { + await writeFile(join(agentsDir, `${name}.md`), '# stub'); + } + const prevAgents = process.env.GSD_AGENTS_DIR; + const prevRuntime = process.env.GSD_RUNTIME; + const prevGemini = process.env.GEMINI_CONFIG_DIR; + delete process.env.GSD_AGENTS_DIR; + delete process.env.GSD_RUNTIME; + process.env.GEMINI_CONFIG_DIR = geminiHome; + try { + const enriched = withProjectRoot(tmpDir, {}, { runtime: 'gemini' }) as Record; + expect(enriched.agents_installed).toBe(true); + } finally { + if (prevAgents === undefined) delete process.env.GSD_AGENTS_DIR; + else process.env.GSD_AGENTS_DIR = prevAgents; + if (prevRuntime === undefined) delete process.env.GSD_RUNTIME; + else process.env.GSD_RUNTIME = prevRuntime; + if (prevGemini === undefined) delete process.env.GEMINI_CONFIG_DIR; + else process.env.GEMINI_CONFIG_DIR = prevGemini; + } + }); + + it('GSD_RUNTIME wins over config.runtime', async () => { + const { MODEL_PROFILES } = await import('./config-query.js'); + const codexHome = join(tmpDir, 'codex-win'); + const agentsDir = join(codexHome, 'agents'); + await mkdir(agentsDir, { recursive: true }); + for (const name of Object.keys(MODEL_PROFILES)) { + await writeFile(join(agentsDir, `${name}.md`), '# stub'); + } + const prevAgents = process.env.GSD_AGENTS_DIR; + const prevRuntime = process.env.GSD_RUNTIME; + const prevCodex = process.env.CODEX_HOME; + delete process.env.GSD_AGENTS_DIR; + process.env.GSD_RUNTIME = 'codex'; + process.env.CODEX_HOME = codexHome; + try { + // config says gemini, env says codex — codex should win and find agents. + const enriched = withProjectRoot(tmpDir, {}, { runtime: 'gemini' }) as Record; + expect(enriched.agents_installed).toBe(true); + } finally { + if (prevAgents === undefined) delete process.env.GSD_AGENTS_DIR; + else process.env.GSD_AGENTS_DIR = prevAgents; + if (prevRuntime === undefined) delete process.env.GSD_RUNTIME; + else process.env.GSD_RUNTIME = prevRuntime; + if (prevCodex === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = prevCodex; + } + }); + + it('unknown GSD_RUNTIME falls through to config/Claude default', () => { + const prevAgents = process.env.GSD_AGENTS_DIR; + const prevRuntime = process.env.GSD_RUNTIME; + delete process.env.GSD_AGENTS_DIR; + process.env.GSD_RUNTIME = 'not-a-runtime'; + try { + // Should not throw; falls back to Claude — missing_agents on a blank tmpDir. + const enriched = withProjectRoot(tmpDir, {}) as Record; + expect(typeof enriched.agents_installed).toBe('boolean'); + } finally { + if (prevAgents === undefined) delete process.env.GSD_AGENTS_DIR; + else process.env.GSD_AGENTS_DIR = prevAgents; + if (prevRuntime === undefined) delete process.env.GSD_RUNTIME; + else process.env.GSD_RUNTIME = prevRuntime; + } + }); + it('GSD_AGENTS_DIR takes precedence over CLAUDE_CONFIG_DIR', async () => { const { MODEL_PROFILES } = await import('./config-query.js'); const winningDir = join(tmpDir, 'winning-agents'); diff --git a/sdk/src/query/init.ts b/sdk/src/query/init.ts index 4a39ab783..ae829297b 100644 --- a/sdk/src/query/init.ts +++ b/sdk/src/query/init.ts @@ -27,7 +27,7 @@ import { loadConfig } from '../config.js'; import { resolveModel, MODEL_PROFILES } from './config-query.js'; import { findPhase } from './phase.js'; import { roadmapGetPhase, getMilestoneInfo } from './roadmap.js'; -import { planningPaths, normalizePhaseName, toPosixPath, resolveAgentsDir } from './helpers.js'; +import { planningPaths, normalizePhaseName, toPosixPath, resolveAgentsDir, detectRuntime } from './helpers.js'; import type { QueryHandler } from './utils.js'; // ─── Internal helpers ────────────────────────────────────────────────────── @@ -79,10 +79,16 @@ function getLatestCompletedMilestone(projectDir: string): { version: string; nam /** * Check which GSD agents are installed on disk. + * + * Runtime-aware per issue #2402: detects the invoking runtime + * (`GSD_RUNTIME` → `config.runtime` → 'claude') and probes that runtime's + * canonical `agents/` directory. `GSD_AGENTS_DIR` still short-circuits. + * * Port of checkAgentsInstalled from core.cjs lines 1274-1306. */ -function checkAgentsInstalled(): { agents_installed: boolean; missing_agents: string[] } { - const agentsDir = resolveAgentsDir(); +function checkAgentsInstalled(config?: { runtime?: unknown }): { agents_installed: boolean; missing_agents: string[] } { + const runtime = detectRuntime(config); + const agentsDir = resolveAgentsDir(runtime); const expectedAgents = Object.keys(MODEL_PROFILES); if (!existsSync(agentsDir)) { @@ -171,7 +177,7 @@ export function withProjectRoot( ): Record { result.project_root = projectDir; - const agentStatus = checkAgentsInstalled(); + const agentStatus = checkAgentsInstalled(config); result.agents_installed = agentStatus.agents_installed; result.missing_agents = agentStatus.missing_agents;