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

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 (100) hide show
  1. package/README.md +90 -112
  2. package/dist/application/capabilities.d.ts +0 -1
  3. package/dist/application/capabilities.js +3 -13
  4. package/dist/application/fleet-query-service.d.ts +0 -3
  5. package/dist/application/fleet-query-service.js +1 -24
  6. package/dist/application/role-creation-service.d.ts +5 -5
  7. package/dist/application/role-creation-service.js +14 -9
  8. package/dist/application/role-removal-service.js +6 -3
  9. package/dist/application/role-repository.d.ts +1 -2
  10. package/dist/application/role-repository.js +7 -7
  11. package/dist/application/session-control.d.ts +0 -12
  12. package/dist/application/session-control.js +0 -42
  13. package/dist/application/session-mutations.d.ts +5 -5
  14. package/dist/application/types.d.ts +1 -8
  15. package/dist/briefing.js +3 -6
  16. package/dist/build-info.json +5 -5
  17. package/dist/cli.js +17 -28
  18. package/dist/config.d.ts +29 -6
  19. package/dist/config.js +320 -40
  20. package/dist/creation.d.ts +1 -1
  21. package/dist/creation.js +2 -2
  22. package/dist/docs.d.ts +1 -1
  23. package/dist/docs.js +51 -95
  24. package/dist/doctor.js +1 -8
  25. package/dist/fleet-proxy.d.ts +1 -1
  26. package/dist/generated-agent-source.d.ts +9 -0
  27. package/dist/generated-agent-source.js +53 -0
  28. package/dist/harness/acp-session-transport.d.ts +8 -0
  29. package/dist/harness/acp-session-transport.js +3 -0
  30. package/dist/harness/agent-session.d.ts +46 -0
  31. package/dist/harness/agent-session.js +1 -0
  32. package/dist/harness/claude-code-session.d.ts +22 -0
  33. package/dist/harness/claude-code-session.js +32 -0
  34. package/dist/harness/claude-code.d.ts +2 -1
  35. package/dist/harness/claude-code.js +39 -81
  36. package/dist/harness/codex-session.d.ts +22 -0
  37. package/dist/harness/codex-session.js +32 -0
  38. package/dist/harness/codex.d.ts +2 -1
  39. package/dist/harness/codex.js +49 -45
  40. package/dist/harness/registry.js +4 -0
  41. package/dist/harness/types.d.ts +6 -38
  42. package/dist/index.d.ts +3 -5
  43. package/dist/index.js +1 -3
  44. package/dist/init-guidance.d.ts +1 -0
  45. package/dist/init-guidance.js +5 -0
  46. package/dist/isolation/resources.d.ts +3 -7
  47. package/dist/isolation/resources.js +3 -7
  48. package/dist/model-env.d.ts +2 -4
  49. package/dist/model-env.js +4 -7
  50. package/dist/monitor.d.ts +1 -24
  51. package/dist/monitor.js +5 -140
  52. package/dist/ops.js +4 -2
  53. package/dist/owner-channel/channel.d.ts +2 -2
  54. package/dist/resolved-plan.d.ts +3 -2
  55. package/dist/resolved-plan.js +21 -5
  56. package/dist/runner.d.ts +5 -15
  57. package/dist/runner.js +26 -102
  58. package/dist/sensitive-config.d.ts +2 -0
  59. package/dist/sensitive-config.js +4 -0
  60. package/dist/session/acp.d.ts +2 -2
  61. package/dist/session/acp.js +2 -3
  62. package/dist/session/activity.d.ts +2 -3
  63. package/dist/session/activity.js +2 -3
  64. package/dist/session/arbiter.d.ts +4 -4
  65. package/dist/session/control.d.ts +3 -3
  66. package/dist/session/types.d.ts +11 -5
  67. package/dist/spawn.d.ts +4 -4
  68. package/dist/spawn.js +60 -32
  69. package/dist/supervisor/none.d.ts +1 -1
  70. package/dist/supervisor/none.js +2 -2
  71. package/dist/tmux.d.ts +1 -14
  72. package/dist/tmux.js +1 -51
  73. package/dist/watchdog/config.js +6 -4
  74. package/dist/watchdog/run.js +8 -23
  75. package/dist/web/auth.d.ts +1 -1
  76. package/dist/web/fleet-config-service.d.ts +11 -8
  77. package/dist/web/fleet-config-service.js +383 -183
  78. package/dist/web/runtime.js +4 -15
  79. package/dist/web/server.d.ts +1 -3
  80. package/dist/web/server.js +3 -14
  81. package/dist/web/topology-promote.js +18 -18
  82. package/dist/web/topology.js +2 -2
  83. package/dist/web/yaml-document-edit.js +2 -0
  84. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  85. package/dist/web-app/assets/index-BbE9EX6n.js +10 -0
  86. package/dist/web-app/index.html +2 -2
  87. package/examples/fleet/agents/Alice.yaml +11 -0
  88. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  89. package/examples/fleet/brains/claude-default.yaml +5 -0
  90. package/examples/fleet/roles/coordinator.yaml +5 -0
  91. package/examples/fleet/roles/developer.yaml +3 -0
  92. package/examples/fleet.yaml +74 -0
  93. package/package.json +4 -8
  94. package/dist/session/tmux.d.ts +0 -28
  95. package/dist/session/tmux.js +0 -80
  96. package/dist/web/terminal/bridge.d.ts +0 -27
  97. package/dist/web/terminal/bridge.js +0 -317
  98. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  99. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  100. 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}/*.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` |
@@ -98,8 +98,8 @@ ours-fleet doctor # verifies everything above, with actionable messages
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 -R "$(npm root -g)/@ours.network/fleet/examples/fleet" ~/fleet
110
111
  cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
111
- $EDITOR ~/fleet.yaml # name your roles, missions, personas
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)
@@ -139,8 +140,8 @@ fleet monitor policy, and same-harness model inherit from the caller. Explicit
139
140
  flags win; changing harness without a model lets the selected harness/fleet
140
141
  defaults choose one. After creation succeeds, fleet can deterministically notify
141
142
  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.
143
+ honest-actor convenience and attribution path, not a security boundary; host
144
+ shells and deliberately bypassed absolute binaries retain direct behavior.
144
145
 
145
146
  ## Local web console
146
147
 
@@ -209,8 +210,8 @@ offline shell and no stale fleet state.
209
210
 
210
211
  The console provides evidence-separated inventory and status, ACP activity and
211
212
  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
213
+ actions, and transactional permanent/temporary creation. Identity is fixed to
214
+ the role name. Creation uses the authenticated
214
215
  daemon inventory and reports verified, missing, or unknown evidence. A missing
215
216
  permanent role identity is created deterministically before launch with local
216
217
  discovery and auto-accept enabled, then the provisioning lease is released so
@@ -243,41 +244,30 @@ never force-adopts or deletes identity state. Permanent roles are
243
244
  provisioned by fleet before launch and never delegate normal identity creation
244
245
  to the harness.
245
246
 
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.
247
+ The web console uses structured ACP activity rather than exposing a raw agent terminal.
249
248
 
250
249
  Security boundaries:
251
250
 
252
251
  - configured `Host` and `Origin`, CSRF, one-time WebSocket tickets, and explicit
253
252
  bind/origin policy are enforced server-side;
254
253
  - 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.
254
+ - normal logs are bounded and redacted;
255
+ - tests use temporary fleet homes and fake supervisors/identity providers—never
256
+ active role state.
257
+
258
+ Permanent spawns write a bare Agent document to
259
+ `~/fleet/agents/<Name>.yaml`; generated-source ownership is recorded so removal
260
+ never deletes an unproven hand-written neighbor. `ours-fleet rm <Name>` unspawns.
261
+
262
+ The web editor reads and writes the split document model explicitly as
263
+ `{manifest, agents}`. Role and Brain presets are visible through Agent refs but
264
+ remain read-only. Saves use one aggregate revision over the manifest and all
265
+ Agent/Role/Brain sources, validate an exact-stem private staging tree, show a
266
+ deterministic redacted diff per document, and hold one manifest-root lock.
267
+ Changed/deleted files are backed up together; any partial failure restores every
268
+ document or reports the private recovery directory. An unchanged save is a
269
+ byte-for-byte no-op and creates no backup. Nested environment, authentication,
270
+ invite, and isolation secrets never leave the host.
281
271
 
282
272
  From inside Claude Code, Codex, or Hermes with the core `ours` plugin installed,
283
273
  say **"spawn an ours agent …"**. The core skill checks for `ours-fleet`, installs
@@ -292,20 +282,19 @@ them and unsticks them:
292
282
 
293
283
  ```sh
294
284
  ours-fleet peek Worker # what is it doing?
295
- ours-fleet send Worker --key 1 # answer the menu it's stuck on
296
285
  ours-fleet send Worker "continue with the tests, then report"
297
286
  ```
298
287
 
299
- Declare standing assignments in `fleet.yaml` (rendered into the overseer's
288
+ Declare standing assignments in the overseer's bare Agent document (rendered into its
300
289
  briefing) — or just write "keep an eye on Alice and Bob every 5 minutes" in a
301
290
  persona; the bundled `oversee-agents` skill defines what that means operationally:
302
291
 
303
292
  ```yaml
304
- roles:
305
- FleetCoordinator:
306
- oversee:
307
- - { role: Alice, interval: 5m }
308
- - { role: Bob, interval: 5m }
293
+ role: { ref: coordinator }
294
+ brain: { ref: claude-default }
295
+ oversee:
296
+ - { agent: Alice, interval: 5m }
297
+ - { agent: Bob, interval: 5m }
309
298
  ```
310
299
 
311
300
  ## Command reference
@@ -315,7 +304,7 @@ ours-fleet docs | man AI-friendly complete reference
315
304
  ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
316
305
  ours-fleet config [-c FILE] validate + print merged plan
317
306
  ours-fleet ls | attach | peek | logs [-f] | status <Name>
318
- ours-fleet send <Name> "text" | --key <K>
307
+ ours-fleet send <Name> "text"
319
308
  ours-fleet spawn [--temp] [<Name> | --role <Name>] [--harness --session --mission --model --approval ...]
320
309
  ours-fleet loops validate|list|status
321
310
  ours-fleet loops reload <Role>
@@ -348,30 +337,32 @@ in its state dir) across supervisor-triggered restarts — systemd/launchd re-in
348
337
  agent process with no arguments, so without this the role would silently fall back to
349
338
  the default `~/fleet.yaml` on its very first crash-restart and fail to resolve.
350
339
 
351
- ## fleet.yaml reference
340
+ ## split configuration field reference
352
341
 
353
- ```yaml
342
+ The following is a schematic field inventory, not a single YAML document. Host
343
+ settings belong in `~/fleet.yaml`; reusable behavior and harness settings belong
344
+ in `~/fleet/roles/<id>.yaml` and `~/fleet/brains/<id>.yaml`; operational fields
345
+ belong in `~/fleet/agents/<id>.yaml`.
346
+
347
+ ```text
354
348
  vars: { work_root: /home/me/work } # ${var} substitution anywhere below
349
+ api_version: ours.network/fleet/v2
355
350
  start_stagger_ms: 0 # delay between agent LAUNCHES (host-wide, ms); 0 = no stagger
356
351
  defaults:
357
- harness: claude-code # for roles that don't set one
358
- session: tmux # tmux (default) | acp
359
352
  permissions: # common intent, translated by each harness/backend
360
353
  approval: ask # ask | auto | allow (`deny` is a deprecated alias)
361
354
  filesystem: workspace # read-only | workspace | unrestricted
362
355
  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
356
  monitor:
366
357
  mode: fleet # fleet (default) | native
367
358
  worklog: # built-ins shown; set false to opt out
368
359
  max_kb: 1024 # rotate only above this active-log size
369
360
  keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
370
361
  max_archives: 12 # recent beside log; older preserved cold
371
- roles:
372
- Name: # [A-Za-z0-9_-]+
362
+ Agent document:
363
+ Name: # filename stem; [A-Za-z0-9_-]+
373
364
  harness: claude-code
374
- session: acp # one flag selects ACP; omit for tmux
365
+ session: acp # optional; ACP is the only supported session
375
366
  session_options:
376
367
  acp:
377
368
  command: claude-agent-acp # optional advanced override
@@ -406,7 +397,7 @@ roles:
406
397
  bio: | # public card (published as bio)
407
398
  briefing_file: curated.md # replaces the generated narrative
408
399
  env: { KEY: value } # extra session env
409
- oversee: [{ role: X, interval: 5m }]
400
+ oversee: [{ agent: X, interval: 5m }]
410
401
  harness_options: # adapter-owned, adapter-validated
411
402
  plugins: { "name@marketplace": false } # claude-code: plugin overrides
412
403
  # mem_palace: false # claude-code: disable memory plugin
@@ -444,17 +435,15 @@ loops: # trusted local scheduled ACP turns
444
435
  If nothing material changed, complete silently without an owner report.
445
436
  ```
446
437
 
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.
438
+ The v2 loader reads the manifest, then sorted bare Role, Brain, and Agent
439
+ documents from its exact stem directory. Duplicate filename-derived IDs are a
440
+ hard error. Identities and Agents are decoupled—removing an Agent never deletes
441
+ an identity. Role, Brain, and operational ownership are composed explicitly;
442
+ there is no generic cross-kind merge. Manifest operational defaults such as
443
+ `monitor` and `permissions` merge only within their owning Agent fields.
455
444
 
456
445
  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
446
+ obsolete, presence-sensitive `OURS_AUTOSTART` variable from ACP harness
458
447
  processes. `ours-mcp proxy` is client-only and never starts a daemon; operators and explicit
459
448
  installer/setup flows remain responsible for starting it.
460
449
 
@@ -597,9 +586,7 @@ since a human can still attach a console and answer.
597
586
  `auto` selects Codex ACP `agent` (`on-request` + `workspace-write`) / Claude
598
587
  `acceptEdits`. `allow` selects Codex ACP's fully non-interactive yolo mode,
599
588
  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.
589
+ `danger-full-access`); Claude uses `bypassPermissions`.
603
590
  `dontAsk` suppresses only the *prompt*, not the denial, which is why an
604
591
  `allow` role previously ran unable to do its job. Legacy `deny` is accepted
605
592
  only for compatibility and retains its conservative Codex `on-request` /
@@ -689,8 +676,8 @@ their boots ~4 s apart instead of firing all seven at once.
689
676
  With `monitor.mode: fleet` (the default), the **ours-fleet supervisor** delivers
690
677
  a role's mail wakes: the per-role runner long-polls the ours daemon's notification API and
691
678
  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.
679
+ through the shared agent session. ACP uses live steering when its adapter supports it
680
+ and falls back to structured `session/prompt`.
694
681
  Set `monitor.interrupt: true` on roles where every configured wake should cancel
695
682
  the active turn before the notification is delivered. This is intentionally
696
683
  content-blind: the supervisor cannot inspect encrypted message bodies, so all
@@ -700,8 +687,7 @@ tool result or pending permission. Fleet waits for terminal ACP tool/update
700
687
  evidence, then steers the wake without calling `session.cancel`. If the tool is
701
688
  still active after 120 seconds, or the adapter cannot expose authenticated tool
702
689
  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.
690
+ delivery. Explicit human and control interrupts remain immediate.
705
691
  The default is `false`: a role that must begin a post-readiness mission
706
692
  immediately, including second-and-later mail received while it is working, must
707
693
  set `monitor.mode: fleet` and `monitor.interrupt: true` explicitly. Readiness and
@@ -794,8 +780,8 @@ invites, message bodies, credentials, or keys. A corrupt overlay fails closed
794
780
  (no effective owners and no mutation), and the last effective owner cannot be
795
781
  revoked.
796
782
 
797
- These commands require a running ACP role with `owner_channel` enabled. Missing,
798
- stopped, tmux, disabled, draining, and unavailable-MCP targets fail without
783
+ These commands require a running role with `owner_channel` enabled. Missing,
784
+ stopped, disabled, draining, and unavailable-MCP targets fail without
799
785
  starting a second client, binding an identity, or opening a network listener.
800
786
 
801
787
  The managed agent can use its ordinary ours `send_message` or `send_file` tool to
@@ -1015,11 +1001,8 @@ gets a durable pre-send marker: a failed/ambiguous send becomes `uncertain` and
1015
1001
  not blindly retried, while files not yet attempted remain recoverable after restart.
1016
1002
  Logs contain counts and byte totals, never filenames or raw bytes.
1017
1003
 
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.
1004
+ Owner channels use the structured agent-session interface for turn-correlated
1005
+ assistant output and reliable automatic replies.
1023
1006
 
1024
1007
  ## Codex roles
1025
1008
 
@@ -1050,27 +1033,22 @@ the structured CLI watcher, because a detached process cannot wake a Codex turn.
1050
1033
  wait is re-entered after each handled message; `ours-codex` instead wakes the idle
1051
1034
  session through its App Server integration.
1052
1035
 
1053
- The main Codex controls needed by fleet roles are available declaratively and when spawning:
1036
+ The main Codex controls are Brain-owned and also available when spawning:
1054
1037
 
1055
1038
  ```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
1039
+ # ~/fleet/brains/codex-review.yaml
1040
+ harness: codex
1041
+ session: acp
1042
+ model: gpt-5.4
1043
+ harness_options:
1044
+ launcher: auto
1045
+ profile: fleet
1046
+ sandbox: read-only
1047
+ approval: on-request
1048
+ monitor: true
1049
+ search: true
1050
+ add_dirs: [/data/shared]
1051
+ config: { model_reasoning_effort: high }
1074
1052
  ```
1075
1053
 
1076
1054
  Equivalent one-off/permanent spawn controls include `--model`, `--permission-mode`,
@@ -1083,7 +1061,7 @@ role-specific ours daemon, or configure the host default in `~/.ours/config.json
1083
1061
 
1084
1062
  Each role can be sandboxed at the environment level via an `isolation:` block —
1085
1063
  **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)
1064
+ agent process is wrapped in [bubblewrap](https://github.com/containers/bubblewrap)
1087
1065
  (rootless, no setuid), resource-limited by `systemd-run --user --scope`.
1088
1066
 
1089
1067
  An empty `isolation: {}` gives a sensible default posture: filesystem-confined to
@@ -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;
@@ -1,30 +1,20 @@
1
1
  export function roleCapabilities(role, status, context = {}) {
2
- const backend = role.configuredBackend ?? (role.detectedBackend === 'acp' || role.detectedBackend === 'tmux' ? role.detectedBackend : undefined);
2
+ const backend = role.configuredBackend ?? (role.detectedBackend === 'acp' ? 'acp' : undefined);
3
3
  const online = status.session.reachability === 'online';
4
4
  const acp = backend === 'acp';
5
- const tmux = backend === 'tmux';
6
5
  const protocolVersion = context.controlProtocolVersion ?? 1;
7
6
  const inactive = status.overall === 'offline'
8
7
  || (!online && status.supervisor.liveness === 'stopped');
9
- const terminalAvailable = tmux && online && context.terminalPtyAvailable === true;
10
- const terminalReason = terminalAvailable ? undefined
11
- : !tmux ? 'wrong_backend'
12
- : !online ? 'offline'
13
- : 'pty_unavailable';
14
8
  return {
15
9
  protocolVersion,
16
10
  inventory: true,
17
11
  status: true,
18
12
  output: {
19
- recent: acp || tmux, stream: (acp && protocolVersion >= 2) || terminalAvailable,
13
+ recent: acp, stream: acp && protocolVersion >= 2,
20
14
  structured: acp, replayCursor: acp && protocolVersion >= 2,
21
15
  },
22
- input: { text: online && (acp || tmux), rawKeys: terminalAvailable, interrupt: online && acp, steering: false },
16
+ input: { text: online && acp, interrupt: online && acp, steering: false },
23
17
  permissions: { observe: acp, respond: acp && online },
24
- terminal: {
25
- available: terminalAvailable, reason: terminalReason,
26
- multiViewer: terminalAvailable, writerLease: terminalAvailable,
27
- },
28
18
  lifecycle: {
29
19
  start: role.lifetime === 'permanent' && status.supervisor.liveness !== 'running',
30
20
  stop: role.lifetime === 'permanent' && !inactive && status.supervisor.liveness !== 'stopped',
@@ -1,13 +1,11 @@
1
1
  import type { SupervisorBackend } from '../supervisor/types.js';
2
2
  import { controlRequest } from '../session/control.js';
3
- import { Tmux } from '../tmux.js';
4
3
  import { type CapabilityContext } from './capabilities.js';
5
4
  import { RoleRepository } from './role-repository.js';
6
5
  import type { RoleCapabilities, RoleRecord, RoleStatus } from './types.js';
7
6
  export interface FleetQueryOptions {
8
7
  repository: RoleRepository;
9
8
  supervisor: SupervisorBackend;
10
- tmux?: Tmux;
11
9
  control?: typeof controlRequest;
12
10
  capabilityContext?: CapabilityContext;
13
11
  /**
@@ -24,7 +22,6 @@ export interface FleetQueryOptions {
24
22
  }
25
23
  export declare class FleetQueryService {
26
24
  private readonly options;
27
- private readonly tmux;
28
25
  private readonly control;
29
26
  constructor(options: FleetQueryOptions);
30
27
  list(): Promise<Array<{
@@ -4,7 +4,6 @@ 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 { Tmux } from '../tmux.js';
8
7
  import { roleCapabilities } from './capabilities.js';
9
8
  import { FleetError } from './errors.js';
10
9
  const clean = (value, max = 512) => value.replace(/[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').trim().slice(0, max);
@@ -50,7 +49,7 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
50
49
  || problems.some(problem => problem.severity === 'error' || problem.source === 'watchdog')
51
50
  || session.readiness === 'failed')
52
51
  return 'attention';
53
- // A reachable ACP/tmux session is the user's live interaction surface. Its
52
+ // A reachable agent session is the user's live interaction surface. Its
54
53
  // evidence remains authoritative even when a service manager no longer owns
55
54
  // the process (for example, a detached ACP session with an inactive unit).
56
55
  if (session.reachability === 'online'
@@ -69,11 +68,9 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
69
68
  }
70
69
  export class FleetQueryService {
71
70
  options;
72
- tmux;
73
71
  control;
74
72
  constructor(options) {
75
73
  this.options = options;
76
- this.tmux = options.tmux ?? new Tmux();
77
74
  this.control = options.control ?? controlRequest;
78
75
  }
79
76
  async list() {
@@ -174,26 +171,6 @@ export class FleetQueryService {
174
171
  };
175
172
  }
176
173
  }
177
- if (intended === 'tmux' || role.detectedBackend === 'tmux') {
178
- try {
179
- const has = await this.tmux.has(role.id);
180
- return {
181
- backend: 'tmux', reachability: has ? 'online' : supervisor === 'stopped' ? 'offline' : 'unknown',
182
- readiness: has ? 'idle' : supervisor === 'running' ? 'starting' : 'failed',
183
- evidence: 'inferred',
184
- // tmux exposes no agent-side evidence at all, and `readiness: idle`
185
- // here is a pane-liveness inference, not an activity claim.
186
- activity: { state: 'unobservable' },
187
- };
188
- }
189
- catch (error) {
190
- return {
191
- backend: 'tmux', reachability: 'unknown', readiness: 'unknown',
192
- evidence: 'inferred', lastError: clean(error.message),
193
- activity: { state: 'unobservable' },
194
- };
195
- }
196
- }
197
174
  return {
198
175
  backend: 'unknown', reachability: supervisor === 'stopped' ? 'offline' : 'unknown',
199
176
  readiness: supervisor === 'stopped' ? 'failed' : 'unknown', evidence: 'inferred',
@@ -12,7 +12,7 @@ export interface CreateRoleSessionRequest {
12
12
  /** null means the selected harness's own default; web blank fields send null. */
13
13
  model?: string | null;
14
14
  reasoningEffort?: string | null;
15
- session: 'acp' | 'tmux';
15
+ session: 'acp';
16
16
  cwd?: string;
17
17
  lifetime: 'permanent' | 'temporary';
18
18
  mission?: string;
@@ -41,7 +41,7 @@ export interface CreationCapabilities {
41
41
  harnesses: Array<{
42
42
  id: 'codex' | 'claude-code';
43
43
  available: boolean;
44
- sessions: Array<'acp' | 'tmux'>;
44
+ sessions: Array<'acp'>;
45
45
  defaultModel?: string;
46
46
  models: string[];
47
47
  modelOptions: HarnessModelOption[];
@@ -71,7 +71,7 @@ export interface CreationPreview {
71
71
  name: string;
72
72
  identity: string;
73
73
  harness: string;
74
- session: 'acp' | 'tmux';
74
+ session: 'acp';
75
75
  model?: string;
76
76
  reasoningEffort?: string;
77
77
  cwd?: string;
@@ -95,7 +95,7 @@ export interface CreationAction {
95
95
  actionId: string;
96
96
  requestHash: string;
97
97
  roleId: string;
98
- session: 'acp' | 'tmux';
98
+ session: 'acp';
99
99
  lifetime: 'permanent' | 'temporary';
100
100
  state: CreationStage;
101
101
  stages: Array<{
@@ -121,7 +121,7 @@ export interface RoleCreationServiceOptions {
121
121
  tempLauncher?: SupervisorLauncher;
122
122
  allowedCwdRoots?: string[];
123
123
  journalDir?: string;
124
- probeReady?: (name: string, session: 'acp' | 'tmux') => Promise<'ready' | 'attention' | 'unknown'>;
124
+ probeReady?: (name: string, session: 'acp') => Promise<'ready' | 'attention' | 'unknown'>;
125
125
  onProgress?: (action: CreationAction) => void;
126
126
  modelCatalogs?: Partial<Record<'codex' | 'claude-code', () => HarnessModelCatalog>>;
127
127
  /** Direct/managed callers must not create or restore the web action journal. */
@@ -10,6 +10,7 @@ import { FleetError, normalizeError } from './errors.js';
10
10
  import { inheritCallerSpawnDefaults } from '../fleet-proxy.js';
11
11
  import { effectivePermissionMode } from '../permissions.js';
12
12
  import { effectiveRoleModel } from '../model-env.js';
13
+ import { getAdapter } from '../harness/registry.js';
13
14
  import { claudeModelCatalog, codexModelCatalog, } from './model-catalog.js';
14
15
  const canonical = (value) => {
15
16
  if (Array.isArray(value))
@@ -109,13 +110,13 @@ export class RoleCreationService {
109
110
  reasons,
110
111
  harnesses: [
111
112
  {
112
- id: 'codex', available: true, sessions: ['acp', 'tmux'],
113
+ id: 'codex', available: true, sessions: ['acp'],
113
114
  defaultModel: resolveRoleModel(undefined, 'codex', defaults),
114
115
  ...codexModels, catalogSource: 'codex-runtime-catalog', customModelAllowed: true,
115
116
  warnings: codexCatalog.warnings,
116
117
  },
117
118
  {
118
- id: 'claude-code', available: true, sessions: ['acp', 'tmux'],
119
+ id: 'claude-code', available: true, sessions: ['acp'],
119
120
  defaultModel: resolveRoleModel(undefined, 'claude-code', defaults),
120
121
  ...claudeModels, catalogSource: 'claude-adapter-2.1', customModelAllowed: true,
121
122
  warnings: claudeCatalog.warnings,
@@ -149,7 +150,7 @@ export class RoleCreationService {
149
150
  const effective = {
150
151
  name: request.name, identity: request.name,
151
152
  harness: request.harness ?? defaults.harness ?? 'claude-code',
152
- session: request.session ?? defaults.session ?? 'tmux',
153
+ session: request.session ?? defaults.session ?? 'acp',
153
154
  model: resolveRoleModel(request.model, request.harness, defaults),
154
155
  reasoningEffort: request.reasoningEffort ?? undefined,
155
156
  cwd, lifetime: request.lifetime,
@@ -264,9 +265,7 @@ export class RoleCreationService {
264
265
  this.stage(action, 'waiting_for_session');
265
266
  const ready = await this.waitForReady(action.roleId, action.session);
266
267
  if (ready === 'ready') {
267
- action.openPath = action.session === 'acp'
268
- ? `/roles/${encodeURIComponent(action.roleId)}/activity`
269
- : `/roles/${encodeURIComponent(action.roleId)}/terminal`;
268
+ action.openPath = `/roles/${encodeURIComponent(action.roleId)}/activity`;
270
269
  this.stage(action, 'session_reachable', 'session is reachable; identity binding has no structured evidence in this version');
271
270
  }
272
271
  else if (ready === 'attention') {
@@ -311,14 +310,20 @@ export class RoleCreationService {
311
310
  throw new FleetError('invalid_request', 'invalid role name');
312
311
  if (!['codex', 'claude-code'].includes(input.harness))
313
312
  throw new FleetError('invalid_request', 'unsupported harness');
314
- if (!['acp', 'tmux'].includes(input.session))
313
+ if (input.session !== 'acp')
315
314
  throw new FleetError('invalid_request', 'unsupported session backend');
316
315
  if (!['permanent', 'temporary'].includes(input.lifetime))
317
316
  throw new FleetError('invalid_request', 'unsupported lifetime');
318
317
  bounded(input.model ?? undefined, 'model', 128);
319
318
  bounded(input.reasoningEffort ?? undefined, 'reasoning effort', 16);
320
- if (input.reasoningEffort != null && !['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(input.reasoningEffort))
321
- throw new FleetError('invalid_request', 'unsupported reasoning effort');
319
+ if (input.reasoningEffort != null) {
320
+ try {
321
+ getAdapter(input.harness).agentSession.resolveBrain({ effort: input.reasoningEffort });
322
+ }
323
+ catch (error) {
324
+ throw new FleetError('invalid_request', error.message);
325
+ }
326
+ }
322
327
  bounded(input.mission, 'mission', 4_096);
323
328
  bounded(input.coordinator, 'coordinator', 128);
324
329
  bounded(input.bio, 'bio', 8_192);