@ours.network/fleet 0.10.0-nightly.3 → 0.10.0

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 (53) hide show
  1. package/README.md +138 -21
  2. package/dist/atomic-file.d.ts +30 -0
  3. package/dist/atomic-file.js +86 -0
  4. package/dist/briefing.d.ts +6 -0
  5. package/dist/briefing.js +43 -13
  6. package/dist/cli.js +98 -22
  7. package/dist/config.d.ts +24 -3
  8. package/dist/config.js +84 -11
  9. package/dist/creation.d.ts +179 -0
  10. package/dist/creation.js +254 -0
  11. package/dist/docs.d.ts +28 -1
  12. package/dist/docs.js +155 -8
  13. package/dist/doctor.js +75 -17
  14. package/dist/harness/claude-code.d.ts +39 -3
  15. package/dist/harness/claude-code.js +128 -26
  16. package/dist/harness/codex.d.ts +7 -1
  17. package/dist/harness/codex.js +58 -11
  18. package/dist/harness/registry.d.ts +2 -0
  19. package/dist/harness/registry.js +19 -0
  20. package/dist/harness/types.d.ts +51 -4
  21. package/dist/isolation/bubblewrap.js +7 -1
  22. package/dist/isolation/policy.d.ts +34 -5
  23. package/dist/isolation/policy.js +114 -7
  24. package/dist/isolation/resources.d.ts +6 -3
  25. package/dist/isolation/resources.js +6 -3
  26. package/dist/isolation/types.d.ts +19 -1
  27. package/dist/monitor.d.ts +33 -4
  28. package/dist/monitor.js +150 -32
  29. package/dist/ops.d.ts +15 -2
  30. package/dist/ops.js +32 -9
  31. package/dist/permissions.d.ts +70 -0
  32. package/dist/permissions.js +97 -0
  33. package/dist/runner.d.ts +65 -2
  34. package/dist/runner.js +262 -27
  35. package/dist/session/acp.d.ts +25 -2
  36. package/dist/session/acp.js +143 -26
  37. package/dist/session/control.d.ts +49 -1
  38. package/dist/session/control.js +116 -12
  39. package/dist/session/tmux.d.ts +9 -2
  40. package/dist/session/tmux.js +36 -4
  41. package/dist/session/types.d.ts +99 -2
  42. package/dist/session/types.js +42 -1
  43. package/dist/spawn.d.ts +27 -2
  44. package/dist/spawn.js +153 -15
  45. package/dist/supervisor/launchd.d.ts +50 -0
  46. package/dist/supervisor/launchd.js +121 -4
  47. package/dist/supervisor/none.js +22 -4
  48. package/dist/supervisor/systemd.d.ts +8 -1
  49. package/dist/supervisor/systemd.js +94 -4
  50. package/dist/supervisor/types.d.ts +36 -3
  51. package/dist/tmux.d.ts +34 -2
  52. package/dist/tmux.js +48 -11
  53. package/package.json +1 -1
package/README.md CHANGED
@@ -54,8 +54,8 @@ Each role gets a state dir (`~/.ours-fleet/agents/<Name>/`) holding its briefing
54
54
  logs, routines, and session markers. On boot the agent reads its briefing: bind
55
55
  identity, publish bio/persona, announce to its coordinator, work — while the
