@ours.network/fleet 1.2.0-nightly.17 → 1.2.0-nightly.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -376,9 +376,9 @@ preserve at most one recent late occurrence. The trusted authoring file and
376
376
  private sealed role snapshot retain exact prompt text; resolved launch, task,
377
377
  room, provenance, and audit presentation surfaces show only bytes and SHA-256.
378
378
 
379
- Every packaged `Developer`, `Critic`, and `LocalCoordinator` Agent Template uses
379
+ Every packaged `Developer`, `Engineer`, `Critic`, and `LocalCoordinator` Agent Template uses
380
380
  `monitor.mode: fleet` with `monitor.interrupt: after_tool`. Consequently every
381
- member of the standard `single`, `pair`, and `team` Room Templates resolves to
381
+ member of the standard `single`, `pair`, `team`, and `engineering` Room Templates resolves to
382
382
  that policy. A custom Agent Template or explicit per-member override remains
383
383
  authoritative and is merged key by key without rewriting unrelated values.
384
384
 
@@ -524,7 +524,7 @@ ours-fleet up
524
524
 
525
525
  ### Rooms and tasks
526
526
 
527
- Init installs editable `single`, `pair`, and `team` definitions under
527
+ Init installs editable `single`, `pair`, `team`, and `engineering` definitions under
528
528
  `~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
529
529
  reference. Inspect them with `ours-fleet template list` and
530
530
  `ours-fleet template show team`. After configuring the authenticated owner below:
@@ -533,8 +533,25 @@ reference. Inspect them with `ours-fleet template list` and
533
533
  ours-fleet task create --title "Solo task" --template single
534
534
  ours-fleet task create --title "Reviewed change" --template pair
535
535
  ours-fleet task create --title "Phased delivery" --template team
536
+ ours-fleet task create --title "Gated feature build" --template engineering
536
537
  ```
537
538
 
539
+ `engineering` pairs a gate-pipeline `Engineer` with the standard `Critic`. The
540
+ Engineer works every non-trivial task through ordered gates — scope and a numbered
541
+ Definition of Done before code, an over-engineering check, spec before
542
+ implementation, test-first, review, a sized security pass, docs, and evidence-backed
543
+ DoD verification — invoking an installed gate-pipeline skill when one is present and
544
+ walking the same gates by hand otherwise. The seat is labelled `Engineer`, so a
545
+ gated seat is never mistaken for a plain `Developer`. To make it the default for
546
+ development tasks, set `tasks: { default_room_template: engineering }` in
547
+ `fleet.yaml`.
548
+
549
+ The `engineering` starter expresses a prompt-level workflow mandate; Fleet does not enforce
550
+ the gates at runtime or install pipeline skills. The Engineer label identifies that mandate,
551
+ not a completed review. When migrating an existing install, a newly added Engineer uses
552
+ the existing Developer template's brain selection and keeps the packaged restrictive permissions.
553
+ Existing customized Engineer files are preserved.
554
+
538
555
  The default `team` gives only its LocalCoordinator Agent Template a 15-minute
539
556
  continuity loop. Each idle-only pass uses authenticated assigned-room evidence,
540
557
  posts at most one missing interval update or status request, and reports `STALLED`
@@ -588,8 +605,9 @@ non-owner-controlled parent fail closed before host setup and are rechecked unde
588
605
  per-setup init lock before publication.
589
606
 
590
607
  The generated task experiences are fixed consequences, not extra questions: `single`
591
- uses `Developer`; `pair` uses `Developer` with an independent `Critic`; and `team` uses
592
- task-local `LocalCoordinator`, `Developer`, and `Critic`. The persistent
608
+ uses `Developer`; `pair` uses `Developer` with an independent `Critic`; `team` uses
609
+ task-local `LocalCoordinator`, `Developer`, and `Critic`; and `engineering` uses the
610
+ gate-pipeline `Engineer` with an independent `Critic`. The persistent
593
611
  `FleetCoordinator` uses the separate packaged `Coordinator` contract and coordination
594
612
  model.
595
613
 
@@ -666,6 +684,29 @@ continuation is serialized per Task and remains alive until convergence or an
666
684
  Owner-action blocker. Re-running `task start <id>` safely resumes the same durable
667
685
  provisioning operation after the blocker is corrected or a process restarts.
668
686
 
687
+ Live health is distinct from durable lifecycle. `task show TASK --json` adds a
688
+ `readiness` observation for active tasks (`ready`, `degraded`, or `unknown`); human
689
+ and authenticated Owner task-show output display the same observation. Task start/work
690
+ continue to reject unavailable health with `task_not_ready`. A same-role untracked
691
+ seat or a changed recorded launch cannot satisfy an original slot. Each control,
692
+ liveness, and production Cowork probe is bounded; members are checked sequentially,
693
+ so total latency grows with room size. These are point-in-time observations, not
694
+ proof that a timed-out session is dead or permission to respawn it.
695
+
696
+ Before deliberate recovery, Fleet Coordinator must inspect existing room seats and
697
+ the exact old launch's termination record and worklog under `recovery/temporary`.
698
+ Keep original slot records and archives intact; record verified old/new identity
699
+ and launch provenance privately. Never restore archived identity state or reuse
700
+ consumed invites. A same-role seat is only a possible replacement, not proven ownership.
701
+
702
+ A replacement's empty inbox does not establish that no work is pending. Paginate
703
+ scoped room/operator history to the end, retain the cursor privately, authenticate
704
+ historical Owner instructions from identity metadata, and compare replies, worklogs,
705
+ and already-completed actions before resuming. Ask the Owner when authority or
706
+ completion is uncertain. Transfer only necessary non-secret context; never replay
707
+ instructions blindly. Observation does not adopt seats, launch replacements, change
708
+ lifecycle, or modify retained recovery evidence.
709
+
669
710
  Set `room.anonymous: true` on a room template, or pass `--anonymous` to
670
711
  `task create`, `task start`, `task work`, or `room create`, to create an
671
712
  anonymous Cowork room. `--no-anonymous` explicitly overrides an anonymous
@@ -20,12 +20,19 @@ export function prepareManagedHarness(role, stateDir, cwd, descriptor, env) {
20
20
  args: [bridge],
21
21
  env: [{ name: 'FLEET_OURS_BRIDGE_DESCRIPTOR', value: descriptor }],
22
22
  };
