feat(fleet): enhancer role + two-agent floor (orchestrator + enhancer) (#615)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #615.
This commit is contained in:
2026-06-22 13:15:59 +00:00
committed by jason.woltje
parent 4434c3c481
commit 2bf66136e4
9 changed files with 184 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ import {
buildTmuxListSessionsCommand,
classifySendResult,
countOrchestrators,
countEnhancers,
detectDrift,
enableFleetUnits,
FLEET_PROFILES,
@@ -1076,9 +1077,9 @@ describe('fleet-polish bundle — boot-survival symmetry', () => {
}
});
it('fleet init --write fails hard when a non-minimal profile lacks exactly one orchestrator', async () => {
// The general profile must yield exactly one orchestrator; the guarantee is
// enforced (not just warned). We assert the happy path writes cleanly.
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();
@@ -1098,7 +1099,9 @@ describe('fleet-polish bundle — boot-survival symmetry', () => {
]);
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 });
}
@@ -2310,47 +2313,63 @@ describe('fleet preset rosters', () => {
},
);
it('general preset: orchestrator + one generalist worker', async () => {
it('general preset: orchestrator + enhancer + one generalist worker', async () => {
const roster = await loadFleetRoster(join(examplesDir, 'general.yaml'));
expect(roster.agents.map((a) => a.name)).toEqual(['orchestrator', 'generalist']);
expect(roster.agents.map((a) => a.name)).toEqual(['orchestrator', 'enhancer', '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 + coder0 + coder1 + reviewer', async () => {
it('coding preset: orchestrator + enhancer + 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 + researcher0 + researcher1 + analyst', async () => {
it('research preset: orchestrator + enhancer + 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 + coder0 + researcher0 + reviewer', async () => {
it('hybrid preset: orchestrator + enhancer + 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`));
const workers = roster.agents.filter((a) => a.name !== 'orchestrator');
// Core agents (orchestrator + enhancer) run claude; only ephemeral workers are pi.
const workers = roster.agents.filter(
(a) => a.className !== 'orchestrator' && a.className !== 'enhancer',
);
for (const worker of workers) {
expect(worker.runtime).toBe('pi');
expect(worker.modelHint).toBe('openai-codex/gpt-5.5:high');
@@ -2492,6 +2511,43 @@ 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');