import { lstatSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import YAML from 'yaml'; import { canonicalizeRoleClass } from '../commands/fleet-personas.js'; interface RawFleetRoster { version?: unknown; transport?: unknown; tmux?: { socket_name?: unknown; socketName?: unknown; holder_session?: unknown; holderSession?: unknown; }; defaults?: { working_directory?: unknown; workingDirectory?: unknown; }; runtimes?: Record; agents?: Array<{ name?: unknown; alias?: unknown; provider?: unknown; runtime?: unknown; class?: unknown; host?: unknown; ssh?: unknown; socket?: unknown; working_directory?: unknown; workingDirectory?: unknown; model_hint?: unknown; modelHint?: unknown; reasoning_level?: unknown; reasoningLevel?: unknown; tool_policy?: unknown; toolPolicy?: unknown; persistent_persona?: unknown; persistentPersona?: unknown; reset_between_tasks?: unknown; resetBetweenTasks?: unknown; kickstart_template?: unknown; kickstartTemplate?: unknown; }>; connector?: { kind?: unknown; matrix?: { homeserver_url?: unknown; user_id?: unknown; room_id?: unknown; }; discord?: { channel_id?: unknown; }; }; } export interface FleetAgent { name: string; alias?: string; provider?: string; runtime: string; className: string; /** Resolved host identity. Absent means the caller's authoritative local host. */ host?: string; /** Explicit SSH destination for a cross-host inventory peer. */ ssh?: string; /** Compatibility declaration; when set it must equal fleet-wide tmux.socketName. */ socket?: string; workingDirectory?: string; modelHint?: string; reasoningLevel?: string; toolPolicy?: string; persistentPersona?: boolean | string; resetBetweenTasks?: boolean; kickstartTemplate?: string; } export type FleetConnector = | { kind: 'tmux' } | { kind: 'discord'; discord: { channelId: string }; } | { kind: 'matrix'; matrix: { homeserverUrl: string; userId: string; roomId: string }; }; export interface FleetRoster { version: 1; transport: 'tmux'; tmux: { socketName: string; holderSession: string; }; defaults: { workingDirectory: string; }; runtimes: Record; agents: FleetAgent[]; connector?: FleetConnector; } export type FleetRosterInputFormat = 'yaml' | 'json'; export function resolveInstalledFleetRosterPath(mosaicHome: string): string { const yamlPath = join(mosaicHome, 'fleet', 'roster.yaml'); try { lstatSync(yamlPath); return yamlPath; } catch (error) { if (!isNodeErrorCode(error, 'ENOENT')) throw error; return join(mosaicHome, 'fleet', 'roster.json'); } } const DEFAULT_HOLDER_SESSION = '_holder'; const DEFAULT_WORKING_DIRECTORY = '~/src'; const DEFAULT_RUNTIME_RESETS: Record = { claude: { resetCommand: '/clear' }, codex: { resetCommand: '/clear' }, opencode: { resetCommand: '/clear' }, pi: { resetCommand: '/new' }, }; /** One structural v1 resolver used by fleet commands and runtime comms composition. */ export function parseFleetRosterV1( source: string, format: FleetRosterInputFormat = 'yaml', ): FleetRoster { const trimmed = source.trim(); const parsed = format === 'json' ? (JSON.parse(trimmed) as RawFleetRoster) : (YAML.parse(trimmed) as RawFleetRoster); return normalizeFleetRosterV1(parsed); } export async function loadFleetRoster(path: string): Promise { const source = await readFile(path, 'utf8'); return parseFleetRosterV1(source, path.endsWith('.json') ? 'json' : 'yaml'); } export function getRosterAgent(roster: FleetRoster, name: string): FleetAgent { const agent = roster.agents.find((candidate) => candidate.name === name); if (!agent) throw new Error(`Agent "${name}" is not in the fleet roster.`); return agent; } export function normalizeFleetRosterV1(raw: RawFleetRoster): FleetRoster { assertObject(raw, 'Fleet roster'); assertKnownKeys(raw, 'Fleet roster', [ 'version', 'transport', 'tmux', 'defaults', 'runtimes', 'agents', 'connector', ]); if (raw.tmux !== undefined) { assertObject(raw.tmux, 'Fleet roster tmux'); assertKnownKeys(raw.tmux, 'Fleet roster tmux', [ 'socket_name', 'socketName', 'holder_session', 'holderSession', ]); } if (raw.defaults !== undefined) { assertObject(raw.defaults, 'Fleet roster defaults'); assertKnownKeys(raw.defaults, 'Fleet roster defaults', [ 'working_directory', 'workingDirectory', ]); } if (raw.runtimes !== undefined) { assertObject(raw.runtimes, 'Fleet roster runtimes'); for (const [runtime, config] of Object.entries(raw.runtimes)) { assertObject(config, `Fleet roster runtime "${runtime}"`); assertKnownKeys(config, `Fleet roster runtime "${runtime}"`, [ 'reset_command', 'resetCommand', ]); } } if (raw.version !== 1) throw new Error('Fleet roster version must be 1.'); if (raw.transport !== 'tmux') throw new Error('Fleet roster transport must be "tmux".'); if (!Array.isArray(raw.agents) || raw.agents.length === 0) { throw new Error('Fleet roster must define at least one agent.'); } const socketName = targetingString( aliasValue(raw.tmux, 'socket_name', 'socketName', 'Fleet roster tmux socket'), '', 'Fleet roster tmux socket_name', /^[A-Za-z0-9_.-]+$/, ); const agents = raw.agents.map(normalizeAgent); assertUniqueAgentNames(agents); for (const agent of agents) { if (agent.socket !== undefined && agent.socket !== socketName) { throw new Error( `Fleet agent "${agent.name}" socket must equal the fleet-wide tmux socket_name; independent per-agent sockets are not supported.`, ); } } return { version: 1, transport: 'tmux', tmux: { socketName, holderSession: stringValue( aliasValue(raw.tmux, 'holder_session', 'holderSession', 'Fleet roster tmux holder'), DEFAULT_HOLDER_SESSION, 'Fleet roster tmux holder_session', ), }, defaults: { workingDirectory: stringValue( aliasValue( raw.defaults, 'working_directory', 'workingDirectory', 'Fleet roster defaults working directory', ), DEFAULT_WORKING_DIRECTORY, 'Fleet roster defaults working_directory', ), }, runtimes: normalizeRuntimes(raw.runtimes as RawFleetRoster['runtimes']), agents, connector: normalizeConnector(raw.connector as RawFleetRoster['connector']), }; } function normalizeAgent(raw: NonNullable[number]): FleetAgent { assertObject(raw, 'Fleet roster agent'); assertKnownKeys(raw, 'Fleet roster agent', [ 'name', 'alias', 'provider', 'runtime', 'class', 'host', 'ssh', 'socket', 'working_directory', 'workingDirectory', 'model_hint', 'modelHint', 'reasoning_level', 'reasoningLevel', 'tool_policy', 'toolPolicy', 'persistent_persona', 'persistentPersona', 'reset_between_tasks', 'resetBetweenTasks', 'kickstart_template', 'kickstartTemplate', ]); const name = stringValue(raw.name, '', 'Fleet roster agent name'); const runtime = stringValue( raw.runtime, '', `Fleet roster agent "${name || ''}" runtime`, ); if (!name || !/^[A-Za-z0-9_.-]+$/.test(name)) { throw new Error(`Invalid fleet agent name: ${name || ''}`); } if (!runtime) throw new Error(`Fleet agent "${name}" must define a runtime.`); return { name, alias: optionalString(raw.alias, `Fleet roster agent "${name}" alias`), provider: optionalString(raw.provider, `Fleet roster agent "${name}" provider`), runtime, className: canonicalizeRoleClass( stringValue(raw.class, 'worker', `Fleet roster agent "${name}" class`), ).canonicalClass, host: optionalTargetingString( raw.host, `Fleet roster agent "${name}" host`, /^[A-Za-z0-9_.:[\]-]+$/, ), ssh: optionalTargetingString( raw.ssh, `Fleet roster agent "${name}" ssh`, /^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9_.:[\]-]+$/, ), socket: optionalTargetingString( raw.socket, `Fleet roster agent "${name}" socket`, /^[A-Za-z0-9_.-]+$/, ), workingDirectory: optionalString( aliasValue( raw, 'working_directory', 'workingDirectory', `Fleet roster agent "${name}" working directory`, ), `Fleet roster agent "${name}" working_directory`, ), modelHint: optionalString( aliasValue(raw, 'model_hint', 'modelHint', `Fleet roster agent "${name}" model hint`), `Fleet roster agent "${name}" model_hint`, ), reasoningLevel: optionalString( aliasValue( raw, 'reasoning_level', 'reasoningLevel', `Fleet roster agent "${name}" reasoning level`, ), `Fleet roster agent "${name}" reasoning_level`, ), toolPolicy: optionalString( aliasValue(raw, 'tool_policy', 'toolPolicy', `Fleet roster agent "${name}" tool policy`), `Fleet roster agent "${name}" tool_policy`, ), persistentPersona: optionalBooleanOrString( aliasValue( raw, 'persistent_persona', 'persistentPersona', `Fleet roster agent "${name}" persistent persona`, ), `Fleet roster agent "${name}" persistent_persona`, ), resetBetweenTasks: optionalBoolean( aliasValue( raw, 'reset_between_tasks', 'resetBetweenTasks', `Fleet roster agent "${name}" reset between tasks`, ), `Fleet roster agent "${name}" reset_between_tasks`, ), kickstartTemplate: optionalString( aliasValue( raw, 'kickstart_template', 'kickstartTemplate', `Fleet roster agent "${name}" kickstart template`, ), `Fleet roster agent "${name}" kickstart_template`, ), }; } function normalizeRuntimes( raw: RawFleetRoster['runtimes'] | undefined, ): Record { const result: Record = { ...DEFAULT_RUNTIME_RESETS }; for (const [runtime, config] of Object.entries(raw ?? {})) { result[runtime] = { resetCommand: stringValue( aliasValue( config, 'reset_command', 'resetCommand', `Fleet roster runtime "${runtime}" reset command`, ), '/clear', `Fleet roster runtime "${runtime}" reset_command`, ), }; } return result; } function normalizeConnector(raw: RawFleetRoster['connector']): FleetConnector | undefined { if (raw === undefined) return undefined; assertObject(raw, 'Fleet roster connector'); assertKnownKeys(raw, 'Fleet roster connector', ['kind', 'matrix', 'discord']); const kind = stringValue(raw.kind, '', 'Fleet roster connector kind'); if (kind === 'tmux') { if (raw.matrix !== undefined || raw.discord !== undefined) { throw new Error('Fleet roster tmux connector must not define matrix or discord settings.'); } return { kind }; } if (kind === 'discord') { if (raw.matrix !== undefined) { throw new Error('Fleet roster discord connector must not define matrix settings.'); } assertObject(raw.discord, 'Fleet roster connector discord'); assertKnownKeys(raw.discord, 'Fleet roster connector discord', ['channel_id']); return { kind, discord: { channelId: requiredString( raw.discord.channel_id, 'Fleet roster connector discord channel_id', ), }, }; } if (kind === 'matrix') { if (raw.discord !== undefined) { throw new Error('Fleet roster matrix connector must not define discord settings.'); } assertObject(raw.matrix, 'Fleet roster connector matrix'); assertKnownKeys(raw.matrix, 'Fleet roster connector matrix', [ 'homeserver_url', 'user_id', 'room_id', ]); return { kind, matrix: { homeserverUrl: requiredString( raw.matrix.homeserver_url, 'Fleet roster connector matrix homeserver_url', ), userId: requiredString(raw.matrix.user_id, 'Fleet roster connector matrix user_id'), roomId: requiredString(raw.matrix.room_id, 'Fleet roster connector matrix room_id'), }, }; } throw new Error('Fleet roster connector kind must be one of: tmux, discord, matrix.'); } function aliasValue>( source: T | undefined, snake: keyof T, camel: keyof T, label: string, ): unknown { const snakeValue = source?.[snake]; const camelValue = source?.[camel]; if (snakeValue !== undefined && camelValue !== undefined && snakeValue !== camelValue) { throw new Error(`${label} aliases ${String(snake)} and ${String(camel)} conflict.`); } return snakeValue ?? camelValue; } function isNodeErrorCode(error: unknown, code: string): boolean { return error instanceof Error && 'code' in error && error.code === code; } function requiredString(value: unknown, label: string): string { const resolved = stringValue(value, '', label).trim(); if (!resolved) throw new Error(`${label} is required.`); return resolved; } function assertObject(value: unknown, label: string): asserts value is Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`${label} must be an object.`); } } function assertKnownKeys( value: Record, label: string, allowedKeys: readonly string[], ): void { const allowed = new Set(allowedKeys); const unknownKeys = Object.keys(value).filter((key) => !allowed.has(key)); if (unknownKeys.length > 0) { throw new Error(`${label} has unknown field(s): ${unknownKeys.join(', ')}.`); } } function assertUniqueAgentNames(agents: FleetAgent[]): void { const seen = new Set(); for (const agent of agents) { if (seen.has(agent.name)) { throw new Error(`Fleet roster has duplicate agent name: ${agent.name}.`); } seen.add(agent.name); } } function stringValue(value: unknown, fallback = '', label = 'Value'): string { if (value === undefined) return fallback; if (typeof value !== 'string') throw new Error(`${label} must be a string.`); return value; } function targetingString(value: unknown, fallback: string, label: string, pattern: RegExp): string { const resolved = stringValue(value, fallback, label); if (resolved && !pattern.test(resolved)) { throw new Error(`${label} contains unsupported targeting characters.`); } return resolved; } function optionalTargetingString( value: unknown, label: string, pattern: RegExp, ): string | undefined { const resolved = optionalString(value, label); if (resolved !== undefined && (!resolved || !pattern.test(resolved))) { throw new Error(`${label} contains unsupported targeting characters.`); } return resolved; } function optionalString(value: unknown, label = 'Value'): string | undefined { if (value === undefined) return undefined; if (typeof value !== 'string') throw new Error(`${label} must be a string.`); return value; } function optionalBoolean(value: unknown, label = 'Value'): boolean | undefined { if (value === undefined) return undefined; if (typeof value !== 'boolean') throw new Error(`${label} must be a boolean.`); return value; } function optionalBooleanOrString(value: unknown, label = 'Value'): boolean | string | undefined { if (value === undefined) return undefined; if (typeof value !== 'boolean' && typeof value !== 'string') { throw new Error(`${label} must be a boolean or string.`); } return value; }