@ours.network/fleet 0.18.1 → 0.19.0-nightly.2

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 (79) hide show
  1. package/README.md +57 -91
  2. package/dist/application/fleet-query-service.js +0 -12
  3. package/dist/application/role-creation-service.js +10 -7
  4. package/dist/application/types.d.ts +0 -11
  5. package/dist/briefing.js +6 -15
  6. package/dist/build-info.json +5 -5
  7. package/dist/cli.js +12 -37
  8. package/dist/config.d.ts +8 -6
  9. package/dist/config.js +46 -30
  10. package/dist/creation.d.ts +38 -22
  11. package/dist/creation.js +111 -24
  12. package/dist/docs.d.ts +1 -1
  13. package/dist/docs.js +44 -95
  14. package/dist/doctor.d.ts +5 -1
  15. package/dist/doctor.js +18 -11
  16. package/dist/fleet-proxy.d.ts +0 -5
  17. package/dist/harness/acp-agent.js +6 -11
  18. package/dist/harness/claude-code.js +11 -200
  19. package/dist/harness/codex.d.ts +1 -4
  20. package/dist/harness/codex.js +12 -70
  21. package/dist/harness/types.d.ts +4 -54
  22. package/dist/loops/manager.d.ts +1 -30
  23. package/dist/loops/manager.js +6 -69
  24. package/dist/loops/state.d.ts +0 -18
  25. package/dist/loops/state.js +0 -4
  26. package/dist/monitor.js +1 -1
  27. package/dist/ops.d.ts +3 -0
  28. package/dist/ops.js +8 -3
  29. package/dist/owner-channel/attachments.d.ts +25 -2
  30. package/dist/owner-channel/attachments.js +61 -5
  31. package/dist/owner-channel/channel.d.ts +29 -30
  32. package/dist/owner-channel/channel.js +291 -291
  33. package/dist/owner-channel/commands.js +89 -0
  34. package/dist/owner-channel/message-recovery.d.ts +25 -0
  35. package/dist/owner-channel/message-recovery.js +114 -0
  36. package/dist/owner-channel/notices.d.ts +0 -7
  37. package/dist/owner-channel/notices.js +0 -9
  38. package/dist/owner-channel/ours-client.d.ts +148 -0
  39. package/dist/owner-channel/ours-client.js +231 -0
  40. package/dist/rooms-tasks/cli.d.ts +4 -0
  41. package/dist/rooms-tasks/cli.js +565 -0
  42. package/dist/rooms-tasks/config.d.ts +15 -0
  43. package/dist/rooms-tasks/config.js +171 -0
  44. package/dist/rooms-tasks/cowork-adapter.d.ts +80 -0
  45. package/dist/rooms-tasks/cowork-adapter.js +48 -0
  46. package/dist/rooms-tasks/index.d.ts +7 -0
  47. package/dist/rooms-tasks/index.js +7 -0
  48. package/dist/rooms-tasks/room-state.d.ts +22 -0
  49. package/dist/rooms-tasks/room-state.js +117 -0
  50. package/dist/rooms-tasks/task-state.d.ts +31 -0
  51. package/dist/rooms-tasks/task-state.js +173 -0
  52. package/dist/rooms-tasks/templates.d.ts +6 -0
  53. package/dist/rooms-tasks/templates.js +80 -0
  54. package/dist/rooms-tasks/types.d.ts +153 -0
  55. package/dist/rooms-tasks/types.js +20 -0
  56. package/dist/runner.d.ts +0 -48
  57. package/dist/runner.js +94 -236
  58. package/dist/session/acp.d.ts +0 -104
  59. package/dist/session/acp.js +10 -213
  60. package/dist/session/conversation-normalizer.d.ts +0 -6
  61. package/dist/session/conversation-normalizer.js +10 -153
  62. package/dist/session/conversation-types.d.ts +4 -23
  63. package/dist/session/types.d.ts +0 -35
  64. package/dist/spawn.js +26 -33
  65. package/dist/supervisor/systemd.js +29 -2
  66. package/dist/watchdog/briefing.js +0 -7
  67. package/dist/watchdog/run.js +3 -3
  68. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  69. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  70. package/dist/web-app/index.html +1 -1
  71. package/dist/worklog.d.ts +1 -7
  72. package/dist/worklog.js +39 -191
  73. package/package.json +3 -1
  74. package/dist/model-env.d.ts +0 -71
  75. package/dist/model-env.js +0 -106
  76. package/dist/owner-channel/mcp.d.ts +0 -24
  77. package/dist/owner-channel/mcp.js +0 -145
  78. package/dist/session/activity.d.ts +0 -31
  79. package/dist/session/activity.js +0 -48
