@ours.network/fleet 1.1.0-nightly.1 → 1.1.0-nightly.11

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 (186) hide show
  1. package/README.md +133 -95
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/fleet-query-service.js +15 -2
  5. package/dist/application/role-command-service.js +9 -0
  6. package/dist/application/role-creation-service.d.ts +2 -21
  7. package/dist/application/role-creation-service.js +26 -64
  8. package/dist/application/role-removal-service.js +6 -3
  9. package/dist/application/task-room-service.d.ts +7 -1
  10. package/dist/application/task-room-service.js +269 -69
  11. package/dist/application/types.d.ts +1 -1
  12. package/dist/briefing.js +11 -7
  13. package/dist/build-info.json +4 -4
  14. package/dist/canonical-json.d.ts +2 -0
  15. package/dist/canonical-json.js +10 -0
  16. package/dist/cli.js +178 -65
  17. package/dist/config-yaml.d.ts +1 -1
  18. package/dist/config.d.ts +87 -2
  19. package/dist/config.js +526 -28
  20. package/dist/creation.d.ts +1 -1
  21. package/dist/creation.js +2 -2
  22. package/dist/daemon-recovery.d.ts +93 -0
  23. package/dist/daemon-recovery.js +328 -0
  24. package/dist/docs.d.ts +1 -1
  25. package/dist/docs.js +94 -102
  26. package/dist/doctor.d.ts +2 -0
  27. package/dist/doctor.js +38 -5
  28. package/dist/fleet-command-audit.d.ts +160 -0
  29. package/dist/fleet-command-audit.js +437 -0
  30. package/dist/fleet-proxy.d.ts +2 -0
  31. package/dist/fleet-proxy.js +30 -8
  32. package/dist/generated-agent-source.d.ts +9 -0
  33. package/dist/generated-agent-source.js +53 -0
  34. package/dist/harness/agent-session.d.ts +18 -0
  35. package/dist/harness/claude-code-session.d.ts +6 -0
  36. package/dist/harness/claude-code-session.js +8 -0
  37. package/dist/harness/claude-code.js +12 -0
  38. package/dist/harness/codex-session.d.ts +6 -0
  39. package/dist/harness/codex-session.js +8 -0
  40. package/dist/harness/codex.js +20 -0
  41. package/dist/harness/registry.js +4 -0
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/init-guidance.d.ts +1 -0
  45. package/dist/init-guidance.js +6 -0
  46. package/dist/model-env.d.ts +2 -3
  47. package/dist/model-env.js +3 -5
  48. package/dist/ops.js +6 -19
  49. package/dist/owner-channel/channel.d.ts +56 -0
  50. package/dist/owner-channel/channel.js +269 -29
  51. package/dist/owner-channel/commands.d.ts +2 -0
  52. package/dist/owner-channel/commands.js +31 -13
  53. package/dist/owner-channel/state.d.ts +2 -0
  54. package/dist/owner-channel/state.js +8 -2
  55. package/dist/preset-bootstrap.d.ts +10 -0
  56. package/dist/preset-bootstrap.js +85 -0
  57. package/dist/preset-migration.d.ts +27 -0
  58. package/dist/preset-migration.js +200 -0
  59. package/dist/reports/artifact.d.ts +10 -0
  60. package/dist/reports/artifact.js +32 -0
  61. package/dist/reports/index.d.ts +5 -0
  62. package/dist/reports/index.js +5 -0
  63. package/dist/reports/render.d.ts +6 -0
  64. package/dist/reports/render.js +240 -0
  65. package/dist/reports/service.d.ts +42 -0
  66. package/dist/reports/service.js +57 -0
  67. package/dist/reports/tasks.d.ts +52 -0
  68. package/dist/reports/tasks.js +177 -0
  69. package/dist/reports/types.d.ts +315 -0
  70. package/dist/reports/types.js +6 -0
  71. package/dist/resolved-plan.d.ts +3 -2
  72. package/dist/resolved-plan.js +25 -5
  73. package/dist/rooms-tasks/cli.d.ts +2 -0
  74. package/dist/rooms-tasks/cli.js +342 -65
  75. package/dist/rooms-tasks/config.js +12 -14
  76. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  77. package/dist/rooms-tasks/external-worker.js +14 -9
  78. package/dist/rooms-tasks/launch-snapshot.d.ts +10 -0
  79. package/dist/rooms-tasks/launch-snapshot.js +159 -0
  80. package/dist/rooms-tasks/member-overrides.d.ts +27 -0
  81. package/dist/rooms-tasks/member-overrides.js +170 -0
  82. package/dist/rooms-tasks/provision.js +82 -37
  83. package/dist/rooms-tasks/room-state.js +4 -0
  84. package/dist/rooms-tasks/task-state.d.ts +2 -0
  85. package/dist/rooms-tasks/task-state.js +19 -1
  86. package/dist/rooms-tasks/templates.d.ts +4 -2
  87. package/dist/rooms-tasks/templates.js +42 -78
  88. package/dist/rooms-tasks/types.d.ts +36 -17
  89. package/dist/rooms-tasks/types.js +2 -4
  90. package/dist/runner.d.ts +9 -2
  91. package/dist/runner.js +84 -20
  92. package/dist/sensitive-config.d.ts +2 -0
  93. package/dist/sensitive-config.js +4 -0
  94. package/dist/session/acp.d.ts +5 -0
  95. package/dist/session/acp.js +27 -3
  96. package/dist/session/control.d.ts +14 -1
  97. package/dist/session/control.js +35 -0
  98. package/dist/spawn.d.ts +8 -33
  99. package/dist/spawn.js +64 -212
  100. package/dist/temp-lifecycle.d.ts +1 -1
  101. package/dist/watchdog/config.d.ts +10 -7
  102. package/dist/watchdog/config.js +29 -19
  103. package/dist/watchdog/run.js +4 -13
  104. package/dist/web/fleet-config-service.d.ts +12 -8
  105. package/dist/web/fleet-config-service.js +404 -185
  106. package/dist/web/topology-promote.js +25 -15
  107. package/dist/web/topology.js +2 -2
  108. package/dist/web/yaml-document-edit.js +2 -0
  109. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  110. package/dist/web-app/index.html +1 -1
  111. package/examples/fleet/agents/Alice.yaml +11 -0
  112. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  113. package/examples/fleet/brains/claude-default.yaml +5 -0
  114. package/examples/fleet/roles/coordinator.yaml +5 -0
  115. package/examples/fleet/roles/developer.yaml +3 -0
  116. package/examples/fleet.yaml +74 -0
  117. package/package.json +3 -1
  118. package/presets/brain-catalog.json +16 -0
  119. package/presets/fleet/agent_templates/Agent.yaml +3 -0
  120. package/presets/fleet/agent_templates/Architect.yaml +3 -0
  121. package/presets/fleet/agent_templates/Critic.yaml +3 -0
  122. package/presets/fleet/agent_templates/Developer.yaml +3 -0
  123. package/presets/fleet/agent_templates/Secretary.yaml +3 -0
  124. package/presets/fleet/agent_templates/Tester.yaml +3 -0
  125. package/presets/fleet/agents/FleetCoordinator.yaml +4 -0
  126. package/presets/fleet/brains/claude-default.yaml +5 -0
  127. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  128. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  129. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  130. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  131. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  132. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  133. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  134. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  135. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  136. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  137. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  138. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  139. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  140. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  141. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  142. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  143. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  144. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  145. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  146. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  147. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  148. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  149. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  150. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  151. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  152. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  153. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  154. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  155. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  156. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  175. package/presets/fleet/roles/Agent.yaml +6 -0
  176. package/presets/fleet/roles/Architect.yaml +6 -0
  177. package/presets/fleet/roles/Critic.yaml +6 -0
  178. package/presets/fleet/roles/Developer.yaml +6 -0
  179. package/presets/fleet/roles/Secretary.yaml +6 -0
  180. package/presets/fleet/roles/Tester.yaml +6 -0
  181. package/presets/fleet/room_templates/pair.yaml +11 -0
  182. package/presets/fleet/room_templates/single.yaml +8 -0
  183. package/presets/fleet/room_templates/team.yaml +12 -0
  184. package/presets/fleet.yaml +7 -0
  185. package/presets/manifest.json +6 -0
  186. package/dist/web-app/assets/index-DhMDVRU1.js +0 -10
