@ours.network/fleet 1.0.5 → 1.1.0-nightly.10

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.
Files changed (159) hide show
  1. package/README.md +122 -133
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.d.ts +0 -1
  5. package/dist/application/capabilities.js +3 -13
  6. package/dist/application/fleet-query-service.d.ts +0 -3
  7. package/dist/application/fleet-query-service.js +14 -25
  8. package/dist/application/role-creation-service.d.ts +5 -24
  9. package/dist/application/role-creation-service.js +27 -67
  10. package/dist/application/role-removal-service.js +6 -3
  11. package/dist/application/role-repository.d.ts +1 -2
  12. package/dist/application/role-repository.js +7 -7
  13. package/dist/application/session-control.d.ts +0 -12
  14. package/dist/application/session-control.js +0 -42
  15. package/dist/application/session-mutations.d.ts +5 -5
  16. package/dist/application/task-room-service.d.ts +1 -1
  17. package/dist/application/task-room-service.js +154 -16
  18. package/dist/application/types.d.ts +1 -8
  19. package/dist/briefing.js +12 -11
  20. package/dist/build-info.json +5 -5
  21. package/dist/cli.js +143 -63
  22. package/dist/config-yaml.d.ts +1 -1
  23. package/dist/config.d.ts +66 -6
  24. package/dist/config.js +398 -43
  25. package/dist/creation.d.ts +1 -1
  26. package/dist/creation.js +2 -2
  27. package/dist/daemon-recovery.d.ts +93 -0
  28. package/dist/daemon-recovery.js +328 -0
  29. package/dist/docs.d.ts +1 -1
  30. package/dist/docs.js +103 -127
  31. package/dist/doctor.js +18 -11
  32. package/dist/fleet-command-audit.d.ts +160 -0
  33. package/dist/fleet-command-audit.js +433 -0
  34. package/dist/fleet-proxy.d.ts +3 -1
  35. package/dist/fleet-proxy.js +30 -8
  36. package/dist/generated-agent-source.d.ts +9 -0
  37. package/dist/generated-agent-source.js +53 -0
  38. package/dist/harness/acp-session-transport.d.ts +8 -0
  39. package/dist/harness/acp-session-transport.js +3 -0
  40. package/dist/harness/agent-session.d.ts +51 -0
  41. package/dist/harness/agent-session.js +1 -0
  42. package/dist/harness/claude-code-session.d.ts +24 -0
  43. package/dist/harness/claude-code-session.js +36 -0
  44. package/dist/harness/claude-code.d.ts +2 -1
  45. package/dist/harness/claude-code.js +41 -81
  46. package/dist/harness/codex-session.d.ts +24 -0
  47. package/dist/harness/codex-session.js +36 -0
  48. package/dist/harness/codex.d.ts +2 -1
  49. package/dist/harness/codex.js +55 -45
  50. package/dist/harness/registry.js +4 -0
  51. package/dist/harness/types.d.ts +6 -38
  52. package/dist/index.d.ts +4 -6
  53. package/dist/index.js +2 -4
  54. package/dist/init-guidance.d.ts +1 -0
  55. package/dist/init-guidance.js +6 -0
  56. package/dist/isolation/resources.d.ts +3 -7
  57. package/dist/isolation/resources.js +3 -7
  58. package/dist/model-env.d.ts +4 -6
  59. package/dist/model-env.js +5 -8
  60. package/dist/monitor.d.ts +1 -24
  61. package/dist/monitor.js +5 -140
  62. package/dist/ops.js +4 -2
  63. package/dist/owner-channel/channel.d.ts +58 -2
  64. package/dist/owner-channel/channel.js +269 -29
  65. package/dist/owner-channel/commands.d.ts +2 -0
  66. package/dist/owner-channel/commands.js +31 -13
  67. package/dist/owner-channel/state.d.ts +2 -0
  68. package/dist/owner-channel/state.js +8 -2
  69. package/dist/preset-bootstrap.d.ts +10 -0
  70. package/dist/preset-bootstrap.js +85 -0
  71. package/dist/reports/artifact.d.ts +10 -0
  72. package/dist/reports/artifact.js +32 -0
  73. package/dist/reports/index.d.ts +5 -0
  74. package/dist/reports/index.js +5 -0
  75. package/dist/reports/render.d.ts +6 -0
  76. package/dist/reports/render.js +240 -0
  77. package/dist/reports/service.d.ts +42 -0
  78. package/dist/reports/service.js +57 -0
  79. package/dist/reports/tasks.d.ts +52 -0
  80. package/dist/reports/tasks.js +177 -0
  81. package/dist/reports/types.d.ts +315 -0
  82. package/dist/reports/types.js +6 -0
  83. package/dist/resolved-plan.d.ts +3 -2
  84. package/dist/resolved-plan.js +21 -5
  85. package/dist/rooms-tasks/cli.js +225 -40
  86. package/dist/rooms-tasks/config.js +24 -13
  87. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  88. package/dist/rooms-tasks/external-worker.js +14 -9
  89. package/dist/rooms-tasks/provision.js +98 -36
  90. package/dist/rooms-tasks/templates.d.ts +0 -1
  91. package/dist/rooms-tasks/templates.js +9 -76
  92. package/dist/rooms-tasks/types.d.ts +10 -16
  93. package/dist/rooms-tasks/types.js +1 -4
  94. package/dist/runner.d.ts +13 -16
  95. package/dist/runner.js +110 -122
  96. package/dist/sensitive-config.d.ts +2 -0
  97. package/dist/sensitive-config.js +4 -0
  98. package/dist/session/acp.d.ts +7 -2
  99. package/dist/session/acp.js +29 -6
  100. package/dist/session/activity.d.ts +2 -3
  101. package/dist/session/activity.js +2 -3
  102. package/dist/session/arbiter.d.ts +4 -4
  103. package/dist/session/control.d.ts +17 -4
  104. package/dist/session/control.js +35 -0
  105. package/dist/session/types.d.ts +11 -5
  106. package/dist/spawn.d.ts +8 -33
  107. package/dist/spawn.js +53 -213
  108. package/dist/supervisor/none.d.ts +1 -1
  109. package/dist/supervisor/none.js +2 -2
  110. package/dist/temp-lifecycle.d.ts +1 -1
  111. package/dist/tmux.d.ts +1 -14
  112. package/dist/tmux.js +1 -51
  113. package/dist/watchdog/config.d.ts +10 -7
  114. package/dist/watchdog/config.js +30 -18
  115. package/dist/watchdog/run.js +12 -36
  116. package/dist/web/auth.d.ts +1 -1
  117. package/dist/web/fleet-config-service.d.ts +11 -8
  118. package/dist/web/fleet-config-service.js +384 -183
  119. package/dist/web/runtime.js +4 -15
  120. package/dist/web/server.d.ts +1 -3
  121. package/dist/web/server.js +3 -14
  122. package/dist/web/topology-promote.js +26 -19
  123. package/dist/web/topology.js +2 -2
  124. package/dist/web/yaml-document-edit.js +2 -0
  125. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  126. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  127. package/dist/web-app/index.html +2 -2
  128. package/examples/fleet/agents/Alice.yaml +11 -0
  129. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  130. package/examples/fleet/brains/claude-default.yaml +5 -0
  131. package/examples/fleet/roles/coordinator.yaml +5 -0
  132. package/examples/fleet/roles/developer.yaml +3 -0
  133. package/examples/fleet.yaml +74 -0
  134. package/package.json +5 -8
  135. package/presets/fleet/agents/Agent.yaml +3 -0
  136. package/presets/fleet/agents/Architect.yaml +3 -0
  137. package/presets/fleet/agents/Critic.yaml +3 -0
  138. package/presets/fleet/agents/Developer.yaml +3 -0
  139. package/presets/fleet/agents/Secretary.yaml +3 -0
  140. package/presets/fleet/agents/Tester.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/roles/Agent.yaml +6 -0
  143. package/presets/fleet/roles/Architect.yaml +6 -0
  144. package/presets/fleet/roles/Critic.yaml +6 -0
  145. package/presets/fleet/roles/Developer.yaml +6 -0
  146. package/presets/fleet/roles/Secretary.yaml +6 -0
  147. package/presets/fleet/roles/Tester.yaml +6 -0
  148. package/presets/fleet/room_templates/pair.yaml +11 -0
  149. package/presets/fleet/room_templates/single.yaml +8 -0
  150. package/presets/fleet/room_templates/team.yaml +12 -0
  151. package/presets/fleet.yaml +7 -0
  152. package/presets/manifest.json +5 -0
  153. package/dist/session/tmux.d.ts +0 -28
  154. package/dist/session/tmux.js +0 -80
  155. package/dist/web/terminal/bridge.d.ts +0 -27
  156. package/dist/web/terminal/bridge.js +0 -317
  157. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  158. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  159. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
