diff --git a/src/cli.ts b/src/cli.ts index 28e7e12d6..b14d5b1f1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -298,6 +298,10 @@ if (cliFlags.messages[0] === 'sessions') { // `gsd headless` — run auto-mode without TUI if (cliFlags.messages[0] === 'headless') { await ensureRtkBootstrap() + // Sync bundled resources before headless runs (#3471). Without this, + // headless-query loads from src/resources/ while auto/interactive load + // from ~/.gsd/agent/extensions/ — different extension copies diverge. + initResources(agentDir) const { runHeadless, parseHeadlessArgs } = await import('./headless.js') await runHeadless(parseHeadlessArgs(process.argv)) process.exit(0) diff --git a/src/headless-query.ts b/src/headless-query.ts index c9aa6ae2b..cc7c134c3 100644 --- a/src/headless-query.ts +++ b/src/headless-query.ts @@ -16,12 +16,22 @@ import { createJiti } from '@mariozechner/jiti' import { fileURLToPath } from 'node:url' +import { join } from 'node:path' +import { homedir } from 'node:os' import type { GSDState } from './resources/extensions/gsd/types.js' import { resolveBundledSourceResource } from './bundled-resource-path.js' const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false }) +// Resolve extensions from the synced agent directory so headless-query +// loads the same extension copy as interactive/auto modes (#3471). +// Falls back to bundled source for source-tree dev workflows. +const agentExtensionsDir = join(process.env.GSD_AGENT_DIR || join(homedir(), '.gsd', 'agent'), 'extensions', 'gsd') +const { existsSync } = await import('node:fs') +const useAgentDir = existsSync(join(agentExtensionsDir, 'state.ts')) const gsdExtensionPath = (...segments: string[]) => - resolveBundledSourceResource(import.meta.url, 'extensions', 'gsd', ...segments) + useAgentDir + ? join(agentExtensionsDir, ...segments) + : resolveBundledSourceResource(import.meta.url, 'extensions', 'gsd', ...segments) async function loadExtensionModules() { const stateModule = await jiti.import(gsdExtensionPath('state.ts'), {}) as any diff --git a/src/tests/headless-query-extension-path.test.ts b/src/tests/headless-query-extension-path.test.ts new file mode 100644 index 000000000..499509187 --- /dev/null +++ b/src/tests/headless-query-extension-path.test.ts @@ -0,0 +1,28 @@ +/** + * Regression test for #3471: headless-query must load extensions from + * the synced agent directory, not directly from src/resources/. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +test("headless-query resolves from agent extensions dir (#3471)", () => { + const src = readFileSync(join(__dirname, "..", "headless-query.ts"), "utf-8"); + assert.ok( + src.includes("agentExtensionsDir") || src.includes(".gsd/agent"), + "headless-query must resolve from synced agent directory", + ); +}); + +test("cli.ts calls initResources before headless (#3471)", () => { + const src = readFileSync(join(__dirname, "..", "cli.ts"), "utf-8"); + const headlessBlock = src.slice(src.indexOf("gsd headless")); + const initIdx = headlessBlock.indexOf("initResources"); + const runIdx = headlessBlock.indexOf("runHeadless"); + assert.ok(initIdx !== -1, "initResources must be called before headless"); + assert.ok(initIdx < runIdx, "initResources must come before runHeadless"); +});