Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .archgate/config.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "domains": { "ci": "CI" } }
{ "domains": { "ci": "CI" }, "baseBranch": "origin/main" }
5 changes: 1 addition & 4 deletions src/engine/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 0 additions & 7 deletions tests/commands/adr/import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 0 additions & 8 deletions tests/helpers/copilot-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
5 changes: 0 additions & 5 deletions tests/helpers/cursor-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
3 changes: 0 additions & 3 deletions tests/helpers/init-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 0 additions & 2 deletions tests/helpers/rules-shim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ describe("rules-shim", () => {
expect(template).toContain('/// <reference path="../rules.d.ts" />');
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 () => {
Expand Down
5 changes: 1 addition & 4 deletions tests/integration/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading