* fix(#381): make gsd_run launcher reachable in fresh-shell-per-block runtimes On runtimes that execute each fenced bash block in a separate shell process (e.g. Claude Code — documented behavior: each Bash command is a separate process; inline shell functions and exported vars do not persist between calls), the once-per-file gsd_run() function was undefined in every block after the preamble block, and the call was swallowed by `2>/dev/null || echo "{}"` into silent empty state. Fix (budget-neutral session-level resolution): - Ship gsd-core/bin/gsd_run, a POSIX sh wrapper that symlink-resolves its own location and execs the co-located gsd-tools.cjs. Exposed on PATH via the npm `bin` field (global installs) and shipped to local installs via the recursive gsd-core/ copy. - The per-file launcher preamble now appends `export PATH='<bindir>':"$PATH"` to the file named by $CLAUDE_ENV_FILE (Claude Code's documented env-persistence mechanism) so later fresh-shell blocks resolve gsd_run from PATH. Guarded as a strict no-op when CLAUDE_ENV_FILE is unset; the inline gsd_run() definition remains the fallback for all other runtimes. The single-quoted dir neutralizes shell metacharacters at source time. - Propagated via scripts/sync-runtime-launcher.cjs to all launcher-using files. - XL workflow byte budget 93000 -> 93200 (the ~130B clause pushes plan-phase.md to 93135; legitimate content growth, ratchet-up per #717). Regression tests (I)/(J) in runtime-launcher-parity.test.cjs cover wrapper delegation and end-to-end PATH persistence (sourcing the env file with a space-bearing install path). Known limitation: an install path containing a literal single-quote yields a malformed env-file line and falls back to the status quo (no regression); rare on sanitized home directories. Closes #381 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#381): add changeset for gsd_run fresh-shell reachability fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#381): scope test (J) bare-PATH execution to POSIX (Windows Git Bash exec bit) Windows Git Bash (msys2) does not honor Node's chmod exec bit for PATH-executing extension-less scripts, so the bare `gsd_run` command lookup failed there even though the env-file PATH persistence was correct. The env-file content assertions (the fix's actual cross-platform logic) still run on every platform; only the final source-and-execute sub-step is gated to non-win32. Global installs on Windows are covered by npm's generated bin shim. 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:
@@ -546,6 +546,117 @@ describe('runtime-launcher-parity (#373)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Issue #381: standalone gsd_run executable + CLAUDE_ENV_FILE persistence ──
|
||||
describe('runtime-launcher-parity — standalone executable (#381)', () => {
|
||||
const BIN_DIR = path.join(__dirname, '..', 'gsd-core', 'bin');
|
||||
const GSD_RUN_SRC = path.join(BIN_DIR, 'gsd_run');
|
||||
|
||||
// ─── (I) gsd_run executable delegates to gsd-tools.cjs beside it ──────────
|
||||
test('(I) gsd_run executable delegates to gsd-tools.cjs beside it', () => {
|
||||
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-381-I-'));
|
||||
try {
|
||||
const binDir = path.join(base, 'gsd-core', 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Copy the real gsd_run executable into the temp bin dir
|
||||
fs.copyFileSync(GSD_RUN_SRC, path.join(binDir, 'gsd_run'));
|
||||
fs.chmodSync(path.join(binDir, 'gsd_run'), 0o755);
|
||||
|
||||
// Write a stub gsd-tools.cjs that echoes its args
|
||||
fs.writeFileSync(
|
||||
path.join(binDir, 'gsd-tools.cjs'),
|
||||
`console.log('GSD_TOOLS_STUB:' + process.argv.slice(2).join(' '))`,
|
||||
);
|
||||
|
||||
const stdout = execFileSync('sh', [path.join(binDir, 'gsd_run'), 'query', 'x'], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.ok(
|
||||
stdout.includes('GSD_TOOLS_STUB:query x'),
|
||||
`Expected stdout to contain "GSD_TOOLS_STUB:query x", got: ${stdout.trim()}`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── (J) preamble persists bin dir to CLAUDE_ENV_FILE ─────────────────────
|
||||
test('(J) preamble persists bin dir to CLAUDE_ENV_FILE so a fresh shell resolves gsd_run', () => {
|
||||
// Use a RUNTIME_DIR whose path contains a SPACE to prove single-quote safety.
|
||||
const baseParent = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-381-J-'));
|
||||
const base = path.join(baseParent, 'has space');
|
||||
try {
|
||||
const binDir = path.join(base, 'gsd-core', 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// gsd_run stub that prints GSD_RUN_STUB:<args>
|
||||
const gsdRunStub = path.join(binDir, 'gsd_run');
|
||||
fs.writeFileSync(gsdRunStub, '#!/bin/sh\necho "GSD_RUN_STUB:$*"\n');
|
||||
fs.chmodSync(gsdRunStub, 0o755);
|
||||
|
||||
// gsd-tools.cjs stub (must exist for preamble first arm to win)
|
||||
fs.writeFileSync(path.join(binDir, 'gsd-tools.cjs'), '// stub');
|
||||
|
||||
const envFile = path.join(baseParent, 'envfile');
|
||||
const snippet = fs.readFileSync(SNIPPET_FILE, 'utf8');
|
||||
|
||||
// Script: just source the preamble with RUNTIME_DIR + CLAUDE_ENV_FILE set
|
||||
const preambleScript = path.join(baseParent, 'run-preamble.sh');
|
||||
fs.writeFileSync(preambleScript, snippet);
|
||||
|
||||
execFileSync('bash', [preambleScript], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
RUNTIME_DIR: base,
|
||||
CLAUDE_ENV_FILE: envFile,
|
||||
PATH: process.env.PATH,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert envfile exists and the persisted line is single-quoted
|
||||
assert.ok(fs.existsSync(envFile), `Expected CLAUDE_ENV_FILE (${envFile}) to be created after preamble runs`);
|
||||
const envFileContent = fs.readFileSync(envFile, 'utf8');
|
||||
// The persisted line must single-quote the directory (neutralising $, spaces, etc.)
|
||||
// and keep "$PATH" expanding at source time.
|
||||
// Expected form: export PATH='<dir>':"$PATH"
|
||||
assert.ok(
|
||||
envFileContent.includes("export PATH='"),
|
||||
`Expected envfile to contain single-quoted export PATH line, got: ${envFileContent.trim()}`,
|
||||
);
|
||||
assert.ok(
|
||||
envFileContent.includes('has space/gsd-core/bin'),
|
||||
`Expected envfile to contain the spaced bin dir, got: ${envFileContent.trim()}`,
|
||||
);
|
||||
assert.ok(
|
||||
envFileContent.includes(':"$PATH"'),
|
||||
`Expected envfile to contain :"$PATH" (double-quoted, expands at source time), got: ${envFileContent.trim()}`,
|
||||
);
|
||||
|
||||
// Windows Git Bash (msys2) does not honor Node's chmod exec bit for PATH-executing
|
||||
// extension-less scripts; the env-file persistence above is the cross-platform proof.
|
||||
// Global installs on Windows are covered by npm's generated bin shim.
|
||||
if (process.platform !== 'win32') {
|
||||
// Simulate a LATER fresh block that SOURCES the env file to get gsd_run on PATH.
|
||||
// The later shell does NOT have the bin dir on PATH beforehand — it only gets it
|
||||
// by sourcing the env file. We use a minimal PATH (no temp bin dir pre-injected).
|
||||
const stdout = execFileSync('bash', ['-c', '. "$CLAUDE_ENV_FILE"; gsd_run hello'], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
CLAUDE_ENV_FILE: envFile,
|
||||
PATH: process.env.PATH,
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
stdout.includes('GSD_RUN_STUB:hello'),
|
||||
`Expected stdout to contain "GSD_RUN_STUB:hello" after sourcing env file, got: ${stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
cleanup(baseParent);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Agent parity — runtime-launcher-parity — agents (#1041) ─────────────────
|
||||
describe('runtime-launcher-parity — agents (#1041)', () => {
|
||||
// ─── (B-agents) Exactly ONE canonical preamble per using agent file ────────
|
||||
|
||||
@@ -81,9 +81,10 @@ const GRACE = 3000;
|
||||
// current high-water mark within GRACE (#597 tighten-only ratchet).
|
||||
// XL high-water mark is execute-phase.md — note that under LINES it was
|
||||
// plan-phase; bytes genuinely re-rank the tier, which is the point of #717.
|
||||
// actualMax=92525 (execute-phase, #913 inline-fallback scope clarification);
|
||||
// slack=475 ≤ GRACE. plan-phase.md=90748 (#922 attempt-based Agent gate), new-project.md=58110.
|
||||
const XL_BUDGET = 93000;
|
||||
// actualMax=93130 (plan-phase, #381 CLAUDE_ENV_FILE persist clause in per-file launcher preamble);
|
||||
// slack=70 ≤ GRACE. execute-phase.md=92880, new-project.md=61685.
|
||||
// 93200: +200B headroom for the #381 CLAUDE_ENV_FILE persist clause added to every per-file launcher preamble (legit content growth, ratchet-up per #717).
|
||||
const XL_BUDGET = 93200;
|
||||
// LARGE high-water mark is docs-update.md. actualMax=54410 (#891 launcher shim expansion);
|
||||
// slack=1590 ≤ GRACE. quick.md=45710, autonomous.md=38030.
|
||||
const LARGE_BUDGET = 56000;
|
||||
@@ -95,9 +96,9 @@ const DEFAULT_BUDGET = 40000;
|
||||
// Grandfathered at current sizes — see PR #2551 for the progressive-disclosure
|
||||
// pattern that future shrinks should follow. Byte counts noted for reference.
|
||||
const XL_WORKFLOWS = new Set([
|
||||
'execute-phase', // 92525 bytes (tier high-water mark; grew in #913 inline-fallback scope clarification)
|
||||
'plan-phase', // 90748 bytes (grew in #922 attempt-based Agent gate)
|
||||
'new-project', // 55850 bytes
|
||||
'execute-phase', // 92880 bytes (grew in #381 CLAUDE_ENV_FILE persist clause)
|
||||
'plan-phase', // 93130 bytes (tier high-water mark; grew in #381 CLAUDE_ENV_FILE persist clause)
|
||||
'new-project', // 61685 bytes
|
||||
]);
|
||||
|
||||
// Multi-step planners and bigger feature workflows. Grandfathered.
|
||||
|
||||
Reference in New Issue
Block a user