@ours.network/fleet 0.18.0-nightly.6 → 0.18.1

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 (60) hide show
  1. package/README.md +81 -43
  2. package/dist/application/fleet-query-service.js +12 -0
  3. package/dist/application/role-creation-service.js +3 -1
  4. package/dist/application/types.d.ts +11 -0
  5. package/dist/briefing.js +9 -2
  6. package/dist/build-info.json +5 -5
  7. package/dist/cli.js +37 -7
  8. package/dist/config.d.ts +6 -3
  9. package/dist/config.js +28 -14
  10. package/dist/creation.d.ts +14 -15
  11. package/dist/creation.js +19 -13
  12. package/dist/docs.d.ts +1 -1
  13. package/dist/docs.js +92 -33
  14. package/dist/doctor.d.ts +1 -5
  15. package/dist/doctor.js +11 -18
  16. package/dist/fleet-proxy.d.ts +5 -0
  17. package/dist/harness/acp-agent.js +11 -6
  18. package/dist/harness/claude-code.js +200 -11
  19. package/dist/harness/codex.d.ts +4 -1
  20. package/dist/harness/codex.js +70 -12
  21. package/dist/harness/types.d.ts +54 -4
  22. package/dist/loops/manager.d.ts +30 -1
  23. package/dist/loops/manager.js +69 -6
  24. package/dist/loops/state.d.ts +18 -0
  25. package/dist/loops/state.js +4 -0
  26. package/dist/model-env.d.ts +71 -0
  27. package/dist/model-env.js +106 -0
  28. package/dist/monitor.js +1 -1
  29. package/dist/ops.js +1 -1
  30. package/dist/owner-channel/attachments.d.ts +2 -25
  31. package/dist/owner-channel/attachments.js +5 -61
  32. package/dist/owner-channel/channel.d.ts +30 -29
  33. package/dist/owner-channel/channel.js +291 -291
  34. package/dist/owner-channel/mcp.d.ts +24 -0
  35. package/dist/owner-channel/mcp.js +145 -0
  36. package/dist/owner-channel/notices.d.ts +7 -0
  37. package/dist/owner-channel/notices.js +9 -0
  38. package/dist/runner.d.ts +48 -0
  39. package/dist/runner.js +237 -85
  40. package/dist/session/acp.d.ts +104 -0
  41. package/dist/session/acp.js +213 -10
  42. package/dist/session/activity.d.ts +31 -0
  43. package/dist/session/activity.js +48 -0
  44. package/dist/session/conversation-normalizer.d.ts +6 -0
  45. package/dist/session/conversation-normalizer.js +153 -10
  46. package/dist/session/conversation-types.d.ts +23 -4
  47. package/dist/session/types.d.ts +35 -0
  48. package/dist/spawn.js +29 -17
  49. package/dist/supervisor/systemd.js +2 -29
  50. package/dist/watchdog/briefing.js +7 -0
  51. package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
  52. package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
  53. package/dist/web-app/index.html +1 -1
  54. package/dist/worklog.d.ts +7 -1
  55. package/dist/worklog.js +191 -39
  56. package/package.json +1 -3
  57. package/dist/owner-channel/message-recovery.d.ts +0 -25
  58. package/dist/owner-channel/message-recovery.js +0 -114
  59. package/dist/owner-channel/ours-client.d.ts +0 -148
  60. package/dist/owner-channel/ours-client.js +0 -231
package/README.md CHANGED
@@ -68,8 +68,6 @@ The state dir contract:
68
68
  | `.identity`, `.cwd`, `.session-id`, `.booted`, `.exit-status`, `.config-path` | supervisor | dot-marker state — session resume and boot bookkeeping |
69
69
  | `.monitor-state.json`, `.monitor-status` | supervisor monitor | atomic body-free cursor/pending state and health |
70
70
  | `.owner-channel-state.json` | owner-channel bridge | bounded wire-ID dedupe only; never message/reply plaintext |
71
- | `.owner-channel-message-recovery.json` | owner-channel bridge | mode-0600 body-free message claim journal: wire ID, history sequence, and claim time only |
72
- | `.owner-channel-attachment-recovery.json` | owner-channel bridge | mode-0600 attachment route journal; never filenames, paths, transcript text, or bytes |
73
71
  | `.owner-channel-binder.lock/`, `.owner-channel-binder.json` | owner-channel supervisor | mode-0600 role/identity + PID/start-marker ownership and release metadata; never mail plaintext or credentials |
74
72
  | `.session-events.jsonl`, `.control.sock`, `.control-token` | ACP backend | bounded typed console projection and private attachment control |
75
73
 
@@ -81,7 +79,7 @@ The state dir contract:
81
79
  | tmux | roles using `session: tmux` (the default) | `apt install tmux` / `brew install tmux` |
82
80
  | Node ≥ 22 | Claude roles using `session: acp` | required by the maintained Claude ACP adapter |
83
81
  | a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
84
- | `ours` CLI + shared daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/cli && ours daemon start` |
82
+ | `ours-mcp` daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/mcp && ours-mcp start` |
85
83
 
86
84
  Linux only: `ours-fleet init` enables *linger* so roles run without a login session
