From c6eb13b91b120eeba055d6e3a0540cd5b5c91f1a Mon Sep 17 00:00:00 2001 From: Daniel Rosales Date: Thu, 23 Jul 2026 00:28:07 -0500 Subject: [PATCH 1/7] fix(opencode): handle Windows project basenames --- internal/setup/plugins/opencode/engram.ts | 10 ++- plugin/opencode/engram.test.ts | 77 +++++++++++++++++++++++ plugin/opencode/engram.ts | 10 ++- 3 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 plugin/opencode/engram.test.ts diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index c5567087..ab603394 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -153,6 +153,10 @@ async function isEngramRunning(): Promise { // ─── Helpers ───────────────────────────────────────────────────────────────── +function basename(path: string): string { + return path.split(/[\\/]/).pop() ?? "unknown" +} + function extractProjectName(directory: string): string { // Try git remote origin URL try { @@ -171,12 +175,12 @@ function extractProjectName(directory: string): string { const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"]) if (result.exitCode === 0) { const root = result.stdout?.toString().trim() - if (root) return root.split("/").pop() ?? "unknown" + if (root) return basename(root) } } catch {} // Final fallback: cwd basename - return directory.split("/").pop() ?? "unknown" + return basename(directory) } function truncate(str: string, max: number): string { @@ -197,7 +201,7 @@ function stripPrivateTags(str: string): string { // ─── Plugin Export ─────────────────────────────────────────────────────────── export const Engram: Plugin = async (ctx) => { - const oldProject = ctx.directory.split("/").pop() ?? "unknown" + const oldProject = basename(ctx.directory) const project = extractProjectName(ctx.directory) // Track tool counts per session (in-memory only, not critical) diff --git a/plugin/opencode/engram.test.ts b/plugin/opencode/engram.test.ts new file mode 100644 index 00000000..db27c81c --- /dev/null +++ b/plugin/opencode/engram.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict" +import { test } from "node:test" + +import { Engram } from "./engram.ts" + +type SpawnResult = { + exitCode: number + stdout?: Buffer +} + +async function createPlugin( + directory: string, + spawnSync: (command: string[]) => SpawnResult, +) { + const requests: Array<{ path: string; body?: unknown }> = [] + + globalThis.Bun = { + spawnSync, + file: () => ({ exists: async () => false }), + } as typeof Bun + + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const url = new URL(input.toString()) + const body = init?.body ? JSON.parse(init.body.toString()) : undefined + requests.push({ path: url.pathname, body }) + + return new Response(JSON.stringify({}), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) + }) as typeof fetch + + const plugin = await Engram({ directory } as never) + return { plugin, requests } +} + +test("uses the Windows basename in compaction recovery outside Git", async () => { + const { plugin } = await createPlugin("C:\\Users\\Blackie", () => ({ exitCode: 1 })) + const output = { context: [] as string[] } + + await plugin["experimental.session.compacting"]?.( + { sessionID: "session-652" } as never, + output, + ) + + assert.match(output.context.at(-1) ?? "", /Use project: 'Blackie'/) + assert.doesNotMatch(output.context.at(-1) ?? "", /C:\\Users\\Blackie/) +}) + +test("uses the Windows basename as old_project during migration", async () => { + const { requests } = await createPlugin("C:\\Users\\Blackie", (command) => { + if (command.includes("get-url")) { + return { + exitCode: 0, + stdout: Buffer.from("https://github.com/Gentleman-Programming/engram.git\n"), + } + } + return { exitCode: 1 } + }) + + assert.deepEqual( + requests.find(({ path }) => path === "/projects/migrate")?.body, + { old_project: "Blackie", new_project: "engram" }, + ) +}) + +test("preserves POSIX basename fallback behavior", async () => { + const { plugin } = await createPlugin("/home/blackie", () => ({ exitCode: 1 })) + const output = { context: [] as string[] } + + await plugin["experimental.session.compacting"]?.( + { sessionID: "session-posix" } as never, + output, + ) + + assert.match(output.context.at(-1) ?? "", /Use project: 'blackie'/) +}) diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index c5567087..ab603394 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -153,6 +153,10 @@ async function isEngramRunning(): Promise { // ─── Helpers ───────────────────────────────────────────────────────────────── +function basename(path: string): string { + return path.split(/[\\/]/).pop() ?? "unknown" +} + function extractProjectName(directory: string): string { // Try git remote origin URL try { @@ -171,12 +175,12 @@ function extractProjectName(directory: string): string { const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"]) if (result.exitCode === 0) { const root = result.stdout?.toString().trim() - if (root) return root.split("/").pop() ?? "unknown" + if (root) return basename(root) } } catch {} // Final fallback: cwd basename - return directory.split("/").pop() ?? "unknown" + return basename(directory) } function truncate(str: string, max: number): string { @@ -197,7 +201,7 @@ function stripPrivateTags(str: string): string { // ─── Plugin Export ─────────────────────────────────────────────────────────── export const Engram: Plugin = async (ctx) => { - const oldProject = ctx.directory.split("/").pop() ?? "unknown" + const oldProject = basename(ctx.directory) const project = extractProjectName(ctx.directory) // Track tool counts per session (in-memory only, not critical) From b8ee6d64798b3363d69416a47b8fc32e25bb82b7 Mon Sep 17 00:00:00 2001 From: Daniel Rosales Date: Thu, 23 Jul 2026 00:41:53 -0500 Subject: [PATCH 2/7] fix(opencode): handle trailing project separators --- internal/setup/plugins/opencode/engram.ts | 2 +- plugin/opencode/engram.test.ts | 32 +++++++++++++++++++++++ plugin/opencode/engram.ts | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index ab603394..4781c68f 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -154,7 +154,7 @@ async function isEngramRunning(): Promise { // ─── Helpers ───────────────────────────────────────────────────────────────── function basename(path: string): string { - return path.split(/[\\/]/).pop() ?? "unknown" + return path.split(/[\\/]/).filter(Boolean).pop() ?? "unknown" } function extractProjectName(directory: string): string { diff --git a/plugin/opencode/engram.test.ts b/plugin/opencode/engram.test.ts index db27c81c..94e794bb 100644 --- a/plugin/opencode/engram.test.ts +++ b/plugin/opencode/engram.test.ts @@ -47,6 +47,38 @@ test("uses the Windows basename in compaction recovery outside Git", async () => assert.doesNotMatch(output.context.at(-1) ?? "", /C:\\Users\\Blackie/) }) +test("ignores trailing Windows separators in the basename fallback", async () => { + const { plugin } = await createPlugin("C:\\Users\\Blackie\\", () => ({ exitCode: 1 })) + const output = { context: [] as string[] } + + await plugin["experimental.session.compacting"]?.( + { sessionID: "session-652-trailing" } as never, + output, + ) + + assert.match(output.context.at(-1) ?? "", /Use project: 'Blackie'/) +}) + +test("uses the Windows basename from the Git-root fallback", async () => { + const { plugin } = await createPlugin("C:\\Users\\Blackie\\project", (command) => { + if (command.includes("rev-parse")) { + return { + exitCode: 0, + stdout: Buffer.from("C:\\Users\\Blackie\\worktrees\\engram-652\n"), + } + } + return { exitCode: 1 } + }) + const output = { context: [] as string[] } + + await plugin["experimental.session.compacting"]?.( + { sessionID: "session-652-git-root" } as never, + output, + ) + + assert.match(output.context.at(-1) ?? "", /Use project: 'engram-652'/) +}) + test("uses the Windows basename as old_project during migration", async () => { const { requests } = await createPlugin("C:\\Users\\Blackie", (command) => { if (command.includes("get-url")) { diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index ab603394..4781c68f 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -154,7 +154,7 @@ async function isEngramRunning(): Promise { // ─── Helpers ───────────────────────────────────────────────────────────────── function basename(path: string): string { - return path.split(/[\\/]/).pop() ?? "unknown" + return path.split(/[\\/]/).filter(Boolean).pop() ?? "unknown" } function extractProjectName(directory: string): string { From 560fe424914e6ecd0cc8dbc075fa38bc63ea6b16 Mon Sep 17 00:00:00 2001 From: Daniel Rosales Date: Thu, 23 Jul 2026 00:52:40 -0500 Subject: [PATCH 3/7] fix(opencode): preserve legacy migration key --- internal/setup/plugins/opencode/engram.ts | 3 ++- plugin/opencode/engram.test.ts | 4 ++-- plugin/opencode/engram.ts | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index 4781c68f..64dd1d53 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -201,7 +201,8 @@ function stripPrivateTags(str: string): string { // ─── Plugin Export ─────────────────────────────────────────────────────────── export const Engram: Plugin = async (ctx) => { - const oldProject = basename(ctx.directory) + // Preserve the previous plugin's key so Windows path-based records can migrate. + const oldProject = ctx.directory.split("/").pop() ?? "unknown" const project = extractProjectName(ctx.directory) // Track tool counts per session (in-memory only, not critical) diff --git a/plugin/opencode/engram.test.ts b/plugin/opencode/engram.test.ts index 94e794bb..1a3992c0 100644 --- a/plugin/opencode/engram.test.ts +++ b/plugin/opencode/engram.test.ts @@ -79,7 +79,7 @@ test("uses the Windows basename from the Git-root fallback", async () => { assert.match(output.context.at(-1) ?? "", /Use project: 'engram-652'/) }) -test("uses the Windows basename as old_project during migration", async () => { +test("migrates the legacy Windows project key to the remote project", async () => { const { requests } = await createPlugin("C:\\Users\\Blackie", (command) => { if (command.includes("get-url")) { return { @@ -92,7 +92,7 @@ test("uses the Windows basename as old_project during migration", async () => { assert.deepEqual( requests.find(({ path }) => path === "/projects/migrate")?.body, - { old_project: "Blackie", new_project: "engram" }, + { old_project: "C:\\Users\\Blackie", new_project: "engram" }, ) }) diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index 4781c68f..64dd1d53 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -201,7 +201,8 @@ function stripPrivateTags(str: string): string { // ─── Plugin Export ─────────────────────────────────────────────────────────── export const Engram: Plugin = async (ctx) => { - const oldProject = basename(ctx.directory) + // Preserve the previous plugin's key so Windows path-based records can migrate. + const oldProject = ctx.directory.split("/").pop() ?? "unknown" const project = extractProjectName(ctx.directory) // Track tool counts per session (in-memory only, not critical) From 4cd6312757661d8642ebd18719d28a7e94776b3d Mon Sep 17 00:00:00 2001 From: Daniel Rosales Date: Thu, 23 Jul 2026 01:03:00 -0500 Subject: [PATCH 4/7] fix(opencode): migrate legacy Git-root keys --- internal/setup/plugins/opencode/engram.ts | 20 +++++++++++++------- plugin/opencode/engram.test.ts | 18 ++++++++++++++++-- plugin/opencode/engram.ts | 20 +++++++++++++------- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index 64dd1d53..1089f3e5 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -157,7 +157,7 @@ function basename(path: string): string { return path.split(/[\\/]/).filter(Boolean).pop() ?? "unknown" } -function extractProjectName(directory: string): string { +function extractProjectNames(directory: string): { oldProject: string; project: string } { // Try git remote origin URL try { const result = Bun.spawnSync(["git", "-C", directory, "remote", "get-url", "origin"]) @@ -165,7 +165,7 @@ function extractProjectName(directory: string): string { const url = result.stdout?.toString().trim() if (url) { const name = url.replace(/\.git$/, "").split(/[/:]/).pop() - if (name) return name + if (name) return { oldProject: name, project: name } } } } catch {} @@ -175,12 +175,20 @@ function extractProjectName(directory: string): string { const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"]) if (result.exitCode === 0) { const root = result.stdout?.toString().trim() - if (root) return basename(root) + if (root) { + return { + oldProject: root.split("/").pop() ?? "unknown", + project: basename(root), + } + } } } catch {} // Final fallback: cwd basename - return basename(directory) + return { + oldProject: directory.split("/").pop() ?? "unknown", + project: basename(directory), + } } function truncate(str: string, max: number): string { @@ -201,9 +209,7 @@ function stripPrivateTags(str: string): string { // ─── Plugin Export ─────────────────────────────────────────────────────────── export const Engram: Plugin = async (ctx) => { - // Preserve the previous plugin's key so Windows path-based records can migrate. - const oldProject = ctx.directory.split("/").pop() ?? "unknown" - const project = extractProjectName(ctx.directory) + const { oldProject, project } = extractProjectNames(ctx.directory) // Track tool counts per session (in-memory only, not critical) const toolCounts = new Map() diff --git a/plugin/opencode/engram.test.ts b/plugin/opencode/engram.test.ts index 1a3992c0..c11db3a5 100644 --- a/plugin/opencode/engram.test.ts +++ b/plugin/opencode/engram.test.ts @@ -79,7 +79,7 @@ test("uses the Windows basename from the Git-root fallback", async () => { assert.match(output.context.at(-1) ?? "", /Use project: 'engram-652'/) }) -test("migrates the legacy Windows project key to the remote project", async () => { +test("does not migrate when the legacy remote project is unchanged", async () => { const { requests } = await createPlugin("C:\\Users\\Blackie", (command) => { if (command.includes("get-url")) { return { @@ -90,9 +90,23 @@ test("migrates the legacy Windows project key to the remote project", async () = return { exitCode: 1 } }) + assert.equal(requests.some(({ path }) => path === "/projects/migrate"), false) +}) + +test("migrates the legacy Windows Git-root key from a nested directory", async () => { + const { requests } = await createPlugin("C:\\repos\\engram\\nested", (command) => { + if (command.includes("rev-parse")) { + return { + exitCode: 0, + stdout: Buffer.from("C:\\repos\\engram\n"), + } + } + return { exitCode: 1 } + }) + assert.deepEqual( requests.find(({ path }) => path === "/projects/migrate")?.body, - { old_project: "C:\\Users\\Blackie", new_project: "engram" }, + { old_project: "C:\\repos\\engram", new_project: "engram" }, ) }) diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index 64dd1d53..1089f3e5 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -157,7 +157,7 @@ function basename(path: string): string { return path.split(/[\\/]/).filter(Boolean).pop() ?? "unknown" } -function extractProjectName(directory: string): string { +function extractProjectNames(directory: string): { oldProject: string; project: string } { // Try git remote origin URL try { const result = Bun.spawnSync(["git", "-C", directory, "remote", "get-url", "origin"]) @@ -165,7 +165,7 @@ function extractProjectName(directory: string): string { const url = result.stdout?.toString().trim() if (url) { const name = url.replace(/\.git$/, "").split(/[/:]/).pop() - if (name) return name + if (name) return { oldProject: name, project: name } } } } catch {} @@ -175,12 +175,20 @@ function extractProjectName(directory: string): string { const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"]) if (result.exitCode === 0) { const root = result.stdout?.toString().trim() - if (root) return basename(root) + if (root) { + return { + oldProject: root.split("/").pop() ?? "unknown", + project: basename(root), + } + } } } catch {} // Final fallback: cwd basename - return basename(directory) + return { + oldProject: directory.split("/").pop() ?? "unknown", + project: basename(directory), + } } function truncate(str: string, max: number): string { @@ -201,9 +209,7 @@ function stripPrivateTags(str: string): string { // ─── Plugin Export ─────────────────────────────────────────────────────────── export const Engram: Plugin = async (ctx) => { - // Preserve the previous plugin's key so Windows path-based records can migrate. - const oldProject = ctx.directory.split("/").pop() ?? "unknown" - const project = extractProjectName(ctx.directory) + const { oldProject, project } = extractProjectNames(ctx.directory) // Track tool counts per session (in-memory only, not critical) const toolCounts = new Map() From b6132d3988819e59018948f48ad6e415d2ef3159 Mon Sep 17 00:00:00 2001 From: Daniel Rosales Date: Thu, 23 Jul 2026 01:09:18 -0500 Subject: [PATCH 5/7] test(opencode): cover legacy migration payloads --- plugin/opencode/engram.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugin/opencode/engram.test.ts b/plugin/opencode/engram.test.ts index c11db3a5..15ad0f3c 100644 --- a/plugin/opencode/engram.test.ts +++ b/plugin/opencode/engram.test.ts @@ -35,7 +35,7 @@ async function createPlugin( } test("uses the Windows basename in compaction recovery outside Git", async () => { - const { plugin } = await createPlugin("C:\\Users\\Blackie", () => ({ exitCode: 1 })) + const { plugin, requests } = await createPlugin("C:\\Users\\Blackie", () => ({ exitCode: 1 })) const output = { context: [] as string[] } await plugin["experimental.session.compacting"]?.( @@ -45,10 +45,14 @@ test("uses the Windows basename in compaction recovery outside Git", async () => assert.match(output.context.at(-1) ?? "", /Use project: 'Blackie'/) assert.doesNotMatch(output.context.at(-1) ?? "", /C:\\Users\\Blackie/) + assert.deepEqual( + requests.find(({ path }) => path === "/projects/migrate")?.body, + { old_project: "C:\\Users\\Blackie", new_project: "Blackie" }, + ) }) test("ignores trailing Windows separators in the basename fallback", async () => { - const { plugin } = await createPlugin("C:\\Users\\Blackie\\", () => ({ exitCode: 1 })) + const { plugin, requests } = await createPlugin("C:\\Users\\Blackie\\", () => ({ exitCode: 1 })) const output = { context: [] as string[] } await plugin["experimental.session.compacting"]?.( @@ -57,6 +61,10 @@ test("ignores trailing Windows separators in the basename fallback", async () => ) assert.match(output.context.at(-1) ?? "", /Use project: 'Blackie'/) + assert.deepEqual( + requests.find(({ path }) => path === "/projects/migrate")?.body, + { old_project: "C:\\Users\\Blackie\\", new_project: "Blackie" }, + ) }) test("uses the Windows basename from the Git-root fallback", async () => { From a9794f3864d774a2341c4fc3faf6ddf6392cfef1 Mon Sep 17 00:00:00 2001 From: Daniel Rosales Date: Thu, 23 Jul 2026 01:15:46 -0500 Subject: [PATCH 6/7] test(opencode): cover trailing POSIX migration --- plugin/opencode/engram.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugin/opencode/engram.test.ts b/plugin/opencode/engram.test.ts index 15ad0f3c..bc64ba3c 100644 --- a/plugin/opencode/engram.test.ts +++ b/plugin/opencode/engram.test.ts @@ -129,3 +129,12 @@ test("preserves POSIX basename fallback behavior", async () => { assert.match(output.context.at(-1) ?? "", /Use project: 'blackie'/) }) + +test("migrates the legacy empty POSIX key for a trailing separator", async () => { + const { requests } = await createPlugin("/home/blackie/", () => ({ exitCode: 1 })) + + assert.deepEqual( + requests.find(({ path }) => path === "/projects/migrate")?.body, + { old_project: "", new_project: "blackie" }, + ) +}) From d0ee2455ba362b1c4ee87e1a18f6bb31277cd71f Mon Sep 17 00:00:00 2001 From: Daniel Rosales Date: Thu, 23 Jul 2026 01:25:48 -0500 Subject: [PATCH 7/7] fix(opencode): reject Windows drive roots --- internal/setup/plugins/opencode/engram.ts | 3 ++- plugin/opencode/engram.test.ts | 18 ++++++++++++++++++ plugin/opencode/engram.ts | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/internal/setup/plugins/opencode/engram.ts b/internal/setup/plugins/opencode/engram.ts index 1089f3e5..51ab1878 100644 --- a/internal/setup/plugins/opencode/engram.ts +++ b/internal/setup/plugins/opencode/engram.ts @@ -154,7 +154,8 @@ async function isEngramRunning(): Promise { // ─── Helpers ───────────────────────────────────────────────────────────────── function basename(path: string): string { - return path.split(/[\\/]/).filter(Boolean).pop() ?? "unknown" + const name = path.split(/[\\/]/).filter(Boolean).pop() + return name && !/^[A-Za-z]:$/.test(name) ? name : "unknown" } function extractProjectNames(directory: string): { oldProject: string; project: string } { diff --git a/plugin/opencode/engram.test.ts b/plugin/opencode/engram.test.ts index bc64ba3c..1e953f14 100644 --- a/plugin/opencode/engram.test.ts +++ b/plugin/opencode/engram.test.ts @@ -138,3 +138,21 @@ test("migrates the legacy empty POSIX key for a trailing separator", async () => { old_project: "", new_project: "blackie" }, ) }) + +for (const [directory, oldProject] of [["C:\\", "C:\\"], ["C:/", ""]]) { + test(`uses unknown for the Windows drive root ${directory}`, async () => { + const { plugin, requests } = await createPlugin(directory, () => ({ exitCode: 1 })) + const output = { context: [] as string[] } + + await plugin["experimental.session.compacting"]?.( + { sessionID: `session-drive-root-${directory}` } as never, + output, + ) + + assert.match(output.context.at(-1) ?? "", /Use project: 'unknown'/) + assert.deepEqual( + requests.find(({ path }) => path === "/projects/migrate")?.body, + { old_project: oldProject, new_project: "unknown" }, + ) + }) +} diff --git a/plugin/opencode/engram.ts b/plugin/opencode/engram.ts index 1089f3e5..51ab1878 100644 --- a/plugin/opencode/engram.ts +++ b/plugin/opencode/engram.ts @@ -154,7 +154,8 @@ async function isEngramRunning(): Promise { // ─── Helpers ───────────────────────────────────────────────────────────────── function basename(path: string): string { - return path.split(/[\\/]/).filter(Boolean).pop() ?? "unknown" + const name = path.split(/[\\/]/).filter(Boolean).pop() + return name && !/^[A-Za-z]:$/.test(name) ? name : "unknown" } function extractProjectNames(directory: string): { oldProject: string; project: string } {