@awebai/oats 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,6 +50,11 @@ launch-configs: # named ways this host starts a har
50
50
  CLAUDE_CONFIG_DIR: { fromEnv: PERSONAL_CLAUDE_DIR }
51
51
  model: opus
52
52
  yolo: false
53
+ mine:
54
+ harness: claude
55
+ default: true # this host's baseline for every claude launch (0.32)
56
+ env:
57
+ CLAUDE_CONFIG_DIR: /home/ana/.claude-personal
53
58
  ```
54
59
 
55
60
  Schema: [`oats-local.schema.json`](oats-local.schema.json). Unknown keys are
@@ -70,7 +75,7 @@ refused (`E_WORKSPACE_SCHEMA`).
70
75
  | `host.name` | This machine's name. A workspace trigger or schedule runs only on the host named by its `runsOn` ([schedules.md](schedules.md)). |
71
76
  | `automations.trust` | The workspace triggers and schedules (`<member>/<id>`) this host agrees to run, or `"*"` for every one the workspace places here (0.30). Absent or empty: none runs. See [Who runs workspace automations](#who-runs-workspace-automations). |
72
77
  | `triggers.disabled`, `schedules.disabled` | Workspace triggers and schedules (`<member>/<id>`) this host does not run, without a commit. Written by `oats trigger disable` / `oats schedule disable`. |
73
- | `launch-configs.<name>` | A named way to start a harness on this host, chosen at spawn or session start, never by the soul. See [Launch configurations](#launch-configurations). |
78
+ | `launch-configs.<name>` | A named way to start a harness on this host, chosen at spawn or session start, never by the soul. `default: true` makes it this host's baseline for its harness (0.32). See [Launch configurations](#launch-configurations). |
74
79
  | `souls.launch` | This machine's launch preference per soul (0.30): `"*"` for every soul, a soul's own entry (its name, or `<package>/<soul>`) over it. A value is a `launch-configs` name or an inline `{ harness, model? }`. It overrides the soul's own `launch:`; explicit spawn flags win over both. See [Launch preferences](#launch-preferences). |
75
80
 
76
81
  How teams are resolved, and what a messaging provider does with them, is in
@@ -81,8 +86,9 @@ How teams are resolved, and what a messaging provider does with them, is in
81
86
  An entry has `harness` (`pi` \| `claude` \| `codex`, required), `executable`
82
87
  (a bare name looked up on `PATH`, or a path relative to this deployment
83
88
  directory), `args` (literal, no shell), `env` (a literal string, or
84
- `{ fromEnv: NAME }` resolved on the host at start), `model` and `yolo`. A
85
- launch configuration is a host choice: a soul never names one.
89
+ `{ fromEnv: NAME }` resolved on the host at start), `model`, `yolo` and
90
+ `default` (0.32; see [the harness default](#the-harness-default)). A launch
91
+ configuration is a host choice: a soul never names one.
86
92
 
87
93
  - Select one with `--launch-config <name>` on `oats spawn`,
88
94
  `oats session start` and `oats session restart`. A named configuration is
@@ -104,6 +110,50 @@ launch configuration is a host choice: a soul never names one.
104
110
  - The old key `runtime` is still read as `harness`, with a
105
111
  `deprecated-runtime-name` warning.
106
112
 
113
+ ### The harness default
114
+
115
+ `default: true` makes a configuration this host's baseline for its harness
116
+ (0.32, feature `launch-config-default`). Use it for what every launch of a
117
+ harness on this machine needs, whatever soul or preference chose it: an
118
+ account directory (`CLAUDE_CONFIG_DIR`), a wrapper `executable`, an argument.
119
+
120
+ - **When it applies:** a new launch that picks the harness without naming a
121
+ configuration: a soul's `launch:`, an inline `souls.launch` preference,
122
+ `--harness` (on a spawn, or on `session start|restart` of an existing
123
+ home), `--reselect-launch`, and the host default (`pi`). It supplies the
124
+ executable, args, env and `yolo`. A `yolo` recorded from a default stays
125
+ with it: a later `--launch-config none` or another harness does not carry
126
+ it over.
127
+ - **The model** comes from whatever picked the harness (`--model`, then the
128
+ preference); the default's `model` is the last fallback, before the
129
+ harness's own.
130
+ - **A named configuration runs as declared**: `--launch-config <name>` or a
131
+ `souls.launch` name never inherits from the default. `--launch-config none`
132
+ (or a `souls.launch` entry of `none`) asks for the bare harness and
133
+ bypasses it.
134
+ - **One per harness.** A second `default: true` for the same harness is
135
+ refused (`E_LAUNCH_CONFIG_INVALID`, naming both); move it by clearing the
136
+ old one first.
137
+ - **Existing homes keep their launch** until `--reselect-launch` or a
138
+ respawn, like any change of preference. Declaring a default is such a
139
+ change: `oats readiness --home` warns `launch-changed` on each existing
140
+ home the default would now apply to, until it is restarted with
141
+ `--reselect-launch` or respawned.
142
+ - **It is visible.** `oats launch-config list` marks it; `spawn --preview`,
143
+ `launch-config preview`, `instance.json` and `oats inspect --home` say when
144
+ a launch's configuration came from the default (`launchConfigDefault`).
145
+ A default with `yolo: true` turns yolo on for every launch of that harness
146
+ here: the preview shows it.
147
+ - **Every kernel that reads the deployment needs OATS 0.32+.** OATS 0.31
148
+ and older refuse the whole `oats-local.yaml` (`E_WORKSPACE_SCHEMA`) once a
149
+ configuration declares `default`.
150
+
151
+ `oats-claude-config` (a one-line file naming the claude binary, found walking
152
+ up from the deployment) is no longer read. A new claude launch with one in
153
+ reach is refused (`E_CLAUDE_CONFIG_REMOVED`) naming the file: declare the
154
+ name it holds as the claude default (`executable: <name>`, `default: true`)
155
+ and delete the file. Homes launched with it keep their recorded executable.
156
+
107
157
  ### Launch preferences
108
158
 
109
159
  A soul says what its role should run on, and each machine may override it
@@ -128,9 +178,11 @@ souls:
128
178
  harness and replaces only its model.
129
179
  - A **launch configuration** name runs that configuration's full recipe. An
130
180
  **inline or soul preference** runs its harness the way this host starts it
131
- without a configuration (the executable on `PATH`, no args, no env), with
132
- its `model`. A preference without `model` uses the harness's own model; it
133
- never borrows a lower layer's.
181
+ without a configuration: [the harness default](#the-harness-default) if
182
+ one is declared, else the executable on `PATH` with no args and no env,
183
+ with the preference's `model`. A preference without `model` uses the
184
+ harness default's model, else the harness's own; it never borrows a lower
185
+ layer's.
134
186
  - **A missing harness is refused**, never replaced: `E_HARNESS_UNAVAILABLE`
135
187
  names the layer that chose it and the fix (install the harness, or override
136
188
  it here in `souls.launch`). `oats souls` still lists the soul, with the
@@ -274,5 +326,10 @@ oats spawn <soul> --preview # the exact modules, teams and provider payloads
274
326
  oats doctor # this deployment's files and the lock
275
327
  ```