87
85
  and survive reboots. macOS: launchd agents start **at login** (no linger
@@ -216,6 +214,22 @@ unknown evidence; a newly launched harness follows its generated first-boot
216
214
  instructions to choose or create and bind the identity. The console never
217
215
  claims that the host created an identity and never deletes one.
218
216
 
217
+ ACP tool diffs are normalized before they enter the durable conversation
218
+ ledger. Small diffs retain their existing before/after representation. When an
219
+ adapter reports an oversized whole-file snapshot, fleet stores only the changed
220
+ region with path, operation, original byte counts, digest, and boundedness
221
+ metadata. Each retained changed side is capped at 64 KiB as a UTF-8-safe,
222
+ newest-content tail. The tail advances to a line boundary when a complete line
223
+ fits. If one logical line alone exceeds the cap, fleet retains its newest
224
+ UTF-8-safe suffix and records that it starts mid-line, together with omitted-byte
225
+ count and digest. Paths retain at most a 4 KiB suffix with the same explicit
226
+ size/digest/omission provenance, and a 320 KiB cap covers the complete normalized
227
+ update. An append therefore cannot replay a large historical file while hiding
228
+ the current appended text. The live web-console transcript projects only events
229
+ from the current runner generation and excludes adapter `session/load` replay;
230
+ those replay events remain in the durable ledger with `agent_replay` provenance
231
+ for diagnosis and recovery rather than appearing as current work.
232
+
219
233
  For a temporary role, those first-boot instructions preserve and bind an
220
234
  existing identity when one is present. If the assigned identity is missing,
221
235
  the role capability-detects the ours MCP `create_temporary_identity` tool and
@@ -346,6 +360,10 @@ defaults:
346
360
  max_tokens: 500000 # session cap (harness-interpreted)
347
361
  monitor:
348
362
  mode: fleet # fleet (default) | native
363
+ worklog: # built-ins shown; set false to opt out
364
+ max_kb: 1024 # rotate only above this active-log size
365
+ keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
366
+ max_archives: 12 # recent beside log; older preserved cold
349
367
  roles:
350
368
  Name: # [A-Za-z0-9_-]+
351
369
  harness: claude-code
@@ -432,6 +450,11 @@ role's `harness_options`, so a fleet can set common Codex permission/profile def
432
450
  and override individual keys per role. `monitor` merges the same way — a role block
433
451
  overrides `defaults.monitor` key-by-key.
434
452
 
453
+ Every supervised role is a client of the operator-configured ours daemon. Fleet
454
+ forces `OURS_AUTOSTART=0` in both tmux and ACP harness processes, after role environment
455
+ overlays, so `env.OURS_AUTOSTART` cannot transfer shared daemon lifecycle ownership to an
456
+ agent. Operators and explicit installer/setup flows remain responsible for starting it.
457
+
435
458
  ### Scheduled agent loops
436
459
 
437
460
  Top-level `loops` schedule literal prompts from trusted local YAML. Enabled targets
@@ -522,10 +545,13 @@ native settings actually grant, against a fixed floor:
522
545
  those requests with nobody to see it. With `unattended: wait` it **warns**,
523
546
  since a human can still attach a console and answer.
524
547
 
525
- **Security meaning.** `ask` maps to Codex `untrusted` / Claude `default`,
526
- `auto` to Codex `on-request` / Claude `acceptEdits`, and `allow` to Codex
527
- `never` / Claude `bypassPermissions`, the non-interactive modes that actually
528
- permit the actions the role was authorized to take.
548
+ **Security meaning.** `ask` maps to Codex `untrusted` / Claude `default`.
549
+ `auto` selects Codex ACP `agent` (`on-request` + `workspace-write`) / Claude
550
+ `acceptEdits`. `allow` selects Codex ACP's fully non-interactive yolo mode,
551
+ reported by the adapter as `agent-full-access` (`never` +
552
+ `danger-full-access`); Claude uses `bypassPermissions`. For Codex tmux, where the
553
+ approval and sandbox flags remain independent, `auto` is `on-request` and
554
+ `allow` is `never` while `filesystem` still chooses the sandbox.
529
555
  `dontAsk` suppresses only the *prompt*, not the denial, which is why an
530
556
  `allow` role previously ran unable to do its job. Legacy `deny` is accepted
531
557
  only for compatibility and retains its conservative Codex `on-request` /
@@ -537,10 +563,14 @@ ACP exposes agent-specific session mode IDs and `session/set_mode`, but no
537
563
  portable permission-policy capability. Fleet uses that primitive where an
538
564
  adapter has a corresponding mode and otherwise translates at the adapter. The
539
565
  bundled Codex ACP adapter couples approval and sandboxing in its advertised
540
- mode IDs, so fleet preserves the selected sandbox preset and enforces approval
541
- independently on the app-server turn request. Thus `allow` + `workspace` is
542
- actually `never` + `workspace-write`, never `danger-full-access`. Live session
543
- metadata reports the normalized policy and the ACP sandbox-preset ID.
566
+ mode IDs. Consequently, neutral `allow` selects `agent-full-access` and widens
567
+ `filesystem: workspace` or `read-only` to `danger-full-access`; neutral `auto`
568
+ selects `agent` and uses `workspace-write` even when the neutral filesystem
569
+ value differs. An explicit `harness_options.sandbox` still wins and selects its
570
+ corresponding ACP preset; explicit native approval overrides also win. Fleet
571
+ reports a coupled-mode mismatch as approximate in `config`/`doctor`, and
572
+ per-role `isolation:` remains the boundary for an `allow` ACP role. Live session
573
+ metadata reports the normalized policy and the exact native mode selected.
544
574
 
545
575
  ### Isolation at creation time
546
576
 
@@ -642,14 +672,13 @@ agent's briefing tells it **not** to arm a native harness Monitor.
642
672
 
643
673
  With `monitor.mode: native`, ours-fleet does not start its supervisor monitor;
644
674
  the generated briefing instead instructs the harness to arm its own wake
645
- mechanism (the structured `ours api watch-notifications` JSONL stream for Claude Code,
646
- or the Codex
675
+ mechanism (`ours-mcp watch` for Claude Code, or the Codex
647
676
  `arm_monitor`/`foreground_monitor` flow). The old `monitor.enabled: true|false`
648
677
  form remains accepted as a compatibility alias for `fleet|native`, respectively,
649
678
  but new configuration should use `mode`.
650
679
 
651
680
  `inject: full` (pushing message bodies
652
- inline) is on the roadmap and needs two new daemon endpoints; today all
681
+ inline) is on the roadmap and needs two new ours-mcp daemon endpoints; today all
653
682
  roles deliver `notification` lines and drain via `get_messages`.
654
683
 
655
684
  ACP stdio remains private to the persistent runner. `send`, `peek`, and the basic
@@ -873,26 +902,22 @@ command path sits behind the authenticated owner-CID check: a non-owner sending
873
902
  `/force-restart` or `/model` is silently ignored exactly like any other
874
903
  unauthorized mail.
875
904
 
876
- Handled wire IDs are durably bounded for deduplication, while message and
877
- response bodies stay out of fleet state. Before consuming mail, fleet reads the
878
- body-free oldest-first metadata from `listIncomingMessages`, atomically journals
879
- the exact wire/sequence slice (at most 200), and calls `getMessages` with that
880
- exact length. A returned wire/sequence set mismatch fails closed. After a crash,
881
- journaled bodies are loaded only from persistent history with `getHistoryItem`;
882
- the journal is pruned only after the existing durable owner-channel state marks
883
- the wire handled. This provides at-least-once turn delivery without placing
884
- message plaintext in fleet state.
905
+ Processed wire IDs are durably bounded for deduplication, while
906
+ message and response bodies stay out of fleet state. Delivery is at-least-once
907
+ across a crash (the bridge requeues fetched input before starting a turn); true
908
+ exactly-once processing would require a leased claim/idempotency primitive in
909
+ ours-mcp.
885
910
 
886
911
  Inbound owner attachments use the same authenticated-CID and exact-wire routing
887
- boundary. Fleet first calls the metadata-only `listIncomingFiles`, groups a
912
+ boundary. Fleet first calls the metadata-only `list_incoming_files`, groups a
888
913
  file-only wake or a same-sender reply-linked text caption, and checks the enabled,
889
914
  count, declared MIME, per-file size, and total-size policy before retrieving any
890
- bytes. It then calls selective `getFiles` only for the admitted wire IDs. An
915
+ bytes. It then calls selective `get_files` only for the admitted wire IDs. An
891
916
  unauthorized sender is ignored without retrieval or reply. A rejected authorized
892
917
  request receives a bounded reason correlated to its file wire.
893
918
 
894
919
  Retrieved files must be regular, non-symlink paths whose byte count and SHA-256
895
- match typed SDK metadata. Fleet additionally checks content signatures against the
920
+ match ours-mcp metadata. Fleet additionally checks content signatures against the
896
921
  declared MIME, sanitizes traversal/control characters from names, and copies each
897
922
  file into a random request-scoped directory at mode 0700 with files at mode 0600.
898
923
  The `[fleet-owner]` turn receives only bounded metadata, the private local paths,
@@ -903,19 +928,17 @@ the agent must use the audio path rather than inventing text.
903
928
  Request files are removed after final delivery and stale directories are removed
904
929
  after `retention_ms`. A bounded mode-0600 recovery journal stores only owner CID
905
930
  and wire routing metadata—never filenames, paths, captions, transcripts, or file
906
- bytes. If a selected file is already read when fleet restarts, fleet resolves
907
- only that journaled wire with `getFileInfo` and retrieves its immutable blob with
908
- `fetchFile`; a claimed managed-agent caption is loaded from message history and
909
- rejoined with its journaled read files before the group is admitted or relayed.
910
- Recovered voice files remain transcript-unavailable and are never sent through
911
- a second transcription attempt. Conversation route state migrates from v1 to a
912
- bounded v2
931
+ bytes. If ours-mcp already marked a selected file processed when fleet restarts,
932
+ fleet resumes only that journaled wire with `save_file`; a deferred managed-agent
933
+ caption is replayed with its journaled processed files before the group is
934
+ admitted or relayed. Conversation route state migrates from v1 to a bounded v2
913
935
  source-wire index so a correlated group keeps the authenticated owner selected by
914
- its original request even after later owner traffic. Corrupt recovery state disables attachment
915
- admission. The host must run an ours daemon compatible with SDK 2.0.1 whose
916
- typed incoming-file, selective retrieval, and file-fetch operations support
917
- these guarantees; `ours config show --json` reports `sttConfigured` without
918
- revealing provider credentials.
936
+ its original request even after later owner traffic. Recovered voice is explicitly
937
+ marked transcript-unavailable. Corrupt recovery state disables attachment
938
+ admission. The host must run an ours-mcp version whose
939
+ `list_incoming_files`, selective `get_files`, and `save_file` schemas support
940
+ these guarantees; `ours-mcp voice-status --json` reports whether transcription
941
+ is currently configured.
919
942
 
920
943
  Managed-agent file egress is a separate trust direction. It retains the same
921
944
  metadata provenance checks, count/per-file/request byte caps, regular-file and
@@ -961,7 +984,7 @@ pass the legacy `ours-fleet spawn --monitor` flag) to record explicit persistent
961
984
  for the harness-native monitor, including supervised restarts. This does not choose the