package/README.md CHANGED
@@ -68,6 +68,8 @@ 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 |
71
73
  | `.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 |
72
74
  | `.session-events.jsonl`, `.control.sock`, `.control-token` | ACP backend | bounded typed console projection and private attachment control |
73
75
 
@@ -79,7 +81,7 @@ The state dir contract:
79
81
  | tmux | roles using `session: tmux` (the default) | `apt install tmux` / `brew install tmux` |
80
82
  | Node ≥ 22 | Claude roles using `session: acp` | required by the maintained Claude ACP adapter |
81
83
  | a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
82
- | `ours-mcp` daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/mcp && ours-mcp start` |
84
+ | `ours` CLI + shared daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/cli && ours daemon start` |
83
85
 
84
86
  Linux only: `ours-fleet init` enables *linger* so roles run without a login session
85
87
  and survive reboots. macOS: launchd agents start **at login** (no linger
@@ -208,27 +210,12 @@ offline shell and no stale fleet state.
208
210
  The console provides evidence-separated inventory and status, ACP activity and
209
211
  permission controls, redacted logs, typed text send, confirmed lifecycle
210
212
  actions, transactional permanent/temporary creation, and a shared tmux browser
211
- terminal. Identity is fixed to the role name. Creation uses the current
212
- authenticated identity existence check and reports verified, missing, or
213
- unknown evidence; a newly launched harness follows its generated first-boot
214
- instructions to choose or create and bind the identity. The console never
215
- claims that the host created an identity and never deletes one.
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.
213
+ terminal. Identity is fixed to the role name. Creation uses the authenticated
214
+ daemon inventory and reports verified, missing, or unknown evidence. A missing
215
+ permanent role identity is created deterministically before launch with local
216
+ discovery and auto-accept enabled, then the provisioning lease is released so
217
+ the harness only has to bind it. Fleet never silently replaces or force-adopts
218
+ an existing identity.
232
219
 
233
220
  For a temporary role, those first-boot instructions preserve and bind an
234
221
  existing identity when one is present. If the assigned identity is missing,
@@ -236,8 +223,9 @@ the role capability-detects the ours MCP `create_temporary_identity` tool and
236
223
  uses it when available, so the newly created identity is owned and cleaned up
237
224
  by that connector session lifecycle. Older ours servers remain compatible via
238
225
  `create_identity`. A collision or creation error stops for operator review;
239
- fleet never force-adopts or deletes identity state. Permanent roles continue to
240
- use normal `create_identity` bootstrap behavior.
226
+ fleet never force-adopts or deletes identity state. Permanent roles are
227
+ provisioned by fleet before launch and never delegate normal identity creation
228
+ to the harness.
241
229
 
242
230
  `node-pty` is optional: if its native module cannot load, ACP and all
243
231
  non-terminal features remain available and tmux Terminal is disabled with a
@@ -360,10 +348,6 @@ defaults:
360
348
  max_tokens: 500000 # session cap (harness-interpreted)
361
349
  monitor:
362
350
  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
367
351
  roles:
368
352
  Name: # [A-Za-z0-9_-]+
369
353
  harness: claude-code
@@ -383,7 +367,7 @@ roles:
383
367
  batch_ms: 2000 # coalesce a burst into one line (default 2000)
384
368
  inject: notification # notification (default) | full (bodies inline; roadmap)
385
369
  owner_channel: # optional trusted owner ingress; requires session: acp
386
- identity: "Name Owner Channel" # existing, dedicated ours identity bound only by fleet
370
+ identity: "Name Owner Channel" # dedicated identity; fleet creates it with safe local defaults
387
371
  owners: [owner-contact-cid] # authenticated ours contact IDs, never display names
388
372
  agent: managed-agent-cid # exact role CID allowed to relay messages/files outward
389
373
  interrupt: false # false queues; true cancels current work first
@@ -450,11 +434,6 @@ role's `harness_options`, so a fleet can set common Codex permission/profile def
450
434
  and override individual keys per role. `monitor` merges the same way — a role block
451
435
  overrides `defaults.monitor` key-by-key.
452
436
 
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
-
458
437
  ### Scheduled agent loops
459
438
 
460
439
  Top-level `loops` schedule literal prompts from trusted local YAML. Enabled targets
@@ -545,13 +524,10 @@ native settings actually grant, against a fixed floor:
545
524
  those requests with nobody to see it. With `unattended: wait` it **warns**,
546
525
  since a human can still attach a console and answer.
547
526
 
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.
527
+ **Security meaning.** `ask` maps to Codex `untrusted` / Claude `default`,
528
+ `auto` to Codex `on-request` / Claude `acceptEdits`, and `allow` to Codex
529
+ `never` / Claude `bypassPermissions`, the non-interactive modes that actually
530
+ permit the actions the role was authorized to take.
555
531
  `dontAsk` suppresses only the *prompt*, not the denial, which is why an
556
532
  `allow` role previously ran unable to do its job. Legacy `deny` is accepted
557
533
  only for compatibility and retains its conservative Codex `on-request` /
@@ -563,14 +539,10 @@ ACP exposes agent-specific session mode IDs and `session/set_mode`, but no
563
539
  portable permission-policy capability. Fleet uses that primitive where an
564
540
  adapter has a corresponding mode and otherwise translates at the adapter. The
565
541
  bundled Codex ACP adapter couples approval and sandboxing in its advertised
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.
542
+ mode IDs, so fleet preserves the selected sandbox preset and enforces approval
543
+ independently on the app-server turn request. Thus `allow` + `workspace` is
544
+ actually `never` + `workspace-write`, never `danger-full-access`. Live session
545
+ metadata reports the normalized policy and the ACP sandbox-preset ID.
574
546
 
575
547
  ### Isolation at creation time
576
548
 
@@ -672,13 +644,14 @@ agent's briefing tells it **not** to arm a native harness Monitor.
672
644
 
673
645
  With `monitor.mode: native`, ours-fleet does not start its supervisor monitor;
674
646
  the generated briefing instead instructs the harness to arm its own wake
675
- mechanism (`ours-mcp watch` for Claude Code, or the Codex
647
+ mechanism (the structured `ours api watch-notifications` JSONL stream for Claude Code,
648
+ or the Codex
676
649
  `arm_monitor`/`foreground_monitor` flow). The old `monitor.enabled: true|false`
677
650
  form remains accepted as a compatibility alias for `fleet|native`, respectively,
678
651
  but new configuration should use `mode`.
679
652
 
680
653
  `inject: full` (pushing message bodies
681
- inline) is on the roadmap and needs two new ours-mcp daemon endpoints; today all
654
+ inline) is on the roadmap and needs two new daemon endpoints; today all
682
655
  roles deliver `notification` lines and drain via `get_messages`.
683
656
 
684
657
  ACP stdio remains private to the persistent runner. `send`, `peek`, and the basic
@@ -689,8 +662,10 @@ console later; no terminal UI is part of the monitor or session backend.
689
662
  ### Trusted owner channel
690
663
 
691
664
  `owner_channel` adds a second ours identity to a role without changing the
692
- role's normal identity. Create that dedicated identity in ours first, connect it
693
- to each owner/controller identity and the managed role identity, put the owners'
665
+ role's normal identity. Fleet creates that dedicated permanent identity when it
666
+ is missing, with local exposure and local auto-accept disabled, and releases the
667
+ provisioning lease before the supervisor binds it. Connect it to each
668
+ owner/controller identity and the managed role identity, put the owners'
694
669
  immutable contact CIDs in `owners`, and put the managed role identity's exact CID
695
670
  in `agent`. The channel identity must not be any role identity or another role's
696
671
  channel identity. Add it to the control plane just like another contact, then
@@ -902,22 +877,26 @@ command path sits behind the authenticated owner-CID check: a non-owner sending
902
877
  `/force-restart` or `/model` is silently ignored exactly like any other
903
878
  unauthorized mail.
904
879
 
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.
880
+ Handled wire IDs are durably bounded for deduplication, while message and
881
+ response bodies stay out of fleet state. Before consuming mail, fleet reads the
882
+ body-free oldest-first metadata from `listIncomingMessages`, atomically journals
883
+ the exact wire/sequence slice (at most 200), and calls `getMessages` with that
884
+ exact length. A returned wire/sequence set mismatch fails closed. After a crash,
885
+ journaled bodies are loaded only from persistent history with `getHistoryItem`;
886
+ the journal is pruned only after the existing durable owner-channel state marks
887
+ the wire handled. This provides at-least-once turn delivery without placing
888
+ message plaintext in fleet state.
910
889
 
911
890
  Inbound owner attachments use the same authenticated-CID and exact-wire routing
912
- boundary. Fleet first calls the metadata-only `list_incoming_files`, groups a
891
+ boundary. Fleet first calls the metadata-only `listIncomingFiles`, groups a
913
892
  file-only wake or a same-sender reply-linked text caption, and checks the enabled,
914
893
  count, declared MIME, per-file size, and total-size policy before retrieving any
915
- bytes. It then calls selective `get_files` only for the admitted wire IDs. An
894
+ bytes. It then calls selective `getFiles` only for the admitted wire IDs. An
916
895
  unauthorized sender is ignored without retrieval or reply. A rejected authorized
917
896
  request receives a bounded reason correlated to its file wire.
918
897
 
919
898
  Retrieved files must be regular, non-symlink paths whose byte count and SHA-256
920
- match ours-mcp metadata. Fleet additionally checks content signatures against the
899
+ match typed SDK metadata. Fleet additionally checks content signatures against the
921
900
  declared MIME, sanitizes traversal/control characters from names, and copies each
922
901
  file into a random request-scoped directory at mode 0700 with files at mode 0600.
923
902
  The `[fleet-owner]` turn receives only bounded metadata, the private local paths,
@@ -928,17 +907,19 @@ the agent must use the audio path rather than inventing text.
928
907
  Request files are removed after final delivery and stale directories are removed
929
908
  after `retention_ms`. A bounded mode-0600 recovery journal stores only owner CID
930
909
  and wire routing metadata—never filenames, paths, captions, transcripts, or file
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
910
+ bytes. If a selected file is already read when fleet restarts, fleet resolves
911
+ only that journaled wire with `getFileInfo` and retrieves its immutable blob with
912
+ `fetchFile`; a claimed managed-agent caption is loaded from message history and
913
+ rejoined with its journaled read files before the group is admitted or relayed.
914
+ Recovered voice files remain transcript-unavailable and are never sent through
915
+ a second transcription attempt. Conversation route state migrates from v1 to a
916
+ bounded v2
935
917
  source-wire index so a correlated group keeps the authenticated owner selected by
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.
918
+ its original request even after later owner traffic. Corrupt recovery state disables attachment
919
+ admission. The host must run an ours daemon compatible with SDK 2.0.1 whose
920
+ typed incoming-file, selective retrieval, and file-fetch operations support
921
+ these guarantees; `ours config show --json` reports `sttConfigured` without
922
+ revealing provider credentials.
942
923
 
943
924
  Managed-agent file egress is a separate trust direction. It retains the same
944
925
  metadata provenance checks, count/per-file/request byte caps, regular-file and
@@ -984,7 +965,7 @@ pass the legacy `ours-fleet spawn --monitor` flag) to record explicit persistent
984
965
  for the harness-native monitor, including supervised restarts. This does not choose the
985
966
  wake owner; use `monitor.mode: fleet|native` for that. In the standard-Codex fallback, the agent separately surfaces the
986
967
  `ours-codex` recommendation before asking to enter `foreground_monitor`. It never backgrounds
987
- `ours-mcp watch`, because a detached watch cannot wake a Codex turn. The foreground
968
+ the structured CLI watcher, because a detached process cannot wake a Codex turn. The foreground
988
969
  wait is re-entered after each handled message; `ours-codex` instead wakes the idle
989
970
  session through its App Server integration.
990
971
 
@@ -1068,7 +1049,7 @@ picks the most recently active session **in the role's `cwd`**. This works
1068
1049
  cleanly as long as each role has its own `cwd` (the common case); two roles
1069
1050
  sharing an identical `cwd` could have their resumes cross — give them distinct
1070
1051
  working directories if that matters. MCP and monitor wiring is provided by
1071
- [`@ours.network/codex`](https://github.com/adapt-toolkit/ours-mcp/tree/main/packages/codex);
1052
+ [`@ours.network/codex`](https://www.npmjs.com/package/@ours.network/codex);
1072
1053
  the core ours skill discovers fleet behavior through `ours-fleet docs`.
1073
1054
  `ours-fleet doctor --harness codex` verifies the CLI, ours plugin, and enhanced launcher/fallback.
1074
1055
 
@@ -1140,7 +1121,7 @@ rollout, anchors, aliases, explicit tags, non-scalar keys, and multiple document
1140
1121
  produce source-positioned warnings; opt into enforcement with
1141
1122
  `--yaml-mode strict`. Strict mode will become the next-major default.
1142
1123
 
1143
- WORKLOG rotation is enabled for roles by default with conservative built-ins:
1124
+ Long-running roles may opt into bounded durable logs:
1144
1125
 
1145
1126
  ```yaml