56
56
  supervisor delivers its mail wakes as `[fleet-monitor]` console lines (see
57
- [Mail monitor](#mail-monitor); `monitor.enabled: false` reverts to the agent
58
- arming its own `ours-mcp watch`). On crash the supervisor relaunches it and the
57
+ [Mail monitor](#mail-monitor)). Set `monitor.mode: native` when the harness
58
+ should own mail wake instead. On crash the supervisor relaunches it and the
59
59
  harness resumes the same session.
60
60
 
61
61
  The state dir contract:
@@ -124,6 +124,8 @@ ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/rebo
124
124
  ours-fleet spawn Coder --harness codex --model gpt-5.4 \
125
125
  --session acp --approval ask --filesystem workspace \
126
126
  --profile fleet --search --monitor --coordinator FleetCoordinator
127
+ # Note: --monitor is legacy consent for Codex's native monitor. Choose the
128
+ # wake owner separately in fleet.yaml with monitor.mode: fleet|native.
127
129
  ```
128
130
 
129
131
  Permanent spawns are written to `~/fleet.d/<Name>.yaml` — your hand-written
@@ -191,8 +193,8 @@ defaults:
191
193
  unattended: deny # deny | wait
192
194
  model: claude-fable-5 # default model for roles that don't set one (per-role model / --model wins)
193
195
  max_tokens: 500000 # session cap (harness-interpreted)
194
- monitor: # supervisor-owned mail wake (fleet-wide default)
195
- enabled: true # default true; a role block overrides key-by-key
196
+ monitor:
197
+ mode: fleet # fleet (default) | native
196
198
  roles:
197
199
  Name: # [A-Za-z0-9_-]+
198
200
  harness: claude-code
@@ -203,9 +205,9 @@ roles:
203
205
  identity: "Display Name" # ours identity to bind (default: Name)
204
206
  cwd: ${work_root}/repo # where the harness process runs
205
207
  coordinator: FleetCoordinator # announce target on boot
206
- monitor: # deterministic wake, owned by the supervisor
207
- enabled: true # default (defaults.monitor.enabled ?? true);
208
- # # false = legacy in-session `ours-mcp watch`
208
+ monitor:
209
+ mode: fleet # fleet = ours-fleet supervisor; native = harness monitor
210
+ interrupt: false # true cancels active work before every configured wake
209
211
  wake_sources: # which daemon events wake the console (default:
210
212
  - message_received # message_received, file_received,
211
213
  - file_received # local_contact_request, pending_message)
@@ -253,6 +255,107 @@ role's `harness_options`, so a fleet can set common Codex permission/profile def
253
255
  and override individual keys per role. `monitor` merges the same way — a role block
254
256
  overrides `defaults.monitor` key-by-key.
255
257
 
258
+ ### Never-prompt failure
259
+
260
+ An unattended role has no console, so a permission request has nobody to answer
261
+ it and is refused **inside the harness** — no prompt, no error, no log line. The
262
+ agent does less than its briefing told it to, reports success, and nothing
263
+ distinguishes that from having done the work. It is caused by a permission mode
264
+ that suppresses the prompt without granting the action (Claude `dontAsk`), or by
265
+ `unattended: deny`.
266
+
267
+ Automatic decisions are now recorded rather than invisible. Every permission
268
+ decided without a human is written to
269
+ `~/.ours-fleet/agents/<Name>/.session-events.jsonl` with the decision, whether
270
+ policy or a person made it, which policy produced it, the reason, and the option
271
+ chosen — and `ours-fleet peek`/`attach` render them. Automatic denial always
272
+ asks for a one-shot rejection, never a standing one, so one unattended refusal
273
+ cannot disable a tool for the rest of the session. A role that can auto-deny
274
+ says so once at startup.
275
+
276
+ To catch this **before** a role runs, see the capability floor below —
277
+ `ours-fleet doctor` fails an under-permissioned unattended role rather than
278
+ letting it discover the problem silently.
279
+
280
+ ### The unattended capability floor
281
+
282
+ A fleet role runs with no console attached, so a permission request has nobody
283
+ to answer it and is refused inside the harness — silently. The agent then does
284
+ less than its briefing told it to and reports no error at all.
285
+
286
+ `ours-fleet config` and `ours-fleet doctor` therefore resolve each role's
287
+ neutral `permissions:` through its harness adapter and check what the resulting
288
+ native settings actually grant, against a fixed floor:
289
+
290
+ | capability | what the role must be able to do |
291
+ | --- | --- |
292
+ | `read-state` | read its briefing, `ROUTINES.md`, and `WORKLOG.md` |
293
+ | `write-state` | append its `WORKLOG.md` and its own state files |
294
+ | `messaging` | bind its identity, send and receive ours mail |
295
+ | `monitor` | arm and observe its mail monitor |
296
+ | `workspace-edit` | edit and test files in its working directory |
297
+ | `status-commands` | run the inspection commands its briefing prescribes |
298
+
299
+ `doctor` reports this per role as `unattended floor: <Role>`. A role configured
300
+ `unattended: deny` that cannot meet the floor **fails** doctor — it would deny
301
+ those requests with nobody to see it. With `unattended: wait` it **warns**,
302
+ since a human can still attach a console and answer.
303
+
304
+ **Security meaning.** `approval: allow` maps to Claude's `bypassPermissions`,
305
+ the mode that actually permits the actions the role was authorized to take.
306
+ `dontAsk` suppresses only the *prompt*, not the denial, which is why an
307
+ `allow` role previously ran unable to do its job. Nothing but an explicit
308
+ `allow` is elevated: `ask` keeps Claude's default mode and `deny` maps to
309
+ `plan`. `allow` is a real grant — give it deliberately, and keep per-role
310
+ [`isolation:`](#agent-isolation) as the outer boundary, which no permission
311
+ mode can cross.
312
+
313
+ ### Isolation at creation time
314
+
315
+ ```sh
316
+ ours-fleet spawn Sec --isolation-file policy.yaml
317
+ ours-fleet spawn --temp Scout --isolation-file policy.yaml
318
+ ```
319
+
320
+ `--isolation-file` supplies the role's sandbox policy when it is created, so its
321
+ **first** launch is already confined. Without it a role gains `isolation:` only when
322
+ you edit `fleet.yaml` and run `up`, and everything before that ran unsandboxed.
323
+
324
+ The file contains exactly the [`isolation:` mapping](#agent-isolation) — the same schema,
325
+ validated by the same code, so it cannot mean something different from the identical block
326
+ in `fleet.yaml`:
327
+
328
+ ```yaml
329
+ network: deny
330
+ fs:
331
+ read: [/opt/reference]
332
+ resources:
333
+ mem: 2G
334
+ ```
335
+
336
+ An invalid file is rejected before anything is created — no config, no state directory, no
337
+ identity reservation.
338
+
339
+ ### Sandboxed credentials and configuration
340
+
341
+ A sandboxed role gets a **per-role writable harness home** under its own state directory
342
+ (`<state>/harness/<harness>/`), so its sessions, history and caches are its own and are
343
+ invisible to every other role. The **shared** credentials, global instructions and
344
+ configuration are layered back read-only:
345
+
346
+ | Harness | Read-only (shared) | Per-role writable |
347
+ | --- | --- | --- |
348
+ | `claude-code` | `~/.claude.json`, `~/.claude/CLAUDE.md`, `settings.json`, `plugins/` | everything else under `~/.claude` |
349
+ | `codex` | `~/.codex/auth.json`, `config.toml`, `AGENTS.md`, `plugins/`, `~/.agents` | everything else under `~/.codex` |
350
+
351
+ An agent can read the credentials it needs and cannot rewrite them, cannot edit the
352
+ instructions every role shares, and cannot alter a peer's configuration. Claude pre-trust
353
+ stays a host-side step performed by the fleet.
354
+
355
+ The forbidden-path list is enforced, not advisory: a role that asks for `~/.ssh`, the ours
356
+ key store, a sibling's state directory — or a parent directory that would expose one, or a
357
+ symlink to one — is refused by `ours-fleet config` before it can launch.
358
+
256
359
  ### Start staggering
257
360
 
258
361
  `start_stagger_ms` (top-level, host-wide, default `0`) spaces out agent **launches**
@@ -273,21 +376,33 @@ their boots ~4 s apart instead of firing all seven at once.
273
376
 
274
377
  ### Mail monitor
275
378
 
276
- With `monitor.enabled` (the default), the **supervisor** delivers a role's mail
277
- wakes: the per-role runner long-polls the ours daemon's notification API and
379
+ With `monitor.mode: fleet` (the default), the **ours-fleet supervisor** delivers
380
+ a role's mail wakes: the per-role runner long-polls the ours daemon's notification API and
278
381
  submits a single `[fleet-monitor] N new messages from … — run get_messages` prompt
279
- through the selected backend. ACP uses structured `session/prompt`; tmux uses
280
- verified console input. It primes the notification cursor *before* the session launches
382
+ through the selected backend. ACP uses live steering when its adapter supports it
383
+ and falls back to structured `session/prompt`; tmux uses verified console input.
384
+ Set `monitor.interrupt: true` on roles where every configured wake should cancel
385
+ the active turn before the notification is delivered. This is intentionally
386
+ content-blind: the supervisor cannot inspect encrypted message bodies, so all
387
+ events selected by `wake_sources` receive the same interrupt policy.
388
+ It primes the notification cursor *before* the session launches
281
389
  (no missed arrivals), cannot be orphaned or left deaf-but-armed, and writes its
282
390
  health to `<agentDir>/.monitor-status` (`armed | degraded | failed`), surfaced in
283
391
  `ours-fleet status`/`doctor`. Injection is held while the pane shows a modal dialog,
284
392
  so an injected wake can never answer a trust/permission prompt; if the dialog is
285
393
  still up after 2 minutes the monitor gives up on that wake and records
286
394
  `degraded: modal wedge …` instead of waiting silently forever (the mail stays
287
- queued and its cursor is not committed until a later delivery is accepted). The agent's
288
- briefing tells it **not** to arm an
289
- in-session Monitor. Set `monitor.enabled: false` to keep the legacy behavior where
290
- the agent arms its own `ours-mcp watch`. `inject: full` (pushing message bodies
395
+ queued and its cursor is not committed until a later delivery is accepted). The
396
+ agent's briefing tells it **not** to arm a native harness Monitor.
397
+
398
+ With `monitor.mode: native`, ours-fleet does not start its supervisor monitor;
399
+ the generated briefing instead instructs the harness to arm its own wake
400
+ mechanism (`ours-mcp watch` for Claude Code, or the Codex
401
+ `arm_monitor`/`foreground_monitor` flow). The old `monitor.enabled: true|false`
402
+ form remains accepted as a compatibility alias for `fleet|native`, respectively,
403
+ but new configuration should use `mode`.
404
+
405
+ `inject: full` (pushing message bodies
291
406
  inline) is on the roadmap and needs two new ours-mcp daemon endpoints; today all
292
407
  roles deliver `notification` lines and drain via `get_messages`.
293
408
 
@@ -315,10 +430,11 @@ A role with `harness: codex` resolves its launcher at every supervised start:
315
430
  3. `launcher: ours-codex` makes the enhanced launcher mandatory and fails clearly if
316
431
  it is missing; `launcher: codex` explicitly selects standard mode.
317
432
 
318
- Monitoring remains consent-first. By default the generated briefing asks in the role's
319
- console before calling `arm_monitor`. Set `harness_options.monitor: true` (or pass
320
- `ours-fleet spawn --monitor`) to record explicit persistent consent for that role, including
321
- supervised restarts. In the standard-Codex fallback, the agent separately surfaces the
433
+ Native Codex monitoring remains consent-first. By default the generated briefing asks in
434
+ the role's console before calling `arm_monitor`. Set `harness_options.monitor: true` (or
435
+ pass the legacy `ours-fleet spawn --monitor` flag) to record explicit persistent consent
436
+ for the harness-native monitor, including supervised restarts. This does not choose the
437
+ wake owner; use `monitor.mode: fleet|native` for that. In the standard-Codex fallback, the agent separately surfaces the
322
438
  `ours-codex` recommendation before asking to enter `foreground_monitor`. It never backgrounds
323
439
  `ours-mcp watch`, because a detached watch cannot wake a Codex turn. The foreground
324
440
  wait is re-entered after each handled message; `ours-codex` instead wakes the idle
@@ -335,7 +451,7 @@ defaults:
335
451
  profile: fleet # $CODEX_HOME/fleet.config.toml
336
452
  sandbox: workspace-write
337
453
  approval: on-request
338
- monitor: true # explicit consent for unattended mail wake
454
+ monitor: true # consent for Codex's native monitor; not the wake-owner selector
339
455
  search: true
340
456
  add_dirs: [/data/shared]
341
457
  config:
@@ -348,7 +464,8 @@ roles:
348
464
  ```
349
465
 
350
466
  Equivalent one-off/permanent spawn controls include `--model`, `--permission-mode`,
351
- `--sandbox`, `--profile`, `--launcher`, `--search`, `--monitor`, repeatable
467
+ `--sandbox`, `--profile`, `--launcher`, `--search`, legacy `--monitor` (native
468
+ Codex monitor consent), repeatable
352
469
  `--codex-config key=value`, and repeatable `--add-dir`. Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
353
470
  role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
354
471
 
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Cross-process file protection: a lock, and a replace that cannot leave a
3
+ * partial file behind.
4
+ *
5
+ * Shared state written with plain `readFileSync` → mutate → `writeFileSync` has
6
+ * two failure modes that only appear under concurrency, which is exactly the
7
+ * condition a fleet creates: two starts interleave and one silently discards
8
+ * the other's entry, or a crash mid-write truncates the last good file.
9
+ */
10
+ export interface LockDeps {
11
+ now?(): number;
12
+ sleep?(ms: number): Promise<void>;
13
+ }
14
+ /**
15
+ * Run `fn` holding a cross-process lock. `mkdir` is atomic on every platform we
16
+ * support, so the directory's existence IS the lock; the timestamp inside lets a
17
+ * crashed holder's lock be broken rather than deadlocking the fleet forever.
18
+ * Same strategy as the runner's launch gate, factored out so both use one.
19
+ *
20
+ * The lock is always released, including when `fn` throws.
21
+ */
22
+ export declare function withFileLock<T>(lockPath: string, fn: () => T | Promise<T>, deps?: LockDeps, staleMs?: number): Promise<T>;
23
+ /**
24
+ * Replace a file's contents atomically: write a temp file in the SAME directory
25
+ * (so the rename cannot cross a filesystem boundary), fsync it, then rename over
26
+ * the target. A reader either sees the old file or the new one — never a
27
+ * half-written one — and an interrupted write leaves the previous contents
28
+ * intact.
29
+ */
30
+ export declare function replaceFileAtomically(path: string, contents: string, mode?: number): void;
@@ -0,0 +1,86 @@
1
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, writeSync, } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ const DEFAULT_STALE_MS = 10_000;
5
+ const POLL_MS = 25;
6
+ /**
7
+ * Run `fn` holding a cross-process lock. `mkdir` is atomic on every platform we
8
+ * support, so the directory's existence IS the lock; the timestamp inside lets a
9
+ * crashed holder's lock be broken rather than deadlocking the fleet forever.
10
+ * Same strategy as the runner's launch gate, factored out so both use one.
11
+ *
12
+ * The lock is always released, including when `fn` throws.
13
+ */
14
+ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_STALE_MS) {
15
+ const now = deps.now ?? (() => Date.now());
16
+ const sleep = deps.sleep ?? ((ms) => new Promise(r => { setTimeout(r, ms); }));
17
+ const stampPath = join(lockPath, 'ts');
18
+ mkdirSync(dirname(lockPath), { recursive: true });
19
+ for (let waited = 0;;) {
20
+ try {
21
+ mkdirSync(lockPath);
22
+ writeFileSync(stampPath, String(now()));
23
+ break;
24
+ }
25
+ catch (e) {
26
+ if (e.code !== 'EEXIST')
27
+ throw e;
28
+ let held = null;
29
+ try {
30
+ const n = parseInt(readFileSync(stampPath, 'utf8').trim(), 10);
31
+ held = Number.isFinite(n) ? n : null;
32
+ }
33
+ catch { /* holder died between mkdir and stamp */ }
34
+ if ((held !== null && now() - held > staleMs) || waited > staleMs * 2) {
35
+ rmSync(lockPath, { recursive: true, force: true }); // break a dead holder's lock
36
+ continue;
37
+ }
38
+ await sleep(POLL_MS);
39
+ waited += POLL_MS;
40
+ }
41
+ }
42
+ try {
43
+ return await fn();
44
+ }
45
+ finally {
46
+ rmSync(lockPath, { recursive: true, force: true });
47
+ }
48
+ }
49
+ /**
50
+ * Replace a file's contents atomically: write a temp file in the SAME directory
51
+ * (so the rename cannot cross a filesystem boundary), fsync it, then rename over
52
+ * the target. A reader either sees the old file or the new one — never a
53
+ * half-written one — and an interrupted write leaves the previous contents
54
+ * intact.
55
+ */
56
+ export function replaceFileAtomically(path, contents, mode = 0o600) {
57
+ const dir = dirname(path);
58
+ mkdirSync(dir, { recursive: true });
59
+ const tmp = join(dir, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
60
+ const fd = openSync(tmp, 'w', mode);
61
+ try {
62
+ writeSync(fd, contents);
63
+ fsyncSync(fd); // the bytes are on disk before anything points at them
64
+ }
65
+ finally {
66
+ closeSync(fd);
67
+ }
68
+ try {
69
+ renameSync(tmp, path);
70
+ }
71
+ catch (e) {
72
+ rmSync(tmp, { force: true });
73
+ throw e;
74
+ }
75
+ // Best-effort: make the rename itself durable. Not supported everywhere.
76
+ try {
77
+ const dirFd = openSync(dir, 'r');
78
+ try {
79
+ fsyncSync(dirFd);
80
+ }
81
+ finally {
82
+ closeSync(dirFd);
83
+ }
84
+ }
85
+ catch { /* platform does not allow fsync on a directory */ }
86
+ }
@@ -6,6 +6,12 @@ export interface BriefingOpts {
6
6
  routinesPath: string;
7
7
  /** Curated body (from briefing_file) replacing the narrative sections. */
8
8
  briefingBody?: string;
9
+ /**
10
+ * What spawn actually established about the role's ours identity (7.3).
11
+ * Defaults to `unverified`, because a briefing generated without that
12
+ * knowledge must not claim one.
13
+ */
14
+ identityGuarantee?: 'verified' | 'created' | 'unverified';
9
15
  }
10
16
  /** Render a role's briefing.md: narrative (or curated body) + mechanical boot steps. */
11
17
  export declare function generateBriefing(role: ResolvedRole, v: BriefingVocab, opts: BriefingOpts): string;
package/dist/briefing.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { userInfo } from 'node:os';
2
+ import { oversightTaxonomyLines } from './session/control.js';
2
3
  /** Render a role's briefing.md: narrative (or curated body) + mechanical boot steps. */
3
4
  export function generateBriefing(role, v, opts) {
4
5
  const L = [];
@@ -22,10 +23,25 @@ export function generateBriefing(role, v, opts) {
22
23
  }
23
24
  L.push('', '## Do these NOW, in order');
24
25
  L.push(`1. ${v.launchNote(role.name)}`);
25
- L.push(`2. BIND your predefined ours identity: call the **${v.bindTool}** tool with`);
26
- L.push(` name "${id}" force=true (search the deferred tool registry first if needed).`);
27
- L.push(` - If no such identity exists yet, call **${v.createTool}** name "${id}" once`);
28
- L.push(' to mint it, then you are bound. Re-binding your OWN identity is always allowed.');
26
+ // What this says depends on what spawn actually VERIFIED (7.3). Asserting a
27
+ // "predefined" identity that nobody checked is how an agent ends up improvising
28
+ // its own infrastructure on first boot.
29
+ const guarantee = opts.identityGuarantee ?? 'unverified';
30
+ if (guarantee === 'unverified') {
31
+ L.push(`2. BIND your ours identity: call the **${v.bindTool}** tool with`);
32
+ L.push(` name "${id}" force=true (search the deferred tool registry first if needed).`);
33
+ L.push(` - This identity was NOT verified when your role was created, so it may not exist.`);
34
+ L.push(` If binding reports no such identity, call **${v.createTool}** name "${id}" once`);
35
+ L.push(' to mint it, then you are bound. Re-binding your OWN identity is always allowed.');
36
+ }
37
+ else {
38
+ L.push(`2. BIND your ours identity: call the **${v.bindTool}** tool with`);
39
+ L.push(` name "${id}" force=true (search the deferred tool registry first if needed).`);
40
+ L.push(` - It was ${guarantee === 'created' ? 'created' : 'verified to exist'} when your role`);
41
+ L.push(' was created, so binding should succeed. If it unexpectedly reports no such');
42
+ L.push(` identity, call **${v.createTool}** name "${id}" once and report the discrepancy —`);
43
+ L.push(' something removed it after your role was created.');
44
+ }
29
45
  L.push(`3. RECONCILE your profile (idempotent): call **${v.currentIdentityTool}** and read your`);
30
46
  L.push(' current bio and persona, so you only write below when they actually differ.');
31
47
  L.push(`4. PUBLISH your public **bio** via **${v.setBioTool}**`);
@@ -34,9 +50,9 @@ export function generateBriefing(role, v, opts) {
34
50
  : ' with a 1–2 sentence summary of your Charter above. Skip if it already matches.');
35
51
  L.push(`5. SET your **persona** (local operating contract, never shared in invites) via`);
36
52
  L.push(` **${v.setPersonaTool}** with the **Charter** section above, verbatim. Skip if it matches.`);
37
- // When the supervisor owns the monitor (monitor.enabled), the agent must NOT arm
53
+ // When the supervisor owns the monitor (monitor.mode=fleet), the agent must NOT arm
38
54
  // its own in-session watch — wakes are injected as [fleet-monitor] lines (design §5).
39
- const wakeNote = role.monitor?.enabled
55
+ const wakeNote = role.monitor?.mode === 'fleet'
40
56
  ? v.supervisedWakeNote(id, role)
41
57
  : v.monitorInstruction(id, role);
42
58
  L.push(`6. ${wakeNote}`);
@@ -50,7 +66,10 @@ export function generateBriefing(role, v, opts) {
50
66
  L.push(`7. Await messages. When the monitor wakes you (or the owner requests a manual check),`);
51
67
  L.push(` call **${v.getMessagesTool}**, act on them,`);
52
68
  L.push(` and reply with ${v.sendTool}. No coordinator is configured — the owner drives you`);
53
- L.push(` via \`tmux attach -t ${role.name}\` or by messaging "${id}".`);
69
+ // NOT `tmux attach -t <name>`: each role's pane lives on its own tmux
70
+ // socket (#32), so a bare attach finds no server. `ours-fleet attach`
71
+ // addresses the right one.
72
+ L.push(` via \`ours-fleet attach ${role.name}\` or by messaging "${id}".`);
54
73
  }
55
74
  if (role.oversee?.length) {
56
75
  L.push('', '## Oversight assignments');
@@ -58,13 +77,24 @@ export function generateBriefing(role, v, opts) {
58
77
  for (const o of role.oversee)
59
78
  L.push(`- **${o.role}** — check every ${o.interval}`);
60
79
  L.push('');
61
- L.push('Procedure (see also the oversee-agents skill if available): on each tick, run');
80
+ L.push('Procedure (see also the oversee-agents skill if available). On each tick, for each ward');
81
+ L.push('run BOTH — they answer different questions:');
62
82
  for (const o of role.oversee)
63
- L.push(`\`ours-fleet peek ${o.role}\``);
64
- L.push('and judge the console: stuck on a prompt/menu/trust dialog → answer it directly with');
65
- L.push('`ours-fleet send <Name> "<text>"` (or `--key <K>` for raw keys); crashed to a shell ');
66
- L.push('investigate and restart; idle with work assigned nudge; healthy do nothing.');
67
- L.push('Escalate over ours messaging only when you cannot resolve it yourself.');
83
+ L.push(`\`ours-fleet status ${o.role}\` then \`ours-fleet peek ${o.role}\``);
84
+ L.push('');
85
+ L.push('**One console command is not a liveness verdict.** A `peek` or `send` that fails tells');
86
+ L.push('you what happened to YOUR REQUEST, and only one of its outcomes says the agent is gone.');
87
+ L.push('Read the result you actually got:');
88
+ L.push('');
89
+ L.push(...oversightTaxonomyLines());
90
+ L.push('');
91
+ L.push('Never translate any other failure into "dead". A busy agent, an unanswered control');
92
+ L.push('plane and a confirmed stop look identical if you only look at one command.');
93
+ L.push('');
94
+ L.push('Then judge the console content: stuck on a prompt/menu/trust dialog → answer it directly');
95
+ L.push('with `ours-fleet send <Name> "<text>"` (or `--key <K>` for raw keys); idle with work');
96
+ L.push('assigned → nudge; actively working → do nothing, and do not mistake a long turn for a');
97
+ L.push('stall. Escalate over ours messaging only when you cannot resolve it yourself.');
68
98
  }
69
99
  L.push('', '## Durable log');
70
100
  L.push(`Append important commands / decisions / results to \`${opts.worklogPath}\` as you go —`);