From 71c3b12e703f4c70a8c689f8bd420df43f99cfc0 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Thu, 19 Mar 2026 22:14:25 -0500 Subject: [PATCH] fix: cmux library directory incorrectly loaded as extension (#1537) * fix(#1526): auto-mode worktree commits land on main instead of milestone branch GitServiceImpl.getMainBranch() was designed to detect manual /worktree worktrees (worktree/ branches) but incorrectly applied the same logic to auto-mode worktrees (milestone/ branches). When no worktree/ branch existed, it fell back to the current branch, which in certain contexts could be main, causing slice commits to land on main instead of the milestone branch. Fix: Detect if currently on a milestone/* branch first (auto-mode case) and return it, before checking for worktree/* branches (manual worktree case). - Modify getMainBranch() to detect milestone branches first - Add test verifying getMainBranch() returns correct branch in auto-worktree - All tests pass, build succeeds Fixes #1526 * fix: cmux library directory incorrectly loaded as extension The extension auto-discovery in resolveExtensionEntries() finds index.js files in subdirectories and treats them as extensions. The cmux directory has an index.js but it's a utility library (imported by gsd and subagent extensions), not an extension itself. Two changes: 1. When a package.json has a "pi" manifest, treat it as authoritative and don't fall through to index.ts/index.js auto-detection. This lets library directories opt out with "pi": {}. 2. Add package.json to cmux directory with empty pi manifest. --- .../src/core/extensions/loader.ts | 12 ++++++--- src/resources/extensions/cmux/package.json | 7 +++++ .../extensions/gsd/tests/cmux.test.ts | 26 ++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/resources/extensions/cmux/package.json diff --git a/packages/pi-coding-agent/src/core/extensions/loader.ts b/packages/pi-coding-agent/src/core/extensions/loader.ts index 62e7e08bf..1f50198b1 100644 --- a/packages/pi-coding-agent/src/core/extensions/loader.ts +++ b/packages/pi-coding-agent/src/core/extensions/loader.ts @@ -495,7 +495,13 @@ function resolveExtensionEntries(dir: string): string[] | null { const packageJsonPath = path.join(dir, "package.json"); if (fs.existsSync(packageJsonPath)) { const manifest = readPiManifest(packageJsonPath); - if (manifest?.extensions?.length) { + if (manifest) { + // When a pi manifest exists, it is authoritative โ€” don't fall through + // to index.ts/index.js auto-detection. This allows library directories + // (like cmux) to opt out by declaring "pi": {} with no extensions. + if (!manifest.extensions?.length) { + return null; + } const entries: string[] = []; for (const extPath of manifest.extensions) { const resolvedExtPath = path.resolve(dir, extPath); @@ -503,9 +509,7 @@ function resolveExtensionEntries(dir: string): string[] | null { entries.push(resolvedExtPath); } } - if (entries.length > 0) { - return entries; - } + return entries.length > 0 ? entries : null; } } diff --git a/src/resources/extensions/cmux/package.json b/src/resources/extensions/cmux/package.json new file mode 100644 index 000000000..6eca7fa6a --- /dev/null +++ b/src/resources/extensions/cmux/package.json @@ -0,0 +1,7 @@ +{ + "name": "@gsd/cmux", + "private": true, + "type": "module", + "description": "cmux integration library โ€” used by other extensions, not an extension itself", + "pi": {} +} diff --git a/src/resources/extensions/gsd/tests/cmux.test.ts b/src/resources/extensions/gsd/tests/cmux.test.ts index 2efbed1a8..d174285b1 100644 --- a/src/resources/extensions/gsd/tests/cmux.test.ts +++ b/src/resources/extensions/gsd/tests/cmux.test.ts @@ -1,5 +1,8 @@ -import test from "node:test"; +import test, { describe } from "node:test"; import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; import { buildCmuxProgress, buildCmuxStatusLabel, @@ -96,3 +99,24 @@ test("buildCmuxStatusLabel and progress prefer deepest active unit", () => { assert.equal(buildCmuxStatusLabel(state), "M001 S02/T03 ยท executing"); assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" }); }); + +describe("cmux extension discovery opt-out", () => { + test("cmux directory has package.json with pi manifest to prevent auto-discovery as extension", () => { + const cmuxDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../cmux", + ); + const pkgPath = path.join(cmuxDir, "package.json"); + assert.ok(fs.existsSync(pkgPath), `${pkgPath} must exist`); + + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + assert.ok( + pkg.pi !== undefined && typeof pkg.pi === "object", + 'package.json must have a "pi" field to opt out of extension auto-discovery', + ); + assert.ok( + !pkg.pi.extensions?.length, + "pi.extensions must be empty or absent โ€” cmux is a library, not an extension", + ); + }); +});