@awebai/oats 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,6 +64,20 @@ their names.
64
64
 
65
65
  ## Resolution
66
66
 
67
+ **Workspace model (0.25, current).** `lib/instance-resolution.mjs#prepareInstance(dir, soul)`
68
+ loads `oats-local.yaml`, discovers the workspace over its Git remotes
69
+ (`lib/workspace.mjs#discoverWorkspace`, or the standalone view), finds the
70
+ soul among the confirmed members / external souls, and calls
71
+ `lib/resolve.mjs#resolveSoul` → an immutable Resolution: `modules[]` (each
72
+ `from: member|package` with commit and digest), `slots`, merged provider
73
+ `payloads`, `skills`, `injects`, `revision`. Capability order is
74
+ `defaults.<slot>` ⊕ `defaults.capabilities` ⊕ `defaults.byTeam[team]` ⊕
75
+ `soul.capabilities` (soul wins; `off` removes; a soul's `<slot>: none` empties
76
+ the slot). `lib/materialize.mjs` then copies every module whole into the home.
77
+ The normative contract is
78
+ [docs/design/2026-09-23-workspace-module-contracts.md](design/2026-09-23-workspace-module-contracts.md).
79
+
80
+ **Classic 0.24 (superseded; still carried for homes without `oats-local.yaml`).**
67
81
  `configChain(context)` loads `oats-config.yaml` from closest scope outward.
68
82
  `resolveCapabilities(context, soulName)`:
69
83
 
@@ -196,14 +210,28 @@ never reconciled into committed souls.
196
210
 
197
211
  ## Acquisition and trust
198
212
 
199
- External installation copies/clones one exact artifact and writes
200
- `oats-lock.json` with source, version/commit, and SHA-256 tree integrity. An
201
- existing destination is never pulled silently. Resolution rejects changed
202
- locked artifacts and unlocked installed/path packages.
213
+ **Workspace model (0.25, current).** Nothing is installed. `oats sync`
214
+ (`lib/packages.mjs#resolvePackages`) resolves every `packages:` entry of
215
+ `oats-workspace.yaml` to a commit, computes the package tree's integrity and
216
+ writes `oats-lock.json` **lockfileVersion 3** (`packages.<id>: { source, url,
217
+ path, version, commit, integrity, capabilities[], approved }`). Executable
218
+ approval is **per package version**, recorded in the lock as
219
+ `approved: { executables: sha256-…, at }` after `oats sync` shows the
220
+ executables and the operator says yes; a spawn of a soul using an unapproved
221
+ package is `E_PACKAGE_UNAPPROVED`, and 0.25.1 re-verifies the approved digest
222
+ against the package tree at the locked commit at every spawn. Member-tier
223
+ capabilities need no approval: membership is the trust (decision 2). The
224
+ verbs `oats install|trust|list|restore|use|migrate` are removed
225
+ (`E_UNKNOWN_COMMAND` naming the replacement).
226
+
227
+ **Classic 0.24 (superseded).** External installation copies/clones one exact
228
+ artifact and writes `oats-lock.json` with source, version/commit, and SHA-256
229
+ tree integrity. An existing destination is never pulled silently. Resolution
230
+ rejects changed locked artifacts and unlocked installed/path packages.
203
231
 
204
232
  Executable package hooks, commands, and launch-environment authority are omitted
205
- until `oats trust <id>` marks the exact locked integrity approved. Bundled
206
- packages are framework-trusted.
233
+ until `oats trust <id>` (0.24) marks the exact locked integrity approved.
234
+ Bundled packages are framework-trusted.
207
235
  Packages under a scope's `owned/` subtree are config-owned. Anything under
208
236
  `installed/` requires a matching lock entry, so an acquired artifact cannot
209
237
  bypass executable trust by its directory location.
@@ -211,7 +239,7 @@ bypass executable trust by its directory location.
211
239
  Distribution packages generalize this: a package materializes each capability it
212
240
  exports into `.agents/capabilities/installed/<id>/`, each independently
213
241
  addressable and independently trusted at its own artifact integrity. There is no
214
- persistent package store. The `lockfileVersion: 2` lock records package
242
+ persistent package store. The 0.24 `lockfileVersion: 2` lock records package
215
243
  provenance (`packages`) and materialized capability identity (`capabilities`)
216
244
  separately. See `docs/design/package-engine-contract.md` for the resolver/lock
217
245
  API and error taxonomy.
@@ -100,9 +100,11 @@ secrets never belong in OATS config. See
100
100
 
101
101
  > **Removed: `oats.web`.** The browser web-panel capability was retired in
102
102
  > favor of the OATS Desktop app (`packages/desktop/`), which bundles the same
103
- > zero-dependency loopback server. If an `oats-lock.json` or
104
- > `oats-config.yaml` still names `oats.web`, remove that entry. Full
105
- > migration steps: [desktop-succession](desktop-succession.md).
103
+ > zero-dependency loopback server. If an `oats-workspace.yaml` (`packages:`,
104
+ > `defaults.capabilities`) or a `soul.yaml` still names `oats.web`, remove that
105
+ > entry and `oats sync`; on a 0.24 classic deployment, remove it from
106
+ > `oats-lock.json` / `oats-config.yaml`. Full migration steps:
107
+ > [desktop-succession](desktop-succession.md).
106
108
 
