@awebai/oats 0.24.13 → 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 (51) hide show
  1. package/bin/oats.mjs +930 -2820
  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 +324 -5
  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.25.0.md +99 -0
  33. package/docs/soul.schema.json +41 -68
  34. package/docs/souls-and-instances.md +175 -108
  35. package/docs/workspace-adoption.md +70 -345
  36. package/docs/workspaces.md +429 -119
  37. package/lib/core.mjs +419 -55
  38. package/lib/instance-resolution.mjs +312 -0
  39. package/lib/materialize.mjs +580 -0
  40. package/lib/packages.mjs +501 -1273
  41. package/lib/remote.mjs +639 -0
  42. package/lib/resolve.mjs +576 -0
  43. package/lib/schedule.mjs +90 -16
  44. package/lib/workspace.mjs +635 -0
  45. package/package.json +1 -1
  46. package/lib/portable-migration-artifacts.mjs +0 -135
  47. package/lib/portable-migration-evidence.mjs +0 -305
  48. package/lib/portable-migration-store.mjs +0 -199
  49. package/lib/portable-migration.mjs +0 -104
  50. package/lib/portable-onboarding-acceptance.mjs +0 -66
  51. 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
@@ -483,6 +485,9 @@ is the first-run readiness view (frame 09) and the Capabilities readiness rows
483
485
  deployment (no `workspace:` in `oats.yaml`), `unknown` until admission is
484
486
  verified against the workspace observation, `pass`/`fail` when it is. Never
485
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.)*
486
491
  - Subject: `--soul <name>` scopes required items to the soul's declared
487
492
  requirements; without it, to the scope's active capabilities.
488
493
 
@@ -680,14 +685,328 @@ location. The receipt says so:
680
685
 
681
686
  ### Feature advertisement — gate every new command on the probe
682
687
 
683
- `oats version --json` `features` now lists: `catalog`, `instance-git`,
688
+ `oats version --json` `features` now lists: `instance-git`,
684
689
  `instance-git-remote`, `souls-declarations`, `lifecycle-plans`,
685
690
  `retire-retention`, `readiness`, `spawn-preview`, `instance-events`,
686
691
  `schedule-history`, and carries the API integers (`instanceGitApi`, `soulsApi`,
687
692
  `lifecycleApi`, `readinessApi`, `spawnPreviewApi`, `eventsApi`,
688
693
  `scheduleHistoryApi`). **Gate on these, never on a version string and never by
689
694
  optimistic invocation**: an older CLI ignores an unknown `--plan` on `retire`
690
- 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`.
691
1010
 
692
1011
  ## Instruction refresh (`oats session recompose`, feature `session-recompose`, OATS 0.24.8+)
693
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