Merge pull request #3958 from jeremymcs/fix/3949-auto-resume-resolve

fix(auto): resolve resource-loader.js from GSD_PKG_ROOT on resume
This commit is contained in:
Jeremy McSpadden 2026-04-10 20:28:36 -05:00 committed by GitHub
commit 925d6088af
4 changed files with 81 additions and 15 deletions

View file

@ -110,6 +110,11 @@ if (!existsSync(appRoot)) {
// GSD_CODING_AGENT_DIR — tells pi's getAgentDir() to return ~/.gsd/agent/ instead of ~/.gsd/agent/
process.env.GSD_CODING_AGENT_DIR = agentDir
// GSD_PKG_ROOT — absolute path to gsd-pi package root. Used by deployed extensions
// (e.g. auto.ts resume path) to import modules like resource-loader.js that live
// in the package tree, not in the deployed ~/.gsd/agent/ tree.
process.env.GSD_PKG_ROOT = gsdRoot
// RTK environment — make ~/.gsd/agent/bin visible to all child-process paths,
// not just the bash tool, and force-disable RTK telemetry for GSD-managed use.
applyRtkProcessEnv(process.env)

View file

@ -125,9 +125,9 @@ import {
} from "./metrics.js";
import { setLogBasePath, logWarning, logError } from "./workflow-logger.js";
import { homedir } from "node:os";
import { join, dirname } from "node:path";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
import { createRequire } from "node:module";
import { atomicWriteSync } from "./atomic-write.js";
import {
autoCommitCurrentBranch,
@ -1334,13 +1334,17 @@ export async function startAuto(
restoreHookState(s.basePath);
// Re-sync managed resources on resume so long-lived auto sessions pick up
// bundled extension updates before resume-time verification/state logic runs.
// GSD_PKG_ROOT is set by loader.ts and points to the gsd-pi package root.
// The relative import ("../../../resource-loader.js") only works from the source
// tree; deployed extensions live at ~/.gsd/agent/extensions/gsd/ where the
// relative path resolves to ~/.gsd/agent/resource-loader.js which doesn't exist.
// Using GSD_PKG_ROOT constructs a correct absolute path in both contexts (#3949).
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(process.env.GSD_HOME || homedir(), ".gsd", "agent");
// Resolve resource-loader from the gsd-pi package root — the relative
// "../../../resource-loader.js" path only works from the source tree but
// breaks when extensions are deployed to ~/.gsd/agent/extensions/gsd/.
const _req = createRequire(import.meta.url);
const pkgRoot = dirname(_req.resolve("gsd-pi/package.json"));
const { initResources } = await import(join(pkgRoot, "dist", "resource-loader.js"));
const pkgRoot = process.env.GSD_PKG_ROOT;
const resourceLoaderPath = pkgRoot
? pathToFileURL(join(pkgRoot, "dist", "resource-loader.js")).href
: new URL("../../../resource-loader.js", import.meta.url).href;
const { initResources } = await import(resourceLoaderPath);
initResources(agentDir);
// Open the project DB before rebuild/derive so resume uses DB-backed
// state instead of falling back to stale markdown parsing (#2940).

View file

@ -22,16 +22,17 @@ describe("resource-loader import path", () => {
);
});
test("uses createRequire to resolve resource-loader from package root", () => {
// The fix uses createRequire to find gsd-pi/package.json, then imports
// dist/resource-loader.js from there — works in both source and deployed.
test("uses GSD_PKG_ROOT to resolve resource-loader from package root", () => {
// The fix uses GSD_PKG_ROOT (set by loader.ts) to construct an absolute
// file URL to dist/resource-loader.js — works in both source and deployed,
// and on Windows where raw paths fail with ERR_UNSUPPORTED_ESM_URL_SCHEME.
assert.ok(
autoSrc.includes('createRequire(import.meta.url)'),
"auto.ts should use createRequire to resolve resource-loader",
autoSrc.includes('process.env.GSD_PKG_ROOT'),
"auto.ts should use GSD_PKG_ROOT to resolve resource-loader",
);
assert.ok(
autoSrc.includes('resolve("gsd-pi/package.json")'),
"auto.ts should resolve gsd-pi package root via package.json",
autoSrc.includes('pathToFileURL'),
"auto.ts should convert path to file URL for cross-platform import()",
);
});
});

View file

@ -0,0 +1,56 @@
// GSD2 — Regression test: auto-mode resume resolves resource-loader.js from deployed path (#3949)
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join, resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const autoTsPath = join(__dirname, "..", "resources", "extensions", "gsd", "auto.ts");
const loaderTsPath = join(__dirname, "..", "loader.ts");
test("loader.ts sets GSD_PKG_ROOT env var", () => {
const loaderSrc = readFileSync(loaderTsPath, "utf-8");
assert.ok(
loaderSrc.includes("process.env.GSD_PKG_ROOT"),
"loader.ts must set GSD_PKG_ROOT so deployed extensions can locate package-root modules",
);
});
test("auto.ts resume uses GSD_PKG_ROOT for resource-loader import, not bare relative path", () => {
const autoSrc = readFileSync(autoTsPath, "utf-8");
// Must reference GSD_PKG_ROOT to build an absolute path
assert.ok(
autoSrc.includes("process.env.GSD_PKG_ROOT"),
"auto.ts must use GSD_PKG_ROOT to resolve resource-loader.js from deployed extension path",
);
// The import must use the computed variable (resourceLoaderPath), not a hardcoded relative path.
assert.ok(
autoSrc.includes("await import(resourceLoaderPath)"),
"auto.ts resource-loader import must use the computed resourceLoaderPath variable, not a hardcoded relative path",
);
// The resourceLoaderPath must be constructed from GSD_PKG_ROOT via pathToFileURL
// (raw filesystem paths break on Windows with ERR_UNSUPPORTED_ESM_URL_SCHEME)
assert.ok(
autoSrc.includes("pathToFileURL(join(pkgRoot,"),
"auto.ts must convert the constructed path to a file URL for cross-platform import()",
);
});
test("GSD_PKG_ROOT resolves resource-loader.js correctly from package root", () => {
// Simulate what auto.ts does: given GSD_PKG_ROOT, construct the path
const pkgRoot = resolve(__dirname, "..", "..");
const resourceLoaderPath = join(pkgRoot, "dist", "resource-loader.js");
// After build, dist/resource-loader.js should exist
// (this test runs post-build in CI; in dev it validates the path construction)
const expectedDir = dirname(resourceLoaderPath);
assert.ok(
expectedDir.endsWith(join("dist")),
`resource-loader path should be under dist/, got: ${expectedDir}`,
);
});