1146
1127
  worklog:
@@ -1150,23 +1131,8 @@ worklog:
1150
1131
  ```
1151
1132
 
1152
1133
  Rotation is conservative: a concurrent change aborts the attempt and retries at
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.
1134
+ a later fleet lifecycle point. Archives remain in the role state directory and
1135
+ may contain the same sensitive material as `WORKLOG.md`.
1170
1136
 
1171
1137
  Claude roles can use a credential-free loopback proxy:
1172
1138
 
@@ -1,6 +1,5 @@
1
1
  import { lstatSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { classifyActivity } from '../session/activity.js';
4
3
  import { controlRequest } from '../session/control.js';
5
4
  import { SessionControlError } from '../session/types.js';
6
5
  import { readExitRecord, readRestartLedger } from '../runner.js';
@@ -56,10 +55,6 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
56
55
  if (session.reachability === 'online'
57
56
  && (session.readiness === 'running' || session.readiness === 'awaiting_permission'))
58
57
  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';
63
58
  if (session.reachability === 'online' && session.readiness === 'idle')
64
59
  return 'ready';
65
60
  if (session.reachability === 'offline'
@@ -158,7 +153,6 @@ export class FleetQueryService {
158
153
  protocolVersion: snapshot.protocolVersion, features: snapshot.features,
159
154
  runtimeModel: snapshot.runtimeModel, reasoningEffort: snapshot.reasoningEffort,
160
155
  permissionMode: snapshot.permissionMode,
161
- activity: classifyActivity(snapshot.activity),
162
156
  };
163
157
  }
164
158
  catch (error) {
@@ -170,7 +164,6 @@ export class FleetQueryService {
170
164
  : failure === 'control-unavailable' ? 'unavailable' : 'unknown',
171
165
  readiness: offline ? 'failed' : 'unknown',
172
166
  evidence: 'authoritative', lastError: clean(error.message),
173
- activity: { state: 'unobservable' },
174
167
  };
175
168
  }
176
169
  }
@@ -181,23 +174,18 @@ export class FleetQueryService {
181
174
  backend: 'tmux', reachability: has ? 'online' : supervisor === 'stopped' ? 'offline' : 'unknown',
182
175
  readiness: has ? 'idle' : supervisor === 'running' ? 'starting' : 'failed',
183
176
  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' },
187
177
  };
188
178
  }
189
179
  catch (error) {
190
180
  return {
191
181
  backend: 'tmux', reachability: 'unknown', readiness: 'unknown',
192
182
  evidence: 'inferred', lastError: clean(error.message),
193
- activity: { state: 'unobservable' },
194
183
  };
195
184
  }
196
185
  }
197
186
  return {
198
187
  backend: 'unknown', reachability: supervisor === 'stopped' ? 'offline' : 'unknown',
199
188
  readiness: supervisor === 'stopped' ? 'failed' : 'unknown', evidence: 'inferred',
200
- activity: { state: 'unobservable' },
201
189
  };
202
190
  }
203
191
  }
@@ -118,9 +118,7 @@ export class RoleCreationService {
118
118
  };
119
119
  const warnings = [];
120
120
  if (request.permissions.approval === 'allow')
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');
121
+ warnings.push('approval=allow maps to an elevated native permission mode');
124
122
  if (request.permissions.filesystem === 'unrestricted')
125
123
  warnings.push('filesystem=unrestricted grants access outside the workspace');
126
124
  if (request.permissions.unattended === 'wait')
@@ -208,9 +206,12 @@ export class RoleCreationService {
208
206
  throw new FleetError('stale_state', `identity '${action.roleId}' appeared after preview; review and confirm reuse`);
209
207
  }
210
208
  const creation = {
211
- // Deliberately strip any mutation methods from the test seam. The web
212
- // uses today's read-only daemon preflight; first-boot owns create/bind.
213
- identityProvisioner: { exists: name => this.identityProvisioner.exists(name) },
209
+ // Permanent identity infrastructure is reconciled before launch. A
210
+ // temporary identity must still be created by its eventual connector
211
+ // session because that exact lease owns cleanup.
212
+ identityProvisioner: preview.effective.lifetime === 'permanent'
213
+ ? this.identityProvisioner
214
+ : { exists: name => this.identityProvisioner.exists(name) },
214
215
  onStage: (stage, evidence) => this.coreStage(action, stage, evidence),
215
216
  };
216
217
  if (preview.effective.lifetime === 'permanent') {
@@ -220,7 +221,9 @@ export class RoleCreationService {
220
221
  await spawnTemp(this.spawnOptions(preview.request, action.actionId), this.options.binPath, this.options.tempLauncher, creation);
221
222
  }
222
223
  this.stage(action, 'launched', 'launch accepted; readiness not yet confirmed');
223
- this.stage(action, 'identity_bootstrap_pending', 'the harness must choose or create and bind its identity from the generated briefing');
224
+ this.stage(action, 'identity_bootstrap_pending', preview.effective.lifetime === 'permanent'
225
+ ? 'fleet established the permanent identity; the harness must bind it from the generated briefing'
226
+ : 'the harness must choose or create and bind its session-owned temporary identity from the generated briefing');
224
227
  this.stage(action, 'waiting_for_session');
225
228
  const ready = await this.waitForReady(action.roleId, action.session);
226
229
  if (ready === 'ready') {
@@ -72,17 +72,6 @@ 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
- };
86
75
  };
87
76
  restart: {
88
77
  circuit: 'closed' | 'open';
package/dist/briefing.js CHANGED
@@ -59,17 +59,15 @@ export function generateBriefing(role, v, opts) {
59
59
  else if (guarantee === 'unverified') {
60
60
  L.push(`2. BIND your ours identity: call the **${v.bindTool}** tool with`);
61
61
  L.push(` name "${id}" force=true (search the deferred tool registry first if needed).`);
62
- L.push(` - This identity was NOT verified when your role was created, so it may not exist.`);
63
- L.push(` If binding reports no such identity, call **${v.createTool}** name "${id}" once`);
64
- L.push(' to mint it, then you are bound. Re-binding your OWN identity is always allowed.');
62
+ L.push(` - This permanent identity was NOT verified before launch. If it does not exist, STOP`);
63
+ L.push(' and report the infrastructure error; identity creation belongs to the fleet lifecycle.');
65
64
  }
66
65
  else {
67
66
  L.push(`2. BIND your ours identity: call the **${v.bindTool}** tool with`);
68
67
  L.push(` name "${id}" force=true (search the deferred tool registry first if needed).`);
69
68
  L.push(` - It was ${guarantee === 'created' ? 'created' : 'verified to exist'} when your role`);
70
- L.push(' was created, so binding should succeed. If it unexpectedly reports no such');
71
- L.push(` identity, call **${v.createTool}** name "${id}" once and report the discrepancy —`);
72
- L.push(' something removed it after your role was created.');
69
+ L.push(' was started, so binding should succeed. If it unexpectedly reports no such identity,');
70
+ L.push(' STOP and report the infrastructure race; do not create or replace it yourself.');
73
71
  }
74
72
  L.push(`3. RECONCILE your profile (idempotent): call **${v.currentIdentityTool}** and read your`);
75
73
  L.push(' current bio and persona, so you only write below when they actually differ.');
@@ -168,11 +166,6 @@ export function generateBriefing(role, v, opts) {
168
166
  L.push('Never translate any other failure into "dead". A busy agent, an unanswered control');
169
167
  L.push('plane and a confirmed stop look identical if you only look at one command.');
170
168
  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('');
176
169
  L.push('Then judge the console content: stuck on a prompt/menu/trust dialog → answer it directly');
177
170
  L.push('with `ours-fleet send <Name> "<text>"` (or `--key <K>` for raw keys); idle with work');
178
171
  L.push('assigned → nudge; actively working → do nothing, and do not mistake a long turn for a');
@@ -183,10 +176,8 @@ export function generateBriefing(role, v, opts) {
183
176
  L.push('it survives restarts.');
184
177
  if (role.worklog) {
185
178
  L.push(`Fleet rotates it above ${role.worklog.max_kb} KiB, keeps approximately the newest ` +
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.');
179
+ `${role.worklog.keep_tail_kb} KiB here, and retains ${role.worklog.max_archives} archives ` +
180
+ 'beside it. Continue writing only WORKLOG.md.');
190
181
  }
191
182
  L.push('', '## Routines');
192
183
  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.1",
3
- "buildId": "1f620d26f86b",
4
- "commit": "3440b37a924055a4479e0e0e424c1cdd69570847",
5
- "dirty": false,
6
- "builtAt": "2026-08-22T12:32:39.824Z",
2
+ "version": "0.19.0-nightly.2",
3
+ "buildId": "13880d55bd06",
4
+ "commit": "ccf83082de238e6dc8ddb1bc557d2b7ffc7e816f",
5
+ "dirty": true,
6
+ "builtAt": "2026-08-23T08:33:08.792Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -25,9 +25,8 @@ 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, effectivePermissionMode, formatNative, } from './permissions.js';
28
+ import { allWarnings, analyzeFleetPermissions, formatNative } from './permissions.js';
29
29
  import { AI_DOCS } from './docs.js';
30
- import { classifyActivity, describeSessionState } from './session/activity.js';
31
30
  import { controlRequest, controlSocketPath, followControl, livenessNote, } from './session/control.js';
32
31
  import { SessionControlError } from './session/types.js';
33
32
  import { readScheduledLoops, storedLoopHealth } from './loops/state.js';
@@ -38,6 +37,7 @@ import { WebAccessStore, passwordAccess, validatePublicOrigin } from './web/acce
38
37
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-proxy.js';
39
38
  import './harness/claude-code.js'; // registers the claude-code adapter
40
39
  import './harness/codex.js'; // registers the codex adapter
40
+ import { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './rooms-tasks/cli.js';
41
41
  // sudo/su shells lack XDG_RUNTIME_DIR, breaking every systemctl/journalctl
42
42
  // --user child (supervisor commands, logs, doctor). Derive it before dispatch. (#9)
43
43
  deriveXdgRuntimeDir();
@@ -364,14 +364,8 @@ program.command('ls').description('list running fleet sessions')
364
364
  continue;
365
365
  try {
366
366
  const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
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
- }
367
+ if (response.ok && response.result?.alive)
368
+ acp.push(`${name}: acp`);
375
369
  }
376
370
  catch { /* ignore stale sockets */ }
377
371
  }
@@ -512,14 +506,6 @@ program.command('status <name>').description('unit/agent state')
512
506
  else if (ledger.consecutiveImmediateFailures > 0)
513
507
  console.log(`restarts: ${ledger.consecutiveImmediateFailures} consecutive immediate `
514
508
  + `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`);
523
509
  const modelStatus = joinPath(agentDir(name), '.model-status');
524
510
  if (existsSync(modelStatus)) {
525
511
  try {
@@ -538,14 +524,8 @@ program.command('status <name>').description('unit/agent state')
538
524
  if (stateDir) {
539
525
  try {
540
526
  const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
541
- if (response.ok) {
542
- const snapshot = response.result;
527
+ if (response.ok)
543
528
  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
- }
549
529
  }
550
530
  catch {
551
531
  console.log('session: acp control unavailable');
@@ -656,10 +636,7 @@ function renderLoopRows(role, state, loop) {
656
636
  return Object.entries(state.loops).filter(([name]) => !loop || name === loop).map(([name, item]) => `${role}/${name} ${item.enabled && !item.operatorDisabled ? 'enabled' : 'disabled'} `
657
637
  + `${item.activeRunId ? 'running' : 'idle'} next=${item.nextDueAt} last=${item.lastOutcome ?? 'never'} `
658
638
  + `counts=${item.counts.started}/${item.counts.completed}/${item.counts.failed} `
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
+ + `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`);
663
640
  }
664
641
  cOpt(loopsCommand.command('status [role] [loop]').description('show live or stored loop state'))
665
642
  .option('--json', 'emit stable JSON')
@@ -1113,7 +1090,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1113
1090
  .option('--coordinator <name>', 'announce target')
1114
1091
  .option('--model <id>', 'model id to launch on (e.g. claude-fable-5); default: launcher default')
1115
1092
  .option('--permission-mode <mode>', 'harness permission mode (Codex: untrusted|on-request|never; Claude: native values)')
1116
- .option('--approval <mode>', 'fleet permission mode: ask|auto|allow (Codex ACP allow selects agent-full-access; deny is deprecated)')
1093
+ .option('--approval <mode>', 'fleet permission mode: ask|auto|allow (deny is deprecated)')
1117
1094
  .option('--filesystem <mode>', 'common filesystem intent: read-only|workspace|unrestricted')
1118
1095
  .option('--unattended <mode>', 'permission behavior without a console: deny|wait')
1119
1096
  .option('--sandbox <mode>', 'Codex sandbox: read-only|workspace-write|danger-full-access')
@@ -1185,17 +1162,13 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1185
1162
  console.log(`spawned ${result.lifetime} agent '${result.role}' through `
1186
1163
  + `${result.caller}'s fleet proxy (state: ${result.statePath})`);
