fix(coding-agent): rebind relative extension roots

This commit is contained in:
Sunil Srivatsa
2026-08-01 13:24:11 -04:00
parent b1a46eec5c
commit df38e5b48e
2 changed files with 29 additions and 4 deletions
@@ -36,6 +36,8 @@ export interface OmpExtensionRoot {
interface InjectedRoot {
path: string;
/** Relative CLI spelling, rebound against the active project on discovery. */
relativePath?: string;
level: "user" | "project";
}
@@ -73,13 +75,16 @@ export function injectOmpExtensionCliRoots(
if (paths.length === 0) return;
const expanded = paths.map(raw => {
const tilde = expandTilde(raw, home);
return path.isAbsolute(tilde) ? tilde : path.resolve(cwd, tilde);
return {
path: path.isAbsolute(tilde) ? tilde : path.resolve(cwd, tilde),
relativePath: path.isAbsolute(tilde) ? undefined : tilde,
};
});
const merged = new Map<string, InjectedRoot>();
for (const root of injectedCliRoots) merged.set(root.path, root);
for (const resolved of expanded) {
for (const { path: resolved, relativePath } of expanded) {
// CLI scope mirrors how `--extension` is treated elsewhere — user-level overrides win.
if (!merged.has(resolved)) merged.set(resolved, { path: resolved, level: "user" });
if (!merged.has(resolved)) merged.set(resolved, { path: resolved, relativePath, level: "user" });
}
injectedCliRoots = [...merged.values()];
}
@@ -154,7 +159,9 @@ async function isDirectory(p: string): Promise<boolean> {
* other sources still surface.
*/
export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtensionRoot[]> {
let candidates: InjectedRoot[] = [...injectedCliRoots];
let candidates: InjectedRoot[] = injectedCliRoots.map(root =>
root.relativePath ? { ...root, path: path.resolve(ctx.cwd, root.relativePath) } : root,
);
if (injectedCliRootMode === "merge") {
const { project, user } = scopeDirs(ctx);
const [projectExtensions, userExtensions, installedPlugins] = await Promise.all([
@@ -158,6 +158,24 @@ test("`--extension` CLI injection is wired through the same provider", async ()
expect(tools.map(t => t.name)).toEqual(expect.arrayContaining(["wcount", "deep-tool"]));
});
test("relative CLI roots rebind when resume switches projects", async () => {
const relativeRoot = "relative-extension";
const launchRoot = path.join(project, relativeRoot);
const destination = path.join(tempDir, "destination");
const destinationRoot = path.join(destination, relativeRoot);
buildExtensionPackage(launchRoot, "launch-skill");
buildExtensionPackage(destinationRoot, "destination-skill");
injectOmpExtensionCliRoots([`./${relativeRoot}`], home, project);
const destinationContext = { cwd: destination, home, repoRoot: destination };
const roots = await listOmpExtensionRoots(destinationContext);
const skills = await loadFromPlugin<{ name: string }>(skillCapability.id, destinationContext);
expect(roots.map(root => root.path)).toEqual([destinationRoot]);
expect(skills.map(skill => skill.name)).toContain("destination-skill");
expect(skills.map(skill => skill.name)).not.toContain("launch-skill");
});
test("explicit-only CLI roots replace stale state and exclude every ambient package source", async () => {
const stale = path.join(tempDir, "stale-extension");
const projectExt = path.join(tempDir, "project-extension");