276
328
 
277
- Environment: `OATS_REMOTE_CACHE` relocates the fetch cache;
278
- `OATS_PACKAGE_CATALOG` names an alternative package catalog file.
329
+ Environment: `OATS_REMOTE_CACHE` relocates the fetch cache (which also holds
330
+ the bounded parsed-read cache and the observations `--max-age` reuses; all of
331
+ it is safe to delete); `OATS_PACKAGE_CATALOG` names an alternative package
332
+ catalog file. The read verbs (`status`, `workspace status`, `souls`,
333
+ `capabilities`, `inspect`, the read forms of `teams` and `soul teams`, and
334
+ `spawn --preview`) take `--max-age <seconds>` to reuse a remote head observed that recently
335
+ ([Observation reuse](desktop-cli-api.md#observation-reuse-feature-observe-max-age-oats-0311)).
@@ -55,8 +55,9 @@ symlink enters as `symlink:<target>`; empty directories and a top-level `.git/`
55
55
  ### 1.3 Access, cache, failures
56
56
 
57
57
  - Git uses the operator's own configuration and credentials; `GIT_TERMINAL_PROMPT=0`,
58
- `GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call.
59
- - A commit is fetched depth 1 (no blob filter) into a bare cache `<cacheDir>/<sha256(key)>/` (default
58
+ `GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call; 10 minutes for the fetch of a commit.
59
+ - A commit is fetched depth 1 with its trees and its blobs up to 64 KiB (larger blobs on demand, when a read
60
+ needs them; whole trees from a server without partial fetches; awebai/oats#384) into a bare cache `<cacheDir>/<sha256(key)>/` (default
60
61
  `~/.cache/oats/remotes`; the CLI honours `OATS_REMOTE_CACHE`) and pinned as `refs/oats/commits/<oid>`.
61
62
  The cache may be wiped at any time. Operations on one cache repo are serialized.
62
63
  - Failures: `E_REMOTE_UNREADABLE { url, key, reason }`, `reason` ∈ `auth`, `not-found`, `network`,
@@ -38,7 +38,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
38
38
  "instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
39
39
  "workspace-v2","instance-modules","spawn-provider-payload","served-identity","packages-no-approval","spawn-name","settings-origins",
40
40
  "team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
41
- "preview-composed-from"],
41
+ "preview-composed-from","observe-max-age","spawn-preview-max-age"],
42
42
  "automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
43
43
  "readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2}
44
44
  ```
@@ -95,6 +95,8 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
95
95
  | `desktop-facts` | the facts under [Desktop facts](#desktop-facts-feature-desktop-facts-oats-0290) | |
96
96
  | `launch-preference` | soul and local launch preferences; `launch`, `launchCurrent`, `launchFrom`; `--reselect-launch`; `key` on soul and agent rows ([Launch preferences](#soul-launch-preferences-feature-launch-preference-oats-0300)) | |
97
97
  | `preview-composed-from` | `composedFrom` on preview `modules[]` ([Composition](#the-preview)) | |
98
+ | `observe-max-age` | `--max-age <s>` on the read verbs and their `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311)) | |
99
+ | `spawn-preview-max-age` | `--max-age <s>` on `spawn --preview` and its `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311), [The preview](#the-preview)) | |
98
100
 
99
101
  Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
100
102
  `workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
@@ -181,6 +183,94 @@ an inherited `OATS_DEPLOYMENT` or `OATS_RESOLUTION` (`details.inherited`).
181
183
  homes in `problems[]`: `legacy-captured-home {code, instances, homes,
182
184
  message}` and `legacy-local-agents {code, dirs, instances, message}`.
183
185
 