962
985
  wake owner; use `monitor.mode: fleet|native` for that. In the standard-Codex fallback, the agent separately surfaces the
963
986
  `ours-codex` recommendation before asking to enter `foreground_monitor`. It never backgrounds
964
- the structured CLI watcher, because a detached process cannot wake a Codex turn. The foreground
987
+ `ours-mcp watch`, because a detached watch cannot wake a Codex turn. The foreground
965
988
  wait is re-entered after each handled message; `ours-codex` instead wakes the idle
966
989
  session through its App Server integration.
967
990
 
@@ -1045,7 +1068,7 @@ picks the most recently active session **in the role's `cwd`**. This works
1045
1068
  cleanly as long as each role has its own `cwd` (the common case); two roles
1046
1069
  sharing an identical `cwd` could have their resumes cross — give them distinct
1047
1070
  working directories if that matters. MCP and monitor wiring is provided by
1048
- [`@ours.network/codex`](https://www.npmjs.com/package/@ours.network/codex);
1071
+ [`@ours.network/codex`](https://github.com/adapt-toolkit/ours-mcp/tree/main/packages/codex);
1049
1072
  the core ours skill discovers fleet behavior through `ours-fleet docs`.
1050
1073
  `ours-fleet doctor --harness codex` verifies the CLI, ours plugin, and enhanced launcher/fallback.
1051
1074
 
@@ -1117,7 +1140,7 @@ rollout, anchors, aliases, explicit tags, non-scalar keys, and multiple document
1117
1140
  produce source-positioned warnings; opt into enforcement with
1118
1141
  `--yaml-mode strict`. Strict mode will become the next-major default.
1119
1142
 
1120
- Long-running roles may opt into bounded durable logs:
1143
+ WORKLOG rotation is enabled for roles by default with conservative built-ins:
1121
1144
 
1122
1145
  ```yaml
1123
1146
  worklog:
@@ -1127,8 +1150,23 @@ worklog:
1127
1150
  ```
1128
1151
 
1129
1152
  Rotation is conservative: a concurrent change aborts the attempt and retries at
1130
- a later fleet lifecycle point. Archives remain in the role state directory and
1131
- may contain the same sensitive material as `WORKLOG.md`.
1153
+ a later fleet lifecycle point. The active log retains a bounded UTF-8 tail,
1154
+ advancing to a line boundary when a complete line fits. If one logical line
1155
+ alone exceeds the tail budget, its newest suffix remains and
1156
+ `.worklog-rotation.json` explicitly records the mid-line start and omitted byte
1157
+ count. It also records SHA-256 digests for the archive and retained live bytes
1158
+ observed when the manifest is written. The complete prior inode is published
1159
+ under a collision-safe UTC name.
1160
+ `max_archives` bounds recent archives beside `WORKLOG.md`; older complete
1161
+ archives move to `WORKLOG.archives/` and are never deleted. Use `worklog: false` on a role or in
1162
+ `defaults` to opt out. Archives may contain the same sensitive material as
1163
+ `WORKLOG.md`. Rotation refuses a symlinked/non-regular live log or a symlinked
1164
+ cold-archive boundary before replacing the live path, and best-effort removes a
1165
+ duplicate link left by a detected failure while the original inode is still
1166
+ available. These are ordinary path/error safeguards, not a security boundary
1167
+ against a malicious concurrent process with the same Unix authority: intentional
1168
+ symlink swaps or archive-directory renames between checks are outside the threat
1169
+ model and require OS-level isolation from that process.
1132
1170
 
1133
1171
  Claude roles can use a credential-free loopback proxy:
1134
1172
 
@@ -1,5 +1,6 @@
1
1
  import { lstatSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { classifyActivity } from '../session/activity.js';
3
4
  import { controlRequest } from '../session/control.js';
4
5
  import { SessionControlError } from '../session/types.js';
5
6
  import { readExitRecord, readRestartLedger } from '../runner.js';
@@ -55,6 +56,10 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
55
56
  if (session.reachability === 'online'
56
57
  && (session.readiness === 'running' || session.readiness === 'awaiting_permission'))
57
58
  return 'busy';
59
+ // `readiness: idle` alone never justifies `ready`. A steered wake turn is
60
+ // invisible to readiness (FLEET-002), so observed activity outranks it.
61
+ if (session.reachability === 'online' && session.activity.state === 'active')
62
+ return 'busy';
58
63
  if (session.reachability === 'online' && session.readiness === 'idle')
59
64
  return 'ready';
60
65
  if (session.reachability === 'offline'
@@ -153,6 +158,7 @@ export class FleetQueryService {
153
158
  protocolVersion: snapshot.protocolVersion, features: snapshot.features,
154
159
  runtimeModel: snapshot.runtimeModel, reasoningEffort: snapshot.reasoningEffort,
155
160
  permissionMode: snapshot.permissionMode,
161
+ activity: classifyActivity(snapshot.activity),
156
162
  };
157
163
  }
158
164
  catch (error) {
@@ -164,6 +170,7 @@ export class FleetQueryService {
164
170
  : failure === 'control-unavailable' ? 'unavailable' : 'unknown',
165
171
  readiness: offline ? 'failed' : 'unknown',
166
172
  evidence: 'authoritative', lastError: clean(error.message),
173
+ activity: { state: 'unobservable' },
167
174
  };
168
175
  }
169
176
  }
@@ -174,18 +181,23 @@ export class FleetQueryService {
174
181
  backend: 'tmux', reachability: has ? 'online' : supervisor === 'stopped' ? 'offline' : 'unknown',
175
182
  readiness: has ? 'idle' : supervisor === 'running' ? 'starting' : 'failed',
176
183
  evidence: 'inferred',
184
+ // tmux exposes no agent-side evidence at all, and `readiness: idle`
185
+ // here is a pane-liveness inference, not an activity claim.
186
+ activity: { state: 'unobservable' },
177
187
  };
178
188
  }
179
189
  catch (error) {
180
190
  return {
181
191
  backend: 'tmux', reachability: 'unknown', readiness: 'unknown',
182
192
  evidence: 'inferred', lastError: clean(error.message),
193
+ activity: { state: 'unobservable' },
183
194
  };
184
195
  }
185
196
  }
