fix(fleet): unify user data-home seam and add actionable unscaffolded-agent error
Integration reconciliation of T2/T3 seams on feat/wf-fleet-mvp: - fleet launch now resolves the user root through defaultFleetDataHome() (MOSAIC_DATA_HOME), the same seam fleet agent new uses, instead of a divergent MOSAIC_USER_HOME variable. - Launching an unscaffolded name raises AGENT_NOT_SCAFFOLDED with the actionable message pointing at 'mosaic fleet agent new <name>' (acceptance carried over from the T3 card after the roster-v2 reconciliation moved it onto the launch path). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
This commit is contained in:
co-authored by
Claude Fable 5
parent
4e2f9888a0
commit
0fdcfa0ff4
@@ -122,6 +122,24 @@ describe('profile-selected overlay', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('unscaffolded agent names', () => {
|
||||||
|
it('points an unscaffolded name at mosaic fleet agent new', () => {
|
||||||
|
const fx = fixture();
|
||||||
|
try {
|
||||||
|
resolveFleetLaunchComposition('ghost', {
|
||||||
|
systemHome: fx.systemHome,
|
||||||
|
userHome: fx.userHome,
|
||||||
|
});
|
||||||
|
throw new Error('expected resolution to fail');
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const launchError = error as FleetLaunchError;
|
||||||
|
expect(launchError.code).toBe('AGENT_NOT_SCAFFOLDED');
|
||||||
|
expect(launchError.message).toContain("no such fleet agent 'ghost'");
|
||||||
|
expect(launchError.message).toContain('mosaic fleet agent new ghost');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('A3 credential validation', () => {
|
describe('A3 credential validation', () => {
|
||||||
it('refuses a symlinked bundle credential file', () => {
|
it('refuses a symlinked bundle credential file', () => {
|
||||||
const fx = fixture();
|
const fx = fixture();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
writeFileSync,
|
writeFileSync,
|
||||||
type Stats,
|
type Stats,
|
||||||
} from 'node:fs';
|
} from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
|
||||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||||
import type { Command } from 'commander';
|
import type { Command } from 'commander';
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
type FleetHarnessContext,
|
type FleetHarnessContext,
|
||||||
type RuntimeName,
|
type RuntimeName,
|
||||||
} from './launch.js';
|
} from './launch.js';
|
||||||
|
import { defaultFleetDataHome } from '../fleet/fleet-agent-scaffold.js';
|
||||||
|
|
||||||
export const FLEET_AGENT_PROFILE_SCHEMA = 1;
|
export const FLEET_AGENT_PROFILE_SCHEMA = 1;
|
||||||
const PROFILE_KEYS = [
|
const PROFILE_KEYS = [
|
||||||
@@ -47,6 +47,7 @@ const CREDENTIAL_FILES: Record<RuntimeName, string> = {
|
|||||||
export type FleetLaunchErrorCode =
|
export type FleetLaunchErrorCode =
|
||||||
| 'SCHEMA_TOO_NEW'
|
| 'SCHEMA_TOO_NEW'
|
||||||
| 'PROFILE_INVALID'
|
| 'PROFILE_INVALID'
|
||||||
|
| 'AGENT_NOT_SCAFFOLDED'
|
||||||
| 'COMPOSITION_FAILED'
|
| 'COMPOSITION_FAILED'
|
||||||
| 'FIRST_AUTH_REFUSAL';
|
| 'FIRST_AUTH_REFUSAL';
|
||||||
|
|
||||||
@@ -513,6 +514,12 @@ export function resolveFleetLaunchComposition(
|
|||||||
throw new FleetLaunchError('PROFILE_INVALID', `invalid fleet agent name: ${name}`);
|
throw new FleetLaunchError('PROFILE_INVALID', `invalid fleet agent name: ${name}`);
|
||||||
}
|
}
|
||||||
const agentDir = join(roots.userHome, 'fleet', 'agents', name);
|
const agentDir = join(roots.userHome, 'fleet', 'agents', name);
|
||||||
|
if (lstatIfPresent(agentDir) === undefined) {
|
||||||
|
throw new FleetLaunchError(
|
||||||
|
'AGENT_NOT_SCAFFOLDED',
|
||||||
|
`no such fleet agent '${name}' — run: mosaic fleet agent new ${name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
assertRealDirectory(agentDir, 'fleet agent directory');
|
assertRealDirectory(agentDir, 'fleet agent directory');
|
||||||
const profilePath = join(agentDir, 'profile.json');
|
const profilePath = join(agentDir, 'profile.json');
|
||||||
const profileInfo = lstatIfPresent(profilePath);
|
const profileInfo = lstatIfPresent(profilePath);
|
||||||
@@ -687,8 +694,7 @@ export function registerFleetLaunchCommand(
|
|||||||
.allowExcessArguments(true)
|
.allowExcessArguments(true)
|
||||||
.action((name: string, opts: { dryRun?: boolean }, command: Command): void => {
|
.action((name: string, opts: { dryRun?: boolean }, command: Command): void => {
|
||||||
try {
|
try {
|
||||||
const userHome =
|
const userHome = deps.userHome ?? defaultFleetDataHome();
|
||||||
deps.userHome ?? process.env['MOSAIC_USER_HOME'] ?? join(homedir(), '.mosaic');
|
|
||||||
const passthrough = command.args.slice(1);
|
const passthrough = command.args.slice(1);
|
||||||
const plan = resolveFleetLaunchComposition(
|
const plan = resolveFleetLaunchComposition(
|
||||||
name,
|
name,
|
||||||
|
|||||||
Reference in New Issue
Block a user