Compare commits
1 Commits
feat/a3a-a
...
feat/h4-pe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92e4ee189a |
@@ -27,49 +27,45 @@ id: software-delivery
|
|||||||
title: Software Delivery
|
title: Software Delivery
|
||||||
description: >-
|
description: >-
|
||||||
The engineering fleet that turns ratified objectives into shipped, reviewed,
|
The engineering fleet that turns ratified objectives into shipped, reviewed,
|
||||||
merged code. The lead (orchestrator) runs the supervisor loop and dispatches
|
merged code. The lead (planner — the orchestrator seat) plans phased FRs into a
|
||||||
ready work; it hands goal-decomposition to the planner, which plans phased FRs
|
depends_on DAG, decomposition splits them into one-PR-each cards, coders execute
|
||||||
into a depends_on DAG, decomposition splits them into one-PR-each cards, coders
|
to green CI, and review / security-review / site-tester / merge-gate guard the
|
||||||
execute to green CI, and review / security-review / site-tester / merge-gate
|
merge. This mirrors today's coding fleet.
|
||||||
guard the merge. This mirrors today's coding fleet.
|
# NOTE: the canonical lead seat is the "orchestrator". In the persona library the
|
||||||
# NOTE: the lead seat is the dedicated "orchestrator" — the always-on coordinator
|
# orchestrator IS the `planner` class (see roles/planner.md: "the planner role IS
|
||||||
# that runs the supervisor tick, dispatches ready work, and routes PRs to the
|
# the existing orchestrator class") — so the lead/floor reference `planner`, the
|
||||||
# merge-gate while holding only lean coordination state. The planner is now a
|
# only class that actually resolves to a role contract.
|
||||||
# distinct seat (heavy goal-decomposition context) that reports to the
|
lead: planner
|
||||||
# orchestrator. The two-agent floor is orchestrator + enhancer.
|
|
||||||
lead: orchestrator
|
|
||||||
floor:
|
floor:
|
||||||
- orchestrator
|
- planner
|
||||||
- enhancer
|
- enhancer
|
||||||
roster:
|
roster:
|
||||||
- class: orchestrator
|
|
||||||
- class: board
|
- class: board
|
||||||
reports_to: orchestrator
|
reports_to: planner
|
||||||
- class: planner
|
- class: planner
|
||||||
reports_to: orchestrator
|
|
||||||
- class: decomposition
|
- class: decomposition
|
||||||
reports_to: planner
|
reports_to: planner
|
||||||
- class: code
|
- class: code
|
||||||
reports_to: decomposition
|
reports_to: decomposition
|
||||||
multiplicity: 2
|
multiplicity: 2
|
||||||
- class: review
|
- class: review
|
||||||
reports_to: orchestrator
|
reports_to: planner
|
||||||
- class: security-review
|
- class: security-review
|
||||||
reports_to: review
|
reports_to: review
|
||||||
- class: site-tester
|
- class: site-tester
|
||||||
reports_to: review
|
reports_to: review
|
||||||
- class: documentation
|
- class: documentation
|
||||||
reports_to: orchestrator
|
reports_to: planner
|
||||||
- class: merge-gate
|
- class: merge-gate
|
||||||
reports_to: orchestrator
|
reports_to: planner
|
||||||
- class: rebase
|
- class: rebase
|
||||||
reports_to: merge-gate
|
reports_to: merge-gate
|
||||||
- class: operator
|
- class: operator
|
||||||
reports_to: orchestrator
|
reports_to: planner
|
||||||
- class: session-review
|
- class: session-review
|
||||||
reports_to: orchestrator
|
reports_to: planner
|
||||||
- class: enhancer
|
- class: enhancer
|
||||||
reports_to: orchestrator
|
reports_to: planner
|
||||||
notes: >-
|
notes: >-
|
||||||
Two-agent floor (orchestrator + enhancer) is always staffed; every other seat is
|
Two-agent floor (orchestrator/planner + enhancer) is always staffed; every other
|
||||||
added on demand.
|
seat is added on demand.
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ their intro so tooling can group them.
|
|||||||
|
|
||||||
| Persona | Purpose |
|
| Persona | Purpose |
|
||||||
| --------------- | ------------------------------------------------------------------------------ |
|
| --------------- | ------------------------------------------------------------------------------ |
|
||||||
| orchestrator | Always-on coordinator — runs the supervisor loop, dispatches ready work |
|
|
||||||
| board | Multi-lens deliberation panel; owns the mission's direction, not its execution |
|
| board | Multi-lens deliberation panel; owns the mission's direction, not its execution |
|
||||||
| planner | Turns ratified objectives into a phased FR plan wired into a `depends_on` DAG |
|
| planner | Turns ratified objectives into a phased FR plan wired into a `depends_on` DAG |
|
||||||
| decomposition | Splits FRs into one-PR-each cards wired with `depends_on` edges |
|
| decomposition | Splits FRs into one-PR-each cards wired with `depends_on` edges |
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
# Orchestrator — fleet role definition
|
|
||||||
|
|
||||||
The **orchestrator** is one half of the fleet's two-agent floor: every fleet runs,
|
|
||||||
at minimum, an **orchestrator** and an **enhancer**. The orchestrator is the
|
|
||||||
fleet's **always-on coordinator and dispatcher** (`class: orchestrator`,
|
|
||||||
`persistent_persona: true`) — it owns fleet _movement_, not the work itself.
|
|
||||||
|
|
||||||
It is a **core, always-on** agent, not an ephemeral per-lane worker.
|
|
||||||
|
|
||||||
## Mandate
|
|
||||||
|
|
||||||
1. **Run the supervisor tick** — perform the readiness scan each loop and keep the
|
|
||||||
two-agent floor (orchestrator + enhancer) healthy, restoring it the moment it
|
|
||||||
drops below the floor.
|
|
||||||
2. **Dispatch ready work** — pick up cards whose `depends_on` edges are satisfied
|
|
||||||
and assign them via the backlog/claim, so no idle agent sits while ready work
|
|
||||||
exists.
|
|
||||||
3. **Delegate decomposition, don't do it** — hand goal-decomposition work to the
|
|
||||||
**planner**, which it coordinates; the orchestrator tracks the resulting plan
|
|
||||||
but does not author the DAG itself.
|
|
||||||
4. **Route PRs to the merge-gate** — push reviewed, ready-to-land PRs at the
|
|
||||||
**merge-gate** (the only merge path); it never approves or merges itself.
|
|
||||||
5. **Interface with the operator/user** — be the fleet's coordination surface,
|
|
||||||
relaying status and accepting direction, while holding only coordination state.
|
|
||||||
6. **Keep the loop turning** — re-dispatch on completion or failure so the fleet
|
|
||||||
keeps moving rather than stalling.
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
- **Does NOT decompose goals into the DAG/cards** — that is the **planner**'s lane,
|
|
||||||
which the orchestrator dispatches to.
|
|
||||||
- **Does NOT write product/source code** (coders), **review** (review), or
|
|
||||||
**approve merges itself** (merge-gate).
|
|
||||||
- **Does NOT carry deep per-task context** — it delegates and tracks, keeping its
|
|
||||||
own context lean so the coordination loop stays fast.
|
|
||||||
|
|
||||||
The orchestrator moves work; it never holds the heavy planning or execution
|
|
||||||
context that the seats it dispatches to carry.
|
|
||||||
|
|
||||||
## Persona
|
|
||||||
|
|
||||||
A lean, decisive coordinator. It thinks in readiness and throughput, dispatches the
|
|
||||||
next ready card the instant a dependency clears, and never lets an idle agent sit
|
|
||||||
while ready work exists — keeping its own context minimal so the loop never slows.
|
|
||||||
|
|
||||||
> Doctrine: `docs/fleet/north-star.md` (two-agent floor + role library).
|
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
The **planner** turns ratified objectives into an executable **plan** — phased
|
The **planner** turns ratified objectives into an executable **plan** — phased
|
||||||
functional requirements (FRs) wired into a `depends_on` DAG.
|
functional requirements (FRs) wired into a `depends_on` DAG.
|
||||||
|
|
||||||
> **Reports to the orchestrator.** The planner is the goal-decomposition seat that
|
> **Alias:** the planner role IS the existing **orchestrator** class. The
|
||||||
> the **orchestrator** dispatches planning work to; it carries the heavy
|
> orchestrator _plays_ planner; this file documents the planning contract, it does
|
||||||
> goal-decomposition context, while the orchestrator holds only the lean
|
> **not** introduce a competing class. The two-agent floor (orchestrator +
|
||||||
> coordination state. The two-agent floor is **orchestrator + enhancer** — the
|
> enhancer) is preserved — do not split planner into a separate persistent agent
|
||||||
> planner is added on demand, not part of the floor.
|
> that would break it.
|
||||||
|
|
||||||
It is a **front-office** role.
|
It is a **front-office** role.
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ It is a **front-office** role.
|
|||||||
between FRs so downstream decomposition can parallelize safely.
|
between FRs so downstream decomposition can parallelize safely.
|
||||||
3. **Emit a plan, not tasks** — the planner's output is the phased FR/DAG
|
3. **Emit a plan, not tasks** — the planner's output is the phased FR/DAG
|
||||||
document. Splitting FRs into one-PR-each cards is the **decomposition** role's job.
|
document. Splitting FRs into one-PR-each cards is the **decomposition** role's job.
|
||||||
4. **Re-plan on failure** — when execution diverges, the planner re-sequences the
|
4. **Re-plan on failure** — when execution diverges, the planner (orchestrator)
|
||||||
DAG rather than letting agents improvise.
|
re-sequences the DAG rather than letting agents improvise.
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
@@ -35,7 +35,6 @@ merge path.
|
|||||||
## Persona
|
## Persona
|
||||||
|
|
||||||
The architect of the mission's shape. It thinks in phases and dependencies, hands
|
The architect of the mission's shape. It thinks in phases and dependencies, hands
|
||||||
a clean DAG to decomposition, and reports its plan back to the orchestrator that
|
a clean DAG to decomposition, and keeps the orchestrator/enhancer floor intact.
|
||||||
dispatched it.
|
|
||||||
|
|
||||||
> Doctrine: `docs/fleet/north-star.md` (two-agent floor + role library).
|
> Doctrine: `docs/fleet/north-star.md` (two-agent floor + role library).
|
||||||
|
|||||||
@@ -46,12 +46,10 @@ describe('listPersonaClasses (real role library)', () => {
|
|||||||
|
|
||||||
it('covers marker-less engineering personas via filename + LIBRARY index', async () => {
|
it('covers marker-less engineering personas via filename + LIBRARY index', async () => {
|
||||||
const classes = await listPersonaClasses(rolesDir);
|
const classes = await listPersonaClasses(rolesDir);
|
||||||
// planner/decomposition have a role file but no inline marker — they resolve
|
// planner/decomposition have a role file but no inline marker (planner aliases
|
||||||
// from the filename + LIBRARY.md row.
|
// the orchestrator class) — they resolve from the filename + LIBRARY.md row.
|
||||||
expect(classes.has('planner')).toBe(true);
|
expect(classes.has('planner')).toBe(true);
|
||||||
expect(classes.has('decomposition')).toBe(true);
|
expect(classes.has('decomposition')).toBe(true);
|
||||||
// The dedicated orchestrator persona resolves (inline marker + filename + row).
|
|
||||||
expect(classes.has('orchestrator')).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an empty set for a missing roles dir (graceful)', async () => {
|
it('returns an empty set for a missing roles dir (graceful)', async () => {
|
||||||
@@ -77,17 +75,11 @@ describe('baseline profiles (real library)', () => {
|
|||||||
|
|
||||||
it('software-delivery has the expected lead, floor, and roster shape', async () => {
|
it('software-delivery has the expected lead, floor, and roster shape', async () => {
|
||||||
const profile = await loadProfile('software-delivery', realLib);
|
const profile = await loadProfile('software-delivery', realLib);
|
||||||
expect(profile.lead).toBe('orchestrator');
|
expect(profile.lead).toBe('planner');
|
||||||
expect(profile.floor).toEqual(['orchestrator', 'enhancer']);
|
expect(profile.floor).toEqual(['planner', 'enhancer']);
|
||||||
const code = profile.roster.find((r) => r.class === 'code');
|
const code = profile.roster.find((r) => r.class === 'code');
|
||||||
expect(code?.multiplicity).toBe(2);
|
expect(code?.multiplicity).toBe(2);
|
||||||
expect(code?.reportsTo).toBe('decomposition');
|
expect(code?.reportsTo).toBe('decomposition');
|
||||||
// The dedicated orchestrator is the lead seat (no reports_to); the planner is
|
|
||||||
// now a distinct seat that reports to it.
|
|
||||||
const orchestrator = profile.roster.find((r) => r.class === 'orchestrator');
|
|
||||||
expect(orchestrator?.reportsTo).toBeUndefined();
|
|
||||||
const planner = profile.roster.find((r) => r.class === 'planner');
|
|
||||||
expect(planner?.reportsTo).toBe('orchestrator');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loadProfile throws on an unknown id', async () => {
|
it('loadProfile throws on an unknown id', async () => {
|
||||||
|
|||||||
@@ -246,8 +246,6 @@ describe('fleet roster parsing', () => {
|
|||||||
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
|
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
|
||||||
[
|
[
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
// Reflects the roster's non-default `class: implementer` (A3a).
|
|
||||||
'MOSAIC_AGENT_CLASS=implementer',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
'MOSAIC_AGENT_RUNTIME=codex',
|
||||||
'MOSAIC_AGENT_MODEL=',
|
'MOSAIC_AGENT_MODEL=',
|
||||||
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
|
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
|
||||||
@@ -257,40 +255,6 @@ describe('fleet roster parsing', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits MOSAIC_AGENT_CLASS=worker for an agent that declares no class', async () => {
|
|
||||||
cleanup = await tempDir();
|
|
||||||
const rosterPath = join(cleanup, 'roster.json');
|
|
||||||
await writeFile(
|
|
||||||
rosterPath,
|
|
||||||
JSON.stringify({
|
|
||||||
version: 1,
|
|
||||||
transport: 'tmux',
|
|
||||||
agents: [{ name: 'coder0', runtime: 'codex' }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const roster = await loadFleetRoster(rosterPath);
|
|
||||||
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toContain(
|
|
||||||
'MOSAIC_AGENT_CLASS=worker\n',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shell-escapes MOSAIC_AGENT_CLASS so a launcher reads it verbatim', async () => {
|
|
||||||
cleanup = await tempDir();
|
|
||||||
const rosterPath = join(cleanup, 'roster.json');
|
|
||||||
await writeFile(
|
|
||||||
rosterPath,
|
|
||||||
JSON.stringify({
|
|
||||||
version: 1,
|
|
||||||
transport: 'tmux',
|
|
||||||
agents: [{ name: 'coder0', runtime: 'codex', class: 'orchestrator' }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const roster = await loadFleetRoster(rosterPath);
|
|
||||||
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toContain(
|
|
||||||
'MOSAIC_AGENT_CLASS=orchestrator\n',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => {
|
it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => {
|
||||||
const generated = [
|
const generated = [
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
@@ -322,28 +286,6 @@ describe('fleet roster parsing', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates (does not duplicate) MOSAIC_AGENT_CLASS on re-launch', () => {
|
|
||||||
const generated = [
|
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
|
||||||
'MOSAIC_AGENT_CLASS=orchestrator',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
const existing = [
|
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
|
||||||
'MOSAIC_AGENT_CLASS=worker',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const merged = mergeAgentEnv(generated, existing);
|
|
||||||
// mergeAgentEnv keys by VAR name, so the regenerated CLASS wins and there is
|
|
||||||
// exactly one MOSAIC_AGENT_CLASS line (no stale worker value left behind).
|
|
||||||
expect(merged).toContain('MOSAIC_AGENT_CLASS=orchestrator');
|
|
||||||
expect(merged).not.toContain('MOSAIC_AGENT_CLASS=worker');
|
|
||||||
expect(merged.match(/^MOSAIC_AGENT_CLASS=/gm)).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
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');
|
||||||
|
|||||||
@@ -490,9 +490,6 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string
|
|||||||
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
|
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
|
||||||
return [
|
return [
|
||||||
`MOSAIC_AGENT_NAME=${shellEnvValue(agent.name)}`,
|
`MOSAIC_AGENT_NAME=${shellEnvValue(agent.name)}`,
|
||||||
// Per-agent class → start-agent-session.sh / launcher reads this to inject the
|
|
||||||
// matching persona contract for the agent's class (default `worker`).
|
|
||||||
`MOSAIC_AGENT_CLASS=${shellEnvValue(agent.className)}`,
|
|
||||||
`MOSAIC_AGENT_RUNTIME=${shellEnvValue(agent.runtime)}`,
|
`MOSAIC_AGENT_RUNTIME=${shellEnvValue(agent.runtime)}`,
|
||||||
// Per-agent model hint → start-agent-session.sh appends `--model <hint>` to
|
// Per-agent model hint → start-agent-session.sh appends `--model <hint>` to
|
||||||
// the `mosaic yolo` launch so workers run on the roster's model (e.g. pi on
|
// the `mosaic yolo` launch so workers run on the roster's model (e.g. pi on
|
||||||
|
|||||||
Reference in New Issue
Block a user