186
+ <a id="observation-reuse-feature-observe-max-age-oats-0311"></a>
187
+ ### Observation reuse (feature `observe-max-age`, OATS 0.32.0)
188
+
189
+ Every read asks each remote for its current head (`git ls-remote`). With
190
+ `--max-age <seconds>` a read verb reuses a head this machine observed at most
191
+ that many seconds ago instead, so a refresh right after another costs no
192
+ network round trip. Gate the flag on the feature: an older kernel may ignore
193
+ it and answer live, without the block.
194
+
195
+ ```text
196
+ oats status | workspace status | souls | capabilities | inspect --soul|--home
197
+ | teams | soul teams <soul> … --max-age <seconds> --json
198
+ oats spawn <soul> … --preview --max-age <seconds> --json (feature spawn-preview-max-age)
199
+ ```
200
+
201
+ - **Values:** whole seconds, `0` to `86400`. `0` is live: it reuses nothing.
202
+ Anything else is `E_BAD_ARGS` (`--max-age needs a value: whole seconds from 0
203
+ to 86400`, `--max-age takes whole seconds from 0 to 86400, got "<v>"`).
204
+ - **The block:** with the flag (`0` included) the document gains one key,
205
+ `observation: {observedAt, reused, localRevision}`: in the result of an
206
+ envelope, at the top level of the roster (after `agents`). `observedAt` is the
207
+ OLDEST remote head the answer used, so the answer is at least that fresh
208
+ everywhere; `reused` is `true` when any head came from an earlier observation.
209
+ A command that read no remote head reports the time it started and
210
+ `reused: false`. Without the flag the key is absent and every document is
211
+ exactly as before. A refusal (`E_SOUL_UNKNOWN`, any error envelope) never
212
+ carries the block.
213
+ - **Spawn preview** (feature `spawn-preview-max-age`, OATS 0.33.0): the
214
+ preview's `result` gains the same block, so you can say "as of
215
+ `<observedAt>`". The `decision` covers the heads the preview used, reused
216
+ or live: a reused head the member has since moved from makes the apply
217
+ (which always observes live) refuse `E_DECISION_STALE`, and the apply
218
+ records the head it observed, so the next preview under `--max-age` shows
219
+ it with a new `decision.revision`. The apply itself refuses the flag.
220
+ - **`localRevision`:** 24 lowercase hex characters, opaque. It digests every
221
+ piece of local configuration the kernel read for this answer:
222
+ `oats-local.yaml` (and each closer `oats-local.yaml` it looked for and did
223
+ not find), `oats-lock.json`, an `OATS_PACKAGE_CATALOG` file, and the
224
+ automations snapshot. Only what the verb actually read counts. The same inputs
225
+ give the same revision; any byte change, or one of those files appearing or
226
+ disappearing, gives another. It names no path and no content. Different verbs
227
+ read different inputs (`workspace status` also reads the automations
228
+ snapshot; `inspect --home` reads no lock), so compare revisions of the same
229
+ verb and arguments only. Keep what you
230
+ hold (catalogs, inspect results) keyed on it: a different revision means the
231
+ deployment's configuration changed outside you (a `teams` edit, a sync, a
232
+ hand edit). A kernel upgrade shows through the probe (`oats version --json`),
233
+ not through `localRevision`: the bundled catalog is not an input. Instance
234
+ homes, member clones and tmux are not inputs either, because the answer
235
+ itself reports them.
236
+ - **What is reused:** only remote heads (the commit a branch or tag named),
237
+ never local state. Instances, `oats-local.yaml` and the lock are read afresh
238
+ by every command. A member's backlink (`oats-membership.yaml`) is read at
239
+ the member's observed head, which may be a reused one: a backlink removed
240
+ less than `<s>` seconds ago can still show the member `confirmed` under
241
+ `--max-age <s>`. Only the backlink's comparison with this workspace is made
242
+ afresh. A reused head's
243
+ `observedAt` (for example `workspace.observedAt` in `workspace status`) is
244
+ the time it was observed, not now.
245
+ - **When a head is not reused:** it is older than the flag allows (or dated
246
+ more than 5 s in the future); it was observed through a different URL
247
+ spelling of the same repository (ssh vs https) or for a different ref; its
248
+ commit can no longer be fetched. Each is observed live, as without the flag.
249
+ A live observation that fails is the usual error, never an older head.
250
+ - **Refusals:** every other command, a spawn apply (with or without
251
+ `--expect-decision`; the form named is `spawn`), every edit form (`teams add|remove|default`,
252
+ `soul teams --add|--remove|--default|--clear-default`) and any `--server`
253
+ invocation refuse the flag before reading or writing anything, with
254
+ `E_BAD_ARGS` "--max-age is not accepted by \`oats <form>\`: only the read
255
+ verbs reuse observations (status, workspace status, souls, capabilities,
256
+ inspect --soul|--home, spawn --preview, and the read forms of teams and soul
257
+ teams)" and,
258
+ with `--server`, "--max-age cannot be combined with --server: observation
259
+ reuse is local to this machine".
260
+ A capability command's argv (`oats <namespace> …`) is its provider's: the
261
+ kernel neither reads nor refuses `--max-age` there. The same holds for
262
+ `capture`, `recall`, `setup` and `experimental`, which parse their own argv:
263
+ `capture`, `setup` and `experimental` refuse it as an unknown argument (not
264
+ `E_BAD_ARGS`), and `recall` ignores unknown flags.
265
+
266
+ The observations are kept under the remote cache
267
+ (`$OATS_REMOTE_CACHE`, default `~/.cache/oats/remotes`), in `.observed/`,
268
+ beside the bounded parsed-read cache in `.parsed/`. An observation record
269
+ keeps a digest of the fetch URL, never the URL. A parsed entry keeps repository
270
+ content as committed (member refs included), and a value that carries a
271
+ credential-bearing URL (userinfo on http(s), or `user:password@` on any
272
+ scheme) is never written. Deleting either is always safe.
273
+
184
274
  <a id="inspect-readiness-and-operation-run-on-the-workspace-model-operationsapi-2-soulsapi-2-readinessapi-2-oats-0260"></a>
