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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +97 -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 +13 -1
  5. package/dist/application/role-creation-service.d.ts +2 -21
  6. package/dist/application/role-creation-service.js +26 -64
  7. package/dist/application/role-removal-service.js +6 -3
  8. package/dist/application/task-room-service.d.ts +1 -1
  9. package/dist/application/task-room-service.js +154 -16
  10. package/dist/application/types.d.ts +1 -1
  11. package/dist/briefing.js +11 -7
  12. package/dist/build-info.json +4 -4
  13. package/dist/cli.js +136 -42
  14. package/dist/config-yaml.d.ts +1 -1
  15. package/dist/config.d.ts +65 -2
  16. package/dist/config.js +385 -26
  17. package/dist/creation.d.ts +1 -1
  18. package/dist/creation.js +2 -2
  19. package/dist/daemon-recovery.d.ts +93 -0
  20. package/dist/daemon-recovery.js +328 -0
  21. package/dist/docs.d.ts +1 -1
  22. package/dist/docs.js +84 -102
  23. package/dist/doctor.js +18 -4
  24. package/dist/fleet-command-audit.d.ts +160 -0
  25. package/dist/fleet-command-audit.js +433 -0
  26. package/dist/fleet-proxy.d.ts +2 -0
  27. package/dist/fleet-proxy.js +30 -8
  28. package/dist/generated-agent-source.d.ts +9 -0
  29. package/dist/generated-agent-source.js +53 -0
  30. package/dist/harness/agent-session.d.ts +18 -0
  31. package/dist/harness/claude-code-session.d.ts +6 -0
  32. package/dist/harness/claude-code-session.js +8 -0
  33. package/dist/harness/claude-code.js +12 -0
  34. package/dist/harness/codex-session.d.ts +6 -0
  35. package/dist/harness/codex-session.js +8 -0
  36. package/dist/harness/codex.js +20 -0
  37. package/dist/harness/registry.js +4 -0
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/init-guidance.d.ts +1 -0
  41. package/dist/init-guidance.js +6 -0
  42. package/dist/model-env.d.ts +2 -3
  43. package/dist/model-env.js +3 -5
  44. package/dist/ops.js +4 -2
  45. package/dist/owner-channel/channel.d.ts +56 -0
  46. package/dist/owner-channel/channel.js +269 -29
  47. package/dist/owner-channel/commands.d.ts +2 -0
  48. package/dist/owner-channel/commands.js +31 -13
  49. package/dist/owner-channel/state.d.ts +2 -0
  50. package/dist/owner-channel/state.js +8 -2
  51. package/dist/preset-bootstrap.d.ts +10 -0
  52. package/dist/preset-bootstrap.js +85 -0
  53. package/dist/reports/artifact.d.ts +10 -0
  54. package/dist/reports/artifact.js +32 -0
  55. package/dist/reports/index.d.ts +5 -0
  56. package/dist/reports/index.js +5 -0
  57. package/dist/reports/render.d.ts +6 -0
  58. package/dist/reports/render.js +240 -0
  59. package/dist/reports/service.d.ts +42 -0
  60. package/dist/reports/service.js +57 -0
  61. package/dist/reports/tasks.d.ts +52 -0
  62. package/dist/reports/tasks.js +177 -0
  63. package/dist/reports/types.d.ts +315 -0
  64. package/dist/reports/types.js +6 -0
  65. package/dist/resolved-plan.d.ts +3 -2
  66. package/dist/resolved-plan.js +21 -5
  67. package/dist/rooms-tasks/cli.js +225 -40
  68. package/dist/rooms-tasks/config.js +24 -13
  69. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  70. package/dist/rooms-tasks/external-worker.js +14 -9
  71. package/dist/rooms-tasks/provision.js +98 -36
  72. package/dist/rooms-tasks/templates.d.ts +0 -1
  73. package/dist/rooms-tasks/templates.js +9 -76
  74. package/dist/rooms-tasks/types.d.ts +10 -16
  75. package/dist/rooms-tasks/types.js +1 -4
  76. package/dist/runner.d.ts +9 -2
  77. package/dist/runner.js +84 -20
  78. package/dist/sensitive-config.d.ts +2 -0
  79. package/dist/sensitive-config.js +4 -0
  80. package/dist/session/acp.d.ts +5 -0
  81. package/dist/session/acp.js +27 -3
  82. package/dist/session/control.d.ts +14 -1
  83. package/dist/session/control.js +35 -0
  84. package/dist/spawn.d.ts +8 -33
  85. package/dist/spawn.js +52 -211
  86. package/dist/temp-lifecycle.d.ts +1 -1
  87. package/dist/watchdog/config.d.ts +10 -7
  88. package/dist/watchdog/config.js +29 -19
  89. package/dist/watchdog/run.js +4 -13
  90. package/dist/web/fleet-config-service.d.ts +11 -8
  91. package/dist/web/fleet-config-service.js +384 -183
  92. package/dist/web/topology-promote.js +25 -15
  93. package/dist/web/topology.js +2 -2
  94. package/dist/web/yaml-document-edit.js +2 -0
  95. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  96. package/dist/web-app/index.html +1 -1
  97. package/examples/fleet/agents/Alice.yaml +11 -0
  98. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  99. package/examples/fleet/brains/claude-default.yaml +5 -0
  100. package/examples/fleet/roles/coordinator.yaml +5 -0
  101. package/examples/fleet/roles/developer.yaml +3 -0
  102. package/examples/fleet.yaml +74 -0
  103. package/package.json +3 -1
  104. package/presets/fleet/agents/Agent.yaml +3 -0
  105. package/presets/fleet/agents/Architect.yaml +3 -0
  106. package/presets/fleet/agents/Critic.yaml +3 -0
  107. package/presets/fleet/agents/Developer.yaml +3 -0
  108. package/presets/fleet/agents/Secretary.yaml +3 -0
  109. package/presets/fleet/agents/Tester.yaml +3 -0
  110. package/presets/fleet/brains/claude-default.yaml +5 -0
  111. package/presets/fleet/roles/Agent.yaml +6 -0
  112. package/presets/fleet/roles/Architect.yaml +6 -0
  113. package/presets/fleet/roles/Critic.yaml +6 -0
  114. package/presets/fleet/roles/Developer.yaml +6 -0
  115. package/presets/fleet/roles/Secretary.yaml +6 -0
  116. package/presets/fleet/roles/Tester.yaml +6 -0
  117. package/presets/fleet/room_templates/pair.yaml +11 -0
  118. package/presets/fleet/room_templates/single.yaml +8 -0
  119. package/presets/fleet/room_templates/team.yaml +12 -0
  120. package/presets/fleet.yaml +7 -0
  121. package/presets/manifest.json +5 -0
  122. 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,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,31 @@ 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