package/README.md CHANGED
@@ -8,8 +8,8 @@ harnesses — from one declarative file.**
8
8
  An AI coding agent in a terminal dies when you close the laptop. `ours-fleet`
9
9
  turns such sessions into **roles**: long-lived agents that
10
10
 
11
- - **run through a selectable session backend** — existing detached tmux consoles
12
- or structured ACP sessions — which you can attach to, peek at, or prompt,
11
+ - **run through structured ACP sessions** behind a shared Codex/Claude Code
12
+ agent-session interface, which you can inspect or prompt,
13
13
  - are **supervised** — systemd (Linux) or launchd (macOS) restarts them on crash
14
14
  and brings them back after a reboot,
15
15
  - **resume their context** across restarts (when the harness supports it),
@@ -19,10 +19,10 @@ turns such sessions into **roles**: long-lived agents that
19
19
  - can **spawn subagents** (permanent or temporary) and **oversee** them: peek into
20
20
  a ward's console, answer a stuck prompt, nudge it back to work.
21
21
 
22
- The whole fleet is described in one hand-written `~/fleet.yaml`
23
- ("docker-compose for agents"): who exists, what harness they run in, their mission,
24
- persona, bio, working directory, and who oversees whom. `ours-fleet up` makes
25
- reality match the file.
22
+ The fleet uses a v2 manifest plus typed documents: `~/fleet.yaml` holds
23
+ fleet-wide operational policy and automation, while `~/fleet/agents`,
24
+ `~/fleet/roles`, and `~/fleet/brains` hold bare Agent, Role, and Brain documents.
25
+ `ours-fleet up` makes reality match that trusted split source set.
26
26
 
27
27
  **Harness-agnostic by design.** The core never assumes a specific agent CLI; each
28
28
  harness is a small adapter (how to launch, how to resume, how to wire config).
@@ -31,21 +31,22 @@ public — each additional harness (Gemini CLI, OpenCode, …) is a small adapte
31
31
  A single fleet can mix harnesses per role:
32
32
 
33
33
  ```yaml
34
- roles:
35
- Reviewer: # runs in Claude Code
36
- harness: claude-code
37
- Prototyper: # runs in Codex CLI
38
- harness: codex
34
+ # ~/fleet/agents/Reviewer.yaml
35
+ role: { ref: reviewer }
36
+ brain: { inline: { harness: claude-code, session: acp } }
37
+
38
+ # ~/fleet/agents/Prototyper.yaml
39
+ role: { inline: { mission: Build prototypes } }
40
+ brain: { inline: { harness: codex, session: acp } }
39
41
  ```
