@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.20

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 (199) hide show
  1. package/README.md +169 -54
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.js +18 -6
  5. package/dist/application/fleet-query-service.js +20 -5
  6. package/dist/application/role-command-service.js +9 -0
  7. package/dist/application/role-creation-service.d.ts +6 -25
  8. package/dist/application/role-creation-service.js +34 -72
  9. package/dist/application/role-repository.d.ts +4 -2
  10. package/dist/application/role-repository.js +6 -3
  11. package/dist/application/session-control.d.ts +3 -1
  12. package/dist/application/session-control.js +4 -2
  13. package/dist/application/task-room-service.d.ts +58 -6
  14. package/dist/application/task-room-service.js +426 -78
  15. package/dist/briefing.js +74 -24
  16. package/dist/build-info.json +4 -4
  17. package/dist/canonical-json.d.ts +2 -0
  18. package/dist/canonical-json.js +10 -0
  19. package/dist/cli.js +227 -73
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +67 -2
  22. package/dist/config.js +260 -30
  23. package/dist/daemon-recovery.d.ts +93 -0
  24. package/dist/daemon-recovery.js +328 -0
  25. package/dist/docs.d.ts +1 -1
  26. package/dist/docs.js +171 -66
  27. package/dist/doctor.d.ts +2 -0
  28. package/dist/doctor.js +54 -4
  29. package/dist/fleet-command-audit.d.ts +165 -0
  30. package/dist/fleet-command-audit.js +543 -0
  31. package/dist/fleet-proxy.d.ts +7 -2
  32. package/dist/fleet-proxy.js +30 -8
  33. package/dist/harness/agent-session.d.ts +5 -0
  34. package/dist/harness/claude-code-session.d.ts +2 -0
  35. package/dist/harness/claude-code-session.js +6 -1
  36. package/dist/harness/claude-code.js +2 -0
  37. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  38. package/dist/harness/codex-app-server-proxy.js +348 -14
  39. package/dist/harness/codex-session.d.ts +11 -2
  40. package/dist/harness/codex-session.js +22 -2
  41. package/dist/harness/codex.d.ts +5 -1
  42. package/dist/harness/codex.js +110 -10
  43. package/dist/harness/types.d.ts +1 -1
  44. package/dist/index.d.ts +5 -4
  45. package/dist/index.js +4 -3
  46. package/dist/init-guidance.d.ts +1 -1
  47. package/dist/init-guidance.js +4 -3
  48. package/dist/init-wizard.d.ts +98 -0
  49. package/dist/init-wizard.js +629 -0
  50. package/dist/lifecycle-summary.d.ts +118 -0
  51. package/dist/lifecycle-summary.js +161 -0
  52. package/dist/loops/config.js +3 -2
  53. package/dist/model-env.d.ts +2 -2
  54. package/dist/model-env.js +1 -1
  55. package/dist/model-recovery.d.ts +1 -1
  56. package/dist/ops.js +2 -17
  57. package/dist/owner-channel/channel.d.ts +58 -0
  58. package/dist/owner-channel/channel.js +321 -42
  59. package/dist/owner-channel/commands.d.ts +6 -1
  60. package/dist/owner-channel/commands.js +34 -19
  61. package/dist/owner-channel/state.d.ts +2 -0
  62. package/dist/owner-channel/state.js +8 -2
  63. package/dist/permissions.js +1 -1
  64. package/dist/preset-bootstrap.d.ts +10 -0
  65. package/dist/preset-bootstrap.js +85 -0
  66. package/dist/preset-migration.d.ts +42 -0
  67. package/dist/preset-migration.js +414 -0
  68. package/dist/reports/artifact.d.ts +10 -0
  69. package/dist/reports/artifact.js +32 -0
  70. package/dist/reports/index.d.ts +5 -0
  71. package/dist/reports/index.js +5 -0
  72. package/dist/reports/render.d.ts +6 -0
  73. package/dist/reports/render.js +240 -0
  74. package/dist/reports/service.d.ts +42 -0
  75. package/dist/reports/service.js +57 -0
  76. package/dist/reports/tasks.d.ts +52 -0
  77. package/dist/reports/tasks.js +177 -0
  78. package/dist/reports/types.d.ts +315 -0
  79. package/dist/reports/types.js +6 -0
  80. package/dist/resolved-plan.js +4 -0
  81. package/dist/rooms-tasks/cli.d.ts +2 -0
  82. package/dist/rooms-tasks/cli.js +503 -75
  83. package/dist/rooms-tasks/close.d.ts +9 -1
  84. package/dist/rooms-tasks/close.js +20 -4
  85. package/dist/rooms-tasks/config.js +16 -14
  86. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  87. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  88. package/dist/rooms-tasks/deletion.d.ts +51 -0
  89. package/dist/rooms-tasks/deletion.js +217 -0
  90. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  91. package/dist/rooms-tasks/external-worker.js +14 -9
  92. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  93. package/dist/rooms-tasks/launch-snapshot.js +176 -0
  94. package/dist/rooms-tasks/member-overrides.d.ts +27 -0
  95. package/dist/rooms-tasks/member-overrides.js +170 -0
  96. package/dist/rooms-tasks/provision.js +179 -49
  97. package/dist/rooms-tasks/room-state.d.ts +1 -0
  98. package/dist/rooms-tasks/room-state.js +12 -1
  99. package/dist/rooms-tasks/task-state.d.ts +115 -3
  100. package/dist/rooms-tasks/task-state.js +361 -11
  101. package/dist/rooms-tasks/templates.d.ts +4 -2
  102. package/dist/rooms-tasks/templates.js +42 -78
  103. package/dist/rooms-tasks/terminal.d.ts +3 -0
  104. package/dist/rooms-tasks/terminal.js +7 -3
  105. package/dist/rooms-tasks/types.d.ts +94 -17
  106. package/dist/rooms-tasks/types.js +14 -4
  107. package/dist/runner.d.ts +9 -2
  108. package/dist/runner.js +107 -40
  109. package/dist/session/acp.d.ts +9 -1
  110. package/dist/session/acp.js +35 -9
  111. package/dist/session/arbiter.d.ts +2 -1
  112. package/dist/session/arbiter.js +2 -0
  113. package/dist/session/codex-app-server-transport.d.ts +53 -0
  114. package/dist/session/codex-app-server-transport.js +184 -0
  115. package/dist/session/codex-app-server.d.ts +122 -0
  116. package/dist/session/codex-app-server.js +1031 -0
  117. package/dist/session/control.d.ts +14 -1
  118. package/dist/session/control.js +38 -0
  119. package/dist/session/conversation-types.d.ts +4 -4
  120. package/dist/session/types.d.ts +24 -6
  121. package/dist/session/types.js +28 -0
  122. package/dist/spawn.d.ts +6 -31
  123. package/dist/spawn.js +57 -234
  124. package/dist/temp-lifecycle.d.ts +1 -1
  125. package/dist/watchdog/config.d.ts +10 -7
  126. package/dist/watchdog/config.js +29 -19
  127. package/dist/watchdog/run.js +4 -13
  128. package/dist/web/fleet-config-service.d.ts +1 -0
  129. package/dist/web/fleet-config-service.js +27 -8
  130. package/dist/web/runtime.js +11 -6
  131. package/dist/web/server.js +28 -1
  132. package/dist/web/topology-promote.js +8 -1
  133. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  134. package/dist/web-app/index.html +1 -1
  135. package/package.json +3 -2
  136. package/presets/brain-catalog.json +16 -0
  137. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  138. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  139. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  140. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  143. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  144. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  147. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  148. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  149. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  152. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  153. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  154. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  190. package/presets/fleet/roles/Coordinator.yaml +30 -0
  191. package/presets/fleet/roles/Critic.yaml +19 -0
  192. package/presets/fleet/roles/Developer.yaml +19 -0
  193. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  194. package/presets/fleet/room_templates/pair.yaml +10 -0
  195. package/presets/fleet/room_templates/single.yaml +7 -0
  196. package/presets/fleet/room_templates/team.yaml +12 -0
  197. package/presets/fleet.yaml +7 -0
  198. package/presets/manifest.json +6 -0
  199. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
