diff --git a/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md b/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md index 5286457a..ca1419b3 100644 --- a/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md +++ b/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md @@ -16,7 +16,7 @@ Bun provides two subprocess APIs: - **`Bun.$` (shell template literals)** — A shell-like API that pipes commands through a subprocess shell. Convenient syntax (`await Bun.$\`git ls-files\`.text()`), but relies on platform-specific shell behavior. - **`Bun.spawn` (array-based)** — A lower-level API that executes a command directly (no intermediate shell). Takes an array of arguments, explicit pipe configuration, and returns a process handle with `stdout`, `stderr`, and `exited` properties. -**The problem:** `Bun.$` hangs on Windows. The shell subprocess does not properly close stdin/stdout pipes, causing deadlocks that block the calling thread indefinitely. When the Archgate CLI runs as an MCP server inside Claude Code or Cursor, this deadlock freezes the entire editor's agent interface — the user must force-kill the process. This was discovered in production and fixed in commit `ca33377`, which replaced all `Bun.$` calls with `Bun.spawn`. +**The problem:** `Bun.$` hangs on Windows. The shell subprocess does not properly close stdin/stdout pipes, causing deadlocks that block the calling thread indefinitely. This was discovered in production and fixed in commit `ca33377`, which replaced all `Bun.$` calls with `Bun.spawn`. **Alternatives considered:** @@ -136,7 +136,6 @@ Bun.spawn(["git diff --cached | head -5"]); // This is a single argument, not a - **Cross-platform reliability** — `Bun.spawn` works identically on macOS, Linux, and Windows. No platform-specific pipe handling differences. - **No deadlocks** — Array-based execution avoids the stdin/stdout pipe issues that cause `Bun.$` to hang on Windows. -- **MCP server safety** — The CLI runs as a long-lived MCP server inside editors. A subprocess deadlock would freeze the entire agent interface. `Bun.spawn` eliminates this risk. - **Explicit argument handling** — Array-based arguments prevent shell injection vulnerabilities. Each argument is passed directly to the command, not interpreted by a shell. - **No shell dependency** — The command does not require a shell interpreter (bash, cmd.exe, PowerShell) to be available or configured correctly. - **Consistent error handling** — `proc.exited` returns a Promise that resolves to the exit code, making error checking uniform across all subprocess calls. diff --git a/.archgate/config.json b/.archgate/config.json index e57fecf4..18262f90 100644 --- a/.archgate/config.json +++ b/.archgate/config.json @@ -1 +1 @@ -{ "domains": { "ci": "CI" } } +{ "domains": { "ci": "CI" }, "baseBranch": "origin/main" } diff --git a/src/engine/loader.ts b/src/engine/loader.ts index fc7b5045..284d22df 100644 --- a/src/engine/loader.ts +++ b/src/engine/loader.ts @@ -308,11 +308,8 @@ export async function loadRuleAdrs( }; } - // Cache-bust: Bun caches import() per-process, so append a timestamp - // to force re-reading from disk on every call (critical for repeated invocations). // Use file:// URL to handle Windows backslash paths in import(). - const rulesUrl = `${pathToFileURL(rulesFile).href}?t=${Date.now()}`; - const mod = await import(rulesUrl); + const mod = await import(pathToFileURL(rulesFile).href); const parsed = RuleSetSchema.safeParse(mod.default); if (!parsed.success) { diff --git a/tests/commands/adr/import.test.ts b/tests/commands/adr/import.test.ts index 2b757c32..01e465e3 100644 --- a/tests/commands/adr/import.test.ts +++ b/tests/commands/adr/import.test.ts @@ -106,13 +106,6 @@ describe("registerAdrImportCommand", () => { expect(sub.options.find((o) => o.long === "--dry-run")).toBeDefined(); }); - test("does not have a --prefix option (domain-aware remapping)", () => { - const parent = new Command("adr"); - registerAdrImportCommand(parent); - const sub = parent.commands.find((c) => c.name() === "import")!; - expect(sub.options.find((o) => o.long === "--prefix")).toBeUndefined(); - }); - test("accepts --list option", () => { const parent = new Command("adr"); registerAdrImportCommand(parent); diff --git a/tests/helpers/copilot-settings.test.ts b/tests/helpers/copilot-settings.test.ts index 86eaf044..1dbb9522 100644 --- a/tests/helpers/copilot-settings.test.ts +++ b/tests/helpers/copilot-settings.test.ts @@ -24,14 +24,6 @@ describe("configureCopilotSettings", () => { expect(existsSync(join(tempDir, ".github", "copilot"))).toBe(true); }); - test("does not create mcp.json", async () => { - await configureCopilotSettings(tempDir); - - expect(existsSync(join(tempDir, ".github", "copilot", "mcp.json"))).toBe( - false - ); - }); - test("returns path to .github/copilot/ directory", async () => { const result = await configureCopilotSettings(tempDir); diff --git a/tests/helpers/cursor-settings.test.ts b/tests/helpers/cursor-settings.test.ts index d6b018e6..04738412 100644 --- a/tests/helpers/cursor-settings.test.ts +++ b/tests/helpers/cursor-settings.test.ts @@ -27,9 +27,4 @@ describe("configureCursorSettings", () => { configureCursorSettings(tempDir); expect(existsSync(join(tempDir, ".cursor"))).toBe(false); }); - - test("does not create mcp.json", () => { - configureCursorSettings(tempDir); - expect(existsSync(join(tempDir, ".cursor", "mcp.json"))).toBe(false); - }); }); diff --git a/tests/helpers/init-project.test.ts b/tests/helpers/init-project.test.ts index 46ad83bf..69506f1e 100644 --- a/tests/helpers/init-project.test.ts +++ b/tests/helpers/init-project.test.ts @@ -85,7 +85,6 @@ describe("initProject", () => { // Cursor plugin is embedded in the VSIX — no project-level files written expect(existsSync(join(tempDir, ".cursor"))).toBe(false); - expect(existsSync(join(tempDir, ".cursor", "mcp.json"))).toBe(false); // Claude settings should NOT exist expect(existsSync(join(tempDir, ".claude", "settings.local.json"))).toBe( @@ -120,8 +119,6 @@ describe("initProject", () => { const content = JSON.parse(await Bun.file(settingsPath).text()); expect(content.agent).toBe("archgate:developer"); - // MCP settings should not be present (MCP removed) - expect(content.enabledMcpjsonServers).toBeUndefined(); }); test("includes editorSettingsPath in result", async () => { diff --git a/tests/helpers/rules-shim.test.ts b/tests/helpers/rules-shim.test.ts index 87f4fbb6..0cc462a9 100644 --- a/tests/helpers/rules-shim.test.ts +++ b/tests/helpers/rules-shim.test.ts @@ -43,8 +43,6 @@ describe("rules-shim", () => { expect(template).toContain('/// '); expect(template).toContain("satisfies RuleSet"); expect(template).toContain("export default {"); - // Should NOT reference defineRules - expect(template).not.toContain("defineRules"); }); test("writeRulesShim creates rules.d.ts in .archgate/", async () => { diff --git a/tests/integration/init.test.ts b/tests/integration/init.test.ts index a7722b90..68ee1b42 100644 --- a/tests/integration/init.test.ts +++ b/tests/integration/init.test.ts @@ -65,10 +65,7 @@ describe("init integration", () => { expect(result.exitCode).toBe(0); // Cursor plugin is embedded in the VSIX — no .cursor/ files written - expect(existsSync(join(tempDir, ".cursor", "rules"))).toBe(false); - expect( - existsSync(join(tempDir, ".cursor", "rules", "archgate-governance.mdc")) - ).toBe(false); + expect(existsSync(join(tempDir, ".cursor"))).toBe(false); }); test("init with --editor copilot creates copilot directory", async () => {