40
42
 
41
43
  ## How it works
42
44
 
43
45
  ```
44
- ~/fleet.yaml + ~/fleet.d/*.yaml your declaration
46
+ ~/fleet.yaml + ~/fleet/{agents,roles,brains,room_templates}/*.yaml your declaration
45
47
  │ ours-fleet up
46
48
  ▼
47
- briefing.md per role ──► tmux session ──► harness CLI (claude …)
48
- └─► ACP client ──► ACP agent (codex-acp …)
49
+ briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
49
50
  ▲ │
50
51
  systemd --user / launchd ───────┘ restart on crash, start at boot/login
51
52
  ```
@@ -78,7 +79,6 @@ The state dir contract:
78
79
  | What | Why | Install |
79
80
  |---|---|---|
80
81
  | Node ≥ 20 | runs `ours-fleet` itself | nodejs.org, `apt`, or `brew` |
81
- | tmux | roles using `session: tmux` (the default) | `apt install tmux` / `brew install tmux` |
82
82
  | Node ≥ 22 | Claude roles using `session: acp` | required by the maintained Claude ACP adapter |
83
83
  | a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
84
84
  | `ours` CLI + shared daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/cli && ours daemon start` |
@@ -91,15 +91,15 @@ equivalent); logs land in `~/.ours-fleet/logs/`.
91
91
 
92
92
  ```sh
93
93
  npm i -g @ours.network/fleet
94
- ours-fleet init # units/dirs/linger for this user
94
+ ours-fleet init # units/dirs/linger + missing standard presets
95
95
  ours-fleet doctor # verifies everything above, with actionable messages
96
96
  ```
97
97
 
98
98
  The maintained Codex and Claude ACP adapters install as optional dependencies of
99
99
  `ours-fleet` and are resolved internally; users do not install adapter commands
100
100
  or add them to `PATH`. An explicit `session_options.acp.command` remains
101
- available for custom adapters. On Node 20–21, tmux and Codex ACP remain
102
- available, while maintained Claude ACP requires upgrading to Node 22.
101
+ available for custom adapters. On Node 20–21, Codex ACP remains available,
102
+ while maintained Claude ACP requires upgrading to Node 22.
103
103
 
104
104
  Each OS user manages their own fleet — to host roles under a sandboxed account,
105
105
  become that account and repeat.
@@ -107,8 +107,9 @@ become that account and repeat.
107
107
  ## Quickstart
108
108
 
109
109
  ```sh