package/README.md CHANGED
@@ -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,17 +31,19 @@ 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,agent_templates,roles,brains,room_templates}/*.yaml your declaration
45
47
  │ ours-fleet up
46
48
  ▼
47
49
  briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
@@ -89,7 +91,7 @@ equivalent); logs land in `~/.ours-fleet/logs/`.
89
91
 
90
92
  ```sh
91
93
  npm i -g @ours.network/fleet
92
- ours-fleet init # units/dirs/linger for this user
94
+ ours-fleet init # units/dirs/linger + missing standard presets
93
95
  ours-fleet doctor # verifies everything above, with actionable messages
94
96
  ```
95
97
 
@@ -105,8 +107,9 @@ become that account and repeat.
105
107
  ## Quickstart
106
108
 
107
109
  ```sh
108
- cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
109
- $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
110
113
  ours-fleet up # boot the fleet (staggered)
111
114
  ours-fleet ls # running consoles
112
115
  ours-fleet attach Alice # watch one live (Ctrl-b d to leave)
@@ -123,19 +126,16 @@ ours-fleet spawn Worker --mission "own the worker repo" \
123
126
  ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/reboot
124
127
 
125
128
  # Codex role: ours-codex is preferred automatically; plain codex is the fallback
126
- ours-fleet spawn Coder --harness codex --model gpt-5.4 \
127
- --session acp --approval ask --filesystem workspace \
128
- --profile fleet --search --monitor --coordinator FleetCoordinator
129
- # Note: --monitor is legacy consent for Codex's native monitor. Choose the
130
- # 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
131
131
  ```
132
132
 
133
133
  Inside a managed ACP role, `ours-fleet spawn` is transparently routed through
134
- that role's live supervisor. `ours-fleet spawn --role DeveloperX --temp` is the
135
- minimal form: omitted harness, session, cwd, coordinator, neutral permissions,
136
- fleet monitor policy, and same-harness model inherit from the caller. Explicit
137
- flags win; changing harness without a model lets the selected harness/fleet
138
- 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
139
139
  the caller's owner channel with the caller and spawned-role details. This is an
140
140
  honest-actor convenience and attribution path, not a security boundary; host
141
141
  shells and deliberately bypassed absolute binaries retain direct behavior.
@@ -252,27 +252,19 @@ Security boundaries:
252
252
  - tests use temporary fleet homes and fake supervisors/identity providers—never
253
253
  active role state.
254
254
 
255
- Permanent spawns are written to `~/fleet.d/<Name>.yaml`; the CLI never edits your
256
- hand-written `~/fleet.yaml`. `ours-fleet rm <Name>` unspawns.
257
-
258
- The web console is the one writer that can touch the base file, and it saves the
259
- file as a whole document: its setup wizard and configuration editor may create,
260
- change or remove any top-level block, including `vars:`, `defaults:`, `roles:`,
261
- `watchdogs:` and `loops:`. (`defaults:`, `watchdogs:` and `loops:` can only live
262
- in the base file — a `~/fleet.d/*.yaml` drop-in may declare `roles:` and nothing
263
- else.) Top-level keys the console does not recognise are round-tripped untouched.
264
-
265
- Console edits are applied as surgical splices against the file's exact bytes, so
266
- an unchanged save is a byte-for-byte no-op and lines outside the edit keep their
267
- comments, spacing and quoting. One bounded exception: changing the *length* of a
268
- block sequence — adding or removing an entry under `watch:`, `oversee:`, `roles:`
269
- or `wake_sources:` — rewrites that one collection as a whole, which drops inline
270
- comments written on its individual items. The loss is confined to the collection
271
- you edited, is shown in the diff before anything is written, and can be declined
272
- by not saving. Each write is revision-guarded, reviewed as a diff of the real
273
- file, validated with the real loader, and preceded by a timestamped backup.
274
- Values under `env:` — and the `vars:` entries they interpolate — are masked in
275
- the diff and never leave the host.
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.
276
268
 
277
269
  From inside Claude Code, Codex, or Hermes with the core `ours` plugin installed,
278
270
  say **"spawn an ours agent …"**. The core skill checks for `ours-fleet`, installs
@@ -290,16 +282,16 @@ ours-fleet peek Worker # what is it doing?
290
282
  ours-fleet send Worker "continue with the tests, then report"
291
283
  ```
292
284
 
293
- 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
294
286
  briefing) — or just write "keep an eye on Alice and Bob every 5 minutes" in a
295
287
  persona; the bundled `oversee-agents` skill defines what that means operationally:
296
288
 
297
289
  ```yaml
298
- roles:
299
- FleetCoordinator:
300
- oversee:
301
- - { role: Alice, interval: 5m }
302
- - { 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 }
303
295
  ```
304
296
 
305
297
  ## Command reference
@@ -310,7 +302,7 @@ ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
310
302
  ours-fleet config [-c FILE] validate + print merged plan
311
303
  ours-fleet ls | attach | peek | logs [-f] | status <Name>
312
304
  ours-fleet send <Name> "text"
313
- ours-fleet spawn [--temp] [<Name> | --role <Name>] [--harness --session --mission --model --approval ...]
305
+ ours-fleet spawn [--temp] [<Name> | --name <Name>] --brain <ID|inline:{...}> --role <ID|inline:{...}> [--approval ...]
314
306
  ours-fleet loops validate|list|status
315
307
  ours-fleet loops reload <Role>
316
308
  ours-fleet loops run-now|disable|enable <Role> <Loop>
@@ -342,33 +334,64 @@ in its state dir) across supervisor-triggered restarts — systemd/launchd re-in
342
334
  agent process with no arguments, so without this the role would silently fall back to
343
335
  the default `~/fleet.yaml` on its very first crash-restart and fail to resolve.
344
336
 
345
- ## fleet.yaml reference
337
+ ## split configuration field reference
346
338
 
347
- ```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
348
345
  vars: { work_root: /home/me/work } # ${var} substitution anywhere below
346
+ api_version: ours.network/fleet/v2
349
347
  start_stagger_ms: 0 # delay between agent LAUNCHES (host-wide, ms); 0 = no stagger
350
348
  defaults:
351
- harness: claude-code # for roles that don't set one
352
- session: acp # supported Fleet↔agent session path
353
349
  permissions: # common intent, translated by each harness/backend
354
350
  approval: ask # ask | auto | allow (`deny` is a deprecated alias)
355
351
  filesystem: workspace # read-only | workspace | unrestricted
356
352
  unattended: deny # deny | wait
357
- model: claude-fable-5 # default model for roles that don't set one (per-role model / --model wins)
358
- max_tokens: 500000 # session cap (harness-interpreted)
359
353
  monitor:
360
354
  mode: fleet # fleet (default) | native
361
355
  worklog: # built-ins shown; set false to opt out
362
356
  max_kb: 1024 # rotate only above this active-log size
363
357
  keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
364
358
  max_archives: 12 # recent beside log; older preserved cold
365
- roles:
366
- Name: # [A-Za-z0-9_-]+
367
- harness: claude-code
368
- session: acp # optional; ACP is the only supported session
369
- session_options:
370
- acp:
371
- command: claude-agent-acp # optional advanced override
359
+ ```
360
+
361
+ Agent Template document (\`~/fleet/agent_templates/Worker.yaml\`) is inert and reusable:
362
+
363
+ ```yaml
364
+ role: { ref: developer }
365
+ brain: { ref: claude-default }
366
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
367
+ ```
368
+
369
+ Persistent Agent instance (\`~/fleet/agents/Name.yaml\`) either remains a canonical
370
+ Role/Brain composition or explicitly reuses a template:
371
+
372
+ ```yaml
373
+ template: Worker
374
+ overrides:
375
+ cwd: /work/name
376
+ permissions: { approval: allow }
377
+ ```
378
+
379
+ Templates never create identities, supervisors, sessions, or lifecycle/status rows.
380
+ Map overrides merge recursively; scalars and arrays replace; null deletion is rejected.
381
+
382
+ Canonical Agent document (\`~/fleet/agents/Name.yaml\`):
383
+
384
+ ```yaml
385
+ role: { ref: RoleID }
386
+ brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
387
+ identity: "Display Name" # ours identity to bind (default: Name)
388
+ cwd: ${work_root}/repo # where the harness process runs
389
+ coordinator: FleetCoordinator # announce target on boot
390
+ ```
391
+
392
+ Additional operational field inventory (schematic, not one YAML document):
393
+
394
+ ```text
372
395
  identity: "Display Name" # ours identity to bind (default: Name)
373
396
  cwd: ${work_root}/repo # where the harness process runs
374
397
  coordinator: FleetCoordinator # announce target on boot
@@ -400,7 +423,7 @@ roles:
400
423
  bio: | # public card (published as bio)
401
424
  briefing_file: curated.md # replaces the generated narrative
402
425
  env: { KEY: value } # extra session env
403
- oversee: [{ role: X, interval: 5m }]
426
+ oversee: [{ agent: X, interval: 5m }]
404
427
  harness_options: # adapter-owned, adapter-validated
405
428
  plugins: { "name@marketplace": false } # claude-code: plugin overrides
406
429
  # mem_palace: false # claude-code: disable memory plugin
@@ -438,13 +461,12 @@ loops: # trusted local scheduled ACP turns
438
461
  If nothing material changed, complete silently without an owner report.
439
462
  ```
440
463
 
441
- Merge order: `fleet.yaml` ← `fleet.d/*.yaml`; a duplicate role name is a hard
442
- error naming both files. Identities and roles are decoupled — removing a role
443
- never deletes an identity. `session` is ACP-backed for both supported harnesses.
444
- `defaults.harness_options` is shallow-merged with each
445
- role's `harness_options`, so a fleet can set common Codex permission/profile defaults
446
- and override individual keys per role. `monitor` merges the same way — a role block
447
- overrides `defaults.monitor` key-by-key.
464
+ The v2 loader reads the manifest, then sorted bare Role, Brain, and Agent
465
+ documents from its exact stem directory. Duplicate filename-derived IDs are a
466
+ hard error. Identities and Agents are decoupled—removing an Agent never deletes
467
+ an identity. Role, Brain, and operational ownership are composed explicitly;
468
+ there is no generic cross-kind merge. Manifest operational defaults such as
469
+ `monitor` and `permissions` merge only within their owning Agent fields.
448
470
 
449
471
  Every supervised role is a client of the operator-configured ours daemon. Fleet strips the
450
472
  obsolete, presence-sensitive `OURS_AUTOSTART` variable from ACP harness
@@ -453,6 +475,28 @@ installer/setup flows remain responsible for starting it.
453
475
 
454
476
  ### Rooms and tasks
455
477
 
478
+ Init installs editable `single`, `pair`, and `team` definitions under
479
+ `~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
480
+ reference. Inspect them with `ours-fleet template list` and
481
+ `ours-fleet template show team`. After configuring the authenticated owner below:
482
+
483
+ ```sh
484
+ ours-fleet task create --title "Solo task" --template single
485
+ ours-fleet task create --title "Reviewed change" --template pair
486
+ ours-fleet task create --title "Phased delivery" --template team
487
+ ```
488
+
489
+ `ours-fleet init -c /path/custom.yaml` seeds `/path/custom/` instead. Init reports
490
+ the packaged preset revision and source directory and only seeds missing files.
491
+ It never upgrades an edited preset. To adopt a newer packaged file explicitly,
492
+ copy the reported source file beside the existing target as `.new-default`, review
493
+ `diff -u`, then replace the target yourself. This is the sole adoption operation;
494
+ rerunning init is not an update. Users with the exact generated six-worker legacy
495
+ starter set must migrate explicitly: first run `ours-fleet migrate-agent-templates -c FILE`
496
+ for a zero-write plan, then review every move/addition/recovery path and rerun with
497
+ `--write`. Customized or ambiguous known starters are refused without
498
+ mutation; unrelated custom persistent Agents are preserved.
499
+
456
500
  Rooms always use `ours-cowork`; there is no room-provider selector. Configure
457
501
  the cowork daemon connection and the room owner directly:
458
502
 
@@ -1037,33 +1081,27 @@ the structured CLI watcher, because a detached process cannot wake a Codex turn.
1037
1081
  wait is re-entered after each handled message; `ours-codex` instead wakes the idle
1038
1082
  session through its App Server integration.
1039
1083
 
1040
- The main Codex controls needed by fleet roles are available declaratively and when spawning:
1084
+ The main Codex controls are Brain-owned and also available when spawning:
1041
1085
 
1042
1086
  ```yaml
1043
- defaults:
1044
- harness: codex
1045
- model: gpt-5.4
1046
- harness_options:
1047
- launcher: auto
1048
- profile: fleet # $CODEX_HOME/fleet.config.toml
1049
- sandbox: workspace-write
1050
- approval: on-request
1051
- monitor: true # consent for Codex's native monitor; not the wake-owner selector
1052
- search: true
1053
- add_dirs: [/data/shared]
1054
- config:
1055
- model_reasoning_effort: high
1056
-
1057
- roles:
1058
- Reviewer:
1059
- harness_options:
1060
- sandbox: read-only # overrides just this default key
1087
+ # ~/fleet/brains/codex-review.yaml
1088
+ harness: codex
1089
+ session: acp
1090
+ model: gpt-5.4
1091
+ harness_options:
1092
+ launcher: auto
1093
+ profile: fleet
1094
+ sandbox: read-only
1095
+ approval: on-request
1096
+ monitor: true
1097
+ search: true
1098
+ add_dirs: [/data/shared]
1099
+ config: { model_reasoning_effort: high }
1061
1100
  ```
1062
1101
 
1063
- Equivalent one-off/permanent spawn controls include `--model`, `--permission-mode`,
1064
- `--sandbox`, `--profile`, `--launcher`, `--search`, legacy `--monitor` (native
1065
- Codex monitor consent), repeatable
1066
- `--codex-config key=value`, and repeatable `--add-dir`. Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1102
+ One-off/permanent spawn selects a Brain that owns model, reasoning, native permission,
1103
+ sandbox, profile, launcher, search, monitor consent, native config, and additional roots.
1104
+ Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1067
1105
  role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
1068
1106
 
1069
1107
  ## Agent isolation
@@ -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
+ }
@@ -4,6 +4,7 @@ import { classifyActivity } from '../session/activity.js';
4
4
  import { controlRequest } from '../session/control.js';
5
5
  import { SessionControlError } from '../session/types.js';
6
6
  import { readExitRecord, readRestartLedger } from '../runner.js';
7
+ import { readDaemonRecoveryStatus } from '../daemon-recovery.js';
7
8
  import { roleCapabilities } from './capabilities.js';
8
9
  import { FleetError } from './errors.js';
9
10
  const clean = (value, max = 512) => value.replace(/[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').trim().slice(0, max);
@@ -46,7 +47,8 @@ function readIsolation(dir) {
46
47
  function sessionOverall(supervisor, session, restart, monitor, isolation, problems) {
47
48
  if (restart.circuit === 'open' || monitor.health === 'failed' || monitor.health === 'degraded'
48
49
  || isolation.degraded
49
- || problems.some(problem => problem.severity === 'error' || problem.source === 'watchdog')
50
+ || problems.some(problem => problem.severity === 'error'
51
+ || problem.source === 'watchdog' || problem.source === 'daemon-recovery')
50
52
  || session.readiness === 'failed')
51
53
  return 'attention';
52
54
  // A reachable agent session is the user's live interaction surface. Its
@@ -74,7 +76,8 @@ export class FleetQueryService {
74
76
  this.control = options.control ?? controlRequest;
75
77
  }
76
78
  async list() {
77
- const roles = await this.options.repository.list();
79
+ const roles = (await this.options.repository.list())
80
+ .filter(role => role.configured && role.lifetime === 'permanent');
78
81
  return Promise.all(roles.map(async (role) => {
79
82
  const status = await this.status(role);
80
83
  return { role, status, capabilities: roleCapabilities(role, status, this.options.capabilityContext) };
@@ -103,6 +106,16 @@ export class FleetQueryService {
103
106
  const lastExit = dir ? readExitRecord(join(dir, '.exit-status')) ?? undefined : undefined;
104
107
  const session = await this.session(role, dir, live.state);
105
108
  const problems = [...role.problems];
109
+ const recovery = dir ? readDaemonRecoveryStatus(dir) : undefined;
110
+ if (recovery && recovery.state !== 'recovered') {
111
+ const paths = ['agent', 'owner']
112
+ .filter(name => recovery.paths[name].state !== 'recovered')
113
+ .map(name => `${name}:${recovery.paths[name].state}`);
114
+ problems.push({
115
+ code: 'daemon_recovery', severity: 'warning', source: 'daemon-recovery',
116
+ detail: `${recovery.state}; ${paths.join(', ') || 'no degraded path'}; epoch ${recovery.epoch || 'unavailable'}`,
117
+ });
118
+ }
106
119
  if (live.state === 'running' && session.reachability !== 'online')
107
120
  problems.push({
108
121
  code: 'supervisor_session_disagreement', severity: 'warning',
@@ -12,6 +12,10 @@ export class RoleLifecycleService {
12
12
  const config = input.config ?? loadConfig(this.options.configPath);
13
13
  const selected = input.roleIds.length ? input.roleIds : config.roles.map(role => role.name);
14
14
  for (const roleId of selected) {
15
+ if (config.agentTemplates?.[roleId] && !config.roles.some(role => role.name === roleId))
16
+ throw new FleetError('capability_unavailable', `'${roleId}' is an inert Agent Template; lifecycle commands target only persistent fleet/agents instances`);
17
+ if (!config.roles.some(role => role.name === roleId))
18
+ throw new FleetError('role_not_found', `no such role '${roleId}'`);
15
19
  const role = await this.options.repository.get(roleId);
16
20
  if (!role)
17
21
  throw new FleetError('role_not_found', `no such role '${roleId}'`);
@@ -51,6 +55,11 @@ export class RoleCommandService {
51
55
  throw new FleetError('idempotency_conflict', 'action ID already belongs to another operation');
52
56
  return prior;
53
57
  }
58
+ const config = loadConfig(this.options.configPath);
59
+ if (config.agentTemplates?.[input.roleId] && !config.roles.some(role => role.name === input.roleId))
60
+ throw new FleetError('capability_unavailable', `'${input.roleId}' is an inert Agent Template; lifecycle commands target only persistent fleet/agents instances`);
61
+ if (!config.roles.some(role => role.name === input.roleId))
62
+ throw new FleetError('role_not_found', `no such role '${input.roleId}'`);
54
63
  const role = await this.options.repository.get(input.roleId);
55
64
  if (!role)
56
65
  throw new FleetError('role_not_found', `no such role '${input.roleId}'`);
@@ -5,21 +5,14 @@ import type { OpsDeps } from '../ops.js';
5
5
  import type { ResolvedRole } from '../config.js';
6
6
  import { FleetError } from './errors.js';
7
7
  import { type ManagedFleetSpawnResult } from '../fleet-proxy.js';
8
- import { type HarnessModelCatalog, type HarnessModelOption } from './model-catalog.js';
9
8
  export interface CreateRoleSessionRequest {
10
9
  name: string;
11
- harness: 'codex' | 'claude-code';
12
- /** null means the selected harness's own default; web blank fields send null. */
13
- model?: string | null;
14
- reasoningEffort?: string | null;
15
- session: 'acp';
10
+ brain: import('../config.js').AgentSelection;
11
+ role: import('../config.js').AgentSelection;
16
12
  cwd?: string;
