@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
@@ -0,0 +1,309 @@
1
+ # Workspace model — module contracts (the spec the implementation is built against)
2
+
3
+ **Status:** normative for implementation · **Decision:** `agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md` · **Example:** [2026-09-23-simplified-workspace-model.md](2026-09-23-simplified-workspace-model.md) · **Plan:** [2026-09-23-workspace-v2-implementation-plan.md](2026-09-23-workspace-v2-implementation-plan.md)
4
+
5
+ These are the canonical kernel modules. There is no `lib/v2/`. Each module below replaces the v1 code named under *Supersedes*; when a phase lands, the superseded module is deleted and its tests rewritten. All modules: ESM, Node ≥ 22, no new runtime dependencies beyond `yaml` (already present) and `node:*`. Errors are `oatsError(code, message, details?)` from `lib/errors.mjs` with the codes listed here — stable, machine-readable, never raw stderr.
6
+
7
+ Conventions: every path returned is absolute and normalized; every commit is a full 40-hex OID; every digest is `sha256-<hex>`; `at` timestamps are ISO-8601 UTC; functions are synchronous unless marked `async`; nothing shells out except `lib/remote.mjs` (to `git`) and the launch path.
8
+
9
+ ---
10
+
11
+ ## 1. `lib/remote.mjs` — observe Git remotes in the operator's access context
12
+
13
+ Supersedes: `repository-observation.mjs` (v1 parts), `source-spec.mjs`.
14
+
15
+ ```js
16
+ export function parseRepoRef(text)
17
+ // "git:github.com/org/repo" | "git:github.com/org/repo.git" | "https://github.com/org/repo(.git)" | "git@github.com:org/repo.git"
18
+ // → { host: "github.com", path: "org/repo", url: "https://github.com/org/repo.git", key: "github.com/org/repo" } | throws E_REPO_REF
19
+ // key is the canonical identity used everywhere else ("<host>/<path>", lowercase host, no .git).
20
+
21
+ export async function observeRemote(ref, { at } = {})
22
+ // at: undefined → the remote's default branch (git ls-remote --symref HEAD); or a full OID; or a tag/branch name.
23
+ // → { key, url, commit, ref: "<resolved symbolic ref or null>", observedAt }
24
+ // throws E_REMOTE_UNREADABLE { url, reason: "auth"|"not-found"|"network"|"timeout" } — NEVER half-succeeds; NEVER prompts.
25
+
26
+ export async function readRemoteFile(ref, commit, path)
27
+ // → { bytes: Buffer, size } | throws E_REMOTE_UNREADABLE | E_REMOTE_PATH_MISSING { path }
28
+ // Bounded: size > 4 MiB → E_REMOTE_FILE_OVERSIZE { path, size, budget }.
29
+
30
+ export async function listRemoteTree(ref, commit, dir, { depth = 2 } = {})
31
+ // → [{ path, type: "blob"|"tree"|"symlink", size? }] relative to dir, depth-bounded; missing dir → [];
32
+ // submodule gitlinks are omitted. Unsafe entry names → E_REMOTE_TREE_UNSAFE { why: "path" } (see below).
33
+
34
+ export async function fetchRemoteTree(ref, commit, dir, destDir)
35
+ // Copies the subtree at <dir> of <commit> into destDir (created; must not exist, not even as a dangling symlink).
36
+ // Regular files and dirs only; everything is inspected BEFORE any write:
37
+ // symlinks / submodules / odd modes → E_REMOTE_TREE_UNSAFE { path, why: "symlink"|"device" }; total > 64 MiB → "oversize";
38
+ // an entry-name component that is "", ".", "..", ".git" (any case) or contains "\"/NUL → why: "path";
39
+ // two entries equal under NFC+case folding (README.md/readme.md, NFC/NFD) → why: "collision".
40
+ // → { files, bytes, digest } digest = sha256 over (relpath, git-normalized mode "755"|"644", size, bytes) in
41
+ // byte-wise relpath order — the "content hash" (umask-independent: a checkout digests like the fetched tree).
42
+
43
+ export function contentDigest(dir)
44
+ // Same digest computation over a local directory (used by materialize to verify a copy); a top-level `.git/` is ignored.
45
+ ```
46
+
47
+ Every module that takes a remote accepts `{ remote, remoteOptions }`: `remote` defaults to this module,
48
+ `remoteOptions` (`cacheDir`, `exec`, …) is threaded into every remote call (tests never touch `~/.cache`).
49
+ `observeRemote`'s `at` is a full OID or a plain ref name (no `-` prefix, no `^{}`/`~`/`:` revision syntax,
50
+ no globs) → else `E_REPO_REF`; the resolved object MUST be a commit (a tag/OID naming a tree or blob →
51
+ `E_REMOTE_UNREADABLE not-found`). Operations on one cache repo are serialized in-process; a fetch that
52
+ loses an on-disk `.lock` race is retried; a pinned commit whose objects were wiped is refetched, never
53
+ reported from the stale pin. ssh runs in BatchMode ALWAYS — `-o BatchMode=yes` is appended to the
54
+ operator's `GIT_SSH_COMMAND`/`core.sshCommand` (or to `ssh`).
55
+
56
+ Implementation: `git ls-remote`, shallow `git fetch --depth 1 --filter=blob:none` into a **content-addressed cache** under `os.homedir()/.cache/oats/remotes/<key-hash>/` (invisible plumbing; may be wiped at any time; never referenced by any other module). Uses the operator's own git configuration and credential helpers; sets `GIT_TERMINAL_PROMPT=0`, `GIT_ASKPASS=/usr/bin/false` (or equivalent) so nothing ever prompts. Timeout 30 s per git call. Local paths (`file:///…` or an absolute path to a bare repo) are valid refs (`key: "local/<abs-path>"`) — this is how tests build remotes.
57
+
58
+ ---
59
+
60
+ ## 2. `lib/workspace.mjs` — declarations, membership, discovery
61
+
62
+ Supersedes: `workspace-definition.mjs`, `workspace-discovery.mjs`, `portable-*.mjs` (declaration parts), `capability-provenance.mjs` (v1), `config-data.mjs` activation parsing.
63
+
64
+ ### Files (schemas in `docs/*.schema.json`, validated with the existing validator style)
65
+
66
+ `oats-workspace.yaml` (schemaVersion 2):
67
+ ```yaml
68
+ schemaVersion: 2
69
+ name: <slug>
70
+ members: [<repo ref>, …] # no revisions permitted → E_WORKSPACE_SCHEMA
71
+ packages: { <package-id>: <version-or-ref> } # e.g. oats.okf: v2.1.3 ; oats.dev: git:github.com/x/y@<ref>
72
+ teams: { <label>: { description } }
73
+ defaults:
74
+ capabilities: { <cap>: { from: <repo key|package> } }
75
+ knowledge: { <cap>: { from } } | none # one entry max per slot
76
+ messaging: … | none
77
+ tasks: … | none
78
+ byTeam: { <label>: { capabilities: { <cap>: { from } | off } } }
79
+ stores: { <name>: <repo ref> } # a REPOSITORY; the root inside it is the provider's (OKF `root`) — no `#path`
80
+ messaging: <opaque provider payload> # may carry byTeam: { <label>: <payload> } — merged base ⊕ byTeam[soul.team], stripped
81
+ external: [ { source: <repo ref>@<full OID>, soul: <path> } ] # revision REQUIRED
82
+ ```
83
+ Schema refuses: absolute paths anywhere; `@revision` on members; unknown top-level keys.
84
+ *(clarified Phase B)* "Absolute paths" means bare filesystem paths as VALUES (`/Users/x/store`, `C:\…`) — host state that belongs in `oats-local.yaml`. A repo ref in `file:///…` or `git:/abs/bare.git@<ref>` form is a **repo ref** (§1 accepts it; it is how tests build remotes), not an absolute path, and is accepted wherever a repo ref is. The JSON schemas encode only what a JSON schema can (shapes, grammars); domain rules — declared teams, duplicate members, canonical `from:` keys, one form per `packages:` value — live in `validateWorkspace`/`validateSoul`, which are the authority; a consumer validating against the schema alone accepts a superset.
85
+ *(refined Phase C, decisions 23–25)* `messaging.byTeam.<label>` must name a label in `teams:` (`E_WORKSPACE_SCHEMA`). Standalone resolution (`standaloneRepo`) adds `oats.core: { from: package }` unless the soul says `off`; the package resolves through the operator's lock exactly as in a workspace (`E_PACKAGE_UNAPPROVED` until approved). `stores` values are repo refs only.
86
+
87
+ *(clarified Phase B)* A `from:` value is `package`, `here` (souls only — a workspace default has no referent for `here` → `E_WORKSPACE_SCHEMA`), or a **canonical** repo key exactly as `parseRepoRef(ref).key` spells it (lowercase host, no scheme, no `git:`, no `.git`; `local/<abs-path>` for file remotes). Any other spelling is a schema problem at validation, never a late `E_NOT_A_MEMBER`. `soul.yaml` requires `name`, `description` and `work`.
88
+
89
+ `oats-membership.yaml`: `{ schemaVersion: 2, workspace: <repo ref>, team?: <label> }` — nothing else.
90
+
91
+ `soul.yaml` (schemaVersion 2):
92
+ ```yaml
93
+ schemaVersion: 2
94
+ name, description, work: worktree|checkout|directory|workspace
95
+ team?: <label>
96
+ private?: true
97
+ capabilities: { <cap>: { from: <repo key|here|package> } | off }
98
+ knowledge | messaging | tasks: <opaque provider payload> | none
99
+ compatibility?: { <cap>: <semver range> }
100
+ ```
101
+ `capabilities/<name>/oats.json` — unchanged manifest; may carry `private: true` and `team: <label>`. Discovery
102
+ relies on `capability` (the `capabilityName` grammar `^[a-z0-9][a-z0-9._-]*$` — the same grammar every
103
+ `capabilities:` key uses, so a discoverable name is always referenceable; no `/`), `version`, `private`, `team`,
104
+ `layer`; a manifest failing that is a problem, not listed. Two souls or two capabilities declaring one name in
105
+ a repo: the first (by path) is listed, the second is a problem. `external[].team` overrides the soul's `team`.
106
+ `loadLocal` throws `E_WORKSPACE_SCHEMA` for an invalid `oats-local.yaml`.
107
+
108
+ `oats-local.yaml`: `{ schemaVersion: 2, workspace: <repo ref>, clones?: { <repo key>: <abs path> }, settings?: { <cap>: { <key>: <value> } }, souls?: { disabled: [<name>] } }`.
109
+
110
+ ### API
111
+
112
+ ```js
113
+ export function loadLocal(dir) // walks up from dir to find oats-local.yaml → { path, local } | E_LOCAL_MISSING
114
+ export async function observeWorkspace(ref, { at } = {})
115
+ // → { key, commit, workspace: <parsed+validated>, observedAt } E_REMOTE_UNREADABLE | E_WORKSPACE_SCHEMA { path, problems[] }
116
+ // A repo without oats-workspace.yaml is E_WORKSPACE_SCHEMA (it is not a workspace host), never a leaked E_REMOTE_PATH_MISSING.
117
+
118
+ export async function confirmMembership(workspaceObs, memberRef)
119
+ // Reads the member's oats-membership.yaml at its default branch in the SAME access context.
120
+ // → { key, commit, confirmed: true, team } |
121
+ // { key, confirmed: false, reason: "not-listed"|"no-backlink"|"backlink-elsewhere"|"cannot-read", detail }
122
+ // Never throws for an unconfirmed member (ANY E_REMOTE_* about the member's file — oversize, symlink, unsafe
123
+ // tree — is an unconfirmed row; an unparseable memberRef is "not-listed"); throws only on schema errors of the
124
+ // WORKSPACE file. Repo keys are case-sensitive in the path part; a backlink that differs only by case is
125
+ // "backlink-elsewhere" with `caseOnly: true` and a hint in `detail`.
126
+
127
+ export async function discoverWorkspace(ref, { at, local } = {})
128
+ // The whole picture, in one access context:
129
+ // → { workspace, members: [{ key, commit, confirmed, reason?, team, souls: [SoulEntry], capabilities: [CapEntry],
130
+ // publishes: { package, version } | null }],
131
+ // external: [{ source, commit, soul: SoulEntry }], problems: [{ code, path, message }] }
132
+ // A remote failure on ONE member's directory listing is a problem of that member, never an abort.
133
+ // SoulEntry = { name, path, repoKey, commit, team, private, definition } (definition = validated soul.yaml)
134
+ // CapEntry = { name, path, repoKey, commit, team, private, manifest }
135
+ // Unconfirmed members contribute nothing but their row. `private` items are included with private:true (callers filter).
136
+ // Unknown team labels on items → problems[] E_TEAM_UNKNOWN (the item is still listed).
137
+
138
+ export function standaloneRepo(ref, commit, discovery?)
139
+ // For a readable member whose workspace cannot be read: souls with from:here capabilities only.
140
+ ```
141
+
142
+ ---
143
+
144
+ ### Member vs package tier — the non-collapse rule (decisions 19–21)
145
+
146
+ A repository may be a **member** (it completed the handshake; its `souls/*` and `capabilities/*` are member-tier, latest state) **and** a **package publisher** (its `oats-package/` is consumed only through `packages:`, versioned, locked, approved). The two never collapse:
147
+
148
+ - `from: <repo key>` looks ONLY under `<repo>/capabilities/<name>/oats.json` at the member's latest state. It never looks inside `oats-package/`. A capability that exists only inside the repo's package → `E_CAPABILITY_MISSING` with `details.hint: "provided by package <id>; use from: package"`.
149
+ - `from: package` looks ONLY in the lock (`packageProviding`). It never looks at member capabilities, even when the package's repo is a member.
150
+ - `discoverWorkspace` lists a member's `oats-package/` presence as `publishes: { package, version }` on the member row (informational) and does NOT enumerate the package's capabilities as member capabilities.
151
+ - `packages:` values: a **bare version** (`v2.1.3`) resolves through the official catalog (`package-catalog.json` in the workspace's `catalog:` source, default the `oats` repo's); a **`git:<repo>@<ref>`** value is a direct package ref, where `<repo>` is any ref §1 understands — `github.com/org/repo`, `https://…`, `git@host:…`, `/abs/bare.git`, `file:///…` — and `<ref>` a tag name or full OID. Both are packages. There is no third form: `lib/packages.mjs#classifyPackageValue` is the one grammar, used by `validateWorkspace` and `resolvePackages`. A `<ref>` (or catalog ref) that resolves to a **branch** is refused (`E_PACKAGE_INTEGRITY { why: "branch" }`) — versions are immutable.
152
+
153
+ ## 3. `lib/resolve.mjs` — from a soul to an immutable resolution
154
+
155
+ Supersedes: `resolution-shape.mjs`, `prepared-resources.mjs`, `prepare-composition.mjs` (declaration parts), the `capabilities.layers/additive` derivation in `core.mjs`.
156
+
157
+ ```js
158
+ export function resolveSoul(discovery, soulEntry, { local, lock, spawn = {} })
159
+ // spawn = { providers?: { <cap>: { <k>: <v> } }, work?, model?, … } (instance-level payload, decision 14)
160
+ // → Resolution (immutable, JSON-serializable):
161
+ // {
162
+ // resolutionApi: 1,
163
+ // soul: { name, repoKey, commit, team, path },
164
+ // modules: [ { name, from: { kind: "member", repoKey, commit } | { kind: "package", package, version, commit, integrity },
165
+ // manifest, layer: "knowledge"|"messaging"|"tasks"|null, private } ],
166
+ // slots: { knowledge: <module name>|null, messaging, tasks },
167
+ // payloads: { <cap>: <merged provider payload: soul ⊕ local.settings[cap] ⊕ spawn.providers[cap]> },
168
+ // skills: [ { module, name, path } ], // composed skill set; duplicates → E_SKILL_DUPLICATE { name, modules }
169
+ // injects: [ { module, path } ],
170
+ // revision: "<sha256 of the canonical JSON of everything above>[0:24]"
171
+ // }
172
+ // Order: workspace.defaults.capabilities ⊕ defaults.byTeam[soul.team] ⊕ soul.capabilities (soul wins; `off` removes).
173
+ // from:here → soul.repoKey. from:<repo> → must be a confirmed member (E_NOT_A_MEMBER) that has the capability
174
+ // (E_CAPABILITY_MISSING), not private unless same repo (E_CAPABILITY_PRIVATE). from:package → lock.packages must
175
+ // provide it (E_PACKAGE_MISSING) and be approved (E_PACKAGE_UNAPPROVED).
176
+ // Slots: a module with manifest.layer fills that slot; two → E_SLOT_CONFLICT; soul `none` empties; else workspace default.
177
+ // compatibility floors checked against package versions → E_COMPATIBILITY.
178
+ ```
179
+
180
+ *(clarified Phase B)*
181
+ - **Membership gate.** `soulEntry` must be a soul discovery listed: a soul of a **confirmed** member row (same `repoKey`, `name`, `commit`), an `external[]` soul, or (standalone) the repo's own. A soul of an unconfirmed member → `E_MEMBERSHIP_UNCONFIRMED { repoKey, reason }`; a soul of a repo the workspace does not list → `E_NOT_A_MEMBER`; an entry the row does not carry (stale/fabricated) → `E_MEMBERSHIP_UNCONFIRMED { reason: "stale" }`. Resolve is the gate; it does not trust the caller.
182
+ - **Slot defaults.** `defaults.<slot>` must name a capability whose manifest declares `layer: <slot>`; no layer or another layer → `E_SLOT_CONFLICT { reason: "layer-mismatch" }`. Soul `<slot>: none` drops the workspace's `defaults.<slot>` only; a layered capability still arriving through `defaults.capabilities`/`byTeam`/the soul is a loud `E_SLOT_CONFLICT { reason: "none" }` (spell `<cap>: off` to remove it) — never a silent empty slot.
183
+ - **Payload keys.** A provider payload (soul, `workspace.messaging`, `local.settings`, `spawn.providers`) may not carry `__proto__`, `constructor` or `prototype` as a key at any depth → `E_WORKSPACE_SCHEMA { reason: "poison-key" }` (YAML/JSON produce them as own keys; merged, `__proto__` would set the prototype of the payload — invisible to the recorded JSON and the revision, visible to every reader).
184
+ - **Compatibility floors** need a version: a package pinned by OID (`git:<repo>@<OID>` records the OID as its version) or any non-version string → `E_COMPATIBILITY { why: "unversioned", capability, package, version, range }`; every `E_COMPATIBILITY` names `capability` and `package`.
185
+ - **Recorded for materialize** (extra fields, part of the revision): `module.dir` — the capability directory as the manifest lists it (repo-relative; a package's `oats-package.json#capabilities[]` entry, which need not equal the capability name), and `from.repoKey` on package modules. An in-memory `lock` is validated like one read from disk (`E_LOCK_SCHEMA`); `approved` must be a well-formed `{ executables: sha256-…, at }`, not merely truthy.
186
+
187
+ ---
188
+
189
+ ## 4. `lib/packages.mjs` — versions, lock v3, approval (rewritten in place)
190
+
191
+ Supersedes itself (v1: installed tier, `install/restore/use`, lock v2).
192
+
193
+ ```js
194
+ export function readLock(dir) / writeLock(dir, lock) // oats-lock.json lockfileVersion 3
195
+ // lock = { lockfileVersion: 3, packages: { <id>: { source: "catalog:<id>"|"git:<key>@<ref>", url, path, version, commit,
196
+ // integrity, capabilities: [<cap names>], approved: { executables: "sha256-…", at } | null } } }
197
+ // (clarified Phase B) `url` is the repo url the package was read from (observeRemote's `url`): the package's repo
198
+ // identity travels in the lock, so resolveSoul/materialize of a catalog-locked package need no catalog at spawn time.
199
+
200
+ export async function resolvePackages(workspace, { catalog, lock })
201
+ // For each workspace.packages entry: catalog lookup or git ref → observeRemote → commit; read oats-package.json at
202
+ // path; enumerate its capability manifests; integrity = contentDigest of the package tree.
203
+ // → { lock: <updated>, changes: [{ id, from: <old version|null>, to: <version>, commit, approvalNeeded: bool }] }
204
+ // A locked entry whose (version → commit) changed → E_PACKAGE_INTEGRITY unless the version string also changed.
205
+ // An unchanged entry keeps its approval ONLY if executablesDigest(tree) still equals approved.executables
206
+ // (else E_PACKAGE_UNAPPROVED). A catalog `path` change at the same version is a new (unapproved) entry.
207
+ // `remote` defaults to lib/remote.mjs; `remoteOptions` is threaded through.
208
+
209
+ export function executablesDigest(packageTree) // sha256 over every manifest's `commands` targets' AND
210
+ // `hooks.*.command` targets' bytes (hooks run unattended at
211
+ // spawn/retire), codepoint order, locale-independent
212
+ // (clarified Phase B) a hook object without `command` is
213
+ // E_PACKAGE_MANIFEST — never an invisible no-op
214
+ export function approve(lock, id, digest, at) // records approval; returns new lock
215
+ export function packageProviding(lock, capName) // → { id, entry } | null; two providers → E_PACKAGE_MISSING { ambiguous }
216
+ // (a package declaring one capability twice → E_PACKAGE_MANIFEST)
217
+ ```
218
+
219
+ `oats-lock.json` is the only persisted state at the deployment besides `oats-local.yaml`.
220
+
221
+ ---
222
+
223
+ ## 5. `lib/materialize.mjs` — copy whole, compose, record
224
+
225
+ Supersedes: the module/skill assembly in `core.mjs` spawn (`prepared` copies, symlinked skill sets, ambient exclusion), `capability-artifacts.mjs`.
226
+
227
+ ```js
228
+ export async function materialize(resolution, home, { fetch = fetchRemoteTree } = {})
229
+ // For each module: fetch its capability dir — the resolver-recorded module.dir (the manifest-listed directory: member
230
+ // <repo>@<commit>/<dir>; package <pkg>@<commit>/<dir> where <dir> is the oats-package.json#capabilities[] entry, which
231
+ // need not equal <name>: capabilities/oats-okf → oats.okf) (clarified Phase B) —
232
+ // into <home>/.oats/modules/<name>/ ; verify contentDigest === module.digest (recorded); copy skills/* into
233
+ // <home>/.agents/skills/<name>/<skill>/ (full copy, not symlink).
234
+ // Compose <home>/AGENTS.md = soul AGENTS.md + each module inject (existing kernel composer; marker comments unchanged).
235
+ // Keep aliases: CLAUDE.md → AGENTS.md ; .claude/skills → ../.agents/skills (relative symlinks, as today).
236
+ // Write instance.json.modules = { <name>: { from, commit, digest, materializedAt } } and instance.json.providers = resolution.payloads.
237
+ // → { modules: […], skills: […], agentsMd: <path> } Any failure → nothing left behind (staging dir + rename).
238
+ // (clarified Phase B) The transaction includes the aliases and the AGENTS.md/instance.json swap: a failure at any
239
+ // commit step rolls back everything placed and restores the previous files (E_MATERIALIZE_HOME { why }); the home's
240
+ // shape (.oats, .agents, .claude and module targets: real directories or absent) is re-checked immediately before
241
+ // the renames; staging is unique per call; two materializations racing on one home → E_MATERIALIZE_HOME { why: "busy" }.
242
+ // (clarified Phase B) The soul body is the LOCAL soul directory — options.soulAgentsMd / options.soulDir, else
243
+ // <home>/soul/AGENTS.md through the instance's `soul` link into the member clone (decision 9: the work target is
244
+ // the only thing that needs a clone). fetchRemoteTree is not a soul-copy primitive; a soul's CLAUDE.md → AGENTS.md
245
+ // alias never crosses the remote.
246
+
247
+ export function driftOf(instanceJson, discovery)
248
+ // → [{ module, recorded: { repoKey, commit }, current: { commit } | null, status: "current"|"moved"|"missing" }]
249
+ ```
250
+
251
+ Launch (in `core.mjs`, edited): the harness is started with cwd = home and **no** skill-exclusion arguments/profile keys; model/provider pinning is untouched.
252
+
253
+ ---
254
+
255
+ ## 6. CLI (in `bin/oats.mjs`, edited) and DTOs
256
+
257
+ - `oats sync [--dir]` — discover, confirm, resolve packages, ask approval (interactive) or list what needs it, write lock, report diff. `--json` → `{ syncApi: 1, workspace, members[], packages[], changes[], approvalNeeded[] }`.
258
+ - `oats package add <id> <version> | remove <id>` — edits `packages:` in the workspace file **when the workspace repo is the current checkout**; otherwise prints the line to add (the workspace file is shared through Git).
259
+ - `oats spawn <soul> …` — preview/apply unchanged in shape; the decision now embeds `resolution.revision`; `--provider <cap> k=v` (repeatable). Preview lists `modules[]` with `changedSince` (previous instance of the soul) and `team`.
260
+ - `oats capabilities` / `oats souls` — every non-private item of every confirmed member + packages, with `origin` (`member <key> @ <commit>` | `package <id> v<ver>`) and `team`. `--json`.
261
+ - `oats workspace status` — membership table (`confirmed` / `no-backlink` / `cannot-read` …), packages, approval state.
262
+ - `oats status` — per instance `modules` with `driftOf`.
263
+ - `oats version --json` — `workspaceApi: 2`, features `+workspace-v2`, `+instance-modules`, `+spawn-provider-payload`. Removed: `init`, `use`, `install`, `restore`.
264
+ *(clarified Phase B)* A feature string is listed only once the binary implements it: `instance-modules` and `spawn-provider-payload` appear when `oats spawn` runs on resolve/materialize (Phase C), not before. A removed verb answers `E_UNKNOWN_COMMAND` naming its replacement in BOTH text and `--json` (`details.removed`/`replacement`), checked before capability dispatch. `oats status` without a deployment is `E_NO_DEPLOYMENT` in both modes.
265
+ *(clarified Phase B)* `oats package add|remove` edits the file only when it is **tracked** by the checkout it sits in (`git ls-files`); an untracked copy gets "the line to add".
266
+
267
+ Errors introduced by this model (all `E_*`, all with `details`): `E_REPO_REF`, `E_REMOTE_UNREADABLE`, `E_REMOTE_PATH_MISSING`, `E_REMOTE_FILE_OVERSIZE`, `E_REMOTE_TREE_UNSAFE`, `E_WORKSPACE_SCHEMA`, `E_MEMBERSHIP_UNCONFIRMED`, `E_LOCAL_MISSING`, `E_TEAM_UNKNOWN`, `E_NOT_A_MEMBER`, `E_CAPABILITY_MISSING`, `E_CAPABILITY_PRIVATE`, `E_PACKAGE_MISSING`, `E_PACKAGE_MANIFEST` (a package's own files are malformed or missing), `E_PACKAGE_INTEGRITY`, `E_PACKAGE_UNAPPROVED`, `E_LOCK_SCHEMA` (oats-lock.json unreadable or not v3), `E_SLOT_CONFLICT`, `E_SKILL_DUPLICATE`, `E_COMPATIBILITY`; `E_REMOVED` is thrown by the phase-A shims for deleted v1 APIs.
268
+
269
+ ---
270
+
271
+ ## 7. Test fixture — `test/fixtures/northwind/build.mjs`
272
+
273
+ ```js
274
+ export async function buildNorthwind(baseDir)
275
+ // Creates FIVE bare Git repos under baseDir/remotes/{agents,platform,data,marketing,knowledge}.git plus
276
+ // baseDir/remotes/experts.git (the external one) and TWO package repos baseDir/remotes/pkg-{okf,framework}.git
277
+ // with the exact contents of the worked example (three teams, private items, byTeam defaults, a package with an
278
+ // executable) PLUS baseDir/remotes/nw-tools.git: a MEMBER (oats-membership team engineering; soul tools-expert;
279
+ // member capability nw-tools-dev) that ALSO publishes package nw.tools under oats-package/ (capabilities nw-lint,
280
+ // nw-deploy with bin/, tag v0.4.0). The workspace file pins it as `nw.tools: git:<nw-tools ref>@v0.4.0`. Returns { refs: { agents: "<abs path>", … }, commits: { … }, catalog: <object mapping ids → {url, ref, path}> }.
281
+ // Deterministic (fixed author/date; hermetic git config incl. core.excludesFile/attributesFile) so digests are stable
282
+ // across runs and machines. A baseDir containing whitespace or "@" is refused (E_FIXTURE_BASEDIR: repo keys embed it).
283
+ // Also exports scenario helpers:
284
+ export async function moveMember(fixture, name, mutate) // commits a change to a member's default branch
285
+ export async function dropBacklink(fixture, name) // removes oats-membership.yaml (→ no-backlink)
286
+ export async function makeUnreadable(fixture, name) // chmod 000 the bare repo (→ cannot-read)
287
+ ```
288
+
289
+ Every module's tests use this fixture and only this fixture; no test invokes bare `oats setup` (standing rule).
290
+
291
+ ---
292
+
293
+ ## Clarifications — Phase C adversarial-review fix round (2026-09-23)
294
+
295
+ Appended, not edited in place; each item names the section it refines. Decision record: `workspace-model-v2.md` decisions 10, 23, 25.
296
+
297
+ **§2 `standaloneRepo` / `discoverOrStandalone` — standalone requires membership (M6/M7).** A standalone view is a **member** whose workspace cannot be read: the repo MUST carry an `oats-membership.yaml` (`discoverRepo(ref).membership` non-null). A repo with no backlink is not a workspace host and not a member → `E_WORKSPACE_SCHEMA` (the original "not a host" error is rethrown), never a standalone view. The fallback engages **only** when reading the host fails for **access** reasons — `E_REMOTE_UNREADABLE` with `reason: "auth"` (which is also how a permission denial classifies) | `"not-found"`; a `"network"` or `"timeout"` failure propagates unchanged (an offline operator is not a public contributor). The discovery it returns carries `standalone: true`, `workspace: null`, one member row (the repo's own, `confirmed: false, reason: "cannot-read"`) and `standaloneReason: { code, reason, url }` — the access failure that triggered it — so `sync`/`status`/`spawn` can report *why* the view is standalone. `oats-local.yaml` `standalone: <repo ref>` asks for the view explicitly (no host lookup); it too requires the repo to be a member.
298
+
299
+ **§2/§3 `oats.core` default (S4).** In the standalone view the kernel adds `oats.core: { from: package }` only when the soul's `capabilities:` says **nothing** about `oats.core`. Any mention — any `from:` (`package`, `here`, a repo key) or `off` — suppresses the default and the soul's own line is what resolves.
300
+
301
+ **§3 payload keys — `byTeam` is reserved (decision 23).** `byTeam` is legal ONLY at the top level of `workspace.messaging` (merged `base ⊕ byTeam[soul.team]`, then stripped). In every other payload layer — a soul's `knowledge:` / `messaging:` / `tasks:`, `oats-local.yaml` `settings.<cap>`, `spawn.providers[cap]` / `--provider` — at **any depth**, it is `E_WORKSPACE_SCHEMA { reason: "reserved-key", path, key: "byTeam" }`, alongside the existing `poison-key` rule. A provider never receives a `byTeam`.
302
+
303
+ **§5/§6 `instance.json.workspace.standalone`.** A prepared spawn writes `instance.json.workspace = { key, commit, resolution, standalone: <boolean>, soul: { repoKey, commit, team } }` next to `modules{}`, `providers{}` and `capabilities[]` (the `toCapabilityRows(resolution, home)` rows). `standalone` is `true` exactly when `prepared.discovery.standalone === true` (then `key` is the member repo's key); `false` on a workspace spawn. `oats sync --json` marks the same view `standalone: true` with `workspace.name = "standalone:<repo>"`; `oats status`/`workspace status` show it in their workspace field.
304
+
305
+ **§6 `oats onboard [<dir>] --workspace <repo ref> [--json]` → `onboardApi: 2` (M13).** The bootstrap (decision 9): writes `<dir>/oats-local.yaml` `{ schemaVersion: 2, workspace: <ref> }` and `<dir>/agents/`, then runs exactly the `sync` body (§6 `oats sync`) over that directory. Result `{ onboardApi: 2, local, dir, agents, lock, sync: <syncApi 1 report>, hosting: { host, hostIsMember, rule }, next: { clone: [{ key, name, url, dir }], spawn: "<setup-expert spawn command>" } }`; exit `2` with `ok: true` when `sync.approvalNeeded` is non-empty. `--workspace` is parsed (`E_REPO_REF`) before anything is written; a second onboard of the same directory is `E_ALREADY_ONBOARDED { local, dir }` (only THIS directory's file counts — an enclosing deployment is a different deployment); a failure before the workspace has been read (unreadable remote, not a host, package/lock errors of the first resolve) removes what onboarding created and carries `details.rolledBack: true, details.dir`; a failure after the read keeps the files (a lock may exist) and carries `details.dir, details.local`. Creates no soul, installs nothing, spawns nothing, writes no `oats-config.yaml`.
306
+
307
+ **§6 `oats package remove <id>` (M15).** BOTH branches — the file tracked by the checkout (edited) and untracked/absent (not edited) — answer `E_PACKAGE_MISSING { id, path? }` when `<id>` is not declared in `packages:`. The tracked receipt is `{ action: "remove", id, value: null, previous: <old value>, edited: true, file }`; the untracked receipt is `{ action: "remove", id, value: null, edited: false, file: null, line: null, hint }` — `previous` is absent and `line` is `null` (a removal has no line to add).
308
+
309
+ **§6 features.** `catalog` is no longer advertised in `oats version --json` `features` (the verb is removed; the official catalog is reached through `packages:` + `sync`, not a command).
@@ -0,0 +1,65 @@
1
+ # Phase 4 — implementing workspace model v2: proposal for the human
2
+
3
+ **Date:** 2026-09-23 · **Status:** PROPOSED — nothing lands before the human approves this plan.
4
+ **Decision:** `agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md` · **Worked example:** [2026-09-23-simplified-workspace-model.md](2026-09-23-simplified-workspace-model.md)
5
+
6
+ ## What we are building, in one paragraph
7
+
8
+ A clean v2 of the kernel's declaration, resolution and launch path: read `oats-workspace.yaml` v2 / `oats-membership.yaml` / `soul.yaml` v2 / `oats-local.yaml` over Git remotes; confirm membership; resolve every capability by `from:`; fetch packages at the locked version with per-version approval in lock v3; copy each capability whole into the instance (`.oats/modules/` + `.agents/skills/`); compose `AGENTS.md`; launch the harness normally. Remove the installed-capability tier, the classic activation config and the v1 declaration readers. No migration. The framework's own repos become the first real v2 workspace and the Desktop follows through new DTOs under new feature names.
9
+
10
+ ## Where the current code is
11
+
12
+ `lib/` is 82 modules / 21 k lines; `core.mjs` alone is 9.2 k. The v1 declaration/portable path is spread over ~20 modules (`workspace-definition`, `workspace-discovery`, `source-spec`, `portable-*`, `capability-provenance`, `prepared-resources`, `resolution-shape`, `captured-*`, `packages` 1.4 k). Much of that machinery exists to carry per-soul versioned sources, migration evidence and the installed tier — the things v2 removes. The lifecycle core (spawn/retire/session/events/schedule, the Desktop DTOs, `binding` validation, the lock) stays.
13
+
14
+ **Approach (human, 2026-09-23): the new modules ARE the kernel — no `lib/v2/`, no seam, no flag.** Each phase replaces canonical code on main and deletes what it supersedes; tests are updated to the new truth in the same phase. Desktop contracts that change do so under new feature names; the Desktop follows in W12.
15
+
16
+ ## Work packages
17
+
18
+ Each is one PR (or two small ones), reviewed by me, on `main`, behind the v2 seam until W7. Order is dependency order; W1–W3 can proceed in parallel lanes.
19
+
20
+ | # | Package | Delivers | Owner | Size |
21
+ |---|---|---|---|---|
22
+ | **W1** | **Schemas + fixtures** | `oats-workspace.schema.json` v2, `oats-membership.schema.json`, `soul.schema.json` v2, `oats-local.schema.json`, lock v3; the Northwind example as a **test fixture workspace** (three teams, five repos as bare local remotes) used by every later package | lead | S |
23
+ | **W2** | **Remote observation** | `lib/remote.mjs`: fetch a file or a tree from a Git remote at default-branch or exact commit, in the operator's access context (git credential helper / `gh` token), with typed `cannot read <url>`; bounded; a content-addressed **fetch cache** under the OS cache dir (invisible plumbing) | lead | M |
24
+ | **W3** | **Membership + discovery** | `lib/workspace.mjs`: read the workspace file; for each member read `oats-membership.yaml`; confirm both halves in one access context (`E_MEMBERSHIP_UNCONFIRMED` naming the missing/unreadable half); enumerate `souls/*/soul.yaml` and `capabilities/*/oats.json`; apply `private`; team labels (`E_TEAM_UNKNOWN`); standalone case | lead | M |
25
+ | **W4** | **Resolution** | `lib/resolve.mjs`: workspace defaults ⊕ `defaults.byTeam` ⊕ soul (`off` removes) → for each `(name, from)`: member lookup or package lookup; slot filling from `layer:` (`E_SLOT_CONFLICT`); produces an immutable **resolution** (the input to preview/apply — reuses `decision.revision`) | lead | M |
26
+ | **W5** | **Packages v2 + lock v3** | `lib/packages.mjs` (rewritten in place): `packages:` → catalog/git ref → commit; integrity; **approval per version** stored in `oats-lock.json` v3 (`approved.executables` digest); `oats package add|remove`; a moved tag fails integrity and re-asks; **delete** `oats install/restore/use/init` and the installed tier | lead | M |
27
+ | **W6** | **Materialization + launch** | `lib/materialize.mjs`: copy each resolved capability whole into `<instance>/.oats/modules/<cap>/`; copy `skills/` into `<instance>/.agents/skills/<cap>/`; compose `AGENTS.md` (soul + injects); record `modules{}` and **`providers{}` (from `spawn --provider <cap> k=v`, the instance-level payload home; merged with soul + `oats-local.yaml` settings before `binding`)** in `instance.json`; keep `CLAUDE.md`/`.claude/skills` aliases; **launch the harness normally** (drop the ambient-skill exclusion; keep model/profile pinning) | lead | M |
28
+ | **W7** | **CLI switch-over** | `oats sync`, `oats spawn` (preview/apply unchanged in shape, now fed by W4/W6), `oats capabilities` / `oats souls` with origin + team, `oats workspace status`; `oats status` shows `modules … @ commit` + `member moved since`; `oats version --json` advertises `workspaceApi: 2` + feature `workspace-v2`; **remove the v1 readers** (`oats.yaml`, per-soul `source:`, `oats-config.yaml` activation) — a v1 file at a v2 path errors naming the schema | lead | M |
29
+ | **W8** | **Delete v1** | Remove `portable-*`, `source-spec`, `capability-provenance` (v1 parts), `prepared-resources`, migration stores/evidence, the installed tier in `packages.mjs`, classic config activation, their tests and docs; `core.mjs` loses everything that only served v1 | lead | L (mostly deletion) |
30
+ | **W9** | **Framework repos as the first workspace (decisions 18–21)** | `oats-workspace.yaml` v2 in `oats` (drop the six imports; `packages:` pins oats.framework/okf/aweb/jira/linear/authoring/dev **as packages**; `teams:` global/engineering; `defaults`); `oats-membership.yaml` in ALL seven repos; the six soul editions rewritten (`oats.okf: {from: package}` etc. even though the repos are members); **a new expert soul in every package repo** — `okf-expert`, `aweb-expert`, `jira-expert`, `linear-expert`, `authoring-expert`, `dev-expert` (v2 `souls/<name>/`, team global, knows and evolves that capability); `package-catalog.json` kept as the official marketplace (bare-version `packages:` entries resolve through it) | lead (member-repo PRs to their owners; the expert souls' AGENTS.md drafted by me, reviewed by the package owner) | L |
31
+ | **W9b** | **`oats.core` and `oats.setup` rewritten for the new architecture (human, 2026-09-23)** | The two official capabilities' skills and injects are rewritten to teach the *new* model, not patched: **`oats.core`** (`oats-operate`, `oats-souls`, the "you run on OATS" inject) — how an agent works inside an instance under this architecture: its home layout (`.oats/modules/`, `.agents/skills/`, `instance.json` modules/providers), the verbs it will actually use (`status`, `spawn --preview/--provider`, `retire`, `session`, `instance events`, `capabilities`, `souls`, `workspace status`), what a workspace/member/package/team is *from the agent's seat*, drift ("member moved since"), how to find and read other souls. **`oats.setup`** (`oats-config`/`oats-packages` → renamed to what they now are: `oats-workspace`, `oats-packages`, `oats-onboarding`) — the whole architecture and its best practices: workspace ↔ repo handshake and why, `oats-workspace.yaml` / `oats-membership.yaml` / `soul.yaml` v2 / `oats-local.yaml` field by field, teams as labels, member-tier vs package-tier and the non-collapse rule, `packages:` + lock v3 + per-version approval, the official catalog vs `git:` refs, `sync`/`package add`, the `<name>-workspace/` convention and clone-then-spawn, private items, external souls, the standalone case, provider payload homes (soul/machine/spawn), what is deliberately NOT versioned and why. Every skill is validated against the shipped CLI (`oats <verb> --help` snapshot test) so they cannot drift from the commands. | lead (swarm + review) | M |
32
+ | **W10** | **Docs** | **`docs/rebuild-to-v2.md` — the rebuild guide (ships with the schemas; states that 0.24.x keeps spawning 0.24.x deployments)**; `docs/workspaces.md` rewritten around v2; `souls-and-instances.md`, `packages.md`, `configuration.md` (mostly deleted), `first-team.md` → the `<name>-workspace/` convention; DTO doc § Workspace v2; release notes | lead | M |
33
+ | **W11** | **Onboarding skill** | `oats-setup-expert` / `oats.setup`: teach `<name>-workspace/`, `oats sync`, clone-then-spawn; **the hosting rule for mixed public/private organisations (decision 26): ask up front whether any member is private; if so the workspace file is hosted in a private repo that is NOT a public member (a dedicated private `workspace` repo is the honest shape), public contributors get the standalone case with `oats.core` by default (decision 25); `oats onboard` prints the same rule in its next-steps**; the `oats-operate` skill updated for the new verbs | lead | S |
34
+ | **W12** | **Desktop follow-through** | New kernel DTOs (from W7) consumed the usual way — engineer reads the merged head, files pins, wires: Capabilities/Souls origin + team columns; "installed" removed as a state; spawn preview shows modules (from/commit/hash) + team; Workspaces surface shows membership status | Desktop engineer, after W7 | M |
35
+
36
+ **Team review (Antares, 2026-09-23) folded in:** rebuild guide (W10), **second review (two aweb teams, stores, soul layout, standalone, public/private hosting → decisions 23–26: `messaging.byTeam`, stores = repo + provider `root`, `souls/` only, `oats.core` standalone default, private host rule taught by onboarding),** `--provider` instance payload (W6/W7), drift display (W7), duplicate-name rule (W6), 0.24.x-keeps-working stated everywhere.
37
+
38
+ **Not in scope:** K11 (member admission UI/"Join"), K12 (transcript verb), 10B editor/detach — they wait behind this.
39
+
40
+ ## How phases land
41
+
42
+ Each phase = one developer-swarm workflow (parallel agents, disjoint files, against a written module contract) → one adversarial-review workflow (independent reviewers with different lenses) → fixes → my four-gate review → merge to main. No compatibility flag: when a phase lands, its modules are the kernel and the v1 code they replace is deleted in the same PR, with tests rewritten. Between phases main is always green and always the canonical state.
43
+
44
+ ## Releases
45
+
46
+ - **0.24.14** — 10A + 10B-0 (already planned; nothing v2). Ships first, independent.
47
+ - **0.25.0** — W1–W8: the new model is the kernel, v1 deleted as each phase landed; `workspaceApi: 2`. Breaking by design (no migration).
48
+ - **0.26.0** — W9 live (framework repos on v2), W11 onboarding, W12 Desktop.
49
+
50
+ ## Risks and how the plan handles them
51
+
52
+ | Risk | Mitigation |
53
+ |---|---|
54
+ | Remote access context is subtle (SSH vs HTTPS vs `gh` token; private repos) | W2 uses git itself (`git ls-remote`, `git archive`/shallow fetch) with the operator's configured credential helpers — no new credential store; typed `cannot read` never guesses |
55
+ | `core.mjs` entanglement makes W8 risky | W8 is deletion behind a green W7; the seam guarantees nothing live depends on what is deleted; CI + the Northwind fixture + Desktop suites are the gate |
56
+ | Package approval UX ("asks once") in non-interactive spawns | `oats sync` is where approval is asked; `spawn` refuses `E_PACKAGE_UNAPPROVED` pointing at `sync` — never prompts mid-spawn |
57
+ | Latest-state members drift between preview and apply | The resolution records the member commit; apply re-observes and refuses `E_DECISION_STALE` if it moved — same mechanism as today's decision revision |
58
+ | Desktop relying on "installed" | Removed as a state in W12; until then the Capabilities view keeps working on 0.24.x DTOs (contracts unchanged) |
59
+
60
+ ## What I need from you
61
+
62
+ 1. **Approve the plan shape** (parallel v2 beside v1, one switch-over release, then delete) or tell me you want in-place.
63
+ 2. **Confirm I implement the kernel packages W1–W11 myself**, with the Desktop engineer on W12 after W7 — or whether Juan's side should take lanes (W2 remote observation and W5 packages are the most separable).
64
+ 3. **0.25.0 as the breaking release number** — fine?
65
+ 4. Anything in W9 you want different for the framework repos (team labels for the six souls: I'd put the five experts under `global`, `oats-setup-expert` under `global`, and any dev capabilities in `oats-dev` under `engineering`).
@@ -1,20 +1,32 @@
1
1
  # Design documents — navigation
2
2
 
3
- Dated design documents record how decisions were reached and what each implementation slice was bounded to. They are **history with current pointers**: the current architecture is explained in [workspaces](../workspaces.md), [contracts](../layers.md), [souls and instances](../souls-and-instances.md) and [knowledge theory](../knowledge-theory.md); readiness is stated in the [release notes](../release-notes/). When a dated document and a current page disagree, the current page wins.
3
+ Dated design documents record how decisions were reached and what each implementation slice was bounded to. They are **history with current pointers**: the current architecture is explained in [workspaces](../workspaces.md), [packages](../packages.md), [configuration](../configuration.md), [souls and instances](../souls-and-instances.md), [contracts](../layers.md) and [knowledge theory](../knowledge-theory.md); readiness is stated in the [release notes](../release-notes/). When a dated document and a current page disagree, the current page wins.
4
4
 
5
- ## Current plan
5
+ ## Current model — workspace v2 (0.25 line)
6
6
 
7
- - [Redesign program board](2026-09-20-redesign-program-board.md) — live status of every work stream (knowledge contract, workspace adoption, messaging readiness, official capabilities, marketplace, five souls, centralised knowledge, Desktop), owners and blockers.
8
- - [Workspace-first adoption plan (2026-09-20)](2026-09-20-workspace-and-portable-adoption-plan.md) — phase order, lane ownership, distribution work packages (`oats.core`, `oats.setup`, official marketplace), exit gates.
7
+ - **[Workspace module contracts (2026-09-23)](2026-09-23-workspace-module-contracts.md) — NORMATIVE for implementation**: `lib/remote.mjs`, `lib/workspace.mjs`, `lib/resolve.mjs`, `lib/packages.mjs` (lock v3), `lib/materialize.mjs`, the CLI verbs and DTOs, the error codes, the Northwind fixture.
8
+ - [Simplified workspace model — worked example (2026-09-23)](2026-09-23-simplified-workspace-model.md) — ACCEPTED: one workspace per org, membership = trust, `from:` as location, nothing installed, full per-instance copy, teams as labels, harnesses start normally. The Decision concept is `agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md`.
9
+ - [Implementation plan (2026-09-23)](2026-09-23-workspace-v2-implementation-plan.md) — phases A/B/C, what each deletes.
10
+ - Operator-facing: [rebuild guide](../rebuild-to-v2.md) (0.24.x → v2; no converter), [Desktop CLI API — workspace model](../desktop-cli-api.md#workspace-model-workspaceapi-2).
11
+ - Open threads (tracked here until closed): the `workspace` work mode still derives its `./work` boundary from the classic `team:` scope; the readiness `enrolled` producer still reads the 0.24 `oats.yaml` backlink; launch configurations / yolo / work-mode setup are still read from a classic config chain.
9
12
 
10
- ## Portable Souls and Git workspaces — the architecture
13
+ ## Superseded by the workspace model
14
+
15
+ Everything below this line that describes per-soul `source:` provenance, `oats.yaml` exports/imports, the installed-capability tier (`.agents/capabilities/installed/`), `oats-config.yaml` scopes, `oats init`/`use`/`install`/`restore`/`trust`/`migrate`, lock v1/v2 or ambient-skill exclusion at launch is **history**. In particular [package-engine-contract.md](package-engine-contract.md) and [package-runtime-api.md](package-runtime-api.md) describe the removed acquisition/materialization engine; the package tier is now [packages.md](../packages.md) + module contract §4. Capability **manifests**, hooks, the operations contract, provider binding wire/codecs and the knowledge/messaging capability contracts are unchanged.
16
+
17
+ ## Earlier plan (0.24)
18
+
19
+ - [Redesign program board](2026-09-20-redesign-program-board.md) — status of the 0.24 work streams (knowledge contract, workspace adoption, messaging readiness, official capabilities, marketplace, five souls, centralised knowledge, Desktop).
20
+ - [Workspace-first adoption plan (2026-09-20)](2026-09-20-workspace-and-portable-adoption-plan.md) — the 0.24 phase order and distribution work packages (`oats.core`, `oats.setup`, official marketplace).
21
+
22
+ ## Portable Souls and Git workspaces — the 0.24 architecture (superseded)
11
23
 
12
24
  - [Portable Souls explainer](2026-09-14-portable-souls-explainer.md) — the short version.
13
25
  - [Portable souls and Git-backed workspaces](2026-09-14-portable-souls-and-git-workspaces.md) — the accepted architecture.
14
26
  - [Contract amendments (14 Sep)](2026-09-14-portable-souls-contract-amendments.md) · [portable declarations](2026-09-15-portable-declarations.md) · [portable data/digest contract](2026-09-15-portable-data-contract.md) · [source observation](2026-09-15-source-observation.md).
15
- - [Fresh-install-first rollout](2026-09-16-fresh-install-first-rollout.md) · [fresh operator walkthrough](2026-09-16-fresh-operator-walkthrough.md) · [portable onboarding and discovery](2026-09-16-portable-onboarding.md) · [migration evidence](2026-09-16-portable-migration-evidence.md).
27
+ - [Fresh-install-first rollout](2026-09-16-fresh-install-first-rollout.md) · [fresh operator walkthrough](2026-09-16-fresh-operator-walkthrough.md) · [portable onboarding and discovery](2026-09-16-portable-onboarding.md) · [migration evidence](2026-09-16-portable-migration-evidence.md) — the `lib/portable-migration*`, `lib/portable-onboarding-acceptance.mjs` modules and the `portable-onboarding-public` acceptance driver these cite are deleted; each note carries a superseded banner pointing at the [simplified workspace model](2026-09-23-simplified-workspace-model.md).
16
28
 
17
- ## Retained execution — artifacts, approval, capture
29
+ ## Retained execution — artifacts, approval, capture (0.24; artifact/approval parts superseded by lock v3)
18
30
 
19
31
  - [Artifact retention](2026-09-14-artifact-retention-contract.md) · [selection lock and approval](2026-09-15-selection-lock-and-approval.md) · [captured resolution records](2026-09-15-captured-resolution-records.md).
20
32
  - [Package preparation](2026-09-15-package-preparation.md) · [command/curriculum preparation](2026-09-16-command-profile-preparation.md) · [prepare request transport](2026-09-16-prepare-request-transport.md) · [public prepare request](2026-09-17-public-prepare-request.md).
@@ -26,7 +38,7 @@ Dated design documents record how decisions were reached and what each implement
26
38
 
27
39
  ## Capabilities and providers
28
40
 
29
- - [Package engine contract](package-engine-contract.md) · [package-runtime API](package-runtime-api.md) · [operations contract](operations-contract.md) · [launch configurations](launch-configurations.md).
41
+ - [Package engine contract](package-engine-contract.md) · [package-runtime API](package-runtime-api.md) — **superseded** (installed tier removed; see [packages](../packages.md)) · [operations contract](operations-contract.md) · [launch configurations](launch-configurations.md).
30
42
  - [Provider binding wire v1](2026-09-16-provider-binding-wire.md) · [provider binding codecs](2026-09-16-provider-binding-codecs.md) · [capability helper/input contract](2026-09-17-capability-helper-input-contract.md).
31
43
  - [Knowledge capability contract](2026-09-16-knowledge-capability-contract.md) · [messaging capability contract](2026-09-16-messaging-capability-contract.md).
32
44
 
@@ -88,6 +88,7 @@ name the provider answered is kept for `oats schedule reconcile`.
88
88
 
89
89
  ## Mutations
90
90
 
91
+ *(0.24 only — `oats use` was removed by the workspace model v2; activation is the soul's `capabilities:` + workspace defaults. Kept for history.)*
91
92
  `oats use ... --json` answers `{ capability, action: enable|disable|
92
93
  layer-none|inherit, target, layer, level, file, settings, before, after
93
94
  {..., effective}, remaining?, note?, missingRequires }`. `--inherit`
@@ -1,6 +1,6 @@
1
1
  # Package engine contract (capability materialization, lock v2)
2
2
 
3
- Status: **FROZEN** for the capability-materialization delivery. This document is
3
+ Status: **SUPERSEDED** by the workspace model v2 ([2026-09-23-workspace-module-contracts.md](2026-09-23-workspace-module-contracts.md) §4–5, [../packages.md](../packages.md)): the installed-capability tier, `oats install`/`restore`/`use`/`trust`, config templates and lock v2 described here were removed; the package tier is now `lib/packages.mjs` (lock v3) + `lib/materialize.mjs`. Kept as history. Original status: **FROZEN** for the capability-materialization delivery. This document is
4
4
  the resolver / projection / lock API that the config-and-CLI lane builds
5
5
  against. It implements the accepted Decision "Packages materialize capabilities
6
6
  while config templates remain explicitly adopted local policy" (2026-07-29) as
@@ -1,6 +1,6 @@
1
1
  # Package-runtime API contract (addendum to the package-engine contract)
2
2
 
3
- Status: **FROZEN** for the capability-materialization delivery, as an addendum to
3
+ Status: **SUPERSEDED** with its parent (workspace model v2 — see [2026-09-23-workspace-module-contracts.md](2026-09-23-workspace-module-contracts.md)); kept as history. Original status: **FROZEN** for the capability-materialization delivery, as an addendum to
4
4
  [`package-engine-contract.md`](./package-engine-contract.md). It answers the
5
5
  maintainer's four clarifications on the M1 freeze (maintainer review of
6
6
  1db919b): the public package-runtime boundary, the npm runtime closure,