Compare commits

...

1 Commits

Author SHA1 Message Date
5e73481f0a feat(fleet): enhancer role + two-agent floor (orchestrator + enhancer) (#614)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
From Jason's north-star (docs/fleet/north-star.md, #613): every fleet has a
two-agent floor — orchestrator + enhancer minimum. Builds on #612's init-R5.

- Presets (general/coding/research/hybrid): add enhancer (claude, class:
  enhancer, persistent_persona) as a core always-on agent. minimal/local-canary
  unchanged.
- fleet.ts: countEnhancers helper; init guarantee extended — non-minimal
  profiles must yield exactly 1 orchestrator AND >=1 enhancer (hard-fail);
  removeAgentFromRoster refuses to drop the sole enhancer (symmetric with the
  sole-orchestrator guard) so the floor holds at runtime, not just init.
- 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).

Verified: 155 fleet tests green (countEnhancers; sole-enhancer remove 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.

Stacked on #612 (feat/fleet-polish-bundle); rebases clean onto main after #612.

Refs #614, #613

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsgTQzV5YUGk1JtCLP4B83
2026-06-22 03:13:53 -05:00
9 changed files with 184 additions and 14 deletions

View File

@@ -62,3 +62,7 @@ Active workstream is **W1 — Federation v1**. Workers should:
## Fleet-polish bundle — boot-survival symmetry (#611) — feat/fleet-polish-bundle ## 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. - 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.

View File

@@ -0,0 +1,26 @@
# 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.

View File

@@ -15,6 +15,10 @@ agents:
runtime: claude runtime: claude
class: orchestrator class: orchestrator
persistent_persona: true persistent_persona: true
- name: enhancer
runtime: claude
class: enhancer
persistent_persona: true
- name: coder0 - name: coder0
runtime: pi runtime: pi
class: implementer class: implementer

View File

@@ -15,6 +15,10 @@ agents:
runtime: claude runtime: claude
class: orchestrator class: orchestrator
persistent_persona: true persistent_persona: true
- name: enhancer
runtime: claude
class: enhancer
persistent_persona: true
- name: generalist - name: generalist
runtime: pi runtime: pi
class: worker class: worker

View File

@@ -15,6 +15,10 @@ agents:
runtime: claude runtime: claude
class: orchestrator class: orchestrator
persistent_persona: true persistent_persona: true
- name: enhancer
runtime: claude
class: enhancer
persistent_persona: true
- name: coder0 - name: coder0
runtime: pi runtime: pi
class: implementer class: implementer

View File

@@ -15,6 +15,10 @@ agents:
runtime: claude runtime: claude
class: orchestrator class: orchestrator
persistent_persona: true persistent_persona: true
- name: enhancer
runtime: claude
class: enhancer
persistent_persona: true
- name: researcher0 - name: researcher0
runtime: pi runtime: pi
class: researcher class: researcher

View File

@@ -0,0 +1,41 @@
# 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).

View File

@@ -20,6 +20,7 @@ import {
buildTmuxListSessionsCommand, buildTmuxListSessionsCommand,
classifySendResult, classifySendResult,
countOrchestrators, countOrchestrators,
countEnhancers,
detectDrift, detectDrift,
enableFleetUnits, enableFleetUnits,
FLEET_PROFILES, 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 () => { it('fleet init --write enforces the two-agent floor (1 orchestrator + >=1 enhancer)', async () => {
// The general profile must yield exactly one orchestrator; the guarantee is // The general profile must yield exactly one orchestrator AND at least one
// enforced (not just warned). We assert the happy path writes cleanly. // enhancer; the guarantee is enforced (not just warned). Happy path writes cleanly.
const home = await tempDir(); const home = await tempDir();
const program = new Command(); const program = new Command();
program.exitOverride(); program.exitOverride();
@@ -1098,7 +1099,9 @@ describe('fleet-polish bundle — boot-survival symmetry', () => {
]); ]);
const written = await readFile(join(home, 'fleet', 'roster.yaml'), 'utf8'); const written = await readFile(join(home, 'fleet', 'roster.yaml'), 'utf8');
const orchestrators = (written.match(/class:\s*orchestrator/g) ?? []).length; const orchestrators = (written.match(/class:\s*orchestrator/g) ?? []).length;
const enhancers = (written.match(/class:\s*enhancer/g) ?? []).length;
expect(orchestrators).toBe(1); expect(orchestrators).toBe(1);
expect(enhancers).toBeGreaterThanOrEqual(1);
} finally { } finally {
await rm(home, { recursive: true, force: true }); 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')); 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 === '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'); 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')); const roster = await loadFleetRoster(join(examplesDir, 'coding.yaml'));
expect(roster.agents.map((a) => a.name)).toEqual([ expect(roster.agents.map((a) => a.name)).toEqual([
'orchestrator', 'orchestrator',
'enhancer',
'coder0', 'coder0',
'coder1', 'coder1',
'reviewer', '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')); const roster = await loadFleetRoster(join(examplesDir, 'research.yaml'));
expect(roster.agents.map((a) => a.name)).toEqual([ expect(roster.agents.map((a) => a.name)).toEqual([
'orchestrator', 'orchestrator',
'enhancer',
'researcher0', 'researcher0',
'researcher1', 'researcher1',
'analyst', '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')); const roster = await loadFleetRoster(join(examplesDir, 'hybrid.yaml'));
expect(roster.agents.map((a) => a.name)).toEqual([ expect(roster.agents.map((a) => a.name)).toEqual([
'orchestrator', 'orchestrator',
'enhancer',
'coder0', 'coder0',
'researcher0', 'researcher0',
'reviewer', '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 () => { 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[]) { for (const preset of ['general', 'coding', 'research', 'hybrid'] as FleetProfile[]) {
const roster = await loadFleetRoster(join(examplesDir, `${preset}.yaml`)); 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) { for (const worker of workers) {
expect(worker.runtime).toBe('pi'); expect(worker.runtime).toBe('pi');
expect(worker.modelHint).toBe('openai-codex/gpt-5.5:high'); 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']); 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 () => { it('serializeRosterToYaml produces YAML that round-trips through loadFleetRoster', async () => {
const yaml = serializeRosterToYaml(baseRoster); const yaml = serializeRosterToYaml(baseRoster);
expect(typeof yaml).toBe('string'); expect(typeof yaml).toBe('string');

View File

@@ -881,11 +881,13 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
await mkdir(dirname(destination), { recursive: true }); await mkdir(dirname(destination), { recursive: true });
await writeFile(destination, content); await writeFile(destination, content);
// Guarantee R5: exactly one orchestrator for every profile except the // Guarantee the two-agent floor: exactly one orchestrator AND at least
// sanctioned no-orchestrator `minimal` preset. A mismatch means a // one enhancer for every profile except the sanctioned no-orchestrator
// corrupted/edited preset — fail hard rather than write a malformed fleet. // `minimal` preset. A mismatch means a corrupted/edited preset — fail hard
// rather than write a malformed fleet.
const written = await loadFleetRoster(destination); const written = await loadFleetRoster(destination);
const orchCount = countOrchestrators(written); const orchCount = countOrchestrators(written);
const enhancerCount = countEnhancers(written);
if (profile === 'minimal') { if (profile === 'minimal') {
console.log( console.log(
`Initialized ${profile} fleet: ${written.agents.length} agent(s) (no orchestrator). Next: mosaic fleet install`, `Initialized ${profile} fleet: ${written.agents.length} agent(s) (no orchestrator). Next: mosaic fleet install`,
@@ -895,10 +897,17 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
`Fleet init failed: the "${profile}" roster has ${orchCount} orchestrator agent(s), ` + `Fleet init failed: the "${profile}" roster has ${orchCount} orchestrator agent(s), ` +
`expected exactly 1 (R5). The preset may be corrupted — re-install the framework.`, `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.`,
);
} else { } else {
const workerCount = written.agents.length - 1; const workerCount = written.agents.length - 1 - enhancerCount;
console.log( console.log(
`Initialized ${profile} fleet: 1 orchestrator + ${workerCount} agent(s). Next: mosaic fleet install`, `Initialized ${profile} fleet: 1 orchestrator + ${enhancerCount} enhancer(s) + ` +
`${workerCount} worker(s). Next: mosaic fleet install`,
); );
} }
}); });
@@ -1945,6 +1954,15 @@ export function countOrchestrators(roster: FleetRoster): number {
return roster.agents.filter((a) => a.className === 'orchestrator').length; 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. */ /** Valid runtime identifiers for fleet agents. */
export const VALID_FLEET_RUNTIMES: readonly string[] = [ export const VALID_FLEET_RUNTIMES: readonly string[] = [
'pi', 'pi',
@@ -1987,6 +2005,15 @@ export function removeAgentFromRoster(roster: FleetRoster, name: string): FleetR
`Cannot remove agent "${name}": it is the sole orchestrator. Add another orchestrator first (R5).`, `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 { return {
...roster, ...roster,
agents: remaining, agents: remaining,