* 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 <noreply@anthropic.com> * 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 <repo>/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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/384-agents-dir-runtime-aware.md
Normal file
5
.changeset/384-agents-dir-runtime-aware.md
Normal file
@@ -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`.
|
||||
30
src/core.cts
30
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -85,9 +85,12 @@ function getLatestCompletedMilestone(cwd: string): { version: string; name: stri
|
||||
|
||||
function withProjectRoot(cwd: string, result: Record<string, unknown>): Record<string, unknown> {
|
||||
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;
|
||||
|
||||
151
tests/bug-384-agents-runtime-aware.test.cjs
Normal file
151
tests/bug-384-agents-runtime-aware.test.cjs
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user