From 5e73481f0a3a38c0046bf46f245221711cd0c6fa Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 22 Jun 2026 03:03:11 -0500 Subject: [PATCH] feat(fleet): enhancer role + two-agent floor (orchestrator + enhancer) (#614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01EsgTQzV5YUGk1JtCLP4B83 --- docs/TASKS.md | 4 + docs/scratchpads/fleet-enhancer-floor.md | 26 +++++++ .../framework/fleet/examples/coding.yaml | 4 + .../framework/fleet/examples/general.yaml | 4 + .../framework/fleet/examples/hybrid.yaml | 4 + .../framework/fleet/examples/research.yaml | 4 + .../mosaic/framework/fleet/roles/enhancer.md | 41 ++++++++++ packages/mosaic/src/commands/fleet.spec.ts | 74 ++++++++++++++++--- packages/mosaic/src/commands/fleet.ts | 37 ++++++++-- 9 files changed, 184 insertions(+), 14 deletions(-) create mode 100644 docs/scratchpads/fleet-enhancer-floor.md create mode 100644 packages/mosaic/framework/fleet/roles/enhancer.md diff --git a/docs/TASKS.md b/docs/TASKS.md index d2a935c..2f8ed74 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -62,3 +62,7 @@ Active workstream is **W1 — Federation v1**. Workers should: ## 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. diff --git a/docs/scratchpads/fleet-enhancer-floor.md b/docs/scratchpads/fleet-enhancer-floor.md new file mode 100644 index 0000000..b800afd --- /dev/null +++ b/docs/scratchpads/fleet-enhancer-floor.md @@ -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. diff --git a/packages/mosaic/framework/fleet/examples/coding.yaml b/packages/mosaic/framework/fleet/examples/coding.yaml index 878787b..006dc3f 100644 --- a/packages/mosaic/framework/fleet/examples/coding.yaml +++ b/packages/mosaic/framework/fleet/examples/coding.yaml @@ -15,6 +15,10 @@ agents: runtime: claude class: orchestrator persistent_persona: true + - name: enhancer + runtime: claude + class: enhancer + persistent_persona: true - name: coder0 runtime: pi class: implementer diff --git a/packages/mosaic/framework/fleet/examples/general.yaml b/packages/mosaic/framework/fleet/examples/general.yaml index 621fc01..91d2ab6 100644 --- a/packages/mosaic/framework/fleet/examples/general.yaml +++ b/packages/mosaic/framework/fleet/examples/general.yaml @@ -15,6 +15,10 @@ agents: runtime: claude class: orchestrator persistent_persona: true + - name: enhancer + runtime: claude + class: enhancer + persistent_persona: true - name: generalist runtime: pi class: worker diff --git a/packages/mosaic/framework/fleet/examples/hybrid.yaml b/packages/mosaic/framework/fleet/examples/hybrid.yaml index e69e225..0e5a1e0 100644 --- a/packages/mosaic/framework/fleet/examples/hybrid.yaml +++ b/packages/mosaic/framework/fleet/examples/hybrid.yaml @@ -15,6 +15,10 @@ agents: runtime: claude class: orchestrator persistent_persona: true + - name: enhancer + runtime: claude + class: enhancer + persistent_persona: true - name: coder0 runtime: pi class: implementer diff --git a/packages/mosaic/framework/fleet/examples/research.yaml b/packages/mosaic/framework/fleet/examples/research.yaml index 9eaf6d8..ee9bdea 100644 --- a/packages/mosaic/framework/fleet/examples/research.yaml +++ b/packages/mosaic/framework/fleet/examples/research.yaml @@ -15,6 +15,10 @@ agents: runtime: claude class: orchestrator persistent_persona: true + - name: enhancer + runtime: claude + class: enhancer + persistent_persona: true - name: researcher0 runtime: pi class: researcher diff --git a/packages/mosaic/framework/fleet/roles/enhancer.md b/packages/mosaic/framework/fleet/roles/enhancer.md new file mode 100644 index 0000000..38c0962 --- /dev/null +++ b/packages/mosaic/framework/fleet/roles/enhancer.md @@ -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). diff --git a/packages/mosaic/src/commands/fleet.spec.ts b/packages/mosaic/src/commands/fleet.spec.ts index 9714c77..66aac40 100644 --- a/packages/mosaic/src/commands/fleet.spec.ts +++ b/packages/mosaic/src/commands/fleet.spec.ts @@ -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'); diff --git a/packages/mosaic/src/commands/fleet.ts b/packages/mosaic/src/commands/fleet.ts index cbba0cb..20ae4b3 100644 --- a/packages/mosaic/src/commands/fleet.ts +++ b/packages/mosaic/src/commands/fleet.ts @@ -881,11 +881,13 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps = await mkdir(dirname(destination), { recursive: true }); await writeFile(destination, content); - // Guarantee R5: exactly one orchestrator 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. + // 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. const written = await loadFleetRoster(destination); const orchCount = countOrchestrators(written); + const enhancerCount = countEnhancers(written); if (profile === 'minimal') { console.log( `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), ` + `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 { - const workerCount = written.agents.length - 1; + const workerCount = written.agents.length - 1 - enhancerCount; 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; } +/** + * 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', @@ -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).`, ); } + // 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,