185
275
  ## Inspect, readiness and operation run
186
276
 
@@ -214,7 +304,7 @@ A module's origin (`from`) is `{kind: "member", repoKey, commit}` or `{kind:
214
304
  ### `oats inspect`
215
305
 
216
306
  ```text
217
- oats inspect (--home <abs> | --soul <name> [--dir <d>]) --json
307
+ oats inspect (--home <abs> | --soul <name> [--dir <d>]) [--max-age <s>] --json
218
308
  ```
219
309
 
220
310
  An instance subject, abridged:
@@ -445,7 +535,25 @@ Feature `workspace-v2`, `workspaceApi: 2`. Model: [workspaces.md](workspaces.md)
445
535
  `E_LOCAL_MISSING {dir, searched}`.
446
536
  - The workspace is read over Git remotes with the operator's credentials,
447
537
  never prompting: `E_REMOTE_UNREADABLE {url, reason: "auth" | "not-found" |
448
- "network" | "timeout"}`.
538
+ "network" | "timeout" | "killed" | "cache" | "unknown"}`. `killed` (the
539
+ system killed git, for example out of memory) also carries `signal`.
540
+ `cache` (OATS 0.33.0) is local: the
541
+ remote cache on this machine could not be written (a git lock still held,
542
+ another oats process still writing it, or a lock file one left when it
543
+ died); `details.cacheDir` and, when known, `details.lock`,
544
+ `details.guard` or `details.holderPid` say which, and the message says
545
+ what to do.
546
+ - **How the Desktop reads it** (0.33.0). Of an `E_REMOTE_UNREADABLE` from
547
+ `status` / `workspace status`, the Desktop keeps the `message` (shown as
548
+ given) and only a bounded cause: `details.reason` (matching
549
+ `^[a-z][a-z-]{0,31}$`) and the host of `details.url`. No path, pid, lock,
550
+ `cacheDir` or other detail field crosses to the renderer. It keys only on
551
+ `code` + `details.reason`: `cache` words the roster "OATS cache
552
+ problem" with the message in full; `network` / `timeout` read "Couldn't
553
+ reach <host>"; any other reason keeps the generic wording. When the
554
+ deployment was observed before, the failed read keeps that observation:
555
+ `/api/panel` serves it with `error` (the message) and `errorCause {code,
556
+ reason, host?}`, and the roster shows it stale instead of empty.
449
557
  - There is no package approval: declaring a package is the trust decision.
450
558
  No payload carries `approvalNeeded`, `approval` or `approved`.
451
559
  - A **standalone view** is a member repository whose workspace is not read
@@ -597,7 +705,7 @@ discovers the workspace over the network to check. Other errors: `E_USAGE`,
597
705
  ### `oats workspace status`
598
706
 
599
707
  ```text
600
- oats workspace status [--dir <d>] --json
708
+ oats workspace status [--dir <d>] [--max-age <s>] --json
601
709
  ```
602
710
 
603
711
  Read-only (it writes no lock):
@@ -646,8 +754,8 @@ Read-only (it writes no lock):
646
754
  ### `oats capabilities` and `oats souls`
647
755
 
648
756
  ```text
649
- oats capabilities [--dir <d>] --json
650
- oats souls [--dir <d>] --json
757
+ oats capabilities [--dir <d>] [--max-age <s>] --json
758
+ oats souls [--dir <d>] [--max-age <s>] --json
651
759
  ```
652
760
 
653
761
  Every item of every confirmed member, the external souls, and the locked
@@ -835,7 +943,7 @@ soul subject), `""` when unknown.
835
943
  ### `oats teams`
836
944
 
837
945
  ```text
838
- oats teams [--dir <d>] --json
946
+ oats teams [--dir <d>] [--max-age <s>] --json
839
947
  oats teams add <label> --team <id> [--description <d>] --json
840
948
  oats teams remove <label> --json
841
949
  oats teams default <label> --json
@@ -874,6 +982,7 @@ oats teams default <label> --json
874
982
 
875
983
  ```text
876
984
  oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <label> | --clear-default] [--dir <d>] --json
985
+ oats soul teams <soul>|'*' [--dir <d>] [--max-age <s>] --json (the read form only)
877
986
  ```
878
987
 
879
988
  ```json
@@ -1083,7 +1192,7 @@ the result with `oats inspect --soul <name> --json`.
1083
1192
  ### The preview
1084
1193
 
1085
1194
  ```text
1086
- oats spawn <soul> [the flags of a real spawn] --preview --json
1195
+ oats spawn <soul> [the flags of a real spawn] --preview [--max-age <s>] --json
1087
1196
  ```
1088
1197
 
1089
1198
  Feature `spawn-preview-2`, `spawnPreviewApi: 2`. The preview runs every
@@ -1135,6 +1244,12 @@ it to a temporary copy (`soulFetched: true`).
1135
1244
  (absent when nothing sets it) are the resolved selection.
1136
1245
  `backendStatus` is `{name, installed, started: false}`, `null` with
1137
1246
  `--no-launch`. `executable` is the resolved harness binary.
1247
+ - `launchConfigDefault` (0.32, feature `launch-config-default`) is `true`
1248
+ when `launchConfig` is this host's default for the harness (its
1249
+ executable, args, env and `yolo` apply without being chosen), `false`
1250
+ otherwise. Show it, and `yolo`, whenever it is `true`. An explicit
1251
+ `--launch-config none` asks for the bare harness and bypasses the default;
1252
+ to run the host's default, omit `--launch-config`.
1138
1253
  - `modelSource` is `"explicit"`, `"soul default"`, `"launch-config <name>"`,
1139
1254
  `"native default"` or `"native default (explicit)"` (`--model
