feat(fleet): HARNESS-HOMES fleet MVP — profile-driven seat composition and launch (#1209) #1213

Open
mos-dt-0 wants to merge 11 commits from mos-dt-0/stack:feat/wf-fleet-mvp into next
Collaborator

Closes #1209 — HARNESS-HOMES fleet MVP: profile-driven seat composition and launch (~/.config/mosaic system root + ~/.mosaic user data root).

Base: next @ 216cd722 (agreed base per Jason's REALIGN-MVP GO). Integration of the T1/T2/T3 doer lanes plus fred's adjudications and two in-box hour-gate fixes. Smoke-tested end-to-end in the mos-dev-stage distrobox on sb-it-1-dt.

What this delivers

  • fleet agent new <name> scaffolds a seat: profile.json (schema 1), SOUL.md, overlay.json, .claude/ seat home with credential symlink into the bundle store.
  • fleet launch <name> [passthrough...] composes the seat at launch: three-layer settings merge (installed system home ← .mosaic user config ← agent overlay.json), managed per-plugin/skill symlink install roots, credential-bundle symlink wiring with A3 lstat validation, settings.generated.json snapshot for drift detection, then exec of the harness with --model/skills/passthrough argv.
  • Error taxonomy: SCHEMA_TOO_NEW | PROFILE_INVALID | AGENT_NOT_SCAFFOLDED | COMPOSITION_FAILED | FIRST_AUTH_REFUSAL — every refusal is a named, tested state.
  • Adjudicated merge semantics (W-F1): hook event arrays directly under top-level hooks concatenate (base first); all other arrays replace; null tombstone deletes a key.
  • profile.json is the sole seat SSOT (roster-v2 reconciliation); MOSAIC_AGENT_NAME injected mechanically from the profile.

Verification

  • Focused suite: 22/22 green (fleet-launch-command.spec.ts), tsc clean.
  • In-box (mos-dev-stage, installed home): dry-runs for two migrated prototype seats RC=0 with correct layer paths; fleet agent new smoketest RC=0; real launches reach an authenticated Claude Code session (blocked past auth only by the account weekly quota — external).
  • Independent exact-byte gate (scrappy): probe seat settings.generated.json (5991 B) recomposed byte-for-byte from the three layers, SHA-256 e08c1b5705ccf3b2fa7a4ba0296f5cffa3c58f80aaa6900a125eafbe7cd4f79d — PASS. Gate covers composition output only.

Fixes made during the hour-gate (in this branch)

  • c16256d4 — compose the system layer from the installed flattened home layout (<home>/runtime/<harness>/settings.json); the repo-tree framework/ prefix does not exist in an installed home.
  • 5e154310 — the managed-install-root pruner tolerates harness metadata files (e.g. installed_plugins.json) and refuses only real directories; previously every seat failed composition on its second launch because Claude Code writes metadata beside the managed links.

Documented findings (not fixed here — follow-on issues)

  1. Scaffold gap: fleet agent new does not seed the sequential-thinking MCP config the launcher preflight hard-requires; a fresh seat fails preflight until mosaic-ensure-sequential-thinking is run. Scaffold should seed it.
  2. Headless defect: an interactive init prompt (keep/import/overwrite) fires mid-launch on every launch; blocks headless seats (worked around with </dev/null).
  3. Snapshot-vs-live drift (by design, documented): post-compose, the launcher/init flow writes model: "opus[1m]" into the live seat settings.json; settings.generated.json preserves composed bytes, so drift detection works as intended. Gate the snapshot, not live state.
  4. W-F5 live evidence — OAuth refresh breaks the bundle chain: Claude Code refreshes tokens via rename-replace, converting the seat's credential symlink into a real file and stranding the shared bundle with a rotated-out (dead) refresh token; every other seat on that bundle then fails auth, and FIRST_AUTH_REFUSAL correctly refuses to clobber the seat file on next launch. Recovery is manual (promote fresh token to bundle, relink). This elevates the Phase-2 W-F5 auth+refresh hard gate: watchdog/promotion/relink + concurrent-refresh handling are mandatory follow-ons.
  5. Installed-tool leakage (pre-existing, not this branch): mosaic-ensure-sequential-thinking writes ~/.claude/settings.json in the invoking user's home even when CLAUDE_CONFIG_DIR points at a seat.
  6. Prototype migration surface: pre-schema seats needed accountbundle profile rename and whole-store plugin-symlink → real-dir migration; both refusals fired as designed with actionable messages.

Known-failing tests on base 216cd722 (pre-existing, unchanged by this branch)

  • 4 failures in src/mutator-gate/mutator-gate.acceptance.spec.ts ('Claude PreCompact' revoke, 'Claude SessionStart(compact)' revoke, promote-lease-lost-ACK D2-v5 backstop, runtime adapters consult broker).
  • guardClaudeSettingsWiring > defaults to the real leaseEnforcementActivatable() load-dependent flake (#1207 2s-probe class; green in isolation) — motivates PR #1207.
Closes #1209 — HARNESS-HOMES fleet MVP: profile-driven seat composition and launch (`~/.config/mosaic` system root + `~/.mosaic` user data root). Base: `next` @ 216cd722 (agreed base per Jason's REALIGN-MVP GO). Integration of the T1/T2/T3 doer lanes plus fred's adjudications and two in-box hour-gate fixes. Smoke-tested end-to-end in the `mos-dev-stage` distrobox on sb-it-1-dt. ## What this delivers - `fleet agent new <name>` scaffolds a seat: `profile.json` (schema 1), `SOUL.md`, `overlay.json`, `.claude/` seat home with credential symlink into the bundle store. - `fleet launch <name> [passthrough...]` composes the seat at launch: three-layer settings merge (installed system home ← `.mosaic` user config ← agent `overlay.json`), managed per-plugin/skill symlink install roots, credential-bundle symlink wiring with A3 lstat validation, `settings.generated.json` snapshot for drift detection, then exec of the harness with `--model`/skills/passthrough argv. - Error taxonomy: `SCHEMA_TOO_NEW | PROFILE_INVALID | AGENT_NOT_SCAFFOLDED | COMPOSITION_FAILED | FIRST_AUTH_REFUSAL` — every refusal is a named, tested state. - Adjudicated merge semantics (W-F1): hook event arrays directly under top-level `hooks` concatenate (base first); all other arrays replace; `null` tombstone deletes a key. - `profile.json` is the sole seat SSOT (roster-v2 reconciliation); `MOSAIC_AGENT_NAME` injected mechanically from the profile. ## Verification - Focused suite: 22/22 green (`fleet-launch-command.spec.ts`), tsc clean. - In-box (mos-dev-stage, installed home): dry-runs for two migrated prototype seats RC=0 with correct layer paths; `fleet agent new smoketest` RC=0; real launches reach an authenticated Claude Code session (blocked past auth only by the account weekly quota — external). - Independent exact-byte gate (scrappy): probe seat `settings.generated.json` (5991 B) recomposed byte-for-byte from the three layers, SHA-256 `e08c1b5705ccf3b2fa7a4ba0296f5cffa3c58f80aaa6900a125eafbe7cd4f79d` — PASS. Gate covers composition output only. ## Fixes made during the hour-gate (in this branch) - c16256d4 — compose the system layer from the **installed flattened** home layout (`<home>/runtime/<harness>/settings.json`); the repo-tree `framework/` prefix does not exist in an installed home. - 5e154310 — the managed-install-root pruner tolerates harness **metadata files** (e.g. `installed_plugins.json`) and refuses only real **directories**; previously every seat failed composition on its second launch because Claude Code writes metadata beside the managed links. ## Documented findings (not fixed here — follow-on issues) 1. **Scaffold gap:** `fleet agent new` does not seed the sequential-thinking MCP config the launcher preflight hard-requires; a fresh seat fails preflight until `mosaic-ensure-sequential-thinking` is run. Scaffold should seed it. 2. **Headless defect:** an interactive init prompt (`keep/import/overwrite`) fires mid-launch on every launch; blocks headless seats (worked around with `</dev/null`). 3. **Snapshot-vs-live drift (by design, documented):** post-compose, the launcher/init flow writes `model: "opus[1m]"` into the live seat `settings.json`; `settings.generated.json` preserves composed bytes, so drift detection works as intended. Gate the snapshot, not live state. 4. **W-F5 live evidence — OAuth refresh breaks the bundle chain:** Claude Code refreshes tokens via rename-replace, converting the seat's credential **symlink into a real file** and stranding the shared bundle with a rotated-out (dead) refresh token; every other seat on that bundle then fails auth, and `FIRST_AUTH_REFUSAL` correctly refuses to clobber the seat file on next launch. Recovery is manual (promote fresh token to bundle, relink). This elevates the Phase-2 W-F5 auth+refresh hard gate: watchdog/promotion/relink + concurrent-refresh handling are mandatory follow-ons. 5. **Installed-tool leakage (pre-existing, not this branch):** `mosaic-ensure-sequential-thinking` writes `~/.claude/settings.json` in the invoking user's home even when `CLAUDE_CONFIG_DIR` points at a seat. 6. **Prototype migration surface:** pre-schema seats needed `account`→`bundle` profile rename and whole-store plugin-symlink → real-dir migration; both refusals fired as designed with actionable messages. ## Known-failing tests on base 216cd722 (pre-existing, unchanged by this branch) - 4 failures in `src/mutator-gate/mutator-gate.acceptance.spec.ts` ('Claude PreCompact' revoke, 'Claude SessionStart(compact)' revoke, promote-lease-lost-ACK D2-v5 backstop, runtime adapters consult broker). - `guardClaudeSettingsWiring > defaults to the real leaseEnforcementActivatable()` load-dependent flake (#1207 2s-probe class; green in isolation) — motivates PR #1207.
mos-dt-0 added 10 commits 2026-08-13 17:05:01 +00:00
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
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
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
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
mos-dt-0 added 1 commit 2026-08-13 17:17:32 +00:00
The lease-overlay split (a42d5e2e) moved the promotion hooks out of the
base Claude settings template; the wiring test still read the base alone
and failed on the absent UserPromptSubmit event, stopping the whole
test:framework-shell chain. The test now composes base + lease overlay
the way a launched seat does (hook event arrays concatenate, base
first) and asserts the same wiring contract against that view.

Reported-by: goals (clean-head probe on 5e154310)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u http://git.mosaicstack.dev/mos-dt-0/stack feat/wf-fleet-mvp:mos-dt-0-feat/wf-fleet-mvp
git checkout mos-dt-0-feat/wf-fleet-mvp
Sign in to join this conversation.