186
197
  return {
187
198
  backend: 'unknown', reachability: supervisor === 'stopped' ? 'offline' : 'unknown',
188
199
  readiness: supervisor === 'stopped' ? 'failed' : 'unknown', evidence: 'inferred',
200
+ activity: { state: 'unobservable' },
189
201
  };
190
202
  }
191
203
  }
@@ -118,7 +118,9 @@ export class RoleCreationService {
118
118
  };
119
119
  const warnings = [];
120
120
  if (request.permissions.approval === 'allow')
121
- warnings.push('approval=allow maps to an elevated native permission mode');
121
+ warnings.push(effective.harness === 'codex' && effective.session === 'acp'
122
+ ? 'approval=allow maps to elevated Codex ACP agent-full-access and widens the native sandbox to danger-full-access'
123
+ : 'approval=allow maps to an elevated native permission mode');
122
124
  if (request.permissions.filesystem === 'unrestricted')
123
125
  warnings.push('filesystem=unrestricted grants access outside the workspace');
124
126
  if (request.permissions.unattended === 'wait')
@@ -72,6 +72,17 @@ export interface RoleStatus {
72
72
  runtimeModel?: SessionSnapshot['runtimeModel'];
73
73
  reasoningEffort?: SessionSnapshot['reasoningEffort'];
74
74
  permissionMode?: SessionSnapshot['permissionMode'];
75
+ /**
76
+ * Activity evidence, kept separate from `readiness` on purpose: `readiness`
77
+ * answers "is a fleet-tracked turn in flight" (the prompt-admission gate),
78
+ * NOT "is this agent working". `state` is the only field a human-facing
79
+ * surface may use to call a role idle or stalled.
80
+ */
81
+ activity: {
82
+ state: 'active' | 'quiet' | 'unobservable';
83
+ activeToolCalls?: number;
84
+ lastUpdateAt?: string;
85
+ };
75
86
  };