package/README.md CHANGED
@@ -8,7 +8,7 @@ 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 structured ACP sessions** behind a shared Codex/Claude Code
11
+ - **run through structured managed sessions** behind one provider-neutral
12
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,
@@ -37,16 +37,16 @@ brain: { inline: { harness: claude-code, session: acp } }
37
37
 
38
38
  # ~/fleet/agents/Prototyper.yaml
39
39
  role: { inline: { mission: Build prototypes } }
40
- brain: { inline: { harness: codex, session: acp } }
40
+ brain: { inline: { harness: codex, session: codex-app-server } }
41
41
  ```
42
42
 
43
43
  ## How it works
44
44
 
45
45
  ```
46
- ~/fleet.yaml + ~/fleet/{agents,roles,brains}/*.yaml your declaration
46
+ ~/fleet.yaml + ~/fleet/{agents,agent_templates,roles,brains,room_templates}/*.yaml your declaration
47
47
  │ ours-fleet up
48
48
  ▼
49
- briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
49
+ briefing.md per role ──► agent session adapter ──► native app-server or ACP transport
50
50
  ▲ │
51
51
  systemd --user / launchd ───────┘ restart on crash, start at boot/login
52
52
  ```
@@ -72,7 +72,7 @@ The state dir contract:
72
72
  | `.owner-channel-message-recovery.json` | owner-channel bridge | mode-0600 body-free message claim journal: wire ID, history sequence, and claim time only |
73
73
  | `.owner-channel-attachment-recovery.json` | owner-channel bridge | mode-0600 attachment route journal; never filenames, paths, transcript text, or bytes |
74
74
  | `.owner-channel-binder.lock/`, `.owner-channel-binder.json` | owner-channel supervisor | mode-0600 role/identity + PID/start-marker ownership and release metadata; never mail plaintext or credentials |
75
- | `.session-events.jsonl`, `.control.sock`, `.control-token` | ACP backend | bounded typed console projection and private attachment control |
75
+ | `.session-events.jsonl`, `.control.sock`, `.control-token` | managed session backend | bounded typed console projection and private attachment control |
76
76
 
77
77
  ## Prerequisites
78
78
 
@@ -91,15 +91,16 @@ 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 # interactive reviewed defaults + host setup
95
95
  ours-fleet doctor # verifies everything above, with actionable messages
96
96
  ```
97
97
 
98
- The maintained Codex and Claude ACP adapters install as optional dependencies of
99
- `ours-fleet` and are resolved internally; users do not install adapter commands
100
- or add them to `PATH`. An explicit `session_options.acp.command` remains
101
- available for custom adapters. On Node 20–21, Codex ACP remains available,
102
- while maintained Claude ACP requires upgrading to Node 22.
98
+ Native Codex roles use the logged-in Codex CLI's `app-server` command directly.
99
+ The maintained Codex and Claude ACP adapters remain bundled optional dependencies
100
+ and are resolved internally; users do not install adapter commands or add them to
101
+ `PATH`. Explicit `session_options.codex_app_server.command` and
102
+ `session_options.acp.command` overrides remain available. On Node 20–21, native
103
+ Codex and Codex ACP remain available, while maintained Claude ACP requires Node 22.
103
104
 
104
105
  Each OS user manages their own fleet — to host roles under a sandboxed account,
105
106
  become that account and repeat.
@@ -107,8 +108,8 @@ become that account and repeat.
107
108
  ## Quickstart
108
109
 
109
110
  ```sh
110
- cp -R "$(npm root -g)/@ours.network/fleet/examples/fleet" ~/fleet
111
- cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
111
+ ours-fleet init # interactive; adds missing defaults and preserves existing files
112
+ ours-fleet config # validates Agents, Roles, Brains, and Room templates
112
113
  $EDITOR ~/fleet/agents/*.yaml # compose Role + Brain and operational settings
113
114
  ours-fleet up # boot the fleet (staggered)
114
115
  ours-fleet ls # running consoles
@@ -126,19 +127,16 @@ ours-fleet spawn Worker --mission "own the worker repo" \
126
127
  ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/reboot
127
128
 
128
129
  # Codex role: ours-codex is preferred automatically; plain codex is the fallback
129
- ours-fleet spawn Coder --harness codex --model gpt-5.4 \
130
- --session acp --approval ask --filesystem workspace \
131
- --profile fleet --search --monitor --coordinator FleetCoordinator
132
- # Note: --monitor is legacy consent for Codex's native monitor. Choose the
133
- # wake owner separately in fleet.yaml with monitor.mode: fleet|native.
130
+ ours-fleet spawn Coder --brain codex-fleet --role developer \
131
+ --approval ask --filesystem workspace --coordinator FleetCoordinator
134
132
  ```
135
133
 
136
- Inside a managed ACP role, `ours-fleet spawn` is transparently routed through
137
- that role's live supervisor. `ours-fleet spawn --role DeveloperX --temp` is the
138
- minimal form: omitted harness, session, cwd, coordinator, neutral permissions,
139
- fleet monitor policy, and same-harness model inherit from the caller. Explicit
140
- flags win; changing harness without a model lets the selected harness/fleet
141
- defaults choose one. After creation succeeds, fleet can deterministically notify
134
+ Inside a managed role, `ours-fleet spawn` is transparently routed through
135
+ that role's live supervisor. `ours-fleet spawn DeveloperX --temp` is the
136
+ minimal form: omitted Brain/Role selections, cwd, coordinator, neutral permissions,
137
+ and fleet monitor policy inherit from the caller. Identity, environment, owner routing,
138
+ room startup, and sensitive inline Brain values do not. Explicit flags win. After
139
+ creation succeeds, fleet can deterministically notify
142
140
  the caller's owner channel with the caller and spawned-role details. This is an
143
141
  honest-actor convenience and attribution path, not a security boundary; host
144
142
  shells and deliberately bypassed absolute binaries retain direct behavior.
@@ -305,7 +303,7 @@ ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
305
303
  ours-fleet config [-c FILE] validate + print merged plan
306
304
  ours-fleet ls | attach | peek | logs [-f] | status <Name>
307
305
  ours-fleet send <Name> "text"
308
- ours-fleet spawn [--temp] [<Name> | --role <Name>] [--harness --session --mission --model --approval ...]
306
+ ours-fleet spawn [--temp] [<Name> | --name <Name>] --brain <ID|inline:{...}> --role <ID|inline:{...}> [--approval ...]
309
307
  ours-fleet loops validate|list|status
310
308
  ours-fleet loops reload <Role>
311
309
  ours-fleet loops run-now|disable|enable <Role> <Loop>
@@ -359,13 +357,42 @@ defaults:
359
357
  max_kb: 1024 # rotate only above this active-log size
360
358
  keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
361
359
  max_archives: 12 # recent beside log; older preserved cold
362
- Agent document:
363
- Name: # filename stem; [A-Za-z0-9_-]+
364
- harness: claude-code
365
- session: acp # optional; ACP is the only supported session
366
- session_options:
367
- acp:
368
- command: claude-agent-acp # optional advanced override
360
+ ```
361
+
362
+ Agent Template document (\`~/fleet/agent_templates/Worker.yaml\`) is inert and reusable:
363
+
364
+ ```yaml
365
+ role: { ref: developer }
366
+ brain: { ref: claude-default }
367
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
368
+ ```
369
+
370
+ Persistent Agent instance (\`~/fleet/agents/Name.yaml\`) either remains a canonical
371
+ Role/Brain composition or explicitly reuses a template:
372
+
373
+ ```yaml
374
+ template: Worker
375
+ overrides:
376
+ cwd: /work/name
377
+ permissions: { approval: allow }
378
+ ```
379
+
380
+ Templates never create identities, supervisors, sessions, or lifecycle/status rows.
381
+ Map overrides merge recursively; scalars and arrays replace; null deletion is rejected.
382
+
383
+ Canonical Agent document (\`~/fleet/agents/Name.yaml\`):
384
+
385
+ ```yaml
386
+ role: { ref: RoleID }
387
+ brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
388
+ identity: "Display Name" # ours identity to bind (default: Name)
389
+ cwd: ${work_root}/repo # where the harness process runs
390
+ coordinator: FleetCoordinator # announce target on boot
391
+ ```
392
+
393
+ Additional operational field inventory (schematic, not one YAML document):
394
+
395
+ ```text
369
396
  identity: "Display Name" # ours identity to bind (default: Name)
370
397
  cwd: ${work_root}/repo # where the harness process runs
371
398
  coordinator: FleetCoordinator # announce target on boot
@@ -377,13 +404,13 @@ Agent document:
377
404
  - file_received # local_contact_request, pending_message)
378
405
  batch_ms: 2000 # coalesce a burst into one line (default 2000)
379
406
  inject: notification # notification (default) | full (bodies inline; roadmap)
380
- owner_channel: # optional trusted owner ingress; requires session: acp
407
+ owner_channel: # optional trusted owner ingress; requires a managed session
381
408
  identity: "Name Owner Channel" # dedicated identity; fleet creates it with safe local defaults
382
409
  owners: [owner-contact-cid] # authenticated ours contact IDs, never display names
383
410
  agent: managed-agent-cid # exact role CID allowed to relay messages/files outward
384
411
  interrupt: false # false queues; true cancels current work first
385
412
  progress_interval_ms: 30000 # fleet-generated progress notices; 0 disables
386
- comments: true # relay live "🟡 Live update:" ACP commentary (default true);
413
+ comments: true # relay live "🟡 Live update:" structured commentary (default true);
387
414
  # restart baseline for /comments on|off
388
415
  attachments: # secure inbound documents, images, and voice
389
416
  enabled: true
@@ -403,13 +430,10 @@ Agent document:
403
430
  # mem_palace: false # claude-code: disable memory plugin
404
431
  # permission_mode: dontAsk # claude-code: launch permission mode —
405
432
  # one of default | acceptEdits | plan | dontAsk | bypassPermissions
406
- # sandbox: workspace-write # codex: --sandbox —
407
- # one of read-only | workspace-write | danger-full-access
408
- # approval: never # codex: --ask-for-approval —
409
- # one of untrusted | on-request | never
410
- # permission_mode: never # codex alias for approval
433
+ # Codex ACP compatibility only: native codex-app-server rejects Codex
434
+ # permission overrides here; use the common permissions block above.
411
435
  # launcher: auto # codex: auto | ours-codex | codex
412
- # profile: fleet # codex: $CODEX_HOME/fleet.config.toml
436
+ # profile: fleet # codex ACP only: $CODEX_HOME/fleet.config.toml
413
437
  # search: true # codex: enable --search (live web search)
414
438
  # add_dirs: [/data/shared] # codex: repeat --add-dir
415
439
  # monitor: true # explicit persistent consent to arm mail wake
@@ -449,6 +473,62 @@ installer/setup flows remain responsible for starting it.
449
473
 
450
474
  ### Rooms and tasks
451
475
 
476
+ Init installs editable `single`, `pair`, and `team` definitions under
477
+ `~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
478
+ reference. Inspect them with `ours-fleet template list` and
479
+ `ours-fleet template show team`. After configuring the authenticated owner below:
480
+
481
+ ```sh
482
+ ours-fleet task create --title "Solo task" --template single
483
+ ours-fleet task create --title "Reviewed change" --template pair
484
+ ours-fleet task create --title "Phased delivery" --template team
485
+ ```
486
+
487
+ `ours-fleet init -c /path/custom.yaml` reviews the literal resolved manifest and
488
+ `/path/custom/` split directory before doing anything. It requires a TTY and two
489
+ default-No confirmations. You select Codex, Claude, or both; then either one explicit
490
+ model for every job or explicit development/review/coordination models; then one
491
+ Quick/Balanced/Thorough reasoning level (`low`/`medium`/`high`). Catalog entries are
492
+ supported IDs, not recommendations or entitlement claims: use `ours-fleet doctor` for
493
+ local Codex availability, while Claude entitlement is validated at launch. No
494
+ `model_chain` is generated and Fleet never silently substitutes another model.
495
+
496
+ A successful rerun adds any missing packaged defaults while preserving every existing
497
+ manifest, Role, Agent Template, and Room Template byte. Explicit adoption of newer defaults
498
+ is a separate reviewable migration. At either confirmation,
499
+ N, Enter, Escape, Ctrl-C, Ctrl-D, or EOF cancels. In a picker, Escape, Ctrl-C, Ctrl-D,
500
+ or EOF cancels; Enter records the highlighted choice (or continues a non-empty multi-select),
501
+ N is ignored, and an empty subscription selection remains blocked. Every cancellation
502
+ before the final Yes performs no host setup or config publication. Non-TTY use stops with
503
+ the same zero-mutation guarantee. After the final Yes, host integration runs before locked
504
+ publication; a hard kill, power loss, or host crash can therefore leave host integration
505
+ or private stage/recovery evidence to inspect.
506
+ Existing targets and their tree must be owner-private regular files/directories on the
507
+ configuration parent's filesystem; symlinks, foreign ownership, unsafe modes, and a
508
+ non-owner-controlled parent fail closed before host setup and are rechecked under the
509
+ per-setup init lock before publication.
510
+
511
+ The generated task experiences are fixed consequences, not extra questions: `single`
512
+ uses `Developer`; `pair` uses `Developer` with an independent `Critic`; and `team` uses
513
+ task-local `LocalCoordinator`, `Developer`, and `Critic`. The persistent
514
+ `FleetCoordinator` uses the separate packaged `Coordinator` contract and coordination
515
+ model.
516
+
517
+ Revision-3 role defaults have an explicit fail-closed adoption command: first run
518
+ `ours-fleet migrate-role-defaults -c FILE` for a zero-write plan, review every removal,
519
+ replacement, addition, preserved custom file, staging path, and recovery path, then rerun
520
+ with `--write`. Only exact semantic matches for packaged-bootstrap and generated
521
+ revision-3 forms are changed; same-named customized files remain byte-identical, and a
522
+ dangling custom reference refuses publication. Rerunning the migration is a no-op.
523
+
524
+ For manual adoption of a single newer packaged file, copy the reported source beside the
525
+ target as `.new-default`, inspect `diff -u TARGET TARGET.new-default`, then replace the
526
+ target yourself. Rerunning init is not an update. Users with the still older exact generated
527
+ six-worker starter Agent set must first run `ours-fleet migrate-agent-templates -c FILE`
528
+ for its zero-write plan, review every move/addition/recovery path, and rerun with `--write`.
529
+ Customized or ambiguous known starters are refused without mutation; unrelated custom
530
+ persistent Agents are preserved.
531
+
452
532
  Rooms always use `ours-cowork`; there is no room-provider selector. Configure
453
533
  the cowork daemon connection and the room owner directly:
454
534
 
@@ -477,6 +557,13 @@ Cowork role, and task. The agent creates that identity itself with ours MCP
477
557
  work immediately. Fleet activates the room from Cowork's authenticated seat; there
478
558
  is no briefing hash, startup ACK, or separate role-briefing readiness gate.
479
559
 
560
+ Set `room.anonymous: true` on a room template, or pass `--anonymous` to
561
+ `task create`, `task start`, `task work`, or `room create`, to create an
562
+ anonymous Cowork room. `--no-anonymous` explicitly overrides an anonymous
563
+ template. Fleet records the resolved value before room creation so retries keep
564
+ the same choice. Temporary members of an anonymous room are instructed to call
565
+ `create_temporary_identity` with `expose_local=false`.
566
+
480
567
  Human task and room results use the same compact Markdown presentation in the
481
568
  CLI and authenticated owner channel: a short heading, icon-plus-word status,
482
569
  code-formatted identifiers, bounded summaries, and actionable recovery or error
@@ -495,7 +582,7 @@ optional `rooms.owner.provider` setting is separate and defaults to
495
582
  ### Scheduled agent loops
496
583
 
497
584
  Top-level `loops` schedule literal prompts from trusted local YAML. Enabled targets
498
- must use `session: acp`; temporary roles never inherit loops, including `roles:
585
+ must use a managed session (`acp` or `codex-app-server`); temporary roles never inherit loops, including `roles:
499
586
  ["*"]`. Fleet rejects an enabled loop when its explicitly selected base config is
500
587
  a symlink, is owned by another user, or is group/world writable. Prompts are
501
588
  bounded and normalized at validation time, but only their size and SHA-256 appear
@@ -582,11 +669,13 @@ native settings actually grant, against a fixed floor:
582
669
  those requests with nobody to see it. With `unattended: wait` it **warns**,
583
670
  since a human can still attach a console and answer.
584
671
 
585
- **Security meaning.** `ask` maps to Codex `untrusted` / Claude `default`.
586
- `auto` selects Codex ACP `agent` (`on-request` + `workspace-write`) / Claude
587
- `acceptEdits`. `allow` selects Codex ACP's fully non-interactive yolo mode,
588
- reported by the adapter as `agent-full-access` (`never` +
589
- `danger-full-access`); Claude uses `bypassPermissions`.
672
+ **Security meaning.** For native Codex, Fleet translates `ask` → `untrusted`,
673
+ `auto` → `on-request`, and `allow` → `never`; users configure only the Fleet
674
+ names. `permissions.filesystem` independently maps `read-only` → `read-only`,
675
+ `workspace` → `workspace-write`, and `unrestricted` → `danger-full-access`.
676
+ Claude maps `ask` to `default`, `auto` to `acceptEdits`, and `allow` to
677
+ `bypassPermissions`. Codex ACP retains its adapter-specific coupled modes:
678
+ `auto` selects `agent`, while `allow` selects `agent-full-access`.
590
679
  `dontAsk` suppresses only the *prompt*, not the denial, which is why an
591
680
  `allow` role previously ran unable to do its job. Legacy `deny` is accepted
592
681
  only for compatibility and retains its conservative Codex `on-request` /
@@ -1038,23 +1127,49 @@ The main Codex controls are Brain-owned and also available when spawning:
1038
1127
  ```yaml
1039
1128
  # ~/fleet/brains/codex-review.yaml
1040
1129
  harness: codex
1041
- session: acp
1130
+ session: codex-app-server
1042
1131
  model: gpt-5.4
1132
+ permissions:
1133
+ approval: auto
1134
+ filesystem: read-only
1135
+ unattended: deny
1043
1136
  harness_options:
1044
1137
  launcher: auto
1045
- profile: fleet
1046
- sandbox: read-only
1047
- approval: on-request
1048
1138
  monitor: true
1049
1139
  search: true
1050
1140
  add_dirs: [/data/shared]
1051
1141
  config: { model_reasoning_effort: high }
1052
1142
  ```
1053
1143
 
1054
- Equivalent one-off/permanent spawn controls include `--model`, `--permission-mode`,
1055
- `--sandbox`, `--profile`, `--launcher`, `--search`, legacy `--monitor` (native
1056
- Codex monitor consent), repeatable
1057
- `--codex-config key=value`, and repeatable `--add-dir`. Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1144
+ `codex-app-server` is the opt-in direct Codex transport. It streams native items
1145
+ and message phases, maps Codex approval
1146
+ requests into Fleet permissions, supports turn steering/interrupt, and resumes
1147
+ the durable Codex thread through Fleet's shared `.session-id` lifecycle.
1148
+ Configure its authority only through the shared `permissions:` block;
1149
+ `on-request` and other Codex policy names are internal adapter values.
1150
+ Codex rejects `--profile` for `app-server`, so native roles also reject
1151
+ `harness_options.profile`; use the explicit config map only for non-authority
1152
+ settings. Native config currently allows only `model_reasoning_effort`; Codex ACP
1153
+ retains its legacy profile and config support.
1154
+ Packaged Codex brains and the init wizard continue to select `session: acp` for
1155
+ compatibility; Claude Code also uses ACP. Existing durable sessions are not assumed portable
1156
+ between unrelated providers; a failed fast resume is handled by Fleet's normal
1157
+ bounded fresh-session recovery.
1158
+
1159
+ To replace the default launcher for a native Codex role:
1160
+
1161
+ ```yaml
1162
+ session_options:
1163
+ codex_app_server:
1164
+ command: [codex, app-server]
1165
+ ```
1166
+
1167
+ The command override is exact: Fleet does not append `--profile`, `--search`, or
1168
+ `app-server`, so include every required argument in the command itself.
1169
+
1170
+ One-off/permanent spawn selects a Brain that owns model, reasoning, native permission,
1171
+ sandbox, profile, launcher, search, monitor consent, native config, and additional roots.
1172
+ Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1058
1173
  role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
1059
1174
 
1060
1175
  ## 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
+ }
@@ -1,7 +1,10 @@
1
+ import { sessionBackendCapabilities } from '../session/types.js';
1
2
  export function roleCapabilities(role, status, context = {}) {
2
- const backend = role.configuredBackend ?? (role.detectedBackend === 'acp' ? 'acp' : undefined);
3
+ const backend = role.configuredBackend
4
+ ?? (role.detectedBackend === 'acp' || role.detectedBackend === 'codex-app-server'
5
+ ? role.detectedBackend : undefined);
3
6
  const online = status.session.reachability === 'online';
4
- const acp = backend === 'acp';
7
+ const session = backend ? sessionBackendCapabilities(backend, role.config?.harness) : undefined;
5
8
  const protocolVersion = context.controlProtocolVersion ?? 1;
6
9
  const inactive = status.overall === 'offline'
7
10
  || (!online && status.supervisor.liveness === 'stopped');
@@ -10,11 +13,20 @@ export function roleCapabilities(role, status, context = {}) {
10
13
  inventory: true,
11
14
  status: true,
12
15
  output: {
13
- recent: acp, stream: acp && protocolVersion >= 2,
14
- structured: acp, replayCursor: acp && protocolVersion >= 2,
16
+ recent: Boolean(session?.streaming),
17
+ stream: Boolean(session?.streaming) && protocolVersion >= 2,
18
+ structured: Boolean(session?.durableConversation),
19
+ replayCursor: Boolean(session?.durableConversation) && protocolVersion >= 2,
20
+ },
21
+ input: {
22
+ text: online && Boolean(session?.promptInput),
23
+ interrupt: online && Boolean(session?.interrupt),
24
+ steering: online && Boolean(session?.steering),
25
+ },
26
+ permissions: {
27
+ observe: Boolean(session?.permissions),
28
+ respond: online && Boolean(session?.permissions),
15
29
  },
16
- input: { text: online && acp, interrupt: online && acp, steering: false },
17
- permissions: { observe: acp, respond: acp && online },
18
30
  lifecycle: {
19
31
  start: role.lifetime === 'permanent' && status.supervisor.liveness !== 'running',
20
32
  stop: role.lifetime === 'permanent' && !inactive && status.supervisor.liveness !== 'stopped',
@@ -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',
@@ -141,14 +154,16 @@ export class FleetQueryService {
141
154
  }
142
155
  async session(role, dir, supervisor) {
143
156
  const intended = role.configuredBackend;
144
- if (intended === 'acp' && dir) {
157
+ if (intended && dir) {
145
158
  try {
146
159
  const response = await this.control(dir, { command: 'snapshot' }, 2_000);
147
160
  if (!response.ok)
148
161
  throw new SessionControlError(response.kind ?? 'backend', response.error ?? 'snapshot failed');
149
162
  const snapshot = response.result;
163
+ if (snapshot.backend !== intended)
164
+ throw new SessionControlError('backend', `live session reports backend '${String(snapshot.backend)}', expected '${intended}'`);
150
165
  return {
151
- backend: 'acp', reachability: snapshot.alive ? 'online' : 'offline',
166
+ backend: intended, reachability: snapshot.alive ? 'online' : 'offline',
152
167
  readiness: snapshot.readiness, evidence: 'authoritative',
153
168
  sessionId: snapshot.sessionId, lastError: snapshot.lastError,
154
169
  pendingPermissionId: snapshot.pendingPermissionId,
@@ -162,7 +177,7 @@ export class FleetQueryService {
162
177
  const failure = error instanceof SessionControlError ? error.kind : 'backend';
163
178
  const offline = failure === 'offline';
164
179
  return {
165
- backend: 'acp',
180
+ backend: intended,
166
181
  reachability: offline ? 'offline'
167
182
  : failure === 'control-unavailable' ? 'unavailable' : 'unknown',
168
183
  readiness: offline ? 'failed' : 'unknown',
@@ -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}'`);