# 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                                 |

## Semantic handoff

`parseRosterV2` and `normalizeRosterV2` remain synchronous and structural. After structural success,
call the asynchronous `validateRosterV2Semantics` handoff before using persona identity or authority.
That validator batches the baseline `fleet/roles/` and operator `fleet/roles.local/` scans, then
delegates every agent to the shared persona resolver.

Semantic validation:

- requires the winning role contract to be readable and non-empty; `LIBRARY.md` membership alone does
  not resolve a class;
- retains `requestedClass` separately from `canonicalClass` in typed output;
- canonicalizes only `implementer` to `code`, `reviewer` to `review`, and
  `operator-interaction` to `interaction`;
- canonicalizes `tool_policy` with the same exact alias table;
- rejects protected class/tool-policy mismatches in either direction, while accepting
  `class: operator-interaction` with `tool_policy: operator-interaction` as canonical
  `interaction`;
- derives immutable protected authority only from canonical class; and
- accepts custom baseline or `roles.local` classes without granting protected authority.

`agents[].alias` remains display-only. Tess and Ultron are instance names, never semantic classes.
Canonicalization happens before role-layer lookup, so a legacy-named override cannot redefine an
alias as separate authority. See [Role Classes and Authority](./role-classes.md) and
[Customize Fleet Roles](../how-to/customize-roles.md).

This handoff performs no filesystem, systemd, tmux, roster, credential, lease, certificate, or
lifecycle mutation.

## 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.
