From 5e15431027a4ef7aad4b2bd36d7ae1f01816ee80 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 13 Aug 2026 11:59:31 -0500 Subject: [PATCH] fix(fleet): tolerate harness metadata files in the managed install root Claude Code writes installed_plugins.json and other metadata files into the seat's plugins directory during a session, so refusing every real entry made composition fail on each seat's second launch. Only a real directory is an unmanaged entry the pruner would orphan; plain files are harness state and pass through untouched. Found by the in-box hour-gate relaunch of the probe seat. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh --- .../src/commands/fleet-launch-command.spec.ts | 22 +++++++++++++++++++ .../src/commands/fleet-launch-command.ts | 14 ++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/mosaic/src/commands/fleet-launch-command.spec.ts b/packages/mosaic/src/commands/fleet-launch-command.spec.ts index d9999cf7..39007d66 100644 --- a/packages/mosaic/src/commands/fleet-launch-command.spec.ts +++ b/packages/mosaic/src/commands/fleet-launch-command.spec.ts @@ -242,6 +242,28 @@ describe('managed plugin and skill links', () => { ); }); + it('tolerates harness metadata files in the install root and still refuses real directories', () => { + const fx = fixture({ schema: 1, harness: 'claude', plugins: [] }); + const pluginHome = join(fx.agentDir, '.claude', 'plugins'); + mkdirSync(pluginHome, { recursive: true }); + writeFileSync(join(pluginHome, 'installed_plugins.json'), '{}\n'); + + const plan = resolveFleetLaunchComposition('fred', { + systemHome: fx.systemHome, + userHome: fx.userHome, + }); + expect(plan.prune).toEqual([]); + expect(readFileSync(join(pluginHome, 'installed_plugins.json'), 'utf8')).toBe('{}\n'); + + mkdirSync(join(pluginHome, 'stray-plugin'), { recursive: true }); + expect(() => + resolveFleetLaunchComposition('fred', { + systemHome: fx.systemHome, + userHome: fx.userHome, + }), + ).toThrowError(/real plugin directory occupies managed install root.*refusing to prune/i); + }); + it('surfaces a real directory at a managed link path without deleting it', () => { const fx = fixture({ schema: 1, harness: 'claude', plugins: ['keep'] }); mkdirSync(join(fx.userHome, 'plugins', 'keep'), { recursive: true }); diff --git a/packages/mosaic/src/commands/fleet-launch-command.ts b/packages/mosaic/src/commands/fleet-launch-command.ts index 61d86abc..42ebbe01 100644 --- a/packages/mosaic/src/commands/fleet-launch-command.ts +++ b/packages/mosaic/src/commands/fleet-launch-command.ts @@ -498,10 +498,16 @@ function resolveManagedLinks( const path = join(installRoot, entry.name); if (desired.has(entry.name)) continue; if (!entry.isSymbolicLink()) { - throw new FleetLaunchError( - 'COMPOSITION_FAILED', - `real ${kind} entry occupies managed install root ${path}; refusing to prune it.`, - ); + // The harness writes its own metadata files (e.g. installed_plugins.json) + // beside the managed links; only a real directory is an unmanaged entry + // the pruner would orphan. + if (entry.isDirectory()) { + throw new FleetLaunchError( + 'COMPOSITION_FAILED', + `real ${kind} directory occupies managed install root ${path}; refusing to prune it.`, + ); + } + continue; } prune.push(path); }