feat(fleet): comms-block emitter + FLEET-LAUNCH runbook (#633) (#638)
Some checks failed
ci/woodpecker/push/ci Pipeline was canceled
ci/woodpecker/push/publish Pipeline was canceled

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #638.
This commit is contained in:
2026-06-22 22:23:50 +00:00
committed by jason.woltje
parent cf8ceb3095
commit 4e84f8e850
7 changed files with 285 additions and 0 deletions

View File

@@ -95,6 +95,7 @@ describe('registerFleetCommand', () => {
expect(agent).toBeDefined();
expect(agent!.options.map((option) => option.long)).toContain('--list');
expect(agent!.commands.map((command) => command.name()).sort()).toEqual([
'comms-block',
'reset',
'roster',
'send',

View File

@@ -7,6 +7,7 @@ import { spawn } from 'node:child_process';
import * as readline from 'node:readline';
import type { Command } from 'commander';
import YAML from 'yaml';
import { resolveCommsBlock } from '../fleet/comms-onboarding.js';
/**
* A function that spawns a command with inherited stdio (TTY passthrough).
@@ -1359,6 +1360,23 @@ export function registerFleetAgentCommands(
}
});
agentCommand
.command('comms-block <role>')
.description(
"Print the Fleet Comms cheat-sheet for a roster role (preview a peer's peer-reach view)",
)
.option('--host <host>', 'Override the fleet host (preview a cross-host peer view)')
.action((role: string, opts: { host?: string }) => {
const mosaicHome = resolveMosaicHomeFromCommand(agentCommand, deps.mosaicHome);
const res = resolveCommsBlock(mosaicHome, role, opts.host);
if (!res.ok) {
console.error(`[mosaic] comms-block: ${res.error}`);
process.exitCode = 1;
return;
}
console.log(res.output);
});
agentCommand
.command('status [agent]')
.description('Show tmux status for the local fleet or one agent')

View File

@@ -7,6 +7,7 @@ import {
buildFleetCommsBlock,
renderPeerReach,
readFleetCommsBlock,
resolveCommsBlock,
type CommsPeer,
} from './comms-onboarding.js';
@@ -185,3 +186,53 @@ describe('readFleetCommsBlock — situational (the context a spawned agent gets)
expect(readFleetCommsBlock(mkdtempSync(join(tmpdir(), 'noroster-')), 'orchestrator')).toBe('');
});
});
describe('resolveCommsBlock — `mosaic fleet comms-block <role>` emitter semantics', () => {
// The emitter wraps readFleetCommsBlock but must NEVER print an empty string silently:
// an unknown role / missing roster has to fail loud (caller maps !ok → stderr + exit 1)
// so `mosaic fleet comms-block bogus` is a visible error, not a confusing no-op. The
// success path returns the block verbatim for `mosaic fleet comms-block <peer>` previews.
let home: string;
beforeEach(() => {
home = mkdtempSync(join(tmpdir(), 'mosaic-commsblk-'));
mkdirSync(join(home, 'fleet'), { recursive: true });
writeFileSync(join(home, 'fleet', 'roster.yaml'), ROSTER);
});
afterEach(() => rmSync(home, { recursive: true, force: true }));
it('returns ok + the cheat-sheet for a roster member', () => {
const res = resolveCommsBlock(home, 'orchestrator', 'w-jarvis');
expect(res.ok).toBe(true);
expect(res.output).toContain('# Fleet Comms');
expect(res.output).toContain('| enhancer |');
expect(res.error).toBeUndefined();
});
it('fails loud (not ok + error naming the role) for a non-member — never silently empty', () => {
const res = resolveCommsBlock(home, 'stranger', 'w-jarvis');
expect(res.ok).toBe(false);
expect(res.output).toBe('');
expect(res.error).toContain('stranger');
});
it('fails loud when no roster exists at the mosaic home', () => {
const noRoster = mkdtempSync(join(tmpdir(), 'mosaic-noroster-'));
const res = resolveCommsBlock(noRoster, 'orchestrator', 'w-jarvis');
expect(res.ok).toBe(false);
expect(res.error).toBeTruthy();
rmSync(noRoster, { recursive: true, force: true });
});
it('fails loud for a missing role argument', () => {
const res = resolveCommsBlock(home, undefined, 'w-jarvis');
expect(res.ok).toBe(false);
expect(res.error).toBeTruthy();
});
it('honors a host override so a peer can preview its own cross-host view', () => {
// coder0-0 viewing with its own host → its self-identity line uses that host.
const res = resolveCommsBlock(home, 'coder0-0', '10.1.10.37');
expect(res.ok).toBe(true);
expect(res.output).toContain('`[10.1.10.37:coder0-0]`');
});
});

View File

@@ -179,5 +179,48 @@ export function readFleetCommsBlock(
});
}
/** Result of resolving a comms-block emit request — see `mosaic fleet comms-block`. */
export interface CommsBlockResult {
/** True when a cheat-sheet was produced; false maps to stderr + non-zero exit. */
ok: boolean;
/** The Fleet-Comms cheat-sheet (empty unless ok). */
output: string;
/** Operator-facing reason when !ok. */
error?: string;
}
/**
* Resolve the Fleet-Comms cheat-sheet for an explicit <role>, backing the
* `mosaic fleet comms-block <role>` command. Unlike readFleetCommsBlock — which
* returns '' on any miss so composeContract can no-op silently during a launch —
* this NEVER silently emits empty: an unknown role or missing roster yields
* ok:false + an operator-facing reason, so the CLI surfaces it (stderr + exit 1)
* rather than printing nothing. That makes it safe to preview any peer's view,
* e.g. `mosaic fleet comms-block coder0-0`.
*/
export function resolveCommsBlock(
mosaicHome: string,
role: string | undefined,
fleetHost?: string,
): CommsBlockResult {
if (!role) {
return { ok: false, output: '', error: 'comms-block requires a <role> argument' };
}
const block = fleetHost
? readFleetCommsBlock(mosaicHome, role, fleetHost)
: readFleetCommsBlock(mosaicHome, role);
if (!block) {
const rosterPath = join(mosaicHome, 'fleet', 'roster.yaml');
return {
ok: false,
output: '',
error: existsSync(rosterPath)
? `role "${role}" is not a member of the fleet roster at ${rosterPath}`
: `no fleet roster at ${rosterPath}`,
};
}
return { ok: true, output: block };
}
/** Default mosaic home (mirrors launch.ts), for callers that don't pass one. */
export const DEFAULT_MOSAIC_HOME_FOR_COMMS = join(homedir(), '.config', 'mosaic');