@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.
- package/README.md +81 -43
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.js +9 -2
- package/dist/build-info.json +5 -5
- package/dist/cli.js +37 -7
- package/dist/config.d.ts +6 -3
- package/dist/config.js +28 -14
- package/dist/creation.d.ts +14 -15
- package/dist/creation.js +19 -13
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +92 -33
- package/dist/doctor.d.ts +1 -5
- package/dist/doctor.js +11 -18
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +200 -11
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +70 -12
- package/dist/harness/types.d.ts +54 -4
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.js +1 -1
- package/dist/ops.js +1 -1
- package/dist/owner-channel/attachments.d.ts +2 -25
- package/dist/owner-channel/attachments.js +5 -61
- package/dist/owner-channel/channel.d.ts +30 -29
- package/dist/owner-channel/channel.js +291 -291
- package/dist/owner-channel/mcp.d.ts +24 -0
- package/dist/owner-channel/mcp.js +145 -0
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +237 -85
- package/dist/session/acp.d.ts +104 -0
- package/dist/session/acp.js +213 -10
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/conversation-normalizer.d.ts +6 -0
- package/dist/session/conversation-normalizer.js +153 -10
- package/dist/session/conversation-types.d.ts +23 -4
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.js +29 -17
- package/dist/supervisor/systemd.js +2 -29
- package/dist/watchdog/briefing.js +7 -0
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -3
- package/dist/owner-channel/message-recovery.d.ts +0 -25
- package/dist/owner-channel/message-recovery.js +0 -114
- package/dist/owner-channel/ours-client.d.ts +0 -148
- 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`
|
|
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`
|
|
527
|
-
`
|
|
528
|
-
|
|
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,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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 (
|
|
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
|
-
|
|
877
|
-
response bodies stay out of fleet state.
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
907
|
-
only that journaled wire with `
|
|
908
|
-
|
|
909
|
-
|
|
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.
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
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://
|
|
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
|
-
|
|
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.
|
|
1131
|
-
|
|
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(
|
|
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
|
|
182
|
-
'beside it.
|
|
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`);
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.18.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
5
|
-
"dirty":
|
|
6
|
-
"builtAt": "2026-08-
|
|
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
|
-
|
|
367
|
-
|
|
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?:
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
...
|
|
472
|
-
...(
|
|
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)
|