Compare commits
1 Commits
feat/fleet
...
feat/f3-m3
| Author | SHA1 | Date | |
|---|---|---|---|
| dcb7477007 |
@@ -58,11 +58,3 @@ Active workstream is **W1 — Federation v1**. Workers should:
|
||||
## F3-m3 — mosaic update re-seeds framework + relaunches agents (#609) — feat/f3-m3-update-reseed
|
||||
|
||||
- Status: implemented + tested. Closes R13: `mosaic update` now re-seeds the framework (data-safe MOSAIC_SYNC_ONLY) after the CLI install so shipped launcher/runtime changes activate; `--relaunch` restarts rostered agents; `--no-reseed` opts out. Detail: scratchpads/f3-m3-update-reseed.md.
|
||||
|
||||
## Fleet-polish bundle — boot-survival symmetry (#611) — feat/fleet-polish-bundle
|
||||
|
||||
- Status: implemented + tested. disable-on-remove (boot-resurrection bug, TDD) + add-enable + init-R5 hard guarantee. 4 new + 147 existing fleet tests green. Detail: scratchpads/fleet-polish-bundle.md.
|
||||
|
||||
## Fleet enhancer role + two-agent floor (#614) — feat/fleet-enhancer-floor (stacked on #612)
|
||||
|
||||
- Status: implemented + tested. enhancer added to 4 presets; init guarantees 1 orchestrator + >=1 enhancer; remove protects the sole enhancer; enhancer role doc. 155 fleet tests green. Detail: scratchpads/fleet-enhancer-floor.md.
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Fleet enhancer role + two-agent floor (#614)
|
||||
|
||||
- **Issue:** #614 · **Branch:** `feat/fleet-enhancer-floor` (stacked on #612 `feat/fleet-polish-bundle`)
|
||||
- **Doctrine:** `docs/fleet/north-star.md` (PR #613) — every fleet = orchestrator + enhancer minimum.
|
||||
|
||||
## Changes
|
||||
|
||||
- **Presets** (general, coding, research, hybrid): add `enhancer` (claude, `class: enhancer`,
|
||||
`persistent_persona: true`) as a core always-on agent alongside the orchestrator. minimal/local-canary
|
||||
unchanged.
|
||||
- **fleet.ts**: `countEnhancers` helper; init guarantee extended — non-minimal profiles must yield
|
||||
exactly 1 orchestrator AND >=1 enhancer (hard-fail otherwise); `removeAgentFromRoster` refuses to drop
|
||||
the sole enhancer (symmetric with the sole-orchestrator guard) so the floor holds at runtime, not just init.
|
||||
- **Role doc**: `framework/fleet/roles/enhancer.md` — the enhancer mandate (monitor → analyze → plan →
|
||||
upgrade tools/skills/harness WITH orchestrator → file Mosaic Stack bug reports) + boundaries (does NOT
|
||||
code or review).
|
||||
|
||||
## Verification
|
||||
|
||||
- 155 fleet tests green (new: countEnhancers; remove-sole-enhancer guard; remove-allows-when-another;
|
||||
init two-agent-floor; every-non-minimal-preset-has-enhancer; updated preset rosters). tsc/eslint/
|
||||
prettier/sanitize clean. TDD on the init guarantee + remove protection.
|
||||
|
||||
## Stacking
|
||||
|
||||
Built on #612's init-R5 code. PR shows #612 + enhancer until #612 merges; then rebase onto main → clean.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Fleet-polish bundle — boot-survival symmetry (#611)
|
||||
|
||||
- **Issue:** #611 · **Branch:** `feat/fleet-polish-bundle` · From the Lead's Codex symmetry-gap finding.
|
||||
|
||||
## Three fixes
|
||||
|
||||
1. **disable-on-remove (BUG, TDD).** `fleet remove` stopped + deleted roster/env/heartbeat but never
|
||||
`systemctl --user disable mosaic-agent@NAME.service` → a removed-but-enabled unit could resurrect on
|
||||
reboot pointing at deleted config. Fix: `buildSystemdDisableCommand` + disable in `remove`
|
||||
(best-effort, gated on !--keep-files).
|
||||
2. **add-enable.** `fleet add` now enables the new agent's unit for boot-survival (best-effort,
|
||||
independent of --start) — symmetry with disable-on-remove.
|
||||
3. **init-R5 guarantee.** `fleet init --write` now FAILS HARD when a non-minimal profile doesn't yield
|
||||
exactly one orchestrator (was a soft warning). `minimal` (sanctioned no-orchestrator) still allowed.
|
||||
|
||||
## Verification
|
||||
|
||||
- 4 new tests (disable builder; remove-invokes-disable; add-invokes-enable; init general → exactly 1
|
||||
orchestrator) + 147 existing fleet tests green (151 total). tsc/eslint/prettier clean.
|
||||
- TDD on the disable bug per contract.
|
||||
@@ -15,10 +15,6 @@ agents:
|
||||
runtime: claude
|
||||
class: orchestrator
|
||||
persistent_persona: true
|
||||
- name: enhancer
|
||||
runtime: claude
|
||||
class: enhancer
|
||||
persistent_persona: true
|
||||
- name: coder0
|
||||
runtime: pi
|
||||
class: implementer
|
||||
|
||||
@@ -15,10 +15,6 @@ agents:
|
||||
runtime: claude
|
||||
class: orchestrator
|
||||
persistent_persona: true
|
||||
- name: enhancer
|
||||
runtime: claude
|
||||
class: enhancer
|
||||
persistent_persona: true
|
||||
- name: generalist
|
||||
runtime: pi
|
||||
class: worker
|
||||
|
||||
@@ -15,10 +15,6 @@ agents:
|
||||
runtime: claude
|
||||
class: orchestrator
|
||||
persistent_persona: true
|
||||
- name: enhancer
|
||||
runtime: claude
|
||||
class: enhancer
|
||||
persistent_persona: true
|
||||
- name: coder0
|
||||
runtime: pi
|
||||
class: implementer
|
||||
|
||||
@@ -15,10 +15,6 @@ agents:
|
||||
runtime: claude
|
||||
class: orchestrator
|
||||
persistent_persona: true
|
||||
- name: enhancer
|
||||
runtime: claude
|
||||
class: enhancer
|
||||
persistent_persona: true
|
||||
- name: researcher0
|
||||
runtime: pi
|
||||
class: researcher
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Enhancer — fleet role definition
|
||||
|
||||
The **enhancer** is one half of the fleet's two-agent floor: every fleet runs, at
|
||||
minimum, an **orchestrator** and an **enhancer**. The orchestrator drives delivery;
|
||||
the enhancer makes the fleet _get better at delivering_ over time.
|
||||
|
||||
It is a **core, always-on** agent (`class: enhancer`, `persistent_persona: true`),
|
||||
not an ephemeral per-lane worker.
|
||||
|
||||
## Mandate
|
||||
|
||||
The enhancer runs the fleet's **continuous-improvement loop**:
|
||||
|
||||
1. **Monitor** fleet activity — agents, heartbeats, sessions, throughput, failures.
|
||||
2. **Analyze** for enhancements and optimizations — friction, gaps, recurring defects,
|
||||
missing or broken tools, skill/harness shortfalls.
|
||||
3. **Plan** a remediation: a concrete improvement with rationale and expected effect.
|
||||
4. **Upgrade fleet capability — with the orchestrator** — tool creation/repair, skills,
|
||||
harness improvements. The orchestrator owns fleet composition; the enhancer advises and
|
||||
implements improvements to the _means of production_, not the product.
|
||||
5. **File upstream bug reports** to Mosaic Stack for real defects, so they flow back to the
|
||||
framework for proper remediation rather than being patched over locally.
|
||||
6. **Recommend which agents are needed** — advise the orchestrator on roles to add/remove as
|
||||
the mission evolves.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- **Does NOT write product/source code.**
|
||||
- **Does NOT review code** (that is the code-review / security-review roles).
|
||||
- **Does NOT perform delivery tasks.**
|
||||
|
||||
Improvement and diagnosis only. When the enhancer finds work that requires coding or review,
|
||||
it files it (bug report / recommendation) and the orchestrator materializes the right worker.
|
||||
|
||||
## Why two, not one
|
||||
|
||||
The orchestrator alone optimizes for _this_ delivery; the enhancer optimizes for _every future_
|
||||
delivery — self-healing the fleet's tools, skills, and harnesses, and routing real defects
|
||||
upstream. Together they are the irreducible core; every other role is added on demand.
|
||||
|
||||
> Doctrine: `docs/fleet/north-star.md` (two-agent floor + role library).
|
||||
@@ -14,13 +14,11 @@ import {
|
||||
buildEnableLingerCommand,
|
||||
buildFleetServiceCommand,
|
||||
buildSystemdEnableCommand,
|
||||
buildSystemdDisableCommand,
|
||||
buildSystemdShowCommand,
|
||||
buildTmuxListPanesCommand,
|
||||
buildTmuxListSessionsCommand,
|
||||
classifySendResult,
|
||||
countOrchestrators,
|
||||
countEnhancers,
|
||||
detectDrift,
|
||||
enableFleetUnits,
|
||||
FLEET_PROFILES,
|
||||
@@ -985,129 +983,6 @@ describe('fleet ps — drift detection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fleet-polish bundle — boot-survival symmetry', () => {
|
||||
async function rosterHome(agents: string): Promise<string> {
|
||||
const home = await tempDir();
|
||||
await mkdir(join(home, 'fleet'), { recursive: true });
|
||||
await writeFile(join(home, 'fleet', 'roster.yaml'), agents);
|
||||
return home;
|
||||
}
|
||||
|
||||
it('buildSystemdDisableCommand returns the systemctl --user disable array', () => {
|
||||
expect(buildSystemdDisableCommand('mosaic-agent@coder0.service')).toEqual([
|
||||
'systemctl',
|
||||
'--user',
|
||||
'disable',
|
||||
'mosaic-agent@coder0.service',
|
||||
]);
|
||||
});
|
||||
|
||||
it('fleet remove DISABLES the unit so a removed agent cannot resurrect on boot', async () => {
|
||||
const home = await rosterHome(
|
||||
[
|
||||
'version: 1',
|
||||
'transport: tmux',
|
||||
'agents:',
|
||||
' - name: orchestrator',
|
||||
' runtime: pi',
|
||||
' class: orchestrator',
|
||||
' - name: coder0',
|
||||
' runtime: codex',
|
||||
' class: worker',
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
const calls: string[][] = [];
|
||||
const runner: CommandRunner = async (command, args) => {
|
||||
calls.push([command, ...args]);
|
||||
return { stdout: '', stderr: '', exitCode: 0 };
|
||||
};
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, { runner, mosaicHome: home });
|
||||
try {
|
||||
await program.parseAsync(['node', 'mosaic', 'fleet', 'remove', 'coder0']);
|
||||
expect(calls).toContainEqual([
|
||||
'systemctl',
|
||||
'--user',
|
||||
'disable',
|
||||
'mosaic-agent@coder0.service',
|
||||
]);
|
||||
// stop must still happen too
|
||||
expect(calls).toContainEqual(['systemctl', '--user', 'stop', 'mosaic-agent@coder0.service']);
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('fleet add ENABLES the new agent unit for boot-survival', async () => {
|
||||
const home = await rosterHome(
|
||||
['version: 1', 'transport: tmux', 'agents:', ' - name: coder0', ' runtime: codex'].join(
|
||||
'\n',
|
||||
) + '\n',
|
||||
);
|
||||
const calls: string[][] = [];
|
||||
const runner: CommandRunner = async (command, args) => {
|
||||
calls.push([command, ...args]);
|
||||
return { stdout: '', stderr: '', exitCode: 0 };
|
||||
};
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, { runner, mosaicHome: home });
|
||||
try {
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'mosaic',
|
||||
'fleet',
|
||||
'add',
|
||||
'coder1',
|
||||
'--runtime',
|
||||
'codex',
|
||||
'--class',
|
||||
'worker',
|
||||
'--no-start',
|
||||
]);
|
||||
expect(calls).toContainEqual([
|
||||
'systemctl',
|
||||
'--user',
|
||||
'enable',
|
||||
'mosaic-agent@coder1.service',
|
||||
]);
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('fleet init --write enforces the two-agent floor (1 orchestrator + >=1 enhancer)', async () => {
|
||||
// The general profile must yield exactly one orchestrator AND at least one
|
||||
// enhancer; the guarantee is enforced (not just warned). Happy path writes cleanly.
|
||||
const home = await tempDir();
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerFleetCommand(program, {
|
||||
runner: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
|
||||
mosaicHome: home,
|
||||
});
|
||||
try {
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'mosaic',
|
||||
'fleet',
|
||||
'init',
|
||||
'--profile',
|
||||
'general',
|
||||
'--write',
|
||||
]);
|
||||
const written = await readFile(join(home, 'fleet', 'roster.yaml'), 'utf8');
|
||||
const orchestrators = (written.match(/class:\s*orchestrator/g) ?? []).length;
|
||||
const enhancers = (written.match(/class:\s*enhancer/g) ?? []).length;
|
||||
expect(orchestrators).toBe(1);
|
||||
expect(enhancers).toBeGreaterThanOrEqual(1);
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('fleet install — auto-enable units for boot-survival', () => {
|
||||
it('buildSystemdEnableCommand and buildEnableLingerCommand return correct command arrays', () => {
|
||||
expect(buildSystemdEnableCommand('mosaic-tmux-holder.service')).toEqual([
|
||||
@@ -2313,63 +2188,47 @@ describe('fleet preset rosters', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('general preset: orchestrator + enhancer + one generalist worker', async () => {
|
||||
it('general preset: orchestrator + one generalist worker', async () => {
|
||||
const roster = await loadFleetRoster(join(examplesDir, 'general.yaml'));
|
||||
expect(roster.agents.map((a) => a.name)).toEqual(['orchestrator', 'enhancer', 'generalist']);
|
||||
expect(roster.agents.map((a) => a.name)).toEqual(['orchestrator', 'generalist']);
|
||||
expect(roster.agents.find((a) => a.name === 'orchestrator')?.runtime).toBe('claude');
|
||||
expect(roster.agents.find((a) => a.name === 'enhancer')?.className).toBe('enhancer');
|
||||
expect(roster.agents.find((a) => a.name === 'generalist')?.runtime).toBe('pi');
|
||||
});
|
||||
|
||||
it('coding preset: orchestrator + enhancer + coder0 + coder1 + reviewer', async () => {
|
||||
it('coding preset: orchestrator + coder0 + coder1 + reviewer', async () => {
|
||||
const roster = await loadFleetRoster(join(examplesDir, 'coding.yaml'));
|
||||
expect(roster.agents.map((a) => a.name)).toEqual([
|
||||
'orchestrator',
|
||||
'enhancer',
|
||||
'coder0',
|
||||
'coder1',
|
||||
'reviewer',
|
||||
]);
|
||||
});
|
||||
|
||||
it('research preset: orchestrator + enhancer + researcher0 + researcher1 + analyst', async () => {
|
||||
it('research preset: orchestrator + researcher0 + researcher1 + analyst', async () => {
|
||||
const roster = await loadFleetRoster(join(examplesDir, 'research.yaml'));
|
||||
expect(roster.agents.map((a) => a.name)).toEqual([
|
||||
'orchestrator',
|
||||
'enhancer',
|
||||
'researcher0',
|
||||
'researcher1',
|
||||
'analyst',
|
||||
]);
|
||||
});
|
||||
|
||||
it('hybrid preset: orchestrator + enhancer + coder0 + researcher0 + reviewer', async () => {
|
||||
it('hybrid preset: orchestrator + coder0 + researcher0 + reviewer', async () => {
|
||||
const roster = await loadFleetRoster(join(examplesDir, 'hybrid.yaml'));
|
||||
expect(roster.agents.map((a) => a.name)).toEqual([
|
||||
'orchestrator',
|
||||
'enhancer',
|
||||
'coder0',
|
||||
'researcher0',
|
||||
'reviewer',
|
||||
]);
|
||||
});
|
||||
|
||||
it('every non-minimal preset carries an enhancer (two-agent floor)', async () => {
|
||||
for (const preset of ['general', 'coding', 'research', 'hybrid'] as FleetProfile[]) {
|
||||
const roster = await loadFleetRoster(join(examplesDir, `${preset}.yaml`));
|
||||
expect(countOrchestrators(roster)).toBe(1);
|
||||
expect(countEnhancers(roster)).toBeGreaterThanOrEqual(1);
|
||||
expect(roster.agents.find((a) => a.className === 'enhancer')?.runtime).toBe('claude');
|
||||
}
|
||||
});
|
||||
|
||||
it('worker agents in new presets use pi runtime with model_hint openai-codex/gpt-5.5:high', async () => {
|
||||
for (const preset of ['general', 'coding', 'research', 'hybrid'] as FleetProfile[]) {
|
||||
const roster = await loadFleetRoster(join(examplesDir, `${preset}.yaml`));
|
||||
// Core agents (orchestrator + enhancer) run claude; only ephemeral workers are pi.
|
||||
const workers = roster.agents.filter(
|
||||
(a) => a.className !== 'orchestrator' && a.className !== 'enhancer',
|
||||
);
|
||||
const workers = roster.agents.filter((a) => a.name !== 'orchestrator');
|
||||
for (const worker of workers) {
|
||||
expect(worker.runtime).toBe('pi');
|
||||
expect(worker.modelHint).toBe('openai-codex/gpt-5.5:high');
|
||||
@@ -2511,43 +2370,6 @@ describe('fleet add/remove — pure helpers', () => {
|
||||
expect(updated.agents.map((a) => a.name)).toEqual(['orchestrator2', 'coder0']);
|
||||
});
|
||||
|
||||
it('countEnhancers counts enhancer-class agents (two-agent floor)', () => {
|
||||
const roster: FleetRoster = {
|
||||
...baseRoster,
|
||||
agents: [
|
||||
{ name: 'orchestrator', runtime: 'claude', className: 'orchestrator' },
|
||||
{ name: 'enhancer', runtime: 'claude', className: 'enhancer' },
|
||||
{ name: 'coder0', runtime: 'codex', className: 'worker' },
|
||||
],
|
||||
};
|
||||
expect(countEnhancers(roster)).toBe(1);
|
||||
expect(countEnhancers(baseRoster)).toBe(0);
|
||||
});
|
||||
|
||||
it('removeAgentFromRoster throws when removing the sole enhancer (two-agent floor)', () => {
|
||||
const roster: FleetRoster = {
|
||||
...baseRoster,
|
||||
agents: [
|
||||
{ name: 'orchestrator', runtime: 'claude', className: 'orchestrator' },
|
||||
{ name: 'enhancer', runtime: 'claude', className: 'enhancer' },
|
||||
],
|
||||
};
|
||||
expect(() => removeAgentFromRoster(roster, 'enhancer')).toThrow('sole enhancer');
|
||||
});
|
||||
|
||||
it('removeAgentFromRoster allows removing an enhancer when another remains', () => {
|
||||
const roster: FleetRoster = {
|
||||
...baseRoster,
|
||||
agents: [
|
||||
{ name: 'orchestrator', runtime: 'claude', className: 'orchestrator' },
|
||||
{ name: 'enhancer', runtime: 'claude', className: 'enhancer' },
|
||||
{ name: 'enhancer2', runtime: 'claude', className: 'enhancer' },
|
||||
],
|
||||
};
|
||||
const updated = removeAgentFromRoster(roster, 'enhancer');
|
||||
expect(updated.agents.map((a) => a.name)).toEqual(['orchestrator', 'enhancer2']);
|
||||
});
|
||||
|
||||
it('serializeRosterToYaml produces YAML that round-trips through loadFleetRoster', async () => {
|
||||
const yaml = serializeRosterToYaml(baseRoster);
|
||||
expect(typeof yaml).toBe('string');
|
||||
|
||||
@@ -227,15 +227,6 @@ export function buildSystemdEnableCommand(unit: string): string[] {
|
||||
return ['systemctl', '--user', 'enable', unit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the systemctl --user disable command for a given unit.
|
||||
* Used by `fleet remove` so a removed agent's enabled unit cannot resurrect on
|
||||
* boot pointing at deleted config (boot-survival symmetry with enable-on-add).
|
||||
*/
|
||||
export function buildSystemdDisableCommand(unit: string): string[] {
|
||||
return ['systemctl', '--user', 'disable', unit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the loginctl enable-linger command for a given user.
|
||||
* Linger allows user systemd services to survive logout.
|
||||
@@ -881,33 +872,20 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await writeFile(destination, content);
|
||||
|
||||
// Guarantee the two-agent floor: exactly one orchestrator AND at least
|
||||
// one enhancer for every profile except the sanctioned no-orchestrator
|
||||
// `minimal` preset. A mismatch means a corrupted/edited preset — fail hard
|
||||
// rather than write a malformed fleet.
|
||||
// Validate: exactly one orchestrator required (R5) — friendly summary on success.
|
||||
const written = await loadFleetRoster(destination);
|
||||
const orchCount = countOrchestrators(written);
|
||||
const enhancerCount = countEnhancers(written);
|
||||
if (profile === 'minimal') {
|
||||
if (orchCount !== 1) {
|
||||
process.stderr.write(
|
||||
`Warning: fleet roster at ${destination} has ${orchCount} orchestrator agent(s) (expected exactly 1).\n`,
|
||||
);
|
||||
console.log(
|
||||
`Initialized ${profile} fleet: ${written.agents.length} agent(s) (no orchestrator). Next: mosaic fleet install`,
|
||||
);
|
||||
} else if (orchCount !== 1) {
|
||||
throw new Error(
|
||||
`Fleet init failed: the "${profile}" roster has ${orchCount} orchestrator agent(s), ` +
|
||||
`expected exactly 1 (R5). The preset may be corrupted — re-install the framework.`,
|
||||
);
|
||||
} else if (enhancerCount < 1) {
|
||||
throw new Error(
|
||||
`Fleet init failed: the "${profile}" roster has no enhancer agent. Every fleet keeps an ` +
|
||||
`orchestrator + enhancer minimum (two-agent floor). The preset may be corrupted — ` +
|
||||
`re-install the framework.`,
|
||||
`Initialized ${profile} fleet: ${written.agents.length} agent(s). Next: mosaic fleet install`,
|
||||
);
|
||||
} else {
|
||||
const workerCount = written.agents.length - 1 - enhancerCount;
|
||||
const workerCount = written.agents.length - 1;
|
||||
console.log(
|
||||
`Initialized ${profile} fleet: 1 orchestrator + ${enhancerCount} enhancer(s) + ` +
|
||||
`${workerCount} worker(s). Next: mosaic fleet install`,
|
||||
`Initialized ${profile} fleet: 1 orchestrator + ${workerCount} agent(s). Next: mosaic fleet install`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1240,24 +1218,6 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
|
||||
|
||||
console.log(`Added ${name} (${opts.runtime}/${opts.class}) to the fleet.`);
|
||||
|
||||
// Enable the unit for boot-survival (non-fatal) — symmetry with
|
||||
// disable-on-remove. Independent of --start so a queued agent still
|
||||
// survives a reboot once its unit exists.
|
||||
try {
|
||||
const enableResult = await runner(
|
||||
...splitCommand(buildSystemdEnableCommand(`mosaic-agent@${name}.service`)),
|
||||
);
|
||||
if (enableResult.exitCode !== 0) {
|
||||
process.stderr.write(
|
||||
`Warning: could not enable mosaic-agent@${name}.service: ${enableResult.stderr || enableResult.stdout || 'non-zero exit'}\n`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
process.stderr.write(
|
||||
`Warning: enable command failed for ${name}: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.start !== false) {
|
||||
await runChecked(runner, buildFleetServiceCommand('start', name));
|
||||
console.log(`Started mosaic-agent@${name}.service.`);
|
||||
@@ -1294,26 +1254,6 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
|
||||
);
|
||||
}
|
||||
|
||||
// Disable the unit (non-fatal) so an enabled instance cannot resurrect on
|
||||
// boot pointing at the now-deleted config — boot-survival symmetry with
|
||||
// enable-on-add. Skipped only when --keep-files keeps the config in place.
|
||||
if (!opts.keepFiles) {
|
||||
try {
|
||||
const disableResult = await runner(
|
||||
...splitCommand(buildSystemdDisableCommand(`mosaic-agent@${name}.service`)),
|
||||
);
|
||||
if (disableResult.exitCode !== 0) {
|
||||
process.stderr.write(
|
||||
`Warning: could not disable mosaic-agent@${name}.service: ${disableResult.stderr || disableResult.stdout || 'non-zero exit'}\n`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
process.stderr.write(
|
||||
`Warning: disable command failed for ${name}: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Write updated roster
|
||||
await writeFile(rosterPath, serializeRosterToYaml(updatedRoster));
|
||||
|
||||
@@ -1954,15 +1894,6 @@ export function countOrchestrators(roster: FleetRoster): number {
|
||||
return roster.agents.filter((a) => a.className === 'orchestrator').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count enhancer agents in a parsed roster. The two-agent floor (north-star)
|
||||
* requires every non-minimal fleet to carry at least one enhancer alongside the
|
||||
* sole orchestrator.
|
||||
*/
|
||||
export function countEnhancers(roster: FleetRoster): number {
|
||||
return roster.agents.filter((a) => a.className === 'enhancer').length;
|
||||
}
|
||||
|
||||
/** Valid runtime identifiers for fleet agents. */
|
||||
export const VALID_FLEET_RUNTIMES: readonly string[] = [
|
||||
'pi',
|
||||
@@ -2005,15 +1936,6 @@ export function removeAgentFromRoster(roster: FleetRoster, name: string): FleetR
|
||||
`Cannot remove agent "${name}": it is the sole orchestrator. Add another orchestrator first (R5).`,
|
||||
);
|
||||
}
|
||||
// Two-agent floor: never drop the last enhancer (the continuous-improvement
|
||||
// loop). Symmetric with the sole-orchestrator guard.
|
||||
const remainingEnhancerCount = remaining.filter((a) => a.className === 'enhancer').length;
|
||||
if (remainingEnhancerCount === 0 && agent.className === 'enhancer') {
|
||||
throw new Error(
|
||||
`Cannot remove agent "${name}": it is the sole enhancer. Every fleet keeps at least one ` +
|
||||
`enhancer (two-agent floor). Add another enhancer first.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
...roster,
|
||||
agents: remaining,
|
||||
|
||||
Reference in New Issue
Block a user