1187
1164
  console.log(` ${result.harness}/${result.session}`
1188
- + `${result.model ? ` model=${result.model}` : ''}`
1189
- + (result.permissionMode
1190
- ? `; permission=${result.permissionMode.fleetMode} native=${result.permissionMode.nativeMode}`
1191
- : '') + '; '
1165
+ + `${result.model ? ` model=${result.model}` : ''}; `
1192
1166
  + `monitor=${result.monitor.mode} interrupt=${result.monitor.interrupt}`);
1193
1167
  if (result.inherited.length)
1194
1168
  console.log(` inherited omitted defaults from ${result.caller}: ${result.inherited.join(', ')}`);
1195
1169
  console.log(`→ watch it: ours-fleet peek ${result.role} | attach: ours-fleet attach ${result.role}`);
1196
1170
  return;
1197
1171
  }
1198
- const plannedPermissionMode = effectivePermissionMode(spawnDryRun(o).resolvedRole);
1199
1172
  if (o.temp) {
1200
1173
  const dir = await spawnTemp(o, binPath);
1201
1174
  console.log(`spawned temp agent '${roleName}' (state: ${dir}; gone on exit/reboot)`);
@@ -1213,8 +1186,6 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1213
1186
  for (const line of formatProvenance(lastProvenance))
1214
1187
  console.log(line);
1215
1188
  }
1216
- console.log(` permission=${plannedPermissionMode.fleetMode} `
1217
- + `native=${plannedPermissionMode.nativeMode}`);
1218
1189
  console.log(`→ watch it: ours-fleet peek ${roleName} | attach: ours-fleet attach ${roleName}`);
1219
1190
  }
1220
1191
  catch (e) {
@@ -1433,6 +1404,10 @@ function configureWebAccess(opts) {
1433
1404
  + 'or --pairing for protection, or --no-password only for intentional unprotected access');
1434
1405
  return undefined;
1435
1406
  }
1407
+ // ── rooms & tasks ──────────────────────────────────────────────────────────
1408
+ registerTemplateCommands(program, cOpt);
1409
+ registerTaskCommands(program, cOpt);
1410
+ registerRoomCommands(program, cOpt);
1436
1411
  program.command('_run <name>', { hidden: true }).description('internal: supervisor entrypoint')
1437
1412
  .option('-c, --configuration <file>')
1438
1413
  .action(async (name, opts) => {