110
- cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
111
- $EDITOR ~/fleet.yaml # name your roles, missions, personas
110
+ ours-fleet init # safe to repeat: creates missing files, never replaces edits
111
+ ours-fleet config # validates Agents, Roles, Brains, and Room templates
112
+ $EDITOR ~/fleet/agents/*.yaml # compose Role + Brain and operational settings
112
113
  ours-fleet up # boot the fleet (staggered)
113
114
  ours-fleet ls # running consoles
114
115
  ours-fleet attach Alice # watch one live (Ctrl-b d to leave)
@@ -125,22 +126,19 @@ ours-fleet spawn Worker --mission "own the worker repo" \
125
126
  ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/reboot
126
127
 
127
128
  # Codex role: ours-codex is preferred automatically; plain codex is the fallback
128
- ours-fleet spawn Coder --harness codex --model gpt-5.4 \
129
- --session acp --approval ask --filesystem workspace \
130
- --profile fleet --search --monitor --coordinator FleetCoordinator
131
- # Note: --monitor is legacy consent for Codex's native monitor. Choose the
132
- # wake owner separately in fleet.yaml with monitor.mode: fleet|native.
129
+ ours-fleet spawn Coder --brain codex-fleet --role developer \
130
+ --approval ask --filesystem workspace --coordinator FleetCoordinator
133
131
  ```
134
132
 
135
133
  Inside a managed ACP role, `ours-fleet spawn` is transparently routed through
136
- that role's live supervisor. `ours-fleet spawn --role DeveloperX --temp` is the
137
- minimal form: omitted harness, session, cwd, coordinator, neutral permissions,
138
- fleet monitor policy, and same-harness model inherit from the caller. Explicit
139
- flags win; changing harness without a model lets the selected harness/fleet
140
- defaults choose one. After creation succeeds, fleet can deterministically notify
134
+ that role's live supervisor. `ours-fleet spawn DeveloperX --temp` is the
135
+ minimal form: omitted Brain/Role selections, cwd, coordinator, neutral permissions,
136
+ and fleet monitor policy inherit from the caller. Identity, environment, owner routing,
137
+ room startup, and sensitive inline Brain values do not. Explicit flags win. After
138
+ creation succeeds, fleet can deterministically notify
141
139
  the caller's owner channel with the caller and spawned-role details. This is an
142
- honest-actor convenience and attribution path, not a security boundary; tmux,
143
- host shells, and deliberately bypassed absolute binaries retain direct behavior.
140
+ honest-actor convenience and attribution path, not a security boundary; host
141
+ shells and deliberately bypassed absolute binaries retain direct behavior.
144
142
 
145
143
  ## Local web console
146
144
 
@@ -209,8 +207,8 @@ offline shell and no stale fleet state.
209
207
 
210
208
  The console provides evidence-separated inventory and status, ACP activity and
211
209
  permission controls, redacted logs, typed text send, confirmed lifecycle
212
- actions, transactional permanent/temporary creation, and a shared tmux browser
213
- terminal. Identity is fixed to the role name. Creation uses the authenticated
210
+ actions, and transactional permanent/temporary creation. Identity is fixed to
211
+ the role name. Creation uses the authenticated
214
212
  daemon inventory and reports verified, missing, or unknown evidence. A missing
215
213
  permanent role identity is created deterministically before launch with local
216
214
  discovery and auto-accept enabled, then the provisioning lease is released so
@@ -243,41 +241,30 @@ never force-adopts or deletes identity state. Permanent roles are
243
241
  provisioned by fleet before launch and never delegate normal identity creation
244
242
  to the harness.
245
243
 
246
- `node-pty` is optional: if its native module cannot load, ACP and all
247
- non-terminal features remain available and tmux Terminal is disabled with a
248
- diagnostic.
244
+ The web console uses structured ACP activity rather than exposing a raw agent terminal.
249
245
 
250
246
  Security boundaries:
251
247
 
252
248
  - configured `Host` and `Origin`, CSRF, one-time WebSocket tickets, and explicit
253
249
  bind/origin policy are enforced server-side;
254
250
  - cwd values must resolve beneath configured local roots;
255
- - terminal bytes are intentionally unredacted and are never copied into audit
256
- records; normal logs are bounded and redacted;
257
- - tests use temporary fleet homes, fake supervisors/identity providers, and
258
- isolated tmux sockets—never active role state.
259
-
260
- Permanent spawns are written to `~/fleet.d/<Name>.yaml`; the CLI never edits your
261
- hand-written `~/fleet.yaml`. `ours-fleet rm <Name>` unspawns.
262
-
263
- The web console is the one writer that can touch the base file, and it saves the
264
- file as a whole document: its setup wizard and configuration editor may create,
265
- change or remove any top-level block, including `vars:`, `defaults:`, `roles:`,
266
- `watchdogs:` and `loops:`. (`defaults:`, `watchdogs:` and `loops:` can only live
267
- in the base file — a `~/fleet.d/*.yaml` drop-in may declare `roles:` and nothing
268
- else.) Top-level keys the console does not recognise are round-tripped untouched.
269
-
270
- Console edits are applied as surgical splices against the file's exact bytes, so
271
- an unchanged save is a byte-for-byte no-op and lines outside the edit keep their
272
- comments, spacing and quoting. One bounded exception: changing the *length* of a
273
- block sequence — adding or removing an entry under `watch:`, `oversee:`, `roles:`
274
- or `wake_sources:` — rewrites that one collection as a whole, which drops inline
275
- comments written on its individual items. The loss is confined to the collection
276
- you edited, is shown in the diff before anything is written, and can be declined
277
- by not saving. Each write is revision-guarded, reviewed as a diff of the real
278
- file, validated with the real loader, and preceded by a timestamped backup.
279
- Values under `env:` — and the `vars:` entries they interpolate — are masked in
280
- the diff and never leave the host.
251
+ - normal logs are bounded and redacted;
252
+ - tests use temporary fleet homes and fake supervisors/identity providers—never
253
+ active role state.
254
+
255
+ Permanent spawns write a bare Agent document to
256
+ `~/fleet/agents/<Name>.yaml`; generated-source ownership is recorded so removal
257
+ never deletes an unproven hand-written neighbor. `ours-fleet rm <Name>` unspawns.
258
+
259
+ The web editor reads and writes the split document model explicitly as
260
+ `{manifest, agents}`. Role and Brain presets are visible through Agent refs but
261
+ remain read-only. Saves use one aggregate revision over the manifest and all
262
+ Agent/Role/Brain sources, validate an exact-stem private staging tree, show a
263
+ deterministic redacted diff per document, and hold one manifest-root lock.
264
+ Changed/deleted files are backed up together; any partial failure restores every
265
+ document or reports the private recovery directory. An unchanged save is a
266
+ byte-for-byte no-op and creates no backup. Nested environment, authentication,
267
+ invite, and isolation secrets never leave the host.
281
268
 
282
269
  From inside Claude Code, Codex, or Hermes with the core `ours` plugin installed,
283
270
  say **"spawn an ours agent …"**. The core skill checks for `ours-fleet`, installs
@@ -292,20 +279,19 @@ them and unsticks them:
292
279
 
293
280
  ```sh
294
281
  ours-fleet peek Worker # what is it doing?
295
- ours-fleet send Worker --key 1 # answer the menu it's stuck on
296
282
  ours-fleet send Worker "continue with the tests, then report"
297
283
  ```
298
284
 
299
- Declare standing assignments in `fleet.yaml` (rendered into the overseer's
285
+ Declare standing assignments in the overseer's bare Agent document (rendered into its
300
286
  briefing) — or just write "keep an eye on Alice and Bob every 5 minutes" in a
301
287
  persona; the bundled `oversee-agents` skill defines what that means operationally:
302
288
 
303
289
  ```yaml
304
- roles:
305
- FleetCoordinator:
306
- oversee:
307
- - { role: Alice, interval: 5m }
308
- - { role: Bob, interval: 5m }
290
+ role: { ref: coordinator }
291
+ brain: { ref: claude-default }
292
+ oversee:
293
+ - { agent: Alice, interval: 5m }
294
+ - { agent: Bob, interval: 5m }
309
295
  ```
310
296
 
311
297
  ## Command reference
@@ -315,8 +301,8 @@ ours-fleet docs | man AI-friendly complete reference
315
301
  ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
316
302
  ours-fleet config [-c FILE] validate + print merged plan
317
303
  ours-fleet ls | attach | peek | logs [-f] | status <Name>
318
- ours-fleet send <Name> "text" | --key <K>
319
- ours-fleet spawn [--temp] [<Name> | --role <Name>] [--harness --session --mission --model --approval ...]
304
+ ours-fleet send <Name> "text"
305
+ ours-fleet spawn [--temp] [<Name> | --name <Name>] --brain <ID|inline:{...}> --role <ID|inline:{...}> [--approval ...]
320
306
  ours-fleet loops validate|list|status
321
307
  ours-fleet loops reload <Role>
322
308
  ours-fleet loops run-now|disable|enable <Role> <Loop>
@@ -348,33 +334,31 @@ in its state dir) across supervisor-triggered restarts — systemd/launchd re-in
348
334
  agent process with no arguments, so without this the role would silently fall back to
349
335
  the default `~/fleet.yaml` on its very first crash-restart and fail to resolve.
350
336
 
351
- ## fleet.yaml reference
337
+ ## split configuration field reference
352
338
 
353
- ```yaml
339
+ The following is a schematic field inventory, not a single YAML document. Host
340
+ settings belong in `~/fleet.yaml`; reusable behavior and harness settings belong
341
+ in `~/fleet/roles/<id>.yaml` and `~/fleet/brains/<id>.yaml`; operational fields
342
+ belong in `~/fleet/agents/<id>.yaml`.
343
+
344
+ ```text
354
345
  vars: { work_root: /home/me/work } # ${var} substitution anywhere below
346
+ api_version: ours.network/fleet/v2
355
347
  start_stagger_ms: 0 # delay between agent LAUNCHES (host-wide, ms); 0 = no stagger
356
348
  defaults:
357
- harness: claude-code # for roles that don't set one
358
- session: tmux # tmux (default) | acp
359
349
  permissions: # common intent, translated by each harness/backend
360
350
  approval: ask # ask | auto | allow (`deny` is a deprecated alias)
361
351
  filesystem: workspace # read-only | workspace | unrestricted
362
352
  unattended: deny # deny | wait
363
- model: claude-fable-5 # default model for roles that don't set one (per-role model / --model wins)
364
- max_tokens: 500000 # session cap (harness-interpreted)
365
353
  monitor:
366
354
  mode: fleet # fleet (default) | native
367
355
  worklog: # built-ins shown; set false to opt out
368
356
  max_kb: 1024 # rotate only above this active-log size
369
357
  keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
370
358
  max_archives: 12 # recent beside log; older preserved cold
371
- roles:
372
- Name: # [A-Za-z0-9_-]+
373
- harness: claude-code
374
- session: acp # one flag selects ACP; omit for tmux
375
- session_options:
376
- acp:
377
- command: claude-agent-acp # optional advanced override
359
+ Agent document (\`~/fleet/agents/Name.yaml\`):
360
+ role: { ref: RoleID }
361
+ brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
378
362
  identity: "Display Name" # ours identity to bind (default: Name)
379
363
  cwd: ${work_root}/repo # where the harness process runs
380
364
  coordinator: FleetCoordinator # announce target on boot
@@ -406,7 +390,7 @@ roles:
406
390
  bio: | # public card (published as bio)
407
391
  briefing_file: curated.md # replaces the generated narrative
408
392
  env: { KEY: value } # extra session env
409
- oversee: [{ role: X, interval: 5m }]
393
+ oversee: [{ agent: X, interval: 5m }]
410
394
  harness_options: # adapter-owned, adapter-validated
411
395
  plugins: { "name@marketplace": false } # claude-code: plugin overrides
412
396
  # mem_palace: false # claude-code: disable memory plugin
@@ -444,22 +428,39 @@ loops: # trusted local scheduled ACP turns
444
428
  If nothing material changed, complete silently without an owner report.
445
429
  ```
446
430
 
447
- Merge order: `fleet.yaml` ← `fleet.d/*.yaml`; a duplicate role name is a hard
448
- error naming both files. Identities and roles are decoupled — removing a role
449
- never deletes an identity. `session` is independent of `harness`, so changing a
450
- role from tmux to ACP does not change its identity, mission, monitor, or permission
451
- contract. `defaults.harness_options` is shallow-merged with each
452
- role's `harness_options`, so a fleet can set common Codex permission/profile defaults
453
- and override individual keys per role. `monitor` merges the same way — a role block
454
- overrides `defaults.monitor` key-by-key.
431
+ The v2 loader reads the manifest, then sorted bare Role, Brain, and Agent
432
+ documents from its exact stem directory. Duplicate filename-derived IDs are a
433
+ hard error. Identities and Agents are decoupled—removing an Agent never deletes
434
+ an identity. Role, Brain, and operational ownership are composed explicitly;
435
+ there is no generic cross-kind merge. Manifest operational defaults such as
436
+ `monitor` and `permissions` merge only within their owning Agent fields.
455
437
 
456
438
  Every supervised role is a client of the operator-configured ours daemon. Fleet strips the
457
- obsolete, presence-sensitive `OURS_AUTOSTART` variable from both tmux and ACP harness
439
+ obsolete, presence-sensitive `OURS_AUTOSTART` variable from ACP harness
458
440
  processes. `ours-mcp proxy` is client-only and never starts a daemon; operators and explicit
459
441
  installer/setup flows remain responsible for starting it.
460
442
 
461
443
  ### Rooms and tasks
462
444
 
445
+ Init installs editable `single`, `pair`, and `team` definitions under
446
+ `~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
447
+ reference. Inspect them with `ours-fleet template list` and
448
+ `ours-fleet template show team`. After configuring the authenticated owner below:
449
+
450
+ ```sh
451
+ ours-fleet task create --title "Solo task" --template single
452
+ ours-fleet task create --title "Reviewed change" --template pair
453
+ ours-fleet task create --title "Phased delivery" --template team
454
+ ```
455
+
456
+ `ours-fleet init -c /path/custom.yaml` seeds `/path/custom/` instead. Init reports
457
+ the packaged preset revision and source directory and only seeds missing files.
458
+ It never upgrades an edited preset. To adopt a newer packaged file explicitly,
459
+ copy the reported source file beside the existing target as `.new-default`, review
460
+ `diff -u`, then replace the target yourself. This is the sole adoption operation;
461
+ rerunning init is not an update. Users upgrading from hardcoded templates should
462
+ run init once (with the same `-c` selection they normally use).
463
+
463
464
  Rooms always use `ours-cowork`; there is no room-provider selector. Configure
464
465
  the cowork daemon connection and the room owner directly:
465
466
 
@@ -597,9 +598,7 @@ since a human can still attach a console and answer.
597
598
  `auto` selects Codex ACP `agent` (`on-request` + `workspace-write`) / Claude
598
599
  `acceptEdits`. `allow` selects Codex ACP's fully non-interactive yolo mode,
599
600
  reported by the adapter as `agent-full-access` (`never` +
600
- `danger-full-access`); Claude uses `bypassPermissions`. For Codex tmux, where the
601
- approval and sandbox flags remain independent, `auto` is `on-request` and
602
- `allow` is `never` while `filesystem` still chooses the sandbox.
601
+ `danger-full-access`); Claude uses `bypassPermissions`.
603
602
  `dontAsk` suppresses only the *prompt*, not the denial, which is why an
604
603
  `allow` role previously ran unable to do its job. Legacy `deny` is accepted
605
604
  only for compatibility and retains its conservative Codex `on-request` /
@@ -689,8 +688,8 @@ their boots ~4 s apart instead of firing all seven at once.
689
688
  With `monitor.mode: fleet` (the default), the **ours-fleet supervisor** delivers
690
689
  a role's mail wakes: the per-role runner long-polls the ours daemon's notification API and
691
690
  submits a single `[fleet-monitor] N new messages from … — run get_messages` prompt
692
- through the selected backend. ACP uses live steering when its adapter supports it
693
- and falls back to structured `session/prompt`; tmux uses verified console input.
691
+ through the shared agent session. ACP uses live steering when its adapter supports it
692
+ and falls back to structured `session/prompt`.
694
693
  Set `monitor.interrupt: true` on roles where every configured wake should cancel
695
694
  the active turn before the notification is delivered. This is intentionally
696
695
  content-blind: the supervisor cannot inspect encrypted message bodies, so all
@@ -700,8 +699,7 @@ tool result or pending permission. Fleet waits for terminal ACP tool/update
700
699
  evidence, then steers the wake without calling `session.cancel`. If the tool is
701
700
  still active after 120 seconds, or the adapter cannot expose authenticated tool
702
701
  boundaries/steering, fleet visibly degrades to non-cancelling steering or queued
703
- delivery. Tmux never receives `C-c` for `after_tool`. Explicit human and control
704
- interrupts remain immediate.
702
+ delivery. Explicit human and control interrupts remain immediate.
705
703
  The default is `false`: a role that must begin a post-readiness mission
706
704
  immediately, including second-and-later mail received while it is working, must
707
705
  set `monitor.mode: fleet` and `monitor.interrupt: true` explicitly. Readiness and
@@ -794,8 +792,8 @@ invites, message bodies, credentials, or keys. A corrupt overlay fails closed
794
792
  (no effective owners and no mutation), and the last effective owner cannot be
795
793
  revoked.
796
794
 
797
- These commands require a running ACP role with `owner_channel` enabled. Missing,
798
- stopped, tmux, disabled, draining, and unavailable-MCP targets fail without
795
+ These commands require a running role with `owner_channel` enabled. Missing,
796
+ stopped, disabled, draining, and unavailable-MCP targets fail without
799
797
  starting a second client, binding an identity, or opening a network listener.
800
798
 
801
799
  The managed agent can use its ordinary ours `send_message` or `send_file` tool to
@@ -1015,11 +1013,8 @@ gets a durable pre-send marker: a failed/ambiguous send becomes `uncertain` and
1015
1013
  not blindly retried, while files not yet attempted remain recoverable after restart.
1016
1014
  Logs contain counts and byte totals, never filenames or raw bytes.
1017
1015
 
1018
- Owner channels currently require `session: acp`. Fleet needs structured,
1019
- turn-correlated assistant output for automatic replies; scraping a tmux pane
1020
- cannot reliably distinguish the final answer from thoughts, tool output, or
1021
- unrelated concurrent work. The config rejects tmux instead of silently offering
1022
- weaker semantics.
1016
+ Owner channels use the structured agent-session interface for turn-correlated
1017
+ assistant output and reliable automatic replies.
1023
1018
 
1024
1019
  ## Codex roles
1025
1020
 
@@ -1050,40 +1045,34 @@ the structured CLI watcher, because a detached process cannot wake a Codex turn.
1050
1045
  wait is re-entered after each handled message; `ours-codex` instead wakes the idle
1051
1046
  session through its App Server integration.
1052
1047
 
1053
- The main Codex controls needed by fleet roles are available declaratively and when spawning:
1048
+ The main Codex controls are Brain-owned and also available when spawning:
1054
1049
 
1055
1050
  ```yaml
1056
- defaults:
1057
- harness: codex
1058
- model: gpt-5.4
1059
- harness_options:
1060
- launcher: auto
1061
- profile: fleet # $CODEX_HOME/fleet.config.toml
1062
- sandbox: workspace-write
1063
- approval: on-request
1064
- monitor: true # consent for Codex's native monitor; not the wake-owner selector
1065
- search: true
1066
- add_dirs: [/data/shared]
1067
- config:
1068
- model_reasoning_effort: high
1069
-
1070
- roles:
1071
- Reviewer:
1072
- harness_options:
1073
- sandbox: read-only # overrides just this default key
1051
+ # ~/fleet/brains/codex-review.yaml
1052
+ harness: codex
1053
+ session: acp
1054
+ model: gpt-5.4
1055
+ harness_options:
1056
+ launcher: auto
1057
+ profile: fleet
1058
+ sandbox: read-only
1059
+ approval: on-request
1060
+ monitor: true
1061
+ search: true
1062
+ add_dirs: [/data/shared]
1063
+ config: { model_reasoning_effort: high }
1074
1064
  ```
1075
1065
 
1076
- Equivalent one-off/permanent spawn controls include `--model`, `--permission-mode`,
1077
- `--sandbox`, `--profile`, `--launcher`, `--search`, legacy `--monitor` (native
1078
- Codex monitor consent), repeatable
1079
- `--codex-config key=value`, and repeatable `--add-dir`. Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1066
+ One-off/permanent spawn selects a Brain that owns model, reasoning, native permission,
1067
+ sandbox, profile, launcher, search, monitor consent, native config, and additional roots.
1068
+ Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1080
1069
  role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
1081
1070
 
1082
1071
  ## Agent isolation
1083
1072
 
1084
1073
  Each role can be sandboxed at the environment level via an `isolation:` block —
1085
1074
  **fully additive: a role with no block behaves exactly as before.** The agent's
1086
- tmux-pane process is wrapped in [bubblewrap](https://github.com/containers/bubblewrap)
1075
+ agent process is wrapped in [bubblewrap](https://github.com/containers/bubblewrap)
1087
1076
  (rootless, no setuid), resource-limited by `systemd-run --user --scope`.
1088
1077
 
1089
1078
  An empty `isolation: {}` gives a sensible default posture: filesystem-confined to
@@ -0,0 +1,17 @@
1
+ import type { ConversationEventV1 } from './session/conversation-types.js';
2
+ import type { AgentSession } from './session/types.js';
3
+ export interface AgentRecoveryEvidence {
4
+ ok: boolean;
5
+ reason: 'RECOVERY_TOOLS_VERIFIED' | 'RECOVERY_PROMPT_MISSING' | 'RECOVERY_TURN_INCOMPLETE' | 'RECOVERY_TURN_FAILED' | 'RECOVERY_CHOOSE_MISSING' | 'RECOVERY_CURRENT_MISSING' | 'RECOVERY_GET_MESSAGES_MISSING' | 'RECOVERY_TOOL_ORDER_INVALID';
6
+ chooseIdentity: boolean;
7
+ currentIdentity: boolean;
8
+ getMessages: boolean;
9
+ turnCompleted: boolean;
10
+ }
11
+ /**
12
+ * Verify one exact recovery turn from its durable conversation ledger. Tool
13
+ * arguments/results are inspected only to derive these booleans and are never
14
+ * returned or persisted by this gate.
15
+ */
16
+ export declare function evaluateAgentRecovery(events: readonly ConversationEventV1[], promptId: string, identity: string): AgentRecoveryEvidence;
17
+ export declare function recoverAgentIdentity(session: AgentSession, identity: string): Promise<AgentRecoveryEvidence>;
@@ -0,0 +1,135 @@
1
+ const TOOL_NAMES = {
2
+ choose: new Set(['choose_identity', 'ours.choose_identity', 'mcp__ours__choose_identity']),
3
+ current: new Set(['current_identity', 'ours.current_identity', 'mcp__ours__current_identity']),
4
+ messages: new Set(['get_messages', 'ours.get_messages', 'mcp__ours__get_messages']),
5
+ };
6
+ function recordFor(records, event) {
7
+ if (event.kind !== 'tool.upsert' || !event.toolCallId)
8
+ return undefined;
9
+ const payload = event.payload;
10
+ const previous = records.get(event.toolCallId) ?? {};
11
+ const next = {
12
+ ...previous,
13
+ ...(payload.title !== undefined ? { title: payload.title } : {}),
14
+ ...(payload.status !== undefined ? { status: payload.status } : {}),
15
+ ...(payload.rawInput !== undefined ? { rawInput: payload.rawInput } : {}),
16
+ ...(payload.status === 'completed' ? { completedSeq: event.seq } : {}),
17
+ };
18
+ records.set(event.toolCallId, next);
19
+ return next;
20
+ }
21
+ function objectInput(input) {
22
+ if (!input || input.truncated || input.redacted || !input.json
23
+ || typeof input.json !== 'object' || Array.isArray(input.json))
24
+ return undefined;
25
+ return input.json;
26
+ }
27
+ function safeChoose(record, identity) {
28
+ if (!record.title || !TOOL_NAMES.choose.has(record.title) || record.status !== 'completed')
29
+ return false;
30
+ const input = objectInput(record.rawInput);
31
+ if (!input)
32
+ return false;
33
+ const keys = Object.keys(input).sort();
34
+ if (keys.some(key => key !== 'force' && key !== 'name'))
35
+ return false;
36
+ return input.name === identity && (input.force === undefined || input.force === false);
37
+ }
38
+ function safeNoOrBoundedInput(record, names, allowed) {
39
+ if (!record.title || !names.has(record.title) || record.status !== 'completed')
40
+ return false;
41
+ if (!record.rawInput)
42
+ return allowed({});
43
+ const input = objectInput(record.rawInput);
44
+ return input !== undefined && allowed(input);
45
+ }
46
+ function safeCurrent(record) {
47
+ return safeNoOrBoundedInput(record, TOOL_NAMES.current, input => Object.keys(input).length === 0);
48
+ }
49
+ function safeMessages(record) {
50
+ return safeNoOrBoundedInput(record, TOOL_NAMES.messages, input => {
51
+ const keys = Object.keys(input);
52
+ if (keys.some(key => key !== 'limit'))
53
+ return false;
54
+ return input.limit === undefined
55
+ || (Number.isSafeInteger(input.limit) && input.limit >= 1 && input.limit <= 200);
56
+ });
57
+ }
58
+ /**
59
+ * Verify one exact recovery turn from its durable conversation ledger. Tool
60
+ * arguments/results are inspected only to derive these booleans and are never
61
+ * returned or persisted by this gate.
62
+ */
63
+ export function evaluateAgentRecovery(events, promptId, identity) {
64
+ const ordered = [...events].sort((a, b) => a.seq - b.seq);
65
+ const admitted = ordered.find(event => event.kind === 'prompt.admitted' && event.promptId === promptId);
66
+ const empty = (reason) => ({
67
+ ok: false, reason, chooseIdentity: false, currentIdentity: false,
68
+ getMessages: false, turnCompleted: false,
69
+ });
70
+ if (!admitted)
71
+ return empty('RECOVERY_PROMPT_MISSING');
72
+ const terminal = ordered.find(event => event.seq > admitted.seq
73
+ && event.kind === 'turn.completed'
74
+ && event.promptId === promptId
75
+ && event.sessionGeneration === admitted.sessionGeneration);
76
+ if (!terminal)
77
+ return empty('RECOVERY_TURN_INCOMPLETE');
78
+ const outcome = terminal.payload.outcome;
79
+ if (outcome !== 'completed')
80
+ return empty('RECOVERY_TURN_FAILED');
81
+ const records = new Map();
82
+ for (const event of ordered) {
83
+ if (event.seq <= admitted.seq || event.seq >= terminal.seq
84
+ || event.promptId !== promptId
85
+ || event.sessionGeneration !== admitted.sessionGeneration)
86
+ continue;
87
+ recordFor(records, event);
88
+ }
89
+ const values = [...records.values()];
90
+ const chooses = values.filter(record => safeChoose(record, identity));
91
+ const currents = values.filter(safeCurrent);
92
+ const messages = values.filter(safeMessages);
93
+ const chooseIdentity = chooses.length > 0;
94
+ const currentIdentity = currents.length > 0;
95
+ const getMessages = messages.length > 0;
96
+ const base = { chooseIdentity, currentIdentity, getMessages, turnCompleted: true };
97
+ if (!chooseIdentity)
98
+ return { ok: false, reason: 'RECOVERY_CHOOSE_MISSING', ...base };
99
+ if (!currentIdentity)
100
+ return { ok: false, reason: 'RECOVERY_CURRENT_MISSING', ...base };
101
+ if (!getMessages)
102
+ return { ok: false, reason: 'RECOVERY_GET_MESSAGES_MISSING', ...base };
103
+ const orderedChain = chooses.some(choose => currents.some(current => messages.some(message => choose.completedSeq !== undefined && current.completedSeq !== undefined
104
+ && message.completedSeq !== undefined
105
+ && choose.completedSeq < current.completedSeq
106
+ && current.completedSeq < message.completedSeq)));
107
+ if (!orderedChain)
108
+ return { ok: false, reason: 'RECOVERY_TOOL_ORDER_INVALID', ...base };
109
+ return { ok: true, reason: 'RECOVERY_TOOLS_VERIFIED', ...base };
110
+ }
111
+ export async function recoverAgentIdentity(session, identity) {
112
+ if (!session.subscribeConversation)
113
+ return {
114
+ ok: false, reason: 'RECOVERY_PROMPT_MISSING', chooseIdentity: false,
115
+ currentIdentity: false, getMessages: false, turnCompleted: false,
116
+ };
117
+ const events = [];
118
+ const unsubscribe = session.subscribeConversation(event => events.push(event));
119
+ try {
120
+ const queued = await session.queuePrompt([
121
+ '[fleet-recovery] The shared ours daemon restarted.',
122
+ `Call ours choose_identity with name ${JSON.stringify(identity)} and force false.`,
123
+ 'Then call current_identity, then get_messages. Complete all three in that order.',
124
+ 'Do not create/delete identities, force-bind, interrupt, or restart any service/session.',
125
+ ].join('\n'), { origin: { kind: 'fleet-monitor' } });
126
+ await queued.completion;
127
+ // Conversation publication is synchronous with terminal settlement in the
128
+ // in-tree ACP store; filtering by exact promptId/sessionGeneration remains
129
+ // the authority even if unrelated events arrived concurrently.
130
+ return evaluateAgentRecovery(events, queued.promptId, identity);
131
+ }
132
+ finally {
133
+ unsubscribe();
134
+ }
135
+ }
@@ -1,6 +1,5 @@
1
1
  import type { RoleRecord, RoleCapabilities, RoleStatus } from './types.js';
2
2
  export interface CapabilityContext {
3
- terminalPtyAvailable?: boolean;
4
3
  controlProtocolVersion?: number;
5
4
  }
6
5
  export declare function roleCapabilities(role: RoleRecord, status: RoleStatus, context?: CapabilityContext): RoleCapabilities;