import type { Command } from 'commander'; import { spawnSync } from 'node:child_process'; import { accessSync, constants } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { DEFAULT_MOSAIC_HOME } from '../constants.js'; import { execBrainTool } from './brain-dispatch.js'; /** * `mosaic comms send` — routed agent messaging (FLEET-COMMS.md doctrine). * * Same-host (default): brain tools/tmux/agent-send.sh. Inter-site * (--site ): the fleet-comms repo's comms-send.sh — never for * local traffic (a git round trip per message; Jason 2026-08-28). * * Exit codes pass through BOTH paths. rc=2 (text in pane, still draft) is * a CONTRACT, not a failure: never retry, confirm with capture-pane. */ export interface CommsSendOptions { readonly class?: string; readonly file?: string; readonly socket?: string; readonly site?: string; readonly commsRepo?: string; } export function defaultCommsRepo(): string { return process.env['MOSAIC_FLEET_COMMS_REPO'] ?? join(homedir(), 'src', 'fleet-comms'); } /** Build the agent-send.sh argv for the same-host path. */ export function tmuxSendArgs(target: string, message: string, opts: CommsSendOptions): string[] { const args = ['-s', target]; if (opts.class) args.push('-C', opts.class); if (opts.socket) args.push('-L', opts.socket); if (opts.file) args.push('-f', opts.file); else args.push('-m', message); return args; } /** Build the comms-send.sh argv for the inter-site path. */ export function fleetCommsSendArgs( site: string, target: string, message: string, opts: CommsSendOptions, ): string[] { const args = ['-t', `${site}/${target}`]; if (opts.class) args.push('-c', opts.class); args.push('-m', message); return args; } export function registerCommsCommand(program: Command): void { const cmd: Command = program .command('comms') .description( 'Routed agent messaging: tmux same-host (default), fleet-comms inter-site (--site)', ) .command('send') .description('send [message...] — same-host tmux unless --site is given') .option('--class ', 'terminal-log | actionable | human | reaction | digest') .option('--file ', 'message body from file (same-host path only)') .option('--socket ', 'tmux socket for the same-host send (e.g. mosaic-fleet)') .option('--site ', 'route via fleet-comms to /') .option('--comms-repo ', 'fleet-comms checkout', defaultCommsRepo()) .argument('', 'destination seat (session name)') .argument('[message...]', 'message text (joined; or use --file)') .action( async ( target: string, messageWords: string[], opts: CommsSendOptions & Record, command: Command, ) => { let mosaicHome: string | undefined; for (let anc: Command | null = command; anc; anc = anc.parent) { const v = (anc.opts() as Record)['mosaicHome']; if (v !== undefined) { mosaicHome = v; break; } } const home = mosaicHome ?? DEFAULT_MOSAIC_HOME; const message = messageWords.join(' '); if (opts.site) { const repo = opts.commsRepo ?? defaultCommsRepo(); const tool = join(repo, 'tools', 'comms-send.sh'); try { accessSync(tool, constants.X_OK); } catch { console.error( `mosaic comms: fleet-comms sender not found (expected ${tool}). ` + 'Clone the fleet-comms repo or point --comms-repo at it.', ); process.exitCode = 127; return; } if (!process.env['MOSAIC_AGENT_NAME']) { console.error( 'mosaic comms: inter-site sends require MOSAIC_AGENT_NAME (sending identity).', ); process.exitCode = 2; // invocation defect: fixable by the caller return; } // comms-send.sh locates its working repo via FLEET_COMMS_REPO // (default $HOME/src/fleet-comms); without this, --comms-repo // would select the executable but not the repository it operates // on (codex review of 9c8b6ebf). const env = { ...process.env, FLEET_COMMS_SITE: opts.site, FLEET_COMMS_REPO: repo }; const result = spawnSync(tool, fleetCommsSendArgs(opts.site, target, message, opts), { stdio: 'inherit', env, }); process.exitCode = result.status ?? 125; return; } // Same-host: the brain tool owns validation (bad class -> its rc 3) // and absence (execBrainTool -> 127 with the resolved path). process.exitCode = execBrainTool( home, 'tools/tmux/agent-send.sh', tmuxSendArgs(target, message, opts), ); }, ); cmd.addHelpText( 'after', '\nExit codes pass through. rc=2 means the text reached the pane but is still a draft: NEVER retry (double-send); confirm with tmux capture-pane.', ); }