A3b — inject resolved persona contract at launch by class
At agent launch, composeContract now reads MOSAIC_AGENT_CLASS (exported into the pane env by the companion A3a goal) and injects the agent's resolved persona role contract into the system prompt, so its identity (mandate + boundaries) is resident from the first turn.
What it does
New readPersonaContractBlock(mosaicHome, klass) in packages/mosaic/src/fleet/persona-contract.ts (mirrors comms-onboarding.ts structure; standalone, no heavy fleet.ts import).
Injected in composeContractbefore the fleet comms block: identity first, then how-to-reach-peers. Guarded exactly like the comms block (only pushed when non-empty).
Override-aware (AC-NS-7 launch proof)
Resolution goes through the persona resolver, so a user-customized persona in fleet/roles.local/wins over the baseline fleet/roles/ of the same class. A dedicated test asserts the override body is injected and the baseline is NOT — the launch-time proof that a customized persona actually reaches the model (not just mosaic fleet persona show).
Tolerant / no-op
Empty/missing/unknown class or missing role file → returns '' so the launcher no-ops silently. Never throws during launch (mirrors readFleetCommsBlock).
Sync resolver twin
composeContract is synchronous and cannot await the async resolvePersona. Added resolvePersonaSync / extractClassesFromDirSync in fleet-personas.ts with identical extraction semantics, sharing a pure accumulateEntry helper so async and sync paths never drift.
Tests
persona-contract.spec.ts (7): baseline inject, override wins, override-only custom persona, no-op for undefined/empty/whitespace/unknown class, no-throw when no roles dirs exist.
compose-contract.spec.ts (+5): baseline inject at launch, override wins at launch (AC-NS-7), no-inject when class unset, no-throw on unknown class, persona placed before fleet comms. Env saved/restored in afterEach.
## A3b — inject resolved persona contract at launch by class
At agent launch, `composeContract` now reads **`MOSAIC_AGENT_CLASS`** (exported into the pane env by the companion A3a goal) and injects the agent's **resolved persona role contract** into the system prompt, so its identity (mandate + boundaries) is resident from the first turn.
### What it does
- New `readPersonaContractBlock(mosaicHome, klass)` in `packages/mosaic/src/fleet/persona-contract.ts` (mirrors `comms-onboarding.ts` structure; standalone, no heavy `fleet.ts` import).
- Injected in `composeContract` **before** the fleet comms block: identity first, then how-to-reach-peers. Guarded exactly like the comms block (only pushed when non-empty).
### Override-aware (AC-NS-7 launch proof)
- Resolution goes through the persona resolver, so a user-customized persona in `fleet/roles.local/` **wins** over the baseline `fleet/roles/` of the same class. A dedicated test asserts the override body is injected and the baseline is NOT — the launch-time proof that a customized persona actually reaches the model (not just `mosaic fleet persona show`).
### Tolerant / no-op
- Empty/missing/unknown class or missing role file → returns `''` so the launcher no-ops silently. Never throws during launch (mirrors `readFleetCommsBlock`).
### Sync resolver twin
- `composeContract` is synchronous and cannot `await` the async `resolvePersona`. Added `resolvePersonaSync` / `extractClassesFromDirSync` in `fleet-personas.ts` with identical extraction semantics, sharing a pure `accumulateEntry` helper so async and sync paths never drift.
### Tests
- `persona-contract.spec.ts` (7): baseline inject, **override wins**, override-only custom persona, no-op for undefined/empty/whitespace/unknown class, no-throw when no roles dirs exist.
- `compose-contract.spec.ts` (+5): baseline inject at launch, **override wins at launch (AC-NS-7)**, no-inject when class unset, no-throw on unknown class, persona placed before fleet comms. Env saved/restored in `afterEach`.
### Gates
- `vitest run`: 606 passed (43 files). `typecheck`: clean. `lint`: clean. `prettier --check`: clean.
Reviewable — do NOT merge. Does not touch `generateAgentEnv`/`fleet.ts` (A3a's scope); only reads `MOSAIC_AGENT_CLASS` from env.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
At agent launch, composeContract reads MOSAIC_AGENT_CLASS (exported into the
pane env by the companion A3a goal) and injects the agent's resolved persona
role contract into the system prompt, so its identity (mandate + boundaries)
is resident from the first turn.
Override-aware: resolution goes through the persona resolver, so a customized
persona in fleet/roles.local/ wins over the baseline fleet/roles/ of the same
class — the launch-time proof of AC-NS-7. Tolerant: any miss (unset/empty/
unknown class, missing file) no-ops silently and never throws during launch,
mirroring readFleetCommsBlock. Persona is injected BEFORE the fleet comms
block (identity first, then how-to-reach-peers).
Adds a synchronous resolvePersonaSync / extractClassesFromDirSync twin in
fleet-personas.ts (composeContract is sync and cannot await), sharing the same
extraction semantics via a pure accumulateEntry helper (no drift).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A3b — inject resolved persona contract at launch by class
At agent launch,
composeContractnow readsMOSAIC_AGENT_CLASS(exported into the pane env by the companion A3a goal) and injects the agent's resolved persona role contract into the system prompt, so its identity (mandate + boundaries) is resident from the first turn.What it does
readPersonaContractBlock(mosaicHome, klass)inpackages/mosaic/src/fleet/persona-contract.ts(mirrorscomms-onboarding.tsstructure; standalone, no heavyfleet.tsimport).composeContractbefore the fleet comms block: identity first, then how-to-reach-peers. Guarded exactly like the comms block (only pushed when non-empty).Override-aware (AC-NS-7 launch proof)
fleet/roles.local/wins over the baselinefleet/roles/of the same class. A dedicated test asserts the override body is injected and the baseline is NOT — the launch-time proof that a customized persona actually reaches the model (not justmosaic fleet persona show).Tolerant / no-op
''so the launcher no-ops silently. Never throws during launch (mirrorsreadFleetCommsBlock).Sync resolver twin
composeContractis synchronous and cannotawaitthe asyncresolvePersona. AddedresolvePersonaSync/extractClassesFromDirSyncinfleet-personas.tswith identical extraction semantics, sharing a pureaccumulateEntryhelper so async and sync paths never drift.Tests
persona-contract.spec.ts(7): baseline inject, override wins, override-only custom persona, no-op for undefined/empty/whitespace/unknown class, no-throw when no roles dirs exist.compose-contract.spec.ts(+5): baseline inject at launch, override wins at launch (AC-NS-7), no-inject when class unset, no-throw on unknown class, persona placed before fleet comms. Env saved/restored inafterEach.Gates
vitest run: 606 passed (43 files).typecheck: clean.lint: clean.prettier --check: clean.Reviewable — do NOT merge. Does not touch
generateAgentEnv/fleet.ts(A3a's scope); only readsMOSAIC_AGENT_CLASSfrom env.🤖 Generated with Claude Code