Found by rehearsing the full install on a greenfield Debian 13 VM
(mosaic-sbx-dev) rather than on a host that already had a working Mosaic
tree. Each one is invisible on a developer machine and fatal on a new host.
1. Required system settings layer. The framework ships runtime/<harness>/
for claude, codex, opencode and pi but a settings.json only for claude,
so requiring the file made every pi, codex and opencode seat refuse to
compose. The system layer is now optional; what must exist is the
harness runtime directory, which is the thing that actually proves the
framework is installed and carries that harness.
2. Required mcpServers in canonical Claude settings. The shipped
settings.json has no such key, so `fleet agent new` refused to scaffold
any Claude seat. Absent now means the same as empty. A present but
wrong-typed value is still an error.
3. Never-enrolled hosts were told their auth directory "must be a real,
non-symlink directory", which reads as a tampering report when the real
situation is that nobody has logged in yet. Absent and wrong-shaped are
now separate messages, and the absent one names `mosaic auth enroll`.
4. A fleet seat whose host had no system SOUL.md reached checkSoul(),
which spawns the interactive `mosaic wizard` with inherited stdio. On a
detached tmux seat that parks the pane on a menu with nobody at it: the
session is live, the systemd unit reports fine, and no agent ever
starts. A seat's identity is its own SOUL.md, written by `fleet agent
new`, so the fleet path checks that and fails loudly instead.
Each fix has a regression test verified red against the unfixed source.
The launch.spec.ts seat fixtures gained a SOUL.md they always should have
had -- without it those tests were satisfied by whatever SOUL.md the
developer's real ~/.config/mosaic happened to contain.
Full suite before and after: the same 5 pre-existing failures in
mutator-gate.acceptance.spec.ts and install-ordering-guard.spec.ts,
1585 -> 1591 passing. typecheck and eslint clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
The roster lane and the harness-homes lane did not touch. start-agent-session.sh
ran `mosaic yolo "$RUNTIME"` with HOME set to the operator's home, so every fleet
seat on a host shared the operator's harness home and, for Claude, the operator's
own ~/.claude credentials. Nothing in framework/ called `mosaic fleet launch` at
all, which meant ~/.mosaic was a directory nothing read.
The pane now runs `mosaic fleet launch "$AGENT_NAME"` when a scaffolded seat
exists at $PANE_HOME/.mosaic/fleet/agents/<name>/profile.json, and the historical
command otherwise. Detection uses $PANE_HOME/.mosaic rather than MOSAIC_DATA_HOME
because the pane environment is cleared with env -i; the composition resolves the
same root from HOME, so the two cannot disagree.
Additive by construction: a host with no scaffolded seats launches exactly as
before, so this can land ahead of any seat being enrolled.
- fleet launch gains --dangerous, threaded to launchFleetRuntime. Without it a
seat launched from the roster would drop the permissions footing `mosaic yolo`
gave it and prompt at a pane with nobody at it. The roster launcher asks for it
explicitly so it stays visible in the process table instead of becoming a
profile default.
- A caller's --model replaces the profile's instead of being appended after it.
The roster carries a model per seat and is the surface operators edit; emitting
both flags would leave the choice to each harness's argument parser.
- Claude workdir trust is written into the seat's .claude.json when the pane will
run in a seat home. It previously always went to the operator's ~/.claude.json,
which would leave the seat prompting on its first turn.
Covers Jason's scope amendment for web1: without this seam, "multiple
authentication accounts and agent pegging to auth" cannot be demonstrated on a
roster-managed seat.
Claude Code saves credentials by writing a sibling temp file and rename()-ing
it over the target. rename(2) replaces a symlink rather than following it, so
the managed link W-F1/W-F2 planted at <seat>/.claude/.credentials.json is
destroyed by the first token refresh and the seat silently forks its
credentials. The in-place fallback arm opens with O_NOFOLLOW and would refuse
the link anyway. Evidence, quoting the 2.1.232 binary:
docs/reports/harness/claude-credential-write-path-2026-08-14.md (jarvis-brain).
CLAUDE_SECURESTORAGE_CONFIG_DIR resolves the credential directory
independently of CLAUDE_CONFIG_DIR, so the temp file and the rename both land
inside the bundle. That is the property the design wanted -- share the
credential, never the transcripts -- with no symlink and no privileges.
- new fleet/credential-sharing.ts owns the harness -> credential-file and
harness -> credential-directory-variable maps, so scaffold and launch cannot
disagree about the mechanism. It also removes the duplicate credential-file
name table the two already carried.
- launch composes CLAUDE_SECURESTORAGE_CONFIG_DIR from the resolved bundle
directory and plans no credential link for Claude. The value is always the
absolute bundle path: Claude reads an empty value as ~/.claude, which is the
operator's own account.
- scaffold stops emitting the credential symlink and its manifest entry for
Claude, and tolerates one left by an earlier scaffold rather than reporting
it as a foreign file or rewriting it.
- FIRST_AUTH_REFUSAL still fires when a real file occupies the seat path.
- Harnesses absent from the map (pi, codex, opencode) keep managed links; the
containment specs now exercise them on pi.
Answers promotion gate #1 negatively for the frozen mechanism and positively
for the replacement. E3.3 (two seats refreshing one bundle at once) is still
open.
AMD1213-C: repair stale array consumer, fail closed on foreign link provenance, validate manifests before mutation, and exercise the fleet MCP preflight call path.
Claude Code writes installed_plugins.json and other metadata files into
the seat's plugins directory during a session, so refusing every real
entry made composition fail on each seat's second launch. Only a real
directory is an unmanaged entry the pruner would orphan; plain files are
harness state and pass through untouched. Found by the in-box hour-gate
relaunch of the probe seat.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
The installed ~/.config/mosaic home flattens the repo's
packages/mosaic/framework/ prefix: the real file is
<home>/runtime/<harness>/settings.json, exactly as launch.ts already
resolves it everywhere. The fleet launch composition leaked the repo
layout (framework/runtime/...) into the system layer path, so a real
installed home failed with COMPOSITION_FAILED while the temp-fixture
specs (which mirrored the same wrong prefix) stayed green. Found by the
in-box hour-gate dry-run against the installed mos-dev-stage home.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
Integration adjudication (fred, W-F1): the general arrays-replace merge rule
conflicts with the gap-7 base/overlay split — base and lease overlay share
the PreToolUse and Stop events, so replace semantics would silently drop the
base QA hooks from every gated seat. Ruling: hook event arrays directly
under the top-level hooks key concatenate (base first); all other arrays
keep replace semantics; null tombstones still delete an event.
- mutator-gate acceptance now asserts lease wiring against the COMPOSED
gated settings (base + lease-overlay via the launcher's own merge),
matching the post-split contract.
- fleet subcommand canary gains the intended new 'agent' surface from T3.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
Integration reconciliation of T2/T3 seams on feat/wf-fleet-mvp:
- fleet launch now resolves the user root through defaultFleetDataHome()
(MOSAIC_DATA_HOME), the same seam fleet agent new uses, instead of a
divergent MOSAIC_USER_HOME variable.
- Launching an unscaffolded name raises AGENT_NOT_SCAFFOLDED with the
actionable message pointing at 'mosaic fleet agent new <name>' (acceptance
carried over from the T3 card after the roster-v2 reconciliation moved it
onto the launch path).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh