@awebai/oats 0.24.12 → 0.25.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.
Files changed (52) hide show
  1. package/bin/oats.mjs +936 -2822
  2. package/docs/capabilities.md +136 -323
  3. package/docs/configuration.md +68 -533
  4. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +19 -14
  5. package/docs/design/2026-09-16-portable-migration-evidence.md +2 -0
  6. package/docs/design/2026-09-16-portable-onboarding.md +4 -2
  7. package/docs/design/2026-09-20-redesign-program-board.md +1 -1
  8. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +2 -0
  9. package/docs/design/2026-09-23-simplified-workspace-model.md +711 -0
  10. package/docs/design/2026-09-23-workspace-module-contracts.md +309 -0
  11. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +65 -0
  12. package/docs/design/README.md +20 -8
  13. package/docs/design/operations-contract.md +1 -0
  14. package/docs/design/package-engine-contract.md +1 -1
  15. package/docs/design/package-runtime-api.md +1 -1
  16. package/docs/desktop-cli-api.md +386 -6
  17. package/docs/desktop-succession.md +3 -4
  18. package/docs/first-team.md +107 -224
  19. package/docs/implementation.md +6 -4
  20. package/docs/integrations.md +45 -44
  21. package/docs/knowledge-capability-authoring.md +10 -4
  22. package/docs/knowledge-migration.md +5 -4
  23. package/docs/knowledge-reference/package-craft.md +11 -3
  24. package/docs/knowledge.md +24 -8
  25. package/docs/layers.md +3 -3
  26. package/docs/oats-local.schema.json +50 -0
  27. package/docs/oats-membership.schema.json +23 -0
  28. package/docs/oats-workspace.schema.json +133 -48
  29. package/docs/official-marketplace.md +9 -6
  30. package/docs/packages.md +229 -440
  31. package/docs/rebuild-to-v2.md +233 -0
  32. package/docs/release-notes/v0.24.13.md +51 -0
  33. package/docs/release-notes/v0.25.0.md +99 -0
  34. package/docs/soul.schema.json +41 -68
  35. package/docs/souls-and-instances.md +175 -108
  36. package/docs/workspace-adoption.md +70 -345
  37. package/docs/workspaces.md +429 -119
  38. package/lib/core.mjs +419 -55
  39. package/lib/instance-resolution.mjs +312 -0
  40. package/lib/materialize.mjs +580 -0
  41. package/lib/packages.mjs +501 -1273
  42. package/lib/remote.mjs +639 -0
  43. package/lib/resolve.mjs +576 -0
  44. package/lib/schedule.mjs +194 -34
  45. package/lib/workspace.mjs +635 -0
  46. package/package.json +1 -1
  47. package/lib/portable-migration-artifacts.mjs +0 -135
  48. package/lib/portable-migration-evidence.mjs +0 -305
  49. package/lib/portable-migration-store.mjs +0 -199
  50. package/lib/portable-migration.mjs +0 -104
  51. package/lib/portable-onboarding-acceptance.mjs +0 -66
  52. package/lib/setup-expert-source.mjs +0 -100
@@ -18,7 +18,7 @@ prints exactly one JSON object on stdout:
18
18
  ```
19
19
 
20
20
  `version` is the installed package's exact semver (e.g. `0.20.0`).
21
- Desktop 0.24 accepts `desktopApi === 1` and semver `>=0.22.0 <0.25.0`
21
+ Desktop 0.25 accepts `desktopApi === 1` and semver `>=0.22.0 <0.26.0`
22
22
  (the earlier Desktop 0.23 band was `>=0.22.0 <0.24.0`). This admits the paired
23
23
  0.24 CLI without changing Desktop API v1. It does not establish complete captured
24
24
  UI, backend, plugin, retirement or recovery parity; capability checks and explicit
@@ -58,8 +58,10 @@ infers a field that is not there:
58
58
  since 0.24.8: `{spawn: boolean}`, see readiness policy).
59
59
  - `provenance: { kind, source, revision, path, workspaceRevision } | null` —
60
60
  where this soul copy came from, as recorded by the kernel when it created
61
- it (`oats onboard` records `packaged-definition` or
62
- `exported-edition-copy`). Souls created before 0.24.7 or authored by hand
61
+ it (the 0.24 bootstrap recorded `packaged-definition` or
62
+ `exported-edition-copy`; the 0.25 `oats onboard` creates no soul and records
63
+ nothing here — see [`oats onboard`](#oats-onboard-onboardapi-2)).
64
+ Souls created before 0.24.7 or authored by hand
63
65
  read `null`; render that as *unrecorded*, not as local or as anything else.
64
66
  - `readiness` — the soul's **declared sources**, joined against
65
67
  `result.capabilities[]` from the same payload. Distinct from launchability
@@ -245,7 +247,7 @@ torn lines and cleared claims silently. API 2:
245
247
 
246
248
  Desktop passes `--limit` (50|100|200) only; `--since` remains a human flag.
247
249
 
248
- ## Schedule run history (`scheduleApi: 2`, OATS 0.24.8+) — K8
250
+ ## Schedule run history (`scheduleApi: 2`, `scheduleHistoryApi: 2` → **3**, OATS 0.24.8+) — K8
249
251
 
250
252
  `oats schedule show|list --json` entries gain **`recentRuns`**: the last 50
251
253
  settled runs (newest first) — every `lastRun` the scheduler recorded once its
@@ -259,6 +261,67 @@ Schedules view (frame 08) renders `recentRuns` as the recent-runs list and the
259
261
  transcript pointer as the handoff; captured-policy definitions are preserved
260
262
  as they are (definition fields are untouched by this addition).
261
263
 
264
+ ### History API 3 (`scheduleHistoryApi: 3`, feature `schedule-read-2`, OATS 0.24.13+) — K8b
265
+
266
+ Gate a Desktop history read on **both** `scheduleHistoryApi === 3` and
267
+ `"schedule-read-2"` in `features[]` (`scheduleApi` stays 2 — mutation verbs are
268
+ unchanged). API 2's reader keyed runs by outcome, read state files whole and
269
+ unchecked, echoed a stored `definition.id` without checking it, and named a
270
+ `transcript` that no reader backs. API 3:
271
+
272
+ - **Run identity is time, not outcome.** `runId = sha256(scheduledFor|startedAt|attemptId)[0:24]`.
273
+ A run's later facts update its one row; `transitions[]` keeps the outcome
274
+ sequence (`["started","unknown","ended"]`); `settled: boolean`
275
+ (`pending: true` is never settled); `recordedAt`. Pre-API-3 rows are returned
276
+ with `runId: null, legacy: true, settled: null, transitions: null` and are
277
+ never merged. `lastRun` carries the same `runId` as its history row.
278
+ - **Bounded, descriptor-safe state.** `oats-schedules.json` and
279
+ `.agents/schedules/state.json` are `lstat`ed (regular file only), opened
280
+ `O_NOFOLLOW|O_NONBLOCK`, `fstat`-verified (dev+ino), and read whole **only
281
+ within a 1 MiB budget** — over budget is a typed `E_SCHEDULE_STATE_OVERSIZE`
282
+ refusal with `details.source`, never truncated JSON. `list`/`show` carry
283
+ `integrity: {sources: [{path: "definitions"|"state", status: "ok"|"absent"|"refused"|"oversize"|"corrupt", bytes}]}`.
284
+ History is capped at 50 rows **at read** (`history: {status, stored, truncated}`);
285
+ one job's corrupt history (`history.status: "corrupt"`, `recentRuns: []`) or
286
+ bad identity (`unreadable: {code, message}`) never fails the other jobs in `list`.
287
+ - **Subject truth.** `list` and `show` echo `scope` (the resolved schedule-owning
288
+ workspace) and canonical `id`. A definition whose own `id` differs from its
289
+ key → `E_SCHEDULE_IDENTITY` (`details.key`, `details.declared`). IDs must match
290
+ `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$` (`E_BAD_ARGS` otherwise, before any read).
291
+ - **Session provenance, never a transcript.** The `transcript` key is gone.
292
+ Each run (and `lastRun`) carries
293
+ `session: {instance: string|null, home: string|null, incarnation: string|null, server: string|null, delivery: "launched"|"delivered-active"|"none"}`
294
+ — facts the recorder had at write time (an active-session wake names the
295
+ instance only when the input result did; `incarnation` = the home's
296
+ `instance.json.createdAt` at record time; `server` = the answering peer for a
297
+ remote command). **There is no reader behind this block**: a consumer renders
298
+ provenance and a precise unavailable reason. A read-only transcript verb is a
299
+ separate seam (K12), not implied by this API.
300
+
301
+ ```
302
+ {"scope":"/abs/ws","scheduleApi":2,"scheduleHistoryApi":3,
303
+ "integrity":{"sources":[{"path":"definitions","status":"ok","bytes":812},{"path":"state","status":"ok","bytes":4410}]},
304
+ "schedules":[{"id":"nightly","scope":"/abs/ws","scheduleApi":2,"scheduleHistoryApi":3,…,
305
+ "history":{"status":"ok","stored":7,"truncated":false},
306
+ "recentRuns":[{"runId":"3f…","scheduledFor":"<iso>","startedAt":"<iso>","outcome":"ended","settled":true,"transitions":["started","ended"],"recordedAt":"<iso>",
307
+ "session":{"instance":"dev-1","home":"/abs/home","incarnation":"<iso>","server":null,"delivery":"launched"}}]}],
308
+ "scheduler":{…}}
309
+ ```
310
+
311
+ **Exact shapes (API 3):**
312
+ - `schedule list --json` → `result = {scope, scheduleApi: 2, scheduleHistoryApi: 3, integrity, schedules: Entry[], scheduler}`.
313
+ - `schedule show <id> --json` → `result = {schedule: Entry}` (one level of nesting; `integrity` is NOT on `show` — it is a scope fact reported by `list`).
314
+ - `Entry` (readable) = definition fields (`id, kind, home, message|operation, cron, enabled, …`) + `{scope, scheduleApi: 2, scheduleHistoryApi: 3, executionStatus, nextRun: ISO|null, lastRun: Run|null, history, recentRuns: Run[], running: boolean, attempt?, pendingWake?}`.
315
+ - `Entry` (unreadable, `list` only) = `{id, scope, scheduleApi: 2, scheduleHistoryApi: 3, unreadable: {code, message}, history: {status: "corrupt", stored: null, truncated: false}, recentRuns: []}` — no definition fields.
316
+ - `history` = `{status: "ok", stored: integer, truncated: boolean}` | `{status: "corrupt", stored: null, truncated: false}`.
317
+ - `Run` (API 3 row) = producer-written fields (`scheduledFor, startedAt, kind, outcome, …`) + `{runId: string, legacy: false, settled: boolean, recordedAt: ISO, transitions: string[], session}`; `transitions[]` elements are outcome strings in write order, first element = the first recorded outcome.
318
+ - `Run` (legacy row) = producer-written fields + `{runId: null, legacy: true, settled: null, transitions: null, session}` — no `recordedAt`, no `key`.
319
+ - `Run` (corrupt element) = `{runId: null, legacy: true, corrupt: true}` only.
320
+ - `session` = `{instance: string|null, home: string|null, incarnation: ISO|null, server: string|null, delivery: "launched"|"delivered-active"|"none"}`; always present on rows and on `lastRun`.
321
+ - `runId` is opaque to consumers: never recompute, never dedup client-side.
322
+ - **Refusals** (`ok:false`): `E_SCHEDULE_STATE_OVERSIZE` / `E_SCHEDULE_INVALID` carry `error.details.source = {path, status, bytes}` (+ `field`); `E_SCHEDULE_IDENTITY` carries `error.details.key` and `error.details.declared`; `E_BAD_ARGS` (id shape) carries no details. A refusal has no `integrity` block — `list` refuses as a whole only when a scope file itself is unreadable.
323
+ - **Open path** (both files): `lstat` → regular file → `open(O_RDONLY|O_NOFOLLOW|O_NONBLOCK)` → `fstat` regular + same dev/ino + `size ≤ 1 MiB` → read exactly `fstat.size` bytes by descriptor (a file that grows past the budget between lstat and fstat is refused, never partially read).
324
+
262
325
  ## Spawn preview (`oats spawn … --preview`, `spawnPreviewApi: 1`, OATS 0.24.8+)
263
326
 
264
327
  The Spawn modal's fields are backed by the kernel's own decision, taken **before
@@ -422,6 +485,9 @@ is the first-run readiness view (frame 09) and the Capabilities readiness rows
422
485
  deployment (no `workspace:` in `oats.yaml`), `unknown` until admission is
423
486
  verified against the workspace observation, `pass`/`fail` when it is. Never
424
487
  login, never team registration; "Skip" leaves it not-applicable, never pass.
488
+ *(The readiness producer still reads the 0.24 `oats.yaml` backlink; under the
489
+ workspace model membership is `oats-membership.yaml` observed by
490
+ `oats workspace status` — re-basing this item is an open thread.)*
425
491
  - Subject: `--soul <name>` scopes required items to the soul's declared
426
492
  requirements; without it, to the scope's active capabilities.
427
493
 
@@ -619,14 +685,328 @@ location. The receipt says so:
619
685
 
620
686
  ### Feature advertisement — gate every new command on the probe
621
687
 
622
- `oats version --json` `features` now lists: `catalog`, `instance-git`,
688
+ `oats version --json` `features` now lists: `instance-git`,
623
689
  `instance-git-remote`, `souls-declarations`, `lifecycle-plans`,
624
690
  `retire-retention`, `readiness`, `spawn-preview`, `instance-events`,
625
691
  `schedule-history`, and carries the API integers (`instanceGitApi`, `soulsApi`,
626
692
  `lifecycleApi`, `readinessApi`, `spawnPreviewApi`, `eventsApi`,
627
693
  `scheduleHistoryApi`). **Gate on these, never on a version string and never by
628
694
  optimistic invocation**: an older CLI ignores an unknown `--plan` on `retire`
629
- and *retires*. Absent feature → the view is unavailable.
695
+ and *retires*. Absent feature → the view is unavailable. (`catalog` was the
696
+ 0.24 `oats catalog` verb's flag; the verb is removed under the workspace model
697
+ and the flag is no longer advertised — the official catalog is reached through
698
+ `packages:` + `oats sync`, not a command.)
699
+
700
+ ## Workspace model (`workspaceApi: 2`)
701
+
702
+ Features: **`workspace-v2`** (the declaration files, `sync`, `package`,
703
+ `workspace status`, `capabilities`, `souls`; `init`/`use`/`install`/`restore`
704
+ removed), **`instance-modules`** (`instance.json.modules` / `providers` /
705
+ `workspace`; `status --json` module drift; preview `modules[]`),
706
+ **`spawn-provider-payload`** (`oats spawn … --provider <cap> k=v`). The probe
707
+ carries `workspaceApi: 2`. Model: [workspaces.md](workspaces.md).
708
+
709
+ Every command below needs a deployment with `oats-local.yaml` (walked up from
710
+ `--dir`/cwd) — else `E_LOCAL_MISSING { dir, searched[] }` — and reads the
711
+ workspace over Git remotes with the operator's credentials, never prompting
712
+ (`E_REMOTE_UNREADABLE { url, reason: "auth"|"not-found"|"network"|"timeout" }`).
713
+ Every `commit` is a full 40-hex OID; every digest is `sha256-<hex>`; every
714
+ `at`/`observedAt` is ISO-8601 UTC. Repo keys are canonical
715
+ (`github.com/org/repo`; `local/<abs-path>` for file remotes).
716
+
717
+ ### Removed verbs answer `E_UNKNOWN_COMMAND` with a replacement
718
+
719
+ `install`, `restore`, `init`, `use`, `trust`, `list`, `catalog`, `remove`,
720
+ `migrate`, `config` — checked before capability dispatch, both modes:
721
+
722
+ ```json
723
+ {"schemaVersion":1,"ok":false,"error":{"code":"E_UNKNOWN_COMMAND","message":"unknown command \"install\" — removed by the workspace model v2; use oats sync","details":{"removed":"install","replacement":"oats sync"}}}
724
+ ```
725
+
726
+ ### oats onboard (onboardApi 2)
727
+
728
+ `oats onboard [<dir>] --workspace <repo ref> [--json]`
729
+
730
+ The **bootstrap** of a deployment (decision 9): realizes a workspace on this
731
+ machine in the taught `<name>-workspace/` layout. It writes
732
+ `<dir>/oats-local.yaml` (`{ schemaVersion: 2, workspace: <ref> }`), creates
733
+ `<dir>/agents/` (the instance homes), then runs exactly the `oats sync` body
734
+ over the directory just written — discover over the remotes, confirm
735
+ membership, resolve `packages:`, approve (TTY) or list what needs approval,
736
+ write `oats-lock.json`. It installs nothing, creates no soul, spawns nothing
737
+ and writes no `oats-config.yaml`; the member clones and the setup-expert spawn
738
+ are printed as next steps. `<dir>` defaults to cwd; `--dir <d>` is the same
739
+ argument (give it once). `--workspace` is required and must be a ref
740
+ `lib/remote.mjs` parses (`E_REPO_REF`) — checked **before** anything is
741
+ written. Captured selectors are refused (`E_BAD_ARGS`).
742
+
743
+ ```json
744
+ {"onboardApi":2,
745
+ "local":"/abs/acme-workspace/oats-local.yaml","dir":"/abs/acme-workspace","agents":"/abs/acme-workspace/agents",
746
+ "lock":"/abs/acme-workspace/oats-lock.json",
747
+ "sync":{"syncApi":1,"…":"the full sync report (next section)"},
748
+ "hosting":{"host":"github.com/acme/agents","hostIsMember":true,
749
+ "rule":"If any member is private, host oats-workspace.yaml in a private repo that is not a public member (a dedicated <org>/workspace repo); public contributors then use the standalone case (from: here capabilities + oats.core)."},
750
+ "next":{"clone":[{"key":"github.com/acme/agents","name":"agents","url":"https://github.com/acme/agents.git","dir":"/abs/acme-workspace/agents-repo"},
751
+ {"key":"github.com/acme/platform","name":"platform","url":"https://github.com/acme/platform.git","dir":"/abs/acme-workspace/platform"}],
752
+ "spawn":"oats spawn oats-setup-expert --dir /abs/acme-workspace"}}
753
+ ```
754
+
755
+ - `sync` is the `syncApi: 1` report of the first sync (members, packages,
756
+ changes, `approvalNeeded`, `problems`); `lock` is the lock it wrote.
757
+ - **Exit `2` with `ok: true`** when `sync.approvalNeeded` is non-empty (approval
758
+ is interactive-only; tell the operator to run `oats sync --dir <dir>` in a
759
+ terminal). Exit `0` otherwise.
760
+ - `hosting` states decision 26 (the kernel cannot see forge visibility, so it
761
+ reports `hostIsMember` and the rule rather than judging).
762
+ - `next.clone[]` is one row per **confirmed** member (`url` = what the
763
+ workspace's `members:` ref resolves to; `dir` = `<dir>/<name>`, or
764
+ `<dir>/agents-repo` for a member called `agents`, since `agents/` is the
765
+ instance homes). `next.spawn` is the setup-expert spawn command string.
766
+ - Errors (all `E_*`): `E_BAD_ARGS` (usage; missing `--workspace`; dir given
767
+ twice), `E_REPO_REF`, `E_ALREADY_ONBOARDED { local, dir }` — **this**
768
+ directory already has `oats-local.yaml` (an enclosing deployment's file does
769
+ not count; run `oats sync` there instead), `E_ONBOARD_FAILED { dir }`
770
+ (cannot inspect/write the directory; `<dir>` exists and is not a directory).
771
+ Failures while **discovering** — `E_REMOTE_UNREADABLE`, `E_WORKSPACE_SCHEMA`
772
+ (not a workspace host), `E_LOCAL_MISSING`, plus `E_PACKAGE_MISSING` /
773
+ `E_PACKAGE_INTEGRITY` / `E_LOCK_SCHEMA` and the other `sync` errors raised
774
+ before the workspace was read — roll back the files onboarding created and
775
+ carry `details.rolledBack: true` and `details.dir`; a typo'd ref never
776
+ leaves a half-onboarded directory that looks finished. Once the workspace
777
+ has been read, a later failure keeps the files (a lock may exist) and
778
+ carries `details.dir` + `details.local` instead.
779
+
780
+ ### `oats sync [--dir <d>] --json` → `syncApi: 1`
781
+
782
+ Discovers, confirms membership, resolves `packages:` to commits, writes
783
+ `oats-lock.json` (lockfileVersion 3), reports. **Exit `2` with `ok: true`** when
784
+ the lock was written but approvals are pending (`approvalNeeded` non-empty);
785
+ approval is interactive-only, so a Desktop must tell the operator to run
786
+ `oats sync` in a terminal. Exit `0` otherwise.
787
+
788
+ ```json
789
+ {"syncApi":1,
790
+ "workspace":{"name":"acme","key":"github.com/acme/agents","url":"https://github.com/acme/agents.git","commit":"<oid>","observedAt":"<iso>",
791
+ "local":"/abs/acme-workspace/oats-local.yaml","lock":"/abs/acme-workspace/oats-lock.json"},
792
+ "members":[{"key":"github.com/acme/agents","name":"agents","commit":"<oid>","confirmed":true,"status":"confirmed","detail":null,"team":"global",
793
+ "souls":["release-manager"],"capabilities":["acme-house-style"],"publishes":null},
794
+ {"key":"github.com/acme/tools","name":"tools","commit":"<oid>","confirmed":true,"status":"confirmed","detail":null,"team":"engineering",
795
+ "souls":["tools-expert"],"capabilities":["acme-tools-dev"],"publishes":{"package":"acme.tools","version":"0.4.0"}},
796
+ {"key":"github.com/acme/billing","name":"billing","commit":"<oid>","confirmed":false,"status":"no-backlink","detail":"github.com/acme/billing@… has no oats-membership.yaml","team":null,
797
+ "souls":[],"capabilities":[],"publishes":null}],
798
+ "packages":[{"id":"oats.okf","version":"2.1.3","source":"catalog:oats.okf","commit":"<oid>","integrity":"sha256-…","capabilities":["oats.okf"],
799
+ "approved":{"executables":"sha256-…","at":"<iso>"}},
800
+ {"id":"acme.tools","version":"0.4.0","source":"git:github.com/acme/tools@v0.4.0","commit":"<oid>","integrity":"sha256-…","capabilities":["acme-deploy","acme-lint"],"approved":null}],
801
+ "changes":[{"id":"acme.tools","from":null,"to":"0.4.0","commit":"<oid>","approvalNeeded":true}],
802
+ "approvalNeeded":[{"id":"acme.tools","version":"0.4.0","commit":"<oid>","executables":"sha256-…",
803
+ "targets":["acme-deploy: command apply → bin/acme-deploy.mjs","acme-deploy: hook spawn → bin/acme-deploy.mjs"]}],
804
+ "problems":[]}
805
+ ```
806
+
807
+ - `members[].status`: `confirmed` | `not-listed` | `no-backlink` |
808
+ `backlink-elsewhere` | `cannot-read`; `detail` explains an unconfirmed row.
809
+ `publishes` reports a member's `oats-package/` (informational — its
810
+ capabilities are **not** in `capabilities[]`; the non-collapse rule).
811
+ - `changes[]`: `from` = previously locked version or `null`; `to` = `null` when
812
+ the package was dropped from `packages:` and from the lock.
813
+ - `approvalNeeded[].targets` are human-readable lines
814
+ (`<cap>: command|hook <name> → <relpath>`); `executables` is the digest an
815
+ approval would record.
816
+ - `problems[]`: `{ code, path, message, repoKey? }` — per-item discovery
817
+ problems (`E_WORKSPACE_SCHEMA`, `E_TEAM_UNKNOWN`, `E_REMOTE_*`, …). Never an
818
+ abort: an unreadable member directory is a problem of that member.
819
+ - Errors: `E_LOCAL_MISSING`, `E_WORKSPACE_SCHEMA { path, problems[] }`,
820
+ `E_REMOTE_UNREADABLE`, `E_LOCK_SCHEMA`, `E_PACKAGE_MISSING` (catalog has no
821
+ such id), `E_PACKAGE_INTEGRITY { why: "branch" | locked/observed }`,
822
+ `E_PACKAGE_UNAPPROVED` (approval digest no longer matches),
823
+ `E_PACKAGE_MANIFEST`, `E_REPO_REF`.
824
+
825
+ ### `oats package add <id> <version|git:<repo>@<ref>> | remove <id> [--dir] --json`
826
+
827
+ Edits `packages:` **only** when `oats-workspace.yaml` is tracked by the Git
828
+ checkout walked up from `--dir`; otherwise reports the line to add.
829
+
830
+ ```json
831
+ {"action":"add","id":"oats.aweb","value":"v1.11.2","previous":null,"edited":true,"file":"/abs/agents/oats-workspace.yaml"}
832
+ {"action":"add","id":"oats.aweb","value":"v1.11.2","edited":false,"file":null,"line":"packages:\n oats.aweb: v1.11.2","hint":"oats-workspace.yaml is not in this checkout; commit the change in the workspace repo, then `oats sync`"}
833
+ ```
834
+
835
+ `remove` → `{ action: "remove", id, value: null, previous: "<old value>", edited: true, file }`
836
+ on the tracked branch. On the untracked branch the shape is
837
+ `{ action: "remove", id, value: null, edited: false, file: null, line: null, hint }`
838
+ — **no `previous`** (nothing was read), `line: null` (there is no line to add
839
+ for a removal). **Both branches** answer `E_PACKAGE_MISSING { id, path? }` when
840
+ `<id>` is not declared in `packages:` — the untracked branch reads the file it
841
+ found to check the declaration even though it does not edit it. `E_USAGE`,
842
+ `E_WORKSPACE_SCHEMA` (bad id/value, or the edit would make the file invalid),
843
+ `E_REPO_REF`. No network.
844
+
845
+ ### `oats workspace status [--dir] --json` → `workspaceStatusApi: 1`
846
+
847
+ ```json
848
+ {"workspaceStatusApi":1,
849
+ "workspace":{"name":"acme","key":"github.com/acme/agents","url":"…","commit":"<oid>","observedAt":"<iso>","local":"/abs/…/oats-local.yaml","teams":["global","engineering"]},
850
+ "members":[ … same rows as sync … ],
851
+ "packages":[ … same rows as sync (from the lock) … ],
852
+ "declaredPackages":["acme.tools","oats.okf"],
853
+ "unsynced":[],
854
+ "stale":[],
855
+ "approval":{"approved":["oats.okf"],"needed":["acme.tools"]},
856
+ "external":[{"source":"git:github.com/oss-collective/experts@<oid>","soul":"security-reviewer","team":"unassigned"}],
857
+ "problems":[]}
858
+ ```
859
+
860
+ `unsynced` = declared in `packages:` but not in the lock (run `sync`);
861
+ `stale` = locked but no longer declared. Read-only: does not write the lock.
862
+
863
+ ### `oats capabilities [--dir] --json` → `capabilitiesApi: 1` · `oats souls [--dir] --json` → `soulsApi: 1`
864
+
865
+ Every **non-private** item of every confirmed member, external souls, and
866
+ locked package capabilities, sorted by name then origin. `origin` is the
867
+ human string (`member <key> @ <8-char commit>` | `package <id> v<version>` |
868
+ `external <key> @ <commit>`); `kind` is the machine field. `team` is the label
869
+ or `"unassigned"`.
870
+
871
+ ```json
872
+ {"capabilitiesApi":1,"workspace":{"name":"acme","key":"github.com/acme/agents","commit":"<oid>"},
873
+ "capabilities":[
874
+ {"name":"acme-house-style","origin":"member github.com/acme/agents @ 3f2a9c1e","kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>",
875
+ "team":"global","private":false,"path":"capabilities/acme-house-style","layer":null,"version":"0.0.0-workspace"},
876
+ {"name":"oats.okf","origin":"package oats.okf v2.1.3","kind":"package","package":"oats.okf","version":"2.1.3","commit":"<oid>",
877
+ "team":"unassigned","private":false,"approved":true}],
878
+ "problems":[]}
879
+ ```
880
+
881
+ ```json
882
+ {"soulsApi":1,"workspace":{…},
883
+ "souls":[
884
+ {"name":"release-manager","origin":"member github.com/acme/agents @ 3f2a9c1e","kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>",
885
+ "team":"engineering","private":false,"path":"souls/release-manager","work":"worktree","description":"Cuts, verifies and announces releases."},
886
+ {"name":"security-reviewer","origin":"external github.com/oss-collective/experts @ 9c4e1f2a","kind":"external",…}],
887
+ "problems":[]}
888
+ ```
889
+
890
+ Package capabilities of declared-but-unsynced packages are absent until `sync`.
891
+ (`soulsApi: 1` is also the integer of the existing `oats inspect --json`
892
+ declarations block; the two payloads are distinguished by their command.)
893
+
894
+ ### `oats spawn <soul> … --preview --json` — additions (Preview API 2 unchanged)
895
+
896
+ On a workspace deployment the preview carries four extra top-level fields, and
897
+ `decision.resolution` binds the resolution revision (so a member that moved
898
+ between preview and apply is `E_DECISION_STALE`):
899
+
900
+ ```json
901
+ {"modules":[
902
+ {"name":"acme-release-tooling","from":{"kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>"},"layer":null,"private":false,
903
+ "changedSince":{"instance":"release-manager-v2","was":"<old oid>"}},
904
+ {"name":"oats.okf","from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"<oid>","integrity":"sha256-…","repoKey":"github.com/awebai/oats-okf"},
905
+ "layer":"knowledge","private":false,"changedSince":false}],
906
+ "team":"engineering",
907
+ "resolution":"6e3050c0d005879441ab017d",
908
+ "workspace":"github.com/acme/agents",
909
+ "spawnPreviewApi":2,"preview":true,"agent":"release-manager","instance":"release-manager-cut","home":"/abs/…",
910
+ "decision":{"instance":"…","home":"…","branch":"…","base":{…},"effective":{…},"resolution":"6e3050c0d005879441ab017d","revision":"<24 hex>"},
911
+ "…":"every Preview API 2 field as before"}
912
+ ```
913
+
914
+ - `modules[].from` is exactly the `from` recorded in `instance.json` on apply.
915
+ - `changedSince`: `null` (no previous instance of this soul), `false`
916
+ (unchanged since the newest previous instance), or
917
+ `{ instance, was }` (`was` = the previous commit, or `null` when the previous
918
+ instance had no such module).
919
+ - `capabilities[]` / `skills[]` keep their Preview-1 meaning; on a workspace
920
+ spawn the authoritative module set is `modules[]` (`capabilities[]` may be
921
+ empty there, since capability rows are filled after materialization).
922
+ - `workspace` is the workspace host's canonical key, `team` the soul's label
923
+ (or `null`), `resolution` the 24-hex revision `decision.resolution` binds.
924
+ - `--provider <cap> <key>=<value>` (repeatable; `a.b=c` nests) is accepted by
925
+ preview and apply. `E_BAD_ARGS` for a malformed pair or when the deployment
926
+ has no `oats-local.yaml`; `E_CAPABILITY_MISSING { capability, soul, modules[] }`
927
+ when the soul does not resolve that capability. `byTeam` is a **reserved
928
+ key**: legal only at the top level of the workspace file's `messaging:`;
929
+ anywhere else in any payload layer (soul, `oats-local.yaml` `settings`,
930
+ `--provider`, at any depth) it is `E_WORKSPACE_SCHEMA { reason: "reserved-key", path, key }`.
931
+ - Soul lookup: `E_SOUL_UNKNOWN { name, members[] }` (not among confirmed
932
+ members or externals), `E_SOUL_AMBIGUOUS { name, repos[] }` (name it
933
+ `<repo>/<soul>`). Resolution errors keep their codes (`E_NOT_A_MEMBER`,
934
+ `E_MEMBERSHIP_UNCONFIRMED { repoKey, reason }`, `E_CAPABILITY_MISSING { hint? }`,
935
+ `E_CAPABILITY_PRIVATE`, `E_PACKAGE_MISSING`, `E_PACKAGE_UNAPPROVED { id, version, commit }`,
936
+ `E_SLOT_CONFLICT { slot, modules[], reason? }`, `E_SKILL_DUPLICATE { name, modules[] }`,
937
+ `E_COMPATIBILITY { capability, package, version, range, why? }`).
938
+
939
+ The apply result (`oats spawn … --json`) is unchanged in shape; the new facts
940
+ live in the home's `instance.json`.
941
+
942
+ ### `instance.json` — `modules`, `providers`, `workspace` (feature `instance-modules`)
943
+
944
+ Written by materialization inside the spawn transaction; read back by
945
+ `oats status --json` and the roster.
946
+
947
+ ```json
948
+ {"modules":{
949
+ "acme-release-tooling":{"from":{"kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>"},
950
+ "commit":"<oid>","digest":"sha256-…","materializedAt":"<iso>"},
951
+ "oats.okf":{"from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"<oid>","integrity":"sha256-…","repoKey":"github.com/awebai/oats-okf"},
952
+ "commit":"<oid>","digest":"sha256-…","materializedAt":"<iso>"}},
953
+ "providers":{"acme-release-tooling":{},"oats.okf":{"owns":"release-manager","reads":["platform-engineer"],"state-dir":"/Users/ana/.oats/okf"}},
954
+ "workspace":{"key":"github.com/acme/agents","commit":"<oid>","resolution":"<24 hex>","standalone":false,
955
+ "soul":{"repoKey":"github.com/acme/agents","commit":"<oid>","team":"engineering"}},
956
+ "capabilities":[{"id":"oats.okf","capability":"oats.okf","origin":"package:oats.okf@2.1.3","…":"one row per module"}]}
957
+ ```
958
+
959
+ `workspace.standalone` is `true` when the instance was spawned from the
960
+ **standalone view** (decisions 10/25: a *member* whose workspace could not be
961
+ read — `workspace.key` is then the member repo's key, and `modules` holds the
962
+ soul's `from: here` capabilities plus `oats.core`); `false` for a workspace
963
+ spawn. The same view is marked `standalone: true` in `oats sync --json` (with
964
+ `workspace.name` = `standalone:<repo>`) and in the roster. `capabilities[]` is
965
+ the per-module row set (`toCapabilityRows`) that `oats inspect`/`status` read.
966
+
967
+ `digest` is the sha256 of the copied module tree (`<home>/.oats/modules/<cap>/`);
968
+ `providers.<cap>` is the merged payload (soul ⊕ `oats-local.yaml`
969
+ `settings.<cap>` ⊕ `--provider`), `{}` for a capability with none. Copies live at
970
+ `<home>/.oats/modules/<cap>/` and `<home>/.agents/skills/<cap>/<skill>/`.
971
+
972
+ ### `oats status [--dir] --json` — module drift
973
+
974
+ On a workspace deployment the roster does one discovery and rewrites each
975
+ instance's `modules` from the recorded map into **drift rows**, and adds a
976
+ top-level `workspace` reachability field:
977
+
978
+ ```json
979
+ {"root":"/abs/acme-workspace/agents",
980
+ "agents":[{"name":"release-manager",…,"instances":[{"instance":"release-manager-cut",…,
981
+ "modules":[
982
+ {"name":"acme-release-tooling","from":{"kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>"},"commit":"<oid>",
983
+ "current":{"commit":"<new oid>"},"status":"moved"},
984
+ {"name":"acme-house-style","from":{…},"commit":"<oid>","current":{"commit":"<oid>"},"status":"missing","reason":"capability-absent"},
985
+ {"name":"oats.okf","from":{"kind":"package",…},"commit":"<oid>","current":{"commit":"<oid>","version":"2.1.3"},"status":"current"}]}]}],
986
+ "workspace":{"reachable":true}}
987
+ ```
988
+
989
+ - `status`: `current` | `moved` (member or locked package now at another
990
+ commit) | `missing` with `reason`: `capability-absent` (gone from the member /
991
+ package), `package-absent` (no longer locked), or the member's unconfirmed
992
+ reason (`no-backlink`, `cannot-read`, `unconfirmed`, …; `current: null`).
993
+ - Package modules without a lock are reported `current`.
994
+ - Offline: `"workspace":{"reachable":false,"code":"E_REMOTE_UNREADABLE","reason":"E_REMOTE_UNREADABLE: network","message":"…"}`
995
+ and `modules` stays the recorded map (no drift rows). A deployment without
996
+ `oats-local.yaml` has no `workspace` field and `modules` as recorded.
997
+ - Text mode prints `modules: <cap> from <member|package …> @ <7-char>` lines
998
+ for non-current modules (`--verbose` for all).
999
+
1000
+ ### Probe
1001
+
1002
+ ```json
1003
+ {"…":"…","features":["…","workspace-v2","instance-modules","spawn-provider-payload"],"workspaceApi":2}
1004
+ ```
1005
+
1006
+ A feature is listed only once the binary implements it. Gate `sync`/`package`/
1007
+ `workspace status`/`capabilities`/`souls` on `workspace-v2`; gate reading
1008
+ `instance.json.modules` and preview `modules[]` on `instance-modules`; gate
1009
+ `--provider` on `spawn-provider-payload`.
630
1010
 
631
1011
  ## Instruction refresh (`oats session recompose`, feature `session-recompose`, OATS 0.24.8+)
632
1012
 
@@ -23,10 +23,9 @@ The CLI diagnoses stale references instead of failing opaquely:
23
23
 
24
24
  - `oats doctor` warns when an `oats-lock.json` still pins `oats.web`, with the
25
25
  fix spelled out.
26
- - Bare `oats install` reports the lock entry as `RETIRED` (with guidance)
27
- rather than a restore failure.
28
- - `oats install oats.web` and a config activation of `oats.web` fail with a
29
- message naming the successor and the exact cleanup steps.
26
+ - A soul or workspace default naming `oats.web` fails at resolution with a
27
+ message naming the successor and the exact cleanup steps (remove it from
28
+ `packages:` / the soul's `capabilities:`).
30
29
 
31
30
  ## Migrating `oats pane` usage
32
31