76
87
  restart: {
77
88
  circuit: 'closed' | 'open';
package/dist/briefing.js CHANGED
@@ -168,6 +168,11 @@ export function generateBriefing(role, v, opts) {
168
168
  L.push('Never translate any other failure into "dead". A busy agent, an unanswered control');
169
169
  L.push('plane and a confirmed stop look identical if you only look at one command.');
170
170
  L.push('');
171
+ L.push('`ours-fleet status` reports `session.readiness`, which is TURN OCCUPANCY only: a mail');
172
+ L.push('wake delivered by steering runs a whole turn with readiness pinned at `idle`. Read the');
173
+ L.push('`activity:` line beside it — `active` means the role is working — and never call a role');
174
+ L.push('idle or stalled from `readiness=idle` alone.');
175
+ L.push('');
171
176
  L.push('Then judge the console content: stuck on a prompt/menu/trust dialog → answer it directly');
172
177
  L.push('with `ours-fleet send <Name> "<text>"` (or `--key <K>` for raw keys); idle with work');
173
178
  L.push('assigned → nudge; actively working → do nothing, and do not mistake a long turn for a');
@@ -178,8 +183,10 @@ export function generateBriefing(role, v, opts) {
178
183
  L.push('it survives restarts.');
179
184
  if (role.worklog) {
180
185
  L.push(`Fleet rotates it above ${role.worklog.max_kb} KiB, keeps approximately the newest ` +
181
- `${role.worklog.keep_tail_kb} KiB here, and retains ${role.worklog.max_archives} archives ` +
182
- 'beside it. Continue writing only WORKLOG.md.');
186
+ `${role.worklog.keep_tail_kb} KiB here, and keeps ${role.worklog.max_archives} recent archives ` +
187
+ 'beside it. Older complete archives move to WORKLOG.archives without deletion; ' +
188
+ '.worklog-rotation.json identifies the latest archive for restart provenance. ' +
189
+ 'Continue writing only WORKLOG.md.');
183
190
  }
184
191
  L.push('', '## Routines');
185
192
  L.push(`If \`${opts.routinesPath}\` exists, re-read it at the START of every wake — before acting`);
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.18.0-nightly.6",
3
- "buildId": "a6e98a19befa",
4
- "commit": "9d10eff5b5b241926326894479810dbae7b026a2",
5
- "dirty": true,
6
- "builtAt": "2026-08-21T22:28:07.994Z",
2
+ "version": "0.18.1",
3
+ "buildId": "1f620d26f86b",
4
+ "commit": "3440b37a924055a4479e0e0e424c1cdd69570847",
5
+ "dirty": false,
6
+ "builtAt": "2026-08-22T12:32:39.824Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -25,8 +25,9 @@ import { stringify } from 'yaml';
25
25
  import { resolvedRolePlan } from './resolved-plan.js';
26
26
  import { creationBuildNote, formatProvenance, readProvenance } from './creation.js';
27
27
  import { doctor } from './doctor.js';
28
- import { allWarnings, analyzeFleetPermissions, formatNative } from './permissions.js';
28
+ import { allWarnings, analyzeFleetPermissions, effectivePermissionMode, formatNative, } from './permissions.js';
29
29
  import { AI_DOCS } from './docs.js';
30
+ import { classifyActivity, describeSessionState } from './session/activity.js';
30
31
  import { controlRequest, controlSocketPath, followControl, livenessNote, } from './session/control.js';
31
32
  import { SessionControlError } from './session/types.js';
32
33
  import { readScheduledLoops, storedLoopHealth } from './loops/state.js';
@@ -363,8 +364,14 @@ program.command('ls').description('list running fleet sessions')
363
364
  continue;
364
365
  try {
365
366
  const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
366
- if (response.ok && response.result?.alive)
367
- acp.push(`${name}: acp`);
367
+ const result = response.result;
368
+ if (response.ok && result?.alive) {
369
+ // Activity, not readiness: an idle-readiness role may be running a
370
+ // steered wake turn (FLEET-002), so `ls` reports what was observed.
371
+ const observed = classifyActivity(result.activity);
372
+ acp.push(`${name}: acp${observed.state === 'active' ? ' (working)'
373
+ : observed.state === 'quiet' ? ' (no recent agent activity)' : ''}`);
374
+ }
368
375
  }
369
376
  catch { /* ignore stale sockets */ }
370
377
  }
@@ -505,6 +512,14 @@ program.command('status <name>').description('unit/agent state')
505
512
  else if (ledger.consecutiveImmediateFailures > 0)
506
513
  console.log(`restarts: ${ledger.consecutiveImmediateFailures} consecutive immediate `
507
514
  + `failures, next delay ${ledger.nextDelayMs}ms (${ledger.lastReason})`);
515
+ // A kill the supervisor never saw leaves no other trace here. Say it out
516
+ // loud rather than letting the absence of a failure streak read as "fine".
517
+ if (ledger.lastTermination?.class === 'abrupt')
518
+ console.log(`last termination: ABRUPT — the previous supervisor run (started `
519
+ + `${ledger.lastTermination.runStartedAt ?? 'unknown'}) died without an orderly exit; `
520
+ + `observed ${ledger.lastTermination.observedAt}`
521
+ + `\n abrupt terminations recorded: ${ledger.abruptTerminations ?? 1}`
522
+ + `\n cause (signal / OOM-kill): journalctl --user -u ours-fleet-agent@${name}.service`);
508
523
  const modelStatus = joinPath(agentDir(name), '.model-status');
509
524
  if (existsSync(modelStatus)) {
510
525
  try {
@@ -523,8 +538,14 @@ program.command('status <name>').description('unit/agent state')
523
538
  if (stateDir) {
524
539
  try {
525
540
  const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
526
- if (response.ok)
541
+ if (response.ok) {
542
+ const snapshot = response.result;
527
543
  console.log(`session: ${JSON.stringify(response.result)}`);
544
+ // `readiness` is turn occupancy, never an activity claim: a steered
545
+ // wake turn runs to completion with readiness pinned at `idle`
546
+ // (FLEET-002). Say which question each field answers.
547
+ console.log(describeSessionState(snapshot.readiness, snapshot.activity));
548
+ }
528
549
  }
529
550
  catch {
530
551
  console.log('session: acp control unavailable');
@@ -635,7 +656,10 @@ function renderLoopRows(role, state, loop) {
635
656
  return Object.entries(state.loops).filter(([name]) => !loop || name === loop).map(([name, item]) => `${role}/${name} ${item.enabled && !item.operatorDisabled ? 'enabled' : 'disabled'} `
636
657
  + `${item.activeRunId ? 'running' : 'idle'} next=${item.nextDueAt} last=${item.lastOutcome ?? 'never'} `
637
658
  + `counts=${item.counts.started}/${item.counts.completed}/${item.counts.failed} `
638
- + `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`);
659
+ + `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`
660
+ + (item.missedGap
661
+ ? ` gap=${item.missedGap.count}@${item.missedGap.fromAt}..${item.missedGap.throughAt}`
662
+ : ''));
639
663
  }
640
664
  cOpt(loopsCommand.command('status [role] [loop]').description('show live or stored loop state'))
641
665
  .option('--json', 'emit stable JSON')
@@ -1089,7 +1113,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1089
1113
  .option('--coordinator <name>', 'announce target')
1090
1114
  .option('--model <id>', 'model id to launch on (e.g. claude-fable-5); default: launcher default')
1091
1115
  .option('--permission-mode <mode>', 'harness permission mode (Codex: untrusted|on-request|never; Claude: native values)')
1092
- .option('--approval <mode>', 'fleet permission mode: ask|auto|allow (deny is deprecated)')
1116
+ .option('--approval <mode>', 'fleet permission mode: ask|auto|allow (Codex ACP allow selects agent-full-access; deny is deprecated)')
1093
1117
  .option('--filesystem <mode>', 'common filesystem intent: read-only|workspace|unrestricted')
1094
1118
  .option('--unattended <mode>', 'permission behavior without a console: deny|wait')
1095
1119
  .option('--sandbox <mode>', 'Codex sandbox: read-only|workspace-write|danger-full-access')
@@ -1161,13 +1185,17 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1161
1185
  console.log(`spawned ${result.lifetime} agent '${result.role}' through `
1162
1186
  + `${result.caller}'s fleet proxy (state: ${result.statePath})`);
1163
1187
  console.log(` ${result.harness}/${result.session}`
1164
- + `${result.model ? ` model=${result.model}` : ''}; `
1188
+ + `${result.model ? ` model=${result.model}` : ''}`
1189
+ + (result.permissionMode
1190
+ ? `; permission=${result.permissionMode.fleetMode} native=${result.permissionMode.nativeMode}`
1191
+ : '') + '; '
1165
1192
  + `monitor=${result.monitor.mode} interrupt=${result.monitor.interrupt}`);
1166
1193
  if (result.inherited.length)
1167
1194
  console.log(` inherited omitted defaults from ${result.caller}: ${result.inherited.join(', ')}`);
1168
1195
  console.log(`→ watch it: ours-fleet peek ${result.role} | attach: ours-fleet attach ${result.role}`);
1169
1196
  return;
1170
1197
  }
1198
+ const plannedPermissionMode = effectivePermissionMode(spawnDryRun(o).resolvedRole);
1171
1199
  if (o.temp) {
1172
1200
  const dir = await spawnTemp(o, binPath);
1173
1201
  console.log(`spawned temp agent '${roleName}' (state: ${dir}; gone on exit/reboot)`);
@@ -1185,6 +1213,8 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1185
1213
  for (const line of formatProvenance(lastProvenance))
1186
1214
  console.log(line);
1187
1215
  }
1216
+ console.log(` permission=${plannedPermissionMode.fleetMode} `
1217
+ + `native=${plannedPermissionMode.nativeMode}`);
1188
1218
  console.log(`→ watch it: ours-fleet peek ${roleName} | attach: ours-fleet attach ${roleName}`);
1189
1219
  }
1190
1220
  catch (e) {
package/dist/config.d.ts CHANGED
@@ -11,6 +11,9 @@ export interface WorklogPolicy {
11
11
  keep_tail_kb: number;
12
12
  max_archives: number;
13
13
  }
14
+ export type WorklogPolicyInput = Partial<WorklogPolicy> | false;
15
+ /** Conservative built-in policy; `worklog: false` is the explicit opt-out. */
16
+ export declare const DEFAULT_WORKLOG_POLICY: Readonly<WorklogPolicy>;
14
17
  export interface AuthProxyConfig {
15
18
  kind: 'anthropic';
16
19
  base_url: string;
@@ -130,10 +133,10 @@ export interface RoleConfig {
130
133
  isolation?: IsolationConfig;
131
134
  monitor?: Partial<MonitorConfig>;
132
135
  owner_channel?: OwnerChannelConfigInput;
133
- worklog?: WorklogPolicy;
136
+ worklog?: WorklogPolicyInput;
134
137
  auth_proxy?: Partial<AuthProxyConfig>;
135
138
  }
136
- export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'> {
139
+ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel' | 'worklog'> {
137
140
  name: string;
138
141
  harness: string;
139
142
  session: SessionBackendId;
@@ -193,7 +196,7 @@ export declare function canonicalCid(value: string): string;
193
196
  export declare function resolveOwnerChannelConfig(defaults: unknown, role: OwnerChannelConfigInput | undefined, session: SessionBackendId, file?: string, name?: string): OwnerChannelConfig | undefined;
194
197
  export declare function resolveModelChain(model: string | undefined, chain: string[] | undefined, file?: string, name?: string): string[] | undefined;
195
198
  export declare function resolveAuthProxy(defaults: unknown, role: Partial<AuthProxyConfig> | undefined, file?: string, name?: string): AuthProxyConfig | undefined;
196
- export declare function resolveWorklogPolicy(defaults: unknown, role: WorklogPolicy | undefined, file?: string, name?: string): WorklogPolicy | undefined;
199
+ export declare function resolveWorklogPolicy(defaults: unknown, role: WorklogPolicyInput | undefined, file?: string, name?: string): WorklogPolicy | undefined;
197
200
  export declare function resolvePermissions(defaults: unknown, role: Partial<CommonPermissions> | undefined, file?: string, name?: string): CommonPermissions;
198
201
  /**
199
202
  * Merge `defaults.monitor` under the role's own `monitor:` key-by-key, validate the
package/dist/config.js CHANGED
@@ -4,10 +4,17 @@ import { agentDir, defaultConfigPath, fleetDDir, home } from './paths.js';
4
4
  import { parseFleetDocument, } from './config-yaml.js';
5
5
  import { harnessRuntimeDir, resolveIsolation, validateIsolationConfig, } from './isolation/policy.js';
6
6
  import { getAdapter } from './harness/registry.js';
7
+ import { resolveRoleModelEnv } from './model-env.js';
7
8
  import { resolveWatchdogs } from './watchdog/config.js';
8
9
  import { resolveLoops } from './loops/config.js';
9
10
  import { CAPABILITIES, CAP_MONITOR_INTERRUPT_AFTER_TOOL } from './capabilities.js';
10
11
  import { runningLabel } from './provenance.js';
12
+ /** Conservative built-in policy; `worklog: false` is the explicit opt-out. */
13
+ export const DEFAULT_WORKLOG_POLICY = Object.freeze({
14
+ max_kb: 1024,
15
+ keep_tail_kb: 256,
16
+ max_archives: 12,
17
+ });
11
18
  /** The 8 content-free event types the ours daemon appends to notifications.log. */
12
19
  export const NOTIFY_EVENT_TYPES = [
13
20
  'message_received', 'file_received', 'sibling_contact_added', 'local_contact_request',
@@ -217,17 +224,23 @@ export function loadConfig(configPath, options = {}) {
217
224
  const harness = r.harness ?? defaults.harness ?? 'claude-code';
218
225
  const defaultHarness = defaults.harness ?? 'claude-code';
219
226
  const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
220
- const model = resolveRoleModel(r.model, r.harness, defaults);
227
+ if (authProxy && harness !== 'claude-code')
228
+ throw new ConfigError(`${file}: role '${name}' auth_proxy is supported only by claude-code`);
229
+ // Environment and runtime model are resolved together so `model:` and the
230
+ // harness's model pin can never disagree (see src/model-env.ts).
231
+ const modelEnv = resolveRoleModelEnv({
232
+ harness,
233
+ model: resolveRoleModel(r.model, r.harness, defaults),
234
+ modelWasExplicit: r.model !== undefined,
235
+ defaultsEnv: (defaults.env ?? {}),
236
+ roleEnv: r.env,
237
+ ...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
238
+ }, message => new ConfigError(`${file}: role '${name}' ${message}`));
239
+ const env = modelEnv.env;
240
+ const model = modelEnv.model;
221
241
  const modelChain = resolveModelChain(model, r.model_chain ?? (inheritsModelDefaults
222
242
  ? defaults.model_chain
223
243
  : undefined), file, name);
224
- if (authProxy && harness !== 'claude-code')
225
- throw new ConfigError(`${file}: role '${name}' auth_proxy is supported only by claude-code`);
226
- const env = {
227
- ...(defaults.env ?? {}),
228
- ...(r.env ?? {}),
229
- ...(authProxy ? { ANTHROPIC_BASE_URL: authProxy.base_url } : {}),
230
- };
231
244
  roles.push({
232
245
  ...r,
233
246
  name,
@@ -461,15 +474,16 @@ export function resolveAuthProxy(defaults, role, file = 'config', name = 'role')
461
474
  };
462
475
  }
463
476
  export function resolveWorklogPolicy(defaults, role, file = 'config', name = 'role') {
464
- if (defaults === undefined && role === undefined)
477
+ if (role === false || (role === undefined && defaults === false))
465
478
  return undefined;
466
- if (defaults !== undefined && !isPlainObject(defaults))
467
- throw new ConfigError(`${file}: defaults.worklog must be a map`);
479
+ if (defaults !== undefined && defaults !== false && !isPlainObject(defaults))
480
+ throw new ConfigError(`${file}: defaults.worklog must be a map or false`);
468
481
  if (role !== undefined && !isPlainObject(role))
469
- throw new ConfigError(`${file}: role '${name}' worklog must be a map`);
482
+ throw new ConfigError(`${file}: role '${name}' worklog must be a map or false`);
470
483
  const merged = {
471
- ...(defaults ?? {}),
472
- ...(role ?? {}),
484
+ ...DEFAULT_WORKLOG_POLICY,
485
+ ...(defaults === false || defaults === undefined ? {} : defaults),
486
+ ...(role === undefined ? {} : role),
473
487
  };
474
488
  const bad = Object.keys(merged).filter(key => !['max_kb', 'keep_tail_kb', 'max_archives'].includes(key));
475
489
  if (bad.length)