fix(sdk): point checkAgentsInstalled at ~/.claude/agents (#2401)

This commit is contained in:
Jeremy McSpadden
2026-04-18 11:11:07 -05:00
committed by GitHub
parent e208e9757c
commit 8ac02084be
6 changed files with 114 additions and 5 deletions

View File

@@ -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 <file>` 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

View File

@@ -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)
</required_reading>

View File

@@ -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];

View File

@@ -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. */

View File

@@ -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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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', () => {

View File

@@ -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)) {