fix(#1013): resolve worktree.baseRef from user/global settings cascade (#1038)

* fix(#1013): resolve worktree.baseRef from user/global settings cascade

cmdWorktreeBaseCheck resolved worktree.baseRef from the project checkout's
.claude/ only (settings.local.json then settings.json). A user/global
worktree.baseRef:"head" — the layer /config writes and the harness honors,
and the only sensible place for a machine-wide preference — was invisible. On a
phase lane (HEAD ahead of origin/HEAD, or no origin/HEAD symref) base-check
returned shouldDegrade:true and execute-phase forced sequential execution,
silently losing the parallel worktree execution the user configured.
CLAUDE_CONFIG_DIR (relocated user config dir) was also ignored.

resolveEffectiveBaseRef now accepts an optional user/global config dir and reads
its settings.json as a third, lowest-precedence layer (project local > project
shared > user/global). cmdWorktreeBaseCheck resolves it via
getGlobalConfigDir('claude'), which honors CLAUDE_CONFIG_DIR. The existing
project-level reads stay as higher-precedence overrides and the injectable
readFile seam is preserved, keeping the unit tests hermetic.

Closes #1013

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1013): backfill changeset PR number to 1038

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-11 11:36:30 -04:00
committed by GitHub
parent 12d6285b4f
commit ab8b84286e
4 changed files with 190 additions and 10 deletions

View File

@@ -14,6 +14,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { execGit as execGitSeam } from './shell-command-projection.cjs';
import { getGlobalConfigDir } from './runtime-homes.cjs';
// ─── Internal helpers ─────────────────────────────────────────────────────────
@@ -160,14 +161,21 @@ export function applyWorktreeBaseRef(settings: Record<string, unknown>): {
}
/**
* Reads settings.local.json then settings.json under claudeDir, extracts
* worktree.baseRef from the first file that provides a non-null string value.
* Reads settings files in a 3-layer cascade and extracts worktree.baseRef from
* the first layer that provides a non-null string value. Layers (highest to lowest
* precedence):
* 1. project local — <claudeDir>/settings.local.json
* 2. project shared — <claudeDir>/settings.json
* 3. user/global — <userClaudeDir>/settings.json (only when userClaudeDir is
* provided AND resolves to a different path than claudeDir)
*
* deps.readFile(path) must return the file contents or null on any error.
* userClaudeDir is optional; when absent/null the user/global layer is skipped.
*/
export function resolveEffectiveBaseRef(
claudeDir: string,
deps?: { readFile?: (p: string) => string | null }
deps?: { readFile?: (p: string) => string | null },
userClaudeDir?: string | null
): string | null {
const readFile: (p: string) => string | null = deps?.readFile ?? ((p: string) => {
try {
@@ -191,28 +199,47 @@ export function resolveEffectiveBaseRef(
}
}
// Layer 1: project local
const localRef = parseBaseRef(localPath);
if (localRef !== null) return localRef;
return parseBaseRef(sharedPath);
// Layer 2: project shared
const sharedRef = parseBaseRef(sharedPath);
if (sharedRef !== null) return sharedRef;
// Layer 3: user/global (only when provided and not the same directory as claudeDir)
if (userClaudeDir && path.resolve(userClaudeDir) !== path.resolve(claudeDir)) {
const userSharedPath = path.join(userClaudeDir, 'settings.json');
const userRef = parseBaseRef(userSharedPath);
if (userRef !== null) return userRef;
}
return null;
}
/**
* CLI command: check current worktree base-ref degradation status.
*
* Reads effective baseRef from <cwd>/.claude settings, runs degradation
* evaluation, writes JSON result to stdout (or injected write), and returns
* the result object.
* Reads effective baseRef from <cwd>/.claude settings (3-layer cascade:
* project local → project shared → user/global), runs degradation evaluation,
* writes JSON result to stdout (or injected write), and returns the result object.
*
* deps.userClaudeDir overrides the user/global config directory resolution
* (default: getGlobalConfigDir('claude'), which honours CLAUDE_CONFIG_DIR).
*/
export function cmdWorktreeBaseCheck(
cwd: string,
_args: string[],
deps?: { execGit?: ExecGitFn; readFile?: (p: string) => string | null; write?: (s: string) => void }
deps?: { execGit?: ExecGitFn; readFile?: (p: string) => string | null; write?: (s: string) => void; userClaudeDir?: string | null }
): ReturnType<typeof evaluateWorktreeBaseDegrade> {
const claudeDir = path.join(cwd, '.claude');
const userClaudeDir = Object.prototype.hasOwnProperty.call(deps ?? {}, 'userClaudeDir')
? (deps as { userClaudeDir?: string | null }).userClaudeDir
: getGlobalConfigDir('claude');
const effectiveBaseRef = resolveEffectiveBaseRef(
claudeDir,
deps?.readFile ? { readFile: deps.readFile } : undefined
deps?.readFile ? { readFile: deps.readFile } : undefined,
userClaudeDir
);
const result = evaluateWorktreeBaseDegrade({
cwd,