1140
1255
  @native-default`). Omitting `--model` and asking for the native default are
@@ -1165,6 +1280,20 @@ it to a temporary copy (`soulFetched: true`).
1165
1280
  `payloadRevision` (the merged payloads). `workspace` is the host key;
1166
1281
  `standalone` marks a standalone view. `task` is the task text or `null`.
1167
1282
 
1283
+ **Observation reuse** (feature `spawn-preview-max-age`, OATS 0.33.0).
1284
+ - `--preview --max-age <s>` reuses member heads this machine observed at
1285
+ most `<s>` seconds ago, as the read verbs do ([Observation
1286
+ reuse](#observation-reuse-feature-observe-max-age-oats-0311): the same
1287
+ values, refusals and fallbacks to a live observation). With the flag (`0`
1288
+ included) the result gains `observation: {observedAt, reused,
1289
+ localRevision}`, shaped exactly as the read verbs' block; without it the
1290
+ preview is exactly as before, and no other field changes shape.
1291
+ - `decision.revision` covers the heads the preview used, reused or live.
1292
+ Apply never reuses (it refuses `--max-age`): a head that moved since the
1293
+ reused observation refuses `E_DECISION_STALE`, and the apply records what
1294
+ it observed, so re-preview under `--max-age` to get the new head and
1295
+ revision.
1296
+
1168
1297
  **Provider settings.**
1169
1298
  - `providers` is the `--provider` map as typed.
1170
1299
  - `settings.<cap>`: the merged payload (manifest defaults, then workspace,
@@ -1346,10 +1475,11 @@ the keyed-spawn fields `decision`, `spawnIdempotencyKey`, `spawnCompleted` and
1346
1475
  ### The roster (`oats status --json`)
1347
1476
 
1348
1477
  ```text
1349
- oats status [--dir <d>] --json
1478
+ oats status [--dir <d>] [--max-age <s>] --json
1350
1479
  ```
1351
1480
 
1352
- Not an envelope: `{root, agents, workspace?, problems?, warnings?}`.
1481
+ Not an envelope: `{root, agents, observation?, workspace?, problems?, warnings?}`
1482
+ (`observation` only with [`--max-age`](#observation-reuse-feature-observe-max-age-oats-0311)).
1353
1483
 
1354
1484
  ```json
1355
1485
  {"root":"/w/agents",
@@ -1812,26 +1942,45 @@ accept `--server <id>`.
1812
1942
  {"context":"/w","level":"/w","file":"/w/oats-local.yaml","selected":null,
1813
1943
  "configurations":[{"name":"reviewers","harness":"claude","executable":null,"args":["--permission-mode","plan"],
1814
1944
  "env":{"ANTHROPIC_API_KEY":{"fromEnv":"REVIEW_KEY"},"REVIEW_MODE":{"redacted":true}},
1815
- "model":"opus","yolo":null,"source":"/w/oats-local.yaml","shadows":[]}]}
1945
+ "model":"opus","yolo":null,"default":false,"source":"/w/oats-local.yaml","shadows":[]}]}
1816
1946
  ```
1817
1947
 
1818
1948
  - **list**: `selected` is `null`, `{home, instance}` or `{soul, agentsRoot}`;
1819
1949
  `level` and `file` are `null` without an `oats-local.yaml` (the set is then
1820
1950
  empty). An environment literal is `{redacted: true}`, a reference
1821
- `{fromEnv}`; values never leave the file.
1951
+ `{fromEnv}`; values never leave the file. `default` (0.32, feature
1952
+ `launch-config-default`) is always a boolean: `true` marks this host's
1953
+ default for the configuration's harness.
1822
1954
  - **set**/**remove**: `{name, action, level, file, before, after,
1823
1955
  effective}`. `set --file` takes `{harness, executable?, args?, env?, model?,
1824
- yolo?}` (`-` reads stdin). `--keep-env` keeps the declared environment when
1825
- `env` is omitted. Errors: `E_LOCAL_MISSING`, `E_BAD_ARGS` (including
1826
- `--home`/`--soul`), `E_LAUNCH_CONFIG_UNKNOWN`, `E_LAUNCH_CONFIG_INVALID`,
1827
- `E_CONFIG_BROKEN`, `E_HOME_UNKNOWN`.
1956
+ yolo?, default?}` (`-` reads stdin); it replaces the whole entry and refuses
1957
+ a key it does not know, so an editor sends `default` back to keep it.
1958
+ `default: false` is written as its absence. A second `default: true` for a
1959
+ harness is `E_LAUNCH_CONFIG_INVALID` with `details: {harness,
1960
+ configurations: [<the declared one>, <this one>]}` and nothing is written;
1961
+ moving the default is two writes, never an automatic move. `--keep-env`
1962
+ keeps the declared environment when `env` is omitted. Routed with
1963
+ `--server`, a definition with `default: true` to a host that does not
1964
+ advertise `launch-config-default` is `E_REMOTE_INCOMPATIBLE` before
1965
+ anything is sent (`default: false` is dropped). Errors: `E_LOCAL_MISSING`,
1966
+ `E_BAD_ARGS` (including `--home`/`--soul`), `E_LAUNCH_CONFIG_UNKNOWN`,
1967
+ `E_LAUNCH_CONFIG_INVALID`, `E_CONFIG_BROKEN`, `E_HOME_UNKNOWN`,
1968
+ `E_REMOTE_INCOMPATIBLE`.
1828
1969
  - **preview** (read-only) answers `{context, selected, selection: {source,
1829
1970
  launchConfig, harness, model, yolo}, harness, model, modelSource, yolo,
1830
- launchConfig, launchConfigSource, executable: {path, declared,
1831
- resolvedFrom}, argv, environment: [{name, fromEnv} | {name, redacted:
1971
+ launchConfig, launchConfigSource, launchConfigDefault, executable: {path,
1972
+ declared, resolvedFrom}, argv, environment: [{name, fromEnv} | {name, redacted:
1832
1973
  true} | {name, reference: true}], command (redacted), prompt, hooks,
1833
1974
  preflight: [{check, ok, detail}], ok}`.