+ Agent document (\`~/fleet/agents/Name.yaml\`):
360
+ role: { ref: RoleID }
361
+ brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
372
362
  identity: "Display Name" # ours identity to bind (default: Name)
373
363
  cwd: ${work_root}/repo # where the harness process runs
374
364
  coordinator: FleetCoordinator # announce target on boot
@@ -400,7 +390,7 @@ roles:
400
390
  bio: | # public card (published as bio)
401
391
  briefing_file: curated.md # replaces the generated narrative
402
392
  env: { KEY: value } # extra session env
403
- oversee: [{ role: X, interval: 5m }]
393
+ oversee: [{ agent: X, interval: 5m }]
404
394
  harness_options: # adapter-owned, adapter-validated
405
395
  plugins: { "name@marketplace": false } # claude-code: plugin overrides
406
396
  # mem_palace: false # claude-code: disable memory plugin
@@ -438,13 +428,12 @@ loops: # trusted local scheduled ACP turns
438
428
  If nothing material changed, complete silently without an owner report.
439
429
  ```
440
430
 
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.
431
+ The v2 loader reads the manifest, then sorted bare Role, Brain, and Agent
432
+ documents from its exact stem directory. Duplicate filename-derived IDs are a
433
+ hard error. Identities and Agents are decoupled—removing an Agent never deletes
434
+ an identity. Role, Brain, and operational ownership are composed explicitly;
435
+ there is no generic cross-kind merge. Manifest operational defaults such as
436
+ `monitor` and `permissions` merge only within their owning Agent fields.
448
437
 
449
438
  Every supervised role is a client of the operator-configured ours daemon. Fleet strips the
450
439
  obsolete, presence-sensitive `OURS_AUTOSTART` variable from ACP harness
@@ -453,6 +442,25 @@ installer/setup flows remain responsible for starting it.
453
442
 
454
443
  ### Rooms and tasks
455
444
 
445
+ Init installs editable `single`, `pair`, and `team` definitions under
446
+ `~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
447
+ reference. Inspect them with `ours-fleet template list` and
448
+ `ours-fleet template show team`. After configuring the authenticated owner below:
449
+
450
+ ```sh
451
+ ours-fleet task create --title "Solo task" --template single
452
+ ours-fleet task create --title "Reviewed change" --template pair
453
+ ours-fleet task create --title "Phased delivery" --template team
454
+ ```
455
+
456
+ `ours-fleet init -c /path/custom.yaml` seeds `/path/custom/` instead. Init reports
457
+ the packaged preset revision and source directory and only seeds missing files.
458
+ It never upgrades an edited preset. To adopt a newer packaged file explicitly,
459
+ copy the reported source file beside the existing target as `.new-default`, review
460
+ `diff -u`, then replace the target yourself. This is the sole adoption operation;
461
+ rerunning init is not an update. Users upgrading from hardcoded templates should
462
+ run init once (with the same `-c` selection they normally use).
463
+
456
464
  Rooms always use `ours-cowork`; there is no room-provider selector. Configure
457
465
  the cowork daemon connection and the room owner directly:
458
466
 
@@ -1037,33 +1045,27 @@ the structured CLI watcher, because a detached process cannot wake a Codex turn.
1037
1045
  wait is re-entered after each handled message; `ours-codex` instead wakes the idle
1038
1046
  session through its App Server integration.
1039
1047
 
1040
- The main Codex controls needed by fleet roles are available declaratively and when spawning:
1048
+ The main Codex controls are Brain-owned and also available when spawning:
1041
1049
 
1042
1050
  ```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
1051
+ # ~/fleet/brains/codex-review.yaml
1052
+ harness: codex
1053
+ session: acp
1054
+ model: gpt-5.4
1055
+ harness_options:
1056
+ launcher: auto
1057
+ profile: fleet
1058
+ sandbox: read-only
1059
+ approval: on-request
1060
+ monitor: true
1061
+ search: true
1062
+ add_dirs: [/data/shared]
1063
+ config: { model_reasoning_effort: high }
1061
1064
  ```
1062
1065
 
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
1066
+ One-off/permanent spawn selects a Brain that owns model, reasoning, native permission,
1067
+ sandbox, profile, launcher, search, monitor consent, native config, and additional roots.
1068
+ Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1067
1069
  role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
1068
1070
 
1069
1071
  ## 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
@@ -103,6 +105,16 @@ export class FleetQueryService {
103
105
  const lastExit = dir ? readExitRecord(join(dir, '.exit-status')) ?? undefined : undefined;
104
106
  const session = await this.session(role, dir, live.state);
105
107
  const problems = [...role.problems];
108
+ const recovery = dir ? readDaemonRecoveryStatus(dir) : undefined;
109
+ if (recovery && recovery.state !== 'recovered') {
110
+ const paths = ['agent', 'owner']
111
+ .filter(name => recovery.paths[name].state !== 'recovered')
112
+ .map(name => `${name}:${recovery.paths[name].state}`);
113
+ problems.push({
114
+ code: 'daemon_recovery', severity: 'warning', source: 'daemon-recovery',
115
+ detail: `${recovery.state}; ${paths.join(', ') || 'no degraded path'}; epoch ${recovery.epoch || 'unavailable'}`,
116
+ });
117
+ }
106
118
  if (live.state === 'running' && session.reachability !== 'online')
107
119
  problems.push({
108
120
  code: 'supervisor_session_disagreement', severity: 'warning',
@@ -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
  }