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