@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.
- package/README.md +138 -21
- package/dist/atomic-file.d.ts +30 -0
- package/dist/atomic-file.js +86 -0
- package/dist/briefing.d.ts +6 -0
- package/dist/briefing.js +43 -13
- package/dist/cli.js +98 -22
- package/dist/config.d.ts +24 -3
- package/dist/config.js +84 -11
- package/dist/creation.d.ts +179 -0
- package/dist/creation.js +254 -0
- package/dist/docs.d.ts +28 -1
- package/dist/docs.js +155 -8
- package/dist/doctor.js +75 -17
- package/dist/harness/claude-code.d.ts +39 -3
- package/dist/harness/claude-code.js +128 -26
- package/dist/harness/codex.d.ts +7 -1
- package/dist/harness/codex.js +58 -11
- package/dist/harness/registry.d.ts +2 -0
- package/dist/harness/registry.js +19 -0
- package/dist/harness/types.d.ts +51 -4
- package/dist/isolation/bubblewrap.js +7 -1
- package/dist/isolation/policy.d.ts +34 -5
- package/dist/isolation/policy.js +114 -7
- package/dist/isolation/resources.d.ts +6 -3
- package/dist/isolation/resources.js +6 -3
- package/dist/isolation/types.d.ts +19 -1
- package/dist/monitor.d.ts +33 -4
- package/dist/monitor.js +150 -32
- package/dist/ops.d.ts +15 -2
- package/dist/ops.js +32 -9
- package/dist/permissions.d.ts +70 -0
- package/dist/permissions.js +97 -0
- package/dist/runner.d.ts +65 -2
- package/dist/runner.js +262 -27
- package/dist/session/acp.d.ts +25 -2
- package/dist/session/acp.js +143 -26
- package/dist/session/control.d.ts +49 -1
- package/dist/session/control.js +116 -12
- package/dist/session/tmux.d.ts +9 -2
- package/dist/session/tmux.js +36 -4
- package/dist/session/types.d.ts +99 -2
- package/dist/session/types.js +42 -1
- package/dist/spawn.d.ts +27 -2
- package/dist/spawn.js +153 -15
- package/dist/supervisor/launchd.d.ts +50 -0
- package/dist/supervisor/launchd.js +121 -4
- package/dist/supervisor/none.js +22 -4
- package/dist/supervisor/systemd.d.ts +8 -1
- package/dist/supervisor/systemd.js +94 -4
- package/dist/supervisor/types.d.ts +36 -3
- package/dist/tmux.d.ts +34 -2
- package/dist/tmux.js +48 -11
- 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)
|
|
58
|
-
|
|
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:
|
|
195
|
-
|
|
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:
|
|
207
|
-
|
|
208
|
-
#
|
|
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.
|
|
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
|
|
280
|
-
|
|
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
|
|
288
|
-
briefing tells it **not** to arm
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
319
|
-
console before calling `arm_monitor`. Set `harness_options.monitor: true` (or
|
|
320
|
-
`ours-fleet spawn --monitor`) to record explicit persistent consent
|
|
321
|
-
|
|
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 #
|
|
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
|
|
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
|
+
}
|
package/dist/briefing.d.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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?.
|
|
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
|
-
|
|
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)
|
|
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('
|
|
65
|
-
L.push('
|
|
66
|
-
L.push('
|
|
67
|
-
L.push('
|
|
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 —`);
|