@awebai/oats 0.32.0 → 0.34.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.
@@ -38,9 +38,10 @@ 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","observe-max-age"],
41
+ "preview-composed-from","observe-max-age","spawn-preview-max-age","capability-show"],
42
42
  "automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
43
- "readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2}
43
+ "readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2,
44
+ "capabilityShowApi":1}
44
45
  ```
45
46
 
46
47
  - The Desktop accepts `desktopApi === 1` and a released `version` inside
@@ -96,6 +97,8 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
96
97
  | `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
98
  | `preview-composed-from` | `composedFrom` on preview `modules[]` ([Composition](#the-preview)) | |
98
99
  | `observe-max-age` | `--max-age <s>` on the read verbs and their `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311)) | |
100
+ | `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)) | |
101
+ | `capability-show` | `oats capabilities show <name>` and its `--file` form, OATS 0.34.0 ([`oats capabilities show`](#oats-capabilities-show)) | `capabilityShowApi: 1` |
99
102
 
100
103
  Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
101
104
  `workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
@@ -194,6 +197,8 @@ it and answer live, without the block.
194
197
  ```text
195
198
  oats status | workspace status | souls | capabilities | inspect --soul|--home
196
199
  | teams | soul teams <soul> … --max-age <seconds> --json
200
+ oats spawn <soul> … --preview --max-age <seconds> --json (feature spawn-preview-max-age)
201
+ oats capabilities show <name> … --max-age <seconds> --json (feature capability-show)
197
202
  ```
198
203
 
199
204
  - **Values:** whole seconds, `0` to `86400`. `0` is live: it reuses nothing.
@@ -206,7 +211,15 @@ oats status | workspace status | souls | capabilities | inspect --soul|--home
206
211
  everywhere; `reused` is `true` when any head came from an earlier observation.
207
212
  A command that read no remote head reports the time it started and
208
213
  `reused: false`. Without the flag the key is absent and every document is
209
- exactly as before.
214
+ exactly as before. A refusal (`E_SOUL_UNKNOWN`, any error envelope) never
215
+ carries the block.
216
+ - **Spawn preview** (feature `spawn-preview-max-age`, OATS 0.33.0): the
217
+ preview's `result` gains the same block, so you can say "as of
218
+ `<observedAt>`". The `decision` covers the heads the preview used, reused
219
+ or live: a reused head the member has since moved from makes the apply
220
+ (which always observes live) refuse `E_DECISION_STALE`, and the apply
221
+ records the head it observed, so the next preview under `--max-age` shows
222
+ it with a new `decision.revision`. The apply itself refuses the flag.
210
223
  - **`localRevision`:** 24 lowercase hex characters, opaque. It digests every
211
224
  piece of local configuration the kernel read for this answer:
212
225
  `oats-local.yaml` (and each closer `oats-local.yaml` it looked for and did
@@ -237,12 +250,14 @@ oats status | workspace status | souls | capabilities | inspect --soul|--home
237
250
  spelling of the same repository (ssh vs https) or for a different ref; its
238
251
  commit can no longer be fetched. Each is observed live, as without the flag.
239
252
  A live observation that fails is the usual error, never an older head.
240
- - **Refusals:** every other command, every edit form (`teams add|remove|default`,
253
+ - **Refusals:** every other command, a spawn apply (with or without
254
+ `--expect-decision`; the form named is `spawn`), every edit form (`teams add|remove|default`,
241
255
  `soul teams --add|--remove|--default|--clear-default`) and any `--server`
242
256
  invocation refuse the flag before reading or writing anything, with
243
257
  `E_BAD_ARGS` "--max-age is not accepted by \`oats <form>\`: only the read
244
258
  verbs reuse observations (status, workspace status, souls, capabilities,
245
- inspect --soul|--home, and the read forms of teams and soul teams)" and,
259
+ capabilities show, inspect --soul|--home, spawn --preview, and the read
260
+ forms of teams and soul teams)" and,
246
261
  with `--server`, "--max-age cannot be combined with --server: observation
247
262
  reuse is local to this machine".
248
263
  A capability command's argv (`oats <namespace> …`) is its provider's: the
@@ -523,7 +538,25 @@ Feature `workspace-v2`, `workspaceApi: 2`. Model: [workspaces.md](workspaces.md)
523
538
  `E_LOCAL_MISSING {dir, searched}`.
524
539
  - The workspace is read over Git remotes with the operator's credentials,
525
540
  never prompting: `E_REMOTE_UNREADABLE {url, reason: "auth" | "not-found" |
526
- "network" | "timeout"}`.
541
+ "network" | "timeout" | "killed" | "cache" | "unknown"}`. `killed` (the
542
+ system killed git, for example out of memory) also carries `signal`.
543
+ `cache` (OATS 0.33.0) is local: the
544
+ remote cache on this machine could not be written (a git lock still held,
545
+ another oats process still writing it, or a lock file one left when it
546
+ died); `details.cacheDir` and, when known, `details.lock`,
547
+ `details.guard` or `details.holderPid` say which, and the message says
548
+ what to do.
549
+ - **How the Desktop reads it** (0.33.0). Of an `E_REMOTE_UNREADABLE` from
550
+ `status` / `workspace status`, the Desktop keeps the `message` (shown as
551
+ given) and only a bounded cause: `details.reason` (matching
552
+ `^[a-z][a-z-]{0,31}$`) and the host of `details.url`. No path, pid, lock,
553
+ `cacheDir` or other detail field crosses to the renderer. It keys only on
554
+ `code` + `details.reason`: `cache` words the roster "OATS cache
555
+ problem" with the message in full; `network` / `timeout` read "Couldn't
556
+ reach <host>"; any other reason keeps the generic wording. When the
557
+ deployment was observed before, the failed read keeps that observation:
558
+ `/api/panel` serves it with `error` (the message) and `errorCause {code,
559
+ reason, host?}`, and the roster shows it stale instead of empty.
527
560
  - There is no package approval: declaring a package is the trust decision.
528
561
  No payload carries `approvalNeeded`, `approval` or `approved`.
529
562
  - A **standalone view** is a member repository whose workspace is not read
@@ -704,6 +737,14 @@ Read-only (it writes no lock):
704
737
  ```
705
738
 
706
739
  - `members[]` and `packages[]` are the sync rows (packages from the lock).
740
+ - **Remote budget (0.33.1).** `oats workspace status` and `oats status` finish
741
+ their remote work within 12 s of their first remote read, whatever the
742
+ machine's load or another process holding a remote's cache. A member not
743
+ read by then is a `cannot-read` row whose `detail` ends `(timeout)`, and its
744
+ git is ended; the command still answers. The workspace definition itself
745
+ (the host) not read by then fails the command as any unreadable host does
746
+ (`E_REMOTE_UNREADABLE`, `reason: "timeout"`). Spawn, sync and every other
747
+ verb have no such budget.
707
748
  - `declaredPackages`: the ids in `packages:` (standalone: the kernel's
708
749
  default). `unsynced`: declared, not locked. `stale`: locked, no longer
709
750
  declared. `external[]`: `{source, soul}`.
@@ -780,6 +821,175 @@ problem`, and (feature `launch-preference`) `key` and `launch`.
780
821
  This document keeps `soulsApi: 1`; the probe's `soulsApi: 2` is the inspect
781
822
  soul row's.
782
823
 
824
+ ### `oats capabilities show`
825
+
826
+ Feature `capability-show`, `capabilityShowApi: 1`, OATS 0.34.0.
827
+
828
+ ```text
829
+ oats capabilities show <name> [--member <repoKey> | --package <id>] [--dir <d>] [--max-age <s>] --json
830
+ oats capabilities show <name> [--member <repoKey> | --package <id>] --file <path> [--dir <d>] [--max-age <s>] --json
831
+ ```
832
+
833
+ What one capability ships: its inject text and each skill's files, and one
834
+ file's text on request. The Desktop's capability page shows them without
835
+ reading clones or caches itself.
836
+
837
+ > **Every `text` and `description` is untrusted repository content.** Render
838
+ > it as plain text (`textContent`), or through a sanitising Markdown renderer
839
+ > that allows no raw HTML, no scripts and no remote images.
840
+
841
+ **Selection.** The rows are exactly the rows of `oats capabilities --json`
842
+ (one discovery, honouring `--max-age`), so the answer's `commit` equals that
843
+ row's `commit`.
844
+
845
+ - `<name>` alone selects the one row with that name.
846
+ - `--member <repoKey>` selects a member row of that repository: the key as a
847
+ row shows it, or any ref spelling of the same repository.
848
+ - `--package <id>` selects that package's row.
849
+ - No row: `E_CAPABILITY_UNKNOWN`, `details: {name}` plus `member` or
850
+ `package` when one was given. An unsynced package is not in the catalog, so
851
+ its capabilities are `E_CAPABILITY_UNKNOWN` until `oats sync`.
852
+ - More than one row: `E_CAPABILITY_AMBIGUOUS`, `details: {name, candidates}`,
853
+ each candidate `{kind, repoKey, origin}` (member) or `{kind, package,
854
+ origin}` (package). Choose one with `--member` or `--package`.
855
+ - `E_BAD_ARGS` for `--member` with `--package`, a missing or second name, an
856
+ unknown flag, and `--server` (the verb reads this machine's workspace only).
857
+ An unknown word after `oats capabilities` (`oats capabilities foo`) is
858
+ `E_BAD_ARGS` too.
859
+
860
+ **Trust path.** Every read goes through the remote cache at a full commit id;
861
+ nothing reads a working clone.
862
+
863
+ - A member capability is read from its member repository at the row's commit.
864
+ - A package capability is read at the locked commit, the same trust path
865
+ spawn uses. First comes spawn's lock check: when the package manifest at
866
+ that commit does not list the capability, or its capability list differs
867
+ from the lock's, the show refuses `E_PACKAGE_INTEGRITY` with spawn's
868
+ details. The full-tree content digest is not recomputed per show: `oats
869
+ sync` proved the lock's integrity over the tree of exactly that commit, and
870
+ the commit id content-addresses the tree.
871
+
872
+ **The show:**
873
+
874
+ ```json
875
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","repoKey":"github.com/awebai/oats-okf","package":"oats.okf","version":"4.0.5",
876
+ "commit":"26d8216f…","path":"oats-package/capabilities/oats-okf",
877
+ "inject":{"path":"injects/okf.md","bytes":2422,"text":"## Knowledge: OKF\n\nYou have two kinds of knowledge. …","binary":false,"truncated":false},
878
+ "skills":[{"name":"okf-consultation","path":"skills/okf-consultation","description":"Consulting your soul's knowledge with the `oats okf` CLI: …",
879
+ "files":[{"path":"skills/okf-consultation/SKILL.md","bytes":6947},{"path":"skills/okf-consultation/references/consult.md","bytes":4465}],
880
+ "filesTruncated":false},
881
+ {"name":"okf-instance-knowledge","path":"skills/okf-instance-knowledge","description":"Keeping this instance's own knowledge …",
882
+ "files":[{"path":"skills/okf-instance-knowledge/SKILL.md","bytes":4787}],"filesTruncated":false}],
883
+ "problems":[]}
884
+ ```
885
+
886
+ - `kind` is `member` or `package`. `repoKey` is set for both kinds; for a
887
+ package it is the repository the package is read from. `package` and
888
+ `version` are the package id and locked version, `null` for a member.
889
+ - `commit` is 40 hex and equals the catalog row's `commit`. `path` is the
890
+ capability directory, repository-relative.
891
+ - `inject` is `{path, bytes, text, binary, truncated}` or `null`. `skills`
892
+ is a list of `{name, path, description, files, filesTruncated}`, each file
893
+ `{path, bytes}`, or `null`. `problems` is a list of `{code, message,
894
+ path}`.
895
+ - With `--max-age` (`0` included) both the show and the `--file` answer gain
896
+ the [`observation`](#observation-reuse-feature-observe-max-age-oats-0311)
897
+ block `{observedAt, reused, localRevision}`.
898
+
899
+ **The `--file` answer:**
900
+
901
+ ```json
902
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"26d8216f…",
903
+ "file":{"path":"skills/okf-instance-knowledge/SKILL.md","bytes":4787,"text":"---\nname: okf-instance-knowledge\n…","binary":false,"truncated":false}}
904
+ ```
905
+
906
+ **Rules.**
907
+
908
+ - **Paths.** Every `path` in `inject`, `skills`, `file` and `problems` is
909
+ POSIX and relative to the capability directory, never the repository.
910
+ Every non-null `path` is a safe relative path: no `.`, `..` or `.git`
911
+ component (any case), no empty component, no `\`. A manifest's inject and
912
+ skill paths are reported as the module install reads them: a `\` is a
913
+ separator, and a leading `./` and trailing slashes are dropped
914
+ (`injects\guide.md` is `injects/guide.md`).
915
+ - **The inject** is the committed file exactly: untrimmed and untemplated.
916
+ (Spawn composes it raw and trimmed, with no settings substitution.)
917
+ - `inject: null`: the manifest declares no inject.
918
+ - Declared but unreadable (missing, a symlink, a directory, over the read
919
+ budget): `inject: {path, bytes: null, text: null, binary: false,
920
+ truncated: false}` and a `problems[]` entry with the remote's code
921
+ (`E_REMOTE_PATH_MISSING`, `E_REMOTE_TREE_UNSAFE`, `E_REMOTE_FILE_OVERSIZE`,
922
+ …). The show still answers ok.
923
+ - Declared as a path that is not a safe relative path (spawn refuses it):
924
+ `inject: {path: null, bytes: null, text: null, binary: false, truncated:
925
+ false}` and a problem with spawn's code (`E_CAPABILITY_MISSING` for a
926
+ member, `E_PACKAGE_MANIFEST` for a package) and `path: null`. The raw
927
+ manifest value appears only inside `message`, JSON-quoted. So
928
+ `inject.path` is `null` only with a problem.
929
+ - **Skills** are in the catalog row's order (by name, in codepoint order),
930
+ enumerated exactly as a spawn enumerates them. `skills` is `null` exactly
931
+ when the catalog row's `skills` is `null`, with a problem carrying spawn's
932
+ code (`E_CAPABILITY_MISSING` or `E_PACKAGE_MANIFEST`) or an `E_REMOTE_*`
933
+ code, and `path: null`. A skill whose path is not safe (a `.git`
934
+ directory) makes the skills unlistable the same way, in both answers.
935
+ - **Files.** `files` is every regular file under the skill directory,
936
+ recursively (no symlinks, no submodules), sorted by path in codepoint order.
937
+ At most 200 per skill; beyond that `filesTruncated` is `true`. `bytes` is
938
+ the blob size. When a skill's files cannot be listed (an unsafe entry name
939
+ in the tree, an unreadable remote), `files` is `null`, `filesTruncated` is
940
+ `false`, and a problem's `path` is the skill's `path`: "could not list"
941
+ never collapses into "listed nothing".
942
+ - **`description`** is the `description` key of SKILL.md's leading `---` YAML
943
+ front matter, when it is a string. It is parsed from the whole SKILL.md,
944
+ not from its 262144-byte text cut. It is `null` when the front matter is
945
+ absent or does not parse, the key is absent or not a string, or SKILL.md is
946
+ unreadable or binary (no problem is reported for it). It is cut to at most
947
+ 1024 UTF-8 bytes on a code point boundary.
948
+ - **Text.** A file is `binary: true, text: null` when it contains a NUL byte
949
+ or is not valid UTF-8. Only the first 262144 + 3 bytes are examined, so the
950
+ cut is decided on the same bytes; when the file is longer, a valid sequence
951
+ they end inside of is not held against it. Otherwise `text` is the content cut to at
952
+ most 262144 UTF-8 bytes on a code point boundary, with `truncated: true`
953
+ when cut. A byte order mark is kept in the text. `bytes` is always the real
954
+ size.
955
+ - **Invariants** (pinned for the Desktop):
956
+ - `binary: true` ⇒ `text: null` and `truncated: false` (`truncated` is a
957
+ text-only flag).
958
+ - `truncated: true` ⇒ `text` is a string and `binary: false`.
959
+ - `bytes` is `null` only for an unreadable declared inject (with its
960
+ problem). In a `--file` answer it is always an integer.
961
+ - **Large files.** A file over the read budget (4 MiB) is listed with its
962
+ size. `--file` refuses it with `E_REMOTE_FILE_OVERSIZE`, passed through
963
+ unchanged: its `details.path` is repository-relative, not
964
+ capability-relative.
965
+
966
+ **`--file <path>`** reads one file the show lists, and nothing else.
967
+
968
+ - A syntactically unsafe path is `E_CAPABILITY_FILE_UNSAFE`, `details:
969
+ {path}`, before anything is read: absolute, empty, a `.`, `..` or `.git`
970
+ component (any case), an empty component, a trailing slash, a `\` or a NUL.
971
+ - Otherwise the path must be the inject's `path` or a path in some skill's
972
+ `files` as the show lists it (the 200 cap included). Anything else is
973
+ `E_CAPABILITY_FILE_UNKNOWN`, `details: {path, name}`. No other file of the
974
+ capability (`oats.json`, scripts, `bin/`) is readable through this verb.
975
+ - A file the show does not list (beyond the 200 cap, a symlink, under a skill
976
+ whose files cannot be listed) is `E_CAPABILITY_FILE_UNKNOWN` by design:
977
+ show "not available", not an error.
978
+ - A listed file the remote cannot read answers the remote's own code
979
+ (`E_REMOTE_FILE_OVERSIZE`, `E_REMOTE_PATH_MISSING`, `E_REMOTE_TREE_UNSAFE`,
980
+ …).
981
+
982
+ **Failures.** Every failure is exactly one error envelope on stdout with a
983
+ nonzero exit, as for every command: `E_BAD_ARGS`, `E_CAPABILITY_UNKNOWN`,
984
+ `E_CAPABILITY_AMBIGUOUS`, `E_PACKAGE_INTEGRITY`, `E_CAPABILITY_FILE_UNSAFE`,
985
+ `E_CAPABILITY_FILE_UNKNOWN`, an `E_REMOTE_*` code, and the workspace's own
986
+ (`E_LOCAL_MISSING`, `E_LOCK_SCHEMA`, …).
987
+
988
+ **Without `--json`** the show prints a short listing for an operator: the
989
+ inject's path and size, each skill with its files and sizes, and the
990
+ problems. `--file` prints the text; a binary file prints a one-line note
991
+ instead, and a truncated file prints its text followed by a one-line note.
992
+
783
993
  <a id="desktop-facts-feature-desktop-facts-oats-0290"></a>
784
994
  ### Desktop facts
785
995
 
@@ -1162,7 +1372,7 @@ the result with `oats inspect --soul <name> --json`.
1162
1372
  ### The preview
1163
1373
 
1164
1374
  ```text
1165
- oats spawn <soul> [the flags of a real spawn] --preview --json
1375
+ oats spawn <soul> [the flags of a real spawn] --preview [--max-age <s>] --json
1166
1376
  ```
1167
1377
 
1168
1378
  Feature `spawn-preview-2`, `spawnPreviewApi: 2`. The preview runs every
@@ -1250,6 +1460,20 @@ it to a temporary copy (`soulFetched: true`).
1250
1460
  `payloadRevision` (the merged payloads). `workspace` is the host key;
1251
1461
  `standalone` marks a standalone view. `task` is the task text or `null`.
1252
1462
 
1463
+ **Observation reuse** (feature `spawn-preview-max-age`, OATS 0.33.0).
1464
+ - `--preview --max-age <s>` reuses member heads this machine observed at
1465
+ most `<s>` seconds ago, as the read verbs do ([Observation
1466
+ reuse](#observation-reuse-feature-observe-max-age-oats-0311): the same
1467
+ values, refusals and fallbacks to a live observation). With the flag (`0`
1468
+ included) the result gains `observation: {observedAt, reused,
1469
+ localRevision}`, shaped exactly as the read verbs' block; without it the
1470
+ preview is exactly as before, and no other field changes shape.
1471
+ - `decision.revision` covers the heads the preview used, reused or live.
1472
+ Apply never reuses (it refuses `--max-age`): a head that moved since the
1473
+ reused observation refuses `E_DECISION_STALE`, and the apply records what
1474
+ it observed, so re-preview under `--max-age` to get the new head and
1475
+ revision.
1476
+
1253
1477
  **Provider settings.**
1254
1478
  - `providers` is the `--provider` map as typed.
1255
1479
  - `settings.<cap>`: the merged payload (manifest defaults, then workspace,
@@ -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) |
@@ -48,6 +48,7 @@ published to npm. Its developer docs are in
48
48
  | `workspace.mjs` | workspace, membership and soul files; discovery |
49
49
  | `resolve.mjs` | a soul's resolution: capabilities, slots, provenance |
50
50
  | `packages.mjs` | `packages:`, the catalog, `oats sync`, `oats-lock.json` |
51
+ | `capability-show.mjs` | `oats capabilities show`: one catalog row's inject and skill files, read at its commit |
51
52
  | `materialize.mjs` | copying modules into a home and composing it |
52
53
  | `core.mjs` | spawn, retire, sessions, hooks, launch recipes, instance metadata |
53
54
  | `instruction-composition.mjs` | the generated `AGENTS.md` |
@@ -58,6 +59,7 @@ published to npm. Its developer docs are in
58
59
  | `tmux-config.mjs`, `session-*.mjs` | the tmux session backend and terminal input |
59
60
  | `capability-contract.mjs`, `provider-binding.mjs` | manifest validation, the hook environment rules, the readiness wire |
60
61
  | `servers.mjs` | routing commands to a registered server |
62
+ | `harness-trust.mjs` | reading (never writing) Claude's and Codex's folder trust for a launch |
61
63
 
62
64
  The kernel is runtime-neutral: nothing in `lib/` depends on a harness or on
63
65
  a provider. Provider behaviour lives in capabilities; the kernel supplies
@@ -93,7 +95,42 @@ gets the plain per-call behaviour. Within a session:
93
95
  - a commit's tree is listed once (`git ls-tree -r -t -l`, bounded by
94
96
  `TREE_INDEX_BUDGET`; anything odd falls back to the per-path reads), and
95
97
  blobs come from one `git cat-file --batch` reader per cache repo (at most
96
- 12 open, killed through `process-group.mjs` on timeout and at close);
98
+ 12 open, ended through `process-group.mjs` on timeout and at close);
99
+ `fetchRemoteTree` copies a module through it too, once `ensureBlobs` has
100
+ fetched what was missing (git re-reads its packs on a miss, so a reader
101
+ opened earlier finds the new blobs; one still missing answers `missing`,
102
+ never a fetch), with what is left of `TREE_BUDGET` as each read's bound;
103
+ a blob the reader answers `missing` or over its bound is read once more
104
+ alone, so the error is the one a copy without a session gives. A
105
+ command that ends normally awaits the close, so its readers are reaped
106
+ before it exits; a `process.exit` (every refusal) ends them in the
107
+ exit hook (`closeNow`), and the system reaps them once the process is gone;
108
+ - a session may have a `deadline` (`READ_REMOTE_BUDGET_MS`, 12 s after it
109
+ starts): the CLI gives one to `status` and `workspace status` only
110
+ (`readBudgetMs`; `OATS_READ_REMOTE_BUDGET_MS` overrides it for tests). Every
111
+ remote step then gets what is left of it instead of its own default: each
112
+ git call's timeout (`sessionExec`: ls-remote, fetch, ls-tree, the cache's
113
+ plumbing; none starts once nothing is left), the git version probe
114
+ (`readVersion`), the batch readers' answers, the cache write lock's wait,
115
+ the half-initialised cache's wait and the lock-race backoff. What the
116
+ deadline ends is a `timeout` (a peel or version it ended is never read as a
117
+ missing commit or an older git), so an unread member degrades as any
118
+ unreadable one. A cut wait never changes what it judges: past the deadline
119
+ no lock is taken or reclaimed (live, stale or unreadable), and a cache
120
+ directory waited for less than in full is not taken for a crash's leftover;
121
+ - every git child is ended with SIGTERM first and SIGKILL only after a
122
+ grace (`terminateGroup`): git removes its own lock files on SIGTERM, and
123
+ a git killed outright leaves one that blocks every later write. The
124
+ SIGKILL goes to the whole group even when git itself has exited, so a
125
+ descendant that ignores SIGTERM (ssh, a remote helper) still ends; but
126
+ never to a group seen empty, whose id may already lead an unrelated
127
+ group. Until git's `close` (`watchGroup`), a member holding its pipes
128
+ keeps the id ours; after it, the group is probed every 50 ms through the
129
+ grace (no pid is allocated while it is a live group's id): empty, and it
130
+ is never signalled again. git's pipes are drained on a kill, never
131
+ destroyed, so `close` keeps waiting for a pipe-holding descendant. The exit
132
+ hook cannot wait for a timer, so it waits a bounded 200 ms synchronously
133
+ (`reapOnExit`);
97
134
  - discovery reads members eight at a time (`DISCOVERY_CONCURRENCY`) with
98
135
  serial results: declaration order, the first failure in that order. The
99
136
  observations and the member reads are two pools, so a discovery runs at
@@ -102,6 +139,45 @@ gets the plain per-call behaviour. Within a session:
102
139
  shared pool would deadlock: a member read holding a slot waits on its
103
140
  member's observation, which needs a slot of its own.
104
141
 
142
+ The cache repos are partial: a commit is fetched with all its trees and
143
+ only the blobs up to `SMALL_BLOB_LIMIT` (64 KiB), which covers every file
144
+ discovery reads, so listings and discovery stay local after one fetch. A
145
+ read that needs a larger blob, or a `fetchRemoteTree` of a module, fetches
146
+ the missing blobs first in one fetch by id (`ensureBlobs`), then applies the
147
+ budgets to their real sizes before anything is written. git never fetches a
148
+ blob lazily (`GIT_NO_LAZY_FETCH=1`, and no url is stored in the cache: each
149
+ fetch passes it with `-c remote.origin.url=`). A server without partial
150
+ fetches gets whole trees; the cache records that (`oats.fetch = full` in its
151
+ config) and the CLI prints the session's notice once, on stderr. Partial
152
+ caches need git 2.45 or later (`PARTIAL_FETCH_GIT`, the first git with
153
+ `GIT_NO_LAZY_FETCH`): with an older git every cache fetches whole trees, a
154
+ partial cache it meets is deleted and fetched again whole, and the same
155
+ notice says why.
156
+
157
+ Every write to a cache repo (its `git init`, config, fetches and pins) holds
158
+ the repo's cross-process write lock, `<cache>/.locks/<repo>.lock`
159
+ (`withCacheWriteLock`): an exclusive file holding `{pid, token, startedAt}`,
160
+ waited for while its holder lives (bounded by a whole fetch, then
161
+ `reason: "cache"` naming the pid), reclaimed when the holder is dead, and
162
+ released only by its owner. Reclaimers take a short guard,
163
+ `<lock>.reclaim`, and check under it that the lock is still the dead
164
+ record before removing it, so a reclaimer that paused cannot delete a
165
+ live process's new lock. A guard whose holder died is never removed
166
+ automatically (that removal would race the same way, with nothing left to
167
+ serialize it): every write refuses at once, `reason: "cache"` naming the
168
+ guard (`details.guard`), until a human removes it once no oats process is
169
+ running. Reads take no lock. A cache repo appears whole
170
+ (`git init` into a private directory, then a rename), so processes making
171
+ the first fetch of one remote all succeed. A git `*.lock` a write meets is
172
+ judged under that lock (`cacheGit`): older oats kernels take no write lock,
173
+ so it is retried briefly, then removed only when it is inside the cache
174
+ repo, a regular file and older than the longest fetch
175
+ (`GIT_FETCH_TIMEOUT_MS` plus a margin): a git killed mid-write. A removal
176
+ is said once as a warning. Anything else is `reason: "cache"` naming the
177
+ file and when it is safe to remove; so is any other local write failure
178
+ (a `FETCH_HEAD` git cannot open, a read-only or full disk), with git's own
179
+ words.
180
+
105
181
  Across commands, `memoAtCommit` keeps parsed reads under
106
182
  `<cache>/.parsed/<kernel fingerprint>/`, keyed by (repo key, full commit,
107
183
  item). The items: `workspace` (the workspace file), `membership` (a member's
@@ -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:
@@ -11,8 +11,8 @@ or workspace membership alone does not make a package official.
11
11
  |---|---|---|---|
12
12
  | `oats.framework` | `oats-framework/v1.4.1` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
13
13
  | `oats.okf` | `v4.0.5` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
14
- | `oats.aweb` | `v1.17.3` | `oats.aweb` (messaging) | |
15
- | `oats.engineering` | `v1.3.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
14
+ | `oats.aweb` | `v1.17.5` | `oats.aweb` (messaging) | |
15
+ | `oats.engineering` | `v1.4.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
16
16
  | `oats.authoring` | `v1.0.3` | `oats.authoring` | |
17
17
  | `oats.jira` | `v1.0.1` | `oats.jira` (tasks) | |
18
18
  | `oats.linear` | `v1.0.1` | `oats.linear` (tasks) | |
package/docs/packages.md CHANGED
@@ -76,7 +76,7 @@ members:
76
76
  packages:
77
77
  oats.framework: v1.4.1
78
78
  oats.okf: v4.0.5
79
- oats.aweb: v1.17.3
79
+ oats.aweb: v1.17.5
80
80
  teams:
81
81
  platform: { team: "platform:acme.aweb.ai", description: Platform engineering }
82
82
  defaults:
@@ -134,7 +134,7 @@ Declaring a package in the workspace's `packages:` is the trust decision
134
134
  ## `oats package add | remove`
135
135
 
136
136
  ```bash
137
- oats package add oats.aweb v1.17.3 # a catalog version
137
+ oats package add oats.aweb v1.17.5 # a catalog version
138
138
  oats package add acme.tools git:github.com/acme/tools@v0.4.0
139
139
  oats package remove acme.tools
140
140
  ```