Files
stack/packages/mosaic/src/commands/comms.ts
T
2026-08-29 16:24:08 +00:00

136 lines
5.0 KiB
TypeScript

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 <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 <target> [message...] — same-host tmux unless --site is given')
.option('--class <class>', 'terminal-log | actionable | human | reaction | digest')
.option('--file <path>', 'message body from file (same-host path only)')
.option('--socket <name>', 'tmux socket for the same-host send (e.g. mosaic-fleet)')
.option('--site <site>', 'route via fleet-comms to <site>/<target>')
.option('--comms-repo <path>', 'fleet-comms checkout', defaultCommsRepo())
.argument('<target>', 'destination seat (session name)')
.argument('[message...]', 'message text (joined; or use --file)')
.action(
async (
target: string,
messageWords: string[],
opts: CommsSendOptions & Record<string, unknown>,
command: Command,
) => {
let mosaicHome: string | undefined;
for (let anc: Command | null = command; anc; anc = anc.parent) {
const v = (anc.opts() as Record<string, string | undefined>)['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.',
);
}