Compare commits
4 Commits
fix/fleet-
...
fix/fleet-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd4021eef7 | ||
| 7498fcb20d | |||
| 42d081613f | |||
| b5c1381e45 |
@@ -17,6 +17,9 @@ Product-owned defaults:
|
|||||||
- `packages/mosaic/framework/tools/tmux/agent-send.sh`
|
- `packages/mosaic/framework/tools/tmux/agent-send.sh`
|
||||||
- `packages/mosaic/framework/tools/tmux/send-message.sh`
|
- `packages/mosaic/framework/tools/tmux/send-message.sh`
|
||||||
|
|
||||||
|
These files are published through `packages/mosaic/package.json`, whose `files`
|
||||||
|
allowlist includes `framework` along with `dist`.
|
||||||
|
|
||||||
Site-owned local roster:
|
Site-owned local roster:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -66,6 +69,14 @@ These commands read the roster and target the configured tmux socket. The
|
|||||||
generated systemd agent services use `start-agent-session.sh`; message delivery
|
generated systemd agent services use `start-agent-session.sh`; message delivery
|
||||||
uses the tmux send tools with `-L mosaic-factory`.
|
uses the tmux send tools with `-L mosaic-factory`.
|
||||||
|
|
||||||
|
`mosaic agent send` is operator-origin traffic unless a caller explicitly says
|
||||||
|
otherwise. The CLI always passes a deterministic source label to
|
||||||
|
`agent-send.sh` with `-S`, defaulting to `<hostname>:operator`, so it does not
|
||||||
|
query the target tmux socket and accidentally identify as an active agent pane.
|
||||||
|
Use `--source-label <label>` or `--source <label>` only when deliberately
|
||||||
|
impersonating a known handoff lane. The lower-level inter-agent wrapper
|
||||||
|
`agent-send.sh -S <label>` remains the explicit source override for scripts.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
Use these checks before expanding the roster:
|
Use these checks before expanding the roster:
|
||||||
@@ -83,6 +94,27 @@ Expected results:
|
|||||||
- `tmux ls` shows only the default tmux server sessions and is not changed by
|
- `tmux ls` shows only the default tmux server sessions and is not changed by
|
||||||
fleet start/stop operations.
|
fleet start/stop operations.
|
||||||
- `mosaic fleet verify` checks exact session targets on the isolated socket.
|
- `mosaic fleet verify` checks exact session targets on the isolated socket.
|
||||||
|
- `systemctl --user status ...` may show `active (exited)` for oneshot units;
|
||||||
|
that means the unit ran, not that an agent pane is live. Treat tmux
|
||||||
|
`has-session`, `list-panes`, process tree, and logs as the liveness evidence.
|
||||||
|
|
||||||
|
## Release Preflight
|
||||||
|
|
||||||
|
Run this checklist before cutting or dogfooding a fleet release:
|
||||||
|
|
||||||
|
- Real AI dogfood: send at least one task through `mosaic agent send`, then
|
||||||
|
confirm the agent accepted/responded using pane, process, or log evidence.
|
||||||
|
- Restart/stop/idempotency: run `mosaic fleet start`, `restart`, `stop`, and a
|
||||||
|
repeated `start` against the named socket; verify the default tmux server is
|
||||||
|
unchanged.
|
||||||
|
- Liveness verification: run `mosaic fleet verify` and confirm roster sessions
|
||||||
|
with `tmux -L mosaic-factory ls` or exact `has-session` checks.
|
||||||
|
- Package dry-run: run `npm pack --dry-run --json` from `packages/mosaic` and
|
||||||
|
confirm `framework/fleet`, `framework/systemd/user`,
|
||||||
|
`framework/tools/fleet`, and `framework/tools/tmux` assets are included.
|
||||||
|
- Mosaic update test: install or upgrade from the packed artifact in a temporary
|
||||||
|
Mosaic home and confirm `mosaic update` or the release upgrade path does not
|
||||||
|
remove local roster/config files.
|
||||||
|
|
||||||
## Rollback
|
## Rollback
|
||||||
|
|
||||||
|
|||||||
35
docs/scratchpads/2026-06-20-fleet-release-hardening.md
Normal file
35
docs/scratchpads/2026-06-20-fleet-release-hardening.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Fleet release hardening
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Harden the Mosaic local fleet release path for operator sends, tmux/systemd verification, package contents, and dogfood release documentation.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Do not edit `docs/TASKS.md`.
|
||||||
|
- Do not change production deployment refs.
|
||||||
|
- Keep fleet transport generic and named-socket safe.
|
||||||
|
- Preserve strict roster validation.
|
||||||
|
- Add tests first or alongside fixes.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Add regression tests for deterministic `mosaic agent send` source labels.
|
||||||
|
2. Strengthen fleet status/verify/package/install-systemd coverage.
|
||||||
|
3. Implement focused CLI/source-label changes.
|
||||||
|
4. Update local canary documentation with dogfood preflight.
|
||||||
|
5. Run formatting, targeted tests, typecheck, lint, and package dry-run evidence.
|
||||||
|
|
||||||
|
## Evidence Log
|
||||||
|
|
||||||
|
- Started from existing `docs/PRD.md`; durable local fleet canary is in v0.1.0 scope.
|
||||||
|
- Loaded `mosaic-fleet-operations` skill; key constraints are isolated tmux sockets, no default tmux positive tests, and `active (exited)` is not liveness.
|
||||||
|
- TDD red: `pnpm --filter @mosaicstack/mosaic test -- src/commands/fleet.spec.ts` initially failed because `node_modules` was absent; after `pnpm install`, the new source-label tests failed on missing `-S`, missing helper, and unknown `--source-label`.
|
||||||
|
- Green implementation: `mosaic agent send` now passes `-S <hostname>:operator` by default and accepts `--source-label` / `--source` overrides.
|
||||||
|
- Test coverage added for tmux-based fleet verify liveness, package `files` allowlist containing `framework`, and explicit operator source-label command construction.
|
||||||
|
- Formatting: `pnpm exec prettier --write packages/mosaic/src/commands/fleet.ts packages/mosaic/src/commands/fleet.spec.ts docs/guides/fleet-local-canary.md docs/scratchpads/2026-06-20-fleet-release-hardening.md`.
|
||||||
|
- Targeted tests: `pnpm --filter @mosaicstack/mosaic test -- src/commands/fleet.spec.ts src/cli-smoke.spec.ts` passed with 49 tests.
|
||||||
|
- Typecheck: `pnpm typecheck` passed.
|
||||||
|
- Lint: `pnpm lint` passed.
|
||||||
|
- Package dry-run: `npm pack --dry-run --json` from `packages/mosaic` included `framework/fleet`, `framework/systemd/user`, `framework/tools/fleet/start-agent-session.sh`, and `framework/tools/tmux/{agent-send.sh,send-message.sh}`.
|
||||||
|
- Review: `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted` approved the supplied diff with no findings; the review tool noted its read-only sandbox could not inspect files directly.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaicstack/mosaic",
|
"name": "@mosaicstack/mosaic",
|
||||||
"version": "0.0.31",
|
"version": "0.0.34",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import {
|
|||||||
buildAgentSendCommand,
|
buildAgentSendCommand,
|
||||||
buildFleetServiceCommand,
|
buildFleetServiceCommand,
|
||||||
generateAgentEnv,
|
generateAgentEnv,
|
||||||
|
getDefaultOperatorSourceLabel,
|
||||||
getRosterAgent,
|
getRosterAgent,
|
||||||
loadFleetRoster,
|
loadFleetRoster,
|
||||||
|
mergeAgentEnv,
|
||||||
registerFleetCommand,
|
registerFleetCommand,
|
||||||
resolveFleetPaths,
|
resolveFleetPaths,
|
||||||
type CommandRunner,
|
type CommandRunner,
|
||||||
@@ -120,6 +122,37 @@ describe('fleet roster parsing', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => {
|
||||||
|
const generated = [
|
||||||
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
|
'MOSAIC_AGENT_RUNTIME=codex',
|
||||||
|
'MOSAIC_AGENT_WORKDIR=/srv/new',
|
||||||
|
'MOSAIC_TMUX_SOCKET=mosaic-factory',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
const existing = [
|
||||||
|
'MOSAIC_AGENT_NAME=old-name',
|
||||||
|
'MOSAIC_AGENT_RUNTIME=old-runtime',
|
||||||
|
'MOSAIC_AGENT_WORKDIR=/srv/old',
|
||||||
|
'MOSAIC_TMUX_SOCKET=old-socket',
|
||||||
|
'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh',
|
||||||
|
'# site note',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
expect(mergeAgentEnv(generated, existing)).toBe(
|
||||||
|
[
|
||||||
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
|
'MOSAIC_AGENT_RUNTIME=codex',
|
||||||
|
'MOSAIC_AGENT_WORKDIR=/srv/new',
|
||||||
|
'MOSAIC_TMUX_SOCKET=mosaic-factory',
|
||||||
|
'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh',
|
||||||
|
'# site note',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
||||||
cleanup = await tempDir();
|
cleanup = await tempDir();
|
||||||
const rosterPath = join(cleanup, 'roster.yaml');
|
const rosterPath = join(cleanup, 'roster.yaml');
|
||||||
@@ -229,10 +262,14 @@ describe('fleet command construction', () => {
|
|||||||
|
|
||||||
it('builds socket-scoped agent send commands', () => {
|
it('builds socket-scoped agent send commands', () => {
|
||||||
const paths = resolveFleetPaths('/home/test/.config/mosaic');
|
const paths = resolveFleetPaths('/home/test/.config/mosaic');
|
||||||
expect(buildAgentSendCommand(paths, 'coder0', 'hello', 'mosaic-factory')).toEqual([
|
expect(
|
||||||
|
buildAgentSendCommand(paths, 'coder0', 'hello', 'mosaic-factory', 'operator:mosaic-cli'),
|
||||||
|
).toEqual([
|
||||||
'/home/test/.config/mosaic/tools/tmux/agent-send.sh',
|
'/home/test/.config/mosaic/tools/tmux/agent-send.sh',
|
||||||
'-L',
|
'-L',
|
||||||
'mosaic-factory',
|
'mosaic-factory',
|
||||||
|
'-S',
|
||||||
|
'operator:mosaic-cli',
|
||||||
'-s',
|
'-s',
|
||||||
'coder0',
|
'coder0',
|
||||||
'-m',
|
'-m',
|
||||||
@@ -255,6 +292,36 @@ describe('fleet command construction', () => {
|
|||||||
expect(calls).toEqual([['systemctl', '--user', 'status', 'mosaic-tmux-holder.service']]);
|
expect(calls).toEqual([['systemctl', '--user', 'status', 'mosaic-tmux-holder.service']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('verifies liveness with tmux has-session and does not trust systemd active exited', async () => {
|
||||||
|
const home = await tempDir();
|
||||||
|
const rosterPath = join(home, 'fleet', 'roster.yaml');
|
||||||
|
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
rosterPath,
|
||||||
|
['version: 1', 'transport: tmux', 'agents:', ' - name: coder0', ' runtime: codex'].join(
|
||||||
|
'\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const calls: string[][] = [];
|
||||||
|
const runner: CommandRunner = async (command, args) => {
|
||||||
|
calls.push([command, ...args]);
|
||||||
|
return { stdout: 'active (exited)\n', stderr: '', exitCode: 0 };
|
||||||
|
};
|
||||||
|
const program = new Command();
|
||||||
|
program.exitOverride();
|
||||||
|
registerFleetCommand(program, { runner, mosaicHome: home });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await program.parseAsync(['node', 'mosaic', 'fleet', 'verify']);
|
||||||
|
expect(calls).toEqual([
|
||||||
|
['tmux', '-L', 'mosaic-factory', 'has-session', '-t', '=_holder:0.0'],
|
||||||
|
['tmux', '-L', 'mosaic-factory', 'has-session', '-t', '=coder0:0.0'],
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
await rm(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('writes init output to the explicit roster path', async () => {
|
it('writes init output to the explicit roster path', async () => {
|
||||||
const home = await tempDir();
|
const home = await tempDir();
|
||||||
const rosterPath = join(home, 'custom', 'roster.yaml');
|
const rosterPath = join(home, 'custom', 'roster.yaml');
|
||||||
@@ -536,6 +603,104 @@ describe('fleet command construction', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes a deterministic operator source label for agent sends', async () => {
|
||||||
|
const home = await tempDir();
|
||||||
|
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(home, 'fleet', 'roster.yaml'),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
transport: 'tmux',
|
||||||
|
agents: [{ name: 'json-agent', runtime: 'pi' }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const calls: string[][] = [];
|
||||||
|
const runner: CommandRunner = async (command, args) => {
|
||||||
|
calls.push([command, ...args]);
|
||||||
|
return { stdout: '', stderr: '', exitCode: 0 };
|
||||||
|
};
|
||||||
|
const program = new Command();
|
||||||
|
program.exitOverride();
|
||||||
|
registerAgentCommand(program, { runner, mosaicHome: home });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await program.parseAsync([
|
||||||
|
'node',
|
||||||
|
'mosaic',
|
||||||
|
'agent',
|
||||||
|
'send',
|
||||||
|
'json-agent',
|
||||||
|
'--message',
|
||||||
|
'status check',
|
||||||
|
]);
|
||||||
|
expect(calls).toEqual([
|
||||||
|
[
|
||||||
|
join(home, 'tools', 'tmux', 'agent-send.sh'),
|
||||||
|
'-L',
|
||||||
|
'mosaic-factory',
|
||||||
|
'-S',
|
||||||
|
getDefaultOperatorSourceLabel(),
|
||||||
|
'-s',
|
||||||
|
'json-agent',
|
||||||
|
'-m',
|
||||||
|
'status check',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
await rm(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows agent sends to override the source label explicitly', async () => {
|
||||||
|
const home = await tempDir();
|
||||||
|
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(home, 'fleet', 'roster.yaml'),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
transport: 'tmux',
|
||||||
|
agents: [{ name: 'coder0', runtime: 'codex' }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const calls: string[][] = [];
|
||||||
|
const runner: CommandRunner = async (command, args) => {
|
||||||
|
calls.push([command, ...args]);
|
||||||
|
return { stdout: '', stderr: '', exitCode: 0 };
|
||||||
|
};
|
||||||
|
const program = new Command();
|
||||||
|
program.exitOverride();
|
||||||
|
registerAgentCommand(program, { runner, mosaicHome: home });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await program.parseAsync([
|
||||||
|
'node',
|
||||||
|
'mosaic',
|
||||||
|
'agent',
|
||||||
|
'send',
|
||||||
|
'coder0',
|
||||||
|
'--message',
|
||||||
|
'handoff',
|
||||||
|
'--source-label',
|
||||||
|
'lead:manual',
|
||||||
|
]);
|
||||||
|
expect(calls).toEqual([
|
||||||
|
[
|
||||||
|
join(home, 'tools', 'tmux', 'agent-send.sh'),
|
||||||
|
'-L',
|
||||||
|
'mosaic-factory',
|
||||||
|
'-S',
|
||||||
|
'lead:manual',
|
||||||
|
'-s',
|
||||||
|
'coder0',
|
||||||
|
'-m',
|
||||||
|
'handoff',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
await rm(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects agent status typos before invoking the runner', async () => {
|
it('rejects agent status typos before invoking the runner', async () => {
|
||||||
const home = await tempDir();
|
const home = await tempDir();
|
||||||
const rosterPath = join(home, 'fleet', 'roster.yaml');
|
const rosterPath = join(home, 'fleet', 'roster.yaml');
|
||||||
@@ -560,4 +725,14 @@ describe('fleet command construction', () => {
|
|||||||
await rm(home, { recursive: true, force: true });
|
await rm(home, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps fleet framework assets in the published package file list', async () => {
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
await readFile(resolve(process.cwd(), 'package.json'), 'utf8'),
|
||||||
|
) as {
|
||||||
|
files?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(packageJson.files).toEqual(expect.arrayContaining(['dist', 'framework']));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { constants } from 'node:fs';
|
import { constants } from 'node:fs';
|
||||||
import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
import { access, chmod, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||||
import { homedir } from 'node:os';
|
import { homedir, hostname } from 'node:os';
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
@@ -148,6 +148,29 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeAgentEnv(generatedEnv: string, existingEnv?: string): string {
|
||||||
|
if (!existingEnv?.trim()) {
|
||||||
|
return generatedEnv;
|
||||||
|
}
|
||||||
|
const generatedKeys = new Set(
|
||||||
|
generatedEnv
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1])
|
||||||
|
.filter((key): key is string => key !== undefined),
|
||||||
|
);
|
||||||
|
const preservedLines = existingEnv.split('\n').filter((line) => {
|
||||||
|
if (!line.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const key = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1];
|
||||||
|
return key === undefined || !generatedKeys.has(key);
|
||||||
|
});
|
||||||
|
if (preservedLines.length === 0) {
|
||||||
|
return generatedEnv;
|
||||||
|
}
|
||||||
|
return [generatedEnv.trimEnd(), ...preservedLines, ''].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] {
|
export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] {
|
||||||
const service = agentName ? `mosaic-agent@${agentName}.service` : 'mosaic-tmux-holder.service';
|
const service = agentName ? `mosaic-agent@${agentName}.service` : 'mosaic-tmux-holder.service';
|
||||||
return ['systemctl', '--user', action, service];
|
return ['systemctl', '--user', action, service];
|
||||||
@@ -158,11 +181,14 @@ export function buildAgentSendCommand(
|
|||||||
agentName: string,
|
agentName: string,
|
||||||
message: string,
|
message: string,
|
||||||
socketName = DEFAULT_SOCKET_NAME,
|
socketName = DEFAULT_SOCKET_NAME,
|
||||||
|
sourceLabel = getDefaultOperatorSourceLabel(),
|
||||||
): string[] {
|
): string[] {
|
||||||
return [
|
return [
|
||||||
join(paths.tmuxToolsDir, 'agent-send.sh'),
|
join(paths.tmuxToolsDir, 'agent-send.sh'),
|
||||||
'-L',
|
'-L',
|
||||||
socketName,
|
socketName,
|
||||||
|
'-S',
|
||||||
|
sourceLabel,
|
||||||
'-s',
|
'-s',
|
||||||
agentName,
|
agentName,
|
||||||
'-m',
|
'-m',
|
||||||
@@ -170,6 +196,11 @@ export function buildAgentSendCommand(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDefaultOperatorSourceLabel(): string {
|
||||||
|
const shortHostname = hostname().split('.')[0] || 'localhost';
|
||||||
|
return `${shortHostname}:operator`;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildAgentResetCommand(
|
export function buildAgentResetCommand(
|
||||||
paths: FleetPaths,
|
paths: FleetPaths,
|
||||||
agentName: string,
|
agentName: string,
|
||||||
@@ -384,15 +415,22 @@ export function registerFleetAgentCommands(
|
|||||||
.command('send <agent>')
|
.command('send <agent>')
|
||||||
.description('Send a message to a local fleet agent')
|
.description('Send a message to a local fleet agent')
|
||||||
.requiredOption('--message <text>', 'Message text')
|
.requiredOption('--message <text>', 'Message text')
|
||||||
.action(async (agent: string, opts: { message: string }) => {
|
.option('--source-label <label>', 'Source label for the message preamble')
|
||||||
|
.option('--source <label>', 'Alias for --source-label')
|
||||||
|
.action(
|
||||||
|
async (agent: string, opts: { message: string; sourceLabel?: string; source?: string }) => {
|
||||||
const roster = await loadRosterFromAgentCommand(agentCommand, deps.mosaicHome);
|
const roster = await loadRosterFromAgentCommand(agentCommand, deps.mosaicHome);
|
||||||
getRosterAgent(roster, agent);
|
getRosterAgent(roster, agent);
|
||||||
const paths = resolveFleetPaths(resolveMosaicHomeFromCommand(agentCommand, deps.mosaicHome));
|
const paths = resolveFleetPaths(
|
||||||
|
resolveMosaicHomeFromCommand(agentCommand, deps.mosaicHome),
|
||||||
|
);
|
||||||
|
const sourceLabel = opts.sourceLabel ?? opts.source ?? getDefaultOperatorSourceLabel();
|
||||||
await runChecked(
|
await runChecked(
|
||||||
runner,
|
runner,
|
||||||
buildAgentSendCommand(paths, agent, opts.message, roster.tmux.socketName),
|
buildAgentSendCommand(paths, agent, opts.message, roster.tmux.socketName, sourceLabel),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
agentCommand
|
agentCommand
|
||||||
.command('reset <agent>')
|
.command('reset <agent>')
|
||||||
@@ -440,18 +478,19 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
|
|||||||
await mkdir(activePaths.systemdUserDir, { recursive: true });
|
await mkdir(activePaths.systemdUserDir, { recursive: true });
|
||||||
await mkdir(activePaths.agentEnvDir, { recursive: true });
|
await mkdir(activePaths.agentEnvDir, { recursive: true });
|
||||||
|
|
||||||
|
const startAgentSessionPath = join(activePaths.fleetToolsDir, 'start-agent-session.sh');
|
||||||
|
const sendMessagePath = join(activePaths.tmuxToolsDir, 'send-message.sh');
|
||||||
|
const agentSendPath = join(activePaths.tmuxToolsDir, 'agent-send.sh');
|
||||||
|
const executableToolPaths = [startAgentSessionPath, sendMessagePath, agentSendPath];
|
||||||
await copyFile(
|
await copyFile(
|
||||||
join(frameworkRoot, 'tools', 'fleet', 'start-agent-session.sh'),
|
join(frameworkRoot, 'tools', 'fleet', 'start-agent-session.sh'),
|
||||||
join(activePaths.fleetToolsDir, 'start-agent-session.sh'),
|
startAgentSessionPath,
|
||||||
);
|
|
||||||
await copyFile(
|
|
||||||
join(frameworkRoot, 'tools', 'tmux', 'send-message.sh'),
|
|
||||||
join(activePaths.tmuxToolsDir, 'send-message.sh'),
|
|
||||||
);
|
|
||||||
await copyFile(
|
|
||||||
join(frameworkRoot, 'tools', 'tmux', 'agent-send.sh'),
|
|
||||||
join(activePaths.tmuxToolsDir, 'agent-send.sh'),
|
|
||||||
);
|
);
|
||||||
|
await copyFile(join(frameworkRoot, 'tools', 'tmux', 'send-message.sh'), sendMessagePath);
|
||||||
|
await copyFile(join(frameworkRoot, 'tools', 'tmux', 'agent-send.sh'), agentSendPath);
|
||||||
|
for (const toolPath of executableToolPaths) {
|
||||||
|
await chmod(toolPath, 0o755);
|
||||||
|
}
|
||||||
await copyFile(
|
await copyFile(
|
||||||
join(frameworkRoot, 'systemd', 'user', 'mosaic-tmux-holder.service'),
|
join(frameworkRoot, 'systemd', 'user', 'mosaic-tmux-holder.service'),
|
||||||
join(activePaths.systemdUserDir, 'mosaic-tmux-holder.service'),
|
join(activePaths.systemdUserDir, 'mosaic-tmux-holder.service'),
|
||||||
@@ -462,10 +501,9 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const agent of roster.agents) {
|
for (const agent of roster.agents) {
|
||||||
await writeFile(
|
const envPath = join(activePaths.agentEnvDir, `${agent.name}.env`);
|
||||||
join(activePaths.agentEnvDir, `${agent.name}.env`),
|
const existingEnv = (await canRead(envPath)) ? await readFile(envPath, 'utf8') : undefined;
|
||||||
generateAgentEnv(roster, agent),
|
await writeFile(envPath, mergeAgentEnv(generateAgentEnv(roster, agent), existingEnv));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Installed fleet files for ${roster.agents.length} agent(s).`);
|
console.log(`Installed fleet files for ${roster.agents.length} agent(s).`);
|
||||||
|
|||||||
Reference in New Issue
Block a user