diff --git a/docs/scratchpads/ms-792-fleet-enoent-installer.md b/docs/scratchpads/ms-792-fleet-enoent-installer.md index 6a649cd..c1cd415 100644 --- a/docs/scratchpads/ms-792-fleet-enoent-installer.md +++ b/docs/scratchpads/ms-792-fleet-enoent-installer.md @@ -19,6 +19,7 @@ Make expected missing or malformed fleet roster configuration fail with an actio - 2026-07-16: Added a shared roster read/parse guard and routed v1 fleet commands plus v1/v2 selection through Commander’s actionable nonzero error path. V2 command modules already return structured nonzero JSON errors for their guarded reads. - 2026-07-16: Replaced installer heading `echo` with format-safe `printf`; added a regression check for the scoped package heading. - 2026-07-16: Rebuilt CLI and manually verified `fleet ps` with no roster prints the initialization hint, exits 1, and has no stack trace. +- 2026-07-17: Rebased #818 onto `origin/main` at `9ddc6fbd` (#791 PR3). The added `fleet regen` command had a canonical roster read in its sibling module; it now uses the same missing-roster guard and Commander exit path. Internal NORTH_STAR, preset, and post-write invariant reads remain intentionally unguarded. ## Verification @@ -27,6 +28,7 @@ Make expected missing or malformed fleet roster configuration fail with an actio - `pnpm lint` — PASS - `pnpm format:check` — PASS - `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/fleet.spec.ts src/commands/install-heading.spec.ts` — PASS (209 tests) +- `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/fleet-regen-command.spec.ts` — PASS (27 tests, including missing canonical roster) - Instrumented Vitest coverage is unavailable because `@vitest/coverage-v8` is not declared in this repository. Each branch added in the roster guard has direct unit coverage. ## Risks / blockers diff --git a/packages/mosaic/src/commands/fleet-regen-command.spec.ts b/packages/mosaic/src/commands/fleet-regen-command.spec.ts index 8155516..575d7fa 100644 --- a/packages/mosaic/src/commands/fleet-regen-command.spec.ts +++ b/packages/mosaic/src/commands/fleet-regen-command.spec.ts @@ -150,6 +150,17 @@ describe('projectRosterV2AgentGeneratedEnv', (): void => { }); describe('mosaic fleet regen', (): void => { + it('reports a missing canonical roster with the shared initialization hint', async (): Promise => { + const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-regen-missing-roster-')); + cleanup = home; + + await expect( + program(home, recordingRunner([])).parseAsync(['node', 'mosaic', 'fleet', 'regen', '--json']), + ).rejects.toThrow( + `No fleet roster found at ${join(home, 'fleet', 'roster.yaml')}. Run \`mosaic fleet init\``, + ); + }); + it('is dry-run by default: reports the plan and writes nothing', async (): Promise => { const home = await fleetHome(); const calls: string[][] = []; diff --git a/packages/mosaic/src/commands/fleet-regen-command.ts b/packages/mosaic/src/commands/fleet-regen-command.ts index 9f3ce67..6f478b0 100644 --- a/packages/mosaic/src/commands/fleet-regen-command.ts +++ b/packages/mosaic/src/commands/fleet-regen-command.ts @@ -1,4 +1,4 @@ -import { readFile, stat } from 'node:fs/promises'; +import { stat } from 'node:fs/promises'; import { homedir } from 'node:os'; import { join, relative, resolve } from 'node:path'; import type { Command } from 'commander'; @@ -18,6 +18,7 @@ import { validateRosterV2Semantics, type FleetRosterV2, } from '../fleet/roster-v2.js'; +import { FleetRosterConfigurationError, readFleetRosterText } from '../fleet/fleet-roster-v1.js'; /** * `mosaic fleet regen` — recovery-framed regeneration of the roster-derived @@ -303,7 +304,7 @@ function defaultReadRoster( mosaicHome: string, ): (rosterPath: string) => Promise { return async (rosterPath: string): Promise => { - const roster = parseRosterV2(await readFile(rosterPath, 'utf8'), 'yaml'); + const roster = parseRosterV2(await readFleetRosterText(rosterPath), 'yaml'); // Enforce the SAME semantic gate as reconcile/plan/verify (persona resolution // + protected-class tool-policy match) so regen cannot project a roster the // rest of the fleet surface would reject. @@ -433,6 +434,10 @@ export function registerFleetRegenCommand(fleetCommand: Command, deps: FleetRege // the operator must finish/verify (and clear the lock) before restarting. if (result.incomplete || result.cleanup) process.exitCode = 1; } catch (error: unknown) { + if (error instanceof FleetRosterConfigurationError) { + fleetCommand.error(error.message, { code: 'fleet.roster', exitCode: 1 }); + return; + } process.exitCode = 1; const message = error instanceof Error ? error.message : String(error); process.stderr.write(`mosaic fleet regen failed: ${message}\n`);