107
109
  ## Building an integration
108
110
 
@@ -39,17 +39,25 @@ Choose stable base IDs, stable owner IDs, nonoverlapping node paths and durable
39
39
  permissions. Confirm aliases and owners explicitly, rather than deriving them
40
40
  from an instance branch or name.
41
41
 
42
- Configure the absolute `bindings-file` for each source soul. Remove obsolete v1
43
- settings such as `record-window-turns` and `record-window-bytes`; v2 accepts only
44
- `bindings-file`, `harvest-runtime` and `harvest-model`. Provision **empty owned
45
- nodes** using `oats okf init`. Accept Git initialization through a reviewed PR
46
- before migration delivery; directory provisioning requires explicit confirmation
47
- and a genuinely non-Git location.
42
+ Configure the absolute `bindings-file` **and** `state-dir` for each source soul
43
+ — the oats.okf 2.1.x binding requires both as normalized absolute host paths
44
+ (`setting state-dir is required (absolute host path)` is a refusal, not a
45
+ default). Remove obsolete v1 settings such as `record-window-turns` and
46
+ `record-window-bytes`; v2 accepts exactly `bindings-file`, `state-dir`,
47
+ `harvest-runtime` and `harvest-model`. Under the 0.25 workspace model these live
48
+ in `oats-local.yaml` `settings.oats.okf` ([configuration.md](configuration.md));
49
+ a rebuilt deployment gets a **fresh** `state-dir`
50
+ ([rebuild-to-v2.md §7b](rebuild-to-v2.md#7b-okf-2-start-a-fresh-state-dir-do-not-re-point-the-old-one)).
51
+ Provision **empty owned nodes** using `oats okf init`. Accept Git initialization
52
+ through a reviewed PR before migration delivery; directory provisioning requires
53
+ explicit confirmation and a genuinely non-Git location.
48
54
 
49
55
  ## 3. Stage and deliver each legacy bundle
50
56
 
51
- From the durable deployment configuration context in an operator shell without
52
- inherited instance identity, selecting the source soul:
57
+ From the deployment directory (the one holding `oats-local.yaml`) in an operator
58
+ shell without inherited instance identity, selecting the source soul with
59
+ `--soul` — the kernel resolves the command exactly as `oats spawn --soul <x>`
60
+ would ([knowledge.md](knowledge.md#inspection-and-operator-commands)):
53
61
 
54
62
  ```bash
55
63
  oats okf migrate --legacy /absolute/soul/knowledge --base project --node expert --output /absolute/empty-migration-stage --soul domain-expert --json
package/docs/knowledge.md CHANGED
@@ -51,11 +51,13 @@ artifact**: npm drops the source worker's `CLAUDE.md -> AGENTS.md` symlink.
51
51
  Acquire the catalog Git payload; do not install a copied npm mirror as a local
52
52
  package or repair missing aliases in installed artifacts.
53
53
 
54
- With a released OATS >=0.23.0 kernel, acquire published OKF 2.0.0 from the
55
- intended deployment configuration context in an operator shell without inherited
56
- instance identity (an explicit `--soul` does not override an invoking instance's
57
- saved settings). The explicit Git source works before and after the v0.23.1
58
- framework catalog integration:
54
+ Under the 0.25 workspace model OKF is a **package**: pin it once in the
55
+ workspace file, let `oats sync` lock and approve it, and let every soul that
56
+ fills the knowledge slot say (or inherit) `oats.okf: { from: package }`.
57
+ Operator-level `oats okf` commands run from the deployment directory with
58
+ `--soul <name>` (an explicit `--soul` does not override an invoking instance's
59
+ saved settings — use a clean shell). The pinned version resolves through the
60
+ official catalog:
59
61
 
60
62
  ```yaml
61
63
  # oats-workspace.yaml
@@ -72,6 +74,7 @@ knowledge:
72
74
  settings:
73
75
  oats.okf:
74
76
  bindings-file: /absolute/config/okf-bindings.json
77
+ state-dir: /absolute/state/okf
75
78
  ```
76
79
 
77
80
  ```bash
@@ -157,7 +160,9 @@ bindings, owner declarations, base metadata or indexes fail required spawn rathe
157
160
  than silently bootstrapping empty knowledge.
158
161
 
159
162
  Provisioning is an explicit operator action. Prepare node-map files (the
160
- `nodes` object above, without its wrapper), then:
163
+ `nodes` object above, without its wrapper), then run from the **deployment
164
+ directory** (the one holding `oats-local.yaml`), naming the soul whose
165
+ `knowledge:` payload and `settings.oats.okf` the command should run with:
161
166
 
162
167
  ```bash
163
168
  # New directory base: refuses an existing destination.
@@ -166,6 +171,19 @@ oats okf init --base team --nodes /absolute/config/team-nodes.json --confirm --s
166
171
  oats okf init --base project --nodes /absolute/config/project-nodes.json --output /absolute/new-bundle-stage --soul domain-expert --json
167
172
  ```
168
173
 
174
+ These run **before any instance exists**. Outside an instance home the kernel
175
+ resolves `oats okf … --soul <name>` exactly as `oats spawn <name>` would
176
+ (discover → resolve → the soul's `oats.okf` module at its locked, approved
177
+ commit), fetches that module into the deployment's module store
178
+ (`<deployment>/.oats/modules/oats.okf@<commit12>/`) and dispatches to that copy
179
+ with the soul's merged payload as `OATS_SETTINGS`; `--soul` is required
180
+ (`E_BAD_ARGS` names it) unless the namespace's capability is a workspace
181
+ default. It never runs "the newest instance's copy" and never an unapproved
182
+ cache read (`E_PACKAGE_UNAPPROVED` until `oats sync` approves the version).
183
+ *0.25.0 still answers `E_CAPABILITY_INACTIVE` here (the operator-level dispatch
184
+ lands in 0.25.1); the interim is to run the module binary directly with
185
+ `OATS_SETTINGS` and `OATS_CLI_BIN` set, as the tarball smoke does.*
186
+
169
187
  Put the Git proposal at the configured root in an operator-owned checkout and
170
188
  review/merge it through a PR before spawning working sources. Existing ownership
171
189
  changes require an explicit reviewed operator change, not harvest. The standalone
@@ -189,7 +207,7 @@ Snapshots are immutable by protocol, not live mounts. For current accepted text:
189
207
  # From the source home:
190
208
  oats okf read --base project --path expert/index.md --json
191
209
  oats okf refresh --json
192
- # From the deployment context, even after source retirement:
210
+ # From the deployment directory (oats-local.yaml), even after source retirement — --soul selects the resolution:
193
211
  oats okf read --source /absolute/state/sources/UUID/source.json --base project --path expert/index.md --soul domain-expert --json
194
212
  oats okf refresh --source /absolute/state/sources/UUID/source.json --soul domain-expert --json
195
213
  ```
@@ -289,9 +307,17 @@ spawn and command exit alone are not successful learning.
289
307
 
290
308
  ## Inspection and operator commands
291
309
 
292
- Run home-local commands from that source home. For cross-source or retired-source
293
- commands, use the durable deployment context in a clean operator shell without
294
- another instance's `OATS_*`/`PI_*` identity; select the configured source soul.
310
+ Run home-local commands from that source home: inside an instance the
311
+ dispatcher resolves `okf` from the home's materialized module
312
+ (`instance.json.modules` → `<home>/.oats/modules/oats.okf/`). For cross-source
313
+ or retired-source commands, run from the **deployment directory** (the one
314
+ holding `oats-local.yaml`) in a clean operator shell without another instance's
315
+ `OATS_*`/`PI_*` identity, and select the source soul with `--soul <name>`: the
316
+ kernel resolves that soul as a spawn would and dispatches to the deployment's
317
+ copy of its `oats.okf` module with the soul's merged payload (see
318
+ [Acquire, bind and provision explicitly](#acquire-bind-and-provision-explicitly)).
319
+ No `oats-config.yaml` chain is consulted; a namespace no module of the soul
320
+ provides is `E_UNKNOWN_COMMAND`.
295
321
 
296
322
  ```bash
297
323
  # Read-only; no capture, refresh, scheduling or worker launch:
@@ -1,5 +1,15 @@
1
1
  # Migrating from OAS to OATS
2
2
 
3
+ > **0.25 status — this is a 0.22–0.24 procedure.** `oats migrate` and
4
+ > `oats trust` are **removed verbs** in the 0.25 kernel (`E_UNKNOWN_COMMAND`
5
+ > naming the replacement), and 0.25 reads none of the files this page
6
+ > converts to (`oats-config.yaml`, `oats-lock.json` v2, the `installed/` tier).
7
+ > An OAS deployment reaches 0.25 in two steps: run this page's commands with a
8
+ > **0.24.x** kernel (`npm install -g @awebai/oats@0.24`), then rebuild for the
9
+ > workspace model with [rebuild-to-v2.md](rebuild-to-v2.md) — which is a rewrite
10
+ > of three shared files, not a conversion, so an operator comfortable with the
11
+ > v2 declarations may skip straight to it and let the old files go.
12
+
3
13
  OATS is the successor to OAS. **OATS 0.22.0 was published on 2026-09-03**:
4
14
  the kernel, Pi adapter, and Desktop assets are available. The published
5
15
  kernel acquired the official OKF, aweb, authoring, and development packages
@@ -17,15 +27,15 @@ while its knowledge and messaging configuration remains unmigrated.
17
27
  > before activation/spawn. The v2 integration is [prepared](release-notes/v0.23.1.md),
18
28
  > not a claim that those dependencies or any deployment have already changed.
19
29
 
20
- ## Upgrade one scope
30
+ ## Upgrade one scope (0.24.x kernel)
21
31
 
22
32
  Finish or preserve active work before changing a daily-use deployment.
23
- Install OATS alongside the old CLI, then inspect the plan for the exact
24
- scope you intend to convert:
33
+ Install OATS **0.24.x** alongside the old CLI (the 0.25 line has no `oats
34
+ migrate`), then inspect the plan for the exact scope you intend to convert:
25
35
 
26
36
  ```bash
27
- npm install -g @awebai/oats@latest
28
- pi install npm:@awebai/oats-pi@latest
37
+ npm install -g @awebai/oats@0.24
38
+ pi install npm:@awebai/oats-pi@0.24
29
39
  oats migrate --from-oas --dry-run --dir /path/to/scope
30
40
  ```
31
41
 
@@ -39,10 +49,11 @@ oats doctor /path/to/scope
39
49
  ```
40
50
 
41
51
  Run the exact `oats trust <capability> --dir <scope>` commands printed by
42
- migration for the executable capabilities you approve. Trust does not
43
- transfer automatically. Verify the team ID and messaging membership with
44
- `oats aweb setup --dir /path/to/scope`, then exercise a real task, harvest,
45
- and retirement as described in [Run your first team](first-team.md).
52
+ migration for the executable capabilities you approve (0.24: per-artifact
53
+ trust; under 0.25 approval is per package version through `oats sync`). Trust
54
+ does not transfer automatically. Verify the team ID and messaging membership
55
+ with `oats aweb setup --dir /path/to/scope`, then exercise a real task,
56
+ harvest, and retirement as described in [Run your first team](first-team.md).
46
57
 
47
58
  For a multi-repository deployment, start with one scope. The explicit
48
59
  `--recursive --dir /path/to/workspace` form converts every discovered OAS
@@ -16,6 +16,19 @@ schemaVersion 2 only; found 1"`, `E_LOCK_SCHEMA`), never a silent fallback.
16
16
  Keep the 0.24 kernel installed until the last 0.24 deployment you care about is
17
17
  rebuilt; the two do not share files.
18
18
 
19
+ **One thing a 0.25 kernel changes for a classic home it does launch.** Decision
20
+ 13 ("harnesses start normally") is a property of the 0.25 *launcher*, not of the
21
+ v2 files: every `pi` launch a 0.25 kernel performs — `oats spawn`, `oats session
22
+ start|restart`, scheduled runs — starts pi with cwd = the instance home and pi's
23
+ own skill and context discovery intact (`--append-system-prompt <home>/AGENTS.md`,
24
+ no `--no-skills` / `--no-context-files` / `--no-prompt-templates` exclusion).
25
+ That holds for a classic 0.24 home (no `oats-local.yaml`, spawned through the
26
+ pre-v2 compose path that 0.25 still carries) exactly as for a module home. If you
27
+ relied on 0.24's ambient-skill exclusion to hide machine-level or repo-level
28
+ skills from an instance, that isolation is gone the moment a 0.25 kernel
29
+ launches it — keep the 0.24 kernel for those homes, or accept the ambient set
30
+ (the spawn preview lists composed skill names so a clash is visible).
31
+
19
32
  ## 1. Decide the one workspace
20
33
 
21
34
  One workspace per organisation. Pick the repo that **hosts**
@@ -93,6 +106,29 @@ Delete `oats.yaml`. Its `exports:` lists are gone: every `souls/*/soul.yaml` and
93
106
  `capabilities/*/oats.json` is discoverable; add `private: true` to the ones that
94
107
  should stay internal. The host repo backlinks to itself like any member.
95
108
 
109
+ ## 3b. Move the souls: `agents/<name>/soul/` → `souls/<name>/`
110
+
111
+ In 0.24 a repo's souls lived at `agents/<name>/soul/` beside that soul's
112
+ instances. Under v2 discovery looks **only** at `souls/<name>/soul.yaml`; the
113
+ `agents/` directory belongs to the *deployment* (instance homes and, under the
114
+ kernel's per-commit soul cache, the fetched soul copies — see §7b) and is not
115
+ read as a soul source. Move every soul as a tracked rename so history follows:
116
+
117
+ ```bash
118
+ mkdir -p souls
119
+ git mv agents/release-manager/soul souls/release-manager
120
+ # … one line per soul; then
121
+ git rm -r --cached agents 2>/dev/null; echo 'agents/' >> .gitignore # instances were never meant to be tracked
122
+ ```
123
+
124
+ `souls/<name>/` keeps its `AGENTS.md`, `CLAUDE.md → AGENTS.md` alias, `skills/`,
125
+ `knowledge/` and `soul.yaml` (rewritten in §4); the directory name must equal
126
+ `soul.yaml#name`. Then fix whatever enumerates the old path: repo tests, scripts,
127
+ CI checks and any `oats.yaml`-era `exports:` tooling that globbed
128
+ `agents/*/soul/soul.yaml` (`git grep -n 'agents/.*/soul'` finds them) — under v2
129
+ they enumerate `souls/*/soul.yaml`. A soul left under `agents/` is invisible to
130
+ `oats souls` and to `oats spawn`; nothing warns about it.
131
+
96
132
  ## 4. Edit every `soul.yaml` to v2
97
133
 
98
134
  | 0.24 | v2 |
@@ -128,6 +164,24 @@ are required. Capabilities the repo exports live at
128
164
  `capabilities/<name>/oats.json` — the manifest is unchanged; you may add
129
165
  `private: true` / `team:`.
130
166
 
167
+ **Carry `team:` on every soul, or on its repo's membership.** A soul's team is
168
+ `soul.yaml#team`, else `oats-membership.yaml#team`, else *unassigned*
169
+ (`null`). Labels never gate anything, but the kernel addresses provider payload
170
+ by label: an unlabelled soul receives the messaging **base** payload only —
171
+ `workspace.messaging` minus `byTeam`, no `byTeam.<label>` block, and no
172
+ `defaults.byTeam.<label>` capabilities either. If your 0.24 deployment had one
173
+ messaging identity per team (§1), a soul that loses its label silently lands
174
+ outside every team-addressed payload; nothing refuses it. Label the membership
175
+ when a whole repo belongs to one team, and the soul when it does not.
176
+
177
+ **Per-soul memory-harvest opt-out:** not available in OKF 2.1.3 — an OKF 2.1.4
178
+ item. The 2.1.3 `knowledge:` payload admits `owner`, `owns`, `reads` and
179
+ `stores` (plus the kernel-rendered `runtime`/`execution`); there is no key that
180
+ keeps a soul registered for reads while excluding it from harvest. A soul that
181
+ must not be harvested today says `knowledge: none` (no OKF at all for that
182
+ soul) or `oats.okf: off`; do not invent a key — the binding refuses unknown
183
+ payload keys.
184
+
131
185
  ## 5. Write `oats-local.yaml` on each machine
132
186
 
133
187
  ```
@@ -142,12 +196,27 @@ schemaVersion: 2
142
196
  workspace: git:github.com/acme/agents
143
197
  settings: # what used to be `settings:` under capabilities.layers.* in oats-config.yaml
144
198
  oats.okf:
145
- bindings-file: /Users/ana/.oats/okf-bindings.json
146
- state-dir: /Users/ana/.oats/okf
199
+ bindings-file: /Users/ana/.oats/okf-bindings.json # required by the OKF binding: absolute host path
200
+ state-dir: /Users/ana/.oats/okf-state # required by the OKF binding: absolute host path; FRESH for a rebuilt deployment (§7b)
201
+ harvest-runtime: pi # optional: pi | claude | codex (default pi)
202
+ oats.aweb:
203
+ delivery: channel # channel (default) | session — see capabilities/oats-aweb/oats.json#settings.delivery
147
204
  souls:
148
205
  disabled: [data-analyst]
149
206
  ```
150
207
 
208
+ `settings.<cap>` is merged into that capability's payload after the soul's
209
+ slot payload and before `spawn --provider` (decision 14); the keys are the
210
+ capability's own (`oats.json#settings`). For **`oats.okf` 2.1.3** the binding
211
+ requires both `bindings-file` and `state-dir` as normalized absolute host
212
+ paths (`setting state-dir is required (absolute host path)` is a refusal, not a
213
+ default) and accepts `harvest-runtime` / `harvest-model`. For **`oats.aweb`**
214
+ the one machine-level key is `delivery`: `channel` (the native aweb channel
215
+ packages wake the instance; default) or `session` (delivery is external —
216
+ `AWEB_DELIVERY=session`, the host wake broker registers the instance once it
217
+ exists; requires an `aw` that ships `aw wake`). `identity.source` is also legal
218
+ here but see §8 for why it belongs at spawn.
219
+
151
220
  Move host paths from `oats-config.yaml` `settings:` here; the `souls:` blocks of
152
221
  `oats-config.yaml` become `--provider` flags at spawn (step 8). Delete
153
222
  `oats-config.yaml`; it is not read. Do not commit `oats-local.yaml`.
@@ -179,6 +248,42 @@ unapproved, and spawns of souls using it are refused (`E_PACKAGE_UNAPPROVED`)
179
248
  until you run `oats sync` in a terminal and say yes. Member capabilities need no
180
249
  approval: membership is the trust.
181
250
 
251
+ ## 7b. OKF 2: start a FRESH `state-dir` — do not re-point the old one
252
+
253
+ OKF 2 pins each knowledge **owner** to a soul by path: at source registration
254
+ (the `oats.okf` spawn hook) it writes `owners.json` in `state-dir` as
255
+ `{ <owner id>: realpath(<home>/soul) }` and refuses a later registration whose
256
+ owner resolves to a different path (`E_OWNER stable owner ID already identifies
257
+ a different soul in this state namespace`).
258
+
259
+ Under v2 that path is no longer your checkout. `oats spawn` fetches the soul
260
+ from its member repo at the confirmed commit into the deployment's
261
+ **per-commit soul cache**, `agents/<name>/souls/<commit12>/` (immutable once
262
+ written; `agents/<name>/soul` is a kernel-swapped pointer to the current one),
263
+ and the instance's `<home>/soul` links **its own commit's directory** — so the
264
+ realpath the hook pins is `<deployment>/agents/<name>/souls/<commit12>`, which
265
+ never equals the 0.24 pin (`<repo>/agents/<name>/soul`) and changes whenever the
266
+ member moves. Two consequences:
267
+
268
+ - **Do not reuse the 0.24 `state-dir`.** Its `owners.json` pins every owner to
269
+ the old path; the first v2 spawn of each soul would be refused with `E_OWNER`.
270
+ Give the rebuilt deployment a fresh `state-dir` (§5) and a fresh
271
+ `bindings-file` if the old one names the old state root. The old `state-dir`
272
+ is **frozen custody**: read-only history (`oats okf inspect --source
273
+ <old-state>/sources/<id>/source.json …` still works against it), never edited,
274
+ never re-pointed at the new soul path. Accepted knowledge is not affected —
275
+ it lives in the bases, not in `state-dir`.
276
+ - **The owner pin is per commit.** OKF 2.1.3 records the realpath at first
277
+ registration and the kernel keeps that commit directory for as long as any
278
+ instance links it, so a running instance's pin stays valid; a *later* spawn of
279
+ the same soul at a newer member commit links a different directory and
280
+ registers under the same owner id → `E_OWNER` again. Until OKF re-bases the
281
+ pin on the owner identity rather than the path (an OKF 2.1.4 item), the
282
+ practical rule is: one `state-dir` per (deployment, soul commit) is safe;
283
+ moving a member that owns knowledge means a fresh `state-dir` for the new
284
+ commit's spawns (the previous one becomes frozen custody, as above). Plan
285
+ knowledge-owning souls' member commits deliberately.
286
+
182
287
  ## 8. Re-take a retained messaging seat with `spawn --provider`
183
288
 
184
289
  In 0.24, an instance-specific messaging identity (a retained seat) was pinned in
@@ -186,14 +291,22 @@ In 0.24, an instance-specific messaging identity (a retained seat) was pinned in
186
291
  **spawn**:
187
292
 
188
293
  ```bash
189
- oats spawn release-manager --purpose seat --provider oats.aweb identity.source=retained:release-seat
294
+ oats spawn release-manager --purpose seat --provider oats.aweb identity.source=/abs/path/to/retained/.aw
190
295
  ```
191
296
 
192
297
  `--provider <cap> key=value` is repeatable; dotted keys nest. The payload is
193
298
  merged after the soul's `messaging:` and the machine's `settings.oats.aweb`, and
194
299
  recorded in `instance.json.providers.oats.aweb`, so exactly one instance holds
195
- the seat while other instances of the soul mint fresh identities. Consult your
196
- messaging capability's documentation for the exact key it reads. The Desktop's
300
+ the seat while other instances of the soul mint fresh identities.
301
+
302
+ **The value is the path itself.** `oats.aweb` reads `identity.source` as the
303
+ absolute path of the `.aw` directory to retain (it must hold `signing.key`); the
304
+ kernel does not resolve symbolic seat names. Because it is an absolute path it is
305
+ a fact about ONE machine, so its other legal home is `oats-local.yaml`
306
+ (`settings.oats.aweb.identity.source: /abs/path`) — never the workspace file
307
+ (absolute paths are refused there, decision 14). Prefer the spawn form: a
308
+ machine-level setting would give the seat to EVERY instance of every messaging
309
+ soul on that machine, and a seat can be held once. The Desktop's
197
310
  confirmed apply carries the same map.
198
311
 
199
312
  ## 9. Spawn, and check drift
@@ -212,6 +325,7 @@ oats status # per instance: modules … [member moved since (n
212
325
  |---|---|
213
326
  | `oats-config.yaml` (and the laptop/workspace/repo config chain, `agent-types`, `capabilities.layers`/`additive`, `souls:`, adopted config templates) | `oats-workspace.yaml` defaults + `soul.yaml` `capabilities:`; `oats-local.yaml` for host settings; `spawn --provider` for per-instance facts |
214
327
  | `oats.yaml` | `oats-membership.yaml` |
328
+ | `agents/<name>/soul/` as the tracked soul source | `souls/<name>/` (tracked); `agents/` is deployment state — instance homes and the kernel's per-commit soul cache `agents/<name>/souls/<commit12>/` |
215
329
  | `.agents/capabilities/installed/` and `owned/` | nothing is installed; `<instance>/.oats/modules/<cap>/` per instance; member capabilities under `<repo>/capabilities/` |
216
330
  | `oats init`, `oats use`, `oats install`, `oats restore`, `oats trust`, `oats list`, `oats catalog`, `oats remove`, `oats migrate`, `oats config` | `oats sync`, `oats package add \| remove`, `oats workspace status`, `oats capabilities`, `oats souls` — each removed verb answers `E_UNKNOWN_COMMAND` naming its replacement |
217
331
  | lock v1 / v2 | lock v3 (`packages` only, with `url`, `capabilities`, `approved`) |
@@ -0,0 +1,94 @@
1
+ # OATS v0.25.1 — workspace-model fix round
2
+
3
+ Kernel/Pi **0.25.1**. Tag `v0.25.1` → the commit carrying these notes; the
4
+ version-bump commit lands after the tag. Consumers gate on `oats version --json`
5
+ `features[]` names and API integers — never on the version.
6
+
7
+ **No API change.** `workspaceApi: 2`, every other API integer and the
8
+ `features[]` list are exactly those of [0.25.0](v0.25.0.md). Every item below
9
+ is a correctness, security or documentation fix found by the team review of
10
+ the 0.25.0 workspace model; the normative record is the "0.25.1 fix round"
11
+ section of `docs/design/2026-09-23-workspace-module-contracts.md` and the
12
+ matching clarifications in
13
+ `agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md`.
14
+
15
+ ## Kernel
16
+
17
+ - **M1 (high) — a running instance's soul no longer changes under it.** Souls
18
+ are fetched into a per-commit cache `agents/<name>/souls/<commit12>/`
19
+ (immutable); `agents/<name>/soul` is an atomically swapped pointer to the
20
+ current commit; each home links its own commit's directory. A 0.25.0 layout
21
+ is migrated in place on first use. OKF's path-pinned owner stays valid per
22
+ instance.
23
+ - **M2 — SSH remotes are fetched over SSH.** The canonical repo key is
24
+ unchanged; the fetch url honours the ref as written (`git@…`/`ssh://` → SSH,
25
+ `https://` → HTTPS, bare `git:` → HTTPS unless `remoteOptions.transport:
26
+ ssh`). A private repo is no longer probed over HTTPS and silently degraded to
27
+ the standalone view; the standalone fallback now reports the host failure
28
+ that triggered it.
29
+ - **M3 (high) — package approval is re-verified at spawn.** `resolveSoul`
30
+ recomputes the executables digest over the package tree at the locked commit
31
+ and refuses `E_PACKAGE_UNAPPROVED { reason: "digest-mismatch" }` when it
32
+ differs from the approved one; one shared `executablesDigestAt` serves
33
+ `sync` and `resolve`.
34
+ - **M4 — annotated tag OIDs are peeled.** `observeRemote` records the peeled
35
+ commit, never a tag object, in results, locks and `instance.json`.
36
+ - **B2 — `work: workspace` spawns on a v2 deployment.** `./work` is the
37
+ deployment directory (the one holding `oats-local.yaml`); no branch recorded;
38
+ the remedy names `oats-local.yaml`.
39
+ - **B3 — operator-level capability commands from the deployment.**
40
+ `oats <ns> <cmd> … --soul <name>` outside a home resolves exactly as a spawn
41
+ of that soul, fetches the module into `<deployment>/.oats/modules/<cap>@<commit12>/`
42
+ and dispatches there with the soul's merged payload (`oats okf init` before
43
+ any instance exists). `--soul` absent → `E_BAD_ARGS`; unknown namespace →
44
+ `E_UNKNOWN_COMMAND`.
45
+ - **L1 — slot `none` empties the slot.** A soul's `knowledge|messaging|tasks:
46
+ none` drops any layer-bearing capability the workspace defaults contributed
47
+ for that layer; only a layer-bearing capability the soul itself declares next
48
+ to `none` is `E_SLOT_CONFLICT`.
49
+ - **L2 — absolute-path refusal is scoped to ref/path fields.** Team
50
+ descriptions and the opaque messaging payload may contain `/`-rooted text.
51
+ - **L3 — one unsafe deep entry no longer blanks a member's souls.** Depth
52
+ filtering precedes the entry-name safety check.
53
+ - **L4 — listing failures are classified.** `maxBuffer` overflow is not
54
+ reported as `timeout`; an unclassified git listing failure becomes a
55
+ discovery problem row (`E_REMOTE_UNREADABLE { reason: "unknown" }`) instead
56
+ of an abort.
57
+ - **L6 — revision splits declarations from payload.** `revision =
58
+ hash(declRevision, payloadRevision)`; decision binding unchanged; preview can
59
+ report `changed since: declarations | payload | both`.
60
+
61
+ ## Documentation
62
+
63
+ - **M5 — rebuild guide gaps closed** (`docs/rebuild-to-v2.md`): the tracked
64
+ `git mv agents/<name>/soul souls/<name>` step and the tests that enumerate
65
+ soul paths; OKF 2 owner re-registration (fresh `state-dir` for a rebuilt
66
+ deployment, the old one frozen custody); `oats-local.yaml` example with
67
+ `settings.oats.aweb.delivery` and `settings.oats.okf` `state-dir` +
68
+ `bindings-file`; unlabelled souls receive the messaging base payload only;
69
+ per-soul memory-harvest opt-out is not available in OKF 2.1.3 (an OKF 2.1.4
70
+ item).
71
+ - **L7 — decision 13 reach.** Every `pi` launch a 0.25 kernel performs starts
72
+ the harness normally, classic 0.24 homes included (rebuild guide §0,
73
+ `conventions.md`). `oats session recompose` is `E_UNSUPPORTED_MODE` for
74
+ module homes; `session-recompose` stays advertised for classic homes
75
+ (`desktop-cli-api.md`).
76
+ - **L8 — v1 no longer presented as live** in `schedules.md`, `conventions.md`,
77
+ `implementation.md`, `execution-targets.md`, `desktop.md`,
78
+ `desktop-succession.md`, `integrations.md`, `migration-from-oas.md`
79
+ (a 0.24.x procedure), `desktop-cli-api.md` (readiness producers are the 0.24
80
+ tier), `knowledge-migration.md` (`state-dir` is required; four settings) and
81
+ `knowledge.md` (operator-level `oats okf … --soul <x>` from the deployment).
82
+
83
+ ## Known follow-ups (unchanged from 0.25.0 unless noted)
84
+
85
+ - The classic (no `oats-local.yaml`) spawn path still runs the pre-v2 compose;
86
+ `composeInstance` still reads `yolo` / `launch-configs` from an
87
+ `oats-config.yaml` chain when one sits above a deployment.
88
+ - The readiness quartet (`readinessApi: 1`) is still produced by the 0.24 tier
89
+ observers; re-basing it on `spawn --preview` / `sync` / `workspace status` is
90
+ a named follow-up.
91
+ - OKF 2's owner pin is per soul path (hence per commit under M1); re-basing it
92
+ on the owner identity is an OKF 2.1.4 item, as is a per-soul harvest opt-out.
93
+ - `oats-local.yaml` `transport:` (M2's per-machine SSH default) needs a schema
94
+ addition before it can be written.
package/docs/schedules.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # Schedules
2
2
 
3
3
  A schedule launches an agent, runs an oats command, or wakes an existing
4
- instance on a cron. Definitions belong to a scope, the team workspace (the
5
- config level that declares the team, else the outermost `oats-config.yaml`
6
- level), and are committable; every `oats schedule` command run anywhere
7
- inside that scope, including from an instance home, reads and writes the
8
- same file. Execution belongs to the host that holds the scope, so a
9
- schedule on a registered server keeps running while your laptop sleeps.
4
+ instance on a cron. Definitions belong to a scope and are committable; every
5
+ `oats schedule` command run anywhere inside that scope, including from an
6
+ instance home, reads and writes the same file. On a **workspace deployment**
7
+ (0.25, [workspaces.md](workspaces.md)) the scope is the deployment directory
8
+ — the one holding `oats-local.yaml` and the `agents/` root (the kernel derives
9
+ it as the directory above the agents root; a leftover `oats-config.yaml` that
10
+ declares `team:` would still win, so remove it); scheduled spawns there
11
+ materialize exactly like `oats spawn`. On a classic 0.24 deployment the scope
12
+ is the team workspace (the config level that declares the team, else the
13
+ outermost `oats-config.yaml` level). Execution belongs to the host that holds
14
+ the scope, so a schedule on a registered server keeps running while your laptop
15
+ sleeps.
10
16
 
11
17
  There is no daemon. One host timer (a launchd user agent on macOS, a systemd
12
18
  user timer on Linux) runs `oats schedule tick --host` once a minute; the tick
@@ -176,7 +176,7 @@ instructions and pins model/provider settings; it excludes nothing.
176
176
  ```bash
177
177
  oats spawn release-manager --purpose cut-3.2 --task "…" # a soul of a confirmed member
178
178
  oats spawn release-manager --preview --json # decide everything, create nothing
179
- oats spawn release-manager --provider oats.aweb identity.source=retained:release-seat # instance-level payload
179
+ oats spawn release-manager --provider oats.aweb identity.source=/abs/path/to/retained/.aw # instance-level payload
180
180
  ```
181
181
 
182
182
  From a deployment (where `oats-local.yaml` is), a spawn: reads the local file →
@@ -208,7 +208,7 @@ Examples of spawn hooks:
208
208
  registers a durable source plus its per-source schedule definition. Missing
209
209
  knowledge is an error, not permission to bootstrap an empty substitute.
210
210
  - `oats.aweb` mints a messaging identity — or, with
211
- `--provider oats.aweb identity.source=retained:<seat>`, re-takes a retained
211
+ `--provider oats.aweb identity.source=/abs/path/of/the/.aw/to/retain`, re-takes a retained
212
212
  one for exactly this instance.
213
213
 
214
214
  ### Work
@@ -346,7 +346,7 @@ store** — it organises and can supply defaults. The messaging provider's paylo
346
346
  |---|---|---|
347
347
  | True of every instance of the soul | `soul.yaml` → `knowledge:` / `messaging:` / `tasks:` | `knowledge: { owns: release-manager }` |
348
348
  | A fact about this machine | `oats-local.yaml` → `settings.<cap>.<key>` (absolute paths are refused in the workspace file) | `settings.oats.okf.state-dir: /Users/ana/.oats/okf` |
349
- | A fact about **this spawn** | `oats spawn … --provider <cap> key=value` (repeatable; dotted keys nest) → `instance.json.providers.<cap>` | `--provider oats.aweb identity.source=retained:release-seat` |
349
+ | A fact about **this spawn** | `oats spawn … --provider <cap> key=value` (repeatable; dotted keys nest) → `instance.json.providers.<cap>` | `--provider oats.aweb identity.source=/abs/path/to/retained/.aw` |
350
350
 
351
351
  The merged payload is `workspace.messaging` (messaging slot only; its base
352
352
  keys ⊕ `byTeam[<soul's team>]`, with `byTeam` itself stripped) ⊕ soul slot
@@ -421,6 +421,13 @@ ref>` — the kernel notices it is a member whose workspace it cannot read and
421
421
  falls back to the standalone view — or `standalone: <repo ref>` to ask for
422
422
  that view explicitly).
423
423
 
424
+ **Executables from public members.** Membership is the trust (decision 2): a
425
+ member capability's hooks and command scripts run on every operator's machine at
426
+ spawn, gated by nothing but the handshake. In a mixed public/private
427
+ organisation keep **souls only** in public members and let executable
428
+ capabilities come from packages (approved per version in the lock) or from
429
+ private members.
430
+
424
431
  **Hosting the workspace file when some members are private.** Everyone who
425
432
  can read the workspace file sees the member list. So: a public member never
426
433
  hosts it when any member is private (it would publish the private repo's