fix(3751): resolveAgentsDir falls back to repo-local .claude/agents (#3762)
* test(#3751): add RED test for resolveAgentsDir missing repo-local .claude/agents Drive resolveAgentsDir() with a repo-local .claude/agents present and ~/.claude/agents absent. Structural contracts assert the function body must reference projectDir when constructing the fallback path, and that init-complex.ts must pass projectDir to the call site. Both fail deterministically before the fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#3751): resolveAgentsDir() falls back to repo-local .claude/agents for --local installs resolveAgentsDir() was probing only GSD_AGENTS_DIR or the runtime-global config dir (~/.claude/agents for Claude). For Claude Code --local installs where agents land in ./.claude/agents, the SDK reported agents_installed: false even when the agent files were present. Precedence (post-fix): 1. GSD_AGENTS_DIR (explicit override, unchanged) 2. <getRuntimeConfigDir(runtime)>/agents (when the dir exists) 3. <projectDir>/.claude/agents (repo-local fallback for claude runtime) Both init.ts:checkAgentsInstalled and init-complex.ts:initNewProject now receive projectDir and thread it to resolveAgentsDir(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(3751): update changeset to reference PR #3762 * fix(3751): add allow-test-rule annotation to satisfy lint-no-source-grep The structural-contract test reads TypeScript SDK source files to verify resolveAgentsDir() signature shape, fallback ordering, and call-site wiring. These are legitimate SDK-seam contracts (the TS source IS the product artifact). The allow-test-rule annotation bypasses the lint-no-source-grep check that flags readFileSync-bound variable usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(3751): repair Windows config-set concurrent-write race withPlanningLock threw on EPERM instead of retrying, causing the losing process to crash silently and its config write to never land. On Windows, when two processes race to create the same lock file via O_EXCL (writeFileSync with flag:'wx'), the OS can return EPERM instead of EEXIST while NTFS holds an internal exclusive handle on the newly-created file during the write. The previous catch block only recognized EEXIST as a contention signal — any other code (including EPERM) fell through to `throw err`. The calling test used .catch(()=>{}) to suppress process errors, so the losing process silently exited without writing its value; the winning process then wrote from the stale pre-race config, producing the observed 'balanced' instead of 'quality'. Fix: port the PLANNING_LOCK_RETRY_ERRNOS Set from main (landed in #3777) into this branch. The set treats EPERM, EBUSY, and six other transient filesystem codes as retry signals rather than fatal errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -109,14 +109,28 @@ export function detectRuntime(config?: { runtime?: unknown }): Runtime {
|
||||
*
|
||||
* Precedence:
|
||||
* 1. `GSD_AGENTS_DIR` — explicit SDK override (wins over runtime selection)
|
||||
* 2. `<getRuntimeConfigDir(runtime)>/agents` — installer-parity default
|
||||
* 2. `<getRuntimeConfigDir(runtime)>/agents` — installer-parity default (when the dir exists)
|
||||
* 3. `<projectDir>/.claude/agents` — repo-local fallback for `--local` Claude installs
|
||||
* (only probed when the global runtime dir is absent or empty, and `projectDir` is given)
|
||||
*
|
||||
* Defaults to Claude when no runtime is passed, matching prior behavior
|
||||
* (see `init-runner.ts`, which is Claude-only by design).
|
||||
*
|
||||
* The repo-local fallback was added for bug #3751: Claude Code `--local` installs
|
||||
* place agent definitions under `./.claude/agents` rather than `~/.claude/agents`,
|
||||
* but the SDK agent-detection path only probed the global directory.
|
||||
*/
|
||||
export function resolveAgentsDir(runtime: Runtime = 'claude'): string {
|
||||
export function resolveAgentsDir(runtime: Runtime = 'claude', projectDir?: string): string {
|
||||
if (process.env.GSD_AGENTS_DIR) return process.env.GSD_AGENTS_DIR;
|
||||
return join(getRuntimeConfigDir(runtime), 'agents');
|
||||
const globalDir = join(getRuntimeConfigDir(runtime), 'agents');
|
||||
if (existsSync(globalDir)) return globalDir;
|
||||
// Repo-local fallback: <projectDir>/.claude/agents for --local Claude installs (#3751).
|
||||
// Only applicable when a projectDir is known; other runtimes don't use .claude/.
|
||||
if (projectDir && runtime === 'claude') {
|
||||
const localDir = join(projectDir, '.claude', 'agents');
|
||||
if (existsSync(localDir)) return localDir;
|
||||
}
|
||||
return globalDir;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -303,7 +303,7 @@ export const initNewProject: QueryHandler = async (_args, projectDir, workstream
|
||||
getModelAlias('gsd-roadmapper', projectDir),
|
||||
]);
|
||||
const runtime = detectRuntime(config as { runtime?: unknown });
|
||||
const agentsDir = resolveAgentsDir(runtime);
|
||||
const agentsDir = resolveAgentsDir(runtime, projectDir);
|
||||
const gitInfo = gitWorktreeInfo(projectDir);
|
||||
const missingRequiredAgents = NEW_PROJECT_REQUIRED_AGENTS.filter(
|
||||
agent => !hasAgentDefinition(agentsDir, agent),
|
||||
|
||||
@@ -220,11 +220,14 @@ function getLatestCompletedMilestone(projectDir: string): { version: string; nam
|
||||
* (`GSD_RUNTIME` → `config.runtime` → 'claude') and probes that runtime's
|
||||
* canonical `agents/` directory. `GSD_AGENTS_DIR` still short-circuits.
|
||||
*
|
||||
* The optional `projectDir` parameter enables the repo-local `.claude/agents`
|
||||
* fallback for Claude `--local` installs (bug #3751).
|
||||
*
|
||||
* Port of checkAgentsInstalled from core.cjs lines 1274-1306.
|
||||
*/
|
||||
function checkAgentsInstalled(config?: { runtime?: unknown }): { agents_installed: boolean; missing_agents: string[] } {
|
||||
function checkAgentsInstalled(config?: { runtime?: unknown }, projectDir?: string): { agents_installed: boolean; missing_agents: string[] } {
|
||||
const runtime = detectRuntime(config);
|
||||
const agentsDir = resolveAgentsDir(runtime);
|
||||
const agentsDir = resolveAgentsDir(runtime, projectDir);
|
||||
const expectedAgents = Object.keys(MODEL_PROFILES);
|
||||
|
||||
if (!existsSync(agentsDir)) {
|
||||
@@ -369,7 +372,7 @@ export function withProjectRoot(
|
||||
): Record<string, unknown> {
|
||||
result.project_root = projectDir;
|
||||
|
||||
const agentStatus = checkAgentsInstalled(config);
|
||||
const agentStatus = checkAgentsInstalled(config, projectDir);
|
||||
result.agents_installed = agentStatus.agents_installed;
|
||||
result.missing_agents = agentStatus.missing_agents;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user