Compare commits

..

1 Commits

Author SHA1 Message Date
0d310c083e fix(install): preserve user fleet data on re-seed + refresh active units (#631)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was canceled
CRITICAL data-loss in the routine update path. `mosaic update` auto-runs
install.sh keep-mode sync (#610); the rsync --delete honored PRESERVE_PATHS but
fleet/ was not listed, so the sync WIPED ~/.config/mosaic/fleet/roster.yaml (and
fleet/run, fleet/agents). Any user running `mosaic update` lost their fleet.

PRIMARY (data-loss):
- install.sh PRESERVE_PATHS += fleet/*.yaml, fleet/agents, fleet/run. The
  framework still SEEDS fleet/examples + fleet/roles + fleet/roster.schema.json
  (synced); the operator's roster, custom rosters, per-agent env, and heartbeat
  run dir are preserved.
- Made the cp (no-rsync) fallback GLOB-AWARE so fleet/*.yaml is preserved there
  too; fixed the restore to re-glob per pattern (restores only the user file,
  not the freshly-synced fleet/ dir).
- file-adapter.ts (TS installer): mirrored the preserve list for dual-installer
  parity. (syncDirectory is copy-only — never --delete — so it never had the
  bug; this is parity + belt-and-suspenders.)

SECONDARY (stale active units):
- refreshActiveFleetUnits(): the re-seed updates ~/.config/mosaic/systemd/user
  but systemd runs ~/.config/systemd/user, so shipped unit fixes (#627) did not
  take effect after update. `mosaic update` now copies the fresh mosaic-*.service
  → the active dir + daemon-reload (best-effort, only when a fleet is installed).

Verified: bash F6 fixture (roster/custom-yaml/agents/run survive + examples
refreshed + schema seeded), 20/20 migration matrix; TS file-adapter keep-mode
test; 2 refreshActiveFleetUnits unit tests. tsc/eslint/prettier/sanitize clean.

Refs #631

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsgTQzV5YUGk1JtCLP4B83
2026-06-22 15:29:37 -05:00
26 changed files with 99 additions and 430 deletions

View File

@@ -1,40 +0,0 @@
# Build & push the pre-baked CI base image (Dockerfile.ci) to the Gitea
# registry CI already publishes to. Reuses the exact kaniko + auth pattern
# from publish.yml (REGISTRY_USER/REGISTRY_PASS from_secret, /kaniko/.docker
# config.json). Other pipelines (ci.yml, publish.yml) pull `ci-base:latest`
# for their install step.
#
# Rebuild ONLY when the dependency set or the image recipe changes — a normal
# code push must not trigger a 25-min image build. `path` applies to push/PR
# events; `event: tag` (releases) rebuilds unconditionally so a tagged release
# always ships a fresh base.
when:
- event: tag
- event: [push, manual]
branch: main
path:
include:
- 'pnpm-lock.yaml'
- 'Dockerfile.ci'
steps:
build-ci-base:
image: gcr.io/kaniko-project/executor:debug
environment:
REGISTRY_USER:
from_secret: gitea_username
REGISTRY_PASS:
from_secret: gitea_password
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
commands:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
- |
# Lockfile-hash tag: an immutable identity for the exact dep set baked
# into this image. `:latest` is the mutable pointer pipelines consume.
LOCK_HASH=$(sha256sum pnpm-lock.yaml | cut -c1-12)
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/ci-base:latest"
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/ci-base:lock-$LOCK_HASH"
/kaniko/executor --context . --dockerfile Dockerfile.ci $DESTINATIONS

View File

@@ -1,43 +0,0 @@
# Pre-baked CI base image for Woodpecker pipelines.
#
# Purpose: eliminate the cold `pnpm install` that dominates every pipeline
# (~731s median). This image ships the native toolchain (no per-run `apk add`)
# AND a warm, content-addressable pnpm store with the dependency-tree tarballs
# already fetched at build time. `pnpm fetch` only populates the store from the
# lockfile — it does NOT run the native node-gyp builds (better-sqlite3,
# node-pty, sqlite3, canvas, sharp); those still compile at `pnpm install`,
# which is exactly why the musl toolchain stays baked into this image. A
# pipeline `pnpm install --frozen-lockfile --prefer-offline` then resolves
# tarballs from local hard-links (no network) and compiles natives against the
# already-present toolchain, in tens of seconds instead of ~731s.
#
# Rebuilt only when `pnpm-lock.yaml` or this Dockerfile change
# (see .woodpecker/ci-image.yml).
#
# Node version is intentionally pinned to 22 (Active LTS at time of writing).
# The node:22 -> node:24 bump lands as a SEPARATE follow-up PR so the cache
# change carries zero runtime-version variables.
FROM node:22-alpine
# Native toolchain required to compile node-gyp deps on musl, plus the
# postgresql-client used by the test step's pg_isready readiness probe. `bash`
# is baked here too — the sanitization step in ci.yml otherwise does a per-run
# `apk add bash`.
RUN apk add --no-cache python3 make g++ postgresql-client bash
# Pin pnpm to the repo's packageManager version via corepack.
RUN corepack enable && corepack prepare pnpm@10.6.2 --activate
WORKDIR /app
# Pin the store location so the pipeline can point `store-dir` at the same path.
ENV PNPM_HOME=/root/.local/share/pnpm
RUN pnpm config set store-dir /root/.local/share/pnpm/store
# Warm the store. `pnpm fetch` populates the content-addressable store with the
# dependency tarballs directly from the lockfile (no package.json / workspace
# needed), so a baked store stays valid until the lockfile changes. Note:
# `fetch` does NOT compile native modules — that happens later at `pnpm install`
# in the pipeline, against the toolchain baked above.
COPY pnpm-lock.yaml ./
RUN pnpm fetch --frozen-lockfile

View File

@@ -79,10 +79,6 @@ Active workstream is **W1 — Federation v1**. Workers should:
- Status: implemented + tested. FIX1 model_hint→MOSAIC_AGENT_MODEL→--model. FIX2 absent socket = default tmux socket (no -L) across parse/spawn/systemd-unit/observe (socketArgs helper, bare-empty shellEnvValue, conditional -L). 158 fleet tests green; shipped presets unaffected (explicit socket_name). Detail: scratchpads/fleet-standup-fixes.md. - Status: implemented + tested. FIX1 model_hint→MOSAIC_AGENT_MODEL→--model. FIX2 absent socket = default tmux socket (no -L) across parse/spawn/systemd-unit/observe (socketArgs helper, bare-empty shellEnvValue, conditional -L). 158 fleet tests green; shipped presets unaffected (explicit socket_name). Detail: scratchpads/fleet-standup-fixes.md.
## north-star doctrine consolidation — doc PR — feat/north-star-doctrine
- Status: applied Mos's consolidated merge-map to docs/fleet/north-star.md (budget governance + control plane/central register + 200k cap + delegation + unified-identity Fleet + role-based naming + tmux security + drift re-captures). Doctrine only; #622/#623/#625/#628 out-of-scope. Conflict checklist green. Detail: scratchpads/north-star-doctrine.md.
## #631 — re-seed preserves user fleet data (CRITICAL) — fix/631-reseed-preserves-fleet-data ## #631 — re-seed preserves user fleet data (CRITICAL) — fix/631-reseed-preserves-fleet-data
- Status: implemented + tested. PRIMARY: install.sh PRESERVE_PATHS += fleet/\*.yaml + fleet/agents + fleet/run (glob-aware cp-fallback); TS parity. SECONDARY: refreshActiveFleetUnits propagates unit fixes to ~/.config/systemd/user on mosaic update. bash F6 + TS + unit tests green. Detail: scratchpads/631-reseed-preserves-fleet.md. - Status: implemented + tested. PRIMARY: install.sh PRESERVE_PATHS += fleet/\*.yaml + fleet/agents + fleet/run (glob-aware cp-fallback); TS parity. SECONDARY: refreshActiveFleetUnits propagates unit fixes to ~/.config/systemd/user on mosaic update. bash F6 + TS + unit tests green. Detail: scratchpads/631-reseed-preserves-fleet.md.

View File

@@ -7,10 +7,10 @@
## Problem ## Problem
The durable tmux fleet runs on the isolated `mosaic-fleet` socket. That isolation The durable tmux fleet runs on the isolated `mosaic-factory` socket. That isolation
(which protects the operator's default tmux) makes the fleet **invisible** to default (which protects the operator's default tmux) makes the fleet **invisible** to default
tooling, and truth is split across three planes no single command joins — systemd tooling, and truth is split across three planes no single command joins — systemd
(`systemctl --user`), tmux (`-L mosaic-fleet`), and the process tree (`pstree`). (`systemctl --user`), tmux (`-L mosaic-factory`), and the process tree (`pstree`).
`agent tail` (`capture-pane`) returns **blank for full-screen TUIs**, and `agent send` `agent tail` (`capture-pane`) returns **blank for full-screen TUIs**, and `agent send`
confirms only keystroke injection, not acceptance. Net: the operator has near-zero confirms only keystroke injection, not acceptance. Net: the operator has near-zero
observability and no safe way to watch a session. observability and no safe way to watch a session.
@@ -56,7 +56,7 @@ observability and no safe way to watch a session.
## Acceptance criteria ## Acceptance criteria
- `mosaic fleet ps` shows all 5 live sessions on `mosaic-fleet` with correct - `mosaic fleet ps` shows all 5 live sessions on `mosaic-factory` with correct
pane/pid/idle and flags the dogfood **drift** (`canary-pi` runtime=pi but pane runs pane/pid/idle and flags the dogfood **drift** (`canary-pi` runtime=pi but pane runs
`dogfood-agent.py`) and the **boot-enable** gap (active but disabled). `dogfood-agent.py`) and the **boot-enable** gap (active but disabled).
- Killing one agent's pane flips its row to dead/stale within one `interval`. - Killing one agent's pane flips its row to dead/stale within one `interval`.
@@ -72,7 +72,7 @@ observability and no safe way to watch a session.
- Unit/CLI specs in `packages/mosaic/src/commands/fleet.spec.ts` (and a new - Unit/CLI specs in `packages/mosaic/src/commands/fleet.spec.ts` (and a new
`fleet-ps`/`watch`/`send-verify` spec) using the injected `CommandRunner` to assert `fleet-ps`/`watch`/`send-verify` spec) using the injected `CommandRunner` to assert
exact tmux/systemd command construction and JSON shape (tenant+host present). exact tmux/systemd command construction and JSON shape (tenant+host present).
- Situational: run against the live `mosaic-fleet` fleet; capture `fleet ps` output, - Situational: run against the live `mosaic-factory` fleet; capture `fleet ps` output,
a kill-and-detect cycle, a read-only `watch`, and a `send --verify` pass/fail pair. a kill-and-detect cycle, a read-only `watch`, and a `send --verify` pass/fail pair.
## Known limitations ## Known limitations

View File

@@ -8,11 +8,11 @@
> Status: `not-started` | `in-progress` | `done` | `blocked` | `failed` > Status: `not-started` | `in-progress` | `done` | `blocked` | `failed`
| id | status | description | depends_on | agent | pr | notes | | id | status | description | depends_on | agent | pr | notes |
| ------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | --------------------- | ----------- | --- | --------------------------------------------------------------------------------------------------------------------------- | | ------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | --------------------- | ----------- | --- | ----------------------------------------------------------------------------------------------------------------------------- |
| FLEET-OBS-000 | done | Plan: north-star + Phase-2 PRD + workstream scaffolding | — | lead | — | persisted 2026-06-20 on `feat/fleet-observability` | | FLEET-OBS-000 | done | Plan: north-star + Phase-2 PRD + workstream scaffolding | — | lead | — | persisted 2026-06-20 on `feat/fleet-observability` |
| FLEET-OBS-001 | done | Heartbeat protocol v1 spec finalized in PRD + framework doc | FLEET-OBS-000 | lead | — | file-based `~/.config/mosaic/fleet/run/<agent>.hb`; spec in PRD | | FLEET-OBS-001 | done | Heartbeat protocol v1 spec finalized in PRD + framework doc | FLEET-OBS-000 | lead | — | file-based `~/.config/mosaic/fleet/run/<agent>.hb`; spec in PRD |
| FLEET-OBS-002 | in-progress | Implement heartbeat responder in `dogfood-agent.py` | FLEET-OBS-001 | fleet-coder | — | dispatched to ad-hoc `mosaic yolo` fleet agent (dogfood) | | FLEET-OBS-002 | in-progress | Implement heartbeat responder in `dogfood-agent.py` | FLEET-OBS-001 | fleet-coder | — | dispatched to ad-hoc `mosaic yolo` fleet agent (dogfood) |
| FLEET-OBS-003 | done | `mosaic fleet ps` — join systemd+tmux+proc+idle+heartbeat; tenant+host tagged; drift + boot-enable flags; `--json` | FLEET-OBS-001 | worker | — | commit ab47831; LIVE-verified on mosaic-fleet; caught canary-pi DRIFT + BOOT-ENABLE. Polish: idleSeconds parse returns null | | FLEET-OBS-003 | done | `mosaic fleet ps` — join systemd+tmux+proc+idle+heartbeat; tenant+host tagged; drift + boot-enable flags; `--json` | FLEET-OBS-001 | worker | — | commit ab47831; LIVE-verified on mosaic-factory; caught canary-pi DRIFT + BOOT-ENABLE. Polish: idleSeconds parse returns null |
| FLEET-OBS-004 | done | `mosaic agent watch <name>` — read-only join (no resize, no keystrokes) | FLEET-OBS-000 | worker | — | `attach -r`; verb wired | | FLEET-OBS-004 | done | `mosaic agent watch <name>` — read-only join (no resize, no keystrokes) | FLEET-OBS-000 | worker | — | `attach -r`; verb wired |
| FLEET-OBS-005 | done | `mosaic agent send --verify` — delivery/acceptance receipt | FLEET-OBS-000 | worker | — | --verify flag; draft-heuristic verify | | FLEET-OBS-005 | done | `mosaic agent send --verify` — delivery/acceptance receipt | FLEET-OBS-000 | worker | — | --verify flag; draft-heuristic verify |
| FLEET-OBS-006 | done | CLI specs for ps/watch/send-verify (tenant+host shape, command construction) | FLEET-OBS-003,004,005 | worker | — | 62 tests green (31 new); re-verified by lead | | FLEET-OBS-006 | done | CLI specs for ps/watch/send-verify (tenant+host shape, command construction) | FLEET-OBS-003,004,005 | worker | — | 62 tests green (31 new); re-verified by lead |

View File

@@ -56,21 +56,13 @@ The Fleet inherits — does not re-invent — the MVP's hard requirements:
One **definition** is the source of truth; the **session** is how it runs. One **definition** is the source of truth; the **session** is how it runs.
| Layer | Owner | Phase-2 reality | Destination | | Layer | Owner | Phase-2 reality | Destination |
| -------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | -------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- |
| **Definition + identity + auth** | gateway / `mosaic-as` (scoped tokens, #541) | `roster.yaml` (tenant-tagged) | one definition; `mosaic agent --new` materializes it | | **Definition + identity + auth** | gateway / `mosaic-as` (scoped tokens, #541) | `roster.yaml` (tenant-tagged) | one definition; `mosaic agent --new` materializes it |
| **Tenancy boundary** | **Linux uid per tenant** (linger, own `systemd --user`, own socket, own `~/.config/mosaic`) | one tenant: `jarvis` = tenant zero | uid-per-tenant; federation aggregates across hosts | | **Tenancy boundary** | **Linux uid per tenant** (linger, own `systemd --user`, own socket, own `~/.config/mosaic`) | one tenant: `jarvis` = tenant zero | uid-per-tenant; federation aggregates across hosts |
| **Runtime** | per-tenant tmux session on isolated socket | dogfood stub sessions (live now on `mosaic-factory`) | claude/codex/pi/opencode TUIs | | **Runtime** | per-tenant tmux session on isolated socket | dogfood stub sessions (live now on `mosaic-factory`) | claude/codex/pi/opencode TUIs |
| **Liveness** | **heartbeat protocol** every runtime answers | protocol defined + dogfood stub answers it | all runtimes answer; "healthy" ≠ "pane alive" | | **Liveness** | **heartbeat protocol** every runtime answers | protocol defined + dogfood stub answers it | all runtimes answer; "healthy" ≠ "pane alive" |
| **Observation** | read-only `watch` (native tmux) + `pipe-pane` stream | CLI `watch`/`ps`; explicit opt-in `attach` for control | + auth-gated webUI streams | | **Observation** | read-only `watch` (native tmux) + `pipe-pane` stream | CLI `watch`/`ps`; explicit opt-in `attach` for control | + auth-gated webUI streams |
| **Control plane** | **federation** across hosts × tenants | records already carry `tenant_id` + `host` | federated gateways expose fleet state; webUI in Phase 5 | | **Control plane** | **federation** across hosts × tenants | records already carry `tenant_id` + `host` | federated gateways expose fleet state; webUI in Phase 5 |
| **Central register** | Postgres `fleet` schema (gateway instance); access via gateway API only | _none in PoC_ (files + `roster.yaml`) | agents, missions, tasks, heartbeats, spend — single network-accessible SSOT; docs = generated projections |
| **Budget / spend governance** | **per-tenant budget policy** ingested by the orchestrator + routing layer | none today (spend is unmetered) | usage-vs-limit feedback ingested; spend auto-paced to the limit window; per-provider/per-account/concurrency/API-$ budgets enforced |
> **PoC socket hygiene:** the PoC fleet runs on the **default tmux socket** (no `-L`).
> The named production-isolation socket is **`mosaic-fleet`** (matches the product brand);
> an absent roster `socket_name` means the default socket everywhere (spawn, `fleet ps`,
> onboarding cheat-sheet). The legacy dogfood canary still runs on the old `mosaic-factory`
> socket pending migration.
## Operating model (inherited, not reinvented) ## Operating model (inherited, not reinvented)
@@ -121,67 +113,6 @@ Every artifact, starting Phase 2, MUST:
3. Define **healthy = answered a heartbeat within N seconds**, never just "pane alive". 3. Define **healthy = answered a heartbeat within N seconds**, never just "pane alive".
4. Make **observation read-only by default**; control is an explicit, separate, opt-in verb. 4. Make **observation read-only by default**; control is an explicit, separate, opt-in verb.
> **OPS INVARIANT — runtime agents need a real TTY.** Claude/Codex/pi/opencode agents
> cannot be bare-launched from a systemd `ExecStart`; a durable harness with a real PTY is
> required. This is **why `start-agent-session.sh` launches into tmux** and uses a
> `MOSAIC_AGENT_COMMAND` override rather than running the runtime directly under systemd.
## Budget & token governance (first-class fleet concern)
Spend is a fleet-level resource, not a per-agent afterthought. The fleet treats token
and API-dollar budget the way it treats liveness: a signal every runtime exposes and the
control plane is accountable for. This rides the same primitives as everything else —
`tenant_id` + `host` on every spend record, **read-only metering by default**, and the
**federation** layer as the cross-host aggregation point (W1) — so budgeting is zero-foreclosure
from day one even while one tenant exists.
**Two spend regimes, one policy surface:**
| Regime | Feedback signal | Fleet obligation |
| ------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| **OAuth-subscription runtimes** (Claude sub, Codex sub) | runtime exposes **current-usage-vs-limit** within a rolling limit window | **ingest** the signal per sub-account; **auto-pace** agentic spend so the window is not exhausted early |
| **API-token runtimes** (metered per token) | provider billing / token counts | enforce **hard $-spend ceilings**; on breach, **downgrade → queue → refuse** (below) |
**Auto-pacing law (OAuth subs) — EVEN-SPREAD default (Jason override, 2026-06-22):** the fleet
paces agentic token spend to consume the limit window **evenly over remaining time**:
target rate = _(remaining usage available)_ ÷ _(remaining time in the window)_. Example: 100% of
a 7-day window = **~14.285%/day**; the system tracks current usage and continuously re-splits the
remainder evenly to hold pace. **Anticipated token-spend-per-task is the budgeting informant**
tasks are scheduled against the daily pace, not run until the quota is gone. Rationale: spreading
delivery evenly beats rapidly exhausting usage and losing **multiple days of momentum**.
**Rapid pacing / overspend requires EXPLICIT user authorization;** absent it, even-spread holds.
Pacing is a control-plane decision, surfaced read-only before it throttles a lane.
**Hard-cap breach behavior (ladder):** when a budget ceiling is hit mid-work, the fleet
**downgrades first** (opus → sonnet → haiku, then Claude → Codex), **queues** the lane at the
cheapest floor until the window resets, and **refuses** only as a last resort. Refusal is never
the first response to a breach.
**Spend accounting, learning & telemetry:**
- **Multi-subscription auto-routing:** a tenant with multiple subscriptions may let the fleet
**auto-route work to the account with the most available usage** (within budget policy).
- **Historical spend learning:** every task's token spend is **recorded**; historical data
continuously updates known **spend-per-task**, **typical daily spend**, and projections — so
estimates self-correct and pacing stays on target.
- **Projected + actual spend on artifacts (Mosaic Stack mandate):** PRDs, missions, and task
decomposition **MUST note projected AND actual token spend** — a Mosaic Stack process standard
(template-level), tracked separately as **#622**.
- **Anonymized telemetry → mosaicstack.dev:** spend data is reported (anonymous) to the
mosaicstack.dev telemetry endpoint so other agents/fleets budget and optimize from real,
anonymized data. Product workstream, tracked separately as **#623**.
**User-settable budgets (the policy surface).** A tenant operator can set budgets for every
configured **provider** (per-provider ceilings), the **account-to-task mapping**, the **agentic
routing flow**, **concurrency** (the spend multiplier), and **hard API-token $-limits**. Budgets
are enforced at the orchestrator + routing boundary, not inside individual workers (a worker never
decides its own budget — see delegation discipline).
**Budget CLI UX (#558):** `mosaic budget set --reset-at` sets the window reset; reset-datetimes
carry **confidence tags** (`user` / `provider` / `estimated` / `unknown`); and **urgency/criticality
is a dispatch-gate modifier** — high-urgency work may override even-spread pacing **within
authorization**. (Also feeds the budgeting workstream, not only this doc.)
## Observation model ## Observation model
| Verb | Behavior | | Verb | Behavior |
@@ -196,83 +127,15 @@ authorization**. (Also feeds the budgeting workstream, not only this doc.)
> (blank for full-screen TUIs), and `attach` is read-write + resizes the session. The > (blank for full-screen TUIs), and `attach` is read-write + resizes the session. The
> verbs above restore "join and observe" safely. > verbs above restore "join and observe" safely.
## Control plane & central register
### Why the register must be Postgres
The fleet is multi-host (w-jarvis + dragon-lin + future). A SQLite file is a local
file — it is not a network service and cannot be shared across hosts. Beyond topology,
Postgres MVCC eliminates the concurrent-writer corruption class Hermes hit with SQLite
under multi-agent access.
Access is exclusively through the **gateway API** (`apps/gateway` — typed, auth-gated,
scoped tokens). No agent or dispatcher pane ever holds a raw DB credential; a
compromised pane cannot corrupt or exfiltrate the register.
### Architecture (layers)
| Layer | Responsibility | Implementation |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Register** | Source of truth: agents, missions, tasks, heartbeats, spend | Postgres `fleet` schema — existing stack instance (`@mosaicstack/db`) |
| **Access** | Typed, auth-gated API | Gateway `fleet/*` routes |
| **Dispatcher** | Brief classification, BOD review, planning/coding/review/test/deploy sequencing + gates → fleet task dispatch | **forge pipeline engine** (`runPipeline`/`resumePipeline`, brief classifier, BOD) **+ thin `forge-exec` adapter → `agent-send.sh`**; NOT a new daemon — forge is reused, only stage→agent dispatch is new |
| **Orchestrator (Mos)** | Goals, missions, judgment, user/PA interface | Context-light; sets intent → re-engages only for decisions |
### Dispatcher = forge (reuse, do not rebuild)
The dispatcher is **not new work**: it is `@mosaicstack/forge`, a fully-implemented
software-factory pipeline engine (brief → Board-of-Directors review → 3 planning stages →
coding → review/remediation → testing → deploy). Forge already provides
`runPipeline`/`resumePipeline`, a brief classifier, and a BOD persona loader, so the fleet
does **not** re-implement sequencing, gate logic, or brief classification. The only new
fleet-owned code is a thin **`forge-exec` TaskExecutor adapter** (`ForgeTask`
`agent-send.sh` to a named agent) — forge's single missing piece — tracked as a Gitea
issue and built post-PoC. The Postgres register backs forge's pipeline state (durable
`resumePipeline`, cross-host) in addition to cross-project missions/tasks/Kanban. The
north-star **'board' role IS forge's Board-of-Directors** — reused from forge, not a new
role implementation.
### Docs as projections
`docs/TASKS.md` and `MISSION-MANIFEST.md` are **generated projections** of the DB,
not hand-maintained. The dispatcher (or a scheduled job) renders Markdown from
`fleet.*` tables and commits the output. DB is authoritative; docs are for human
reference.
### Spend
`fleet.spend_ledger` records projected and actual token spend per agent/mission/task
(ties to issue #622). The dispatcher enforces budget caps before dispatching. Mos reads
the roll-up via API — no raw DB access, no context-bloating dumps.
### Federation
Cross-host fleet state flows through federated gateway queries (existing
`federation_peers` / `federation_grants` machinery). This is the existing north-star
invariant: **control plane rides federation (W1), not a bespoke broker.** No new
broker introduced.
### Scope
This is Phase 45 of this roadmap, materialized. It MUST NOT block the PoC (which
runs correctly on files + `roster.yaml`). Begin when Phase 2 heartbeat protocol is
stable and concurrent-agent count makes file coordination the bottleneck.
### Open sub-decision
Dedicated Postgres **instance** vs. dedicated **schema** in the existing instance.
Recommendation: dedicated schema, existing instance (a migration file, not new infra);
re-evaluate if isolation or write-volume demands it.
## Phased roadmap ## Phased roadmap
| Phase | Outcome | Status | | Phase | Outcome | Status |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| 01 | tmux PoC, hardening, published CLI v0.0.34 (#565#568) | ✅ done | | 01 | tmux PoC, hardening, published CLI v0.0.34 (#565#568) | ✅ done |
| **2 — Observability** | `fleet ps` (host+tenant aware join), heartbeat protocol + dogfood stub answers it, `agent watch` (read-only), `agent send --verify` receipts | ▶ now | | **2 — Observability** | `fleet ps` (host+tenant aware join), heartbeat protocol + dogfood stub answers it, `agent watch` (read-only), `agent send --verify` receipts | ▶ now |
| 3 — Real runtimes | claude/codex/pi/opencode answer heartbeat; **hybrid lifecycle** (core always-on: **orchestrator + enhancer**; ephemeral workers per lane) | planned | | 3 — Real runtimes | claude/codex/pi/opencode answer heartbeat; **hybrid lifecycle** (core always-on: **orchestrator + enhancer**; ephemeral workers per lane) | planned |
| 4 — Unified definition | one agent schema in gateway; `mosaic agent --new` → materialized per-tenant session; uid-tenant provisioning; **`fleet` schema migration + `forge-exec` TaskExecutor adapter (forge → `agent-send.sh`)** | planned | | 4 — Unified definition | one agent schema in gateway; `mosaic agent --new` → materialized per-tenant session; uid-tenant provisioning | planned |
| 5 — Control plane | federation-backed cross-host × cross-tenant fleet view; **webUI** (surface chosen then) for MVP-X1 parity; **central register live (spend ledger, docs-as-projections, multi-host Kanban)** | planned | | 5 — Control plane | federation-backed cross-host × cross-tenant fleet view; **webUI** (surface chosen then) for MVP-X1 parity | planned |
## Decisions of record (2026-06-20, with Jason) ## Decisions of record (2026-06-20, with Jason)
@@ -301,57 +164,6 @@ re-evaluate if isolation or write-volume demands it.
- **Orchestrator chat connector:** the orchestrator is reachable over a user-chosen connector - **Orchestrator chat connector:** the orchestrator is reachable over a user-chosen connector
(tmux now; Telegram/Discord/Matrix/Slack configurable). Validated live: **"Mos" orchestrator (tmux now; Telegram/Discord/Matrix/Slack configurable). Validated live: **"Mos" orchestrator
on Discord** via the Claude Code discord channel plugin (w-jarvis). on Discord** via the Claude Code discord channel plugin (w-jarvis).
- **Session context cap = 200k tokens (GLOBAL to all Claude sessions):** Claude Code sessions are
capped at a **max 200k-token context window**. Long-running sessions extended toward 1M tokens
have proven **worse in practice** (degraded steering, off-plan divergence); 200k is the standard.
**Enforcement split:** the _window_ lives in **`~/.claude/settings.json`** (host-global) as
`"autoCompactWindow": 200000` + `"autoCompactEnabled": true`; the _1M-disable_ lives in **launch
ENV** (`CLAUDE_CODE_DISABLE_1M_CONTEXT=1`, plus `CLAUDE_CODE_AUTO_COMPACT_WINDOW=200000`) wherever
a `[1m]` model can be selected (`mos-claude.service` + the fleet Claude launcher), so every Claude
agent is capped at spawn. (settings = window; env = 1M-disable.)
- **Worker context bound (#8):** workers are kept context-bounded via the **ephemeral-per-lane
lifecycle + native compaction**, not via the 200k knob. The explicit `autoCompactWindow` 200k knob
**stays Claude-specific** — the _principle_ (bounded context) extends to workers, the _knob_ does not.
- **Orchestrator delegation discipline:** the orchestrator **delegates all delivery work** to
subagents / workflows / ultracode / coder agents and confines its own context to \*\*orchestration
- the personal-assistant lane\*\*. Keeping delivery out of the orchestrator's window keeps its
context unpolluted and measurably reduces off-plan divergence. The orchestrator coordinates and
decides; it does not implement.
- **Budget governance is fleet doctrine:** token/API-dollar budgeting is a first-class fleet concern
(see "Budget & token governance"). OAuth-sub usage-vs-limit feedback is ingested per account, spend
is **auto-paced EVEN-SPREAD over remaining time** (rapid/overspend only on explicit authorization),
spend is **tracked historically** to self-correct per-task/daily estimates, multi-sub tenants may
**auto-route by available usage**, and operators set budgets per provider, per account-to-task
mapping, per routing flow, per concurrency level, and as hard API-$ ceilings.
- **Spend accounting is a Mosaic Stack process mandate:** PRDs, missions, and task decomposition
**MUST carry projected + actual token spend**; used locally for pacing and reported as **anonymized
telemetry to mosaicstack.dev**. The template standard (#622) and telemetry product (#623) are
tracked separately.
- **Unified identity = "Fleet" (Jason, 2026-06-22):** the product is **Mosaic Fleet** — one unified
user-facing identity and CLI surface. **forge** is the Fleet's **internal** delivery/orchestration
engine (not a separate product); the control-plane **Postgres register is the Fleet's register**;
workers/runtime are the **Fleet substrate**. **"factory" is RETIRED as a product term** — it was
only ever the software-factory concept (which forge implements) and the old `mosaic-factory` tmux
socket name. The production-isolation socket is now **`mosaic-fleet`** (matches the product brand);
the legacy dogfood canary remains on the old `mosaic-factory` socket pending migration. **Code stays
layered** (forge + fleet + control-plane as internal layers);
only the **identity + CLI surface unify under Fleet.**
- **Role-based session naming (Jason, 2026-06-22):** agent tmux sessions are named by **role**
(`orchestrator`, `enhancer`, `research`, `coder0-0`, …), not by persona. **Persona lives in
`SOUL.md`**; the front-end / Discord presents a **friendly alias** (e.g. "Mos" = the orchestrator's
alias). The session name is the stable addressing handle; the alias is presentation.
### Control plane & central register
- **Store:** Postgres (existing stack instance, dedicated `fleet` schema via `@mosaicstack/db`). SQLite rejected: (1) it is a local file — structurally incompatible with a multi-host fleet; (2) concurrent multi-agent writes caused repeated corruption in Hermes. "SQLite + access service" rejected as reinventing a DB server badly; "LLM agent gating DB access" rejected as slow, expensive, and a single point of failure.
- **Access:** gateway API only (`apps/gateway`, `fleet/*` routes). No raw DB credentials in any agent/dispatcher pane — directly mitigates the tmux attack-surface concern.
- **Dispatcher = forge (reuse, not a new build):** the dispatcher IS `@mosaicstack/forge`'s pipeline engine (`runPipeline`/`resumePipeline` + brief classifier + BOD persona loader), a fully-implemented software-factory pipeline (brief → BOD review → 3 planning stages → coding → review/remediation → testing → deploy). We do **not** design/build a new dispatcher and do **not** re-implement sequencing, gate logic, or brief classification. The only new fleet-owned piece is a thin **`forge-exec` TaskExecutor adapter** (suggested package `packages/forge-exec`) mapping a `ForgeTask``agent-send.sh` dispatch to a named fleet agent — forge's single missing piece. It is tracked as a Gitea issue and built **post-PoC** (not now).
- **Register backs forge:** the Postgres `fleet` register is genuinely new (neither forge nor the fleet has cross-project state). It BACKS forge's pipeline state (durable `resumePipeline`, cross-host) plus cross-project missions/tasks/Kanban.
- **'board' role = forge BOD:** the north-star role-library 'board' role IS forge's Board-of-Directors — reused, not reinvented.
- **Orchestration vs. dispatch:** Orchestrator (Mos) sets intent and handles judgment; forge works the mechanical pipeline (sequencing, gates, status transitions, spend ledger). LLM escalation reserved for judgment: mission decomposition, re-planning on failure.
- **Spend in the register:** `fleet.spend_ledger` tracks projected vs. actual tokens per agent/mission/task; ties to issue #622.
- **Docs as projections:** `docs/TASKS.md` and `MISSION-MANIFEST.md` become generated exports of the DB, not hand-maintained.
- **Sub-decision pending:** dedicated schema in existing PG instance (recommended) vs. dedicated PG instance. Revisit if isolation or write-volume demands it.
## Future enhancements (north-star, post-MVP — not on the MVP track) ## Future enhancements (north-star, post-MVP — not on the MVP track)
@@ -361,16 +173,6 @@ re-evaluate if isolation or write-volume demands it.
A major enhancement over the current third-party channel plugin; **not required for the MVP**, A major enhancement over the current third-party channel plugin; **not required for the MVP**,
but a committed north-star target. `ASSUMPTION:` ships as a Mosaic-owned plugin so the fleet but a committed north-star target. `ASSUMPTION:` ships as a Mosaic-owned plugin so the fleet
controls Discord UX (threads, reactions, attachments, per-thread context) end-to-end. controls Discord UX (threads, reactions, attachments, per-thread context) end-to-end.
- **Matrix on a local homeserver — strategic future transport.** **F4 (in progress) IS the Matrix
connector**: an orchestrator chat connector speaking the Matrix client-server API against a
self-hosted homeserver (Conduit default, Synapse alt). Matrix is named here as the strategic
future transport — peer to tmux/Discord, not superseded by them.
- **tmux fleet attack-surface hardening.** Many always-on tmux sessions are an attack surface;
`tmux send-keys` / socket access could enable malicious action against agents directly.
Mitigations to build toward: socket ownership/perms, per-tenant socket isolation (already an
invariant), authenticated `agent-send`, and an audit of who can write to any pane. **Post-MVP
unless a P0 surfaces.** The control-plane register reinforces this (gateway-API access = no raw
DB creds in panes). A not-started risk-assessment + mitigation-plan task rides the Fleet `TASKS.md`.
## Assumptions (veto-able) ## Assumptions (veto-able)
@@ -382,30 +184,3 @@ re-evaluate if isolation or write-volume demands it.
- `ASSUMPTION:` Fleet is workstream **W-FLEET** under `mvp-20260312`; a rollup row in - `ASSUMPTION:` Fleet is workstream **W-FLEET** under `mvp-20260312`; a rollup row in
`docs/TASKS.md` and a workstream declaration in `MISSION-MANIFEST.md` are proposed to `docs/TASKS.md` and a workstream declaration in `MISSION-MANIFEST.md` are proposed to
the MVP orchestrator, not written by this workstream. the MVP orchestrator, not written by this workstream.
- `ASSUMPTION:` OAuth-subscription runtimes (Claude sub, Codex sub) expose a machine-readable
current-usage-vs-limit signal the fleet can poll/ingest; if a provider exposes no such signal,
that provider's accounts fall back to API-style hard-ceiling budgeting only (no auto-pacing).
- `ASSUMPTION:` budget policy lives at the orchestrator + routing layer and is surfaced through the
same CLI→TUI→webUI parity (MVP-X1) as the rest of fleet state — not a separate budgeting daemon.
- `ASSUMPTION:` the 200k session cap is enforced by Claude Code settings/env composition (model
variant + `autoCompactWindow`), not by a Mosaic wrapper; a wrapper is the fallback only if the
harness later removes those knobs.
- `ASSUMPTION:` The central register (Postgres `fleet` schema + gateway API + forge as dispatcher) is
the Phase 45 control plane, begun after Phase 2 observability is proven. It is a dedicated
**W-FLEET** sub-workstream entry, not a separate mission. The dispatcher is `@mosaicstack/forge`
(reused, not a new daemon); the only new fleet-owned code is the thin **`forge-exec` TaskExecutor
adapter** (suggested package `packages/forge-exec`, `ForgeTask``agent-send.sh`), tracked as a
Gitea issue and built post-PoC.
---
> **Release procedure (drift re-capture, 2026-06-22):** `mosaic update` only propagates new fleet
> commands when the **CLI version is bumped** — without a version bump, fleet command changes never
> reach installed hosts. The release/version-bump procedure (bump → publish → `mosaic update`
> [→ `--relaunch`]) must be documented so fleet changes actually land. (Also feeds the budgeting
> workstream.)
>
> **Tracked separately (not in scope for this doc PR):** **#622** PRD/mission/task projected+actual
> spend template standard · **#623** anonymized spend telemetry → mosaicstack.dev (product) ·
> **#625** `tenant_id` roster-schema field (multi-tenant; invariant #1 home) · **#628** `forge-exec`
> TaskExecutor adapter (post-PoC). This PR records **doctrine only** — no implementation.

View File

@@ -1,7 +1,7 @@
# Local Fleet Canary # Local Fleet Canary
The local fleet canary runs a small tmux-backed Mosaic agent fleet on an The local fleet canary runs a small tmux-backed Mosaic agent fleet on an
isolated tmux socket. The default socket is `mosaic-fleet`; the commands do isolated tmux socket. The default socket is `mosaic-factory`; the commands do
not use or stop the default tmux server. not use or stop the default tmux server.
## Files ## Files
@@ -67,7 +67,7 @@ mosaic agent tail canary-pi -n 80
These commands read the roster and target the configured tmux socket. The These commands read the roster and target the configured tmux socket. The
generated systemd agent services use `start-agent-session.sh`; message delivery generated systemd agent services use `start-agent-session.sh`; message delivery
uses the tmux send tools with `-L mosaic-fleet`. uses the tmux send tools with `-L mosaic-factory`.
`mosaic agent send` is operator-origin traffic unless a caller explicitly says `mosaic agent send` is operator-origin traffic unless a caller explicitly says
otherwise. The CLI always passes a deterministic source label to otherwise. The CLI always passes a deterministic source label to
@@ -82,7 +82,7 @@ impersonating a known handoff lane. The lower-level inter-agent wrapper
Use these checks before expanding the roster: Use these checks before expanding the roster:
```bash ```bash
tmux -L mosaic-fleet ls tmux -L mosaic-factory ls
tmux ls tmux ls
mosaic fleet verify mosaic fleet verify
systemctl --user status mosaic-tmux-holder.service systemctl --user status mosaic-tmux-holder.service
@@ -90,7 +90,7 @@ systemctl --user status mosaic-tmux-holder.service
Expected results: Expected results:
- `tmux -L mosaic-fleet ls` shows `_holder` and roster agent sessions. - `tmux -L mosaic-factory ls` shows `_holder` and roster agent sessions.
- `tmux ls` shows only the default tmux server sessions and is not changed by - `tmux ls` shows only the default tmux server sessions and is not changed by
fleet start/stop operations. fleet start/stop operations.
- `mosaic fleet verify` checks exact session targets on the isolated socket. - `mosaic fleet verify` checks exact session targets on the isolated socket.
@@ -108,7 +108,7 @@ Run this checklist before cutting or dogfooding a fleet release:
repeated `start` against the named socket; verify the default tmux server is repeated `start` against the named socket; verify the default tmux server is
unchanged. unchanged.
- Liveness verification: run `mosaic fleet verify` and confirm roster sessions - Liveness verification: run `mosaic fleet verify` and confirm roster sessions
with `tmux -L mosaic-fleet ls` or exact `has-session` checks. with `tmux -L mosaic-factory ls` or exact `has-session` checks.
- Package dry-run: run `npm pack --dry-run --json` from `packages/mosaic` and - Package dry-run: run `npm pack --dry-run --json` from `packages/mosaic` and
confirm `framework/fleet`, `framework/systemd/user`, confirm `framework/fleet`, `framework/systemd/user`,
`framework/tools/fleet`, and `framework/tools/tmux` assets are included. `framework/tools/fleet`, and `framework/tools/tmux` assets are included.
@@ -140,5 +140,5 @@ This rollback leaves the default tmux server untouched. If a canary session is
still present after service stop, remove only the isolated socket server: still present after service stop, remove only the isolated socket server:
```bash ```bash
tmux -L mosaic-fleet kill-server tmux -L mosaic-factory kill-server
``` ```

View File

@@ -17,7 +17,7 @@ Implement enough product surface to use the fleet locally:
- roster schema and examples - roster schema and examples
- local canary docs and rollback instructions - local canary docs and rollback instructions
- tests for CLI behavior where practical - tests for CLI behavior where practical
- canary verification on named tmux socket `mosaic-fleet` - canary verification on named tmux socket `mosaic-factory`
## Non-goals ## Non-goals
@@ -30,7 +30,7 @@ Implement enough product surface to use the fleet locally:
- CLI can initialize a minimal roster outside product defaults. - CLI can initialize a minimal roster outside product defaults.
- CLI can install user systemd units and fleet helper scripts to a configurable Mosaic home. - CLI can install user systemd units and fleet helper scripts to a configurable Mosaic home.
- CLI can start/stop/status/verify a canary fleet using `mosaic-fleet`. - CLI can start/stop/status/verify a canary fleet using `mosaic-factory`.
- `mosaic agent send` uses existing named-socket/exact-target tmux tooling. - `mosaic agent send` uses existing named-socket/exact-target tmux tooling.
- `mosaic agent reset` targets only the named agent session on the named socket. - `mosaic agent reset` targets only the named agent session on the named socket.
- Verification proves default tmux sessions remain untouched. - Verification proves default tmux sessions remain untouched.

View File

@@ -31,7 +31,7 @@ with a second agent on `dragon-lin`.
## Environment facts (verified 2026-06-20) ## Environment facts (verified 2026-06-20)
- Fleet is live on `W-jarvis` (uid 1000, `jarvis`, `Linger=yes`) on tmux socket - Fleet is live on `W-jarvis` (uid 1000, `jarvis`, `Linger=yes`) on tmux socket
`mosaic-fleet`: `_holder`, `canary-pi`, `dogfood-coder`, `dogfood-orchestrator`, `mosaic-factory`: `_holder`, `canary-pi`, `dogfood-coder`, `dogfood-orchestrator`,
`dogfood-reviewer`. All panes run `~/.config/mosaic/fleet/dogfood-agent.py` (stub), `dogfood-reviewer`. All panes run `~/.config/mosaic/fleet/dogfood-agent.py` (stub),
including `canary-pi` (roster says runtime=pi → **drift**). including `canary-pi` (roster says runtime=pi → **drift**).
- Holder + `mosaic-agent@*` units are `active (exited)` but `UnitFileState=disabled` - Holder + `mosaic-agent@*` units are `active (exited)` but `UnitFileState=disabled`
@@ -56,7 +56,7 @@ with a second agent on `dragon-lin`.
with dragon-lin coder, commit docs, begin Phase-2 delivery (heartbeat + `fleet ps`). with dragon-lin coder, commit docs, begin Phase-2 delivery (heartbeat + `fleet ps`).
- 2026-06-20 (session 2): Built Phase-2 CLI via worker (commit ab47831): `fleet ps`, - 2026-06-20 (session 2): Built Phase-2 CLI via worker (commit ab47831): `fleet ps`,
`agent watch`, `agent send --verify`, 62 tests. LIVE-verified `fleet ps` on `agent watch`, `agent send --verify`, 62 tests. LIVE-verified `fleet ps` on
mosaic-fleet — correctly flagged canary-pi DRIFT + BOOT-ENABLE, tenant_id+host in JSON. mosaic-factory — correctly flagged canary-pi DRIFT + BOOT-ENABLE, tenant_id+host in JSON.
Heartbeat responder added to dogfood-agent.py (FLEET-OBS-002) — `fleet ps` HB now Heartbeat responder added to dogfood-agent.py (FLEET-OBS-002) — `fleet ps` HB now
`healthy` for all 4 agents. `healthy` for all 4 agents.
- Coordination: dual-engine-reviewed (Claude+Codex) and merged framework PRs #572 - Coordination: dual-engine-reviewed (Claude+Codex) and merged framework PRs #572

View File

@@ -11,14 +11,14 @@
## FIX 2 — socket default trap (absent ⇒ literal default socket, no -L everywhere) ## FIX 2 — socket default trap (absent ⇒ literal default socket, no -L everywhere)
- THE TRAP (3 sites): parseRosterText fallback was DEFAULT_SOCKET_NAME; systemd unit had - THE TRAP (3 sites): parseRosterText fallback was DEFAULT_SOCKET_NAME; systemd unit had
`Environment=MOSAIC_TMUX_SOCKET=mosaic-fleet` + `ExecStop ${…:-mosaic-fleet}`; start-agent-session `Environment=MOSAIC_TMUX_SOCKET=mosaic-factory` + `ExecStop ${…:-mosaic-factory}`; start-agent-session
defaulted `:-mosaic-fleet`. All fixed → absent socket = '' = default tmux socket (no -L). defaulted `:-mosaic-factory`. All fixed → absent socket = '' = default tmux socket (no -L).
- `socketArgs(name)` helper → `name ? ['-L', name] : []`; replaced all ~15 -L render sites in fleet.ts. - `socketArgs(name)` helper → `name ? ['-L', name] : []`; replaced all ~15 -L render sites in fleet.ts.
- shellEnvValue('') now emits a **bare** `VAR=` (not `''`) — unambiguous empty in systemd EnvironmentFile - shellEnvValue('') now emits a **bare** `VAR=` (not `''`) — unambiguous empty in systemd EnvironmentFile
(a quoted '' could become a literal socket named "''"). (a quoted '' could become a literal socket named "''").
- start-agent-session.sh: `_tmux` wrapper passes -L only when socket set; mosaic-agent@.service: dropped the - start-agent-session.sh: `_tmux` wrapper passes -L only when socket set; mosaic-agent@.service: dropped the
socket default + conditional ExecStop. So spawn == observe == onboarding cheat-sheet. socket default + conditional ExecStop. So spawn == observe == onboarding cheat-sheet.
- CONTAINMENT: all 6 shipped presets set socket_name: mosaic-fleet explicitly → unaffected; only - CONTAINMENT: all 6 shipped presets set socket_name: mosaic-factory explicitly → unaffected; only
socket-less rosters (the PoC) get default-socket behavior. DEFAULT_SOCKET_NAME exported for explicit use. socket-less rosters (the PoC) get default-socket behavior. DEFAULT_SOCKET_NAME exported for explicit use.
## Verification ## Verification

View File

@@ -1,19 +0,0 @@
# north-star doctrine consolidation (#620-adjacent doc PR)
- **Branch:** `feat/north-star-doctrine` (off main). Source: Mos's consolidated handoff + 2 drafts (budgeting/200k/delegation + control-plane). ONE conflict-free PR per the merge-map.
## Applied (merge-map, in order)
1. Stack table: +2 rows (Central register, Budget/spend governance) after Control plane + PoC-socket-hygiene note.
2. `## Budget & token governance` after Invariants (even-spread pacing [Jason override], hard-cap ladder, multi-sub auto-routing, historical learning, #558 CLI UX) + TTY OPS INVARIANT note.
3. `## Control plane & central register` after Observation model (Postgres fleet schema, gateway-API access, dispatcher = forge pipeline engine + forge-exec adapter [NOT a daemon], register backs forge, board = forge BOD).
4. Phased roadmap Phase 4/5 annotated (fleet schema migration + forge-exec; central register live).
5. Decisions of record (2026-06-22): doctrine §1(c) bullets (200k cap, worker bound #8, delegation, budget, spend mandate, unified identity Fleet, role-based session naming) + control-plane 6c `### Control plane & central register` subgroup.
6. Future enhancements: Matrix-future-transport (#10, F4 IS Matrix) + tmux security hardening (§5).
7. Assumptions: doctrine §1(d) (3) + control-plane 6e (1) + release-procedure note + tracked-separately note.
## Conflict checklist: all ✓
1 Decisions-2026-06-22; order Invariants→Budget→Observation→Control plane→Roadmap; 2 stack rows; even-spread (no opportunistic/HOLD); control-plane UNHELD; forge-exec = tracked #628 post-PoC; §7 drift re-captures all present (#8/#10/#558/TTY/release).
## Out of scope (cited in doc + PR): #622 (spend template std), #623 (telemetry product), #625 (tenant_id schema), #628 (forge-exec adapter). Doctrine only — no implementation.

View File

@@ -8,7 +8,7 @@ package, normally at:
~/.config/mosaic/fleet/roster.yaml ~/.config/mosaic/fleet/roster.yaml
``` ```
The default tmux socket is `mosaic-fleet` so fleet commands do not touch the The default tmux socket is `mosaic-factory` so fleet commands do not touch the
default tmux server. default tmux server.
## Examples ## Examples

View File

@@ -1,7 +1,7 @@
version: 1 version: 1
transport: tmux transport: tmux
tmux: tmux:
socket_name: mosaic-fleet socket_name: mosaic-factory
holder_session: _holder holder_session: _holder
defaults: defaults:
working_directory: ~ working_directory: ~

View File

@@ -1,7 +1,7 @@
version: 1 version: 1
transport: tmux transport: tmux
tmux: tmux:
socket_name: mosaic-fleet socket_name: mosaic-factory
holder_session: _holder holder_session: _holder
defaults: defaults:
working_directory: ~ working_directory: ~

View File

@@ -1,7 +1,7 @@
version: 1 version: 1
transport: tmux transport: tmux
tmux: tmux:
socket_name: mosaic-fleet socket_name: mosaic-factory
holder_session: _holder holder_session: _holder
defaults: defaults:
working_directory: ~ working_directory: ~

View File

@@ -1,7 +1,7 @@
version: 1 version: 1
transport: tmux transport: tmux
tmux: tmux:
socket_name: mosaic-fleet socket_name: mosaic-factory
holder_session: _holder holder_session: _holder
defaults: defaults:
working_directory: ~/src working_directory: ~/src

View File

@@ -1,7 +1,7 @@
version: 1 version: 1
transport: tmux transport: tmux
tmux: tmux:
socket_name: mosaic-fleet socket_name: mosaic-factory
holder_session: _holder holder_session: _holder
defaults: defaults:
working_directory: ~/src working_directory: ~/src

View File

@@ -1,7 +1,7 @@
version: 1 version: 1
transport: tmux transport: tmux
tmux: tmux:
socket_name: mosaic-fleet socket_name: mosaic-factory
holder_session: _holder holder_session: _holder
defaults: defaults:
working_directory: ~ working_directory: ~

View File

@@ -18,11 +18,11 @@
"properties": { "properties": {
"socket_name": { "socket_name": {
"type": "string", "type": "string",
"default": "mosaic-fleet" "default": "mosaic-factory"
}, },
"socketName": { "socketName": {
"type": "string", "type": "string",
"default": "mosaic-fleet" "default": "mosaic-factory"
}, },
"holder_session": { "holder_session": {
"type": "string", "type": "string",

View File

@@ -33,7 +33,7 @@ Per-agent overrides live outside the package in:
Example: Example:
```dotenv ```dotenv
MOSAIC_TMUX_SOCKET=mosaic-fleet MOSAIC_TMUX_SOCKET=mosaic-factory
MOSAIC_AGENT_RUNTIME=claude MOSAIC_AGENT_RUNTIME=claude
MOSAIC_AGENT_WORKDIR=$HOME/src/your-project MOSAIC_AGENT_WORKDIR=$HOME/src/your-project
# Optional escape hatch for PoC/canary agents: # Optional escape hatch for PoC/canary agents:
@@ -50,8 +50,8 @@ chmod +x ~/.config/mosaic/tools/fleet/start-agent-session.sh
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user start mosaic-tmux-holder.service systemctl --user start mosaic-tmux-holder.service
systemctl --user start mosaic-agent@canary.service systemctl --user start mosaic-agent@canary.service
tmux -L mosaic-fleet ls tmux -L mosaic-factory ls
``` ```
Do not use `tmux kill-server` without `-L mosaic-fleet`; this pattern is meant Do not use `tmux kill-server` without `-L mosaic-factory`; this pattern is meant
to avoid disturbing the user's default tmux server. to avoid disturbing the user's default tmux server.

View File

@@ -6,7 +6,7 @@ After=default.target
[Service] [Service]
Type=oneshot Type=oneshot
RemainAfterExit=yes RemainAfterExit=yes
Environment=MOSAIC_TMUX_SOCKET=mosaic-fleet Environment=MOSAIC_TMUX_SOCKET=mosaic-factory
Environment=MOSAIC_TMUX_HOLDER=_holder Environment=MOSAIC_TMUX_HOLDER=_holder
ExecStart=/bin/bash -lc 'tmux -L "$MOSAIC_TMUX_SOCKET" has-session -t "=${MOSAIC_TMUX_HOLDER}:0.0" 2>/dev/null || tmux -L "$MOSAIC_TMUX_SOCKET" new-session -d -s "$MOSAIC_TMUX_HOLDER" "while true; do sleep 3600; done"' ExecStart=/bin/bash -lc 'tmux -L "$MOSAIC_TMUX_SOCKET" has-session -t "=${MOSAIC_TMUX_HOLDER}:0.0" 2>/dev/null || tmux -L "$MOSAIC_TMUX_SOCKET" new-session -d -s "$MOSAIC_TMUX_HOLDER" "while true; do sleep 3600; done"'
ExecStop=-/bin/bash -lc 'tmux -L "$MOSAIC_TMUX_SOCKET" kill-server' ExecStop=-/bin/bash -lc 'tmux -L "$MOSAIC_TMUX_SOCKET" kill-server'

View File

@@ -3,7 +3,7 @@ set -euo pipefail
AGENT_NAME=${1:-${MOSAIC_AGENT_NAME:-}} AGENT_NAME=${1:-${MOSAIC_AGENT_NAME:-}}
# Absent socket ⇒ the LITERAL default tmux socket (no -L). The roster's # Absent socket ⇒ the LITERAL default tmux socket (no -L). The roster's
# socket_name is honored when set; absent never silently becomes mosaic-fleet # socket_name is honored when set; absent never silently becomes mosaic-factory
# (spawn stays consistent with the onboarding cheat-sheet + fleet ps observe). # (spawn stays consistent with the onboarding cheat-sheet + fleet ps observe).
MOSAIC_TMUX_SOCKET=${MOSAIC_TMUX_SOCKET:-} MOSAIC_TMUX_SOCKET=${MOSAIC_TMUX_SOCKET:-}
MOSAIC_AGENT_RUNTIME=${MOSAIC_AGENT_RUNTIME:-pi} MOSAIC_AGENT_RUNTIME=${MOSAIC_AGENT_RUNTIME:-pi}

View File

@@ -35,7 +35,7 @@ delivers reliably to local OR remote panes.
agent-send.sh -s <dst_session> -m "message" agent-send.sh -s <dst_session> -m "message"
# Local target on a Mosaic fleet socket # Local target on a Mosaic fleet socket
agent-send.sh -L mosaic-fleet -s '=coder0' -m "message" agent-send.sh -L mosaic-factory -s '=coder0' -m "message"
# Remote target (over ssh) # Remote target (over ssh)
agent-send.sh -H user@host -s <dst_session> -m "message" agent-send.sh -H user@host -s <dst_session> -m "message"
@@ -58,9 +58,9 @@ commands do not fall back to tmux's prefix matching behavior.
Durable Mosaic fleets should use a dedicated tmux socket, for example: Durable Mosaic fleets should use a dedicated tmux socket, for example:
```bash ```bash
tmux -L mosaic-fleet ls tmux -L mosaic-factory ls
agent-send.sh -L mosaic-fleet -s '=coder0' -m "status?" agent-send.sh -L mosaic-factory -s '=coder0' -m "status?"
send-message.sh -L mosaic-fleet -t '=coder0' -m "raw pane message" send-message.sh -L mosaic-factory -t '=coder0' -m "raw pane message"
``` ```
This keeps fleet operations away from the user's default tmux server. It is the This keeps fleet operations away from the user's default tmux server. It is the

View File

@@ -132,14 +132,14 @@ describe('fleet roster parsing', () => {
const roster = await loadFleetRoster(rosterPath); const roster = await loadFleetRoster(rosterPath);
expect(roster.tmux.socketName).toBe(''); // absent ⇒ default socket (no -L), not mosaic-fleet expect(roster.tmux.socketName).toBe(''); // absent ⇒ default socket (no -L), not mosaic-factory
expect(roster.tmux.holderSession).toBe('_holder'); expect(roster.tmux.holderSession).toBe('_holder');
expect(roster.agents).toHaveLength(1); expect(roster.agents).toHaveLength(1);
expect(getRosterAgent(roster, 'canary-pi').runtime).toBe('pi'); expect(getRosterAgent(roster, 'canary-pi').runtime).toBe('pi');
}); });
it('socketArgs: named socket → -L <name>; empty → no -L (default socket)', () => { it('socketArgs: named socket → -L <name>; empty → no -L (default socket)', () => {
expect(socketArgs('mosaic-fleet')).toEqual(['-L', 'mosaic-fleet']); expect(socketArgs('mosaic-factory')).toEqual(['-L', 'mosaic-factory']);
expect(socketArgs('')).toEqual([]); expect(socketArgs('')).toEqual([]);
}); });
@@ -152,14 +152,14 @@ describe('fleet roster parsing', () => {
'version: 1', 'version: 1',
'transport: tmux', 'transport: tmux',
'tmux:', 'tmux:',
' socket_name: mosaic-fleet', ' socket_name: mosaic-factory',
'agents:', 'agents:',
' - name: canary-pi', ' - name: canary-pi',
' runtime: pi', ' runtime: pi',
].join('\n'), ].join('\n'),
); );
const roster = await loadFleetRoster(rosterPath); const roster = await loadFleetRoster(rosterPath);
expect(roster.tmux.socketName).toBe('mosaic-fleet'); expect(roster.tmux.socketName).toBe('mosaic-factory');
expect(buildTmuxListSessionsCommand(roster.tmux.socketName)).toContain('-L'); expect(buildTmuxListSessionsCommand(roster.tmux.socketName)).toContain('-L');
}); });
@@ -189,7 +189,7 @@ describe('fleet roster parsing', () => {
JSON.stringify({ JSON.stringify({
version: 1, version: 1,
transport: 'tmux', transport: 'tmux',
tmux: { socket_name: 'mosaic-fleet' }, tmux: { socket_name: 'mosaic-factory' },
defaults: { working_directory: '/srv/mosaic' }, defaults: { working_directory: '/srv/mosaic' },
agents: [{ name: 'coder0', runtime: 'codex', class: 'implementer' }], agents: [{ name: 'coder0', runtime: 'codex', class: 'implementer' }],
}), }),
@@ -202,7 +202,7 @@ describe('fleet roster parsing', () => {
'MOSAIC_AGENT_RUNTIME=codex', 'MOSAIC_AGENT_RUNTIME=codex',
'MOSAIC_AGENT_MODEL=', 'MOSAIC_AGENT_MODEL=',
'MOSAIC_AGENT_WORKDIR=/srv/mosaic', 'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
'MOSAIC_TMUX_SOCKET=mosaic-fleet', 'MOSAIC_TMUX_SOCKET=mosaic-factory',
'', '',
].join('\n'), ].join('\n'),
); );
@@ -213,7 +213,7 @@ describe('fleet roster parsing', () => {
'MOSAIC_AGENT_NAME=coder0', 'MOSAIC_AGENT_NAME=coder0',
'MOSAIC_AGENT_RUNTIME=codex', 'MOSAIC_AGENT_RUNTIME=codex',
'MOSAIC_AGENT_WORKDIR=/srv/new', 'MOSAIC_AGENT_WORKDIR=/srv/new',
'MOSAIC_TMUX_SOCKET=mosaic-fleet', 'MOSAIC_TMUX_SOCKET=mosaic-factory',
'', '',
].join('\n'); ].join('\n');
const existing = [ const existing = [
@@ -231,7 +231,7 @@ describe('fleet roster parsing', () => {
'MOSAIC_AGENT_NAME=coder0', 'MOSAIC_AGENT_NAME=coder0',
'MOSAIC_AGENT_RUNTIME=codex', 'MOSAIC_AGENT_RUNTIME=codex',
'MOSAIC_AGENT_WORKDIR=/srv/new', 'MOSAIC_AGENT_WORKDIR=/srv/new',
'MOSAIC_TMUX_SOCKET=mosaic-fleet', 'MOSAIC_TMUX_SOCKET=mosaic-factory',
'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh', 'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh',
'# site note', '# site note',
'', '',
@@ -324,7 +324,7 @@ describe('fleet roster parsing', () => {
const localCanary = await loadFleetRoster(join(examplesDir, 'local-canary.yaml')); const localCanary = await loadFleetRoster(join(examplesDir, 'local-canary.yaml'));
expect(minimal.agents.map((agent) => agent.name)).toEqual(['canary-pi']); expect(minimal.agents.map((agent) => agent.name)).toEqual(['canary-pi']);
expect(localCanary.tmux.socketName).toBe('mosaic-fleet'); expect(localCanary.tmux.socketName).toBe('mosaic-factory');
expect(localCanary.agents.map((agent) => agent.name)).toEqual(['lead', 'coder0', 'reviewer0']); expect(localCanary.agents.map((agent) => agent.name)).toEqual(['lead', 'coder0', 'reviewer0']);
expect(localCanaryText).not.toMatch(/usc|ultron|secrev/i); expect(localCanaryText).not.toMatch(/usc|ultron|secrev/i);
}); });
@@ -349,11 +349,11 @@ describe('fleet command construction', () => {
it('builds socket-scoped agent send commands', () => { it('builds socket-scoped agent send commands', () => {
const paths = resolveFleetPaths('/home/test/.config/mosaic'); const paths = resolveFleetPaths('/home/test/.config/mosaic');
expect( expect(
buildAgentSendCommand(paths, 'coder0', 'hello', 'mosaic-fleet', 'operator:mosaic-cli'), buildAgentSendCommand(paths, 'coder0', 'hello', 'mosaic-factory', 'operator:mosaic-cli'),
).toEqual([ ).toEqual([
'/home/test/.config/mosaic/tools/tmux/agent-send.sh', '/home/test/.config/mosaic/tools/tmux/agent-send.sh',
'-L', '-L',
'mosaic-fleet', 'mosaic-factory',
'-S', '-S',
'operator:mosaic-cli', 'operator:mosaic-cli',
'-s', '-s',
@@ -841,10 +841,10 @@ describe('fleet ps — command construction', () => {
}); });
it('builds exact tmux list-panes command with the correct format string', () => { it('builds exact tmux list-panes command with the correct format string', () => {
expect(buildTmuxListPanesCommand('canary-pi', 'mosaic-fleet')).toEqual([ expect(buildTmuxListPanesCommand('canary-pi', 'mosaic-factory')).toEqual([
'tmux', 'tmux',
'-L', '-L',
'mosaic-fleet', 'mosaic-factory',
'list-panes', 'list-panes',
'-t', '-t',
'=canary-pi:0.0', '=canary-pi:0.0',
@@ -1167,7 +1167,7 @@ describe('fleet install — auto-enable units for boot-survival', () => {
const minimalRoster: FleetRoster = { const minimalRoster: FleetRoster = {
version: 1, version: 1,
transport: 'tmux', transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' }, tmux: { socketName: 'mosaic-factory', holderSession: '_holder' },
defaults: { workingDirectory: '~/src' }, defaults: { workingDirectory: '~/src' },
runtimes: { codex: { resetCommand: '/clear' } }, runtimes: { codex: { resetCommand: '/clear' } },
agents: [{ name: 'coder0', runtime: 'codex', className: 'worker' }], agents: [{ name: 'coder0', runtime: 'codex', className: 'worker' }],
@@ -1189,7 +1189,7 @@ describe('fleet install — auto-enable units for boot-survival', () => {
const minimalRoster: FleetRoster = { const minimalRoster: FleetRoster = {
version: 1, version: 1,
transport: 'tmux', transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' }, tmux: { socketName: 'mosaic-factory', holderSession: '_holder' },
defaults: { workingDirectory: '~/src' }, defaults: { workingDirectory: '~/src' },
runtimes: { codex: { resetCommand: '/clear' } }, runtimes: { codex: { resetCommand: '/clear' } },
agents: [{ name: 'coder0', runtime: 'codex', className: 'worker' }], agents: [{ name: 'coder0', runtime: 'codex', className: 'worker' }],
@@ -1216,7 +1216,7 @@ describe('fleet install — auto-enable units for boot-survival', () => {
const minimalRoster: FleetRoster = { const minimalRoster: FleetRoster = {
version: 1, version: 1,
transport: 'tmux', transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' }, tmux: { socketName: 'mosaic-factory', holderSession: '_holder' },
defaults: { workingDirectory: '~/src' }, defaults: { workingDirectory: '~/src' },
runtimes: { codex: { resetCommand: '/clear' } }, runtimes: { codex: { resetCommand: '/clear' } },
agents: [{ name: 'coder0', runtime: 'codex', className: 'worker' }], agents: [{ name: 'coder0', runtime: 'codex', className: 'worker' }],
@@ -1388,10 +1388,10 @@ describe('fleet ps — command sequences issued', () => {
describe('buildTmuxListSessionsCommand', () => { describe('buildTmuxListSessionsCommand', () => {
it('builds exact list-sessions command with session_name format', () => { it('builds exact list-sessions command with session_name format', () => {
expect(buildTmuxListSessionsCommand('mosaic-fleet')).toEqual([ expect(buildTmuxListSessionsCommand('mosaic-factory')).toEqual([
'tmux', 'tmux',
'-L', '-L',
'mosaic-fleet', 'mosaic-factory',
'list-sessions', 'list-sessions',
'-F', '-F',
'#{session_name}', '#{session_name}',
@@ -1642,11 +1642,11 @@ describe('fleet ps — unmanaged socket sessions', () => {
describe('agent watch', () => { describe('agent watch', () => {
it('builds exact grouped-viewer creation command', () => { it('builds exact grouped-viewer creation command', () => {
expect( expect(
buildAgentWatchCreateViewerCommand('canary-pi', 'canary-pi-watch-123', 'mosaic-fleet'), buildAgentWatchCreateViewerCommand('canary-pi', 'canary-pi-watch-123', 'mosaic-factory'),
).toEqual([ ).toEqual([
'tmux', 'tmux',
'-L', '-L',
'mosaic-fleet', 'mosaic-factory',
'new-session', 'new-session',
'-d', '-d',
'-t', '-t',
@@ -1657,10 +1657,10 @@ describe('agent watch', () => {
}); });
it('builds exact viewer attach command (read-only)', () => { it('builds exact viewer attach command (read-only)', () => {
expect(buildAgentWatchAttachCommand('canary-pi-watch-123', 'mosaic-fleet')).toEqual([ expect(buildAgentWatchAttachCommand('canary-pi-watch-123', 'mosaic-factory')).toEqual([
'tmux', 'tmux',
'-L', '-L',
'mosaic-fleet', 'mosaic-factory',
'attach', 'attach',
'-r', '-r',
'-t', '-t',
@@ -1669,10 +1669,10 @@ describe('agent watch', () => {
}); });
it('builds exact viewer kill command', () => { it('builds exact viewer kill command', () => {
expect(buildAgentWatchKillViewerCommand('canary-pi-watch-123', 'mosaic-fleet')).toEqual([ expect(buildAgentWatchKillViewerCommand('canary-pi-watch-123', 'mosaic-factory')).toEqual([
'tmux', 'tmux',
'-L', '-L',
'mosaic-fleet', 'mosaic-factory',
'kill-session', 'kill-session',
'-t', '-t',
'canary-pi-watch-123', 'canary-pi-watch-123',
@@ -1769,10 +1769,10 @@ describe('agent watch', () => {
describe('agent send --verify', () => { describe('agent send --verify', () => {
it('builds exact verify capture-pane command', () => { it('builds exact verify capture-pane command', () => {
expect(buildAgentVerifyAcceptedCommand('canary-pi', 'mosaic-fleet', 5)).toEqual([ expect(buildAgentVerifyAcceptedCommand('canary-pi', 'mosaic-factory', 5)).toEqual([
'tmux', 'tmux',
'-L', '-L',
'mosaic-fleet', 'mosaic-factory',
'capture-pane', 'capture-pane',
'-t', '-t',
'=canary-pi:0.0', '=canary-pi:0.0',
@@ -2484,7 +2484,7 @@ describe('fleet add/remove — pure helpers', () => {
const baseRoster: FleetRoster = { const baseRoster: FleetRoster = {
version: 1, version: 1,
transport: 'tmux', transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' }, tmux: { socketName: 'mosaic-factory', holderSession: '_holder' },
defaults: { workingDirectory: '~/src' }, defaults: { workingDirectory: '~/src' },
runtimes: { codex: { resetCommand: '/clear' } }, runtimes: { codex: { resetCommand: '/clear' } },
agents: [ agents: [
@@ -2610,7 +2610,7 @@ describe('fleet add/remove — pure helpers', () => {
await writeFile(rosterPath, yaml); await writeFile(rosterPath, yaml);
const loaded = await loadFleetRoster(rosterPath); const loaded = await loadFleetRoster(rosterPath);
expect(loaded.agents.map((a) => a.name)).toEqual(['orchestrator', 'coder0']); expect(loaded.agents.map((a) => a.name)).toEqual(['orchestrator', 'coder0']);
expect(loaded.tmux.socketName).toBe('mosaic-fleet'); expect(loaded.tmux.socketName).toBe('mosaic-factory');
expect(loaded.agents[0]!.className).toBe('orchestrator'); expect(loaded.agents[0]!.className).toBe('orchestrator');
} finally { } finally {
await rm(dir, { recursive: true, force: true }); await rm(dir, { recursive: true, force: true });

View File

@@ -122,7 +122,7 @@ type FleetServiceAction = 'start' | 'stop' | 'restart' | 'status';
* rosters/callers that explicitly want isolation; it is NO LONGER the silent * rosters/callers that explicitly want isolation; it is NO LONGER the silent
* fallback for a socket-less roster (that now resolves to the default socket). * fallback for a socket-less roster (that now resolves to the default socket).
*/ */
export const DEFAULT_SOCKET_NAME = 'mosaic-fleet'; export const DEFAULT_SOCKET_NAME = 'mosaic-factory';
const DEFAULT_HOLDER_SESSION = '_holder'; const DEFAULT_HOLDER_SESSION = '_holder';
const DEFAULT_WORKING_DIRECTORY = '~/src'; const DEFAULT_WORKING_DIRECTORY = '~/src';
@@ -130,7 +130,7 @@ const DEFAULT_WORKING_DIRECTORY = '~/src';
* tmux `-L` args for a socket name. An empty/absent socket ⇒ the LITERAL default * tmux `-L` args for a socket name. An empty/absent socket ⇒ the LITERAL default
* tmux socket (no `-L`), so spawn, observe (`fleet ps`/watch), and the onboarding * tmux socket (no `-L`), so spawn, observe (`fleet ps`/watch), and the onboarding
* cheat-sheet all agree. A named socket ⇒ `-L <name>`. `DEFAULT_SOCKET_NAME` * cheat-sheet all agree. A named socket ⇒ `-L <name>`. `DEFAULT_SOCKET_NAME`
* remains a constant for callers that explicitly want mosaic-fleet; it is no * remains a constant for callers that explicitly want mosaic-factory; it is no
* longer the silent fallback for a socket-less roster. * longer the silent fallback for a socket-less roster.
*/ */
export function socketArgs(socketName: string): string[] { export function socketArgs(socketName: string): string[] {
@@ -1689,7 +1689,7 @@ function normalizeRoster(raw: RawFleetRoster): FleetRoster {
transport: 'tmux', transport: 'tmux',
tmux: { tmux: {
// Absent socket_name ⇒ '' (the literal default tmux socket, no -L) — NOT // Absent socket_name ⇒ '' (the literal default tmux socket, no -L) — NOT
// mosaic-fleet. Shipped presets set socket_name explicitly, so they are // mosaic-factory. Shipped presets set socket_name explicitly, so they are
// unaffected; only socket-less rosters get default-socket behavior. // unaffected; only socket-less rosters get default-socket behavior.
socketName: stringValue( socketName: stringValue(
raw.tmux?.socket_name ?? raw.tmux?.socketName, raw.tmux?.socket_name ?? raw.tmux?.socketName,

View File

@@ -48,9 +48,9 @@ describe('parseRosterAgents', () => {
it('parses an optional per-agent socket', () => { it('parses an optional per-agent socket', () => {
const peers = parseRosterAgents( const peers = parseRosterAgents(
['agents:', ' - name: a', ' class: worker', ' socket: mosaic-fleet'].join('\n'), ['agents:', ' - name: a', ' class: worker', ' socket: mosaic-factory'].join('\n'),
); );
expect(peers[0]).toMatchObject({ name: 'a', socket: 'mosaic-fleet' }); expect(peers[0]).toMatchObject({ name: 'a', socket: 'mosaic-factory' });
}); });
it('stops at the next top-level key', () => { it('stops at the next top-level key', () => {
@@ -99,9 +99,9 @@ describe('renderPeerReach — same-host vs cross-host', () => {
}); });
it('emits -L <socket> for a named socket', () => { it('emits -L <socket> for a named socket', () => {
const peer: CommsPeer = { name: 'coder0', className: 'implementer', socket: 'mosaic-fleet' }; const peer: CommsPeer = { name: 'coder0', className: 'implementer', socket: 'mosaic-factory' };
expect(renderPeerReach(peer, 'w-jarvis', send)).toBe( expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(
`${send} -L mosaic-fleet -s coder0 -m "…"`, `${send} -L mosaic-factory -s coder0 -m "…"`,
); );
}); });
@@ -111,10 +111,10 @@ describe('renderPeerReach — same-host vs cross-host', () => {
className: 'implementer', className: 'implementer',
host: '10.1.10.37', host: '10.1.10.37',
ssh: 'jwoltje@10.1.10.37', ssh: 'jwoltje@10.1.10.37',
socket: 'mosaic-fleet', socket: 'mosaic-factory',
}; };
expect(renderPeerReach(peer, 'w-jarvis', send)).toBe( expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(
`${send} -L mosaic-fleet -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`, `${send} -L mosaic-factory -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`,
); );
}); });
}); });