From 8ac02084be8b5bdd1829c89a6210ef440058b5f4 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Sat, 18 Apr 2026 11:11:07 -0500 Subject: [PATCH] fix(sdk): point checkAgentsInstalled at ~/.claude/agents (#2401) --- CHANGELOG.md | 1 + get-shit-done/workflows/profile-user.md | 2 +- sdk/src/init-runner.ts | 3 +- sdk/src/query/helpers.ts | 19 ++++++ sdk/src/query/init.test.ts | 89 +++++++++++++++++++++++++ sdk/src/query/init.ts | 5 +- 6 files changed, 114 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8197f5f7..5be565265 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 +- **`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) ## [1.37.1] - 2026-04-17 diff --git a/get-shit-done/workflows/profile-user.md b/get-shit-done/workflows/profile-user.md index 4fe48f11d..e066ad56d 100644 --- a/get-shit-done/workflows/profile-user.md +++ b/get-shit-done/workflows/profile-user.md @@ -9,7 +9,7 @@ Read all files referenced by the invoking prompt's execution_context before star Key references: - @$HOME/.claude/get-shit-done/references/ui-brand.md (display patterns) -- @$HOME/.claude/get-shit-done/agents/gsd-user-profiler.md (profiler agent definition) +- @$HOME/.claude/agents/gsd-user-profiler.md (profiler agent definition) - @$HOME/.claude/get-shit-done/references/user-profiling.md (profiling reference doc) diff --git a/sdk/src/init-runner.ts b/sdk/src/init-runner.ts index 22581f597..0d787b49d 100644 --- a/sdk/src/init-runner.ts +++ b/sdk/src/init-runner.ts @@ -33,11 +33,12 @@ import type { GSDEventStream } from './event-stream.js'; import { loadConfig } from './config.js'; import { runPhaseStepSession } from './session-runner.js'; import { sanitizePrompt } from './prompt-sanitizer.js'; +import { resolveAgentsDir } from './query/helpers.js'; // ─── Constants ─────────────────────────────────────────────────────────────── const GSD_TEMPLATES_DIR = join(homedir(), '.claude', 'get-shit-done', 'templates'); -const GSD_AGENTS_DIR = join(homedir(), '.claude', 'agents'); +const GSD_AGENTS_DIR = resolveAgentsDir(); const RESEARCH_TYPES = ['STACK', 'FEATURES', 'ARCHITECTURE', 'PITFALLS'] as const; type ResearchType = (typeof RESEARCH_TYPES)[number]; diff --git a/sdk/src/query/helpers.ts b/sdk/src/query/helpers.ts index 4de9a848f..6ff012175 100644 --- a/sdk/src/query/helpers.ts +++ b/sdk/src/query/helpers.ts @@ -19,8 +19,27 @@ import { join, 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 ────────────────────────────────────────────────────── + +/** + * 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`. + */ +export function resolveAgentsDir(): 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'); +} + // ─── Types ────────────────────────────────────────────────────────────────── /** Paths to common .planning files. */ diff --git a/sdk/src/query/init.test.ts b/sdk/src/query/init.test.ts index 70c14dddd..427c34b85 100644 --- a/sdk/src/query/init.test.ts +++ b/sdk/src/query/init.test.ts @@ -116,6 +116,95 @@ describe('withProjectRoot', () => { const enriched = withProjectRoot(tmpDir, result, {}); expect(enriched.response_language).toBeUndefined(); }); + + // Regression: #2400 — checkAgentsInstalled was looking at the wrong default + // directory (~/.claude/get-shit-done/agents) while the installer writes to + // ~/.claude/agents, causing agents_installed: false even on clean installs. + it('reports agents_installed: true when all expected agents exist in GSD_AGENTS_DIR', async () => { + const { MODEL_PROFILES } = await import('./config-query.js'); + const agentsDir = join(tmpDir, 'fake-agents'); + await mkdir(agentsDir, { recursive: true }); + for (const name of Object.keys(MODEL_PROFILES)) { + await writeFile(join(agentsDir, `${name}.md`), '# stub'); + } + const prev = process.env.GSD_AGENTS_DIR; + process.env.GSD_AGENTS_DIR = agentsDir; + try { + const enriched = withProjectRoot(tmpDir, {}); + expect(enriched.agents_installed).toBe(true); + expect(enriched.missing_agents).toEqual([]); + } finally { + if (prev === undefined) delete process.env.GSD_AGENTS_DIR; + else process.env.GSD_AGENTS_DIR = prev; + } + }); + + it('reports missing agents when GSD_AGENTS_DIR is empty', async () => { + const agentsDir = join(tmpDir, 'empty-agents'); + await mkdir(agentsDir, { recursive: true }); + const prev = process.env.GSD_AGENTS_DIR; + process.env.GSD_AGENTS_DIR = agentsDir; + try { + const enriched = withProjectRoot(tmpDir, {}) as Record; + expect(enriched.agents_installed).toBe(false); + expect((enriched.missing_agents as string[]).length).toBeGreaterThan(0); + } finally { + if (prev === undefined) delete process.env.GSD_AGENTS_DIR; + else process.env.GSD_AGENTS_DIR = prev; + } + }); + + // Regression: #2400 follow-up — installer honors CLAUDE_CONFIG_DIR for custom + // Claude install roots. The SDK check must follow the same precedence or it + // false-negatives agent presence on non-default installs. + it('honors CLAUDE_CONFIG_DIR when GSD_AGENTS_DIR is unset', async () => { + const { MODEL_PROFILES } = await import('./config-query.js'); + const configDir = join(tmpDir, 'custom-claude'); + const agentsDir = join(configDir, '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 prevClaude = process.env.CLAUDE_CONFIG_DIR; + delete process.env.GSD_AGENTS_DIR; + process.env.CLAUDE_CONFIG_DIR = configDir; + 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 (prevClaude === undefined) delete process.env.CLAUDE_CONFIG_DIR; + else process.env.CLAUDE_CONFIG_DIR = prevClaude; + } + }); + + 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'); + const losingDir = join(tmpDir, 'losing-config', 'agents'); + await mkdir(winningDir, { recursive: true }); + await mkdir(losingDir, { recursive: true }); + // Only populate the winning dir. + for (const name of Object.keys(MODEL_PROFILES)) { + await writeFile(join(winningDir, `${name}.md`), '# stub'); + } + const prevAgents = process.env.GSD_AGENTS_DIR; + const prevClaude = process.env.CLAUDE_CONFIG_DIR; + process.env.GSD_AGENTS_DIR = winningDir; + process.env.CLAUDE_CONFIG_DIR = join(tmpDir, 'losing-config'); + try { + const enriched = withProjectRoot(tmpDir, {}) 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 (prevClaude === undefined) delete process.env.CLAUDE_CONFIG_DIR; + else process.env.CLAUDE_CONFIG_DIR = prevClaude; + } + }); }); describe('initExecutePhase', () => { diff --git a/sdk/src/query/init.ts b/sdk/src/query/init.ts index 3533c3ecc..4a39ab783 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 } from './helpers.js'; +import { planningPaths, normalizePhaseName, toPosixPath, resolveAgentsDir } from './helpers.js'; import type { QueryHandler } from './utils.js'; // ─── Internal helpers ────────────────────────────────────────────────────── @@ -82,8 +82,7 @@ function getLatestCompletedMilestone(projectDir: string): { version: string; nam * Port of checkAgentsInstalled from core.cjs lines 1274-1306. */ function checkAgentsInstalled(): { agents_installed: boolean; missing_agents: string[] } { - const agentsDir = process.env.GSD_AGENTS_DIR - || join(homedir(), '.claude', 'get-shit-done', 'agents'); + const agentsDir = resolveAgentsDir(); const expectedAgents = Object.keys(MODEL_PROFILES); if (!existsSync(agentsDir)) {