1834
1975
 
1976
+ `launchConfigDefault` (0.32) is `true` when `launchConfig` is this host's
1977
+ default for the harness rather than a chosen configuration; the spawn preview
1978
+ carries the same top-level field, `instance.json` records it as
1979
+ `launch.launchConfigDefault: true`, and `inspect --json` of a home answers
1980
+ `instance.launchConfig` and `instance.launchConfigDefault`. The closed `Launch`
1981
+ object is unchanged: its `effective.launchConfig` names the default
1982
+ configuration.
1983
+
1835
1984
  A successful envelope can carry `ok: false`: show the failed `preflight`
1836
1985
  checks. The prompt is named, never the task body. A home predating launch
1837
1986
  recipes answers its frozen command with `selection.source: "frozen-command"`
package/docs/desktop.md CHANGED
@@ -110,6 +110,22 @@ registered remote workspace the timer and definitions live on that server, so
110
110
  they do not depend on the Mac staying awake. See [Schedules](schedules.md) for
111
111
  the CLI, cron semantics, observed outcomes and recovery commands.
112
112
 
113
+ ## Instances on servers
114
+
115
+ Every instance a registered server reports shows in its workspace's roster, whoever spawned it. You
116
+ can open its terminal, start, restart, stop and remove it, and read its readiness, activity, Git
117
+ and diffs, as for a local one. Only its pull request is not read here, since the forge reads this
118
+ computer's clones. Every command goes to the server by the instance's home (`--server <id> --home
119
+ <path>`), never by a bare name. Stop and Remove show the plan the server makes, and confirm
120
+ against it.
121
+
122
+ A read waits for the server: the view says "Reading from <server>…", and gives up after about
123
+ 45 seconds with "Couldn't reach <server>." When the server refuses, you see its code and message;
124
+ nothing is read from this computer in its place. A row that can't be opened says why on the row:
125
+ Herdr no longer supported, gone from <server>, not reachable on <server>, or <server> not reached.
126
+ For an instance a server no longer lists, the reason names the command that removes it from this
127
+ computer (`oats server forget <server> --instance <name>`).
128
+
113
129
  ## Remote terminals
114
130
 
115
131
  A terminal for an instance on a registered server is a viewer over ssh
@@ -80,6 +80,23 @@ Host-owned provider values (absolute paths, state roots) go under `settings:` in
80
80
  `oats-local.yaml` afterwards — never in the workspace file, whose schema refuses
81
81
  them. Do not commit `oats-local.yaml`.
82
82
 
