Compare commits
1 Commits
feat/758-r
...
docs/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c3f28ae5a |
182
docs/PRD.md
182
docs/PRD.md
@@ -79,52 +79,6 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fleet Declarative Configuration Management Workstream (FCM, #758)
|
|
||||||
|
|
||||||
### Problem and objective
|
|
||||||
|
|
||||||
The local Mosaic fleet has a roster, generated agent environment files, user-systemd units, tmux
|
|
||||||
sessions, heartbeat files, examples, profiles, and separate gateway-backed agent records. These
|
|
||||||
planes have drifted and are not one safe operator lifecycle. The objective is one **local fleet
|
|
||||||
roster** as the desired-state SSOT, with generated environment, systemd, tmux, and heartbeat
|
|
||||||
artifacts as rebuildable projections; it does not merge the local fleet control plane with the
|
|
||||||
gateway-backed agent catalog.
|
|
||||||
|
|
||||||
### Normative requirements
|
|
||||||
|
|
||||||
| ID | Requirement |
|
|
||||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `FCM-REQ-01` | The roster SHALL be the sole writable desired-state source for local fleet membership, launch policy, and persisted lifecycle target. Generated environment files, systemd enablement, tmux sessions, and heartbeat state SHALL be non-authoritative projections. |
|
|
||||||
| `FCM-REQ-02` | The implementation SHALL provide one executable structural contract for YAML/JSON input and one shared semantic validator. Roster load, profile validation, provision, migration, and apply SHALL reuse the existing baseline-plus-`roles.local` profile/persona resolver; a parallel role resolver is forbidden. |
|
|
||||||
| `FCM-REQ-03` | The local fleet CLI SHALL expose documented programmatic validate, show, plan, apply/reconcile, create, inspect, update, delete, start, stop, restart, status, verify, and doctor operations with stable JSON and exit-code behavior. Existing `fleet add/remove` compatibility aliases may remain during the stated deprecation window. |
|
|
||||||
| `FCM-REQ-04` | A fresh create SHALL persist `enabled:true` and `desired_state:stopped` unless an explicit persisted start is requested. The model SHALL distinguish enabled state, persisted desired state, and observed state. Migration, apply, reboot, and rollback SHALL not start an agent that was observed stopped before cutover. |
|
|
||||||
| `FCM-REQ-05` | The launch chain SHALL consume deterministic, digest-stamped generated input only. Optional local overrides SHALL be parsed as strict data, may not shadow authoritative generated keys, and may not contain arbitrary commands, credential values, channels, or unknown `MOSAIC_AGENT_*` keys. Forbidden legacy keys, including `MOSAIC_AGENT_COMMAND`, SHALL be privately quarantined before launch and reported only by key name and content hash. |
|
|
||||||
| `FCM-REQ-06` | Mutations and apply SHALL validate before mutation, use an expected generation/lock, write projections atomically, produce a deterministic plan, and emit recovery information on partial failure. Reconciliation SHALL act only on local, enabled, roster-owned projections and SHALL not kill unmanaged tmux sessions by fuzzy name. |
|
|
||||||
| `FCM-REQ-07` | Canonical required classes are `code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`. `validator` issues an independent final certificate but has no merge authority; `merge-gate` remains sole approve-to-land/merge authority. Team-leader capacity is bounded by an orchestrator-issued lease, and interaction is request/status only. Tess and Ultron are configurable instance/display names, not required machine identities. |
|
|
||||||
| `FCM-REQ-08` | v1 migration SHALL be field-complete, reversible, and explicit about aliases, unresolved classes, lifecycle inference, generated-file regeneration, local override quarantine, schema-only remote/connector fields, and rollback. Every shipped example, profile, and service preset SHALL be migrated and executable, retained as an explicitly versioned v1 fixture, or retired with a replacement and deprecation note. |
|
|
||||||
| `FCM-REQ-09` | M1–M5 SHALL remain local tmux/systemd control-plane work. Remote/SSH reconciliation, connector mutation, secret references, arbitrary command/channel overrides, gateway/API convergence, and UI configuration storage are excluded and require a separate PRD/threat model. |
|
|
||||||
| `FCM-REQ-10` | Documentation and examples are delivery gates. The M0 checklist at [docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) and the baseline disposition inventory at [docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) SHALL be maintained as acceptance evidence. |
|
|
||||||
|
|
||||||
### Acceptance criteria
|
|
||||||
|
|
||||||
1. `AC-FCM-01`: A valid local v2 roster can be parsed from YAML or JSON, validated structurally and semantically through the shared resolver, and rendered canonically; invalid fields, duplicate names, unresolved classes, unsupported runtime/model combinations, socket ambiguity, and incompatible options fail closed.
|
|
||||||
2. `AC-FCM-02`: `plan` reports deterministic desired-versus-observed differences for roster, generated environment, systemd enablement, tmux/session, heartbeat, installed-asset revision, and provable orphans without mutation; `apply --check` reports drift without mutation.
|
|
||||||
3. `AC-FCM-03`: Local create/update/delete is generation-guarded, atomic, idempotent, and safe by default; it permits supported runtime/model/harness/effort/workdir/role changes without direct editing of generated environment files and does not start a newly created agent unless explicitly persisted.
|
|
||||||
4. `AC-FCM-04`: The generated-env/local-override launch chain rejects generated-key shadowing, arbitrary command override, unknown keys, shell evaluation, and sensitive-value diagnostics before any agent starts; known-safe legacy input is regenerated or strictly relocated, and forbidden input is quarantined.
|
|
||||||
5. `AC-FCM-05`: Local lifecycle reconciliation implements the persisted/transient start-stop rules, exact default/named tmux socket targeting, systemd/tmux status, stale generated state, unmanaged-session reporting, and rollback without surprise restarts or fuzzy destructive targeting.
|
|
||||||
6. `AC-FCM-06`: A v1 roster migration previews field-by-field disposition, preserves observed stopped/running state, inventories rather than reconciles remote/schema-only entries, supports a canary and rollback, and classifies every shipped example, profile, and service preset according to the M0 inventory.
|
|
||||||
7. `AC-FCM-07`: Required role authority is validated: validator certificate is consumed but does not merge, merge-gate is the sole merge authority, team-leader leases do not change roster/credentials/authority, and interaction/Tess cannot claim orchestration or merge powers.
|
|
||||||
8. `AC-FCM-08`: Documentation, examples, migration, troubleshooting, operational recovery, package/update asset drift, schema/example/profile validation, independent code/security review, validator certificate, and terminal-green CI are complete before #758 closes.
|
|
||||||
|
|
||||||
### M0 implementation gate
|
|
||||||
|
|
||||||
No source, schema, role, example, profile, systemd, or live-fleet change is authorized before M0
|
|
||||||
lands. M0 consists only of these normative requirements, the complete task DAG, the scoped
|
|
||||||
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
|
|
||||||
are defined in [docs/TASKS.md](./TASKS.md) and must remain one card/one PR.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tess Interaction Agent Workstream (TESS)
|
## Tess Interaction Agent Workstream (TESS)
|
||||||
|
|
||||||
### Problem and Objective
|
### Problem and Objective
|
||||||
@@ -1146,3 +1100,139 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
|
|||||||
10. ASSUMPTION: **Conversations and messages get their own PG tables** (not stored in brain's entity model). They follow a chat-specific schema with proper foreign keys to users and projects. Rationale: Chat has different access patterns (streaming, pagination, search) than brain entities.
|
10. ASSUMPTION: **Conversations and messages get their own PG tables** (not stored in brain's entity model). They follow a chat-specific schema with proper foreign keys to users and projects. Rationale: Chat has different access patterns (streaming, pagination, search) than brain entities.
|
||||||
|
|
||||||
11. RESOLVED: **Pi handles all target LLM providers natively.** Anthropic, OpenAI/Codex, Z.ai, Ollama, LM Studio, and llama.cpp are all supported via Pi's built-in providers or `models.json` configuration with `openai-completions` API type. No custom provider adapters needed in @mosaicstack/agent — only configuration management.
|
11. RESOLVED: **Pi handles all target LLM providers natively.** Anthropic, OpenAI/Codex, Z.ai, Ollama, LM Studio, and llama.cpp are all supported via Pi's built-in providers or `models.json` configuration with `openai-completions` API type. No custom provider adapters needed in @mosaicstack/agent — only configuration management.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fleet Declarative Configuration Management (#758)
|
||||||
|
|
||||||
|
### Status and objective
|
||||||
|
|
||||||
|
- **Requirement ID:** `FCM-PRD-001`
|
||||||
|
- **Status:** approved architecture; M0 documentation gate in progress
|
||||||
|
- **Authority:** issue #758 and the independently approved baseline plan
|
||||||
|
|
||||||
|
Provide one understandable, schema-validated lifecycle for the local Mosaic fleet. The operator-owned YAML/JSON roster is the canonical desired-state input. Generated agent environment files, systemd enablement, tmux sessions, heartbeat state, and installed framework assets are derived or observed state. Mutations must pass through one shared compiler/reconciler and must be previewable, atomic where possible, recoverable, automation-safe, and non-destructive toward unmanaged resources.
|
||||||
|
|
||||||
|
### Normative scope
|
||||||
|
|
||||||
|
#### In scope for M0–M5
|
||||||
|
|
||||||
|
1. A narrow v2 YAML/JSON roster for local tmux/systemd fleets.
|
||||||
|
2. One executable structural contract with schema/parser parity and canonical snake_case output.
|
||||||
|
3. Semantic validation through the existing baseline plus `roles.local` profile/persona/provision resolver; a parallel role resolver is forbidden.
|
||||||
|
4. Canonical classes `code`, `review`, `security-review`, `validator`, `merge-gate`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`, including documented legacy aliases.
|
||||||
|
5. Read/validate/plan/apply/migrate, full local fleet-agent CRUD, lifecycle, status, verify, stable JSON output, and documented exit codes.
|
||||||
|
6. Deterministic `.env.generated` projections, a strict non-shell `.env.local` allowlist, generation/digest stamping, and fail-closed quarantine of forbidden legacy keys.
|
||||||
|
7. v1 inventory, preview, field-complete migration, canary cutover, rollback, compatibility aliases, and explicit disposition of every shipped example/profile.
|
||||||
|
8. Documentation, packaging/update checks, clean-install/cold-start dogfood, and independent correctness, security, validator, and merge gates.
|
||||||
|
|
||||||
|
#### Out of scope for M0–M5
|
||||||
|
|
||||||
|
- Kubernetes-style resource envelopes.
|
||||||
|
- Remote/SSH reconciliation or distributed placement mutation.
|
||||||
|
- Connector/Matrix/Discord lifecycle mutation.
|
||||||
|
- Secret-reference or credential-provider schema.
|
||||||
|
- Arbitrary command or channel overrides.
|
||||||
|
- Gateway `/api/agents` mapping, control-plane convergence, UI configuration storage, or rename of that separate DB-backed catalog.
|
||||||
|
- Live-fleet mutation during M0.
|
||||||
|
|
||||||
|
Each excluded capability requires a separate post-M5 PRD and threat model. Existing v1 remote/connector fields are inventory-only: local apply must reject them without invoking systemd or tmux.
|
||||||
|
|
||||||
|
### Authority and identity decisions
|
||||||
|
|
||||||
|
- The roster owns fleet membership, launch policy, and persisted lifecycle intent.
|
||||||
|
- Role/persona contracts are product reference data; `roles.local` is the update-surviving local extension layer.
|
||||||
|
- Tess and Ultron are configurable instance/display names, not schema identities.
|
||||||
|
- `validator` issues the independent final validation certificate but cannot approve-to-land or merge.
|
||||||
|
- `merge-gate` remains the sole approve-to-land and merge authority after required review, security, validation, CI, and queue gates.
|
||||||
|
- `orchestrator` may apply validated owner-policy-compliant topology changes and grant/revoke bounded capacity leases.
|
||||||
|
- `team-leader` may accept/release and use a named lease but cannot change global topology, re-lease capacity, or gain merge authority.
|
||||||
|
- `review` and `security-review` provide independent correctness and security records respectively; neither authors the reviewed change.
|
||||||
|
- `interaction` is request/status only. `enhancer` proposes fleet improvements. `code` authors implementation but cannot self-review.
|
||||||
|
- Operator policy remains the exception, pause, and lease-revocation boundary.
|
||||||
|
|
||||||
|
A capacity lease names existing agents, purpose, and expiry. It never changes class, runtime, tools, credentials, roster ownership, or merge authority.
|
||||||
|
|
||||||
|
### Lifecycle and generated-state decisions
|
||||||
|
|
||||||
|
The normative dimensions are `enabled`, persisted `desired_state: running|stopped`, and observed `running|stopped|error|unknown|unmanaged`.
|
||||||
|
|
||||||
|
1. Fresh create defaults to enabled and stopped; `create --start` persists running.
|
||||||
|
2. v1 migration preserves known observed running/stopped state. Unknown state blocks apply for that entry.
|
||||||
|
3. Start/stop without `--persist` is transient and reports drift; the next apply/reboot restores persisted intent.
|
||||||
|
4. Start/stop with `--persist` atomically updates generation and converges the local unit/session.
|
||||||
|
5. Restart does not change desired state and rejects stopped entries unless explicitly started.
|
||||||
|
6. Apply acts only on local, enabled, roster-owned entries. Ownership must be proven before stale projections are quarantined or removed; fuzzy names never authorize stopping an unmanaged session.
|
||||||
|
7. Rollback restores roster/projection generation and captured unit enablement, stops processes introduced by the failed generation, and never starts an agent that was stopped before cutover.
|
||||||
|
8. The systemd unit reads only `%i.env.generated` after migration. `.env.local` is parsed as data, cannot shadow authoritative keys, and never uses shell `source`, `eval`, expansion, or command substitution.
|
||||||
|
9. `MOSAIC_AGENT_COMMAND`, channel flags, credential variables, and unknown agent keys are forbidden. Migration reports key names and content hashes only—never values—and blocks launch/apply until disposition.
|
||||||
|
|
||||||
|
### Functional requirements
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| FCM-FR-01 | Show and validate YAML/JSON using one structural contract and shared semantic role/topology validation. |
|
||||||
|
| FCM-FR-02 | Produce a deterministic, non-mutating desired-versus-observed plan covering roster, projections, units, sessions, installed assets, and orphans. |
|
||||||
|
| FCM-FR-03 | Apply under a lock with expected-generation checks, atomic writes/backups, ordered convergence, post-verification, and machine-readable recovery data. |
|
||||||
|
| FCM-FR-04 | Provide create, inspect, update, remove, list, start, stop, restart, status, validate, reconcile, doctor, dry-run, and automation-safe operations. |
|
||||||
|
| FCM-FR-05 | Validate duplicate names, unsupported classes/runtimes/models/options, topology cycles, missing role contracts, stale generated state, unmanaged sessions, unit drift, and socket ambiguity. |
|
||||||
|
| FCM-FR-06 | Generate deterministic, mode-0600, digest-stamped launch projections; safely parse only allowlisted local operational overrides. |
|
||||||
|
| FCM-FR-07 | Read v1 for one deprecation window, write v2 after migration, preserve known lifecycle intent, and provide preview/canary/rollback. |
|
||||||
|
| FCM-FR-08 | Classify every shipped example/profile as migrated, versioned compatibility fixture, or retired with replacement. |
|
||||||
|
| FCM-FR-09 | Report desired, observed, generation, drift, readiness, ownership, and failing plane without exposing privileged values. |
|
||||||
|
| FCM-FR-10 | Keep gateway-backed `mosaic agent` records explicitly separate from local `mosaic fleet` desired state. |
|
||||||
|
|
||||||
|
### Non-functional requirements
|
||||||
|
|
||||||
|
- **Security:** fail closed on command/credential/unknown overrides; reject traversal, injection, shadowing, and unauthorized topology/lifecycle actions; never expose secret or command values.
|
||||||
|
- **Reliability:** lock plus expected generation; temporary write, fsync, atomic rename, recoverable backup; deterministic idempotent replay; ordered rollback on partial failure.
|
||||||
|
- **Safety:** no destructive inference from stale names; no local actions for remote/schema-only entries; stopped agents remain stopped through migration and reboot.
|
||||||
|
- **Compatibility:** canonical snake_case serialization with bounded v1 camelCase/alias input support and explicit warnings.
|
||||||
|
- **Observability:** stable text/JSON status, drift, plan, migration, and recovery output with documented exit codes `0` success, `2` invalid, `3` drift, `4` conflict, `5` partial failure, and `6` policy denial.
|
||||||
|
- **Maintainability:** schema, roster load, profiles, provision, migration, and apply share the existing role-resolution implementation.
|
||||||
|
- **Documentation:** every field, command, transition, migration rule, recovery workflow, class power, and example is linked from the fleet docs IA and validated in CI.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
|
||||||
|
| ID | Acceptance criterion |
|
||||||
|
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| FCM-AC-01 | `docs/PRD.md`, `docs/TASKS.md`, docs-IA checklist, and example/profile inventory are approved before implementation. |
|
||||||
|
| FCM-AC-02 | YAML and JSON positive/negative, round-trip, unknown-field, enum, duplicate, topology, and property/fuzz tests prove schema/parser parity and canonical serialization. |
|
||||||
|
| FCM-AC-03 | Every class resolves through the existing profile/persona resolver; authority and lease tests enforce the normative matrix. |
|
||||||
|
| FCM-AC-04 | Every shipped example/profile has a CI-valid migrate/compatibility/retire disposition with no unresolved class at M1 exit. |
|
||||||
|
| FCM-AC-05 | Validate/show/plan are deterministic and non-mutating; JSON shapes and exit codes are contract-tested. |
|
||||||
|
| FCM-AC-06 | Generated/local env precedence, mode, digest, forbidden shadowing, command injection, quarantine, and no-value diagnostics pass independent security tests. |
|
||||||
|
| FCM-AC-07 | CRUD is generation-guarded, atomic/recoverable, idempotent, concurrency-tested, and creates stopped agents unless start is explicitly persisted. |
|
||||||
|
| FCM-AC-08 | Apply/lifecycle exactly implements the transition contract, including transient/persisted operations, reboot, partial failure, and rollback. |
|
||||||
|
| FCM-AC-09 | Remote/schema-only and unmanaged entries receive zero local lifecycle calls; local targeting covers named and default tmux sockets exactly. |
|
||||||
|
| FCM-AC-10 | Status/verify/doctor expose all state planes and actionable drift without secret, credential, or privileged command values. |
|
||||||
|
| FCM-AC-11 | v1 migration handles every mapped field, known/unknown observed state, aliases, env quarantine, current 9-managed/3-unmanaged synthetic fixture, canary, and rollback. |
|
||||||
|
| FCM-AC-12 | `fleet add/remove` compatibility aliases and v1 reads remain for the approved deprecation window while v2 writers emit only v2. |
|
||||||
|
| FCM-AC-13 | Package/install/update tests prove schema, tools, units, roles, docs, and examples ship together while site-owned state survives. |
|
||||||
|
| FCM-AC-14 | Fleet documentation checklist is complete, links/format/examples validate, and operator recovery procedures match tested behavior. |
|
||||||
|
| FCM-AC-15 | Independent correctness review, security review, validator certificate, terminal-green CI, and merge-gate approval complete before issue closure. |
|
||||||
|
|
||||||
|
### Risks and mitigations
|
||||||
|
|
||||||
|
| Risk | Mitigation / verification |
|
||||||
|
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Schema and parser drift create false validation | One executable contract or bidirectional parity tests; shared semantic resolver. |
|
||||||
|
| Apply starts intentionally stopped agents | Persist separate desired state; migration preserves known observed state; reboot/rollback tests. |
|
||||||
|
| Preserved env files become a hidden control plane | Generated-only unit input; strict local allowlist; shadow rejection and quarantine before start. |
|
||||||
|
| Command, secret, or credential values leak | Values never enter v2 output; key-name/hash-only diagnostics; adversarial security tests and review. |
|
||||||
|
| Stale artifacts cause destructive cleanup | Proof-of-ownership requirement; unmanaged/remote zero-call tests; deterministic plan before apply. |
|
||||||
|
| Concurrent writers or crashes corrupt roster | Lock, expected generation, fsync/rename, backup, failure injection, and recovery plan. |
|
||||||
|
| New compiler duplicates role logic | Hard prohibition on parallel resolver; parity tests across profile, provision, roster, migration, apply. |
|
||||||
|
| Control-plane naming confuses automation | Explicit local `mosaic fleet` versus gateway DB catalog documentation; no implicit mapping in M1–M5. |
|
||||||
|
| Legacy examples silently teach invalid classes | Complete disposition inventory and M1 CI exit gate. |
|
||||||
|
|
||||||
|
### Verification and milestone intent
|
||||||
|
|
||||||
|
- **M0:** requirements, authority, lifecycle, migration mapping, TASKS DAG, docs IA, and legacy inventory approved; no implementation or live mutation.
|
||||||
|
- **M1:** narrow v2 compiler, shared resolver, roles/aliases, validate/show/plan, and all shipped example/profile dispositions.
|
||||||
|
- **M2:** safe launch projection and generation-guarded atomic CRUD; command/credential quarantine proven before lifecycle.
|
||||||
|
- **M3:** local-only apply/lifecycle/status/verify/doctor implementing the full transition table.
|
||||||
|
- **M4:** field-complete v1 migration, compatibility window, orphan inventory, canary, and rollback.
|
||||||
|
- **M5:** accepted documentation IA, package/update checks, clean-install dogfood, independent correctness/security/validator evidence, and merge-gate release approval.
|
||||||
|
|
||||||
|
Detailed delivery dependencies and acceptance mappings are canonical in `docs/TASKS.md`. Documentation acceptance is tracked in [`docs/scratchpads/758-fleet-config-docs-ia-checklist.md`](scratchpads/758-fleet-config-docs-ia-checklist.md), and shipped artifact disposition is inventoried in [`docs/tasks/758-legacy-example-profile-disposition.md`](tasks/758-legacy-example-profile-disposition.md).
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Documentation Sitemap
|
# Documentation Sitemap
|
||||||
|
|
||||||
|
## Fleet declarative configuration management
|
||||||
|
|
||||||
|
- [Normative requirements](PRD.md#fleet-declarative-configuration-management-758) — issue #758 scope, authority, lifecycle, migration, acceptance, risks, and milestones.
|
||||||
|
- [M0–M5 delivery DAG](TASKS.md#w4--fleet-declarative-configuration-management-758) — one-card/one-PR implementation order and independent gates.
|
||||||
|
- [Documentation IA acceptance checklist](scratchpads/758-fleet-config-docs-ia-checklist.md) — required paths, owners, evidence, and exit checks.
|
||||||
|
- [Legacy example/profile disposition inventory](tasks/758-legacy-example-profile-disposition.md) — shipped artifacts and M1 migration decisions.
|
||||||
|
|
||||||
## Native Kanban and canonical task SOT
|
## Native Kanban and canonical task SOT
|
||||||
|
|
||||||
- [Canonical requirements](requirements/native-kanban-sot.md) — ratified P0–P3 requirements and acceptance criteria.
|
- [Canonical requirements](requirements/native-kanban-sot.md) — ratified P0–P3 requirements and acceptance criteria.
|
||||||
@@ -41,10 +48,3 @@
|
|||||||
- [Retention and deprecation evidence](tess/M5-MIGRATION-RETENTION-DEPRECATION.md)
|
- [Retention and deprecation evidence](tess/M5-MIGRATION-RETENTION-DEPRECATION.md)
|
||||||
- [Verification matrix](tess/VERIFICATION-MATRIX.md)
|
- [Verification matrix](tess/VERIFICATION-MATRIX.md)
|
||||||
- [Documentation checklist](tess/M5-003-DOCUMENTATION-CHECKLIST.md)
|
- [Documentation checklist](tess/M5-003-DOCUMENTATION-CHECKLIST.md)
|
||||||
- [Independent Option 2 runtime-portability qualification (2026-07-14)](tess/qualification/2026-07-14-option2-runtime-portability.md)
|
|
||||||
|
|
||||||
## Runtime-neutral Mos portability
|
|
||||||
|
|
||||||
- [Optional AI egress gateway ADR](architecture/ADR-MOS-EGRESS-GATEWAYS.md) — placement and gates for LiteLLM, Bifrost, and purpose-built translation proxies.
|
|
||||||
- [Runtime-neutral Mos identity and failover mission](https://git.mosaicstack.dev/mosaicstack/stack/issues/754)
|
|
||||||
- [Logical identity and connector lease/fencing implementation](https://git.mosaicstack.dev/mosaicstack/stack/issues/755)
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
|
|
||||||
## Workstream Rollup
|
## Workstream Rollup
|
||||||
|
|
||||||
| id | status | workstream | progress | tasks file | notes |
|
| id | status | workstream | progress | tasks file | notes |
|
||||||
| --- | ----------------- | ------------------------------ | ---------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
| --- | ----------------- | ------------------------ | ----------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||||
| W1 | planning-complete | Federation v1 (FED) | 0 / 7 milestones | [docs/federation/TASKS.md](./federation/TASKS.md) | M1 task breakdown populated; M2–M7 deferred to mission planning |
|
| W1 | planning-complete | Federation v1 (FED) | 0 / 7 milestones | [docs/federation/TASKS.md](./federation/TASKS.md) | M1 task breakdown populated; M2–M7 deferred to mission planning |
|
||||||
| W2 | planning-complete | Tess interaction agent | 0 / 5 milestones | [docs/tess/TASKS.md](./tess/TASKS.md) | Issue #706; independent planning gate PASS; M1 issue #707 ready |
|
| W2 | planning-complete | Tess interaction agent | 0 / 5 milestones | [docs/tess/TASKS.md](./tess/TASKS.md) | Issue #706; independent planning gate PASS; M1 issue #707 ready |
|
||||||
| W3 | planning-complete | Native Kanban/SOT | 0 / 4 phases | [docs/native-kanban-sot/TASKS.md](./native-kanban-sot/TASKS.md) | Issue #751; canon independently approved; implementation held until canon merges |
|
| W3 | planning-complete | Native Kanban/SOT | 0 / 4 phases | [docs/native-kanban-sot/TASKS.md](./native-kanban-sot/TASKS.md) | Issue #751; canon independently approved; implementation held until canon merges |
|
||||||
| W4 | planning-complete | Fleet configuration management | 0 / 12 cards | This file (§ Fleet configuration management #758) | Issue #758; M0 docs gate defines the implementation DAG before any fleet mutation |
|
| W4 | in-progress | Fleet declarative config | M0 / 6 milestones | [W4 DAG below](#w4--fleet-declarative-configuration-management-758) | Issue #758; M0 requirements/docs only; implementation blocked on M0 gates |
|
||||||
|
|
||||||
## Cross-Cutting Tracking
|
## Cross-Cutting Tracking
|
||||||
|
|
||||||
@@ -43,30 +43,6 @@ Active workstream is **W1 — Federation v1**. Workers should:
|
|||||||
2. Read [docs/federation/TASKS.md](./federation/TASKS.md) for the next pending task
|
2. Read [docs/federation/TASKS.md](./federation/TASKS.md) for the next pending task
|
||||||
3. Follow per-task agent + tier guidance from the workstream manifest
|
3. Follow per-task agent + tier guidance from the workstream manifest
|
||||||
|
|
||||||
## Fleet configuration management (#758) — M0–M5 implementation DAG
|
|
||||||
|
|
||||||
> **PRD:** [Fleet declarative configuration management](./PRD.md#fleet-declarative-configuration-management-workstream-fcm-758) · **M0 acceptance:** [docs IA checklist](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) · **baseline dispositions:** [legacy example/profile inventory](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md)
|
|
||||||
>
|
|
||||||
> Every row below is one independently reviewable card and **one PR**. `depends_on` is a
|
|
||||||
> hard DAG edge; no card may silently absorb another card's scope. All source cards require
|
|
||||||
> the repository quality gates, independent code and security review, terminal-green CI, and
|
|
||||||
> the applicable acceptance evidence before merge. Issue #758 remains open until M5 closes.
|
|
||||||
|
|
||||||
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
|
|
||||||
| ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------ | ----------------- | --------------------------------------- | ---------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| FCM-M0-001 | in-progress | Publish normative PRD requirements/acceptance criteria, this M0–M5 DAG, docs-IA checklist, and legacy example/profile disposition inventory; no implementation changes | #758 | sonnet | mosaicstack/stack | `docs/758-fleet-config-management` | — | 18K | M0 exit: approved docs; every shipped example/profile/service preset classified; docs-only PR |
|
|
||||||
| FCM-M1-001 | not-started | Implement narrow local-tmux v2 roster structural contract/compiler with YAML/JSON canonicalization and schema/parser parity tests | #758 | codex | mosaicstack/stack | `feat/758-roster-v2-compiler` | FCM-M0-001 | 30K | No lifecycle, remote, connector, secret, channel, or gateway work |
|
|
||||||
| FCM-M1-002 | not-started | Reuse existing profile/persona/provision resolver for roster semantics; add canonical class/authority validation and approved aliases | #758 | codex | mosaicstack/stack | `feat/758-shared-role-resolution` | FCM-M0-001 | 25K | Validator is certificate-only; merge-gate remains sole merge authority |
|
|
||||||
| FCM-M1-003 | not-started | Convert the M0 legacy inventory into executable example/profile/service-preset validation and explicit v1-version/retirement checks | #758 | codex | mosaicstack/stack | `test/758-example-profile-dispositions` | FCM-M1-001, FCM-M1-002 | 20K | Every shipped artifact must validate, be versioned v1, or be retired with replacement |
|
|
||||||
| FCM-M2-001 | not-started | Migrate generic launch chain to deterministic `.env.generated` plus strict data-only `.env.local`; quarantine forbidden legacy keys | #758 | codex | mosaicstack/stack | `feat/758-generated-env-boundary` | FCM-M1-001, FCM-M1-002 | 30K | No arbitrary command compatibility path; diagnostics expose key names/hashes only |
|
|
||||||
| FCM-M2-002 | not-started | Add generation-guarded local fleet agent create/get/update/delete mutations with plan/dry-run, atomic roster writes, and recovery output | #758 | codex | mosaicstack/stack | `feat/758-fleet-agent-crud` | FCM-M1-001, FCM-M2-001 | 30K | Fresh create persists stopped unless explicit persisted start |
|
|
||||||
| FCM-M3-001 | not-started | Implement local roster-owned reconcile/apply plus lifecycle/status/verify/doctor contracts and stable JSON/exit codes | #758 | codex | mosaicstack/stack | `feat/758-local-reconciler` | FCM-M2-001, FCM-M2-002 | 35K | Exact systemd/tmux ownership; remote/schema-only entries are inventory only |
|
|
||||||
| FCM-M3-002 | not-started | Add isolated systemd/tmux lifecycle, drift, socket, unmanaged-session, crash, and rollback acceptance coverage | #758 | sonnet | mosaicstack/stack | `test/758-reconciler-lifecycle-gates` | FCM-M3-001 | 25K | Proves stopped-state preservation and zero fuzzy destructive targeting |
|
|
||||||
| FCM-M4-001 | not-started | Implement field-complete v1-to-v2 inventory/preview/migrator with alias, lifecycle, env-quarantine, and remote/connector disposition evidence | #758 | codex | mosaicstack/stack | `feat/758-v1-v2-migrator` | FCM-M1-003, FCM-M3-001 | 35K | Preview first; no unreviewed lifecycle inference |
|
|
||||||
| FCM-M4-002 | not-started | Add reversible canary migration, rollback, stale-projection/orphan classification, and current-host 9-managed/3-unmanaged fixture coverage | #758 | sonnet | mosaicstack/stack | `test/758-migration-rollback-gates` | FCM-M4-001, FCM-M3-002 | 25K | Never starts a previously stopped agent or kills an unproven unmanaged session |
|
|
||||||
| FCM-M5-001 | not-started | Deliver the accepted fleet documentation IA, how-to/operations/migration references, and link/example validation | #758 | haiku | mosaicstack/stack | `docs/758-fleet-config-operator-docs` | FCM-M1-003, FCM-M2-002, FCM-M3-001, FCM-M4-001 | 24K | Must close every checklist item or record an approved deferral |
|
|
||||||
| FCM-M5-002 | not-started | Package/update asset-drift checks, rolling local canary, independent validation certificate, and release evidence | #758 | sonnet | mosaicstack/stack | `feat/758-fleet-config-release-gate` | FCM-M3-002, FCM-M4-002, FCM-M5-001 | 30K | Final #758 gate: quality, independent code/security review, validator certificate, merge-gate approval, green CI |
|
|
||||||
|
|
||||||
## Thin-core prompt diet (#528) — feat/contract-thin-core
|
## Thin-core prompt diet (#528) — feat/contract-thin-core
|
||||||
|
|
||||||
- Status: PR open, awaiting maintainer merge ratification (fleet-governing change).
|
- Status: PR open, awaiting maintainer merge ratification (fleet-governing change).
|
||||||
@@ -117,3 +93,64 @@ Active workstream is **W1 — Federation v1**. Workers should:
|
|||||||
## #633 — comms-block emitter + FLEET-LAUNCH runbook — feat/633-comms-block-runbook
|
## #633 — comms-block emitter + FLEET-LAUNCH runbook — feat/633-comms-block-runbook
|
||||||
|
|
||||||
- Status: implemented + tested (TDD). `mosaic fleet comms-block <role> [--host]` wraps resolveCommsBlock → readFleetCommsBlock; fails loud (stderr + exit 1) on unknown role / missing roster instead of silent empty. docs/fleet/FLEET-LAUNCH.md runbook: worker path + orchestrator .env fold (MOSAIC_AGENT_COMMAND; line-41 [-z] short-circuits line-44 yolo hardcode) + 3 launch gotchas + #632 preserve note + North-Star 4-field arc (harness ✅/model ✅ roster-native today; yolo + command/channels = PATH B #636). 177 fleet+comms tests green (6 new resolveCommsBlock cases). PATH A of the A→B→webUI arc. Detail: scratchpads/633-comms-block-runbook.md.
|
- Status: implemented + tested (TDD). `mosaic fleet comms-block <role> [--host]` wraps resolveCommsBlock → readFleetCommsBlock; fails loud (stderr + exit 1) on unknown role / missing roster instead of silent empty. docs/fleet/FLEET-LAUNCH.md runbook: worker path + orchestrator .env fold (MOSAIC_AGENT_COMMAND; line-41 [-z] short-circuits line-44 yolo hardcode) + 3 launch gotchas + #632 preserve note + North-Star 4-field arc (harness ✅/model ✅ roster-native today; yolo + command/channels = PATH B #636). 177 fleet+comms tests green (6 new resolveCommsBlock cases). PATH A of the A→B→webUI arc. Detail: scratchpads/633-comms-block-runbook.md.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## W4 — Fleet declarative configuration management (#758)
|
||||||
|
|
||||||
|
**Rules:** The table below is the canonical M0–M5 dependency DAG. Each delivery card owns one short-lived branch and one PR. Gate cards (`*-ROR`, `*-SEC`, `*-VAL`, `*-MERGE`) independently attest to the referenced delivery PR and do not author that PR. No implementation starts until `FCM-M0-MERGE` is complete. `done` requires merged PR, terminal-green CI, and linked tracking closure/evidence.
|
||||||
|
|
||||||
|
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
|
||||||
|
| ------------ | ----------- | -------------------------------------------------------------------------------------------------------------------- | ----- | ------ | ----- | ---------------------------------- | ----------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| FCM-M0-01 | in-progress | Ratify requirements, authority/lifecycle/migration decisions, DAG, docs IA checklist, and shipped artifact inventory | #758 | haiku | stack | docs/issue-758-m0 | — | 18K | One docs-only PR; maps FCM-AC-01; no source/schema/roles/examples/systemd/live changes |
|
||||||
|
| FCM-M0-ROR | not-started | Independent requirements/content review of M0 PR | #758 | sonnet | stack | — | FCM-M0-01 | 8K | Verify approved plan fidelity, DAG completeness, links, and every card→AC mapping; non-author attestation |
|
||||||
|
| FCM-M0-SEC | not-started | Independent security review of authority, quarantine, lifecycle, and migration requirements | #758 | sonnet | stack | — | FCM-M0-01 | 8K | Threat-model requirements only; verify no secret-value handling and no surprise-start path; non-author attestation |
|
||||||
|
| FCM-M0-VAL | not-started | Validator certificate for M0 acceptance baseline | #758 | sonnet | stack | — | FCM-M0-ROR, FCM-M0-SEC | 6K | Confirm FCM-AC-01 and no unresolved architecture blocker; validator cannot merge |
|
||||||
|
| FCM-M0-MERGE | not-started | Merge-gate approval and squash merge of M0 PR | #758 | haiku | stack | — | FCM-M0-VAL | 3K | Terminal-green CI required; unlocks implementation |
|
||||||
|
| FCM-M1-01 | not-started | Implement narrow v2 executable schema, canonical serialization, and schema/parser parity suite | #758 | codex | stack | feat/fcm-v2-contract | FCM-M0-MERGE | 30K | One PR; FCM-AC-02; structural validation only, no lifecycle mutation |
|
||||||
|
| FCM-M1-02 | not-started | Share existing profile/persona/provision resolver for roster semantic validation and topology policy | #758 | codex | stack | feat/fcm-shared-role-validation | FCM-M1-01 | 28K | One PR; FCM-AC-03; parallel resolver forbidden |
|
||||||
|
| FCM-M1-03 | not-started | Add/ratify validator, team-leader, interaction role contracts, aliases, authority, and lease tests | #758 | codex | stack | feat/fcm-role-authority | FCM-M1-02 | 24K | One PR; FCM-AC-03; merge-gate remains sole merger |
|
||||||
|
| FCM-M1-04 | not-started | Resolve every shipped example/profile disposition and add CI validation through shared contract/resolver | #758 | codex | stack | feat/fcm-example-profile-migration | FCM-M1-03 | 28K | One PR; FCM-AC-04; inventory rows cannot remain decision-required |
|
||||||
|
| FCM-M1-05 | not-started | Implement non-mutating config show, validate, and deterministic plan with stable JSON/exit codes | #758 | codex | stack | feat/fcm-config-read-plan | FCM-M1-02 | 32K | One PR; FCM-AC-05, FCM-AC-09, FCM-AC-10 |
|
||||||
|
| FCM-M1-DOC | not-started | Publish v2 fields, roles/leases, desired-vs-observed, and example/profile disposition docs | #758 | haiku | stack | docs/fcm-m1-contract | FCM-M1-03, FCM-M1-04, FCM-M1-05 | 16K | One PR; FCM-AC-14; update sitemap and docs checklist evidence |
|
||||||
|
| FCM-M1-ROR | not-started | Independent correctness review of all M1 delivery PRs | #758 | sonnet | stack | — | FCM-M1-01, FCM-M1-02, FCM-M1-03, FCM-M1-04, FCM-M1-05, FCM-M1-DOC | 14K | Exact-head reviews; schema/parser/resolver parity and docs checked |
|
||||||
|
| FCM-M1-SEC | not-started | Independent security review of validation, authority, aliases, and input hardening | #758 | sonnet | stack | — | FCM-M1-01, FCM-M1-02, FCM-M1-03, FCM-M1-04, FCM-M1-05 | 12K | Fuzz/injection/topology/policy findings must be resolved |
|
||||||
|
| FCM-M1-VAL | not-started | Validator certificate for M1 contract/compiler exit | #758 | sonnet | stack | — | FCM-M1-ROR, FCM-M1-SEC | 8K | Certify FCM-AC-02–05 and no mutation/lifecycle path |
|
||||||
|
| FCM-M1-MERGE | not-started | Merge-gate approval for M1 completion | #758 | haiku | stack | — | FCM-M1-VAL | 4K | All M1 PRs merged, terminal-green, inventory resolved |
|
||||||
|
| FCM-M2-01 | not-started | Implement deterministic mode-0600 `.env.generated` projection with generation/digest stamps | #758 | codex | stack | feat/fcm-generated-env | FCM-M1-MERGE | 30K | One PR; FCM-AC-06; no unit launch migration yet |
|
||||||
|
| FCM-M2-02 | not-started | Implement strict data-only `.env.local` parser, shadow rejection, and forbidden legacy-key quarantine | #758 | codex | stack | feat/fcm-local-env-quarantine | FCM-M2-01 | 34K | One PR; FCM-AC-06; never output values or privileged commands |
|
||||||
|
| FCM-M2-03 | not-started | Migrate generic unit/launcher to generated input with fail-closed digest validation | #758 | codex | stack | feat/fcm-launch-chain | FCM-M2-02 | 32K | One PR; FCM-AC-06; old `%i.env` cannot launch v2 |
|
||||||
|
| FCM-M2-04 | not-started | Implement generation-guarded atomic fleet-agent create/get/list/update/delete and compatibility aliases | #758 | codex | stack | feat/fcm-atomic-crud | FCM-M2-03 | 38K | One PR; FCM-AC-07, FCM-AC-12; create defaults stopped; no apply engine |
|
||||||
|
| FCM-M2-DOC | not-started | Publish generated-env chain, quarantine, and CRUD operator/developer guides | #758 | haiku | stack | docs/fcm-m2-projection-crud | FCM-M2-02, FCM-M2-03, FCM-M2-04 | 16K | One PR; FCM-AC-14; synthetic values only |
|
||||||
|
| FCM-M2-ROR | not-started | Independent correctness review of M2 projection and CRUD PRs | #758 | sonnet | stack | — | FCM-M2-01, FCM-M2-02, FCM-M2-03, FCM-M2-04, FCM-M2-DOC | 14K | Crash/concurrency/idempotency/permissions review |
|
||||||
|
| FCM-M2-SEC | not-started | Independent security review of launch chain, overrides, quarantine, paths, and diagnostics | #758 | sonnet | stack | — | FCM-M2-01, FCM-M2-02, FCM-M2-03, FCM-M2-04 | 16K | Adversarial shell/systemd/tmux/path/secret tests; FCM-AC-06–07 |
|
||||||
|
| FCM-M2-VAL | not-started | Validator certificate for M2 safe-projection/CRUD exit | #758 | sonnet | stack | — | FCM-M2-ROR, FCM-M2-SEC | 8K | Prove no hidden launch authority or surprise starts |
|
||||||
|
| FCM-M2-MERGE | not-started | Merge-gate approval for M2 completion | #758 | haiku | stack | — | FCM-M2-VAL | 4K | All M2 PRs merged and terminal-green |
|
||||||
|
| FCM-M3-01 | not-started | Implement locked local-only config apply with ordered convergence and machine-readable recovery | #758 | codex | stack | feat/fcm-local-apply | FCM-M2-MERGE | 40K | One PR; FCM-AC-08–10; zero calls for remote/unmanaged entries |
|
||||||
|
| FCM-M3-02 | not-started | Implement transient/persisted start, stop, restart, and fleet-wide lifecycle transitions | #758 | codex | stack | feat/fcm-lifecycle | FCM-M3-01 | 36K | One PR; FCM-AC-08–09; exact socket targeting |
|
||||||
|
| FCM-M3-03 | not-started | Implement status, verify, and doctor desired/observed/generation/drift/readiness contracts | #758 | codex | stack | feat/fcm-status-doctor | FCM-M3-01 | 30K | One PR; FCM-AC-09–10; safe effective output only |
|
||||||
|
| FCM-M3-04 | not-started | Add failure-injection, reboot/linger, unmanaged ownership, socket, and rollback integration suite | #758 | codex | stack | test/fcm-lifecycle-recovery | FCM-M3-02, FCM-M3-03 | 32K | One PR; FCM-AC-08–10 |
|
||||||
|
| FCM-M3-DOC | not-started | Publish CLI, lifecycle, status/drift, reconcile/recover, and systemd/tmux troubleshooting docs | #758 | haiku | stack | docs/fcm-m3-operations | FCM-M3-02, FCM-M3-03, FCM-M3-04 | 18K | One PR; FCM-AC-14 |
|
||||||
|
| FCM-M3-ROR | not-started | Independent correctness review of M3 lifecycle/recovery PRs | #758 | sonnet | stack | — | FCM-M3-01, FCM-M3-02, FCM-M3-03, FCM-M3-04, FCM-M3-DOC | 16K | Exact targeting, state transitions, recovery ordering |
|
||||||
|
| FCM-M3-SEC | not-started | Independent security review of apply/lifecycle authority and unmanaged-resource protection | #758 | sonnet | stack | — | FCM-M3-01, FCM-M3-02, FCM-M3-03, FCM-M3-04 | 16K | Policy denial, injection, TOCTOU, no-value output |
|
||||||
|
| FCM-M3-VAL | not-started | Validator certificate for M3 local lifecycle exit | #758 | sonnet | stack | — | FCM-M3-ROR, FCM-M3-SEC | 10K | Certify full transition table and FCM-AC-08–10 |
|
||||||
|
| FCM-M3-MERGE | not-started | Merge-gate approval for M3 completion | #758 | haiku | stack | — | FCM-M3-VAL | 4K | All M3 PRs merged and terminal-green |
|
||||||
|
| FCM-M4-01 | not-started | Implement field-complete v1 inventory, preview, aliases, unsupported-field reporting, and v2 writer | #758 | codex | stack | feat/fcm-v1-migrator | FCM-M3-MERGE | 38K | One PR; FCM-AC-11–12; no mutation without `--write` |
|
||||||
|
| FCM-M4-02 | not-started | Implement observed-state preservation, canary cutover, orphan classification, and reversible rollback | #758 | codex | stack | feat/fcm-migration-cutover | FCM-M4-01 | 40K | One PR; FCM-AC-11; unknown state blocks; stopped stays stopped |
|
||||||
|
| FCM-M4-03 | not-started | Add synthetic 9-managed/3-unmanaged migration, env quarantine, upgrade, and rollback E2E fixtures | #758 | codex | stack | test/fcm-migration-e2e | FCM-M4-02 | 34K | One PR; FCM-AC-11–12; no real credential/live-host data |
|
||||||
|
| FCM-M4-DOC | not-started | Publish v1→v2 field map, aliases, example disposition, backup/restore, and migration runbook | #758 | haiku | stack | docs/fcm-m4-migration | FCM-M4-01, FCM-M4-02, FCM-M4-03 | 18K | One PR; FCM-AC-14 |
|
||||||
|
| FCM-M4-ROR | not-started | Independent correctness review of M4 migration/cutover PRs | #758 | sonnet | stack | — | FCM-M4-01, FCM-M4-02, FCM-M4-03, FCM-M4-DOC | 16K | Field completeness, state preservation, rollback fidelity |
|
||||||
|
| FCM-M4-SEC | not-started | Independent security review of migration inventory, quarantine, and cutover | #758 | sonnet | stack | — | FCM-M4-01, FCM-M4-02, FCM-M4-03 | 16K | Secret-safe reporting and non-destructive ownership proof |
|
||||||
|
| FCM-M4-VAL | not-started | Validator certificate for M4 compatibility/migration exit | #758 | sonnet | stack | — | FCM-M4-ROR, FCM-M4-SEC | 10K | Certify FCM-AC-11–12 and rollback evidence |
|
||||||
|
| FCM-M4-MERGE | not-started | Merge-gate approval for M4 completion | #758 | haiku | stack | — | FCM-M4-VAL | 4K | All M4 PRs merged and terminal-green |
|
||||||
|
| FCM-M5-01 | not-started | Complete fleet documentation IA, sitemap, validated examples, and checklist evidence | #758 | haiku | stack | docs/fcm-complete-ia | FCM-M4-MERGE | 28K | One PR; FCM-AC-14; no required checklist item incomplete |
|
||||||
|
| FCM-M5-02 | not-started | Add package/install/update asset-drift and site-owned-state preservation qualification | #758 | codex | stack | test/fcm-package-upgrade | FCM-M4-MERGE | 32K | One PR; FCM-AC-13 |
|
||||||
|
| FCM-M5-03 | not-started | Run clean-home install, cold-start, local canary, rolling restart, failure, and rollback qualification | #758 | codex | stack | test/fcm-dogfood-qualification | FCM-M5-01, FCM-M5-02 | 30K | One PR; FCM-AC-08, FCM-AC-11, FCM-AC-13; synthetic harness/evidence only; never mutate production fleet |
|
||||||
|
| FCM-M5-ROR | not-started | Independent final correctness and documentation review | #758 | sonnet | stack | — | FCM-M5-01, FCM-M5-02, FCM-M5-03 | 16K | Verify FCM-AC-01–14 evidence and docs links/examples |
|
||||||
|
| FCM-M5-SEC | not-started | Independent final security review and threat-gate closure | #758 | sonnet | stack | — | FCM-M5-01, FCM-M5-02, FCM-M5-03 | 18K | Review launch/migration/lifecycle authority, secret handling, recovery |
|
||||||
|
| FCM-M5-VAL | not-started | Ultron/validator final acceptance certificate | #758 | sonnet | stack | — | FCM-M5-ROR, FCM-M5-SEC | 12K | Independent certificate for FCM-AC-01–15; no merge authority |
|
||||||
|
| FCM-M5-MERGE | not-started | Merge-gate final approve-to-land, terminal CI verification, issue closure, and release handoff | #758 | haiku | stack | — | FCM-M5-VAL | 6K | Sole merge path; FCM-AC-15; squash merge and close #758 after green CI |
|
||||||
|
|
||||||
|
### W4 acceptance mapping check
|
||||||
|
|
||||||
|
Every delivery card maps to at least one `FCM-AC-*` criterion in its notes. Gate cards verify those mappings rather than introducing implementation. The detailed documentation checklist is [`docs/scratchpads/758-fleet-config-docs-ia-checklist.md`](scratchpads/758-fleet-config-docs-ia-checklist.md); the shipped artifact inventory is [`docs/tasks/758-legacy-example-profile-disposition.md`](tasks/758-legacy-example-profile-disposition.md).
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
# ADR: Optional AI egress gateways for runtime-neutral Mos
|
|
||||||
|
|
||||||
**Status:** Proposed for controlled prototypes; not approved as Mosaic core
|
|
||||||
|
|
||||||
**Date:** 2026-07-14
|
|
||||||
|
|
||||||
**Issues:** #754, #755
|
|
||||||
|
|
||||||
**Decision owner:** Mosaic Gateway / provider-adapter architecture
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The emergency Mos continuity path kept Claude Code as the harness and translated Anthropic Messages traffic to Codex OAuth through a small localhost proxy. That preserved the existing Claude Discord plugin and transcript, but exposed two architectural facts:
|
|
||||||
|
|
||||||
1. Harness identity, channel entitlement, provider credentials, and inference transport are separate concerns.
|
|
||||||
2. A generic AI gateway can improve provider routing, budgets, and observability, but must not become Mosaic's identity, authorization, tenant, or orchestration boundary.
|
|
||||||
|
|
||||||
The Tess qualification report also found that current provider rebinding is not identity-continuous failover. Mosaic still needs a logical agent identity, durable connector lease/fencing, canonical handoff/checkpoint, exactly-once receipts, concrete harness adapters, and cross-harness rollback E2E.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
Mosaic MAY support LiteLLM, Bifrost, the purpose-built Claude/Codex proxy, or future gateways as optional egress implementations behind `IProviderAdapter` / `AgentRuntimeProvider`.
|
|
||||||
|
|
||||||
Mosaic Gateway remains authoritative for:
|
|
||||||
|
|
||||||
- authenticated actor and tenant identity;
|
|
||||||
- logical agent identity and connector binding;
|
|
||||||
- authorization, approval, and policy;
|
|
||||||
- lease epoch and stale-holder fencing;
|
|
||||||
- audit correlation and redaction;
|
|
||||||
- canonical handoff/checkpoint state;
|
|
||||||
- idempotency and side-effect receipts.
|
|
||||||
|
|
||||||
An egress gateway MUST NOT:
|
|
||||||
|
|
||||||
- receive channel ingress directly;
|
|
||||||
- authorize tools or connector ownership;
|
|
||||||
- define Mosaic tenant or agent identity;
|
|
||||||
- persist raw Mosaic handoffs or channel credentials;
|
|
||||||
- bypass adapter capability negotiation;
|
|
||||||
- silently fail over when policy, lease, or provider health is uncertain.
|
|
||||||
|
|
||||||
Allowed topology:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Discord / Matrix / CLI / web
|
|
||||||
↓
|
|
||||||
Mosaic Gateway: identity, authz, lease/fence, approvals, audit
|
|
||||||
↓
|
|
||||||
IProviderAdapter / AgentRuntimeProvider
|
|
||||||
↓
|
|
||||||
optional egress gateway
|
|
||||||
↓
|
|
||||||
upstream provider or subscription-backed OAuth session
|
|
||||||
```
|
|
||||||
|
|
||||||
## Candidate assessment
|
|
||||||
|
|
||||||
### Purpose-built `raine/claude-code-proxy`
|
|
||||||
|
|
||||||
**Disposition:** Approved only for the verified emergency localhost bridge.
|
|
||||||
|
|
||||||
Strengths:
|
|
||||||
|
|
||||||
- explicit Codex device OAuth flow;
|
|
||||||
- small operational surface;
|
|
||||||
- Anthropic Messages translation suitable for Claude Code;
|
|
||||||
- model and reasoning-effort enforcement;
|
|
||||||
- straightforward loopback systemd supervision and rollback.
|
|
||||||
|
|
||||||
Constraints:
|
|
||||||
|
|
||||||
- not a Mosaic multi-tenant control plane;
|
|
||||||
- Claude built-in channels still depend on Claude subscription entitlement and feature lookup;
|
|
||||||
- model aliases can obscure the upstream model unless proxy policy/logs are treated as evidence;
|
|
||||||
- no replacement for connector leasing, canonical handoff, or exactly-once effects.
|
|
||||||
|
|
||||||
### LiteLLM
|
|
||||||
|
|
||||||
**Disposition:** Candidate for a formal adapter-only prototype and terms/security review.
|
|
||||||
|
|
||||||
Current documentation states that ChatGPT subscription access is available through an OAuth device-code flow. LiteLLM also provides broad provider routing, virtual keys, budgets, observability, and OpenAI/Anthropic-compatible surfaces.
|
|
||||||
|
|
||||||
Required prototype gates:
|
|
||||||
|
|
||||||
- verify the exact ChatGPT subscription OAuth flow and supported models against current provider terms;
|
|
||||||
- document token location, encryption, revocation, refresh, scope, and incident response;
|
|
||||||
- prove tenant isolation and prevent virtual keys from becoming Mosaic principals;
|
|
||||||
- verify streaming, tool calls, reasoning controls, cancellation, and idempotency metadata;
|
|
||||||
- fail closed instead of selecting an unhealthy provider merely to return a result;
|
|
||||||
- demonstrate that Mosaic audit correlation survives gateway retries/failover;
|
|
||||||
- keep channel ingress and connector credentials outside LiteLLM.
|
|
||||||
|
|
||||||
Source references:
|
|
||||||
|
|
||||||
- [LiteLLM ChatGPT subscription provider](https://docs.litellm.ai/docs/providers/chatgpt)
|
|
||||||
- [LiteLLM providers](https://docs.litellm.ai/docs/providers)
|
|
||||||
|
|
||||||
### Bifrost
|
|
||||||
|
|
||||||
**Disposition:** Candidate for governance/routing research; subscription OAuth compatibility unverified.
|
|
||||||
|
|
||||||
Useful concepts include virtual keys, budgets, rate limits, weighted load balancing, and automatic provider failover. Those features may inform Mosaic egress policy, but Bifrost virtual keys are downstream credentials—not Mosaic actors or tenants.
|
|
||||||
|
|
||||||
Required prototype gates:
|
|
||||||
|
|
||||||
- verify Codex/ChatGPT subscription OAuth rather than assuming API-key compatibility;
|
|
||||||
- map budgets and virtual keys to server-derived Mosaic tenants without duplicating authority;
|
|
||||||
- prove failover does not violate connector lease, approval, or exactly-once semantics;
|
|
||||||
- ensure request/response logs are redacted before persistence;
|
|
||||||
- disable or constrain automatic failover when policy or side-effect state is ambiguous.
|
|
||||||
|
|
||||||
Source references:
|
|
||||||
|
|
||||||
- [Bifrost overview](https://docs.getbifrost.ai/overview)
|
|
||||||
- [Bifrost repository](https://github.com/maximhq/bifrost)
|
|
||||||
|
|
||||||
### `teremterem/claude-code-gpt-5-codex`
|
|
||||||
|
|
||||||
**Disposition:** Not selected as the emergency implementation; useful as a historical LiteLLM recipe.
|
|
||||||
|
|
||||||
The reviewed repository uses `OPENAI_API_KEY`, tells previously authenticated Claude users to log out, and documents a Claude Web Search schema incompatibility. Logging Claude out conflicts with the channel-entitlement requirement observed in the live Mos cutover. The repository therefore does not, as provided, satisfy subscription-OAuth plus built-in-channel continuity.
|
|
||||||
|
|
||||||
Source references:
|
|
||||||
|
|
||||||
- [Repository](https://github.com/teremterem/claude-code-gpt-5-codex)
|
|
||||||
- [Environment template](https://github.com/teremterem/claude-code-gpt-5-codex/blob/main/.env.template)
|
|
||||||
|
|
||||||
## Security consequences
|
|
||||||
|
|
||||||
- Subscription OAuth grants are high-value credentials and require the same lifecycle controls as service credentials.
|
|
||||||
- Downstream virtual keys reduce provider-key exposure but do not establish user, tenant, or agent authority.
|
|
||||||
- Automatic retry/failover can duplicate tool or external side effects unless Mosaic owns operation IDs and receipts.
|
|
||||||
- Gateway telemetry can contain prompts, tool schemas, and model output; redaction and retention policy must apply before persistence.
|
|
||||||
- A localhost unauthenticated translation endpoint must remain loopback-only and process-isolated.
|
|
||||||
|
|
||||||
## Acceptance before production use
|
|
||||||
|
|
||||||
1. Threat model and provider-terms review approved.
|
|
||||||
2. Credential lifecycle and revocation drill documented and exercised.
|
|
||||||
3. Adapter contract tests pass for streaming, tools, cancellation, reasoning policy, errors, and audit correlation.
|
|
||||||
4. Tenant-bound authorization remains entirely in Mosaic Gateway.
|
|
||||||
5. Failure injection proves no duplicate side effects across retries or provider failover.
|
|
||||||
6. Rollback to the prior provider path is exercised.
|
|
||||||
7. Independent code and security reviews approve the exact deployed revision.
|
|
||||||
|
|
||||||
## Follow-up
|
|
||||||
|
|
||||||
- #754 owns cross-harness logical identity, checkpoint, receipt, adapter, and failover work.
|
|
||||||
- #755 / PR #757 implements the first logical identity and connector lease/fencing boundary.
|
|
||||||
- A later issue should prototype LiteLLM and Bifrost behind the provider adapter after #755 is merged and independently qualified.
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Fleet Configuration Management — Documentation IA Acceptance Checklist
|
|
||||||
|
|
||||||
**Issue:** #758 · **Scope:** M0 documentation gate for the local fleet declarative-configuration program.
|
|
||||||
|
|
||||||
This checklist is an acceptance contract for documentation and examples. It does not authorize
|
|
||||||
schema, runtime, systemd, role, profile, or live-fleet changes. An item is complete only when its
|
|
||||||
named artifact exists, is linked from the fleet documentation entry point, and its evidence is
|
|
||||||
recorded in the M0 task/PR.
|
|
||||||
|
|
||||||
## M0 baseline acceptance
|
|
||||||
|
|
||||||
- [ ] `docs/PRD.md` states the roster as desired-state SSOT; generated environment, systemd,
|
|
||||||
tmux, and heartbeat artifacts as non-authoritative projections; and fail-closed handling of
|
|
||||||
unsupported or quarantined legacy input.
|
|
||||||
- [ ] `docs/PRD.md` defines the required classes and authority boundary: `validator` certifies but
|
|
||||||
does not merge; `merge-gate` remains sole approve-to-land/merge authority; `team-leader`
|
|
||||||
capacity is lease-bounded; `interaction` is request/status only; instance names such as Tess
|
|
||||||
and Ultron remain configurable.
|
|
||||||
- [ ] `docs/PRD.md` defines local lifecycle semantics for `enabled`, persisted desired state, and
|
|
||||||
observed state, including stopped-state preservation through migration, apply, and reboot.
|
|
||||||
- [ ] `docs/PRD.md` defines the generated-env/local-override boundary, explicitly denies arbitrary
|
|
||||||
command overrides in M1–M5, and requires key-name/hash-only quarantine diagnostics.
|
|
||||||
- [ ] `docs/PRD.md` identifies the M1–M5 local-tmux scope and excludes remote reconciliation,
|
|
||||||
connector mutation, secret references, arbitrary commands/channels, gateway convergence, and
|
|
||||||
UI configuration storage.
|
|
||||||
- [ ] `docs/TASKS.md` contains the complete M0–M5 one-card/one-PR dependency DAG for #758 with
|
|
||||||
agent tier, branch, dependency, estimate, and evidence expectations.
|
|
||||||
- [ ] `docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md` classifies every current shipped
|
|
||||||
fleet example, profile, and service preset before M1 implementation starts.
|
|
||||||
|
|
||||||
## Required documentation IA for M1–M5
|
|
||||||
|
|
||||||
| Path | Minimum content | Delivery gate |
|
|
||||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- |
|
|
||||||
| `docs/fleet/README.md` | Fleet configuration entry point, desired-vs-observed decision tree, link map | M5 |
|
|
||||||
| `docs/fleet/concepts/desired-vs-observed-state.md` | SSOT/projection model, drift, generation and ownership | M5 |
|
|
||||||
| `docs/fleet/concepts/identity-class-runtime.md` | Stable name, display alias, class, runtime/provider/model separation | M5 |
|
|
||||||
| `docs/fleet/concepts/role-authority-and-leases.md` | Required roles, validator/merge-gate separation, lease limits | M5 |
|
|
||||||
| `docs/fleet/concepts/generated-env-launch-chain.md` | Generated/local files, precedence, quarantine and non-shell parsing | M5 |
|
|
||||||
| `docs/fleet/reference/roster-v2.schema.json` | Executable v2 structural contract | M1 |
|
|
||||||
| `docs/fleet/reference/roster-v2-fields.md` | Every field, default, constraint, compatibility behavior and examples | M1 |
|
|
||||||
| `docs/fleet/reference/cli.md` | `config`, `agent`, lifecycle, plan/apply, JSON and exit-code contracts | M2–M3 |
|
|
||||||
| `docs/fleet/reference/role-classes.md` | Canonical classes, aliases, authority matrix and instance-name rule | M1 |
|
|
||||||
| `docs/fleet/reference/lifecycle-transitions.md` | Create/start/stop/restart/apply/reboot/rollback transition table | M3 |
|
|
||||||
| `docs/fleet/reference/status-and-drift.md` | Desired/observed/managed state, orphans, revision mismatch, doctor output | M3 |
|
|
||||||
| `docs/fleet/how-to/create-update-delete-agent.md` | Safe CRUD, expected generation, dry-run and rollback | M2 |
|
|
||||||
| `docs/fleet/how-to/start-stop-restart.md` | Persisted versus one-shot lifecycle actions | M3 |
|
|
||||||
| `docs/fleet/how-to/configure-tess-interaction.md` | Configurable interaction instance; no hardcoded identity | M5 |
|
|
||||||
| `docs/fleet/how-to/configure-ultron-validator.md` | Configurable validator instance; no merge authority | M5 |
|
|
||||||
| `docs/fleet/how-to/customize-roles.md` | Existing baseline + `roles.local` resolution and validation | M1 |
|
|
||||||
| `docs/fleet/operations/reconcile-and-recover.md` | Plan/apply failure recovery, generation lock and canary rollout | M3 |
|
|
||||||
| `docs/fleet/operations/env-quarantine.md` | Legacy-key inventory, private quarantine and redaction behavior | M2 |
|
|
||||||
| `docs/fleet/operations/systemd-tmux-troubleshooting.md` | Socket ambiguity, ownership proof, systemd/tmux drift | M3 |
|
|
||||||
| `docs/fleet/operations/backup-restore.md` | Roster/projection backup and rollback boundaries | M4 |
|
|
||||||
| `docs/fleet/operations/upgrade-assets.md` | Source-vs-installed asset revision detection and safe refresh | M5 |
|
|
||||||
| `docs/fleet/migration/v1-to-v2.md` | Normative field map, observed-state preservation and rollback | M4 |
|
|
||||||
| `docs/fleet/migration/example-profile-disposition.md` | Final disposition of every shipped example/profile | M1–M4 |
|
|
||||||
| `docs/fleet/migration/legacy-class-aliases.md` | Alias, unresolved-class, and retirement rules | M1 |
|
|
||||||
|
|
||||||
## PRD acceptance-criteria mapping
|
|
||||||
|
|
||||||
| PRD acceptance criterion | Owning card(s) | Required evidence |
|
|
||||||
| ------------------------------------------------------------ | ---------------------------------- | --------------------------------------------------------------------------------------- |
|
|
||||||
| `AC-FCM-01` schema, semantic validation, canonical rendering | FCM-M1-001, FCM-M1-002 | YAML/JSON positive/negative and schema/parser/resolver parity tests |
|
|
||||||
| `AC-FCM-02` deterministic plan and no-mutation check | FCM-M3-001 | Stable JSON/exit-code and desired-versus-observed fixture tests |
|
|
||||||
| `AC-FCM-03` safe generation-guarded CRUD | FCM-M2-002 | Create/update/delete idempotency, expected-generation, dry-run, and recovery tests |
|
|
||||||
| `AC-FCM-04` generated/local boundary and quarantine | FCM-M2-001 | Launch-chain, shadow, injection, redaction, and forbidden-key tests |
|
|
||||||
| `AC-FCM-05` lifecycle/reconcile/socket/drift safety | FCM-M3-001, FCM-M3-002 | Isolated systemd/tmux, stopped-state, orphan, socket, and rollback evidence |
|
|
||||||
| `AC-FCM-06` v1 migration and example/profile disposition | FCM-M4-001, FCM-M4-002, FCM-M1-003 | Preview/canary/rollback fixture plus executable disposition inventory |
|
|
||||||
| `AC-FCM-07` authority and lease boundaries | FCM-M1-002 | Role/authority/lease denial tests and resolved role contracts |
|
|
||||||
| `AC-FCM-08` documentation and final release gate | FCM-M5-001, FCM-M5-002 | Checklist closure, link/example validation, reviews, certificate, and terminal-green CI |
|
|
||||||
|
|
||||||
## Cross-cutting evidence gates
|
|
||||||
|
|
||||||
- [ ] Every retained or migrated YAML/JSON example, profile, and service preset validates through the
|
|
||||||
same executable schema and shared baseline-plus-`roles.local` resolver used by the CLI.
|
|
||||||
- [ ] Every retired example/profile/service preset has a replacement link and deprecation note; no
|
|
||||||
unresolved legacy class or tool-policy alias remains silently shipped.
|
|
||||||
- [ ] Documentation examples contain no secret values, arbitrary command override, or product-hardcoded
|
|
||||||
Tess/Ultron identity.
|
|
||||||
- [ ] CLI snippets distinguish local fleet desired-state commands from the separate gateway-backed
|
|
||||||
`mosaic agent` catalog.
|
|
||||||
- [ ] Migration, quarantine, lifecycle, status, and troubleshooting documentation state that values of
|
|
||||||
legacy sensitive keys are never printed.
|
|
||||||
- [ ] M5 release review verifies links, schema/example validation, and that all checklist rows have
|
|
||||||
owner/evidence or an explicit approved deferral.
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# Fleet Configuration Management — Legacy Example, Profile, and Service Disposition Inventory
|
|
||||||
|
|
||||||
**Issue:** #758 · **Baseline:** `origin/main` `49e8a541` · **Status:** M0 inventory; no source
|
|
||||||
examples or profiles are changed by this document.
|
|
||||||
|
|
||||||
The v2 compiler may not silently accept an unresolved class. Before M1 exits, every shipped file
|
|
||||||
below must be either migrated and executable, retained as an explicitly versioned v1 fixture, or
|
|
||||||
retired with a replacement/deprecation note. Class resolution must use the existing
|
|
||||||
profile/persona/provision baseline-plus-`roles.local` resolver; this inventory does not create a
|
|
||||||
parallel resolver.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
| Shipped file | Current class evidence | M0 disposition decision | Required M1/M4 evidence |
|
|
||||||
| ---------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
||||||
| `framework/fleet/examples/coding.yaml` | `orchestrator`, `enhancer`, `implementer`, `reviewer` | Migrate: `implementer → code`, `reviewer → review`; retain orchestration/enhancer intent | v2 fixture validates; role aliases and authority matrix tested |
|
|
||||||
| `framework/fleet/examples/general.yaml` | `orchestrator`, `enhancer`, `worker` | Migrate only after operator chooses a concrete canonical role for `worker`; no implicit conversion | Explicit replacement class, or versioned v1 fixture/retirement note |
|
|
||||||
| `framework/fleet/examples/hybrid.yaml` | `orchestrator`, `enhancer`, `implementer`, `researcher`, `reviewer` | Migrate aliases; resolve `researcher` through existing role resolver or retain/version | Shared resolver validation; no ad-hoc class scanner |
|
|
||||||
| `framework/fleet/examples/local-canary.yaml` | `orchestrator`, `implementer`, `reviewer` | Migrate aliases; preserve its local-tmux canary purpose | v2 fixture validates and preserves safe stopped/running behavior |
|
|
||||||
| `framework/fleet/examples/minimal.yaml` | `canary` | Retire or version as v1 unless an existing canonical role contract is selected deliberately | Replacement link/deprecation note or CI-valid v1 fixture |
|
|
||||||
| `framework/fleet/examples/operator-interaction.yaml` | `operator-interaction` | Migrate alias to `interaction`; preserve instance/display name as configuration, not schema identity | v2 interaction fixture validates; no Tess literal is required |
|
|
||||||
| `framework/fleet/examples/research.yaml` | `orchestrator`, `enhancer`, `researcher`, `analyst` | Resolve `researcher`/`analyst` through baseline + `roles.local`, or version/retire | Resolver evidence and explicit disposition for each unresolved class |
|
|
||||||
|
|
||||||
## Profiles
|
|
||||||
|
|
||||||
| Shipped file | Current class evidence | M0 disposition decision | Required M1/M4 evidence |
|
|
||||||
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
||||||
| `framework/fleet/profiles/business.yaml` | `ceo`, `coo`, `cfo`, `product-manager`, `marketing-lead`, `sales-lead`, `operations-manager`, `customer-success-manager`, `code`, `review` | Retain only if every class resolves through the existing role library/`roles.local`; otherwise version/retire rather than weakening validation | Shared resolver CI result for every class; documented role source or replacement |
|
|
||||||
| `framework/fleet/profiles/marketing.yaml` | `marketing-lead`, `content-strategist`, `copywriter`, `seo-specialist`, `social-media-manager`, `brand-strategist`, `growth-marketer`, `ux-designer` | Same resolver-or-version/retire rule | Per-class resolver CI result and replacement/deprecation record if unresolved |
|
|
||||||
| `framework/fleet/profiles/personal-assistant.yaml` | `personal-assistant`, `executive-assistant`, `scheduler`, `inbox-manager`, `researcher` | Same resolver-or-version/retire rule | Per-class resolver CI result; do not infer `interaction` equivalence |
|
|
||||||
| `framework/fleet/profiles/research.yaml` | `lead-researcher`, `researcher`, `data-analyst`, `data-scientist`, `market-analyst`, `documentation`, `review` | Same resolver-or-version/retire rule | Per-class resolver CI result and explicit compatibility posture |
|
|
||||||
| `framework/fleet/profiles/software-delivery.yaml` | `orchestrator`, `board`, `planner`, `decomposition`, `code`, `review`, `security-review`, `site-tester`, `documentation`, `merge-gate`, `rebase`, `operator`, `session-review`, `enhancer` | Retain as the governance reference; add `validator`, `team-leader`, and `interaction` only through approved role/profile work, not silent substitution | CI validates all current classes; separate fixture proves required M1 authority seats |
|
|
||||||
|
|
||||||
## Service presets
|
|
||||||
|
|
||||||
| Shipped file | Current policy evidence | M0 disposition decision | Required M1/M4 evidence |
|
|
||||||
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `framework/fleet/services/operator-interaction.yaml` | Generic policy only: `runtime: pi`, `model: openai/gpt-5.6-sol`, `reasoning: high`, `tool_policy: operator-interaction`; provisioning supplies the agent name as data | Retain as a generic service policy, not a Tess identity. Migrate `tool_policy: operator-interaction` only through the approved interaction tool-policy alias/semantic resolver; do not infer a class or machine name from this file. | Service-policy fixture validates runtime/model/reasoning and alias behavior; generic provisioning proves a configured interaction instance is supplied without a hardcoded Tess name. |
|
|
||||||
|
|
||||||
## Required disposition controls
|
|
||||||
|
|
||||||
1. **No silent aliasing:** only `implementer → code`, `reviewer → review`, and
|
|
||||||
`operator-interaction → interaction` are approved deterministic aliases in this M0 baseline.
|
|
||||||
`worker`, `analyst`, `canary`, and domain-specific classes require resolver evidence or an
|
|
||||||
explicit version/retirement decision.
|
|
||||||
2. **No identity hardcoding:** Tess and Ultron are optional instance/display names. An example/profile
|
|
||||||
may demonstrate the capability but must not make a product name a required class or machine ID.
|
|
||||||
3. **No lifecycle inference from an example:** examples describe desired configuration only; migration
|
|
||||||
of an installed v1 roster separately preserves observed stopped/running state.
|
|
||||||
4. **No secret or command migration:** examples/profiles must not introduce credential values or
|
|
||||||
`MOSAIC_AGENT_COMMAND`; those legacy keys are M2 quarantine inputs, never v2 authoring fields.
|
|
||||||
5. **Service presets are included:** service policies are inventoried alongside examples/profiles.
|
|
||||||
They may express launch/tool policy, but do not create a class, a canonical agent identity, or a
|
|
||||||
second validation path.
|
|
||||||
6. **Evidence is executable:** M1/M4 CI must enumerate these exact files, validate retained/migrated
|
|
||||||
inputs through the shared resolver, and fail if a file lacks its documented disposition.
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# Fleet Roster v2 Structural Contract
|
|
||||||
|
|
||||||
**Status:** FCM-M1-001 local-tmux structural compiler contract. This document describes parsing,
|
|
||||||
strict structural validation, normalized in-memory representation, and deterministic rendering only.
|
|
||||||
It does not authorize role resolution, lifecycle reconciliation, mutation, migration, remote
|
|
||||||
placement, connector configuration, secret references, arbitrary commands, channels, gateway
|
|
||||||
mapping, or any live-fleet change.
|
|
||||||
|
|
||||||
The executable schema is [`roster-v2.schema.json`](./roster-v2.schema.json). The compiler exports
|
|
||||||
the same schema and its test parses this file and compares it structurally with the executable contract.
|
|
||||||
|
|
||||||
## Format and canonical shape
|
|
||||||
|
|
||||||
The compiler accepts YAML or JSON. It reads only snake_case source fields and renders canonical,
|
|
||||||
snake_case YAML. Rendering sorts runtime keys and agents by stable name. Agent names, class names,
|
|
||||||
and tool-policy names are structural identifiers; whether a class or policy resolves is a later
|
|
||||||
shared-resolver concern.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: 2
|
|
||||||
generation: 1
|
|
||||||
transport: tmux
|
|
||||||
tmux:
|
|
||||||
socket_name: mosaic-fleet
|
|
||||||
holder_session: _holder
|
|
||||||
defaults:
|
|
||||||
working_directory: ~/src
|
|
||||||
runtime: pi
|
|
||||||
runtimes:
|
|
||||||
pi:
|
|
||||||
reset_command: /new
|
|
||||||
agents:
|
|
||||||
- name: coder0
|
|
||||||
alias: Coder 0
|
|
||||||
class: code
|
|
||||||
runtime: pi
|
|
||||||
provider: openai
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
reasoning: high
|
|
||||||
tool_policy: code
|
|
||||||
working_directory: ~/src
|
|
||||||
persistent_persona: false
|
|
||||||
reset_between_tasks: true
|
|
||||||
lifecycle:
|
|
||||||
enabled: true
|
|
||||||
desired_state: stopped
|
|
||||||
launch:
|
|
||||||
yolo: true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Root fields
|
|
||||||
|
|
||||||
| Field | Required | Constraint | Meaning |
|
|
||||||
| ------------ | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `version` | yes | integer constant `2` | Identifies this contract. Version `1` is explicitly rejected by this compiler and remains on the existing v1 path until M4 migration. |
|
|
||||||
| `generation` | yes | positive safe integer | Desired-state generation. M2 uses it for mutation guards; M1 does not mutate it. |
|
|
||||||
| `transport` | yes | constant `tmux` | M1–M5 support local tmux only. |
|
|
||||||
| `tmux` | yes | strict object | Explicit local socket and holder-session configuration. |
|
|
||||||
| `defaults` | yes | strict object | Default work directory and one supported local runtime. |
|
|
||||||
| `runtimes` | yes | non-empty object | Declared local runtime reset policy map. |
|
|
||||||
| `agents` | yes | non-empty array | Local fleet entries. Duplicate stable names are rejected. |
|
|
||||||
|
|
||||||
## Nested fields
|
|
||||||
|
|
||||||
| Path | Required | Constraint |
|
|
||||||
| ---------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
|
|
||||||
| `tmux.socket_name` | yes | non-empty `[A-Za-z0-9_.-]+`; an explicit named socket prevents default-versus-named socket ambiguity |
|
|
||||||
| `tmux.holder_session` | yes | non-empty `[A-Za-z0-9_.-]+` |
|
|
||||||
| `defaults.working_directory` | yes | non-empty string |
|
|
||||||
| `defaults.runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
|
|
||||||
| `runtimes.<runtime>.reset_command` | yes | non-empty string; runtime key must be a supported local runtime |
|
|
||||||
| `agents[].name` | yes | unique `[A-Za-z0-9][A-Za-z0-9_.-]*` stable machine identity |
|
|
||||||
| `agents[].alias` | yes | non-empty display string |
|
|
||||||
| `agents[].class` | yes | `[a-z][a-z0-9-]*`; structural only in M1, semantic role resolution is FCM-M1-002 |
|
|
||||||
| `agents[].runtime` | yes | `claude`, `codex`, `opencode`, or `pi`; it must be declared in `runtimes` |
|
|
||||||
| `agents[].provider`, `model`, `working_directory` | yes | non-empty strings; provider/model capability resolution is a later card |
|
|
||||||
| `agents[].reasoning` | yes | `low`, `medium`, or `high` |
|
|
||||||
| `agents[].tool_policy` | yes | `[a-z][a-z0-9-]*`; structural only in M1 |
|
|
||||||
| `agents[].persistent_persona`, `reset_between_tasks` | yes | booleans |
|
|
||||||
| `agents[].lifecycle.enabled` | yes | boolean; stored now, reconciled in FCM-M3-001 |
|
|
||||||
| `agents[].lifecycle.desired_state` | yes | `running` or `stopped` |
|
|
||||||
| `agents[].launch.yolo` | yes | boolean; structured data only, not an arbitrary command escape hatch |
|
|
||||||
|
|
||||||
## Fail-closed boundary
|
|
||||||
|
|
||||||
Every object is `additionalProperties: false`. The compiler rejects unknown, missing, malformed,
|
|
||||||
and wrong-type fields before producing a model. It specifically rejects remote/SSH/host/socket
|
|
||||||
per-agent fields, connector blocks, secret references, channel fields, arbitrary command fields,
|
|
||||||
and gateway fields because they are unsupported in the local-tmux M1 contract. It does not silently
|
|
||||||
ignore v1 camelCase input, version `1`, or a source that does not parse to an object.
|
|
||||||
|
|
||||||
The v2 compiler is intentionally isolated from the existing v1 loader. Existing v1 rosters and
|
|
||||||
current examples/profiles continue on their current path; FCM-M4 owns explicit inventory, preview,
|
|
||||||
migration, and rollback. FCM-M2 owns generated-file/local-override quarantine, and FCM-M3 owns
|
|
||||||
runtime lifecycle and reconciliation.
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
||||||
"$id": "https://mosaicstack.dev/schemas/fleet/roster-v2.schema.json",
|
|
||||||
"title": "Mosaic local tmux fleet roster v2",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["version", "generation", "transport", "tmux", "defaults", "runtimes", "agents"],
|
|
||||||
"properties": {
|
|
||||||
"version": {
|
|
||||||
"const": 2
|
|
||||||
},
|
|
||||||
"generation": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1,
|
|
||||||
"maximum": 9007199254740991
|
|
||||||
},
|
|
||||||
"transport": {
|
|
||||||
"const": "tmux"
|
|
||||||
},
|
|
||||||
"tmux": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["socket_name", "holder_session"],
|
|
||||||
"properties": {
|
|
||||||
"socket_name": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[A-Za-z0-9_.-]+$"
|
|
||||||
},
|
|
||||||
"holder_session": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[A-Za-z0-9_.-]+$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaults": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["working_directory", "runtime"],
|
|
||||||
"properties": {
|
|
||||||
"working_directory": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"enum": ["claude", "codex", "opencode", "pi"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimes": {
|
|
||||||
"type": "object",
|
|
||||||
"minProperties": 1,
|
|
||||||
"propertyNames": {
|
|
||||||
"enum": ["claude", "codex", "opencode", "pi"]
|
|
||||||
},
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["reset_command"],
|
|
||||||
"properties": {
|
|
||||||
"reset_command": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agents": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"alias",
|
|
||||||
"class",
|
|
||||||
"runtime",
|
|
||||||
"provider",
|
|
||||||
"model",
|
|
||||||
"reasoning",
|
|
||||||
"tool_policy",
|
|
||||||
"working_directory",
|
|
||||||
"persistent_persona",
|
|
||||||
"reset_between_tasks",
|
|
||||||
"lifecycle",
|
|
||||||
"launch"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]*$"
|
|
||||||
},
|
|
||||||
"alias": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"class": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[a-z][a-z0-9-]*$"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"enum": ["claude", "codex", "opencode", "pi"]
|
|
||||||
},
|
|
||||||
"provider": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"model": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"reasoning": {
|
|
||||||
"enum": ["low", "medium", "high"]
|
|
||||||
},
|
|
||||||
"tool_policy": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[a-z][a-z0-9-]*$"
|
|
||||||
},
|
|
||||||
"working_directory": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"persistent_persona": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"reset_between_tasks": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"lifecycle": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["enabled", "desired_state"],
|
|
||||||
"properties": {
|
|
||||||
"enabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"desired_state": {
|
|
||||||
"enum": ["running", "stopped"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"launch": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["yolo"],
|
|
||||||
"properties": {
|
|
||||||
"yolo": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
48
docs/scratchpads/758-fleet-config-docs-ia-checklist.md
Normal file
48
docs/scratchpads/758-fleet-config-docs-ia-checklist.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# #758 Fleet configuration documentation IA acceptance checklist
|
||||||
|
|
||||||
|
**Scope:** M0 planning gate for issue #758. This checklist defines required documentation outcomes; it does not authorize source, schema, role, example, systemd, or live-fleet changes.
|
||||||
|
|
||||||
|
## Acceptance states
|
||||||
|
|
||||||
|
Use `required`, `deferred`, or `complete`. A deferred item requires a target milestone and rationale. M5 cannot exit with a required item incomplete.
|
||||||
|
|
||||||
|
| Path | Purpose | Owner milestone | Required evidence | State |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `docs/PRD.md` | Normative requirements, scope, authority, lifecycle, migration, risks, and acceptance criteria | M0 | Approved requirements PR linked to #758 | required |
|
||||||
|
| `docs/TASKS.md` | M0–M5 one-card/one-PR DAG and review gates | M0 | Every delivery card maps to acceptance criteria and dependencies | required |
|
||||||
|
| `docs/fleet/README.md` | Fleet configuration entry point and operator decision tree | M5 | Links all accepted fleet-config pages and passes link validation | required |
|
||||||
|
| `docs/fleet/concepts/desired-vs-observed-state.md` | Desired, generated, and observed state boundaries | M5 | Matches lifecycle contract and status JSON | required |
|
||||||
|
| `docs/fleet/concepts/identity-class-runtime.md` | Stable identity, alias, class, runtime/provider/model policy | M5 | Matches executable schema and shared role resolver | required |
|
||||||
|
| `docs/fleet/concepts/role-authority-and-leases.md` | Authority matrix and bounded capacity leases | M1 | Independently reviewed against role contracts | required |
|
||||||
|
| `docs/fleet/concepts/generated-env-launch-chain.md` | `.env.generated`, `.env.local`, quarantine, and unit/launcher chain | M2 | Security review and launch-chain tests linked | required |
|
||||||
|
| `docs/fleet/reference/roster-v2.schema.json` | Published executable schema artifact | M1 | Schema/parser parity suite consumes this artifact | required |
|
||||||
|
| `docs/fleet/reference/roster-v2-fields.md` | Every field, default, constraint, compatibility rule, and example | M1 | Field-by-field parity review | required |
|
||||||
|
| `docs/fleet/reference/cli.md` | Config, CRUD, lifecycle commands, JSON shapes, and exit codes | M3 | CLI contract tests and examples linked | required |
|
||||||
|
| `docs/fleet/reference/role-classes.md` | Required/optional classes, aliases, and powers | M1 | Existing resolver validates every documented class | required |
|
||||||
|
| `docs/fleet/reference/lifecycle-transitions.md` | Complete enabled/desired/observed transition table | M3 | Lifecycle and reboot tests linked | required |
|
||||||
|
| `docs/fleet/reference/status-and-drift.md` | Drift planes, readiness, ownership proof, and safe output | M3 | Golden JSON/status tests linked | required |
|
||||||
|
| `docs/fleet/how-to/create-update-delete-agent.md` | Safe CRUD and dry-run workflows | M2 | Fresh and idempotent examples validate | required |
|
||||||
|
| `docs/fleet/how-to/start-stop-restart.md` | Transient versus persisted lifecycle operations | M3 | Matches transition tests | required |
|
||||||
|
| `docs/fleet/how-to/configure-tess-interaction.md` | Configurable interaction instance, not hardcoded identity | M5 | Example validates through shared resolver | required |
|
||||||
|
| `docs/fleet/how-to/configure-ultron-validator.md` | Validator instance without merge authority | M5 | Example validates and authority review passes | required |
|
||||||
|
| `docs/fleet/how-to/customize-roles.md` | Baseline plus `roles.local` resolution and policy | M1 | No parallel resolver described or implemented | required |
|
||||||
|
| `docs/fleet/operations/reconcile-and-recover.md` | Plan/apply, partial failure, recovery, and rollback | M3 | Failure-injection evidence linked | required |
|
||||||
|
| `docs/fleet/operations/env-quarantine.md` | Legacy key inventory and secret-safe disposition | M2 | Security tests prove no values are emitted | required |
|
||||||
|
| `docs/fleet/operations/systemd-tmux-troubleshooting.md` | Exact local targeting and non-destructive diagnostics | M3 | Named/default socket and unmanaged-session tests linked | required |
|
||||||
|
| `docs/fleet/operations/backup-restore.md` | Generation backup and rollback | M4 | Migration rollback drill linked | required |
|
||||||
|
| `docs/fleet/operations/upgrade-assets.md` | Source/installed asset drift and update behavior | M5 | Package/update test linked | required |
|
||||||
|
| `docs/fleet/migration/v1-to-v2.md` | Normative field mapping and stopped-state preservation | M4 | Migration fixtures and canary evidence linked | required |
|
||||||
|
| `docs/fleet/migration/example-profile-disposition.md` | Disposition of every shipped example/profile | M1 | Inventory has no unresolved item at M1 exit | required |
|
||||||
|
| `docs/fleet/migration/legacy-class-aliases.md` | Deterministic aliases and manual-review classes | M1 | Alias/unknown-role tests linked | required |
|
||||||
|
| `docs/SITEMAP.md` | Navigation index | M5 | Link checker passes with all required pages | required |
|
||||||
|
|
||||||
|
## Cross-cutting acceptance checks
|
||||||
|
|
||||||
|
- [ ] Every documented command has stable text and JSON behavior plus an exit-code contract.
|
||||||
|
- [ ] Every documented schema field is accepted by the executable validator, and every accepted field is documented.
|
||||||
|
- [ ] Examples use synthetic non-secret values only.
|
||||||
|
- [ ] Generated files are explicitly labeled non-authoritative and rebuildable.
|
||||||
|
- [ ] Gateway-backed `mosaic agent` records are documented as a separate control plane; no implicit convergence is promised.
|
||||||
|
- [ ] Local M1–M5 scope excludes remote/SSH reconciliation, connector mutation, secret-reference schema, arbitrary commands/channels, and UI/gateway config convergence.
|
||||||
|
- [ ] Independent correctness, security, and validator evidence is linked before M5 acceptance.
|
||||||
|
- [ ] Documentation links and formatting pass repository gates.
|
||||||
57
docs/tasks/758-legacy-example-profile-disposition.md
Normal file
57
docs/tasks/758-legacy-example-profile-disposition.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# #758 Legacy shipped example and profile disposition inventory
|
||||||
|
|
||||||
|
**Baseline:** `origin/main` at M0 planning time. This inventory is normative input to M1; M0 changes no examples or profiles.
|
||||||
|
|
||||||
|
## Allowed dispositions
|
||||||
|
|
||||||
|
1. **Migrate:** rewrite as v2 and validate with the executable schema plus the existing baseline/`roles.local` resolver.
|
||||||
|
2. **Compatibility fixture:** retain as explicitly versioned v1 input for migration tests; it must not be advertised as current authoring guidance.
|
||||||
|
3. **Retire:** remove only in its own implementation PR with a replacement and deprecation note.
|
||||||
|
|
||||||
|
No item may remain `decision-required` when M1 exits.
|
||||||
|
|
||||||
|
## Fleet examples
|
||||||
|
|
||||||
|
| Shipped path | M0 observed concern | Planned disposition | Target card | Required evidence |
|
||||||
|
| -------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------- |
|
||||||
|
| `packages/mosaic/framework/fleet/examples/minimal.yaml` | Uses unresolved legacy `canary`; v1 field form also requires migration | decision-required: map through existing resolver, retain as compatibility fixture, or retire with replacement | FCM-M1-04 | Explicit M1 disposition plus v2 schema/shared-resolver CI |
|
||||||
|
| `packages/mosaic/framework/fleet/examples/coding.yaml` | Legacy `implementer`/`reviewer` aliases may be present | migrate; preserve alias cases separately in migration fixtures | FCM-M1-04 | Alias conversion and current v2 validation |
|
||||||
|
| `packages/mosaic/framework/fleet/examples/general.yaml` | Legacy/general classes require authority review | migrate or retire unsupported roles with replacement | FCM-M1-04 | No unresolved class |
|
||||||
|
| `packages/mosaic/framework/fleet/examples/hybrid.yaml` | Mixed runtime/provider capability combinations | migrate | FCM-M1-04 | Runtime-capability and resolver validation |
|
||||||
|
| `packages/mosaic/framework/fleet/examples/local-canary.yaml` | Local lifecycle and socket semantics | migrate | FCM-M1-04 | Default/named socket and stopped-state fixtures |
|
||||||
|
| `packages/mosaic/framework/fleet/examples/operator-interaction.yaml` | `operator-interaction` becomes `interaction`; Tess remains instance data | migrate | FCM-M1-04 | Alias test and interaction role resolution |
|
||||||
|
| `packages/mosaic/framework/fleet/examples/research.yaml` | Legacy `analyst` may lack canonical contract | decision-required: map through existing resolver or retire | FCM-M1-04 | Explicit M1 disposition; no silent alias |
|
||||||
|
|
||||||
|
## System-type profiles
|
||||||
|
|
||||||
|
| Shipped path | M0 observed concern | Planned disposition | Target card | Required evidence |
|
||||||
|
| ------------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------------------------------------- | ----------- | -------------------------------------------- |
|
||||||
|
| `packages/mosaic/framework/fleet/profiles/software-delivery.yaml` | Must represent required governance seats or explicit waivers | migrate | FCM-M1-04 | Full-profile topology validation |
|
||||||
|
| `packages/mosaic/framework/fleet/profiles/business.yaml` | Classes must resolve through shared resolver | migrate or document waived-class policy | FCM-M1-04 | Profile validation parity |
|
||||||
|
| `packages/mosaic/framework/fleet/profiles/marketing.yaml` | Classes must resolve through shared resolver | migrate or document waived-class policy | FCM-M1-04 | Profile validation parity |
|
||||||
|
| `packages/mosaic/framework/fleet/profiles/personal-assistant.yaml` | Interaction/orchestration authority must remain bounded | migrate | FCM-M1-04 | Authority and topology tests |
|
||||||
|
| `packages/mosaic/framework/fleet/profiles/research.yaml` | Potential legacy analyst/worker class ambiguity | decision-required: resolved local role or retirement | FCM-M1-04 | Explicit M1 disposition; no unresolved class |
|
||||||
|
|
||||||
|
## Shipped fleet service presets
|
||||||
|
|
||||||
|
| Shipped path | M0 observed concern | Planned disposition | Target card | Required evidence |
|
||||||
|
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- |
|
||||||
|
| `packages/mosaic/framework/fleet/services/operator-interaction.yaml` | Uses legacy `operator-interaction` class/tool policy and launch hints; it participates in the interaction/Tess launch contract even though it is not an example/profile | migrate to canonical `interaction` through the shared resolver, preserving Tess as instance data; retain legacy alias coverage in a dedicated compatibility fixture | FCM-M1-04 | v2 schema, shared resolver, runtime-capability, tool-policy, and alias tests |
|
||||||
|
|
||||||
|
No other shipped file currently exists under `packages/mosaic/framework/fleet/services/`. Future service presets added before FCM-M1-04 must be inventoried under the same migrate/compatibility/retire rule.
|
||||||
|
|
||||||
|
## Required compatibility fixtures
|
||||||
|
|
||||||
|
M1/M4 may add dedicated test fixtures rather than retaining public examples in an obsolete form:
|
||||||
|
|
||||||
|
- v1 snake_case and camelCase equivalents;
|
||||||
|
- deterministic `implementer → code`, `reviewer → review`, and `operator-interaction → interaction` aliases;
|
||||||
|
- ambiguous `worker`, `analyst`, `canary`, and unknown classes that fail pending explicit resolution;
|
||||||
|
- schema-only `host`, `ssh`, `socket`, and top-level `connector` fields reported as unsupported for local v2 apply;
|
||||||
|
- the observed 9-roster/12-projection mismatch represented synthetically, with three unmanaged/orphan candidates;
|
||||||
|
- running, stopped/dead, and unknown observed-state migration cases;
|
||||||
|
- legacy generated, allowlisted local, forbidden command/channel/credential, and unknown env keys using names and synthetic hashes only—never values.
|
||||||
|
|
||||||
|
## Exit rule
|
||||||
|
|
||||||
|
FCM-M1-04 cannot close until every inventoried example, profile, and service preset is migrated, retained as a versioned compatibility fixture, or retired with documented replacement evidence. CI must validate all shipped YAML/JSON examples, profiles, and service presets through the same executable schema and shared profile/persona resolver used by roster load, migration, and apply.
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
# Tess / Option 2 runtime-portability qualification — 2026-07-14
|
|
||||||
|
|
||||||
**Issue context:** #706–#711 and runtime-neutral Mos follow-up #754
|
|
||||||
|
|
||||||
**Qualified revision:** `d0771835542d` (`origin/main` at review time)
|
|
||||||
|
|
||||||
**Reviewer/runtime:** Independent Pi lane requested as `openai-codex/gpt-5.6-sol:high`
|
|
||||||
|
|
||||||
**Runtime resolution note:** Mosaic warned that `gpt-5.6-sol` was not present in the provider model catalog and proceeded with it as a custom model ID. This warning was part of the original qualification log and is material provenance; downstream claims must not treat catalog recognition as verified.
|
|
||||||
|
|
||||||
**Verdict:** REQUEST CHANGES
|
|
||||||
|
|
||||||
**Evidence type:** Point-in-time qualification; later commits and PR #757 must be reviewed separately
|
|
||||||
|
|
||||||
## Purpose and provenance
|
|
||||||
|
|
||||||
This report preserves the complete independent qualification that was previously available only in `/tmp/tess-option2-qualification.log`. It distinguishes passing component tests from the missing operational proof required for identity-continuous Mos failover.
|
|
||||||
|
|
||||||
No credential values, OAuth tokens, Discord tokens, device codes, or auth-file contents are included. Commands and results are retained so another environment can reproduce or challenge the findings.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 1. Verdict
|
|
||||||
|
|
||||||
## **REQUEST CHANGES**
|
|
||||||
|
|
||||||
The current Option 2 implementation is a useful portability foundation, but it is **not qualified against AC-TESS-01..11** and is not equivalent to true same-Mos-identity failover.
|
|
||||||
|
|
||||||
Primary blockers:
|
|
||||||
|
|
||||||
1. **AC-TESS-01/02:** The required `mosaic tess` command does not exist; only `mosaic interaction` is registered (`packages/mosaic/src/commands/interaction.ts:60`). The cross-surface test proves CLI enrollment followed by Discord approval/stop, not bidirectional Discord/CLI chat streaming.
|
|
||||||
2. **AC-TESS-04:** Fleet/tmux and Matrix providers are implemented as libraries but are not registered in the production gateway. `AgentModule` registers only Hermes (`apps/gateway/src/agent/agent.module.ts:34`).
|
|
||||||
3. **Mos handoff is not operational or durable:** Production uses `InMemoryInteractionCoordinationPort` (`apps/gateway/src/coord/coord.module.ts:18`), with no Mos-side consumer. Restart loses handoff ownership, idempotency, activity, and results.
|
|
||||||
4. **AC-TESS-06/10:** Restart tests are good local persistence tests, but no real connector/harness failover or exercised rollback exists. Rollback is documentation-only.
|
|
||||||
5. **AC-TESS-08:** The parity suite validates a selected shared intersection using mocked transports. Matrix is not production-wired and tmux drops the runtime message idempotency key before delivery.
|
|
||||||
6. **AC-TESS-09:** M5 qualification remains `not-started`; no live Discord, Matrix homeserver, tmux/Mos consumer, Claude Code/Pi/Codex failover, or deployment rollback was tested.
|
|
||||||
7. **PR #750 mismatch:** Its description promises send-error coverage as HTTP 400, but both gateway and TUI test use HTTP 403 (`packages/mosaic/src/tui/gateway-api.interaction-errors.test.ts:25-34`).
|
|
||||||
|
|
||||||
### AC disposition
|
|
||||||
|
|
||||||
| AC | Result | Evidence |
|
|
||||||
| --- | ----------------- | ------------------------------------------------------------------------------- |
|
|
||||||
| 01 | **Fail** | No `mosaic tess`; no bidirectional same-session chat/stream E2E |
|
|
||||||
| 02 | **Fail** | Generic CLI exists, but fleet/Matrix providers are unreachable in production |
|
|
||||||
| 03 | Pass | Pi profile/model/reasoning/effective-policy tests passed |
|
|
||||||
| 04 | **Fail** | No registered fleet provider or real Mos consumer |
|
|
||||||
| 05 | Partial | Hermes normalization/fail-closed matrix passes; live capability path is limited |
|
|
||||||
| 06 | Partial | PGlite restart/idempotency passes; no actual harness failover |
|
|
||||||
| 07 | Partial | Focused denial/replay tests pass; full M5 abuse qualification absent |
|
|
||||||
| 08 | Partial | Mocked shared-intersection parity passes; Matrix not operationally wired |
|
|
||||||
| 09 | **Fail** | Baselines/CI green, but required E2E/security/rollback qualification absent |
|
|
||||||
| 10 | **Fail** | Inventory incomplete/inconsistent; rollback not exercised |
|
|
||||||
| 11 | Pass/ledger stale | Documentation and sitemap exist; plugin/catalog ledger remains unresolved |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 2. Exact test commands and results
|
|
||||||
|
|
||||||
Initial focused attempts failed before collection because this detached worktree had no dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm --filter @mosaicstack/agent exec vitest run ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: startup failure, `Cannot find module 'vitest/config'`.
|
|
||||||
|
|
||||||
Setup used:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
corepack pnpm --store-dir /home/jarvis/.local/share/pnpm/store/v10 \
|
|
||||||
install --frozen-lockfile --ignore-scripts
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: PASS, 1,240 packages linked.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
corepack pnpm turbo run build \
|
|
||||||
--filter='@mosaicstack/gateway^...' \
|
|
||||||
--filter='@mosaicstack/mosaic^...'
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **17/17 dependency builds successful**.
|
|
||||||
|
|
||||||
### Focused suites
|
|
||||||
|
|
||||||
```bash
|
|
||||||
corepack pnpm --filter @mosaicstack/agent exec vitest run \
|
|
||||||
src/runtime-provider-parity.test.ts \
|
|
||||||
src/matrix-native-runtime-provider.test.ts \
|
|
||||||
src/tmux-fleet-runtime-provider.test.ts \
|
|
||||||
src/durable-session.test.ts \
|
|
||||||
src/hermes-runtime-provider.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **5 files, 39/39 tests passed**.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
corepack pnpm --filter @mosaicstack/gateway exec vitest run \
|
|
||||||
src/agent/durable-session.repository.test.ts \
|
|
||||||
src/__tests__/integration/tess-cross-surface.integration.test.ts \
|
|
||||||
src/plugin/discord-ingress.security.spec.ts \
|
|
||||||
src/coord/interaction-coordination.service.test.ts \
|
|
||||||
src/coord/interaction-coordination.routing.e2e.test.ts \
|
|
||||||
src/agent/hermes-runtime-reachability.e2e.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **6 files, 36/36 tests passed**. PGlite close/reopen recovery passed in 504 ms.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
corepack pnpm --filter @mosaicstack/mosaic exec vitest run \
|
|
||||||
src/fleet/matrix-native-runtime-transport.test.ts \
|
|
||||||
src/fleet/tess-service-profile.test.ts \
|
|
||||||
src/commands/interaction.test.ts \
|
|
||||||
src/tui/gateway-api.interaction-errors.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **4 files, 15/15 tests passed**.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
corepack pnpm --filter @mosaicstack/coord exec vitest run \
|
|
||||||
src/__tests__/interaction-coordination.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **1 file, 7/7 tests passed**.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
corepack pnpm --filter @mosaicstack/gateway exec vitest run \
|
|
||||||
src/agent/interaction.controller.test.ts \
|
|
||||||
src/commands/command-authorization.service.spec.ts \
|
|
||||||
src/agent/__tests__/runtime-provider-registry.service.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **3 files, 27/27 tests passed**.
|
|
||||||
|
|
||||||
Focused total: **124/124 tests passed** after dependency setup.
|
|
||||||
|
|
||||||
### Baselines
|
|
||||||
|
|
||||||
```bash
|
|
||||||
TURBO_FORCE=true corepack pnpm typecheck
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **42/42 tasks successful**.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
TURBO_FORCE=true corepack pnpm lint
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **23/23 tasks successful**.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
corepack pnpm format:check
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **PASS — all files matched Prettier style**.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
~/.config/mosaic/tools/woodpecker/pipeline-status.sh \
|
|
||||||
-r mosaicstack/stack -n 1796
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: **SUCCESS** at `d0771835542d`; all test, build, sanitization, typecheck, lint, format, and publish steps green.
|
|
||||||
|
|
||||||
No tracked files outside the pre-existing `.mosaic/orchestrator/*` launcher changes were modified.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 3. Stale ledger inconsistencies
|
|
||||||
|
|
||||||
1. `docs/tess/MISSION-MANIFEST.md` still says:
|
|
||||||
- current milestone M1;
|
|
||||||
- progress 0/5;
|
|
||||||
- M2/M3/M5 not started.
|
|
||||||
2. `docs/tess/TASKS.md` says:
|
|
||||||
- M4-V failed;
|
|
||||||
- M4-W-001 and TESS-PLG-001 in progress;
|
|
||||||
- M5-V not started.
|
|
||||||
3. Provider issue state conflicts:
|
|
||||||
- #707–#709 remain open although M1–M3 rows are recorded done/pass.
|
|
||||||
- #710 and #711 are closed although M4-V failed and M5-V is not started.
|
|
||||||
4. M5 work was marked done despite depending on failed M4-V.
|
|
||||||
5. TESS-M3-002 says `mosaic tess` is done, but only `mosaic interaction` exists.
|
|
||||||
6. PR #750 removed stale service references from operational docs, but `docs/tess/TASKS.md` still contains `MosCoordinationService` in historical notes.
|
|
||||||
7. `docs/tess/MIGRATION-INVENTORY.md` remains an “initial inventory” with several capabilities marked `adapt`; `M5-MIGRATION-INVENTORY.md` marks grouped capabilities deferred/fail-closed. Neither supplies the complete owner/evidence matrix AC-TESS-10 requires.
|
|
||||||
8. TESS-M2-FUP-001 remains real: the unkeyed SHA-256 compatibility branch still exists at `durable-session.repository.ts:427-431`.
|
|
||||||
9. TESS-PLG-001 claims catalog registration was folded into W-001, but production evidence shows provider registration in the gateway—not a completed `packages/mosaic` plugin catalog.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 4. Gap to true same-Mos-identity failover
|
|
||||||
|
|
||||||
Current code can relaunch the same roster name under another runtime and can rebind a durable interaction session to another provider/runtime ID. That is **replacement**, not identity-continuous failover.
|
|
||||||
|
|
||||||
Missing pieces:
|
|
||||||
|
|
||||||
- No canonical logical Mos identity independent of harness-native session IDs.
|
|
||||||
- No exclusive connector lease or monotonic fencing epoch; session rebinding is effectively last-write-wins.
|
|
||||||
- No stale-holder rejection preventing the old harness from continuing side effects.
|
|
||||||
- No normalized Claude Code/Pi/Codex checkpoint/import/export adapters.
|
|
||||||
- No durable Mos coordination transport or Mos consumer.
|
|
||||||
- No canonical handoff containing mission/task refs, git state, causal sequence, pending operations, capability requirements, and acknowledgements.
|
|
||||||
- No end-to-end receipt journal across connectors.
|
|
||||||
- Matrix has deterministic transaction IDs, but tmux delivery discards `RuntimeMessage.idempotencyKey`.
|
|
||||||
- No fault-injection test transferring Mos among Claude Code, Pi, and Codex and then rolling back.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 5. Minimal follow-up issue decomposition
|
|
||||||
|
|
||||||
| Order | Issue | Minimum acceptance criteria |
|
|
||||||
| ----- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| 1 | **Logical identity and security fencing** | Server-derived `{tenant, logicalAgentId, connectorId, harness, leaseEpoch, scopes, expiry}`; signed/fenced execution grant; stale/forged/cross-tenant grants denied and audited; no connector credential in handoffs |
|
|
||||||
| 2 | **Durable connector lease** | PostgreSQL-backed exclusive lease with CAS, monotonic epoch, TTL/heartbeat, explicit takeover, and gateway rejection of stale holders; connectors for Claude Code, Pi, and Codex |
|
|
||||||
| 3 | **Canonical handoff/checkpoint** | Versioned, sealed schema containing canonical mission/task/git references, checkpoint digest, causal sequence, required capabilities, pending/ambiguous operation references, and source/destination acknowledgement; no raw secrets or mandatory harness transcript |
|
|
||||||
| 4 | **Exactly-once connector journal** | Durable operation IDs and receipts; idempotency propagated through every adapter; Matrix transaction mapping; tmux replaced or wrapped with receiver-side durable dedupe; ambiguous effects remain held for authorized reconciliation |
|
|
||||||
| 5 | **Cross-harness failover and rollback E2E** | Real Mos identity moves Claude Code → Pi → Codex and back; inject crashes before/after lease transfer, handoff persistence, send, and acknowledgement; stale connector fenced; no duplicate side effects; canonical state preserved; rollback evidence published |
|
|
||||||
| 6 | **Generic gateway research ADR** | Evaluate LiteLLM subscription OAuth and Bifrost concepts without adding either to core; include terms/security review, credential lifecycle, tenant mapping, budgets, failover semantics, and adapter-only prototype |
|
|
||||||
|
|
||||||
## Generic gateway placement
|
|
||||||
|
|
||||||
Allowed topology:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Discord / CLI / web
|
|
||||||
↓
|
|
||||||
Mosaic Gateway: auth, tenant scope, policy, approvals, audit
|
|
||||||
↓
|
|
||||||
IProviderAdapter / AgentRuntimeProvider
|
|
||||||
↓
|
|
||||||
optional LiteLLM or Bifrost egress proxy
|
|
||||||
↓
|
|
||||||
upstream provider
|
|
||||||
```
|
|
||||||
|
|
||||||
- **LiteLLM ChatGPT subscription OAuth:** research-only, opt-in, behind an adapter. Subscription credentials require explicit terms, revocation, scope, token-storage, and audit review. They must never become Mosaic identity or core configuration.
|
|
||||||
- **Bifrost:** virtual keys are downstream proxy credentials, not Mosaic principals. Budget and failover concepts may inform Mosaic routing, but tenant policy, authorization, and audit remain in Mosaic.
|
|
||||||
- Neither product may introduce schemas into Mosaic core, receive direct calls from channels/agents, or bypass `IProviderAdapter`/`AgentRuntimeProvider`.
|
|
||||||
- Mosaic should also correct its existing “all providers unhealthy → use one anyway” fallback behavior before adopting more automatic failover (`routing-engine.service.ts:204-212`).
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import {
|
|
||||||
ROSTER_V2_JSON_SCHEMA,
|
|
||||||
RosterV2ValidationError,
|
|
||||||
parseRosterV2,
|
|
||||||
renderRosterV2Yaml,
|
|
||||||
} from './roster-v2.js';
|
|
||||||
|
|
||||||
const validRoster = `
|
|
||||||
version: 2
|
|
||||||
generation: 7
|
|
||||||
transport: tmux
|
|
||||||
tmux:
|
|
||||||
socket_name: mosaic-fleet
|
|
||||||
holder_session: _holder
|
|
||||||
defaults:
|
|
||||||
working_directory: ~/src
|
|
||||||
runtime: pi
|
|
||||||
runtimes:
|
|
||||||
pi:
|
|
||||||
reset_command: /new
|
|
||||||
agents:
|
|
||||||
- name: coder0
|
|
||||||
alias: Coder 0
|
|
||||||
class: code
|
|
||||||
runtime: pi
|
|
||||||
provider: openai
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
reasoning: high
|
|
||||||
tool_policy: code
|
|
||||||
working_directory: ~/src
|
|
||||||
persistent_persona: false
|
|
||||||
reset_between_tasks: true
|
|
||||||
lifecycle:
|
|
||||||
enabled: true
|
|
||||||
desired_state: stopped
|
|
||||||
launch:
|
|
||||||
yolo: true
|
|
||||||
`;
|
|
||||||
|
|
||||||
describe('roster v2 structural compiler', (): void => {
|
|
||||||
it('parses YAML into a normalized typed model and renders canonical YAML', (): void => {
|
|
||||||
const roster = parseRosterV2(validRoster, 'yaml');
|
|
||||||
|
|
||||||
expect(roster).toEqual({
|
|
||||||
version: 2,
|
|
||||||
generation: 7,
|
|
||||||
transport: 'tmux',
|
|
||||||
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' },
|
|
||||||
defaults: { workingDirectory: '~/src', runtime: 'pi' },
|
|
||||||
runtimes: { pi: { resetCommand: '/new' } },
|
|
||||||
agents: [
|
|
||||||
{
|
|
||||||
name: 'coder0',
|
|
||||||
alias: 'Coder 0',
|
|
||||||
className: 'code',
|
|
||||||
runtime: 'pi',
|
|
||||||
provider: 'openai',
|
|
||||||
model: 'gpt-5.6-sol',
|
|
||||||
reasoning: 'high',
|
|
||||||
toolPolicy: 'code',
|
|
||||||
workingDirectory: '~/src',
|
|
||||||
persistentPersona: false,
|
|
||||||
resetBetweenTasks: true,
|
|
||||||
lifecycle: { enabled: true, desiredState: 'stopped' },
|
|
||||||
launch: { yolo: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(renderRosterV2Yaml(roster)).toBe(validRoster.trimStart());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses JSON and produces the same normalized model', (): void => {
|
|
||||||
const yaml = parseRosterV2(validRoster, 'yaml');
|
|
||||||
const json = JSON.stringify({
|
|
||||||
version: 2,
|
|
||||||
generation: 7,
|
|
||||||
transport: 'tmux',
|
|
||||||
tmux: { socket_name: 'mosaic-fleet', holder_session: '_holder' },
|
|
||||||
defaults: { working_directory: '~/src', runtime: 'pi' },
|
|
||||||
runtimes: { pi: { reset_command: '/new' } },
|
|
||||||
agents: [
|
|
||||||
{
|
|
||||||
name: 'coder0',
|
|
||||||
alias: 'Coder 0',
|
|
||||||
class: 'code',
|
|
||||||
runtime: 'pi',
|
|
||||||
provider: 'openai',
|
|
||||||
model: 'gpt-5.6-sol',
|
|
||||||
reasoning: 'high',
|
|
||||||
tool_policy: 'code',
|
|
||||||
working_directory: '~/src',
|
|
||||||
persistent_persona: false,
|
|
||||||
reset_between_tasks: true,
|
|
||||||
lifecycle: { enabled: true, desired_state: 'stopped' },
|
|
||||||
launch: { yolo: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(parseRosterV2(json, 'json')).toEqual(yaml);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sorts runtime and agent maps in the deterministic renderer', (): void => {
|
|
||||||
const roster = parseRosterV2(
|
|
||||||
validRoster
|
|
||||||
.replace(
|
|
||||||
'runtimes:\n pi:\n reset_command: /new',
|
|
||||||
'runtimes:\n pi:\n reset_command: /new\n codex:\n reset_command: /clear',
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
'agents:\n - name: coder0',
|
|
||||||
'agents:\n - name: reviewer\n alias: Reviewer\n class: review\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: medium\n tool_policy: review\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true\n - name: coder0',
|
|
||||||
),
|
|
||||||
'yaml',
|
|
||||||
);
|
|
||||||
|
|
||||||
const rendered = renderRosterV2Yaml(roster);
|
|
||||||
expect(rendered.indexOf(' codex:')).toBeLessThan(rendered.indexOf(' pi:'));
|
|
||||||
expect(rendered.indexOf(' - name: coder0')).toBeLessThan(
|
|
||||||
rendered.indexOf(' - name: reviewer'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
['v1 document', validRoster.replace('version: 2', 'version: 1'), /v1.*existing v1 path/i],
|
|
||||||
[
|
|
||||||
'unknown connector',
|
|
||||||
`${validRoster}\nconnector:\n kind: matrix\n`,
|
|
||||||
/unsupported field.*connector/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'remote host',
|
|
||||||
validRoster.replace(
|
|
||||||
'runtime: pi\n provider',
|
|
||||||
'runtime: pi\n host: remote\n provider',
|
|
||||||
),
|
|
||||||
/unsupported field.*host/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'secret reference',
|
|
||||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n secret_ref: vault://x'),
|
|
||||||
/unsupported field.*secret_ref/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'channel override',
|
|
||||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n channels: discord'),
|
|
||||||
/unsupported field.*channels/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'arbitrary command',
|
|
||||||
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n command: whoami'),
|
|
||||||
/unsupported field.*command/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'gateway field',
|
|
||||||
`${validRoster}\ngateway:\n url: https://gateway.example\n`,
|
|
||||||
/unsupported field.*gateway/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'missing required field',
|
|
||||||
validRoster.replace(' model: gpt-5.6-sol\n', ''),
|
|
||||||
/model.*required/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'invalid type',
|
|
||||||
validRoster.replace('generation: 7', 'generation: seven'),
|
|
||||||
/generation.*integer/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'unsafe generation',
|
|
||||||
validRoster.replace('generation: 7', 'generation: 9007199254740992'),
|
|
||||||
/generation.*integer/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'duplicate names',
|
|
||||||
validRoster.replace(
|
|
||||||
' - name: coder0',
|
|
||||||
' - name: coder0\n alias: Duplicate\n class: code\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: high\n tool_policy: code\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true\n - name: coder0',
|
|
||||||
),
|
|
||||||
/duplicate agent name/i,
|
|
||||||
],
|
|
||||||
['invalid name', validRoster.replace('name: coder0', 'name: ../coder0'), /invalid agent name/i],
|
|
||||||
[
|
|
||||||
'invalid transport',
|
|
||||||
validRoster.replace('transport: tmux', 'transport: matrix'),
|
|
||||||
/transport.*tmux/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'invalid runtime',
|
|
||||||
validRoster.replace('runtime: pi', 'runtime: matrix'),
|
|
||||||
/runtime.*supported/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'invalid reasoning',
|
|
||||||
validRoster.replace('reasoning: high', 'reasoning: extreme'),
|
|
||||||
/reasoning.*low.*medium.*high/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'ambiguous socket',
|
|
||||||
validRoster.replace('socket_name: mosaic-fleet', 'socket_name: default/socket'),
|
|
||||||
/socket_name/i,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'agent socket override',
|
|
||||||
validRoster.replace(
|
|
||||||
'runtime: pi\n provider',
|
|
||||||
'runtime: pi\n socket: another\n provider',
|
|
||||||
),
|
|
||||||
/unsupported field.*socket/i,
|
|
||||||
],
|
|
||||||
])('rejects %s', (_name: string, source: string, expected: RegExp): void => {
|
|
||||||
expect((): void => {
|
|
||||||
parseRosterV2(source, 'yaml');
|
|
||||||
}).toThrow(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects malformed JSON as a validation error', (): void => {
|
|
||||||
expect((): void => {
|
|
||||||
parseRosterV2('{', 'json');
|
|
||||||
}).toThrow(RosterV2ValidationError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('declares supported runtime map keys in the executable schema', (): void => {
|
|
||||||
expect(ROSTER_V2_JSON_SCHEMA).toMatchObject({
|
|
||||||
properties: {
|
|
||||||
runtimes: { propertyNames: { enum: ['claude', 'codex', 'opencode', 'pi'] } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the checked-in documentation schema structurally identical to the executable schema', async (): Promise<void> => {
|
|
||||||
const schemaPath = fileURLToPath(
|
|
||||||
new URL('../../../../docs/fleet/reference/roster-v2.schema.json', import.meta.url),
|
|
||||||
);
|
|
||||||
const documented = await readFile(schemaPath, 'utf8');
|
|
||||||
|
|
||||||
expect(JSON.parse(documented) as unknown).toEqual(ROSTER_V2_JSON_SCHEMA);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
import YAML from 'yaml';
|
|
||||||
|
|
||||||
export const ROSTER_V2_SUPPORTED_RUNTIMES = ['claude', 'codex', 'opencode', 'pi'] as const;
|
|
||||||
export const ROSTER_V2_REASONING_LEVELS = ['low', 'medium', 'high'] as const;
|
|
||||||
export const ROSTER_V2_DESIRED_STATES = ['running', 'stopped'] as const;
|
|
||||||
|
|
||||||
export type RosterV2RuntimeName = (typeof ROSTER_V2_SUPPORTED_RUNTIMES)[number];
|
|
||||||
export type RosterV2ReasoningLevel = (typeof ROSTER_V2_REASONING_LEVELS)[number];
|
|
||||||
export type RosterV2DesiredState = (typeof ROSTER_V2_DESIRED_STATES)[number];
|
|
||||||
export type RosterV2InputFormat = 'json' | 'yaml';
|
|
||||||
|
|
||||||
export interface FleetRosterV2Tmux {
|
|
||||||
readonly socketName: string;
|
|
||||||
readonly holderSession: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Defaults {
|
|
||||||
readonly workingDirectory: string;
|
|
||||||
readonly runtime: RosterV2RuntimeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Runtime {
|
|
||||||
readonly resetCommand: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Lifecycle {
|
|
||||||
readonly enabled: boolean;
|
|
||||||
readonly desiredState: RosterV2DesiredState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Launch {
|
|
||||||
readonly yolo: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2Agent {
|
|
||||||
readonly name: string;
|
|
||||||
readonly alias: string;
|
|
||||||
readonly className: string;
|
|
||||||
readonly runtime: RosterV2RuntimeName;
|
|
||||||
readonly provider: string;
|
|
||||||
readonly model: string;
|
|
||||||
readonly reasoning: RosterV2ReasoningLevel;
|
|
||||||
readonly toolPolicy: string;
|
|
||||||
readonly workingDirectory: string;
|
|
||||||
readonly persistentPersona: boolean;
|
|
||||||
readonly resetBetweenTasks: boolean;
|
|
||||||
readonly lifecycle: FleetRosterV2Lifecycle;
|
|
||||||
readonly launch: FleetRosterV2Launch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetRosterV2 {
|
|
||||||
readonly version: 2;
|
|
||||||
readonly generation: number;
|
|
||||||
readonly transport: 'tmux';
|
|
||||||
readonly tmux: FleetRosterV2Tmux;
|
|
||||||
readonly defaults: FleetRosterV2Defaults;
|
|
||||||
readonly runtimes: Readonly<Record<string, FleetRosterV2Runtime>>;
|
|
||||||
readonly agents: readonly FleetRosterV2Agent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RosterV2ValidationError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'RosterV2ValidationError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type JsonSchema = string | number | boolean | null | JsonSchema[] | { [key: string]: JsonSchema };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executable v2 structural contract. The checked-in documentation schema is
|
|
||||||
* structurally compared to this value in roster-v2.spec.ts.
|
|
||||||
*/
|
|
||||||
export const ROSTER_V2_JSON_SCHEMA: JsonSchema = {
|
|
||||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
||||||
$id: 'https://mosaicstack.dev/schemas/fleet/roster-v2.schema.json',
|
|
||||||
title: 'Mosaic local tmux fleet roster v2',
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['version', 'generation', 'transport', 'tmux', 'defaults', 'runtimes', 'agents'],
|
|
||||||
properties: {
|
|
||||||
version: { const: 2 },
|
|
||||||
generation: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER },
|
|
||||||
transport: { const: 'tmux' },
|
|
||||||
tmux: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['socket_name', 'holder_session'],
|
|
||||||
properties: {
|
|
||||||
socket_name: { type: 'string', pattern: '^[A-Za-z0-9_.-]+$' },
|
|
||||||
holder_session: { type: 'string', pattern: '^[A-Za-z0-9_.-]+$' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['working_directory', 'runtime'],
|
|
||||||
properties: {
|
|
||||||
working_directory: { type: 'string', minLength: 1 },
|
|
||||||
runtime: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
runtimes: {
|
|
||||||
type: 'object',
|
|
||||||
minProperties: 1,
|
|
||||||
propertyNames: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
|
|
||||||
additionalProperties: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['reset_command'],
|
|
||||||
properties: { reset_command: { type: 'string', minLength: 1 } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
agents: {
|
|
||||||
type: 'array',
|
|
||||||
minItems: 1,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: [
|
|
||||||
'name',
|
|
||||||
'alias',
|
|
||||||
'class',
|
|
||||||
'runtime',
|
|
||||||
'provider',
|
|
||||||
'model',
|
|
||||||
'reasoning',
|
|
||||||
'tool_policy',
|
|
||||||
'working_directory',
|
|
||||||
'persistent_persona',
|
|
||||||
'reset_between_tasks',
|
|
||||||
'lifecycle',
|
|
||||||
'launch',
|
|
||||||
],
|
|
||||||
properties: {
|
|
||||||
name: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9_.-]*$' },
|
|
||||||
alias: { type: 'string', minLength: 1 },
|
|
||||||
class: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
|
|
||||||
runtime: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
|
|
||||||
provider: { type: 'string', minLength: 1 },
|
|
||||||
model: { type: 'string', minLength: 1 },
|
|
||||||
reasoning: { enum: [...ROSTER_V2_REASONING_LEVELS] },
|
|
||||||
tool_policy: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
|
|
||||||
working_directory: { type: 'string', minLength: 1 },
|
|
||||||
persistent_persona: { type: 'boolean' },
|
|
||||||
reset_between_tasks: { type: 'boolean' },
|
|
||||||
lifecycle: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['enabled', 'desired_state'],
|
|
||||||
properties: {
|
|
||||||
enabled: { type: 'boolean' },
|
|
||||||
desired_state: { enum: [...ROSTER_V2_DESIRED_STATES] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
launch: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['yolo'],
|
|
||||||
properties: { yolo: { type: 'boolean' } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ROOT_KEYS = ['version', 'generation', 'transport', 'tmux', 'defaults', 'runtimes', 'agents'];
|
|
||||||
const TMUX_KEYS = ['socket_name', 'holder_session'];
|
|
||||||
const DEFAULT_KEYS = ['working_directory', 'runtime'];
|
|
||||||
const RUNTIME_KEYS = ['reset_command'];
|
|
||||||
const AGENT_KEYS = [
|
|
||||||
'name',
|
|
||||||
'alias',
|
|
||||||
'class',
|
|
||||||
'runtime',
|
|
||||||
'provider',
|
|
||||||
'model',
|
|
||||||
'reasoning',
|
|
||||||
'tool_policy',
|
|
||||||
'working_directory',
|
|
||||||
'persistent_persona',
|
|
||||||
'reset_between_tasks',
|
|
||||||
'lifecycle',
|
|
||||||
'launch',
|
|
||||||
];
|
|
||||||
const LIFECYCLE_KEYS = ['enabled', 'desired_state'];
|
|
||||||
const LAUNCH_KEYS = ['yolo'];
|
|
||||||
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
||||||
const TMUX_IDENTIFIER = /^[A-Za-z0-9_.-]+$/;
|
|
||||||
const POLICY_IDENTIFIER = /^[a-z][a-z0-9-]*$/;
|
|
||||||
|
|
||||||
/** Parses YAML or JSON and compiles only the local-tmux roster v2 contract. */
|
|
||||||
export function parseRosterV2(source: string, format?: RosterV2InputFormat): FleetRosterV2 {
|
|
||||||
const parsed = parseSource(source, format);
|
|
||||||
return normalizeRosterV2(parsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Renders canonical snake_case YAML with sorted runtime and agent entries. */
|
|
||||||
export function renderRosterV2Yaml(roster: FleetRosterV2): string {
|
|
||||||
const normalized = normalizeRosterV2(toSourceShape(roster));
|
|
||||||
return YAML.stringify(toSourceShape(normalized));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSource(source: string, format?: RosterV2InputFormat): unknown {
|
|
||||||
const resolvedFormat = format ?? (source.trimStart().startsWith('{') ? 'json' : 'yaml');
|
|
||||||
try {
|
|
||||||
if (resolvedFormat === 'json') return JSON.parse(source) as unknown;
|
|
||||||
return YAML.parse(source) as unknown;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const detail = error instanceof Error ? error.message : String(error);
|
|
||||||
throw new RosterV2ValidationError(`Roster v2 ${resolvedFormat} parse failed: ${detail}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeRosterV2(raw: unknown): FleetRosterV2 {
|
|
||||||
const root = requiredObject(raw, 'Roster v2');
|
|
||||||
assertKnownKeys(root, 'Roster v2', ROOT_KEYS);
|
|
||||||
if (root.version === 1) {
|
|
||||||
throw new RosterV2ValidationError(
|
|
||||||
'Roster v2 compiler rejects v1 input; use the existing v1 path until migration.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (root.version !== 2) throw new RosterV2ValidationError('Roster v2 version must be 2.');
|
|
||||||
|
|
||||||
const generation = requiredPositiveInteger(root.generation, 'Roster v2 generation');
|
|
||||||
const transport = requiredEnum(root.transport, 'Roster v2 transport', ['tmux'] as const);
|
|
||||||
const tmux = normalizeTmux(root.tmux);
|
|
||||||
const defaults = normalizeDefaults(root.defaults);
|
|
||||||
const runtimes = normalizeRuntimes(root.runtimes);
|
|
||||||
const agents = normalizeAgents(root.agents, runtimes);
|
|
||||||
|
|
||||||
if (!runtimes[defaults.runtime]) {
|
|
||||||
throw new RosterV2ValidationError(
|
|
||||||
`Roster v2 defaults runtime "${defaults.runtime}" must be declared in runtimes.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { version: 2, generation, transport, tmux, defaults, runtimes, agents };
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTmux(value: unknown): FleetRosterV2Tmux {
|
|
||||||
const raw = requiredObject(value, 'Roster v2 tmux');
|
|
||||||
assertKnownKeys(raw, 'Roster v2 tmux', TMUX_KEYS);
|
|
||||||
return {
|
|
||||||
socketName: requiredTmuxIdentifier(raw.socket_name, 'Roster v2 tmux socket_name'),
|
|
||||||
holderSession: requiredTmuxIdentifier(raw.holder_session, 'Roster v2 tmux holder_session'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeDefaults(value: unknown): FleetRosterV2Defaults {
|
|
||||||
const raw = requiredObject(value, 'Roster v2 defaults');
|
|
||||||
assertKnownKeys(raw, 'Roster v2 defaults', DEFAULT_KEYS);
|
|
||||||
return {
|
|
||||||
workingDirectory: requiredString(raw.working_directory, 'Roster v2 defaults working_directory'),
|
|
||||||
runtime: requiredRuntime(raw.runtime, 'Roster v2 defaults runtime'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRuntimes(value: unknown): Readonly<Record<string, FleetRosterV2Runtime>> {
|
|
||||||
const raw = requiredObject(value, 'Roster v2 runtimes');
|
|
||||||
const names = Object.keys(raw);
|
|
||||||
if (names.length === 0)
|
|
||||||
throw new RosterV2ValidationError('Roster v2 runtimes must not be empty.');
|
|
||||||
|
|
||||||
const result: Record<string, FleetRosterV2Runtime> = {};
|
|
||||||
for (const name of names.sort()) {
|
|
||||||
const runtime = requiredRuntime(name, 'Roster v2 runtime name');
|
|
||||||
const config = requiredObject(raw[name], `Roster v2 runtime "${runtime}"`);
|
|
||||||
assertKnownKeys(config, `Roster v2 runtime "${runtime}"`, RUNTIME_KEYS);
|
|
||||||
result[runtime] = {
|
|
||||||
resetCommand: requiredString(
|
|
||||||
config.reset_command,
|
|
||||||
`Roster v2 runtime "${runtime}" reset_command`,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAgents(
|
|
||||||
value: unknown,
|
|
||||||
runtimes: Readonly<Record<string, FleetRosterV2Runtime>>,
|
|
||||||
): readonly FleetRosterV2Agent[] {
|
|
||||||
if (!Array.isArray(value) || value.length === 0) {
|
|
||||||
throw new RosterV2ValidationError('Roster v2 agents must be a non-empty array.');
|
|
||||||
}
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const agents = value.map((candidate: unknown, index: number): FleetRosterV2Agent => {
|
|
||||||
const raw = requiredObject(candidate, `Roster v2 agents[${index}]`);
|
|
||||||
assertKnownKeys(raw, `Roster v2 agents[${index}]`, AGENT_KEYS);
|
|
||||||
const name = requiredIdentifier(raw.name, `Roster v2 agents[${index}] name`);
|
|
||||||
if (seen.has(name))
|
|
||||||
throw new RosterV2ValidationError(`Roster v2 has duplicate agent name: ${name}.`);
|
|
||||||
seen.add(name);
|
|
||||||
const runtime = requiredRuntime(raw.runtime, `Roster v2 agent "${name}" runtime`);
|
|
||||||
if (!runtimes[runtime]) {
|
|
||||||
throw new RosterV2ValidationError(
|
|
||||||
`Roster v2 agent "${name}" runtime "${runtime}" must be declared in runtimes.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
alias: requiredString(raw.alias, `Roster v2 agent "${name}" alias`),
|
|
||||||
className: requiredPolicyIdentifier(raw.class, `Roster v2 agent "${name}" class`),
|
|
||||||
runtime,
|
|
||||||
provider: requiredString(raw.provider, `Roster v2 agent "${name}" provider`),
|
|
||||||
model: requiredString(raw.model, `Roster v2 agent "${name}" model`),
|
|
||||||
reasoning: requiredEnum(
|
|
||||||
raw.reasoning,
|
|
||||||
`Roster v2 agent "${name}" reasoning`,
|
|
||||||
ROSTER_V2_REASONING_LEVELS,
|
|
||||||
),
|
|
||||||
toolPolicy: requiredPolicyIdentifier(
|
|
||||||
raw.tool_policy,
|
|
||||||
`Roster v2 agent "${name}" tool_policy`,
|
|
||||||
),
|
|
||||||
workingDirectory: requiredString(
|
|
||||||
raw.working_directory,
|
|
||||||
`Roster v2 agent "${name}" working_directory`,
|
|
||||||
),
|
|
||||||
persistentPersona: requiredBoolean(
|
|
||||||
raw.persistent_persona,
|
|
||||||
`Roster v2 agent "${name}" persistent_persona`,
|
|
||||||
),
|
|
||||||
resetBetweenTasks: requiredBoolean(
|
|
||||||
raw.reset_between_tasks,
|
|
||||||
`Roster v2 agent "${name}" reset_between_tasks`,
|
|
||||||
),
|
|
||||||
lifecycle: normalizeLifecycle(raw.lifecycle, name),
|
|
||||||
launch: normalizeLaunch(raw.launch, name),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return agents.sort((left: FleetRosterV2Agent, right: FleetRosterV2Agent): number =>
|
|
||||||
left.name.localeCompare(right.name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLifecycle(value: unknown, agentName: string): FleetRosterV2Lifecycle {
|
|
||||||
const raw = requiredObject(value, `Roster v2 agent "${agentName}" lifecycle`);
|
|
||||||
assertKnownKeys(raw, `Roster v2 agent "${agentName}" lifecycle`, LIFECYCLE_KEYS);
|
|
||||||
return {
|
|
||||||
enabled: requiredBoolean(raw.enabled, `Roster v2 agent "${agentName}" lifecycle enabled`),
|
|
||||||
desiredState: requiredEnum(
|
|
||||||
raw.desired_state,
|
|
||||||
`Roster v2 agent "${agentName}" lifecycle desired_state`,
|
|
||||||
ROSTER_V2_DESIRED_STATES,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLaunch(value: unknown, agentName: string): FleetRosterV2Launch {
|
|
||||||
const raw = requiredObject(value, `Roster v2 agent "${agentName}" launch`);
|
|
||||||
assertKnownKeys(raw, `Roster v2 agent "${agentName}" launch`, LAUNCH_KEYS);
|
|
||||||
return { yolo: requiredBoolean(raw.yolo, `Roster v2 agent "${agentName}" launch yolo`) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredObject(value: unknown, label: string): Record<string, unknown> {
|
|
||||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
||||||
throw new RosterV2ValidationError(`${label} must be an object.`);
|
|
||||||
}
|
|
||||||
return value as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertKnownKeys(
|
|
||||||
value: Record<string, unknown>,
|
|
||||||
label: string,
|
|
||||||
allowedKeys: readonly string[],
|
|
||||||
): void {
|
|
||||||
const allowed = new Set(allowedKeys);
|
|
||||||
const unknown = Object.keys(value).filter((key: string): boolean => !allowed.has(key));
|
|
||||||
if (unknown.length > 0) {
|
|
||||||
throw new RosterV2ValidationError(`${label} has unsupported field(s): ${unknown.join(', ')}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredString(value: unknown, label: string): string {
|
|
||||||
if (typeof value !== 'string' || value.trim() === '') {
|
|
||||||
throw new RosterV2ValidationError(`${label} is required and must be a non-empty string.`);
|
|
||||||
}
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredIdentifier(value: unknown, label: string): string {
|
|
||||||
const result = requiredString(value, label);
|
|
||||||
if (!IDENTIFIER.test(result)) {
|
|
||||||
throw new RosterV2ValidationError(`Invalid agent name (${label}): ${result}.`);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredTmuxIdentifier(value: unknown, label: string): string {
|
|
||||||
const result = requiredString(value, label);
|
|
||||||
if (!TMUX_IDENTIFIER.test(result))
|
|
||||||
throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredPolicyIdentifier(value: unknown, label: string): string {
|
|
||||||
const result = requiredString(value, label);
|
|
||||||
if (!POLICY_IDENTIFIER.test(result))
|
|
||||||
throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredPositiveInteger(value: unknown, label: string): number {
|
|
||||||
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
|
|
||||||
throw new RosterV2ValidationError(`${label} must be a positive integer.`);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredBoolean(value: unknown, label: string): boolean {
|
|
||||||
if (typeof value !== 'boolean') throw new RosterV2ValidationError(`${label} must be a boolean.`);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredRuntime(value: unknown, label: string): RosterV2RuntimeName {
|
|
||||||
return requiredEnum(value, label, ROSTER_V2_SUPPORTED_RUNTIMES);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredEnum<T extends string>(value: unknown, label: string, allowed: readonly T[]): T {
|
|
||||||
if (typeof value !== 'string' || !allowed.includes(value as T)) {
|
|
||||||
throw new RosterV2ValidationError(
|
|
||||||
`${label} must be one of the supported values: ${allowed.join(', ')}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return value as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toSourceShape(roster: FleetRosterV2): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
version: roster.version,
|
|
||||||
generation: roster.generation,
|
|
||||||
transport: roster.transport,
|
|
||||||
tmux: { socket_name: roster.tmux.socketName, holder_session: roster.tmux.holderSession },
|
|
||||||
defaults: {
|
|
||||||
working_directory: roster.defaults.workingDirectory,
|
|
||||||
runtime: roster.defaults.runtime,
|
|
||||||
},
|
|
||||||
runtimes: Object.fromEntries(
|
|
||||||
Object.entries(roster.runtimes)
|
|
||||||
.sort(([left], [right]): number => left.localeCompare(right))
|
|
||||||
.map(([name, runtime]): [string, unknown] => [
|
|
||||||
name,
|
|
||||||
{ reset_command: runtime.resetCommand },
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
agents: [...roster.agents]
|
|
||||||
.sort((left: FleetRosterV2Agent, right: FleetRosterV2Agent): number =>
|
|
||||||
left.name.localeCompare(right.name),
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(agent: FleetRosterV2Agent): Record<string, unknown> => ({
|
|
||||||
name: agent.name,
|
|
||||||
alias: agent.alias,
|
|
||||||
class: agent.className,
|
|
||||||
runtime: agent.runtime,
|
|
||||||
provider: agent.provider,
|
|
||||||
model: agent.model,
|
|
||||||
reasoning: agent.reasoning,
|
|
||||||
tool_policy: agent.toolPolicy,
|
|
||||||
working_directory: agent.workingDirectory,
|
|
||||||
persistent_persona: agent.persistentPersona,
|
|
||||||
reset_between_tasks: agent.resetBetweenTasks,
|
|
||||||
lifecycle: {
|
|
||||||
enabled: agent.lifecycle.enabled,
|
|
||||||
desired_state: agent.lifecycle.desiredState,
|
|
||||||
},
|
|
||||||
launch: { yolo: agent.launch.yolo },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user