* fix(#683): auto-degrade phase execution to sequential on worktree base mismatch Claude Code forks worktree-isolated executors off the repository default branch (origin/HEAD), not the orchestrator's HEAD. Running /gsd-execute-phase on a branch diverged from the default (unmerged milestone/feature branch) left every executor without the phase's plan files and tripped the worktree-branch-check guard with `exit 42` — 100% reproducible, all OSes. - New module src/worktree-base-ref.cts: HEAD-vs-fork-base drift detection (origin/HEAD with symbolic-ref fallback) and no-clobber worktree.baseRef management, exposed as `worktree base-check` / `worktree set-baseref`. - execute-phase.md: pre-dispatch, for Claude Code with worktrees enabled, auto-degrades the run to sequential on the main tree when a base mismatch is detected, recommending worktree.baseRef:"head". The exit-42 guard stays as a backstop. - Installer: fresh local Claude installs set worktree.baseRef:"head" in .claude/settings.local.json (no-clobber, respecting an explicit shared settings.json value); upgrades print an opt-in notice pointing at `gsd-tools worktree set-baseref`. - Docs: how-to guide, CLI/config reference, planning-config cross-ref. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#683): auto-apply worktree.baseRef on upgrade; gate fresh+upgrade on use_worktrees Per maintainer direction: on a local Claude Code UPGRADE, set worktree.baseRef:"head" automatically (no opt-in notice) when the project's workflow.use_worktrees is enabled, instead of merely printing a remediation notice. For consistency the FRESH path is now gated the same way: both paths compute worktrees-enabled once (bounded walk-up read of .planning/config.json, default enabled unless workflow.use_worktrees === false) and apply the no-clobber baseRef only when enabled — never overwriting an explicit value in settings.local.json or a shared settings.json. gsd-tools worktree set-baseref remains for manual use. Docs + changeset updated; tests hardened (file-exists assertions, fresh+disabled case, upgrade idempotency). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#683): measure workflow byte-budget on LF, fixing Windows-only CI failure The workflow-size-budget test failed only on Windows: git checks out the .md files as CRLF (no eol=lf in .gitattributes) and byteCount used fs.statSync().size (raw on-disk bytes), counting an extra \r per line. That inflated execute-phase.md — the XL high-water-mark file pinned near its ceiling by the tighten-only ratchet — from 88492 LF bytes to ~90245 on Windows, over the 90000 XL ceiling, while passing on the LF-checkout Mac/Linux runners. The ceilings are explicitly "calibrated against raw `wc -c`" on an LF checkout, so the measurement should be LF-based on every platform. byteCount now reads the file and counts Buffer.byteLength after stripping CR, making the budget platform-independent (a no-op on LF checkouts; verified statSync === normalized for all 88 workflow files). No ceilings changed. Added a regression test asserting CRLF and LF content of the same file count identically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#683): make worktree-base-ref test path mocks Windows-safe (path.join) tests/worktree-base-ref.test.cjs keyed its injected readFile/writeFile mocks (and a few expected `file` values) with forward-slash template literals like `${claudeDir}/settings.local.json`. The module composes those paths with path.join(), which emits backslashes on Windows, so the mock keys never matched the module's lookup → readFile returned null → resolveEffectiveBaseRef / cmdWorktreeBaseCheck / cmdWorktreeSetBaseRef (and the JSONC variants) failed on the Windows full-test runner only (they passed on Mac/Linux, and the install tests passed because they use the real filesystem). The module is correct; only the test fixtures hardcoded '/'. All mock keys and path assertions now use path.join(base, ...) mirroring the module, so they match on every platform (no-op on POSIX). 19 path references across 16 lines. 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:
@@ -31,6 +31,10 @@ const {
|
||||
const {
|
||||
resolveAntigravityGlobalDir,
|
||||
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
||||
const {
|
||||
applyWorktreeBaseRef,
|
||||
readBaseRefFromSettings,
|
||||
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
|
||||
|
||||
/**
|
||||
* Runtimes that register hyphen-form `name:` per #2808 AND copy agent bodies
|
||||
@@ -8585,6 +8589,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
// agentsSrc is declared here (let, not const) because installCodexConfig() inside the
|
||||
// Codex config block below also references it, and that block is outside the try scope.
|
||||
let agentsSrc = path.join(src, 'agents');
|
||||
// Capture upgrade signal BEFORE files are written (#683). Must be declared at function
|
||||
// scope (outside the try block below) so it is accessible in the settings section later.
|
||||
// Absent VERSION = fresh install; present VERSION = upgrade/re-install.
|
||||
const priorInstallExisted = fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION'));
|
||||
try {
|
||||
installerMigrationResult = runInstallerMigrations({
|
||||
configDir: targetDir,
|
||||
@@ -10116,6 +10124,68 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
|
||||
: localCmd('gsd-update-banner.js'));
|
||||
|
||||
// #683: Set worktree.baseRef:"head" in settings.local.json for local Claude installs.
|
||||
// Both fresh and upgrade paths apply only when worktrees are enabled for the project.
|
||||
// Never applies to global installs, non-Claude runtimes, or when the user already
|
||||
// has an explicit baseRef in EITHER settings.local.json OR settings.json (no-clobber).
|
||||
// Guard: skip entirely when settings is not a plain object (e.g. parsed to [] or primitive)
|
||||
// to avoid crashing applyWorktreeBaseRef on unexpected top-level shapes.
|
||||
if (isLocalClaude && settings !== null && typeof settings === 'object' && !Array.isArray(settings)) {
|
||||
// Read shared settings.json baseRef so no-clobber spans both files (#683 FIX 1).
|
||||
// shared settings.json no-clobber is checked here; settings.local.json no-clobber
|
||||
// is enforced inside applyWorktreeBaseRef itself.
|
||||
const sharedSettingsForBaseRef = readSettings(path.join(targetDir, 'settings.json')) || {};
|
||||
const sharedBaseRef = readBaseRefFromSettings(sharedSettingsForBaseRef);
|
||||
|
||||
// Compute worktrees-enabled ONCE for both fresh and upgrade paths (FIX A: DRY + consistency).
|
||||
// Read workflow.use_worktrees from .planning/config.json by walking up from
|
||||
// targetDir (same walk-up pattern as readGsdRuntimeProfileResolver). Defaults
|
||||
// to enabled (true) when the file is missing, unreadable, or the key is absent;
|
||||
// only boolean false disables (string "false" stays enabled).
|
||||
let worktreesEnabled = true; // default: enabled
|
||||
try {
|
||||
let probeDir = path.resolve(targetDir);
|
||||
for (let depth = 0; depth < 8; depth += 1) {
|
||||
const candidate = path.join(probeDir, '.planning', 'config.json');
|
||||
if (fs.existsSync(candidate)) {
|
||||
try {
|
||||
const parsed = JSON.parse(stripJsonComments(fs.readFileSync(candidate, 'utf-8')));
|
||||
if (parsed && typeof parsed === 'object' &&
|
||||
parsed.workflow && parsed.workflow.use_worktrees === false) {
|
||||
worktreesEnabled = false;
|
||||
}
|
||||
} catch {
|
||||
// Malformed config.json — treat as enabled (safe fallback).
|
||||
}
|
||||
break;
|
||||
}
|
||||
const parent = path.dirname(probeDir);
|
||||
if (parent === probeDir) break;
|
||||
probeDir = parent;
|
||||
}
|
||||
} catch {
|
||||
// Any unexpected error reading .planning — default to enabled.
|
||||
}
|
||||
|
||||
if (worktreesEnabled && sharedBaseRef === null) {
|
||||
if (!priorInstallExisted) {
|
||||
// Fresh install — apply no-clobber baseRef set.
|
||||
// canonical no-clobber logic: src/worktree-base-ref.cts applyWorktreeBaseRef (#683)
|
||||
const { changed } = applyWorktreeBaseRef(settings);
|
||||
if (changed) {
|
||||
console.log(` ${green}✓${reset} Set worktree.baseRef:"head" for Claude Code worktrees (forks phase worktrees off HEAD; #683)`);
|
||||
}
|
||||
} else {
|
||||
// Upgrade — auto-apply no-clobber baseRef set when worktrees are enabled.
|
||||
const { changed } = applyWorktreeBaseRef(settings);
|
||||
if (changed) {
|
||||
console.log(` ${green}✓${reset} Enabled worktree.baseRef:"head" for Claude Code worktrees (forks phase worktrees off HEAD; #683)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// When worktreesEnabled is false: do nothing, print nothing (both fresh and upgrade).
|
||||
}
|
||||
|
||||
persistActiveProfileMarker();
|
||||
return {
|
||||
settingsPath,
|
||||
|
||||
Reference in New Issue
Block a user