83
+ **Trust the deployment once, for unattended launches.** Claude Code and Codex
84
+ ask before they work in a folder they have not seen, and every instance home is
85
+ new: a launch that stops at that prompt waits for a human. OATS never writes
86
+ the harnesses' configuration, so trust the deployment directory yourself, once
87
+ per harness you use:
88
+
89
+ ```bash
90
+ cd ~/acme && claude # accept the folder-trust prompt, then quit
91
+ cd ~/acme && codex # choose "Trust and continue", then quit
92
+ ```
93
+
94
+ One entry covers every instance home under the deployment
95
+ ([souls-and-instances.md](souls-and-instances.md#unattended-launches-folder-trust)
96
+ says how each harness applies it). Until then, a claude or codex spawn warns
97
+ that its session will stop at the folder-trust prompt, and so does
98
+ `oats readiness`.
99
+
83
100
  ## 3. Give the deployment a team
84
101
 
85
102
  With a messaging capability in the soul's composition, every instance lives in
@@ -117,8 +134,9 @@ oats spawn backend-expert --purpose first-fix --task "Fix one small issue, run t
117
134
  oats status
118
135
  ```
119
136
 
120
- `--harness pi|claude|codex` picks the harness; complete any native folder
121
- trust or authentication prompt in the printed session. The instance home is
137
+ `--harness pi|claude|codex` picks the harness; complete any native
138
+ authentication prompt in the printed session (folder trust is the one-time step
139
+ in section 2). The instance home is
122
140
  `agents/<soul>/instances/<instance>/`; `work/` is its repository view;
123
141
  `.oats/modules/<cap>/` are the copied capabilities and `.agents/skills/<skill>/` their skills;
124
142
  `instance.json` records `modules` (from, commit, digest), `providers` and
@@ -30,7 +30,7 @@ published to npm. Its developer docs are in
30
30
  | `lib/` | the kernel (below) |
31
31
  | `injects/` | the kernel and work-mode instruction blocks composed into every instance |
32
32
  | `skills/` | bootstrap skills shipped with the kernel |
33
- | `capabilities/` | this repository's own member capabilities (`oats-workspace-experts`), discovered at the member's latest state |
33
+ | `capabilities/` | this repository's own member capabilities (`oats-desktop-ui`, `oats-workspace-experts`), discovered at the member's latest state |
34
34
  | `mirrors/` | generated byte mirrors of the official packages' capabilities (for example `oats-okf*`, checked by `scripts/check-okf-mirror.mjs`): release-lane and test material, kept out of `capabilities/` so member discovery does not list them a second time; not shipped in the npm package |
35
35
  | `oats-package/` | the `oats.framework` package |
36
36
  | `souls/` | this repository's own souls (a workspace member) |
@@ -44,6 +44,7 @@ published to npm. Its developer docs are in
44
44
  | module | owns |
45
45
  |---|---|
46
46
  | `remote.mjs` | repo refs, reading Git remotes, content digests |
47
+ | `local-inputs.mjs` | the local configuration a command read, for `observation.localRevision` |
47
48
  | `workspace.mjs` | workspace, membership and soul files; discovery |
48
49
  | `resolve.mjs` | a soul's resolution: capabilities, slots, provenance |
49
50
  | `packages.mjs` | `packages:`, the catalog, `oats sync`, `oats-lock.json` |
@@ -57,11 +58,130 @@ published to npm. Its developer docs are in
57
58
  | `tmux-config.mjs`, `session-*.mjs` | the tmux session backend and terminal input |
58
59
  | `capability-contract.mjs`, `provider-binding.mjs` | manifest validation, the hook environment rules, the readiness wire |
59
60
  | `servers.mjs` | routing commands to a registered server |
61
+ | `harness-trust.mjs` | reading (never writing) Claude's and Codex's folder trust for a launch |
60
62
 
61
63
  The kernel is runtime-neutral: nothing in `lib/` depends on a harness or on
62
64
  a provider. Provider behaviour lives in capabilities; the kernel supplies
63
65
  their contracts ([layers](layers.md)).
64
66
 
67
+ ### The remote read path
68
+
69
+ Every CLI command owns one read session (`createReadSession` in
70
+ `remote.mjs`, carried to every remote call as `remoteOptions.session`; the
71
+ CLI closes it when the command ends). A library caller without a session
72
+ gets the plain per-call behaviour. Within a session:
73
+
74
+ - a head is observed once per (cache repo, ref), and a commit peeled once; at
75
+ most eight observations run at once (`OBSERVE_LIMIT`), each holding its slot
76
+ for all its git work (the `ls-remote` and the fetch of the commit it names,
77
+ or the fetch of a reused record's commit);
78
+ - a whole-workspace discovery prefetches its members' heads together with the
79
+ host's (`prefetchMembers` in `workspace.mjs`): the member list comes from the
80
+ host's last observation record and the parsed `workspace` entry at that
81
+ commit, never from a git process, and the answer still uses the list at the
82
+ host commit observed now. A prefetched failure is adopted by the member's own
83
+ observation, not retried in the command. A prefetch no caller adopts (the
84
+ host could not be observed, or the member was dropped since) is abandoned:
85
+ one still queued runs no git. `observeWorkspace` alone (the `teams` reads,
86
+ `inspect --home`) never prefetches;
87
+ - closing the session (the end of the command, or `process.exit`) rejects
88
+ every queued observation and aborts every git child still running for it
89
+ (the session's `AbortSignal` rides every `runGit`). `runGit` starts git as
90
+ its own process group, so a timeout, an output overflow or an abort kills
91
+ git's ssh or remote helper with it; before a capability
92
+ command runs its provider, the CLI ends the idle batch readers
93
+ (`closeBatches`);
94
+ - a commit's tree is listed once (`git ls-tree -r -t -l`, bounded by
95
+ `TREE_INDEX_BUDGET`; anything odd falls back to the per-path reads), and
96
+ blobs come from one `git cat-file --batch` reader per cache repo (at most
97
+ 12 open, ended through `process-group.mjs` on timeout and at close);
98
+ `fetchRemoteTree` copies a module through it too, once `ensureBlobs` has
99
+ fetched what was missing (git re-reads its packs on a miss, so a reader
100
+ opened earlier finds the new blobs; one still missing answers `missing`,
101
+ never a fetch), with what is left of `TREE_BUDGET` as each read's bound;
102
+ a blob the reader answers `missing` or over its bound is read once more
103
+ alone, so the error is the one a copy without a session gives. A
104
+ command that ends normally awaits the close, so its readers are reaped
105
+ before it exits; a `process.exit` (every refusal) ends them in the
106
+ exit hook (`closeNow`), and the system reaps them once the process is gone;
107
+ - every git child is ended with SIGTERM first and SIGKILL only after a
108
+ grace (`terminateGroup`): git removes its own lock files on SIGTERM, and
109
+ a git killed outright leaves one that blocks every later write. The
110
+ SIGKILL goes to the whole group even when git itself has exited, so a
111
+ descendant that ignores SIGTERM (ssh, a remote helper) still ends; but
112
+ never to a group seen empty, whose id may already lead an unrelated
113
+ group. Until git's `close` (`watchGroup`), a member holding its pipes
114
+ keeps the id ours; after it, the group is probed every 50 ms through the
115
+ grace (no pid is allocated while it is a live group's id): empty, and it
116
+ is never signalled again. git's pipes are drained on a kill, never
117
+ destroyed, so `close` keeps waiting for a pipe-holding descendant. The exit
118
+ hook cannot wait for a timer, so it waits a bounded 200 ms synchronously
119
+ (`reapOnExit`);
120
+ - discovery reads members eight at a time (`DISCOVERY_CONCURRENCY`) with
121
+ serial results: declaration order, the first failure in that order. The
122
+ observations and the member reads are two pools, so a discovery runs at
123
+ most sixteen short-lived git processes at once (eight of them fetches at
124
+ most), plus up to twelve cat-file readers: twenty-eight git processes. One
125
+ shared pool would deadlock: a member read holding a slot waits on its
126
+ member's observation, which needs a slot of its own.
127
+
128
+ The cache repos are partial: a commit is fetched with all its trees and
129
+ only the blobs up to `SMALL_BLOB_LIMIT` (64 KiB), which covers every file
130
+ discovery reads, so listings and discovery stay local after one fetch. A
131
+ read that needs a larger blob, or a `fetchRemoteTree` of a module, fetches
132
+ the missing blobs first in one fetch by id (`ensureBlobs`), then applies the
133
+ budgets to their real sizes before anything is written. git never fetches a
134
+ blob lazily (`GIT_NO_LAZY_FETCH=1`, and no url is stored in the cache: each
135
+ fetch passes it with `-c remote.origin.url=`). A server without partial
136
+ fetches gets whole trees; the cache records that (`oats.fetch = full` in its
137
+ config) and the CLI prints the session's notice once, on stderr. Partial
138
+ caches need git 2.45 or later (`PARTIAL_FETCH_GIT`, the first git with
139
+ `GIT_NO_LAZY_FETCH`): with an older git every cache fetches whole trees, a
140
+ partial cache it meets is deleted and fetched again whole, and the same
141
+ notice says why.
142
+
143
+ Every write to a cache repo (its `git init`, config, fetches and pins) holds
144
+ the repo's cross-process write lock, `<cache>/.locks/<repo>.lock`
145
+ (`withCacheWriteLock`): an exclusive file holding `{pid, token, startedAt}`,
146
+ waited for while its holder lives (bounded by a whole fetch, then
147
+ `reason: "cache"` naming the pid), reclaimed when the holder is dead, and
148
+ released only by its owner. Reclaimers take a short guard,
149
+ `<lock>.reclaim`, and check under it that the lock is still the dead
150
+ record before removing it, so a reclaimer that paused cannot delete a
151
+ live process's new lock. A guard whose holder died is never removed
152
+ automatically (that removal would race the same way, with nothing left to
153
+ serialize it): every write refuses at once, `reason: "cache"` naming the
154
+ guard (`details.guard`), until a human removes it once no oats process is
155
+ running. Reads take no lock. A cache repo appears whole
156
+ (`git init` into a private directory, then a rename), so processes making
157
+ the first fetch of one remote all succeed. A git `*.lock` a write meets is
158
+ judged under that lock (`cacheGit`): older oats kernels take no write lock,
159
+ so it is retried briefly, then removed only when it is inside the cache
160
+ repo, a regular file and older than the longest fetch
161
+ (`GIT_FETCH_TIMEOUT_MS` plus a margin): a git killed mid-write. A removal
162
+ is said once as a warning. Anything else is `reason: "cache"` naming the
163
+ file and when it is safe to remove; so is any other local write failure
164
+ (a `FETCH_HEAD` git cannot open, a read-only or full disk), with git's own
165
+ words.
166
+
167
+ Across commands, `memoAtCommit` keeps parsed reads under
168
+ `<cache>/.parsed/<kernel fingerprint>/`, keyed by (repo key, full commit,
169
+ item). The items: `workspace` (the workspace file), `membership` (a member's
170
+ backlink outcome), `enumerate` (a member's souls and capabilities),
171
+ `package-soul` and `external-soul` (one soul file each; their error handling
172
+ differs), `package-manifests` (a package's manifests), `list` (a skill
173
+ listing) and `tree-oids` (the tree ids of a set of directories). An entry is only ever a pure function of those bytes and this kernel's
174
+ code, never local state and never a transient error; it is written
175
+ atomically, a corrupt one is a miss, and `pruneStores` bounds the store
176
+ (`PARSED_LIMITS`, least recently used first). `--max-age` adds the observation
177
+ store `<cache>/.observed/` ([Observation reuse](desktop-cli-api.md#observation-reuse-feature-observe-max-age-oats-0311)):
178
+ one record per (repo key, ref args, url digest), so two spellings of one repo
179
+ keep a record each; the url itself is never written. Adding a cached item
180
+ means choosing an item name unique to its producer (the item string its
181
+ call site passes to `memoAtCommit`, or to `atCommit` in `workspace.mjs`) and
182
+ adding it to `test/parsed-cache.test.mjs`; `test/read-path-scale.test.mjs` pins the member
183
+ scaling by call count.
184
+
65
185
  ## Tests and gates
66
186
 
67
187
  | command | what it checks |
@@ -41,7 +41,7 @@ arrives from.
41
41
  # oats-workspace.yaml: one default per slot, for every soul
42
42
  packages:
43
43
  oats.okf: v4.0.5
44
- oats.aweb: v1.17.3
44
+ oats.aweb: v1.17.5
45
45
  oats.linear: v1.0.1
46
46
  oats.jira: v1.0.1
47
47
  defaults: