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/<name> branches) but incorrectly applied the same logic to auto-mode
worktrees (milestone/<MID> branches). When no worktree/<name> 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.
This commit is contained in:
Jeremy McSpadden 2026-03-19 22:14:25 -05:00 committed by GitHub
parent aa8d3ee059
commit 71c3b12e70
3 changed files with 40 additions and 5 deletions

View file

@ -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;
}
}

View file

@ -0,0 +1,7 @@
{
"name": "@gsd/cmux",
"private": true,
"type": "module",
"description": "cmux integration library — used by other extensions, not an extension itself",
"pi": {}
}

View file

@ -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",
);
});
});