23
+ // Portable allow covers bound identity messaging, not all tools on the server.
24
+ const tools = role.harness === 'codex' && role.permissions?.approval === 'allow'
25
+ ? Object.fromEntries([
26
+ 'current_identity', 'get_messages', 'list_history', 'list_contacts', 'send_message',
27
+ ].map(name => [name, { approval_mode: 'approve' }]))
28
+ : undefined;
23
29
  const native = {
24
30
  mcp_servers: {
25
31
  ours: {
26
32
  command: process.execPath,
27
33
  args: [bridge],
28
34
  env: { FLEET_OURS_BRIDGE_DESCRIPTOR: descriptor },
35
+ ...(tools ? { tools } : {}),
29
36
  },
30
37
  },
31
38
  plugins: { 'ours@ours-codex-marketplace': { enabled: false } },
@@ -35,6 +42,7 @@ export function prepareManagedHarness(role, stateDir, cwd, descriptor, env) {
35
42
  const config = child.CODEX_CONFIG ? JSON.parse(child.CODEX_CONFIG) : {};
36
43
  child.CODEX_CONFIG = JSON.stringify({
37
44
  ...config,
45
+ mcp_servers: { ...config.mcp_servers, ...native.mcp_servers },
38
46
  plugins: { ...config.plugins, ...native.plugins },
39
47
  });
40
48
  // codex-acp otherwise prefers a same-name connector from the user's config.
@@ -1,4 +1,4 @@
1
- import { type LiveReadinessDeps } from '../rooms-tasks/live-readiness.js';
1
+ import { type LiveReadinessDeps, type TaskReadinessIssue } from '../rooms-tasks/live-readiness.js';
2
2
  import { type FleetConfig } from '../config.js';
3
3
  import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
4
4
  import { provisionMembers } from '../rooms-tasks/provision.js';
@@ -90,7 +90,9 @@ export declare function resolveRoomLaunchPolicy(template: TemplateSnapshot | und
90
90
  export interface TaskRoomServiceDeps {
91
91
  liveReadiness?: LiveReadinessDeps;
92
92
  loadConfiguration?(path?: string): FleetConfig;
93
- cowork?(config: FleetConfig): CoworkAdapter;
93
+ cowork?(config: FleetConfig, options?: {
94
+ timeoutMs: number;
95
+ }): CoworkAdapter;
94
96
  binPath?(): string;
95
97
  provisionMembers?: typeof provisionMembers;
96
98
  moveTaskToList?: typeof moveTaskToList;
@@ -306,6 +308,11 @@ export declare class TaskRoomApplicationService {
306
308
  taskId: string;
307
309
  outcome?: TaskOutcome;
308
310
  }): Promise<TaskSettlementPlan>;
311
+ /** Point-in-time health, separate from durable provisioning and lifecycle. */
312
+ observeTaskReadiness(taskId: string): Promise<{
313
+ state: 'ready';
314
+ } | TaskReadinessIssue>;
315
+ private observeLiveReadiness;
309
316
  private assertTaskLiveReadiness;
310
317
  ensureTaskWork(input: {
311
318
  actor: TaskRoomActor;
@@ -663,10 +663,30 @@ export class TaskRoomApplicationService {
663
663
  (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
664
664
  return this.acceptTerminal(task.task_id, 'done', task.room_id, input.outcome);
665
665
  }
666
- async assertTaskLiveReadiness(task, room, cfg) {
667
- const cowork = this.deps.cowork ? this.deps.cowork(cfg)
668
- : createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config });
666
+ /** Point-in-time health, separate from durable provisioning and lifecycle. */
667
+ async observeTaskReadiness(taskId) {
668
+ const task = readTask(taskId);
669
+ const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
670
+ try {
671
+ const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
672
+ return await this.observeLiveReadiness(task, room, cfg) ?? { state: 'ready' };
673
+ }
674
+ catch {
675
+ return { state: 'unknown', reason: 'readiness_unavailable' };
676
+ }
677
+ }
678
+ async observeLiveReadiness(task, room, cfg) {
679
+ const cowork = this.deps.cowork ? this.deps.cowork(cfg, { timeoutMs: 2_000 })
680
+ : createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config, timeoutMs: 2_000 });
669
681
  const issue = await taskLiveReadiness(task, room, cowork, this.deps.liveReadiness);
682
+ if (issue)
683
+ return issue;
684
+ if (JSON.stringify(readTask(task.task_id)) !== JSON.stringify(task)
685
+ || (room && JSON.stringify(getRoomRecord(room.room_id)) !== JSON.stringify(room)))
686
+ return { state: 'unknown', reason: 'records_changed' };
687
+ }
688
+ async assertTaskLiveReadiness(task, room, cfg) {
689
+ const issue = await this.observeLiveReadiness(task, room, cfg);
670
690
  if (issue)
671
691
  throw new TaskRoomApplicationError('task_not_ready', `Task readiness is ${issue.state}: ${issue.reason}. Ask Fleet Coordinator to inspect the room and member status and choose supported recovery; do not blindly relaunch or reuse invites.`, { task: task.task_id, room: task.room_id ?? 'missing', readiness: issue.state,
672
692
  reason: issue.reason, ...(issue.member ? { member: issue.member } : {}) });
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.17",
3
- "buildId": "165e9b4f7cc7",
4
- "commit": "e720586890943d424840f0f9b96d0ac5051d8a6c",
2
+ "version": "1.2.0-nightly.19",
3
+ "buildId": "1175f8bb300d",
4
+ "commit": "3872302b8c5bd5f427ad798df3883262a8928554",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-26T21:55:24.104Z",
6
+ "builtAt": "2026-09-27T11:02:00.743Z",
7
7
  "capabilities": [
8
8
  "cowork.http-management-v1",
9
9
  "monitor.interrupt.after_tool"
package/dist/docs.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Keep this concise enough to place directly in an agent context. Unlike
5
5
  * Commander's per-command help, this describes how the pieces compose.
6
6
  */
7
- export declare const AI_DOCS = "# ours-fleet reference\n\nours-fleet runs persistent or temporary, identity-bound AI roles through a\nprovider-neutral managed-session interface:\n\n- harness: `claude-code`, `codex` or `hermes`\n- session: `acp` (default) or `codex-app-server` (Codex only)\n- lifetime: permanent (supervised, restartable) or `spawn --temp`\n\n## Codex ACP runtime\n\nFleet bundles codex-acp 1.10.0 (Codex ^0.153.3). Native shell Codex upgrades do\nnot upgrade this dependency. Selection: Agent env.CODEX_PATH, then inherited\nservice CODEX_PATH, then the adapter's bundled Codex. Use an absolute executable\npath without arguments; an empty override selects the bundle. Fleet retains its\npermission proxy around the selected runtime. Custom ACP commands and native\ncodex-app-server sessions own their separate launch behavior.\n\nDoctor reports native and ACP-selected executable paths and versions per configured\nACP Agent, including the platform binary behind the npm launcher, and flags drift\nor unsupported/unknown combinations. Run it in the service's environment; it probes\ncurrent configuration, not existing processes. gpt-6-astra on a bundled ACP launch\nrequires Codex >=0.153.3; older runtimes fail with an actionable launch error.\n\nPersistent overrides belong in Agent env; temporary overrides belong in the Agent\nTemplate before spawning (existing temporary launch snapshots are sealed). Service\noverrides must reach both persistent and temporary services. New temporary\nsupervisors inherit CODEX_PATH (including empty) from the spawning process through\nsystemd-run/launchctl; sealed Agent env still wins. A shell export does\nnot update running services; init does not persist CODEX_PATH. After verification,\nrestart only affected agents, or coordinate replacement of a temporary agent with\na stale sealed override. Reinstall Fleet with optional dependencies to repair its\nbundle. Custom ACP runtime provenance is reported as unknown.\n\n## Discover and validate\n\nHermes uses `harness: hermes`, `session: acp`, and an explicit Brain `model`.\nProvision native provider/credentials with the role stopped, pointing HERMES_HOME\nat `<role-state-dir>/harness/hermes`. Fleet manages model.default and manual\ndangerous-command approvals; the home retains native memory, skills and state.\nUse a literal native model.provider so startup can detect model/provider fallback.\nEvery restart receives the full briefing in a fresh conversation; old ACP IDs\nare not restored. Use Fleet monitoring. Home/plugin MCP providers must be disabled;\nours and optional extras are declared through ACP. Tool availability is unverified\nuntil actual use, without a startup probe prompt.\n\nHermes approval ask/auto/allow maps to default/accept_edits/dont_ask. Terminal and\nwrite_file/patch approval is not universal mediation of browser, memory, skills,\ndelegation or MCP. Fleet waits at most 50 seconds for permission, against the\ntested native dangerous-command timeout of 60 seconds. Workspace confinement is\napproximate without verified OS isolation. Read-only, legacy deny, Fleet provider\noverrides, effort, model_chain, native monitoring and after_tool are unsupported.\nHERMES_HOME is not a complete isolation boundary for native external configuration\nor credential fallbacks. Process supervision uses the existing Fleet lifecycle.\nTested Hermes: 0.21.1, source d15ed4445207dda418b984e8bda0f68f48b8c6f3,\nPython ACP 0.9.0, protocol 1. Unknown builds need compatibility conformance first.\n\n```sh\nours-fleet docs # this complete reference (`man` is an alias)\nours-fleet help <command> # exact flags for one command\nours-fleet config [-c FILE] # validate and print the merged plan; no changes\nours-fleet doctor [-c FILE] [--harness codex|claude-code|hermes]\nours-fleet version [--json] # build identity, capabilities, every install on PATH\n```\n\nConfiguration v2 is `~/fleet.yaml` plus typed bare documents under the exact\nstem directories `~/fleet/agents`, `~/fleet/agent_templates`, `~/fleet/roles`, `~/fleet/brains`, and\n`~/fleet/room_templates`.\nThe manifest owns fleet-wide operational defaults and automation; each Agent\nselects one inline/ref Role and Brain and carries its operational fields.\nAgent Templates under `~/fleet/agent_templates` are inert reusable launch definitions;\nonly explicit files under `~/fleet/agents` are persistent lifecycle instances.\nRoom members use `agent_template` and receive immutable content-addressed snapshots.\nAn Agent Template may declare up to 64 temporary-only named `loops`; persistent Agent\ninstances reject them. Each loop requires `interval` (1m..30d) and bounded\nnonblank `prompt`, with optional `enabled` (default true), `initial_delay`\n(default interval, 0s..30d), and `jitter` (default 0s, < interval, <=1h).\nExecution is fixed skip-if-busy with no ordinary missed-tick backlog/replay;\nrestart recovery may preserve at most one recent late occurrence.\nLegacy top-level `roles:` and `fleet.d` are rejected. Validate the complete\ntrusted source set with `config` and `doctor` before starting or restarting.\n\nPermanent `spawn` writes `~/fleet/agents/Name.yaml`. The web console edits an\nexplicit `{manifest, agents, agent_templates}` model while Role/Brain presets remain read-only.\nIts aggregate revision includes every Agent/Role/Brain/Room-template source, previews a\nredacted per-document diff in an exact-stem private staging tree, and saves under\none root lock with a private multi-file backup and full rollback. A no-op is\nbyte-identical and creates no backup.\n\n## Build identity and install provenance\n\n`--version` prints a semver and nothing else, and a semver does NOT identify an\nartifact. Version bumps land in a release commit of their own, so every build cut\nbetween two releases carries the PREVIOUS version while already containing new\nbehaviour. One host ran two installs that both reported 0.16.0 \u2014 same version,\ndifferent build. One accepted `monitor.interrupt: after_tool`, the other\nrejected it as invalid. Their\n`dist/cli.js` were byte-identical \u2014 the divergence was in other modules.\n\nEvery build therefore stamps `dist/build-info.json` with a build id (first 12 hex\nof a sha256 over the rest of `dist/`), the commit it was cut from, and the\ncapability tokens the shipped code declares \u2014 for example\n`monitor.interrupt.after_tool`. Ask any executable what it is:\n\n```sh\nours-fleet version # ours-fleet 0.17.0+9f1c2a3b4d5e, capabilities, PATH installs\nours-fleet version --json # the same as machine-readable JSON, no environment values\n```\n\nRead a capability, never a version number, to decide whether a setting is\nsupported. When a build rejects a value it knows the name of, it says which\ncapability is missing and which build rejected it, because another install on the\nsame host may accept the identical file. `config` prints the build that resolved\nthe plan; `status <Name>` says so when the build reporting on a role is not the\none that created it (roles record their creating build in `creation.json`).\n\n`ours-fleet doctor` runs an `install` check that lists every `ours-fleet` on\nPATH plus the one executing, and FAILS when two installs share a semver but are\ndifferent builds, or when the running artifact is a DIFFERENT artifact from the\none PATH resolves to. A second prefix holding identical content is not a skew\nand is not reported. A PATH entry the shell would not execute \u2014 a directory, or\na file without its execute bit \u2014 is not counted as an install at all.\nInstalls built before this stamp existed report `+unknown`; they are compared by\nhashing their `dist/` instead, so two pre-provenance installs are still told\napart. To fix a flagged host, remove or update the stale install \u2014 do not rely on\nPATH order.\n\n## Lifecycle and console commands\n\n```sh\nours-fleet init [-c FILE] # interactive missing-default seed; TTY required\nours-fleet up|down [Name...]\nours-fleet restart [Name...] # preserve/resume harness context\nours-fleet force-restart [Name...] # fresh context; briefing is reloaded\nours-fleet ls\nours-fleet status|peek|attach|logs Name\nours-fleet logs -f Name\nours-fleet send Name \"prompt\"\nours-fleet rm Name\nours-fleet watchdog-report <name> [run-id] [--list] [--json]\nours-fleet watchdog-run <name>\n```\n\n`init` is an interactive missing-default workflow that preserves existing files byte-for-byte.\nIt names the resolved manifest and split directory, asks a default-No confirmation,\nthen subscriptions (Codex, Claude, or both), model assignment (one explicit model for\nevery job or explicit development/review/coordination choices), one reasoning level,\nand a final default-No review. Quick/Balanced/Thorough generate low/medium/high.\nModel pickers show packaged supported IDs; catalog membership is not a recommendation\nor entitlement claim. Run `ours-fleet doctor` for local Codex availability; Claude\nentitlement is checked when a role launches. The generated brains contain no\n`model_chain`, so Fleet never silently substitutes another model.\n\nAt either confirmation, N, Enter, Escape, Ctrl-C, Ctrl-D, or EOF cancels. In a picker,\nEscape, Ctrl-C, Ctrl-D, or EOF cancels; Enter records the highlight or continues a\nnon-empty multi-select, N is ignored, and an empty subscription selection remains blocked.\nEvery cancellation before final approval performs no host or configuration mutation.\nRedirected/non-TTY invocation is refused with the same guarantee. After the final Yes,\nhost setup precedes publication. Path ownership/type/mode/symlink and\nsame-filesystem checks run before host setup and again under a per-setup lock. Publication\nstages and validates the complete combined setup, backs up whichever old target(s) exist,\nand retains a private recovery record. A hard process/host termination cannot promise\nrollback; inspect host integration and private init stage/recovery evidence.\n\n`peek`, `attach`, and text `send` use the structured agent session.\nAttachment also accepts `/permit <permission-id> <option-id>`, `/interrupt`,\nand `/detach`.\n\n## Local web console\n\nThe npm package includes the web console; installed users do not clone the repo\nor run `npm run build`:\n\n```sh\nnpm i -g @ours.network/fleet\nours-fleet init\nours-fleet doctor\nours-fleet web # install/update service, start, pair browser\n```\n\nThe normal command uses stable `http://127.0.0.1:49271/`, installs an\nowner-level systemd user service (Linux) or LaunchAgent (macOS), and opens a\nfive-minute one-use pairing link in the local browser. After pairing, bookmark\nthe plain URL or install the PWA. To pair a new, signed-out, or revoked browser,\nrun `ours-fleet web open`.\n\n```sh\nours-fleet web status\nours-fleet web start|stop|restart\nours-fleet web open\nours-fleet web revoke-all # revoke every browser and active session\nours-fleet web uninstall\nours-fleet web serve --port 0 --no-open # isolated foreground/testing mode\n```\n\nThe console is IPv4-loopback-only by default. Both `localhost` and\n`127.0.0.1` are accepted locally. For an nginx/TLS reverse proxy, keep the\ndefault bind and declare the exact browser origin:\n\n`ours-fleet web install --public-origin https://fleet.example.com --password-file /secure/fleet-password`\n\nFleet reads the password file during setup and persists only a salted scrypt\nverifier. New browsers authenticate and retain rotating HttpOnly/SameSite\ntrusted-device credentials. If nginx already authenticates, the operator may\ndeliberately select `--no-password`; the CLI and browser warn that anyone\nreaching the origin can control the fleet. First setup requires an explicit\nchoice: `--password-file` or `--pairing` for protected access, or\n`--no-password` for intentional unprotected access.\n\nUse `--bind ADDRESS` only for an intentional direct listen. A non-loopback\nbind is rejected unless `--public-origin` is also present. Host/Origin checks\nuse the declaration and do not trust forwarded headers. Configure nginx to\nproxy HTTP and WebSocket upgrades to `127.0.0.1:49271` and terminate TLS;\nfleet accepts nginx's loopback upstream Host, so no Host rewrite is required.\nBrowser credentials add Secure for HTTPS, and `revoke-all` invalidates all\ntrusted devices. Role creation offers harness-scoped known-model choices\nwhile still accepting a typed model ID; blank explicitly uses the selected\nharness's own default.\n\n## Spawn\n\n```sh\nours-fleet spawn [--temp] [Name | --name Name] \\\n --brain BRAIN_ID --role ROLE_ID \\\n --cwd /absolute/path --identity Identity --coordinator Coordinator \\\n --approval ask|auto|allow \\\n --filesystem read-only|workspace|unrestricted \\\n --unattended deny|wait --isolation-file /path/isolation.yaml\n```\n\nPermanent spawn writes `~/fleet/agents/Name.yaml` and starts a supervised role.\n`--temp` writes active state under `~/.ours-fleet/tmp` and starts an independent\ntransient supervisor (a collected systemd unit or submitted launchd job). It is\nnot enabled across reboot and does not die when the role that spawned it restarts.\nBrain definitions own the managed session backend. When a temporary role's bound identity\ncloses or its session ends, the supervisor, monitor and live roster entry retire\ntogether; state moves intact to `~/.ours-fleet/recovery/temporary` with a\ntermination record. Failed launches use the same archive rather than deleting\ntheir briefing, provenance, logs or partial supervisor metadata.\n\nNamed `down` and `rm` commands can target an exact state-backed temporary role\neven though it is absent from merged fleet YAML. The recorded transient unit/job\nis authoritative. Missing/incomplete ownership metadata is reconciled only from\nan exact `_run-temp <role>` process-table match: one match may be adopted, zero\nsettles as stopped, and ambiguity or an unreadable table fails closed. Launching\nrecords receive a bounded grace so a not-yet-registered transient unit cannot be\nmistaken for a stopped one. Stale recorded supervisors are reclaimed in bounded\nbatches by moving their state to the same recovery archive, never by blind deletion.\n\nThe Fleet supervisor creates and binds the assigned identity before the harness\nstarts. Temporary identities survive harness reconnects and are released only when\nthe logical run ends. Permanent identities remain available after the supervisor\nreleases ownership. Agents receive ready tools; no model-owned identity bootstrap\nor onboarding skill is required.\n\nThe temporary supervisor provisions identity and verifies room readiness before\nstarting the harness. After readiness, only sustained authoritative absence\ncloses the role. Unreachable, malformed, or\nvalid-but-empty daemon indexes are ambiguous and reset closure debounce rather\nthan becoming cleanup authority.\n\nInside a managed role, public `ours-fleet` commands cross an authenticated\nsupervisor attribution boundary before Commander parsing. The original CLI remains\nthe executor inside the role's existing OS sandbox, and ordinary CLI validation is\nthe source of truth. Hidden worker entry points remain internal; public lifecycle and\noperator commands are not restricted by the proxy.\n\nCommand invocation, raw argv, read-only work, validation failures, and generic\noutcomes are never forwarded to the Owner-visible channel. Fleet announces only\nconfirmed Agent, Task, and Room lifecycle changes. Local diagnostics retain\nstructurally redacted command metadata. Lifecycle delivery uncertainty is logged,\nnever recursively announced, and never reruns or blindly retries an effect.\nRoom participant summaries describe creation and activation. Fleet has no public\npost-create Room membership mutation, so it does not claim a separate membership event.\n\nOmitted Brain and Role selections, working directory, coordinator, neutral permissions,\nand fleet monitor policy inherit from the calling Agent. Explicit options always win.\nIdentity, mission/profile text, environment, owner routing, auth proxy, room startup,\nisolation, worklog, and sensitive inline Brain values never inherit implicitly.\nThis automatic proxy is a convenience and\nattribution mechanism, not an isolation boundary: an unrestricted role can still\ninvoke another binary path directly. Host/operator shells keep the ordinary direct\nCLI behavior.\n\nBrain owns harness, session, model, reasoning effort, token limits, and native harness\noptions. Removed runtime flags are rejected with migration guidance rather than silently\nreinterpreted. A selection is a stable ID or an explicit `inline:{...}` mapping.\n\nPackaged Developer, Critic, and LocalCoordinator Agent Templates set\n`monitor: { mode: fleet, interrupt: after_tool }`. Every member of the standard\n`single`, `pair`, and `team` Room Templates therefore resolves to fleet-owned,\nafter-tool delivery. Explicit per-member and custom Agent Template values remain\nauthoritative and merge key by key.\n\nManaged ACP stall recovery is opt-in: `monitor: { mode: fleet, stall_recovery: true,\n stall_timeout_ms: 900000 }`. The validated timeout is 60000\u201386400000 milliseconds.\nOmitted/false preserves existing behavior. Silence is measured from meaningful live\nprogress; replay and retry chatter do not reset it. Strong authenticated retry\nevidence requires one window, generic silence two. Tools, permissions, modal or\nunknown boundaries, steering and human cancellation protect the turn. Recovery\nuses one bounded cancel and diagnostic continuation in the same ACP session/queue\nslot; it never restarts the adapter. Startup and queued mail wait behind recovery.\nA durable claim permits at most one automatic attempt per ACP session ID, including\nacross supervisor restarts; later mail is non-cancelling. Failed or re-stalled\nrecovery reports a blocker. Inspect `.stall-recovery/audit.jsonl` and recorded\nterminal events before continuing; never replay ambiguous or completed mutations.\nNative monitoring/non-ACP sessions are unchanged. No room or identity coupling.\n\n## fleet.yaml\n\n```yaml\napi_version: ours.network/fleet/v2\nvars:\n work_root: /home/me/work\nstart_stagger_ms: 0\ndefaults:\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n monitor:\n mode: fleet # fleet (default) | native\nwatchdogs:\n nightwatch: # [A-Za-z0-9_-], must not collide with a role name\n coordinator: FleetCoordinator # required \u2014 where alerts go\n # everything below is optional\n enabled: true # default true; false = configured but never scheduled\n interval: 10m # default 10m; 30s | 10m | 2h, minimum 1m\n watch: [Alice, CodexReviewer] # explicit lists are exact; omit for configured + live temp roles\n agent: { ref: WatchdogAgent } # required: declared Agent ID, or canonical inline Agent definition\n identity: Watchdog-nightwatch # default: Watchdog-<name>\n timeout: 5m # default 5m; a run past this is killed and recorded as error\n keep_reports: 50 # default 50 reports retained per watchdog\n alert_cooldown: 60m # default 60m before the same finding alerts again\n prompt_file: /abs/extra.md # optional extra focus, APPENDED to the fixed contract\n```\n\nAn Agent is a separate bare document under `~/fleet/agents/<ID>.yaml`:\n\n```yaml\nrole: { inline: { mission: Coordinate work and delegate implementation. } }\nbrain: { inline: { harness: codex, session: codex-app-server, model: gpt-model-id } }\nidentity: Coordinator\ncwd: ${work_root}/project\noversee: [{ agent: Worker, interval: 5m }]\n```\n\nA watchdog observes and reports; it never restarts, stops, spawns, or removes a\nrole, answers a pending permission, edits a workspace, or approves anything on\nthe owner's behalf. `watchdogs:` may appear only in the base config\n(`~/fleet.yaml` or `-c FILE`); Agent/Role/Brain documents never own it.\nThe selected Agent owns Brain, Role, permissions, isolation, and every other\nagent setting. Legacy watchdog `harness`, `model`, `session`, and\n`isolation` fields fail with migration guidance.\nWhen `watch:` is omitted, each run watches the configured roles plus temporary\nfleet roles that are live when the run starts. An explicit `watch:` list is\nnever augmented.\n\nAgent operational values override manifest operational defaults. Role and Brain\nownership never cross-merges. `${name}` substitutes entries from `vars`.\nBrain fields include `max_tokens` and `autocompact_pct`; isolation is Agent-owned.\nUse README.md for the complete isolation policy and resource-cap schema.\n\nSupervised roles connect to the operator-configured ours daemon; they do not own its\nlifecycle. Fleet strips the obsolete, presence-sensitive `OURS_AUTOSTART` variable from\nagent-session children; `ours-mcp proxy` is client-only and never starts a daemon. Start\nthe shared daemon only through an explicit operator or installer/setup flow.\n\n## Rooms and tasks\n\n`init` materializes editable `single`, `pair`, and `team` Room templates plus\ntheir exact-cased Agent, Role, and Brain presets. The command prints the packaged\npreset revision and source directory. Inspect provenance and content before use:\n\n```sh\nours-fleet config [-c FILE]\nours-fleet template list [-c FILE]\nours-fleet template show team [-c FILE]\nours-fleet task create --title \"Solo task\" --template single [-c FILE]\nours-fleet task create --title \"Reviewed change\" --template pair [-c FILE]\nours-fleet task create --title \"Phased delivery\" --template team [-c FILE]\nours-fleet spawn --temp Scout --role SELECTION --brain SELECTION --loops-file PRIVATE.yaml\nours-fleet task start TASK --member developer --loops-file PRIVATE.yaml\nours-fleet task start TASK --member critic --no-loops\n```\n\n`--loops-file` must be an owner-only, non-symlink regular file <=1 MB containing\nexactly one top-level non-empty `loops:` mapping. `--no-loops` explicitly\ndisables loops. They are mutually exclusive and rejected for permanent spawn.\nFor a grouped room member the whole CLI block overrides its Agent Template;\nthe template overrides omission. Omission preserves historical no-loop behavior\nand never inherits manifest wildcard loops. Fleet validates before side effects,\nseals normalized timings plus exact private prompts, and reuses that snapshot for\nidempotent start, retry, recovery, and replacement. Trusted authoring and the\nprivate sealed runtime retain exact prompts; resolved launch, task, room,\nprovenance, and audit presentations show source, policy, timing, prompt bytes,\nand prompt SHA-256 instead of prompt text.\n\nAn alternate manifest `-c /path/custom.yaml` uses `/path/custom/` as its split\nroot. Repeated init only fills missing files and never adopts a newer default.\nExact known revision-3 through revision-5 packaged-bootstrap and generated role defaults have an exact-semantic,\nfail-closed migration (dry-run by default):\n\n`ours-fleet migrate-role-defaults [-c FILE]`\n`ours-fleet migrate-role-defaults [-c FILE] --write`\n\nReview removals, replacements, additions, preserved custom files, staging path,\nand recovery path. Same-named custom files stay byte-identical; dangling custom\nreferences refuse publication, and a successful rerun is a no-op. For explicit\nsingle-file adoption, copy from init's reported packaged source beside the target\nas `.new-default`, inspect `diff -u TARGET TARGET.new-default`, then replace it.\n\nThe exact generated six-worker legacy starter Agent set has its own fail-closed\nmigration (dry-run by default):\n\n`ours-fleet migrate-agent-templates [-c FILE]`\n`ours-fleet migrate-agent-templates [-c FILE] --write`\n\nReview the dry-run moves, addition, staging path, and retained recovery-backup path\nbefore `--write`. Customized/partial known starters and unsafe trees refuse without\nmutation; unrelated custom persistent Agents remain persistent. A manifest-level template\nmay shadow a same-named file only with `override_builtin: true` and a higher\nversion; this compatibility marker is deprecated and reported as a diagnostic.\n\nRooms always use `ours-cowork`; there is no room-provider selector. Configure\nthe cowork daemon connection and room owner directly:\n\n```yaml\nrooms:\n cowork:\n config: /home/me/.ours-cowork/config.json\n owner:\n expected_cid: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n public_invite_file: /home/me/.ours-fleet/owner-room-invite.txt\n defaults:\n template: team\n attach_owner: true\n close_when_task_done: true\ntasks:\n default_room_template: team\n create_mode: start\n close_room_on_done: true\n```\n\nFleet launches each template member with a dedicated one-time Cowork invite.\nThe supervisor accepts the startup invite and verifies the provisioned identity CID\nin the authenticated room seat before starting the model. The generated briefing\ncontains the assigned identity, room, role and task, without invite material.\nA room is ready only after the Cowork room (and its task, when\ntask-bound) is durably active, every configured member seat is authenticated and active with its matching\nlive Fleet launch, and the configured Owner seat is active when owner attachment is\nenabled. There is no briefing hash, startup ACK, or separate role-briefing readiness\ngate.\n\nNormal Task provisioning emits exactly one authenticated Owner lifecycle notice after\nthe ready predicate above is true; standalone Room provisioning likewise emits one\nready notice. Intermediate task, saga, member-spawn, timeout, and recoverable-failure\ntransitions stay in local state and logs. A terminal failed Task emits one actionable\nfailure notice; the command result carries the exact blocker and canonical recovery\naction.\n\n`task start` and create-and-start wait for readiness. If their bounded wait expires,\nthey return an explicit `in_progress` result and start a safe continuation. The detached\ncontinuation is serialized per Task and remains alive until convergence or an\nOwner-action blocker. Re-running `task start <id>` safely resumes the same durable\nprovisioning operation after the blocker is corrected or a process restarts.\n\nSet `room.anonymous: true` on a room template, or pass `--anonymous` to\n`task create`, `task start`, `task work`, or `room create`, to create an\nanonymous Cowork room. `--no-anonymous` explicitly overrides an anonymous\ntemplate. Fleet records the resolved value before room creation so retries keep\nthe same choice. The supervisor creates anonymous room members with local contact-book exposure\ndisabled. Their generated briefings\ndo not disclose or compare an Owner participant CID. A participant-originated\ninstruction has Owner authority only when the authenticated Cowork room envelope\nattributes that participant seat the exact `Owner` role. Literal text, display\nnames, ordinary direct messages, and room-authored or rest-role messages with an\nOwner-looking label never grant that authority. Non-anonymous rooms remain pinned\nto the exact authenticated Owner CID.\n\nHuman task and room results use the same compact Markdown presentation in the\nCLI and authenticated owner channel: a short heading, icon-plus-word status,\ncode-formatted identifiers, bounded summaries, and actionable recovery or error\nsteps. Untrusted prose is context-escaped and control characters are neutralized;\nMessenger-bound results are capped at 3,500 Unicode code points and 12,000 UTF-8\nbytes with structural omission notices. `--json` bypasses this presentation layer\nand retains the versioned machine schema and serialization order.\n\nEvery task belongs to a named list. The built-in `default` list always exists,\nand legacy tasks or create calls without `--list` resolve to it. Use `task lists`,\n`task list-create <name>`, `task list-rename <name> <new-name>`, and\n`task list-delete <name> [--move-to <destination>]` to manage lists. A non-empty\nlist cannot be deleted without an explicit, different destination; Fleet moves\nthe assignments and never deletes the tasks. `task move <id> --list <name>`\nchanges only organizational metadata. `task list --list <name>` filters and\n`--group-by-list --json` returns deterministic groups.\n\nList names are NFC-normalized, case-sensitive, and limited to 64 Unicode code\npoints. Leading/trailing whitespace, controls, format/path characters, normalized\nduplicates, and the reserved exact name `default` are rejected. The authenticated\nowner channel provides the matching `/task` subcommands, while authenticated web\nclients use `/api/v1/task-lists`, `/api/v1/tasks`, and\n`/api/v1/tasks/:id/list`; every adapter delegates to the same application service.\nMessenger's multiline command grammar treats surrounding whitespace on each\nvalue line as transport framing; the canonical value passed to the shared service\nis the trimmed line. CLI arguments and REST strings are passed verbatim.\n\nOlder prerelease files with the exact legacy `provider: cowork` key under\n`rooms:` still load, but the key is ignored and omitted from resolved\nconfiguration. Remove it when editing the file. Any other legacy value is an\nerror. The optional `rooms.owner.provider` setting is separate and defaults to\n`messenger-server`.\n\nFinish and Delete are distinct terminal task actions:\n\n`ours-fleet task finish <id>` moves an active or review task to `done` and\ndeletes its associated Cowork room after retiring its members. The room then\ndisappears from normal Fleet and Cowork views; its brief, messages, repository\nreferences, and attachments are not retained as an inspectable archive.\nThe prerelease configuration names `tasks.close_room_on_done` and\n`rooms.defaults.close_when_task_done` are retained for compatibility, but\n`true` now means this close-then-delete behavior.\n\n`ours-fleet room delete <id> <id>` is the canonical destructive room command.\n`room close <id> <id>` remains a deprecated alias with identical deletion\nsemantics. Older prerelease `closed` room records are deleted directly the next\ntime `room list` reconciles Fleet with Cowork.\n\n`ours-fleet task delete <id> <id>` permanently deletes a task in ANY lifecycle\nstate \u2014 backlog, provisioning, active, blocked, review, done, cancelled, failed,\nor partially settled. The exact task ID is required twice for confirmation. A\ndurable deletion intent is persisted before any side effect; the cleanup worker\nthen retires managed room members with evidence, closes and deletes an attached\nroom (tolerating already-missing remote rooms), releases the sealed launch\nsnapshot, and unlinks the task record last. While cleanup settles the task is\nhidden from normal listings and every lifecycle mutation is rejected; if\ncleanup cannot complete (for example Cowork is unreachable), the deletion stays\nin a precise resumable state \u2014 repeat the identical delete command to converge\nafter outages, crashes, or restarts. Deletion\nnever fabricates a `done` transition. A metadata-only deletion receipt\n(acceptance actor, original state, timestamps, completion) is retained under\n`deletion-receipts/` as durable audit evidence. An already-missing task is an\nidempotent no-op. The Owner-channel equivalent is `/task delete <id> <id>`; the\nmanagement API equivalent is `DELETE /api/v1/tasks/<id>?confirm=<id>` (200 when\nsettled, 202 while pending; `GET /api/v1/tasks?includeDeleting=true` exposes\ndeletion-pending tasks to operators).\n\n## Permissions\n\nPrefer the harness-neutral `permissions` block:\n\n- `approval: ask|auto|allow`: portable permission policy. `deny` remains a\n deprecated, fail-closed compatibility alias for existing fleet files.\n- `filesystem: read-only|workspace|unrestricted`: filesystem intent\n- `unattended: deny|wait`: what a managed session does when no console can\n answer a request\n\nThe backend translates this common intent. Native Codex app-server roles reject\npermission aliases in `harness_options`; Codex ACP and Claude retain their\nlegacy native-override compatibility. Do not choose `allow`/`unrestricted`\nor Claude `bypassPermissions` without explicit authorization.\n\n### Creation-time isolation\n\n`ours-fleet spawn --isolation-file <path>` supplies a role's sandbox policy at\ncreation, so the FIRST launch is already confined \u2014 a role that only gains\n`isolation:` on a later `up` ran unsandboxed until then.\n\nThe file holds exactly the `isolation:` mapping documented above and nothing\nelse \u2014 the same schema, validated by the same code, so a policy written here\ncannot mean something different from the identical block in fleet.yaml:\n\n```yaml\nnetwork: deny\nfs:\n read: [/opt/reference]\nresources:\n mem: 2G\n```\n\nInvalid files are rejected before anything is created: no config, no state\ndirectory, no identity reservation. Works for both permanent and `--temp` roles.\n\n### Never-prompt failure\n\nThe failure this section exists to prevent leaves no error message anywhere.\n\nAn unattended role has no console. When the harness needs a permission decision\nthere is nobody to ask, so the request is refused INSIDE the harness \u2014 no\nprompt, no error, no log line. The agent simply does less than its briefing told\nit to, reports success, and nothing distinguishes that from having done the\nwork. Two settings produce it:\n\n1. a permission mode that suppresses the prompt without granting the action\n (Claude `dontAsk`, which is why neutral `allow` maps to\n `bypassPermissions` instead); and\n2. `unattended: deny`, which refuses every request that reaches it.\n\n**Automatic decisions are now recorded.** Every permission request decided\nwithout a human emits a completed event into\n`~/.ours-fleet/agents/<Name>/.session-events.jsonl` carrying the decision,\nwhether policy or a person made it, the policy that produced it\n(`permissions.unattended=deny` vs `permissions.approval=deny`/`=allow`),\nthe reason, and the option selected. `ours-fleet peek` and `attach` render\nthem. Automatic denial asks for a one-shot rejection, never a standing one, so a\nsingle unattended refusal cannot disable a tool for the rest of the session.\n\nA role that can auto-deny logs one line at startup saying so.\n\nTo detect an under-permissioned role BEFORE it runs, use the capability floor\nbelow: `ours-fleet doctor` fails such a role rather than letting it discover\nthe problem silently at work.\n\n### The unattended capability floor\n\nAn unattended role has no console, so a permission request cannot be answered \u2014\nit is refused, silently, inside the harness. The agent then does less than it\nwas told to and reports no error. To make that visible before launch,\n`ours-fleet config` and `ours-fleet doctor` resolve each role's neutral\npermissions through its harness and check the result against a fixed floor:\n\n- `read-state` \u2014 read its briefing, ROUTINES.md, and WORKLOG.md\n- `write-state` \u2014 append its WORKLOG and its own state files\n- `messaging` \u2014 bind its identity, send and receive ours mail\n- `monitor` \u2014 arm and observe its mail monitor\n- `workspace-edit` \u2014 edit and test files in its working directory\n- `status-commands` \u2014 run the inspection commands its briefing prescribes\n\n`doctor` reports this per role as `unattended floor: <Role>`. A role with\n`unattended: deny` that cannot meet the floor FAILS doctor, because it will\ndeny those requests with nobody to see it; with `unattended: wait` it warns,\nbecause a human can still attach and answer.\n\nSecurity meaning: native Codex maps `ask` \u2192 `untrusted`, `auto` \u2192\n`on-request`, and `allow` \u2192 `never`; its independent filesystem mapping is\n`read-only` \u2192 `read-only`, `workspace` \u2192 `workspace-write`, and\n`unrestricted` \u2192 `danger-full-access`. Users configure only the Fleet names.\nClaude maps `ask` to `default`, `auto` to `acceptEdits`, and `allow` to\n`bypassPermissions`. Codex ACP retains its adapter-specific coupled modes:\n`auto` selects `agent`, while `allow` selects `agent-full-access`. These\nmodes genuinely permit the actions the role was authorized to take \u2014\n`dontAsk` only suppresses the prompt while still refusing the action. Nothing\nother than an explicit `allow` becomes non-interactive. Legacy `deny` keeps\nits conservative Codex `on-request` / Claude `plan` translation. `allow` is therefore a real grant and\nrequires explicit authorization; per-role `isolation:` remains the outer\nboundary that a permission mode cannot cross.\n\nACP carries agent-advertised session mode IDs and `session/set_mode`, but those\nIDs are agent-specific and ACP defines no portable permission-policy capability.\nFleet therefore uses the ACP primitive where an adapter exposes a matching mode\nand otherwise performs the harness translation above. The bundled Codex ACP\nadapter couples approval and sandboxing in its advertised mode IDs. Neutral\n`allow` therefore selects `agent-full-access` and widens `filesystem:\nworkspace` or `read-only` to `danger-full-access`; neutral `auto` selects\n`agent` and `workspace-write` even when the neutral filesystem value differs.\nAn explicit `harness_options.sandbox` selects its corresponding ACP preset and\nstill wins, as does an explicit native approval override. `config` and\n`doctor` report a coupled-mode mismatch as approximate. Use per-role\n`isolation:` as the outer boundary for an `allow` ACP role. The live session\nreports both its effective normalized mode and the exact native mode selected.\n\nSee also: `spawn --approval/--filesystem/--unattended` set this intent at\ncreation, and `ours-fleet config` prints each role's neutral settings, their\nnative translation, and any warning \u2014 the same text `doctor` reports.\n\nClaude `harness_options`: `permission_mode` (default, acceptEdits, plan,\ndontAsk, bypassPermissions), `plugins`, `mem_palace`,\n`mem_palace_midsession_autosave`, `mcp_servers` and `mcp_servers_only`.\n\n`mcp_servers` declares MCP servers for the role, in `.mcp.json`'s own shape\n(a map of name to `{ command, args, env }`, or `{ type: http|sse, url,\nheaders }`). By default they are ADDED to whatever the OS user running the role\nalready has configured. The Claude Code adapter sends them in `session/new`.\n\nWhen `mcp_servers` is absent, Fleet sends ACP's protocol-required empty\n`mcpServers` array without an exclusive override, so the agent keeps its inherited\nservers. An explicitly empty configured set is different: Fleet preserves that intent\nthrough the bundled adapter's compatibility path and disables every inherited server.\n\n`mcp_servers_only: true` makes the declared set EXCLUSIVE through\n`strictMcpConfig`. It is all-or-nothing and it ignores every\nother MCP configuration: project `.mcp.json`, user settings, and **plugins**.\nThe ours connector is normally installed as a plugin, so a strict role that does\nnot re-declare it has no `send_message` and no `get_messages` \u2014 it cannot even\nreport that it has gone mute. Fleet therefore refuses a strict role whose\n`mcp_servers` does not name the connector; declare it explicitly, e.g.\n`ours: { command: ours-mcp, args: [proxy] }`.\n\nBoth options, and `plugins`, reach an ACP session through the bundled Claude ACP\nagent's `_meta` vocabulary. A role that sets `session_options.acp.command` runs\nan agent fleet did not choose and cannot be promised them, so that combination is\nrefused at validation rather than accepted and dropped. This narrows a role's\ntool surface; it does not stop the harness deferring tool schemas, which is the\nharness's own decision.\n\nCodex `harness_options`: `launcher` (auto, ours-codex, codex), `search`,\n`config`, `add_dirs`, and `monitor`; Codex ACP additionally supports\n`profile`. Native app-server roles reject `profile` because Codex does not\naccept `--profile` for `app-server`. They\nmust express approval and filesystem authority through the shared\n`permissions:` block, never through harness options. The adapter translates\n`ask|auto|allow` to Codex `untrusted|on-request|never` and translates\n`read-only|workspace|unrestricted` to Codex\n`read-only|workspace-write|danger-full-access`. Native `config` currently\nallows only `model_reasoning_effort`; ACP retains the broader Codex config surface.\n\nFor Codex, the opt-in `session: codex-app-server` directly runs Codex's native\nJSONL app server behind Fleet's provider-neutral session contract. It supports native item\nstreaming, commentary/final phases, prompt admission, steering, interruption,\npermission requests, durable conversation projection, and thread resume. Packaged\nCodex brains and the init wizard continue to select `session: acp` for compatibility;\nClaude Code also uses ACP. Native Codex defaults to `codex app-server` (or `ours-codex app-server`\nwhen launcher auto finds it). Override the exact command when necessary:\n\n`session_options: { codex_app_server: { command: [codex, app-server] } }`\n\nAn exact command override receives no appended profile, search, or subcommand\narguments. Native sessions persist their Codex thread in Fleet's `.session-id`\nand participate in its bounded fresh/resume recovery; cross-provider conversation\nportability is never assumed.\n\n## ACP adapters\n\nThe maintained `@agentclientprotocol/codex-acp` and\n`@agentclientprotocol/claude-agent-acp` runtimes are bundled automatically as\noptional ours-fleet dependencies. The supervisor resolves their executable\nentrypoints internally, so default ACP roles do not depend on global PATH.\nours-fleet and both maintained adapters require Node 22 or newer.\n\nOverride an adapter only when necessary with `session_options.acp.command`\n(string or argv list). If optional dependencies were deliberately omitted,\nours-fleet falls back to a compatible globally installed `codex-acp` or\n`claude-agent-acp`. `ours-fleet doctor -c FILE` verifies the resolved adapter.\n\n## Boot-time daemon readiness\n\nLinux permanent-agent services wait for their configured daemon before launching\nFleet's runner. This uses the role's saved manifest path and environment, including\nhost profiles, gateway endpoints and expected daemon identity; no Docker container\nnames or default ports are assumed. The read-only probe requires verified startup\nreadiness and an available identity index. Unavailable or unauthorized daemons leave\nthe service waiting, with a diagnostic code in the journal. Each wait is bounded\nand systemd retries it until the daemon is ready; stopping the service cancels it.\nThis does not consume the agent crash budget or discard its session context.\nOnce the runner starts, its normal crash circuit breaker still applies.\nAfter upgrading Fleet, rerun init to refresh the generated service template.\nTemporary agents and macOS services retain their existing lifecycle behavior.\n\n## Reliable mail wake\n\n`monitor.mode` selects exactly one wake owner:\n\n- `fleet` (default): the ours-fleet supervisor consumes body-free daemon\n events and advances its durable cursor only after delivery is accepted. ACP\n uses live steering when supported and falls back to structured\n `session/prompt`.\n- `native`: ours-fleet starts no supervisor monitor; the generated briefing\n instructs Claude Code or Codex to arm its harness-native wake mechanism.\n\nSet `monitor.interrupt: true` in fleet mode to cancel active work before every\nconfigured wake. Set it to `after_tool` to preserve an active ACP tool (and any\npending permission), then steer the wake at the first tool-terminal boundary\nwithout cancellation. A hung boundary is bounded at 120 seconds and falls back\nto non-cancelling steering/queueing; adapters without authenticated tool events\nuse the same conservative fallback. Explicit human/control interrupts remain\nimmediate. The policy is content-blind because the supervisor cannot inspect\nencrypted message bodies. Message bodies are released only when the role calls\nthe ours `get_messages` tool.\n\nThe default is `false`. For a temporary role whose mission intentionally arrives\nafter its readiness announcement, set `mode: fleet` and `interrupt: true`\nexplicitly. The readiness announcement does not change the transport: the\nmission remains ordinary ours mail, fleet injects only the body-free wake, and\nthe role calls `get_messages` before acting. Every later configured wake uses\nthe same interruption policy.\n\nLegacy `monitor.enabled: true|false` remains accepted as an alias for\n`mode: fleet|native`; use `mode` in new configuration. Codex's separate\n`harness_options.monitor: true` is native-monitor consent, not monitor-owner\nselection.\nInspect `ours-fleet status Name`, `peek Name`, role logs, and\n`~/.ours-fleet/agents/Name/.monitor-status` when diagnosing delivery.\n\n## Trusted owner channel\n\nAny managed role may declare a separate ours identity which fleet \u2014 never the agent \u2014\ncreates when missing and binds:\n\n```yaml\nowner_channel:\n identity: Coordinator Owner Channel\n owners: [authenticated-owner-contact-cid]\n agent: authenticated-managed-agent-cid\n interrupt: false\n progress_interval_ms: 30000\n comments: true\n attachments:\n enabled: true\n max_files_per_request: 4\n max_file_bytes: 10485760\n max_request_bytes: 20971520\n retention_ms: 86400000\n```\n\nPermanent role identities are also reconciled before launch. Fleet creates a\nmissing role identity with local exposure and local auto-accept enabled. A\nmissing owner-channel identity uses the safer inverse policy: both are disabled.\nThe short provisioning lease is released before the agent or channel binds.\nTemporary role identities remain connector-owned because their creating lease\ndefines their cleanup lifetime.\n\nThis does not replace the role identity. Normal identity mail remains untrusted\npeer input: the agent reads it through `get_messages` and replies through\n`send_message`. Mail arriving on the dedicated channel from a CID in `owners`\nis injected as a direct `[fleet-owner]` prompt. Mail from the exact `agent`\nCID is forwarded as a new message to the latest authenticated owner conversation;\nits files may also be relayed through this channel. A reply reference selects the\nowner of that authenticated source wire instead of the latest conversation.\nEvery other CID is rejected and warned about without reflecting its body. Fleet sends\naccepted/queued/progress/interrupted/failure notices and routes the ACP turn's\nfinal assistant text back to the authenticated sender with its source wire ID.\nFor file replies of every kind \u2014 a response artifact, a proactive note, or an\nin-turn attachment \u2014 the agent calls ours `send_file` to the channel identity\nand may pair it with a reply-linked caption; fleet, not the agent, chooses the\nowner. That is the only delivery route an agent is given: a tool call either\ndelivers or reports an error, where a file written to disk does neither.\nOwner messages whose trimmed text starts with `/` are deterministic\nsupervisor commands and never enter the model: `/help` (alias `/commands`),\n`/status`, `/comments [status|on|off]`, `/interrupt`, `/clear`,\n`/compact`, `/model <model-id>`, `/restart`, `/force-restart`, `/ls`,\n`/peek`, `/worklog`, and\n`/version`. Unknown or malformed commands answer with the help text instead of\nbeing forwarded; plain messages reach the agent unchanged. `/clear`,\n`/compact`, and `/model` are forwarded only when the role's bundled ACP\nadapter executes them locally (claude-code: all three; codex: `/compact`\nonly) and are otherwise refused with a notice, so slash text never reaches the\nmodel as a prompt.\n\nThe supervisor advertises the same primary registry entries as recipient-scoped\nours typed commands. Their single arguments field is converted back to the text\nafter the slash command name and enters the same dispatcher; aliases stay\nslash-only. Typed handlers repeat the live authenticated Owner-CID check before\ndispatch and return a null protocol completion because the existing correlated\nowner-channel reply remains the result.\n\nWhile a request runs, the agent's live ACP commentary is relayed as messages\nprefixed with the single stable label `\uD83D\uDFE1 Live update:`, so an owner can see\nexactly which messages the setting controls. `owner_channel.comments`\n(default `true`, so existing channels keep their current behavior) is the\nRESTART BASELINE; `/comments on|off` changes only the running session and is\ndeliberately not persisted, so a restart always returns to the checked-in\nconfiguration. `/comments status` reports the live value, the baseline, and\nwhether the backend emits live comments at all. Suppressing live comments never\nsuppresses receipts, progress notices, or the final answer.\n\nOwner documents, images, and voice messages use the same authenticated sender\nand source-wire boundary. Fleet inspects body-free metadata first and rejects\ndisabled, over-count, or over-size requests before selective\nretrieval. Unauthorized CIDs are never retrieved or answered. Reply-linked text\nand files from the same sender become one ordered request; a file-only wake also\nstarts a turn. Retrieved bytes must match their structured size and SHA-256,\nwhile MIME values, extensions, file categories, and declared-versus-detected mismatches\nremain report-only metadata. Symlinks or non-regular paths fail closed. Sanitized copies live only in a mode-0700 request directory as\nmode-0600 files and are removed after completion or bounded stale retention.\n\nThe legacy `attachments.allowed_mime` key is accepted and ignored so existing\nconfigurations keep loading; it is omitted from resolved configuration and cannot\naffect admission.\n\nVoice prompts include a bounded transcript only when typed daemon metadata reports success.\nFailure or unavailability is explicit and preserves the private audio path as the\ninput for direct review. Run `ours config show --json` and inspect `sttConfigured` without\nrevealing provider credentials.\nA mode-0600 message claim journal stores only wire ID, persistent-history\nsequence, and claim time. Fleet journals the exact body-free oldest-first slice\nbefore calling `getMessages` with that slice length, rejects a returned set\nmismatch, and loads a crash-recovered body only through `getHistoryItem`.\nThe attachment crash journal contains only authenticated CID and wire routing\ndata; it never stores captions, filenames, paths, transcript text, or bytes.\nJournaled read files resume through `getFileInfo` and `fetchFile`. A claimed\nagent caption is loaded from history and rejoined before the group is admitted. Fleet\nresolves one authenticated owner route before retrieving bytes, admits every file\nbefore emitting the caption or any file, and sends every part to that same route.\nUnknown correlated routes remain queued without retrieval and receive one bounded\ncorrelated notice. Admission rejection consumes the whole group with one NACK;\nonce emission starts, a transport error becomes terminal uncertain delivery and\nthe group is never blind-retried. Bounded v2 source-wire routing state is migrated\nfrom v1 on read. Corrupt state disables attachment admission rather than weakening\nprovenance checks.\n\nThe channel identity must be unique and must not be a role identity. The bridge\npersists bounded wire IDs only, never message/reply plaintext, and requeues input\nbefore starting its turn for at-least-once crash recovery. It currently requires\nthe structured agent-session interface so correlated final replies retain their\ndelivery guarantee.\n\n### Live contact and owner administration\n\nThe supervisor which is already running the managed role remains the sole binder of\n`owner_channel.identity`. The CLI reaches that exact live `OwnerChannel`\nthrough the role's token-authenticated, mode-0600 Unix control socket for contact\ninspection and setup; it never starts another ours client and never force-binds:\n\nRapid supervised restart is serialized by a role-scoped single-binder lease.\nThe predecessor closes its authenticated control socket and MCP proxy before\nreleasing ownership. The replacement waits at most five seconds and retries the\ndaemon bind only when PID/start-marker metadata proves the holder was the same\nrole and owner-channel identity. Foreign, live, corrupt, or otherwise\nunverifiable ownership remains fail-closed; fleet never uses `force=true`.\n\nIf that matching predecessor misses the bound, its still-authenticated control\nroute may send one fixed, digest-deduplicated recovery notice through the latest\nauthenticated owner conversation (or the sole configured owner). Notice\nplaintext is never persisted. With no safe deterministic route fleet guesses no\nrecipient and leaves the actionable failure in the web console and role logs.\nThe remote recovery action is `/restart`; inspect repeated failures with\n`ours-fleet logs <Role>` or the web console.\n\n```sh\nours-fleet owner-channel contact list <Role>\nours-fleet owner-channel contact invite <Role> [--name <label>]\nours-fleet owner-channel contact add <Role> (--invite-file <path> | --invite-stdin) [--name <label>]\nours-fleet owner-channel owner list <Role>\nours-fleet owner-channel owner authorize <Role> <exact-64-hex-contact-cid>\nours-fleet owner-channel owner revoke <Role> <exact-64-hex-contact-cid>\n```\n\nContact establishment and owner authorization are separate security steps.\n`contact add` never authorizes: invite redemption is pending until the peer\nverifies it. Once `contact list` reports the established contact, authorize\nits exact immutable CID explicitly. Invite creation emits invite material only\non stdout; acceptance reads it from a file or stdin, not argv.\n\nConfigured `owners` remain the baseline. On legacy channels without `agent`,\nlive authorizations/revocations are an immediately effective, restart-persistent\noverlay. Managed-agent CID gating makes fleet configuration authoritative and\ndisables live owner mutation and direct control-socket sends. `owner list` labels\nbaseline versus dynamic entries and effective status. The atomic mode-0600 file\ncontains bounded CIDs and audit actions only. Corruption disables all effective\nowners and refuses mutation rather than resurrecting authority; revoking the\nlast effective owner is always refused.\n\nA missing/stopped role, role without `owner_channel`, unavailable\nMCP client, or a role entering shutdown returns an actionable error with no\nside effects. Management uses no network listener and never logs or persists\ninvite material.\n\nFor any non-final message\u2014progress, blocker, suggestion, or later proactive note\u2014\nthe managed agent calls ordinary ours `send_message` to the channel identity.\nFleet checks only that the authenticated sender CID exactly equals `agent`, then\nforwards the text as a new message. There is no task/request/update type, phase,\nreply correlation, or owner recipient argument. A sole owner is the safe fallback;\nwith multiple owners and no inbound route history the relay fails closed. Devices\nsharing one identity share its CID; separate owner identities hand off the route\nwhen either sends channel mail. The ACP final is separate: fleet extracts it from\nthe completed turn and deterministically replies to the initiating owner wire.\n\nThe bounded mode-0600 route state stores CIDs, wire IDs, timestamps, delivery state,\nand hashes but never message plaintext. Unauthorized attempts produce a bounded\nCID-only owner warning; attempted bodies are neither reflected nor persisted.\n\nFor a mobile owner, establish the contact first, wait for peer verification,\nauthorize its exact CID, and revoke that same CID when access ends. The bounded\nmode-0600 CID overlay survives supervisor restart and remains fail-closed on\ncorruption. Update bodies remain memory-only. After a crash/restart, unfinished\ndeferred owner input follows the existing at-least-once replay path; the restarted\nsupervisor remains the sole binder.\n\n## Stable config and YAML migration\n\n`ours-fleet config --json` emits schemaVersion 1 resolved plans. Environment\nvalues and mission/persona/bio bodies are withheld; environment keys are sorted\nand values are marked redacted. Additive fields may appear in schema 1, while a\nremoval or semantic reuse requires a new schema version.\n\nYAML parsing always rejects duplicate keys. The current default\n`--yaml-mode compat` warns with file/line/column for anchors, aliases, explicit\ntags, non-scalar keys, and multiple documents. Use `--yaml-mode strict` in CI\nnow; strict becomes the next-major default and compat is the temporary migration\nescape hatch.\n\n## Bounded worklogs, auth proxy, and model recovery\n\nWORKLOG rotation is enabled by default with\n`worklog: { max_kb: 1024, keep_tail_kb: 256, max_archives: 12 }`. Maps may\noverride individual values; `worklog: false` on a role or in defaults opts out.\nFleet rotates only at that role's launch/resume lifecycle boundary. Concurrent\nchanges defer rotation. The active file keeps a bounded UTF-8 tail and advances\nto a line boundary when a complete line fits. If one logical line alone exceeds\nthe budget, its newest suffix remains and the rotation manifest records the\nmid-line start and omitted byte count. The complete prior inode receives a\ncollision-safe UTC archive name, and\n`.worklog-rotation.json` records restart provenance. `max_archives` bounds\nrecent archives beside WORKLOG.md; older complete archives move to\n`WORKLOG.archives/` without deletion. All archives share the role's sensitive\nstate boundary. Fleet refuses a symlinked/non-regular live log or a symlinked\ncold-archive boundary before replacing the live path and best-effort removes a\nduplicate publication left by a detected failure while the original inode is\nstill available. The manifest records SHA-256 digests for the archive and live\nbytes observed when it is written. These checks address ordinary path hazards,\nnot intentional path mutation by a malicious concurrent process with the same\nUnix authority; that is outside the threat model and requires OS-level isolation.\n\nACP tool diffs are bounded before entering web conversation events. Existing\nsmall before/after diffs are unchanged. Oversized whole-file snapshots are\nreduced to the actual changed region plus path, operation, original byte counts,\ndigest, and omission metadata. Each retained side is a newest-content UTF-8 tail\nof at most 64 KiB, advanced to a line boundary when a complete line fits. An\noverlong single line keeps its newest suffix and explicitly records a mid-line\nstart. Paths retain at most a 4 KiB suffix with byte count, digest, and omitted\nprefix metadata; the complete normalized update is capped at 320 KiB. A large\nappend therefore retains current appended content, not the historical prefix.\nThe live web-console transcript includes only the current runner generation and\nexcludes adapter session/load replay. Replayed events remain durable with\nagent_replay provenance for diagnosis and recovery.\n\n`auth_proxy: { kind: anthropic, base_url, required, health_url }` is Claude-only\nand loopback-only. Fleet injects only ANTHROPIC_BASE_URL and doctor rejects\ncredential env keys. The privileged reference companion is\n`contrib/anthropic-auth-proxy.mjs`; deploy it separately as a dedicated account\nwith a 0600 token file and per-role listener access. Fleet never installs it or\nreads its credential.\n\n`model_chain` is an ordered authorization list and its first entry must equal\n`model`. Only sustained high-confidence entitlement/quota 429 evidence advances\none entry. Transient 429, overload, auth, policy, and unknown errors never\ndown-shift. Runtime state is atomic in .model-recovery.json; exhaustion is\nfail-closed and held down. Change the declared chain/model and restart to\nreconcile explicitly; no chain preserves detection-only behavior.\n";
7
+ export declare const AI_DOCS = "# ours-fleet reference\n\nours-fleet runs persistent or temporary, identity-bound AI roles through a\nprovider-neutral managed-session interface:\n\n- harness: `claude-code`, `codex` or `hermes`\n- session: `acp` (default) or `codex-app-server` (Codex only)\n- lifetime: permanent (supervised, restartable) or `spawn --temp`\n\n## Codex ACP runtime\n\nFleet bundles codex-acp 1.10.0 (Codex ^0.153.3). Native shell Codex upgrades do\nnot upgrade this dependency. Selection: Agent env.CODEX_PATH, then inherited\nservice CODEX_PATH, then the adapter's bundled Codex. Use an absolute executable\npath without arguments; an empty override selects the bundle. Fleet retains its\npermission proxy around the selected runtime. Custom ACP commands and native\ncodex-app-server sessions own their separate launch behavior.\n\nDoctor reports native and ACP-selected executable paths and versions per configured\nACP Agent, including the platform binary behind the npm launcher, and flags drift\nor unsupported/unknown combinations. Run it in the service's environment; it probes\ncurrent configuration, not existing processes. gpt-6-astra on a bundled ACP launch\nrequires Codex >=0.153.3; older runtimes fail with an actionable launch error.\n\nPersistent overrides belong in Agent env; temporary overrides belong in the Agent\nTemplate before spawning (existing temporary launch snapshots are sealed). Service\noverrides must reach both persistent and temporary services. New temporary\nsupervisors inherit CODEX_PATH (including empty) from the spawning process through\nsystemd-run/launchctl; sealed Agent env still wins. A shell export does\nnot update running services; init does not persist CODEX_PATH. After verification,\nrestart only affected agents, or coordinate replacement of a temporary agent with\na stale sealed override. Reinstall Fleet with optional dependencies to repair its\nbundle. Custom ACP runtime provenance is reported as unknown.\n\n## Discover and validate\n\nHermes uses `harness: hermes`, `session: acp`, and an explicit Brain `model`.\nProvision native provider/credentials with the role stopped, pointing HERMES_HOME\nat `<role-state-dir>/harness/hermes`. Fleet manages model.default and manual\ndangerous-command approvals; the home retains native memory, skills and state.\nUse a literal native model.provider so startup can detect model/provider fallback.\nEvery restart receives the full briefing in a fresh conversation; old ACP IDs\nare not restored. Use Fleet monitoring. Home/plugin MCP providers must be disabled;\nours and optional extras are declared through ACP. Tool availability is unverified\nuntil actual use, without a startup probe prompt.\n\nHermes approval ask/auto/allow maps to default/accept_edits/dont_ask. Terminal and\nwrite_file/patch approval is not universal mediation of browser, memory, skills,\ndelegation or MCP. Fleet waits at most 50 seconds for permission, against the\ntested native dangerous-command timeout of 60 seconds. Workspace confinement is\napproximate without verified OS isolation. Read-only, legacy deny, Fleet provider\noverrides, effort, model_chain, native monitoring and after_tool are unsupported.\nHERMES_HOME is not a complete isolation boundary for native external configuration\nor credential fallbacks. Process supervision uses the existing Fleet lifecycle.\nTested Hermes: 0.21.1, source d15ed4445207dda418b984e8bda0f68f48b8c6f3,\nPython ACP 0.9.0, protocol 1. Unknown builds need compatibility conformance first.\n\n```sh\nours-fleet docs # this complete reference (`man` is an alias)\nours-fleet help <command> # exact flags for one command\nours-fleet config [-c FILE] # validate and print the merged plan; no changes\nours-fleet doctor [-c FILE] [--harness codex|claude-code|hermes]\nours-fleet version [--json] # build identity, capabilities, every install on PATH\n```\n\nConfiguration v2 is `~/fleet.yaml` plus typed bare documents under the exact\nstem directories `~/fleet/agents`, `~/fleet/agent_templates`, `~/fleet/roles`, `~/fleet/brains`, and\n`~/fleet/room_templates`.\nThe manifest owns fleet-wide operational defaults and automation; each Agent\nselects one inline/ref Role and Brain and carries its operational fields.\nAgent Templates under `~/fleet/agent_templates` are inert reusable launch definitions;\nonly explicit files under `~/fleet/agents` are persistent lifecycle instances.\nRoom members use `agent_template` and receive immutable content-addressed snapshots.\nAn Agent Template may declare up to 64 temporary-only named `loops`; persistent Agent\ninstances reject them. Each loop requires `interval` (1m..30d) and bounded\nnonblank `prompt`, with optional `enabled` (default true), `initial_delay`\n(default interval, 0s..30d), and `jitter` (default 0s, < interval, <=1h).\nExecution is fixed skip-if-busy with no ordinary missed-tick backlog/replay;\nrestart recovery may preserve at most one recent late occurrence.\nLegacy top-level `roles:` and `fleet.d` are rejected. Validate the complete\ntrusted source set with `config` and `doctor` before starting or restarting.\n\nPermanent `spawn` writes `~/fleet/agents/Name.yaml`. The web console edits an\nexplicit `{manifest, agents, agent_templates}` model while Role/Brain presets remain read-only.\nIts aggregate revision includes every Agent/Role/Brain/Room-template source, previews a\nredacted per-document diff in an exact-stem private staging tree, and saves under\none root lock with a private multi-file backup and full rollback. A no-op is\nbyte-identical and creates no backup.\n\n## Build identity and install provenance\n\n`--version` prints a semver and nothing else, and a semver does NOT identify an\nartifact. Version bumps land in a release commit of their own, so every build cut\nbetween two releases carries the PREVIOUS version while already containing new\nbehaviour. One host ran two installs that both reported 0.16.0 \u2014 same version,\ndifferent build. One accepted `monitor.interrupt: after_tool`, the other\nrejected it as invalid. Their\n`dist/cli.js` were byte-identical \u2014 the divergence was in other modules.\n\nEvery build therefore stamps `dist/build-info.json` with a build id (first 12 hex\nof a sha256 over the rest of `dist/`), the commit it was cut from, and the\ncapability tokens the shipped code declares \u2014 for example\n`monitor.interrupt.after_tool`. Ask any executable what it is:\n\n```sh\nours-fleet version # ours-fleet 0.17.0+9f1c2a3b4d5e, capabilities, PATH installs\nours-fleet version --json # the same as machine-readable JSON, no environment values\n```\n\nRead a capability, never a version number, to decide whether a setting is\nsupported. When a build rejects a value it knows the name of, it says which\ncapability is missing and which build rejected it, because another install on the\nsame host may accept the identical file. `config` prints the build that resolved\nthe plan; `status <Name>` says so when the build reporting on a role is not the\none that created it (roles record their creating build in `creation.json`).\n\n`ours-fleet doctor` runs an `install` check that lists every `ours-fleet` on\nPATH plus the one executing, and FAILS when two installs share a semver but are\ndifferent builds, or when the running artifact is a DIFFERENT artifact from the\none PATH resolves to. A second prefix holding identical content is not a skew\nand is not reported. A PATH entry the shell would not execute \u2014 a directory, or\na file without its execute bit \u2014 is not counted as an install at all.\nInstalls built before this stamp existed report `+unknown`; they are compared by\nhashing their `dist/` instead, so two pre-provenance installs are still told\napart. To fix a flagged host, remove or update the stale install \u2014 do not rely on\nPATH order.\n\n## Lifecycle and console commands\n\n```sh\nours-fleet init [-c FILE] # interactive missing-default seed; TTY required\nours-fleet up|down [Name...]\nours-fleet restart [Name...] # preserve/resume harness context\nours-fleet force-restart [Name...] # fresh context; briefing is reloaded\nours-fleet ls\nours-fleet status|peek|attach|logs Name\nours-fleet logs -f Name\nours-fleet send Name \"prompt\"\nours-fleet rm Name\nours-fleet watchdog-report <name> [run-id] [--list] [--json]\nours-fleet watchdog-run <name>\n```\n\n`init` is an interactive missing-default workflow that preserves existing files byte-for-byte.\nIt names the resolved manifest and split directory, asks a default-No confirmation,\nthen subscriptions (Codex, Claude, or both), model assignment (one explicit model for\nevery job or explicit development/review/coordination choices), one reasoning level,\nand a final default-No review. Quick/Balanced/Thorough generate low/medium/high.\nModel pickers show packaged supported IDs; catalog membership is not a recommendation\nor entitlement claim. Run `ours-fleet doctor` for local Codex availability; Claude\nentitlement is checked when a role launches. The generated brains contain no\n`model_chain`, so Fleet never silently substitutes another model.\n\nAt either confirmation, N, Enter, Escape, Ctrl-C, Ctrl-D, or EOF cancels. In a picker,\nEscape, Ctrl-C, Ctrl-D, or EOF cancels; Enter records the highlight or continues a\nnon-empty multi-select, N is ignored, and an empty subscription selection remains blocked.\nEvery cancellation before final approval performs no host or configuration mutation.\nRedirected/non-TTY invocation is refused with the same guarantee. After the final Yes,\nhost setup precedes publication. Path ownership/type/mode/symlink and\nsame-filesystem checks run before host setup and again under a per-setup lock. Publication\nstages and validates the complete combined setup, backs up whichever old target(s) exist,\nand retains a private recovery record. A hard process/host termination cannot promise\nrollback; inspect host integration and private init stage/recovery evidence.\n\n`peek`, `attach`, and text `send` use the structured agent session.\nAttachment also accepts `/permit <permission-id> <option-id>`, `/interrupt`,\nand `/detach`.\n\n## Local web console\n\nThe npm package includes the web console; installed users do not clone the repo\nor run `npm run build`:\n\n```sh\nnpm i -g @ours.network/fleet\nours-fleet init\nours-fleet doctor\nours-fleet web # install/update service, start, pair browser\n```\n\nThe normal command uses stable `http://127.0.0.1:49271/`, installs an\nowner-level systemd user service (Linux) or LaunchAgent (macOS), and opens a\nfive-minute one-use pairing link in the local browser. After pairing, bookmark\nthe plain URL or install the PWA. To pair a new, signed-out, or revoked browser,\nrun `ours-fleet web open`.\n\n```sh\nours-fleet web status\nours-fleet web start|stop|restart\nours-fleet web open\nours-fleet web revoke-all # revoke every browser and active session\nours-fleet web uninstall\nours-fleet web serve --port 0 --no-open # isolated foreground/testing mode\n```\n\nThe console is IPv4-loopback-only by default. Both `localhost` and\n`127.0.0.1` are accepted locally. For an nginx/TLS reverse proxy, keep the\ndefault bind and declare the exact browser origin:\n\n`ours-fleet web install --public-origin https://fleet.example.com --password-file /secure/fleet-password`\n\nFleet reads the password file during setup and persists only a salted scrypt\nverifier. New browsers authenticate and retain rotating HttpOnly/SameSite\ntrusted-device credentials. If nginx already authenticates, the operator may\ndeliberately select `--no-password`; the CLI and browser warn that anyone\nreaching the origin can control the fleet. First setup requires an explicit\nchoice: `--password-file` or `--pairing` for protected access, or\n`--no-password` for intentional unprotected access.\n\nUse `--bind ADDRESS` only for an intentional direct listen. A non-loopback\nbind is rejected unless `--public-origin` is also present. Host/Origin checks\nuse the declaration and do not trust forwarded headers. Configure nginx to\nproxy HTTP and WebSocket upgrades to `127.0.0.1:49271` and terminate TLS;\nfleet accepts nginx's loopback upstream Host, so no Host rewrite is required.\nBrowser credentials add Secure for HTTPS, and `revoke-all` invalidates all\ntrusted devices. Role creation offers harness-scoped known-model choices\nwhile still accepting a typed model ID; blank explicitly uses the selected\nharness's own default.\n\n## Spawn\n\n```sh\nours-fleet spawn [--temp] [Name | --name Name] \\\n --brain BRAIN_ID --role ROLE_ID \\\n --cwd /absolute/path --identity Identity --coordinator Coordinator \\\n --approval ask|auto|allow \\\n --filesystem read-only|workspace|unrestricted \\\n --unattended deny|wait --isolation-file /path/isolation.yaml\n```\n\nPermanent spawn writes `~/fleet/agents/Name.yaml` and starts a supervised role.\n`--temp` writes active state under `~/.ours-fleet/tmp` and starts an independent\ntransient supervisor (a collected systemd unit or submitted launchd job). It is\nnot enabled across reboot and does not die when the role that spawned it restarts.\nBrain definitions own the managed session backend. When a temporary role's bound identity\ncloses or its session ends, the supervisor, monitor and live roster entry retire\ntogether; state moves intact to `~/.ours-fleet/recovery/temporary` with a\ntermination record. Failed launches use the same archive rather than deleting\ntheir briefing, provenance, logs or partial supervisor metadata.\n\nNamed `down` and `rm` commands can target an exact state-backed temporary role\neven though it is absent from merged fleet YAML. The recorded transient unit/job\nis authoritative. Missing/incomplete ownership metadata is reconciled only from\nan exact `_run-temp <role>` process-table match: one match may be adopted, zero\nsettles as stopped, and ambiguity or an unreadable table fails closed. Launching\nrecords receive a bounded grace so a not-yet-registered transient unit cannot be\nmistaken for a stopped one. Stale recorded supervisors are reclaimed in bounded\nbatches by moving their state to the same recovery archive, never by blind deletion.\n\nThe Fleet supervisor creates and binds the assigned identity before the harness\nstarts. Temporary identities survive harness reconnects and are released only when\nthe logical run ends. Permanent identities remain available after the supervisor\nreleases ownership. Agents receive ready tools; no model-owned identity bootstrap\nor onboarding skill is required.\n\nThe temporary supervisor provisions identity and verifies room readiness before\nstarting the harness. After readiness, only sustained authoritative absence\ncloses the role. Unreachable, malformed, or\nvalid-but-empty daemon indexes are ambiguous and reset closure debounce rather\nthan becoming cleanup authority.\n\nInside a managed role, public `ours-fleet` commands cross an authenticated\nsupervisor attribution boundary before Commander parsing. The original CLI remains\nthe executor inside the role's existing OS sandbox, and ordinary CLI validation is\nthe source of truth. Hidden worker entry points remain internal; public lifecycle and\noperator commands are not restricted by the proxy.\n\nCommand invocation, raw argv, read-only work, validation failures, and generic\noutcomes are never forwarded to the Owner-visible channel. Fleet announces only\nconfirmed Agent, Task, and Room lifecycle changes. Local diagnostics retain\nstructurally redacted command metadata. Lifecycle delivery uncertainty is logged,\nnever recursively announced, and never reruns or blindly retries an effect.\nRoom participant summaries describe creation and activation. Fleet has no public\npost-create Room membership mutation, so it does not claim a separate membership event.\n\nOmitted Brain and Role selections, working directory, coordinator, neutral permissions,\nand fleet monitor policy inherit from the calling Agent. Explicit options always win.\nIdentity, mission/profile text, environment, owner routing, auth proxy, room startup,\nisolation, worklog, and sensitive inline Brain values never inherit implicitly.\nThis automatic proxy is a convenience and\nattribution mechanism, not an isolation boundary: an unrestricted role can still\ninvoke another binary path directly. Host/operator shells keep the ordinary direct\nCLI behavior.\n\nBrain owns harness, session, model, reasoning effort, token limits, and native harness\noptions. Removed runtime flags are rejected with migration guidance rather than silently\nreinterpreted. A selection is a stable ID or an explicit `inline:{...}` mapping.\n\nPackaged Developer, Critic, and LocalCoordinator Agent Templates set\n`monitor: { mode: fleet, interrupt: after_tool }`. Every member of the standard\n`single`, `pair`, and `team` Room Templates therefore resolves to fleet-owned,\nafter-tool delivery. Explicit per-member and custom Agent Template values remain\nauthoritative and merge key by key.\n\nManaged ACP stall recovery is opt-in: `monitor: { mode: fleet, stall_recovery: true,\n stall_timeout_ms: 900000 }`. The validated timeout is 60000\u201386400000 milliseconds.\nOmitted/false preserves existing behavior. Silence is measured from meaningful live\nprogress; replay and retry chatter do not reset it. Strong authenticated retry\nevidence requires one window, generic silence two. Tools, permissions, modal or\nunknown boundaries, steering and human cancellation protect the turn. Recovery\nuses one bounded cancel and diagnostic continuation in the same ACP session/queue\nslot; it never restarts the adapter. Startup and queued mail wait behind recovery.\nA durable claim permits at most one automatic attempt per ACP session ID, including\nacross supervisor restarts; later mail is non-cancelling. Failed or re-stalled\nrecovery reports a blocker. Inspect `.stall-recovery/audit.jsonl` and recorded\nterminal events before continuing; never replay ambiguous or completed mutations.\nNative monitoring/non-ACP sessions are unchanged. No room or identity coupling.\n\n## fleet.yaml\n\n```yaml\napi_version: ours.network/fleet/v2\nvars:\n work_root: /home/me/work\nstart_stagger_ms: 0\ndefaults:\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n monitor:\n mode: fleet # fleet (default) | native\nwatchdogs:\n nightwatch: # [A-Za-z0-9_-], must not collide with a role name\n coordinator: FleetCoordinator # required \u2014 where alerts go\n # everything below is optional\n enabled: true # default true; false = configured but never scheduled\n interval: 10m # default 10m; 30s | 10m | 2h, minimum 1m\n watch: [Alice, CodexReviewer] # explicit lists are exact; omit for configured + live temp roles\n agent: { ref: WatchdogAgent } # required: declared Agent ID, or canonical inline Agent definition\n identity: Watchdog-nightwatch # default: Watchdog-<name>\n timeout: 5m # default 5m; a run past this is killed and recorded as error\n keep_reports: 50 # default 50 reports retained per watchdog\n alert_cooldown: 60m # default 60m before the same finding alerts again\n prompt_file: /abs/extra.md # optional extra focus, APPENDED to the fixed contract\n```\n\nAn Agent is a separate bare document under `~/fleet/agents/<ID>.yaml`:\n\n```yaml\nrole: { inline: { mission: Coordinate work and delegate implementation. } }\nbrain: { inline: { harness: codex, session: codex-app-server, model: gpt-model-id } }\nidentity: Coordinator\ncwd: ${work_root}/project\noversee: [{ agent: Worker, interval: 5m }]\n```\n\nA watchdog observes and reports; it never restarts, stops, spawns, or removes a\nrole, answers a pending permission, edits a workspace, or approves anything on\nthe owner's behalf. `watchdogs:` may appear only in the base config\n(`~/fleet.yaml` or `-c FILE`); Agent/Role/Brain documents never own it.\nThe selected Agent owns Brain, Role, permissions, isolation, and every other\nagent setting. Legacy watchdog `harness`, `model`, `session`, and\n`isolation` fields fail with migration guidance.\nWhen `watch:` is omitted, each run watches the configured roles plus temporary\nfleet roles that are live when the run starts. An explicit `watch:` list is\nnever augmented.\n\nAgent operational values override manifest operational defaults. Role and Brain\nownership never cross-merges. `${name}` substitutes entries from `vars`.\nBrain fields include `max_tokens` and `autocompact_pct`; isolation is Agent-owned.\nUse README.md for the complete isolation policy and resource-cap schema.\n\nSupervised roles connect to the operator-configured ours daemon; they do not own its\nlifecycle. Fleet strips the obsolete, presence-sensitive `OURS_AUTOSTART` variable from\nagent-session children; `ours-mcp proxy` is client-only and never starts a daemon. Start\nthe shared daemon only through an explicit operator or installer/setup flow.\n\n## Rooms and tasks\n\n`init` materializes editable `single`, `pair`, and `team` Room templates plus\ntheir exact-cased Agent, Role, and Brain presets. The command prints the packaged\npreset revision and source directory. Inspect provenance and content before use:\n\n```sh\nours-fleet config [-c FILE]\nours-fleet template list [-c FILE]\nours-fleet template show team [-c FILE]\nours-fleet task create --title \"Solo task\" --template single [-c FILE]\nours-fleet task create --title \"Reviewed change\" --template pair [-c FILE]\nours-fleet task create --title \"Phased delivery\" --template team [-c FILE]\nours-fleet spawn --temp Scout --role SELECTION --brain SELECTION --loops-file PRIVATE.yaml\nours-fleet task start TASK --member developer --loops-file PRIVATE.yaml\nours-fleet task start TASK --member critic --no-loops\n```\n\n`--loops-file` must be an owner-only, non-symlink regular file <=1 MB containing\nexactly one top-level non-empty `loops:` mapping. `--no-loops` explicitly\ndisables loops. They are mutually exclusive and rejected for permanent spawn.\nFor a grouped room member the whole CLI block overrides its Agent Template;\nthe template overrides omission. Omission preserves historical no-loop behavior\nand never inherits manifest wildcard loops. Fleet validates before side effects,\nseals normalized timings plus exact private prompts, and reuses that snapshot for\nidempotent start, retry, recovery, and replacement. Trusted authoring and the\nprivate sealed runtime retain exact prompts; resolved launch, task, room,\nprovenance, and audit presentations show source, policy, timing, prompt bytes,\nand prompt SHA-256 instead of prompt text.\n\nAn alternate manifest `-c /path/custom.yaml` uses `/path/custom/` as its split\nroot. Repeated init only fills missing files and never adopts a newer default.\nExact known revision-3 through revision-5 packaged-bootstrap and generated role defaults have an exact-semantic,\nfail-closed migration (dry-run by default):\n\n`ours-fleet migrate-role-defaults [-c FILE]`\n`ours-fleet migrate-role-defaults [-c FILE] --write`\n\nReview removals, replacements, additions, preserved custom files, staging path,\nand recovery path. Same-named custom files stay byte-identical; dangling custom\nreferences refuse publication, and a successful rerun is a no-op. For explicit\nsingle-file adoption, copy from init's reported packaged source beside the target\nas `.new-default`, inspect `diff -u TARGET TARGET.new-default`, then replace it.\n\nThe exact generated six-worker legacy starter Agent set has its own fail-closed\nmigration (dry-run by default):\n\n`ours-fleet migrate-agent-templates [-c FILE]`\n`ours-fleet migrate-agent-templates [-c FILE] --write`\n\nReview the dry-run moves, addition, staging path, and retained recovery-backup path\nbefore `--write`. Customized/partial known starters and unsafe trees refuse without\nmutation; unrelated custom persistent Agents remain persistent. A manifest-level template\nmay shadow a same-named file only with `override_builtin: true` and a higher\nversion; this compatibility marker is deprecated and reported as a diagnostic.\n\nRooms always use `ours-cowork`; there is no room-provider selector. Configure\nthe cowork daemon connection and room owner directly:\n\n```yaml\nrooms:\n cowork:\n config: /home/me/.ours-cowork/config.json\n owner:\n expected_cid: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n public_invite_file: /home/me/.ours-fleet/owner-room-invite.txt\n defaults:\n template: team\n attach_owner: true\n close_when_task_done: true\ntasks:\n default_room_template: team\n create_mode: start\n close_room_on_done: true\n```\n\nFleet launches each template member with a dedicated one-time Cowork invite.\nThe supervisor accepts the startup invite and verifies the provisioned identity CID\nin the authenticated room seat before starting the model. The generated briefing\ncontains the assigned identity, room, role and task, without invite material.\nA room is ready only after the Cowork room (and its task, when\ntask-bound) is durably active, every configured member seat is authenticated and active with its matching\nlive Fleet launch, and the configured Owner seat is active when owner attachment is\nenabled. There is no briefing hash, startup ACK, or separate role-briefing readiness\ngate.\n\nNormal Task provisioning emits exactly one authenticated Owner lifecycle notice after\nthe ready predicate above is true; standalone Room provisioning likewise emits one\nready notice. Intermediate task, saga, member-spawn, timeout, and recoverable-failure\ntransitions stay in local state and logs. A terminal failed Task emits one actionable\nfailure notice; the command result carries the exact blocker and canonical recovery\naction.\n\n`task start` and create-and-start wait for readiness. If their bounded wait expires,\nthey return an explicit `in_progress` result and start a safe continuation. The detached\ncontinuation is serialized per Task and remains alive until convergence or an\nOwner-action blocker. Re-running `task start <id>` safely resumes the same durable\nprovisioning operation after the blocker is corrected or a process restarts.\n\nSet `room.anonymous: true` on a room template, or pass `--anonymous` to\n`task create`, `task start`, `task work`, or `room create`, to create an\nanonymous Cowork room. `--no-anonymous` explicitly overrides an anonymous\ntemplate. Fleet records the resolved value before room creation so retries keep\nthe same choice. The supervisor creates anonymous room members with local contact-book exposure\ndisabled. Their generated briefings\ndo not disclose or compare an Owner participant CID. A participant-originated\ninstruction has Owner authority only when the authenticated Cowork room envelope\nattributes that participant seat the exact `Owner` role. Literal text, display\nnames, ordinary direct messages, and room-authored or rest-role messages with an\nOwner-looking label never grant that authority. Non-anonymous rooms remain pinned\nto the exact authenticated Owner CID.\n\nHuman task and room results use the same compact Markdown presentation in the\nCLI and authenticated owner channel: a short heading, icon-plus-word status,\ncode-formatted identifiers, bounded summaries, and actionable recovery or error\nsteps. Untrusted prose is context-escaped and control characters are neutralized;\nMessenger-bound results are capped at 3,500 Unicode code points and 12,000 UTF-8\nbytes with structural omission notices. `--json` bypasses this presentation layer\nand retains the versioned machine schema and serialization order.\n\nActive task `show` reports current readiness separately from lifecycle, also in\nOwner commands. Start/work preserve the fail-closed `task_not_ready` behavior.\nUnknown control health does not prove a dead agent or authorize replacement.\nBefore deliberate recovery, Fleet Coordinator inspects current seats and exact\nold-launch worklogs/termination evidence in `recovery/temporary`, preserves\noriginal records, and privately records verified old/new launch provenance.\nA new inbox is not complete history: page scoped history to the end, authenticate\nhistorical Owner authors, and compare completed actions and replies before resuming.\nDo not restore archived identity state, reuse invites, adopt same-role seats or\nblindly replay instructions. Ask the Owner if authority/completion is uncertain.\n\nEvery task belongs to a named list. The built-in `default` list always exists,\nand legacy tasks or create calls without `--list` resolve to it. Use `task lists`,\n`task list-create <name>`, `task list-rename <name> <new-name>`, and\n`task list-delete <name> [--move-to <destination>]` to manage lists. A non-empty\nlist cannot be deleted without an explicit, different destination; Fleet moves\nthe assignments and never deletes the tasks. `task move <id> --list <name>`\nchanges only organizational metadata. `task list --list <name>` filters and\n`--group-by-list --json` returns deterministic groups.\n\nList names are NFC-normalized, case-sensitive, and limited to 64 Unicode code\npoints. Leading/trailing whitespace, controls, format/path characters, normalized\nduplicates, and the reserved exact name `default` are rejected. The authenticated\nowner channel provides the matching `/task` subcommands, while authenticated web\nclients use `/api/v1/task-lists`, `/api/v1/tasks`, and\n`/api/v1/tasks/:id/list`; every adapter delegates to the same application service.\nMessenger's multiline command grammar treats surrounding whitespace on each\nvalue line as transport framing; the canonical value passed to the shared service\nis the trimmed line. CLI arguments and REST strings are passed verbatim.\n\nOlder prerelease files with the exact legacy `provider: cowork` key under\n`rooms:` still load, but the key is ignored and omitted from resolved\nconfiguration. Remove it when editing the file. Any other legacy value is an\nerror. The optional `rooms.owner.provider` setting is separate and defaults to\n`messenger-server`.\n\nFinish and Delete are distinct terminal task actions:\n\n`ours-fleet task finish <id>` moves an active or review task to `done` and\ndeletes its associated Cowork room after retiring its members. The room then\ndisappears from normal Fleet and Cowork views; its brief, messages, repository\nreferences, and attachments are not retained as an inspectable archive.\nThe prerelease configuration names `tasks.close_room_on_done` and\n`rooms.defaults.close_when_task_done` are retained for compatibility, but\n`true` now means this close-then-delete behavior.\n\n`ours-fleet room delete <id> <id>` is the canonical destructive room command.\n`room close <id> <id>` remains a deprecated alias with identical deletion\nsemantics. Older prerelease `closed` room records are deleted directly the next\ntime `room list` reconciles Fleet with Cowork.\n\n`ours-fleet task delete <id> <id>` permanently deletes a task in ANY lifecycle\nstate \u2014 backlog, provisioning, active, blocked, review, done, cancelled, failed,\nor partially settled. The exact task ID is required twice for confirmation. A\ndurable deletion intent is persisted before any side effect; the cleanup worker\nthen retires managed room members with evidence, closes and deletes an attached\nroom (tolerating already-missing remote rooms), releases the sealed launch\nsnapshot, and unlinks the task record last. While cleanup settles the task is\nhidden from normal listings and every lifecycle mutation is rejected; if\ncleanup cannot complete (for example Cowork is unreachable), the deletion stays\nin a precise resumable state \u2014 repeat the identical delete command to converge\nafter outages, crashes, or restarts. Deletion\nnever fabricates a `done` transition. A metadata-only deletion receipt\n(acceptance actor, original state, timestamps, completion) is retained under\n`deletion-receipts/` as durable audit evidence. An already-missing task is an\nidempotent no-op. The Owner-channel equivalent is `/task delete <id> <id>`; the\nmanagement API equivalent is `DELETE /api/v1/tasks/<id>?confirm=<id>` (200 when\nsettled, 202 while pending; `GET /api/v1/tasks?includeDeleting=true` exposes\ndeletion-pending tasks to operators).\n\n## Permissions\n\nPrefer the harness-neutral `permissions` block:\n\n- `approval: ask|auto|allow`: portable permission policy. `deny` remains a\n deprecated, fail-closed compatibility alias for existing fleet files.\n- `filesystem: read-only|workspace|unrestricted`: filesystem intent\n- `unattended: deny|wait`: what a managed session does when no console can\n answer a request\n\nThe backend translates this common intent. Native Codex app-server roles reject\npermission aliases in `harness_options`; Codex ACP and Claude retain their\nlegacy native-override compatibility. Do not choose `allow`/`unrestricted`\nor Claude `bypassPermissions` without explicit authorization.\n\n### Creation-time isolation\n\n`ours-fleet spawn --isolation-file <path>` supplies a role's sandbox policy at\ncreation, so the FIRST launch is already confined \u2014 a role that only gains\n`isolation:` on a later `up` ran unsandboxed until then.\n\nThe file holds exactly the `isolation:` mapping documented above and nothing\nelse \u2014 the same schema, validated by the same code, so a policy written here\ncannot mean something different from the identical block in fleet.yaml:\n\n```yaml\nnetwork: deny\nfs:\n read: [/opt/reference]\nresources:\n mem: 2G\n```\n\nInvalid files are rejected before anything is created: no config, no state\ndirectory, no identity reservation. Works for both permanent and `--temp` roles.\n\n### Never-prompt failure\n\nThe failure this section exists to prevent leaves no error message anywhere.\n\nAn unattended role has no console. When the harness needs a permission decision\nthere is nobody to ask, so the request is refused INSIDE the harness \u2014 no\nprompt, no error, no log line. The agent simply does less than its briefing told\nit to, reports success, and nothing distinguishes that from having done the\nwork. Two settings produce it:\n\n1. a permission mode that suppresses the prompt without granting the action\n (Claude `dontAsk`, which is why neutral `allow` maps to\n `bypassPermissions` instead); and\n2. `unattended: deny`, which refuses every request that reaches it.\n\n**Automatic decisions are now recorded.** Every permission request decided\nwithout a human emits a completed event into\n`~/.ours-fleet/agents/<Name>/.session-events.jsonl` carrying the decision,\nwhether policy or a person made it, the policy that produced it\n(`permissions.unattended=deny` vs `permissions.approval=deny`/`=allow`),\nthe reason, and the option selected. `ours-fleet peek` and `attach` render\nthem. Automatic denial asks for a one-shot rejection, never a standing one, so a\nsingle unattended refusal cannot disable a tool for the rest of the session.\n\nA role that can auto-deny logs one line at startup saying so.\n\nTo detect an under-permissioned role BEFORE it runs, use the capability floor\nbelow: `ours-fleet doctor` fails such a role rather than letting it discover\nthe problem silently at work.\n\n### The unattended capability floor\n\nAn unattended role has no console, so a permission request cannot be answered \u2014\nit is refused, silently, inside the harness. The agent then does less than it\nwas told to and reports no error. To make that visible before launch,\n`ours-fleet config` and `ours-fleet doctor` resolve each role's neutral\npermissions through its harness and check the result against a fixed floor:\n\n- `read-state` \u2014 read its briefing, ROUTINES.md, and WORKLOG.md\n- `write-state` \u2014 append its WORKLOG and its own state files\n- `messaging` \u2014 bind its identity, send and receive ours mail\n- `monitor` \u2014 arm and observe its mail monitor\n- `workspace-edit` \u2014 edit and test files in its working directory\n- `status-commands` \u2014 run the inspection commands its briefing prescribes\n\n`doctor` reports this per role as `unattended floor: <Role>`. A role with\n`unattended: deny` that cannot meet the floor FAILS doctor, because it will\ndeny those requests with nobody to see it; with `unattended: wait` it warns,\nbecause a human can still attach and answer.\n\nSecurity meaning: native Codex maps `ask` \u2192 `untrusted`, `auto` \u2192\n`on-request`, and `allow` \u2192 `never`; its independent filesystem mapping is\n`read-only` \u2192 `read-only`, `workspace` \u2192 `workspace-write`, and\n`unrestricted` \u2192 `danger-full-access`. Users configure only the Fleet names.\nClaude maps `ask` to `default`, `auto` to `acceptEdits`, and `allow` to\n`bypassPermissions`. Codex ACP retains its adapter-specific coupled modes:\n`auto` selects `agent`, while `allow` selects `agent-full-access`. These\nmodes genuinely permit the actions the role was authorized to take \u2014\n`dontAsk` only suppresses the prompt while still refusing the action. Nothing\nother than an explicit `allow` becomes non-interactive. Legacy `deny` keeps\nits conservative Codex `on-request` / Claude `plan` translation. `allow` is therefore a real grant and\nrequires explicit authorization; per-role `isolation:` remains the outer\nboundary that a permission mode cannot cross.\n\nACP carries agent-advertised session mode IDs and `session/set_mode`, but those\nIDs are agent-specific and ACP defines no portable permission-policy capability.\nFleet therefore uses the ACP primitive where an adapter exposes a matching mode\nand otherwise performs the harness translation above. The bundled Codex ACP\nadapter couples approval and sandboxing in its advertised mode IDs. Neutral\n`allow` therefore selects `agent-full-access` and widens `filesystem:\nworkspace` or `read-only` to `danger-full-access`; neutral `auto` selects\n`agent` and `workspace-write` even when the neutral filesystem value differs.\nAn explicit `harness_options.sandbox` selects its corresponding ACP preset and\nstill wins, as does an explicit native approval override. `config` and\n`doctor` report a coupled-mode mismatch as approximate. Use per-role\n`isolation:` as the outer boundary for an `allow` ACP role. The live session\nreports both its effective normalized mode and the exact native mode selected.\n\nSee also: `spawn --approval/--filesystem/--unattended` set this intent at\ncreation, and `ours-fleet config` prints each role's neutral settings, their\nnative translation, and any warning \u2014 the same text `doctor` reports.\n\nClaude `harness_options`: `permission_mode` (default, acceptEdits, plan,\ndontAsk, bypassPermissions), `plugins`, `mem_palace`,\n`mem_palace_midsession_autosave`, `mcp_servers` and `mcp_servers_only`.\n\n`mcp_servers` declares MCP servers for the role, in `.mcp.json`'s own shape\n(a map of name to `{ command, args, env }`, or `{ type: http|sse, url,\nheaders }`). By default they are ADDED to whatever the OS user running the role\nalready has configured. The Claude Code adapter sends them in `session/new`.\n\nWhen `mcp_servers` is absent, Fleet sends ACP's protocol-required empty\n`mcpServers` array without an exclusive override, so the agent keeps its inherited\nservers. An explicitly empty configured set is different: Fleet preserves that intent\nthrough the bundled adapter's compatibility path and disables every inherited server.\n\n`mcp_servers_only: true` makes the declared set EXCLUSIVE through\n`strictMcpConfig`. It is all-or-nothing and it ignores every\nother MCP configuration: project `.mcp.json`, user settings, and **plugins**.\nThe ours connector is normally installed as a plugin, so a strict role that does\nnot re-declare it has no `send_message` and no `get_messages` \u2014 it cannot even\nreport that it has gone mute. Fleet therefore refuses a strict role whose\n`mcp_servers` does not name the connector; declare it explicitly, e.g.\n`ours: { command: ours-mcp, args: [proxy] }`.\n\nBoth options, and `plugins`, reach an ACP session through the bundled Claude ACP\nagent's `_meta` vocabulary. A role that sets `session_options.acp.command` runs\nan agent fleet did not choose and cannot be promised them, so that combination is\nrefused at validation rather than accepted and dropped. This narrows a role's\ntool surface; it does not stop the harness deferring tool schemas, which is the\nharness's own decision.\n\nCodex `harness_options`: `launcher` (auto, ours-codex, codex), `search`,\n`config`, `add_dirs`, and `monitor`; Codex ACP additionally supports\n`profile`. Native app-server roles reject `profile` because Codex does not\naccept `--profile` for `app-server`. They\nmust express approval and filesystem authority through the shared\n`permissions:` block, never through harness options. The adapter translates\n`ask|auto|allow` to Codex `untrusted|on-request|never` and translates\n`read-only|workspace|unrestricted` to Codex\n`read-only|workspace-write|danger-full-access`. Native `config` currently\nallows only `model_reasoning_effort`; ACP retains the broader Codex config surface.\n\nFor Codex, the opt-in `session: codex-app-server` directly runs Codex's native\nJSONL app server behind Fleet's provider-neutral session contract. It supports native item\nstreaming, commentary/final phases, prompt admission, steering, interruption,\npermission requests, durable conversation projection, and thread resume. Packaged\nCodex brains and the init wizard continue to select `session: acp` for compatibility;\nClaude Code also uses ACP. Native Codex defaults to `codex app-server` (or `ours-codex app-server`\nwhen launcher auto finds it). Override the exact command when necessary:\n\n`session_options: { codex_app_server: { command: [codex, app-server] } }`\n\nAn exact command override receives no appended profile, search, or subcommand\narguments. Native sessions persist their Codex thread in Fleet's `.session-id`\nand participate in its bounded fresh/resume recovery; cross-provider conversation\nportability is never assumed.\n\n## ACP adapters\n\nThe maintained `@agentclientprotocol/codex-acp` and\n`@agentclientprotocol/claude-agent-acp` runtimes are bundled automatically as\noptional ours-fleet dependencies. The supervisor resolves their executable\nentrypoints internally, so default ACP roles do not depend on global PATH.\nours-fleet and both maintained adapters require Node 22 or newer.\n\nOverride an adapter only when necessary with `session_options.acp.command`\n(string or argv list). If optional dependencies were deliberately omitted,\nours-fleet falls back to a compatible globally installed `codex-acp` or\n`claude-agent-acp`. `ours-fleet doctor -c FILE` verifies the resolved adapter.\n\n## Boot-time daemon readiness\n\nLinux permanent-agent services wait for their configured daemon before launching\nFleet's runner. This uses the role's saved manifest path and environment, including\nhost profiles, gateway endpoints and expected daemon identity; no Docker container\nnames or default ports are assumed. The read-only probe requires verified startup\nreadiness and an available identity index. Unavailable or unauthorized daemons leave\nthe service waiting, with a diagnostic code in the journal. Each wait is bounded\nand systemd retries it until the daemon is ready; stopping the service cancels it.\nThis does not consume the agent crash budget or discard its session context.\nOnce the runner starts, its normal crash circuit breaker still applies.\nAfter upgrading Fleet, rerun init to refresh the generated service template.\nTemporary agents and macOS services retain their existing lifecycle behavior.\n\n## Reliable mail wake\n\n`monitor.mode` selects exactly one wake owner:\n\n- `fleet` (default): the ours-fleet supervisor consumes body-free daemon\n events and advances its durable cursor only after delivery is accepted. ACP\n uses live steering when supported and falls back to structured\n `session/prompt`.\n- `native`: ours-fleet starts no supervisor monitor; the generated briefing\n instructs Claude Code or Codex to arm its harness-native wake mechanism.\n\nSet `monitor.interrupt: true` in fleet mode to cancel active work before every\nconfigured wake. Set it to `after_tool` to preserve an active ACP tool (and any\npending permission), then steer the wake at the first tool-terminal boundary\nwithout cancellation. A hung boundary is bounded at 120 seconds and falls back\nto non-cancelling steering/queueing; adapters without authenticated tool events\nuse the same conservative fallback. Explicit human/control interrupts remain\nimmediate. The policy is content-blind because the supervisor cannot inspect\nencrypted message bodies. Message bodies are released only when the role calls\nthe ours `get_messages` tool.\n\nThe default is `false`. For a temporary role whose mission intentionally arrives\nafter its readiness announcement, set `mode: fleet` and `interrupt: true`\nexplicitly. The readiness announcement does not change the transport: the\nmission remains ordinary ours mail, fleet injects only the body-free wake, and\nthe role calls `get_messages` before acting. Every later configured wake uses\nthe same interruption policy.\n\nLegacy `monitor.enabled: true|false` remains accepted as an alias for\n`mode: fleet|native`; use `mode` in new configuration. Codex's separate\n`harness_options.monitor: true` is native-monitor consent, not monitor-owner\nselection.\nInspect `ours-fleet status Name`, `peek Name`, role logs, and\n`~/.ours-fleet/agents/Name/.monitor-status` when diagnosing delivery.\n\n## Trusted owner channel\n\nAny managed role may declare a separate ours identity which fleet \u2014 never the agent \u2014\ncreates when missing and binds:\n\n```yaml\nowner_channel:\n identity: Coordinator Owner Channel\n owners: [authenticated-owner-contact-cid]\n agent: authenticated-managed-agent-cid\n interrupt: false\n progress_interval_ms: 30000\n comments: true\n attachments:\n enabled: true\n max_files_per_request: 4\n max_file_bytes: 10485760\n max_request_bytes: 20971520\n retention_ms: 86400000\n```\n\nPermanent role identities are also reconciled before launch. Fleet creates a\nmissing role identity with local exposure and local auto-accept enabled. A\nmissing owner-channel identity uses the safer inverse policy: both are disabled.\nThe short provisioning lease is released before the agent or channel binds.\nTemporary role identities remain connector-owned because their creating lease\ndefines their cleanup lifetime.\n\nThis does not replace the role identity. Normal identity mail remains untrusted\npeer input: the agent reads it through `get_messages` and replies through\n`send_message`. Mail arriving on the dedicated channel from a CID in `owners`\nis injected as a direct `[fleet-owner]` prompt. Mail from the exact `agent`\nCID is forwarded as a new message to the latest authenticated owner conversation;\nits files may also be relayed through this channel. A reply reference selects the\nowner of that authenticated source wire instead of the latest conversation.\nEvery other CID is rejected and warned about without reflecting its body. Fleet sends\naccepted/queued/progress/interrupted/failure notices and routes the ACP turn's\nfinal assistant text back to the authenticated sender with its source wire ID.\nFor file replies of every kind \u2014 a response artifact, a proactive note, or an\nin-turn attachment \u2014 the agent calls ours `send_file` to the channel identity\nand may pair it with a reply-linked caption; fleet, not the agent, chooses the\nowner. That is the only delivery route an agent is given: a tool call either\ndelivers or reports an error, where a file written to disk does neither.\nOwner messages whose trimmed text starts with `/` are deterministic\nsupervisor commands and never enter the model: `/help` (alias `/commands`),\n`/status`, `/comments [status|on|off]`, `/interrupt`, `/clear`,\n`/compact`, `/model <model-id>`, `/restart`, `/force-restart`, `/ls`,\n`/peek`, `/worklog`, and\n`/version`. Unknown or malformed commands answer with the help text instead of\nbeing forwarded; plain messages reach the agent unchanged. `/clear`,\n`/compact`, and `/model` are forwarded only when the role's bundled ACP\nadapter executes them locally (claude-code: all three; codex: `/compact`\nonly) and are otherwise refused with a notice, so slash text never reaches the\nmodel as a prompt.\n\nThe supervisor advertises the same primary registry entries as recipient-scoped\nours typed commands. Their single arguments field is converted back to the text\nafter the slash command name and enters the same dispatcher; aliases stay\nslash-only. Typed handlers repeat the live authenticated Owner-CID check before\ndispatch and return a null protocol completion because the existing correlated\nowner-channel reply remains the result.\n\nWhile a request runs, the agent's live ACP commentary is relayed as messages\nprefixed with the single stable label `\uD83D\uDFE1 Live update:`, so an owner can see\nexactly which messages the setting controls. `owner_channel.comments`\n(default `true`, so existing channels keep their current behavior) is the\nRESTART BASELINE; `/comments on|off` changes only the running session and is\ndeliberately not persisted, so a restart always returns to the checked-in\nconfiguration. `/comments status` reports the live value, the baseline, and\nwhether the backend emits live comments at all. Suppressing live comments never\nsuppresses receipts, progress notices, or the final answer.\n\nOwner documents, images, and voice messages use the same authenticated sender\nand source-wire boundary. Fleet inspects body-free metadata first and rejects\ndisabled, over-count, or over-size requests before selective\nretrieval. Unauthorized CIDs are never retrieved or answered. Reply-linked text\nand files from the same sender become one ordered request; a file-only wake also\nstarts a turn. Retrieved bytes must match their structured size and SHA-256,\nwhile MIME values, extensions, file categories, and declared-versus-detected mismatches\nremain report-only metadata. Symlinks or non-regular paths fail closed. Sanitized copies live only in a mode-0700 request directory as\nmode-0600 files and are removed after completion or bounded stale retention.\n\nThe legacy `attachments.allowed_mime` key is accepted and ignored so existing\nconfigurations keep loading; it is omitted from resolved configuration and cannot\naffect admission.\n\nVoice prompts include a bounded transcript only when typed daemon metadata reports success.\nFailure or unavailability is explicit and preserves the private audio path as the\ninput for direct review. Run `ours config show --json` and inspect `sttConfigured` without\nrevealing provider credentials.\nA mode-0600 message claim journal stores only wire ID, persistent-history\nsequence, and claim time. Fleet journals the exact body-free oldest-first slice\nbefore calling `getMessages` with that slice length, rejects a returned set\nmismatch, and loads a crash-recovered body only through `getHistoryItem`.\nThe attachment crash journal contains only authenticated CID and wire routing\ndata; it never stores captions, filenames, paths, transcript text, or bytes.\nJournaled read files resume through `getFileInfo` and `fetchFile`. A claimed\nagent caption is loaded from history and rejoined before the group is admitted. Fleet\nresolves one authenticated owner route before retrieving bytes, admits every file\nbefore emitting the caption or any file, and sends every part to that same route.\nUnknown correlated routes remain queued without retrieval and receive one bounded\ncorrelated notice. Admission rejection consumes the whole group with one NACK;\nonce emission starts, a transport error becomes terminal uncertain delivery and\nthe group is never blind-retried. Bounded v2 source-wire routing state is migrated\nfrom v1 on read. Corrupt state disables attachment admission rather than weakening\nprovenance checks.\n\nThe channel identity must be unique and must not be a role identity. The bridge\npersists bounded wire IDs only, never message/reply plaintext, and requeues input\nbefore starting its turn for at-least-once crash recovery. It currently requires\nthe structured agent-session interface so correlated final replies retain their\ndelivery guarantee.\n\n### Live contact and owner administration\n\nThe supervisor which is already running the managed role remains the sole binder of\n`owner_channel.identity`. The CLI reaches that exact live `OwnerChannel`\nthrough the role's token-authenticated, mode-0600 Unix control socket for contact\ninspection and setup; it never starts another ours client and never force-binds:\n\nRapid supervised restart is serialized by a role-scoped single-binder lease.\nThe predecessor closes its authenticated control socket and MCP proxy before\nreleasing ownership. The replacement waits at most five seconds and retries the\ndaemon bind only when PID/start-marker metadata proves the holder was the same\nrole and owner-channel identity. Foreign, live, corrupt, or otherwise\nunverifiable ownership remains fail-closed; fleet never uses `force=true`.\n\nIf that matching predecessor misses the bound, its still-authenticated control\nroute may send one fixed, digest-deduplicated recovery notice through the latest\nauthenticated owner conversation (or the sole configured owner). Notice\nplaintext is never persisted. With no safe deterministic route fleet guesses no\nrecipient and leaves the actionable failure in the web console and role logs.\nThe remote recovery action is `/restart`; inspect repeated failures with\n`ours-fleet logs <Role>` or the web console.\n\n```sh\nours-fleet owner-channel contact list <Role>\nours-fleet owner-channel contact invite <Role> [--name <label>]\nours-fleet owner-channel contact add <Role> (--invite-file <path> | --invite-stdin) [--name <label>]\nours-fleet owner-channel owner list <Role>\nours-fleet owner-channel owner authorize <Role> <exact-64-hex-contact-cid>\nours-fleet owner-channel owner revoke <Role> <exact-64-hex-contact-cid>\n```\n\nContact establishment and owner authorization are separate security steps.\n`contact add` never authorizes: invite redemption is pending until the peer\nverifies it. Once `contact list` reports the established contact, authorize\nits exact immutable CID explicitly. Invite creation emits invite material only\non stdout; acceptance reads it from a file or stdin, not argv.\n\nConfigured `owners` remain the baseline. On legacy channels without `agent`,\nlive authorizations/revocations are an immediately effective, restart-persistent\noverlay. Managed-agent CID gating makes fleet configuration authoritative and\ndisables live owner mutation and direct control-socket sends. `owner list` labels\nbaseline versus dynamic entries and effective status. The atomic mode-0600 file\ncontains bounded CIDs and audit actions only. Corruption disables all effective\nowners and refuses mutation rather than resurrecting authority; revoking the\nlast effective owner is always refused.\n\nA missing/stopped role, role without `owner_channel`, unavailable\nMCP client, or a role entering shutdown returns an actionable error with no\nside effects. Management uses no network listener and never logs or persists\ninvite material.\n\nFor any non-final message\u2014progress, blocker, suggestion, or later proactive note\u2014\nthe managed agent calls ordinary ours `send_message` to the channel identity.\nFleet checks only that the authenticated sender CID exactly equals `agent`, then\nforwards the text as a new message. There is no task/request/update type, phase,\nreply correlation, or owner recipient argument. A sole owner is the safe fallback;\nwith multiple owners and no inbound route history the relay fails closed. Devices\nsharing one identity share its CID; separate owner identities hand off the route\nwhen either sends channel mail. The ACP final is separate: fleet extracts it from\nthe completed turn and deterministically replies to the initiating owner wire.\n\nThe bounded mode-0600 route state stores CIDs, wire IDs, timestamps, delivery state,\nand hashes but never message plaintext. Unauthorized attempts produce a bounded\nCID-only owner warning; attempted bodies are neither reflected nor persisted.\n\nFor a mobile owner, establish the contact first, wait for peer verification,\nauthorize its exact CID, and revoke that same CID when access ends. The bounded\nmode-0600 CID overlay survives supervisor restart and remains fail-closed on\ncorruption. Update bodies remain memory-only. After a crash/restart, unfinished\ndeferred owner input follows the existing at-least-once replay path; the restarted\nsupervisor remains the sole binder.\n\n## Stable config and YAML migration\n\n`ours-fleet config --json` emits schemaVersion 1 resolved plans. Environment\nvalues and mission/persona/bio bodies are withheld; environment keys are sorted\nand values are marked redacted. Additive fields may appear in schema 1, while a\nremoval or semantic reuse requires a new schema version.\n\nYAML parsing always rejects duplicate keys. The current default\n`--yaml-mode compat` warns with file/line/column for anchors, aliases, explicit\ntags, non-scalar keys, and multiple documents. Use `--yaml-mode strict` in CI\nnow; strict becomes the next-major default and compat is the temporary migration\nescape hatch.\n\n## Bounded worklogs, auth proxy, and model recovery\n\nWORKLOG rotation is enabled by default with\n`worklog: { max_kb: 1024, keep_tail_kb: 256, max_archives: 12 }`. Maps may\noverride individual values; `worklog: false` on a role or in defaults opts out.\nFleet rotates only at that role's launch/resume lifecycle boundary. Concurrent\nchanges defer rotation. The active file keeps a bounded UTF-8 tail and advances\nto a line boundary when a complete line fits. If one logical line alone exceeds\nthe budget, its newest suffix remains and the rotation manifest records the\nmid-line start and omitted byte count. The complete prior inode receives a\ncollision-safe UTC archive name, and\n`.worklog-rotation.json` records restart provenance. `max_archives` bounds\nrecent archives beside WORKLOG.md; older complete archives move to\n`WORKLOG.archives/` without deletion. All archives share the role's sensitive\nstate boundary. Fleet refuses a symlinked/non-regular live log or a symlinked\ncold-archive boundary before replacing the live path and best-effort removes a\nduplicate publication left by a detected failure while the original inode is\nstill available. The manifest records SHA-256 digests for the archive and live\nbytes observed when it is written. These checks address ordinary path hazards,\nnot intentional path mutation by a malicious concurrent process with the same\nUnix authority; that is outside the threat model and requires OS-level isolation.\n\nACP tool diffs are bounded before entering web conversation events. Existing\nsmall before/after diffs are unchanged. Oversized whole-file snapshots are\nreduced to the actual changed region plus path, operation, original byte counts,\ndigest, and omission metadata. Each retained side is a newest-content UTF-8 tail\nof at most 64 KiB, advanced to a line boundary when a complete line fits. An\noverlong single line keeps its newest suffix and explicitly records a mid-line\nstart. Paths retain at most a 4 KiB suffix with byte count, digest, and omitted\nprefix metadata; the complete normalized update is capped at 320 KiB. A large\nappend therefore retains current appended content, not the historical prefix.\nThe live web-console transcript includes only the current runner generation and\nexcludes adapter session/load replay. Replayed events remain durable with\nagent_replay provenance for diagnosis and recovery.\n\n`auth_proxy: { kind: anthropic, base_url, required, health_url }` is Claude-only\nand loopback-only. Fleet injects only ANTHROPIC_BASE_URL and doctor rejects\ncredential env keys. The privileged reference companion is\n`contrib/anthropic-auth-proxy.mjs`; deploy it separately as a dedicated account\nwith a 0600 token file and per-role listener access. Fleet never installs it or\nreads its credential.\n\n`model_chain` is an ordered authorization list and its first entry must equal\n`model`. Only sustained high-confidence entitlement/quota 429 evidence advances\none entry. Transient 429, overload, auth, policy, and unknown errors never\ndown-shift. Runtime state is atomic in .model-recovery.json; exhaustion is\nfail-closed and held down. Change the declared chain/model and restart to\nreconcile explicitly; no chain preserves detection-only behavior.\n";
8
8
  /**
9
9
  * What every shipped spawn-skill variant must say, and must not say.
10
10
  *
package/dist/docs.js CHANGED
@@ -492,6 +492,17 @@ Messenger-bound results are capped at 3,500 Unicode code points and 12,000 UTF-8
492
492
  bytes with structural omission notices. \`--json\` bypasses this presentation layer
493
493
  and retains the versioned machine schema and serialization order.
494
494
 
495
+ Active task \`show\` reports current readiness separately from lifecycle, also in
496
+ Owner commands. Start/work preserve the fail-closed \`task_not_ready\` behavior.
497
+ Unknown control health does not prove a dead agent or authorize replacement.
498
+ Before deliberate recovery, Fleet Coordinator inspects current seats and exact
499
+ old-launch worklogs/termination evidence in \`recovery/temporary\`, preserves
500
+ original records, and privately records verified old/new launch provenance.
501
+ A new inbox is not complete history: page scoped history to the end, authenticate
502
+ historical Owner authors, and compare completed actions and replies before resuming.
503
+ Do not restore archived identity state, reuse invites, adopt same-role seats or
504
+ blindly replay instructions. Ask the Owner if authority/completion is uncertain.
505
+
495
506
  Every task belongs to a named list. The built-in \`default\` list always exists,
496
507
  and legacy tasks or create calls without \`--list\` resolve to it. Use \`task lists\`,
497
508
  \`task list-create <name>\`, \`task list-rename <name> <new-name>\`, and
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ type JsonObject = Record<string, unknown>;
2
3
  interface TerminalRecoveryOptions {
3
4
  sendToCodex(line: string): void;
4
5
  emitToClient(line: string): void;
@@ -39,6 +40,8 @@ export declare class CodexTerminalRecovery {
39
40
  private finishTurn;
40
41
  }
41
42
  /** Transform one app-server NDJSON request; malformed input passes through. */
42
- export declare function rewriteCodexAppServerRequest(line: string, approval: string, expectedSandbox: string, disableInheritedMcp?: boolean): string;
43
+ export declare function rewriteCodexAppServerRequest(line: string, approval: string, expectedSandbox: string, disableInheritedMcp?: boolean, managedOursTools?: JsonObject): string;
44
+ /** ACP only carries transport fields; read policy from Fleet's complete config. */
45
+ export declare function managedCodexToolPolicy(env: NodeJS.ProcessEnv): JsonObject | undefined;
43
46
  export declare function runCodexAppServerProxy(): void;
44
47
  export {};
@@ -300,9 +300,9 @@ function sandboxMode(policy) {
300
300
  * Fleet keeps the preset's sandbox policy, but restores the independently
301
301
  * configured approval policy on the request Codex actually executes.
302
302
  */
303
- function rewriteMessage(value, approval, expectedSandbox, disableInheritedMcp) {
303
+ function rewriteMessage(value, approval, expectedSandbox, disableInheritedMcp, managedOursTools) {
304
304
  if (Array.isArray(value))
305
- return value.map(candidate => rewriteMessage(candidate, approval, expectedSandbox, disableInheritedMcp));
305
+ return value.map(candidate => rewriteMessage(candidate, approval, expectedSandbox, disableInheritedMcp, managedOursTools));
306
306
  if (!isObject(value) || !isObject(value.params))
307
307
  return value;
308
308
  if (value.method === 'turn/start') {
@@ -318,17 +318,52 @@ function rewriteMessage(value, approval, expectedSandbox, disableInheritedMcp) {
318
318
  params: { ...value.params, config: { ...config, mcp_servers: {} } },
319
319
  };
320
320
  }
321
+ if (managedOursTools
322
+ && (value.method === 'thread/start' || value.method === 'thread/resume')) {
323
+ const config = isObject(value.params.config) ? value.params.config : {};
324
+ const servers = isObject(config.mcp_servers) ? config.mcp_servers : {};
325
+ const ours = servers.ours;
326
+ // Policy cannot create or re-enable a connector that ACP did not inject.
327
+ if (!isObject(ours) || ours.enabled === false)
328
+ return value;
329
+ const tools = isObject(ours.tools) ? { ...ours.tools } : {};
330
+ for (const [name, policy] of Object.entries(managedOursTools)) {
331
+ if (isObject(policy))
332
+ tools[name] = {
333
+ ...(isObject(tools[name]) ? tools[name] : {}), ...policy,
334
+ };
335
+ }
336
+ return { ...value, params: { ...value.params, config: { ...config,
337
+ mcp_servers: { ...servers, ours: { ...ours, tools } },
338
+ } } };
339
+ }
321
340
  return value;
322
341
  }
323
342
  /** Transform one app-server NDJSON request; malformed input passes through. */
324
- export function rewriteCodexAppServerRequest(line, approval, expectedSandbox, disableInheritedMcp = false) {
343
+ export function rewriteCodexAppServerRequest(line, approval, expectedSandbox, disableInheritedMcp = false, managedOursTools) {
325
344
  try {
326
- return JSON.stringify(rewriteMessage(JSON.parse(line), approval, expectedSandbox, disableInheritedMcp));
345
+ return JSON.stringify(rewriteMessage(JSON.parse(line), approval, expectedSandbox, disableInheritedMcp, managedOursTools));
327
346
  }
328
347
  catch {
329
348
  return line;
330
349
  }
331
350
  }
351
+ /** ACP only carries transport fields; read policy from Fleet's complete config. */
352
+ export function managedCodexToolPolicy(env) {
353
+ if (env.FLEET_OURS_MANAGED !== '1')
354
+ return undefined;
355
+ try {
356
+ const config = JSON.parse(env.CODEX_CONFIG ?? '{}');
357
+ const servers = isObject(config) ? config.mcp_servers : undefined;
358
+ const ours = isObject(servers) ? servers.ours : undefined;
359
+ return isObject(ours) && ours.enabled !== false && isObject(ours.tools)
360
+ ? ours.tools : undefined;
361
+ }
362
+ catch {
363
+ // Invalid config never grants tool approval. ACP owns config diagnostics.
364
+ return undefined;
365
+ }
366
+ }
332
367
  function requiredChoice(name, allowed) {
333
368
  const value = process.env[name];
334
369
  if (value && allowed.has(value))
@@ -355,6 +390,7 @@ export function runCodexAppServerProxy() {
355
390
  const approval = requiredChoice(APPROVAL_ENV, APPROVAL_POLICIES);
356
391
  const expectedSandbox = requiredChoice(SANDBOX_ENV, SANDBOX_MODES);
357
392
  const disableInheritedMcp = process.env[DISABLE_INHERITED_MCP_ENV] === '1';
393
+ const managedOursTools = managedCodexToolPolicy(process.env);
358
394
  const launch = realCodexLaunch();
359
395
  const env = { ...process.env };
360
396
  delete env.CODEX_PATH;
@@ -450,7 +486,7 @@ export function runCodexAppServerProxy() {
450
486
  input.on('line', line => {
451
487
  if (finished)
452
488
  return;
453
- const rewritten = rewriteCodexAppServerRequest(line, approval, expectedSandbox, disableInheritedMcp);
489
+ const rewritten = rewriteCodexAppServerRequest(line, approval, expectedSandbox, disableInheritedMcp, managedOursTools);
454
490
  recovery.observeClientLine(rewritten);
455
491
  writeToChild(rewritten);
456
492
  });
@@ -7,7 +7,7 @@ import { packagedPresetRoot } from './preset-bootstrap.js';
7
7
  import { listTemplates } from './rooms-tasks/templates.js';
8
8
  const WORK_KINDS = ['development', 'review', 'coordination'];
9
9
  const ROLE_WORK = {
10
- Developer: 'development', Critic: 'review',
10
+ Developer: 'development', Engineer: 'development', Critic: 'review',
11
11
  LocalCoordinator: 'coordination', Coordinator: 'coordination', FleetCoordinator: 'coordination',
12
12
  };
13
13
  const REASONING_EFFORT = {
@@ -170,6 +170,7 @@ export function formatSetupSummary(answers, configuration) {
170
170
  ' One-agent work: one Developer',
171
171
  ' Reviewed pair: Developer + independent Critic',
172
172
  ' Team: LocalCoordinator + Developer + Critic',
173
+ ' Gated engineering: Engineer (gate pipeline) + independent Critic',
173
174
  ' FleetCoordinator: coordination model',
174
175
  '',
175
176
  'After the final Yes, Fleet performs host service setup before publishing the complete configuration.',
@@ -228,7 +229,7 @@ export function generateSetup(answers) {
228
229
  if (tuples.size !== 1)
229
230
  throw new Error('one-model assignment requires the same exact model for development, review, and coordination');
230
231
  }
231
- for (const role of ['Coordinator', 'LocalCoordinator', 'Developer', 'Critic']) {
232
+ for (const role of ['Coordinator', 'LocalCoordinator', 'Developer', 'Engineer', 'Critic']) {
232
233
  files.set(`roles/${role}.yaml`, preset(`roles/${role}.yaml`));
233
234
  if (role === 'Coordinator')
234
235
  continue;
@@ -243,7 +244,7 @@ export function generateSetup(answers) {
243
244
  }
244
245
  files.set('agents/FleetCoordinator.yaml', preset('agents/FleetCoordinator.yaml')
245
246
  .replace('brain: { ref: claude-default }', 'brain: { ref: coordination }'));
246
- for (const name of ['single', 'pair', 'team'])
247
+ for (const name of ['single', 'pair', 'team', 'engineering'])
247
248
  files.set(`room_templates/${name}.yaml`, preset(`room_templates/${name}.yaml`));
248
249
  return { files, answers };
249
250
  }
@@ -401,8 +402,8 @@ function validateStaged(stageManifest) {
401
402
  const config = loadConfig(stageManifest, { yamlMode: 'strict' });
402
403
  const templates = listTemplates(config.roomTemplates ?? {});
403
404
  const names = new Set(templates.map(template => template.name));
404
- if (!['pair', 'single', 'team'].every(name => names.has(name)))
405
- throw new Error('generated setup did not resolve the default single, pair, and team experiences');
405
+ if (!['pair', 'single', 'team', 'engineering'].every(name => names.has(name)))
406
+ throw new Error('generated setup did not resolve the default single, pair, team, and engineering experiences');
406
407
  for (const template of templates)
407
408
  for (const member of template.members)
408
409
  if (!config.agentTemplates?.[member.agent_template])
@@ -1398,6 +1398,7 @@ export class OwnerChannel {
1398
1398
  await this.fleetOps.provisionTask(task.task_id);
1399
1399
  return outcome;
1400
1400
  }),
1401
+ observeTaskReadiness: taskId => new TaskRoomApplicationService(this.options.configPath).observeTaskReadiness(taskId),
1401
1402
  taskProvisioningOutcome: taskId => new TaskRoomApplicationService(this.options.configPath).taskProvisioningOutcome(taskId),
1402
1403
  listTasks: filter => new TaskRoomApplicationService(this.options.configPath).listTasks(filter),
1403
1404
  groupedTasks: filter => new TaskRoomApplicationService(this.options.configPath).groupedTasks(filter),
@@ -1,3 +1,4 @@
1
+ import type { TaskReadinessIssue } from '../rooms-tasks/live-readiness.js';
1
2
  import type { InterruptOutcome, SessionEvent, SessionSnapshot } from '../session/types.js';
2
3
  import type { RoomOrchestrationRecord, TaskOutcome, TaskRecord, TaskTerminalIntent, TaskListRecord } from '../rooms-tasks/types.js';
3
4
  import { type OwnerCommentsState } from './notices.js';
@@ -58,6 +59,9 @@ export interface OwnerCommandContext {
58
59
  terminalTask(taskId: string, kind: TaskTerminalIntent['kind'], outcome?: TaskOutcome): Promise<void>;
59
60
  createTask(input: Omit<CreateTaskRequest, 'actor'>): Promise<TaskRecord>;
60
61
  startTask(taskId: string): Promise<TaskProvisioningOutcome | TaskRecord>;
62
+ observeTaskReadiness?(taskId: string): Promise<{
63
+ state: 'ready';
64
+ } | TaskReadinessIssue>;
61
65
  taskProvisioningOutcome?(taskId: string): TaskProvisioningOutcome;
62
66
  listTasks(filter?: {
63
67
  state?: TaskState | TaskState[];
@@ -306,6 +306,7 @@ export const ownerCommands = [
306
306
  const trailingLines = argLines.slice(1).join('\n').trim() || undefined;
307
307
  const showTask = async (id) => {
308
308
  const { task: t } = ctx.getTask(id);
309
+ const readiness = t.state === 'active' ? await ctx.observeTaskReadiness?.(id) : undefined;
309
310
  await ctx.reply(renderMarkdownResult({
310
311
  icon: '📋', title: 'Task details',
311
312
  fields: [
@@ -313,6 +314,8 @@ export const ownerCommands = [
313
314
  { label: 'Title', value: t.title },
314
315
  { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
315
316
  { label: 'List', value: t.list_name ?? 'default', kind: 'code' },
317
+ ...(readiness ? [{ label: 'Readiness', value: readiness.state },
318
+ ...('reason' in readiness ? [{ label: 'Reason', value: readiness.reason }] : [])] : []),
316
319
  ...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
317
320
  ...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
318
321
  ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
@@ -69,8 +69,11 @@ const V5_GENERATED_ROLE_DEFAULT_FINGERPRINTS = Object.freeze({
69
69
  });
70
70
  const CURRENT_ROLE_DEFAULTS = new Set([
71
71
  'roles/Coordinator.yaml', 'roles/LocalCoordinator.yaml', 'roles/Developer.yaml', 'roles/Critic.yaml',
72
+ 'roles/Engineer.yaml',
72
73
  'agent_templates/LocalCoordinator.yaml', 'agent_templates/Developer.yaml', 'agent_templates/Critic.yaml',
74
+ 'agent_templates/Engineer.yaml',
73
75
  'room_templates/single.yaml', 'room_templates/pair.yaml', 'room_templates/team.yaml',
76
+ 'room_templates/engineering.yaml',
74
77
  'agents/FleetCoordinator.yaml',
75
78
  ]);
76
79
  export const legacyStarterMigrationManifest = Object.freeze({
@@ -258,6 +261,18 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
258
261
  unlinkSync(target);
259
262
  copyFileSync(join(source, relative), target, constants.COPYFILE_EXCL);
260
263
  chmodSync(target, 0o600);
264
+ // New Engineer seats use the existing Developer's selected brain. This
265
+ // preserves wizard/custom provider choices without copying permissions.
266
+ if (relative === 'agent_templates/Engineer.yaml' && classified.additions.includes(path)) {
267
+ const developer = join(root, 'agent_templates/Developer.yaml');
268
+ if (existsSync(developer)) {
269
+ const value = parseFleetDocument(developer, readFileSync(developer, 'utf8'), 'strict').value;
270
+ if (value.brain !== undefined && JSON.stringify(value.brain) !== JSON.stringify({ ref: 'claude-default' })) {
271
+ const bytes = readFileSync(target, 'utf8').replace('brain: { ref: claude-default }', () => `brain: ${JSON.stringify(value.brain)}`);
272
+ writeFileSync(target, bytes, { mode: 0o600 });
273
+ }
274
+ }
275
+ }
261
276
  fsyncPath(target);
262
277
  }
263
278
  for (const kind of ['roles', 'agent_templates', 'room_templates', 'agents'])
@@ -520,10 +520,10 @@ function taskRoomService(configuration) {
520
520
  function allTemplates(cfg) {
521
521
  return cfg.roomTemplates ?? {};
522
522
  }
523
- function coworkFor(cfg) {
523
+ function coworkFor(cfg, options) {
524
524
  if (!cfg.rooms)
525
525
  throw new ConfigError('rooms: configuration is required before creating or querying rooms');
526
- return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
526
+ return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config, ...options });
527
527
  }
528
528
  function resolveRoomTemplate(cfg, name) {
529
529
  if (!name)
@@ -832,16 +832,18 @@ export function registerTaskCommands(parent, cOpt) {
832
832
  dieTaskRoom(e);
833
833
  }
834
834
  });
835
- taskCmd.command('show <id>')
835
+ cOpt(taskCmd.command('show <id>'))
836
836
  .description('show task details')
837
837
  .option('--json', 'JSON output')
838
838
  .action(async (id, opts) => {
839
839
  try {
840
- const service = taskRoomService();
840
+ const service = taskRoomService(opts.configuration);
841
841
  const { task: t, orchestration: room } = service.getTask(id);
842
+ const readiness = t.state === 'active' ? await service.observeTaskReadiness(id) : undefined;
842
843
  if (opts.json) {
843
844
  console.log(JSON.stringify({
844
845
  schema_version: 1, task: t, orchestration: room ?? null,
846
+ ...(readiness ? { readiness } : {}),
845
847
  }, null, 2));
846
848
  return;
847
849
  }
@@ -851,6 +853,8 @@ export function registerTaskCommands(parent, cOpt) {
851
853
  { label: 'ID', value: t.task_id, kind: 'code' },
852
854
  { label: 'Title', value: t.title },
853
855
  { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
856
+ ...(readiness ? [{ label: 'Readiness', value: readiness.state },
857
+ ...('reason' in readiness ? [{ label: 'Reason', value: readiness.reason }] : [])] : []),
854
858
  ...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
855
859
  ...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
856
860
  ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
@@ -13,7 +13,8 @@ export async function taskLiveReadiness(task, room, cowork, deps = {}) {
13
13
  return degraded('room_record_mismatch');
14
14
  const expected = room.template_snapshot?.members.reduce((n, member) => n + member.count, 0);
15
15
  if (expected === undefined || room.member_seats.length !== expected
16
- || new Set(room.member_seats.map(seat => seat.role_name)).size !== expected)
16
+ || new Set(room.member_seats.map(seat => seat.role_name)).size !== expected
17
+ || new Set(room.member_seats.map(seat => seat.identity_cid)).size !== expected)
17
18
  return degraded('member_roster_mismatch');
18
19
  let remote;
19
20
  try {
@@ -25,11 +26,11 @@ export async function taskLiveReadiness(task, room, cowork, deps = {}) {
25
26
  if (!remote || remote.room_id !== room.room_id || remote.identity_cid !== room.room_identity_cid
26
27
  || remote.state !== 'active')
27
28
  return degraded('room_identity_or_state_mismatch');
28
- if (room.owner_seat_cid && !remote.seats.some(seat => seat.identity_cid === room.owner_seat_cid && seat.seat_state === 'active'))
29
+ if (room.owner_seat_cid && remote.seats.filter(seat => seat.identity_cid === room.owner_seat_cid && seat.seat_state === 'active').length !== 1)
29
30
  return degraded('owner_seat_missing');
30
31
  for (const seat of room.member_seats) {
31
32
  const member = seat.role_name;
32
- if (seat.seat_state !== 'active' || !seat.identity_cid || !seat.invite_id
33
+ if (!/^[A-Za-z0-9_-]+$/.test(member) || seat.seat_state !== 'active' || !seat.identity_cid || !seat.invite_id
33
34
  || seat.launch?.state !== 'launched' || !seat.launch.launch_id || seat.retirement)
34
35
  return degraded('member_record_incomplete', member);
35
36
  const matches = remote.seats.filter(item => item.display_name === member && item.seat_state !== 'removed');
@@ -59,7 +60,7 @@ export async function taskLiveReadiness(task, room, cowork, deps = {}) {
59
60
  return unknown('supervisor_probe_unavailable', member);
60
61
  }
61
62
  try {
62
- const response = await (deps.control ?? controlRequest)(dir, { command: 'status' }, 2_000);
63
+ const response = await (deps.control ?? controlRequest)(dir, { command: 'status', controller: false }, 2_000);
63
64
  if (!response.ok)
64
65
  return unknown('control_unavailable', member);
65
66
  const status = response.result;
@@ -67,9 +68,17 @@ export async function taskLiveReadiness(task, room, cowork, deps = {}) {
67
68
  return degraded('agent_not_ready', member);
68
69
  if (status?.alive !== true || !['idle', 'running', 'awaiting_permission'].includes(status.readiness ?? ''))
69
70
  return unknown('agent_readiness_unknown', member);
71
+ const after = (deps.supervisor ?? readTempSupervisor)(dir);
72
+ if (!after || after.role !== member || after.launchId !== seat.launch.launch_id)
73
+ return degraded('supervisor_launch_changed', member);
70
74
  }
71
75
  catch {
72
76
  return unknown('control_unavailable', member);
73
77
  }
74
78
  }
79
+ const memberCids = new Set(room.member_seats.map(seat => seat.identity_cid));
80
+ const memberRoles = new Set(room.member_seats.map(seat => seat.cowork_role));
81
+ if (remote.seats.some(seat => seat.seat_state === 'active' && memberRoles.has(seat.role)
82
+ && !memberCids.has(seat.identity_cid) && seat.identity_cid !== room.owner_seat_cid))
83
+ return degraded('untracked_member_seats');
75
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.17",
3
+ "version": "1.2.0-nightly.19",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -0,0 +1,5 @@
1
+ role: { ref: Engineer }
2
+ brain: { ref: claude-default }
3
+ coordinator: FleetCoordinator
4
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
5
+ monitor: { mode: fleet, interrupt: after_tool }
@@ -0,0 +1,40 @@
1
+ mission: Own implementation and task execution for the assigned goal through the engineering gate pipeline, with maintainable code and tests.
2
+ persona: |
3
+ Make the smallest coherent change that satisfies the task. Preserve unrelated work, validate
4
+ observable behavior in proportion to risk, and report deviations. Do not waive failing tests,
5
+ publish, deploy, or mutate Fleet lifecycle without authority.
6
+
7
+ Work every non-trivial task through the engineering gate pipeline, in order: tighten scope and
8
+ record a numbered Definition of Done before any code; pressure-test the design against
9
+ over-engineering and prefer the simplest solution that works; write the spec down before
10
+ implementing; implement test-first — write the failing test, watch it fail for the right
11
+ reason, then make it pass; run a code review; run a security pass sized to the real attack
12
+ surface; update the documentation the change touches; and verify every Definition of Done item
13
+ with evidence before calling anything done. Report each gate's outcome in the assigned Cowork
14
+ room as it closes. If a gate-pipeline skill is installed in this session, invoke it at task
15
+ start and follow its gates; if none is installed, say so in the room and walk the same gates by
16
+ hand — never skip a gate silently. Trivial one-line fixes, typos, and purely mechanical
17
+ refactors are exempt from the full pipeline; claim the exemption out loud when using it.
18
+
19
+ Task state, progress, checkpoints, findings, review verdicts, completion, handoffs, and ordinary
20
+ errors are routine task communication: report them only in the assigned Cowork room. When a
21
+ LocalCoordinator is present, read and follow its instructions directly in the room and keep all
22
+ replies and evidence in that same room; it does not become a separate reporting channel. Fleet
23
+ Coordinator ownership of lifecycle operations does not make it the routine task-communication sink.
24
+
25
+ Distinguish infrastructure or orchestration failure from ordinary task difficulty, review
26
+ disagreement, asynchronous delay, implementation defects, normal review findings, test failures
27
+ under active diagnosis, or incomplete work. Send a direct message only for a confirmed blocker or
28
+ infrastructure or orchestration problem that prevents safe progress or room communication and
29
+ requires Fleet-level action. Report it once
30
+ to the configured Fleet Coordinator with authenticated sender, available task/room context,
31
+ observed state, bounded safe attempts, and the canonical next action. Never include invites or
32
+ invite fingerprints, keys, tokens, unrelated messages, or private workspace content. Retry only
33
+ transient blocker-report transport at most once after backoff. Never retry the failed identity,
34
+ room, or lifecycle operation after an identity/CID mismatch, consumed/invalid invite, permission
35
+ failure, or lost lifecycle state. Avoid busy-polling,
36
+ continue independent safe work, or declare BLOCKED/resting. Report peer nonresponse only 10 minutes
37
+ after a direct room attempt unless the room contract defines another window; honor any later
38
+ absolute ETA from the peer's timestamp or stated start time. Leave recover,
39
+ block/unblock, review/finish, deletion, replacement, and respawn decisions to Fleet Coordinator.
40
+ bio: Gate-pipeline implementation specialist; engage to deliver a bounded change with gate-by-gate verification evidence.
@@ -0,0 +1,19 @@
1
+ version: 1
2
+ description: "Engineering pair: Engineer works the gate pipeline and Critic independently reviews"
3
+ room: { quiet_membership: false, anonymous: false }
4
+ contract: |
5
+ Engineer owns implementation and verification evidence, and works through the engineering gate
6
+ pipeline: scope and a numbered Definition of Done before code, an over-engineering check, spec
7
+ before implementation, test-first implementation, review, a security pass sized to the change,
8
+ documentation, and evidence-backed verification of every Definition of Done item — reporting
9
+ each gate's outcome in this room. The seat label Engineer marks the gated pipeline; a plain
10
+ Developer seat is never the gated one.
11
+ Critic independently challenges assumptions, may audit against the same gates, and withholds
12
+ sign-off on material failures.
13
+ Both keep routine state, findings, verdicts, completion, handoffs, and ordinary errors in this room.
14
+ Direct Fleet Coordinator contact is reserved for a confirmed blocker or infrastructure or orchestration
15
+ problem that prevents safe progress or room communication and requires Fleet-level action.
16
+ Completion requires Engineer evidence and Critic sign-off.
17
+ members:
18
+ - { slot: developer, role: Engineer, count: 1, agent_template: Engineer }
19
+ - { slot: critic, role: Critic, count: 1, agent_template: Critic }