17
13
  lifetime: 'permanent' | 'temporary';
18
- mission?: string;
19
14
  coordinator?: string;
20
15
  permissions: CommonPermissions;
21
- bio?: string;
22
- persona?: string;
23
16
  monitor?: WebCreationMonitor;
24
17
  openAfterCreate: boolean;
25
18
  highRiskAcknowledged?: boolean;
@@ -38,17 +31,6 @@ export type WebCreationMonitor = {
38
31
  export interface CreationCapabilities {
39
32
  available: boolean;
40
33
  reasons: string[];
41
- harnesses: Array<{
42
- id: 'codex' | 'claude-code';
43
- available: boolean;
44
- sessions: Array<'acp'>;
45
- defaultModel?: string;
46
- models: string[];
47
- modelOptions: HarnessModelOption[];
48
- catalogSource: string;
49
- customModelAllowed: true;
50
- warnings: string[];
51
- }>;
52
34
  lifetimes: Array<'permanent' | 'temporary'>;
53
35
  identityBootstrap: {
54
36
  mode: 'current-fleet-first-boot';
@@ -123,7 +105,6 @@ export interface RoleCreationServiceOptions {
123
105
  journalDir?: string;
124
106
  probeReady?: (name: string, session: 'acp') => Promise<'ready' | 'attention' | 'unknown'>;
125
107
  onProgress?: (action: CreationAction) => void;
126
- modelCatalogs?: Partial<Record<'codex' | 'claude-code', () => HarnessModelCatalog>>;
127
108
  /** Direct/managed callers must not create or restore the web action journal. */
128
109
  journal?: boolean;
129
110
  }