@awebai/oats 0.29.3 → 0.30.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 (232) hide show
  1. package/README.md +12 -6
  2. package/bin/oats.mjs +203 -54
  3. package/capabilities/oats-aweb/bin/oats-aweb.mjs +538 -204
  4. package/capabilities/oats-aweb/injects/aweb.md +1 -1
  5. package/capabilities/oats-aweb/lib/binding-wire.mjs +31 -22
  6. package/capabilities/oats-aweb/oats.json +5 -12
  7. package/capabilities/oats-aweb/skills/VENDORED.md +4 -4
  8. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +1 -1
  9. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +83 -13
  10. package/capabilities/oats-code-review/injects/reviewer.md +26 -0
  11. package/capabilities/oats-code-review/oats.json +16 -0
  12. package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +66 -0
  13. package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +30 -0
  14. package/capabilities/oats-code-review/skills/security-review/SKILL.md +56 -0
  15. package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +34 -0
  16. package/capabilities/oats-developer/injects/developer.md +38 -0
  17. package/capabilities/oats-developer/oats.json +17 -0
  18. package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +43 -0
  19. package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +47 -0
  20. package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +65 -0
  21. package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +37 -0
  22. package/capabilities/oats-developer/skills/worktrees/SKILL.md +36 -0
  23. package/capabilities/oats-engineering-expert/injects/expert.md +37 -0
  24. package/capabilities/oats-engineering-expert/oats.json +17 -0
  25. package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +37 -0
  26. package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +52 -0
  27. package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +50 -0
  28. package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +53 -0
  29. package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +49 -0
  30. package/capabilities/oats-okf/bin/oats-okf.mjs +16 -9
  31. package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
  32. package/capabilities/oats-okf/lib/config.mjs +2 -1
  33. package/capabilities/oats-okf/lib/consult.mjs +26 -4
  34. package/capabilities/oats-okf/lib/harvest-switch.mjs +16 -3
  35. package/capabilities/oats-okf/lib/inspection.mjs +26 -7
  36. package/capabilities/oats-okf/lib/io.mjs +9 -1
  37. package/capabilities/oats-okf/lib/sources.mjs +34 -3
  38. package/capabilities/oats-okf/lib/stores.mjs +8 -6
  39. package/capabilities/oats-okf/lib/worker.mjs +7 -17
  40. package/capabilities/oats-okf/oats.json +6 -3
  41. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
  42. package/capabilities/oats-okf-harvest/oats.json +3 -3
  43. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
  44. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +37 -16
  45. package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
  46. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +6 -1
  47. package/capabilities/oats-okf-maintenance/oats.json +2 -2
  48. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +17 -2
  49. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
  50. package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
  51. package/capabilities/oats-workspace-experts/oats.json +9 -0
  52. package/docs/capabilities.md +160 -171
  53. package/docs/capability-manifest.schema.json +7 -10
  54. package/docs/configuration.md +213 -64
  55. package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
  56. package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
  57. package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
  58. package/docs/design/2026-09-27-team-model-v2.md +116 -0
  59. package/docs/design/2026-09-28-automations-trust.md +38 -0
  60. package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
  61. package/docs/design/HISTORY.md +65 -0
  62. package/docs/design/README.md +23 -54
  63. package/docs/desktop-cli-api.md +1787 -1777
  64. package/docs/desktop.md +30 -91
  65. package/docs/execution-targets.md +146 -292
  66. package/docs/first-team.md +31 -17
  67. package/docs/implementation.md +76 -288
  68. package/docs/integrations.md +118 -320
  69. package/docs/knowledge-capability-authoring.md +25 -52
  70. package/docs/knowledge-reference/acceptance.md +3 -3
  71. package/docs/knowledge-reference/adoption.md +1 -1
  72. package/docs/knowledge-reference/harvester.md +2 -2
  73. package/docs/knowledge-reference/package-craft.md +3 -3
  74. package/docs/knowledge-reference/provider-mapping.md +3 -6
  75. package/docs/knowledge-reference/reader-capture.md +3 -3
  76. package/docs/knowledge-theory.md +62 -166
  77. package/docs/knowledge.md +225 -404
  78. package/docs/layers.md +42 -97
  79. package/docs/oats-local.schema.json +58 -5
  80. package/docs/oats-membership.schema.json +1 -8
  81. package/docs/oats-package.schema.json +5 -5
  82. package/docs/oats-workspace.schema.json +8 -22
  83. package/docs/official-catalog.md +25 -28
  84. package/docs/packages.md +45 -63
  85. package/docs/plans/0.30-close-out.md +61 -0
  86. package/docs/release-lane.md +77 -0
  87. package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
  88. package/docs/release-notes/v0.19.0.md +48 -147
  89. package/docs/release-notes/v0.19.1.md +2 -3
  90. package/docs/release-notes/v0.19.3.md +2 -15
  91. package/docs/release-notes/v0.20.0.md +0 -15
  92. package/docs/release-notes/v0.22.0.md +71 -138
  93. package/docs/release-notes/v0.22.1.md +42 -90
  94. package/docs/release-notes/v0.22.10.md +1 -1
  95. package/docs/release-notes/v0.22.11.md +1 -47
  96. package/docs/release-notes/v0.22.12.md +4 -13
  97. package/docs/release-notes/v0.22.13.md +1 -42
  98. package/docs/release-notes/v0.22.14.md +3 -11
  99. package/docs/release-notes/v0.22.15.md +1 -46
  100. package/docs/release-notes/v0.22.16.md +6 -8
  101. package/docs/release-notes/v0.22.18.md +1 -99
  102. package/docs/release-notes/v0.22.19.md +3 -14
  103. package/docs/release-notes/v0.22.2.md +6 -15
  104. package/docs/release-notes/v0.22.3.md +0 -1
  105. package/docs/release-notes/v0.22.4.md +1 -14
  106. package/docs/release-notes/v0.22.5.md +2 -12
  107. package/docs/release-notes/v0.22.6.md +0 -3
  108. package/docs/release-notes/v0.23.0.md +9 -25
  109. package/docs/release-notes/v0.23.1.md +9 -25
  110. package/docs/release-notes/v0.23.2.md +2 -4
  111. package/docs/release-notes/v0.24.0.md +56 -97
  112. package/docs/release-notes/v0.24.1.md +7 -11
  113. package/docs/release-notes/v0.24.10.md +34 -45
  114. package/docs/release-notes/v0.24.11.md +12 -20
  115. package/docs/release-notes/v0.24.12.md +35 -48
  116. package/docs/release-notes/v0.24.13.md +34 -41
  117. package/docs/release-notes/v0.24.2.md +9 -13
  118. package/docs/release-notes/v0.24.3.md +7 -11
  119. package/docs/release-notes/v0.24.4.md +6 -6
  120. package/docs/release-notes/v0.24.5.md +6 -10
  121. package/docs/release-notes/v0.24.6.md +2 -5
  122. package/docs/release-notes/v0.24.7.md +46 -75
  123. package/docs/release-notes/v0.24.8.md +58 -96
  124. package/docs/release-notes/v0.24.9.md +38 -54
  125. package/docs/release-notes/v0.25.0.md +59 -76
  126. package/docs/release-notes/v0.25.1.md +57 -81
  127. package/docs/release-notes/v0.25.2.md +51 -70
  128. package/docs/release-notes/v0.25.3.md +11 -13
  129. package/docs/release-notes/v0.25.4.md +9 -13
  130. package/docs/release-notes/v0.25.5.md +3 -5
  131. package/docs/release-notes/v0.25.6.md +20 -29
  132. package/docs/release-notes/v0.25.7.md +5 -7
  133. package/docs/release-notes/v0.25.8.md +26 -39
  134. package/docs/release-notes/v0.26.0.md +175 -646
  135. package/docs/release-notes/v0.27.0.md +4 -5
  136. package/docs/release-notes/v0.27.1.md +4 -6
  137. package/docs/release-notes/v0.27.2.md +1 -1
  138. package/docs/release-notes/v0.28.0.md +57 -124
  139. package/docs/release-notes/v0.29.0.md +89 -208
  140. package/docs/release-notes/v0.29.1.md +1 -1
  141. package/docs/release-notes/v0.29.2.md +3 -4
  142. package/docs/release-notes/v0.29.4.md +90 -0
  143. package/docs/release-notes/v0.30.0.md +205 -0
  144. package/docs/schedules.md +280 -349
  145. package/docs/servers.md +99 -117
  146. package/docs/soul.schema.json +2 -9
  147. package/docs/souls-and-instances.md +145 -158
  148. package/docs/workspaces.md +132 -215
  149. package/lib/automations.mjs +28 -6
  150. package/lib/core.mjs +226 -74
  151. package/lib/instance-events.mjs +1 -1
  152. package/lib/instance-inspect.mjs +109 -34
  153. package/lib/instance-lifecycle.mjs +14 -1
  154. package/lib/instance-resolution.mjs +26 -27
  155. package/lib/launch-preference.mjs +87 -0
  156. package/lib/materialize.mjs +3 -3
  157. package/lib/packages.mjs +2 -5
  158. package/lib/resolve.mjs +29 -87
  159. package/lib/schedule.mjs +32 -18
  160. package/lib/teams-verbs.mjs +195 -0
  161. package/lib/teams.mjs +190 -0
  162. package/lib/triggers.mjs +53 -17
  163. package/lib/workspace.mjs +54 -147
  164. package/package-catalog.json +9 -15
  165. package/package.json +1 -1
  166. package/skills/oats-getting-started/SKILL.md +25 -13
  167. package/capabilities/oats-review/injects/review.md +0 -69
  168. package/capabilities/oats-review/oats.json +0 -10
  169. package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
  170. package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
  171. package/docs/conventions.md +0 -90
  172. package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
  173. package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
  174. package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
  175. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
  176. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
  177. package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
  178. package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
  179. package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
  180. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
  181. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
  182. package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
  183. package/docs/design/2026-09-15-captured-dispatch.md +0 -127
  184. package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
  185. package/docs/design/2026-09-15-package-preparation.md +0 -100
  186. package/docs/design/2026-09-15-portable-data-contract.md +0 -121
  187. package/docs/design/2026-09-15-portable-declarations.md +0 -189
  188. package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
  189. package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
  190. package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
  191. package/docs/design/2026-09-15-source-observation.md +0 -119
  192. package/docs/design/2026-09-16-captured-admission.md +0 -77
  193. package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
  194. package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
  195. package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
  196. package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
  197. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
  198. package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
  199. package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
  200. package/docs/design/2026-09-16-portable-onboarding.md +0 -179
  201. package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
  202. package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
  203. package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
  204. package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
  205. package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
  206. package/docs/design/2026-09-17-captured-native-start.md +0 -58
  207. package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
  208. package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
  209. package/docs/design/2026-09-17-public-captured-start.md +0 -108
  210. package/docs/design/2026-09-17-public-prepare-request.md +0 -90
  211. package/docs/design/2026-09-18-captured-pi-host.md +0 -205
  212. package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
  213. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
  214. package/docs/design/2026-09-20-redesign-program-board.md +0 -142
  215. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
  216. package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
  217. package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
  218. package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
  219. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
  220. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
  221. package/docs/design/2026-09-24-phase-d-plan.md +0 -305
  222. package/docs/design/2026-09-25-teams-contract.md +0 -258
  223. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
  224. package/docs/design/desktop-ux-plan.md +0 -362
  225. package/docs/design/launch-configurations.md +0 -168
  226. package/docs/design/okf-mirror-provenance.md +0 -105
  227. package/docs/design/operations-contract.md +0 -141
  228. package/docs/oats-member.schema.json +0 -38
  229. package/skills/integration-authoring/SKILL.md +0 -84
  230. package/skills/oats-support/SKILL.md +0 -79
  231. package/skills/skill-craft/SKILL.md +0 -109
  232. package/skills/soul-craft/SKILL.md +0 -116
@@ -1,613 +1,446 @@
1
- # Workspace model — module contracts (the spec the implementation is built against)
1
+ # Workspace model: module contracts
2
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)
3
+ **Status:** normative for `lib/remote.mjs`, `lib/workspace.mjs`, `lib/resolve.mjs`, `lib/packages.mjs`,
4
+ `lib/materialize.mjs` and the CLI verbs built on them. When this record and a reference page
5
+ (for example [workspaces](../workspaces.md) or [packages](../packages.md)) disagree about
6
+ operator-visible behaviour, the reference page wins.
4
7
 
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
+ Conventions: ESM, Node 22+, no runtime dependency beyond `yaml` and `node:*`. Errors are
9
+ `oatsError(code, message, details)` with stable `E_*` codes. Commits are full 40-hex OIDs, digests
10
+ `sha256-<hex>`. Only `lib/remote.mjs` (to `git`) and the launch path shell out. Modules that read a
11
+ remote take `{ remote, remoteOptions }`: `remote` defaults to `lib/remote.mjs` (tests inject a fake);
12
+ `remoteOptions` (`cacheDir`, `exec`, `transport`) reaches every remote call.
8
13
 
9
14
  ---
10
15
 
11
16
  ## 1. `lib/remote.mjs` — observe Git remotes in the operator's access context
12
17
 
13
- Supersedes: `repository-observation.mjs` (v1 parts), `source-spec.mjs`.
18
+ ### 1.1 Repo refs and keys
19
+
20
+ `parseRepoRef(text, options?) → { host, path, url, key }` or `E_REPO_REF`. Accepted forms:
21
+ `git:host/org/repo`, `https://host/org/repo`, `git@host:org/repo.git`, `ssh://[user@]host[:port]/org/repo`,
22
+ `/abs/bare.git` and `file:///abs/path`. The fetch `url` keeps SSH forms as written, canonicalizes HTTPS,
23
+ and fetches `git:` over HTTPS unless `options.transport === "ssh"`.
24
+
25
+ `key` is the identity everywhere: `<host>/<path>` with a lowercase host, no scheme, no `.git`; a local
26
+ remote's key is `local/<abs-path>`. Modules compare keys, never URLs. The path part is case-sensitive.
27
+
28
+ ### 1.2 Reads
14
29
 
15
30
  ```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.
31
+ observeRemote(ref, { at }) // → { key, url, commit, ref, observedAt }
32
+ readRemoteFile(ref, commit, path) // → { bytes, size }
33
+ listRemoteTree(ref, commit, dir, { depth = 2 }) // → [{ path, type: "blob"|"tree"|"symlink", size? }]
34
+ fetchRemoteTree(ref, commit, dir, destDir, { allowSymlinks }) // → { files, bytes, digest }
35
+ contentDigest(dir, { allowSymlinks }) // → "sha256-<hex>"
45
36
  ```
46
37
 
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.
38
+ - `observeRemote`: `at` absent or `HEAD` is the default branch; else a full OID or a plain tag or branch
39
+ name (revision syntax or a leading `-` is `E_REPO_REF`). `commit` is always the peeled commit. No match,
40
+ or a non-commit object, is `E_REMOTE_UNREADABLE { reason: "not-found" }`.
41
+ - `readRemoteFile`: missing or a directory → `E_REMOTE_PATH_MISSING`; a symlink →
42
+ `E_REMOTE_TREE_UNSAFE { why: "symlink" }`; over 4 MiB → `E_REMOTE_FILE_OVERSIZE { path, size, budget }`.
43
+ - `listRemoteTree`: a missing `dir` is `[]`; submodules are omitted; entries beyond `depth` are dropped
44
+ before names are checked.
45
+ - `fetchRemoteTree`: `destDir` must not exist; a missing `dir` is `E_REMOTE_PATH_MISSING`. Every entry is checked before anything is written, then
46
+ the copy is staged and renamed in. `E_REMOTE_TREE_UNSAFE { why }`: `path` (a name component empty,
47
+ `.`, `..`, `.git` or containing `\` or NUL), `collision` (equal under NFC and case folding), `symlink`
48
+ (unless `allowSymlinks` admits it and it stays inside the tree), `device`, `oversize` (over 64 MiB),
49
+ `exists`. The kernel admits one symlink (`OATS_ALIAS_SYMLINK`): `CLAUDE.md` → `AGENTS.md`.
50
+
51
+ **Content digest.** SHA-256 over `"F" NUL relpath NUL mode NUL size NUL bytes NUL` per file in byte-wise
52
+ path order, `mode` normalized to `755` or `644` (a checkout digests like the fetched tree). An admitted
53
+ symlink enters as `symlink:<target>`; empty directories and a top-level `.git/` do not count.
54
+
55
+ ### 1.3 Access, cache, failures
56
+
57
+ - Git uses the operator's own configuration and credentials; `GIT_TERMINAL_PROMPT=0`,
58
+ `GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call.
59
+ - A commit is fetched depth 1 (no blob filter) into a bare cache `<cacheDir>/<sha256(key)>/` (default
60
+ `~/.cache/oats/remotes`; the CLI honours `OATS_REMOTE_CACHE`) and pinned as `refs/oats/commits/<oid>`.
61
+ The cache may be wiped at any time. Operations on one cache repo are serialized.
62
+ - Failures: `E_REMOTE_UNREADABLE { url, key, reason }`, `reason` ∈ `auth`, `not-found`, `network`,
63
+ `timeout`, `unknown`. Nothing half-succeeds.
57
64
 
58
65
  ---
59
66
 
60
67
  ## 2. `lib/workspace.mjs` — declarations, membership, discovery
61
68
 
62
- Supersedes: `workspace-definition.mjs`, `workspace-discovery.mjs`, `portable-*.mjs` (declaration parts), `capability-provenance.mjs` (v1), `config-data.mjs` activation parsing.
69
+ ### 2.1 Declaration files
63
70
 
64
- ### Files (schemas in `docs/*.schema.json`, validated with the existing validator style)
71
+ Schemas are in `docs/*.schema.json`; the `validate*` functions add the domain rules and are the
72
+ authority. Only `schemaVersion: 2` is read.
65
73
 
66
- `oats-workspace.yaml` (schemaVersion 2):
67
74
  ```yaml
75
+ # oats-workspace.yaml (committed in the host)
68
76
  schemaVersion: 2
69
77
  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 } }
78
+ members: [<repo ref>] # no @revision
79
+ packages: { <id>: v2.1.3 | git:<repo>@<ref> }
80
+ teams: { <label>: { team?: <provider team id>, description? } } # shared teams
73
81
  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
+ capabilities: { <cap>: { from: <repo key> | package } | off }
83
+ knowledge | messaging | tasks: { <cap>: { from } } | none # at most one entry
84
+ stores: { <name>: <repo ref> }
85
+ messaging: <opaque provider payload>
86
+ external: [{ source: <repo ref>@<full OID>, soul: <repo-relative dir> }]
82
87
  ```
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
88
 
89
- `oats-membership.yaml`: `{ schemaVersion: 2, workspace: <repo ref>, team?: <label> }` — nothing else.
89
+ - `oats-membership.yaml` (in each member): `{ schemaVersion: 2, workspace: <repo ref> }`, the backlink.
90
+ - `souls/<name>/soul.yaml`: `name` (equal to the directory), `description`, `work`
91
+ (`worktree | checkout | directory | workspace`), `capabilities` (`{ from: here | package | <repo key> }`
92
+ or `off`), slot payloads (`knowledge | messaging | tasks`: opaque or `none`), `compatibility`.
93
+ `private:` is ignored with the warning `soul-private-ignored`.
94
+ - `oats-local.yaml` (per machine): `workspace` (required), `standalone`, `clones`, `settings`,
95
+ `souls.disabled`, local team keys, host keys ([configuration](../configuration.md)). `loadLocal(dir)`
96
+ walks up to it: none is `E_LOCAL_MISSING`; an `oats-config.yaml` on the way is `E_CONFIG_BROKEN`.
97
+
98
+ **Teams.** Shared teams and their provider ids are in `oats-workspace.yaml` `teams:`. Local teams,
99
+ `defaultTeam`, `souls.teams` and `souls.default` are in `oats-local.yaml`. There is no `team:` in soul,
100
+ membership or `external[]` entries, no `byTeam`, and no kernel-defined team. See
101
+ [Team model v2](2026-09-27-team-model-v2.md).
102
+
103
+ ### 2.2 Validation rules
104
+
105
+ - **Absolute paths** are refused in ref and path fields only (`members`, `packages`, `stores`,
106
+ `external[].source` and `.soul`, `defaults.*.from`), never in descriptions or `messaging`.
107
+ `file:///…` and `git:/abs/bare.git@<ref>` are repo refs, not paths.
108
+ - **`from:`** is `package`, `here` (souls only) or a key spelled exactly as `parseRepoRef(ref).key`.
109
+ - Members must parse and not repeat. `packages:` values have exactly two forms (§2.6).
110
+ - **Removed keys** (`defaults.byTeam`, `messaging.byTeam`, `external[].team`, `team` in membership and
111
+ soul files, top-level `byTeam` in a soul slot payload or `settings.<cap>`) are problems with
112
+ `reason: "removed-key"`.
113
+ - Invalid files raise `E_WORKSPACE_SCHEMA { path, repoKey, commit, problems[] }`.
114
+
115
+ ### 2.3 Membership
116
+
117
+ `confirmMembership(workspaceObs, memberRef)` reads the member's `oats-membership.yaml` at its default
118
+ branch, in the same access context → `{ key, commit, confirmed: true }` or `{ key, confirmed: false,
119
+ reason, detail }`, `reason` ∈ `not-listed`, `cannot-read`, `no-backlink` (missing, invalid or unusable
120
+ file) and `backlink-elsewhere` (`caseOnly: true` when only letter case differs). It throws only for an
121
+ invalid workspace file.
122
+
123
+ ### 2.4 Discovery
90
124
 
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> }
125
+ ```js
126
+ discoverWorkspace(ref, { at, local, lock })
127
+ // → { workspace, key, url, commit, observedAt, members, external, packageSouls, problems, warnings }
128
+ // member row: { key, ref, commit, confirmed, reason?, detail?, souls, capabilities, publishes }
129
+ // SoulEntry: { name, path, repoKey, commit, private: false, definition }
130
+ // CapEntry: { name, path, repoKey, commit, private, manifest }
100
131
  ```
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
132
 
108
- `oats-local.yaml`: `{ schemaVersion: 2, workspace: <repo ref>, clones?: { <repo key>: <abs path> }, settings?: { <cap>: { <key>: <value> } }, souls?: { disabled: [<name>] } }`.
133
+ - A repo without `oats-workspace.yaml` is `E_WORKSPACE_SCHEMA { notAHost: true }`.
134
+ - An unconfirmed member contributes only its row. A failure or invalid item in one member is a problem,
135
+ never an abort; of two same-named items in one repo, the second is a problem.
136
+ - Members are read at their latest commit. A capability manifest needs `capability`
137
+ (`^[a-z0-9][a-z0-9._-]*$`) and `version` and must pass the kernel manifest contract, or it is not listed.
138
+ - `publishes: { package, version } | null` reports a member's `oats-package/` (§2.6).
139
+ - Package souls come from the lock, at the locked commit, for still-declared packages: `package`,
140
+ `version`, `qualifiedName` (`<package>/<soul>`), `agentName` (`<package>--<soul>`), `digest`.
141
+ - `external[]` souls are read at the pinned OID.
142
+
143
+ ### 2.5 Standalone view
144
+
145
+ A standalone view is a member whose workspace cannot be read. `discoverOrStandalone(local)` builds it
146
+ when `oats-local.yaml` says `standalone:` (`standaloneReason: "explicit"`), or when `workspace:` names a
147
+ member whose host fails with `auth` or `not-found` (`"unreadable-host"`, with `hostFailure { code,
148
+ reason, url }`); `network` and `timeout` propagate. The repo must carry `oats-membership.yaml`. The view
149
+ has `standalone: true`, `workspace: null` and one unconfirmed row (`cannot-read`); souls keep only
150
+ `from: here` capabilities, plus `oats.core: { from: package }` unless the soul mentions `oats.core`.
151
+
152
+ ### 2.6 Member vs package tier — the non-collapse rule
153
+
154
+ A repository may be a member (its `souls/*` and `capabilities/*`, latest commit) and publish a package
155
+ (its `oats-package/`, consumed only through `packages:`, versioned and locked). They never collapse:
156
+
157
+ - `from: <repo key>` looks only at `capabilities/<name>/oats.json`. A name found only in the repo's
158
+ package is `E_CAPABILITY_MISSING` with `details.hint: "provided by package <id>; use from: package"`.
159
+ - `from: package` looks only in the lock, even when the package's repo is a member.
160
+ - A `packages:` value is a **bare version** (`v2.1.3`, through the official catalog) or
161
+ **`git:<repo>@<ref>`** (`<repo>` any §1 form, `<ref>` a tag or full OID). There is no third form;
162
+ `classifyPackageValue` is the one grammar. A branch is refused (§4.3).
109
163
 
110
- ### API
164
+ ---
111
165
 
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
- ```
166
+ ## 3. `lib/resolve.mjs` — from a soul to an immutable resolution
141
167
 
142
- ---
168
+ `resolveSoul(discovery, soulEntry, { local, lock, spawn: { providers }, catalog })` → Resolution.
143
169
 
144
- ### Member vs package tier — the non-collapse rule (decisions 19–21)
170
+ ### 3.1 Membership gate
145
171
 
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:
172
+ The soul must be one the discovery lists: a confirmed member's (same `repoKey`, `name`, `commit`; or the
173
+ repo's own, standalone), an `external[]` soul, or a listed package soul. Otherwise `E_NOT_A_MEMBER`,
174
+ `E_MEMBERSHIP_UNCONFIRMED { reason }` (`reason: "stale"` for an entry the row lacks), or
175
+ `E_PACKAGE_MISSING { reason: "stale" }`. An in-memory lock is validated (`E_LOCK_SCHEMA`).
147
176
 
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.
177
+ ### 3.2 Composition and lookups
152
178
 
153
- ## 3. `lib/resolve.mjs` — from a soul to an immutable resolution
179
+ Order, soul wins, `off` removes: `defaults.<slot>` ⊕ `defaults.capabilities` ⊕ `soul.capabilities`.
180
+ Teams compose nothing: composition is the same for every person and machine.
154
181
 
155
- Supersedes: `resolution-shape.mjs`, `prepared-resources.mjs`, `prepare-composition.mjs` (declaration parts), the `capabilities.layers/additive` derivation in `core.mjs`.
182
+ - `from: here`: the soul's repo; in a package soul, its own locked package.
183
+ - `from: <repo key>`: a confirmed member (`E_NOT_A_MEMBER`) listing it (`E_CAPABILITY_MISSING`). A
184
+ `private` capability serves only its own repo's souls (`E_CAPABILITY_PRIVATE`).
185
+ - `from: package`: a lock (`E_PACKAGE_MISSING { reason: "no-lock" }`), one locked provider
186
+ (`E_PACKAGE_MISSING`, `{ ambiguous }` for two), still declared (`reason: "undeclared"`). The package
187
+ must list exactly the lock's capabilities (`E_PACKAGE_INTEGRITY { why: "capabilities" }`).
188
+
189
+ Declaring a package is the trust decision; there is no approval gate. Member capabilities are trusted by
190
+ membership and their hooks run on every operator's machine, so a mixed public/private organisation keeps
191
+ executable capabilities in packages or private members.
192
+
193
+ ### 3.3 Slots
194
+
195
+ - A module with `layer: <slot>` fills that slot; two are `E_SLOT_CONFLICT`.
196
+ - A slot default must be of that layer (`E_SLOT_CONFLICT { reason: "layer-mismatch" }`).
197
+ - A soul's `<slot>: none` drops `defaults.<slot>` and every capability of that layer the workspace
198
+ defaults contributed. One the soul itself declares beside `none` is `E_SLOT_CONFLICT { reason: "none" }`.
199
+
200
+ ### 3.4 Payloads
201
+
202
+ Merged per module, later wins (objects deep-merge): manifest `settings.<key>.default` ⊕
203
+ `workspace.messaging` (messaging layer) ⊕ soul slot payload ⊕ `oats-local.yaml settings.<cap>` ⊕
204
+ `--provider <cap> k=v`. All refusals are `E_WORKSPACE_SCHEMA` with a `reason`:
205
+
206
+ - `poison-key`: `__proto__`, `constructor` or `prototype` at any depth;
207
+ - `removed-key`: top-level `byTeam` (any segment of a `--provider` key);
208
+ - `host-only-key`: a manifest `hostOnly` key outside `oats-local.yaml`, so host paths stay out of
209
+ committed files;
210
+ - `setting-value`: a value outside `settings.<key>.values`.
211
+
212
+ `--provider` for a capability the soul does not resolve is `E_CAPABILITY_MISSING`. The kernel merges and
213
+ delivers; what a provider consumes is its own contract. Teams reach providers as `OATS_DEFAULT_TEAM*` and
214
+ `OATS_TEAMS`, not in settings.
215
+
216
+ ### 3.5 Checks
217
+
218
+ - `skills[]` entries must hold `SKILL.md` or `<skill>/SKILL.md` (`E_CAPABILITY_MISSING` or
219
+ `E_PACKAGE_MANIFEST`); a skill name twice is `E_SKILL_DUPLICATE`.
220
+ - `compatibility.oats` must admit the kernel (`E_CAPABILITY_INCOMPATIBLE`); `agents:` in a manifest is
221
+ `E_CAPABILITY_AGENTS_REMOVED`.
222
+ - `soul.compatibility` floors apply to package versions; an OID or non-version pin is
223
+ `E_COMPATIBILITY { why: "unversioned" }`.
224
+
225
+ ### 3.6 The Resolution
156
226
 
157
227
  ```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.
228
+ { resolutionApi: 1, soul: { name, repoKey, commit, path },
229
+ modules: [{ name, layer, private, dir, manifest,
230
+ from: { kind: "member", repoKey, commit }
231
+ | { kind: "package", package, version, commit, integrity, repoKey } }],
232
+ slots, skills, injects, payloads, payloadOrigins,
233
+ teams, defaultTeam, slotsFrom, capabilitiesFrom, turnedOff, declRevision, payloadRevision, revision }
178
234
  ```
179
235
 
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.
236
+ `module.dir` is the listed capability directory (for a package, the `oats-package.json#capabilities[]`
237
+ entry). `declRevision` covers `{ resolutionApi, soul, modules, slots, skills, injects }`,
238
+ `payloadRevision` the payloads, `revision` both (24 hex of SHA-256 over canonical JSON). A spawn decision
239
+ binds `revision`. Provenance fields enter neither revision.
186
240
 
187
241
  ---
188
242
 
189
- ## 4. `lib/packages.mjs` — versions, lock v3, approval (rewritten in place)
243
+ ## 4. `lib/packages.mjs` — packages and lock v3
190
244
 
191
- Supersedes itself (v1: installed tier, `install/restore/use`, lock v2).
245
+ Nothing is installed; `oats-lock.json` and `oats-local.yaml` are the only persisted deployment state.
246
+
247
+ ### 4.1 Lock v3
192
248
 
193
249
  ```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)
250
+ { lockfileVersion: 3, packages: { <id>: {
251
+ source: "catalog:<id>" | "git:<key>@<ref>", url, path, version, commit, integrity,
252
+ capabilities: [<cap>], souls?: [{ name, path, digest }] } } }
217
253
  ```
218
254
 
219
- `oats-lock.json` is the only persisted state at the deployment besides `oats-local.yaml`.
255
+ `readLock` (missing file: empty lock) and `writeLock` (atomic, canonical). `url` lets spawn work without
256
+ a catalog; `integrity` is the content digest of the tree at `path`. Any other `lockfileVersion` is
257
+ `E_LOCK_SCHEMA`. A legacy `approved` field is ignored and dropped on write.
258
+
259
+ ### 4.2 `resolvePackages(workspace, { catalog, lock })`
260
+
261
+ A catalog version resolves through `package-catalog.json` shipped with the kernel
262
+ (`OATS_PACKAGE_CATALOG` overrides; unknown id: `E_PACKAGE_MISSING`); a git value reads `oats-package/`.
263
+ Each entry's manifests, souls and integrity are read at the observed commit →
264
+ `{ lock, changes: [{ id, from, to, commit }] }` (`to: null`: no longer declared).
265
+
266
+ - A branch is `E_PACKAGE_INTEGRITY { why: "branch" }`.
267
+ - An unchanged version, source and path is re-verified: a moved commit or different integrity,
268
+ capabilities or souls is `E_PACKAGE_INTEGRITY`.
269
+ - A malformed package is `E_PACKAGE_MANIFEST`.
270
+
271
+ `packageProviding(lock, cap)` → `{ id, entry }`, `null` or `E_PACKAGE_MISSING { ambiguous }`.
272
+
273
+ ### 4.3 Trust and removed APIs
274
+
275
+ There is no approval step or record: declaring a package in `packages:` is the trust decision, and the
276
+ lock pins commit and integrity. `oats sync --approve` is `E_BAD_ARGS`. Pre-workspace exports remain as
277
+ shims that throw `E_REMOVED { name, contract }`, pointing at this record.
220
278
 
221
279
  ---
222
280
 
223
281
  ## 5. `lib/materialize.mjs` — copy whole, compose, record
224
282
 
225
- Supersedes: the module/skill assembly in `core.mjs` spawn (`prepared` copies, symlinked skill sets, ambient exclusion), `capability-artifacts.mjs`.
283
+ ### 5.1 `materialize(resolution, home, options)`
284
+
285
+ 1. Fetch each module at `from.commit` from `module.dir` into `<home>/.oats/modules/<name>/`. A package
286
+ must match the lock's commit (`E_MATERIALIZE_INTEGRITY { why: "lock" }`); an unknown repo is
287
+ `E_MATERIALIZE_SOURCE`.
288
+ 2. The copy's digest must equal the fetch's and any `module.digest` (`E_MATERIALIZE_INTEGRITY`).
289
+ 3. Copy skills whole to `<home>/.agents/skills/<name>/<skill>/`.
290
+ 4. Compose `<home>/AGENTS.md` = soul body (`options.soulAgentsMd` or `soulDir`) + kernel blocks + module
291
+ injects; operating guidance comes from a module such as `oats.core`. Keep `CLAUDE.md → AGENTS.md`
292
+ and `.claude/skills → ../.agents/skills`.
293
+ 5. Record `modules: { <name>: { from, commit, digest, materializedAt } }`, `providers` (the payloads) and
294
+ `resolutionRevision` in `instance.json`.
295
+
296
+ Everything is staged, then renamed in with a rollback journal: any failure restores the previous files
297
+ (`E_MATERIALIZE_HOME { why }`; a concurrent run is `why: "busy"`).
298
+
299
+ ### 5.2 Soul source and instance record
300
+
301
+ - A workspace soul's source goes to `agents/<agent>/souls/<commit12>/`, immutable and never removed;
302
+ `agents/<agent>/soul` is an atomically swapped symlink to the current one. A package soul must match
303
+ its locked digest (`why: "soul-digest"`); a source without `soul.yaml` or `AGENTS.md` is
304
+ `E_SOUL_INCOMPLETE`. A preview writes nothing.
305
+ - Homes carry no soul link. `instance.json` records `soulDir` (the instance's own commit directory),
306
+ `workspace: { key, name, deployment, commit, resolution, standalone, soul: { id, repoKey, commit },
307
+ layers }`, `teams`, `defaultTeam` and `capabilityMeta` (hook `meta`, merged at spawn and every launch;
308
+ retire reads it as `OATS_META`).
309
+ - Hooks get `OATS_SOUL` (`soulDir`) and `OATS_SOUL_ID`, a stable key for provider state:
310
+ `<repo key>#<soul>`, or `package:<id>#<soul>` for a package soul.
311
+
312
+ ### 5.3 Drift
313
+
314
+ `driftOf(instanceJson, discovery, { lock })` → `[{ module, from, recorded, current, status, reason? }]`,
315
+ `status` ∈ `current | moved | missing`, against the member's commit or the lock. `soulDriftOf` does the
316
+ same for the recorded soul. Drift is shown, never prevented.
226
317
 
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
- ```
318
+ ---
250
319
 
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.
320
+ ## 6. CLI verbs and DTOs (`bin/oats.mjs`)
252
321
 
253
- ---
322
+ Verbs take `--dir` (walks up to `oats-local.yaml`) and `--json` (one envelope).
323
+
324
+ ### 6.1 `oats onboard [<dir>] --workspace <repo ref>`
325
+
326
+ Writes `oats-local.yaml` and `agents/`, then runs the `sync` body. An existing `oats-local.yaml` there is
327
+ `E_ALREADY_ONBOARDED`; a failure before the workspace is read rolls back (`details.rolledBack: true`).
328
+ → `{ onboardApi: 2, standalone?, local, dir, agents, lock, sync, hosting: { host, hostIsMember, rule },
329
+ next: { clone: [{ key, name, url, dir, present, host }], spawn, souls } }`.
330
+
331
+ ### 6.2 `oats sync`
332
+
333
+ Discovers, resolves `packages:`, writes the lock, creates `agents/`, refreshes the automations
334
+ snapshot (standalone: only the catalog package providing `oats.core`). → `{ syncApi: 1, standalone?,
335
+ workspace: { name, key, url, commit, observedAt, local, lock }, members: [{ key, name, commit,
336
+ confirmed, status, detail, souls, capabilities, publishes }], packages: [{ id, version, source, commit,
337
+ integrity, capabilities, souls }], changes, problems, warnings, automations }`. Text: §8.
338
+
339
+ ### 6.3 `oats package add <id> <value> | remove <id>`
340
+
341
+ Edits `packages:` only when `oats-workspace.yaml` is tracked by its checkout; otherwise prints the change
342
+ to make. Invalid value: `E_WORKSPACE_SCHEMA`; removing an undeclared id: `E_PACKAGE_MISSING`. Receipts:
343
+ `{ action, id, value, previous, edited: true, file }` or `{ action, id, value, edited: false, file: null,
344
+ line, hint }` (`line: null` for `remove`).
345
+
346
+ ### 6.4 `oats workspace status`, `oats capabilities`, `oats souls`
347
+
348
+ - `workspace status` → `{ workspaceStatusApi: 1, standalone?, workspace: { name, key, url, commit,
349
+ observedAt, local, teams, file }, members, packages, declaredPackages, unsynced, stale, external,
350
+ automations, defaults, clones, disabledSouls, lock }`.
351
+ - `capabilities` / `souls` → `{ capabilitiesApi | soulsApi: 1, standalone?, workspace, capabilities |
352
+ souls, problems }`: every item of confirmed members, external and package souls, and locked package
353
+ capabilities, with `origin` and `kind`. Private capabilities carry `private: true`; soul rows carry
354
+ `teams` and `defaultTeam`.
355
+
356
+ ### 6.5 `oats spawn <soul>`
254
357
 
255
- ## 6. CLI (in `bin/oats.mjs`, edited) and DTOs
358
+ - A bare name must be unique (`E_SOUL_UNKNOWN`, `E_SOUL_AMBIGUOUS`); `<member>/<soul>` or
359
+ `<package>/<soul>` qualifies it. A disabled soul is `E_SOUL_DISABLED`.
360
+ - Discover → resolve → materialize; `--provider <cap> <key>=<value>` is the spawn payload layer.
361
+ - `--preview` adds `modules` (`{ name, from, layer, private, declares, changedSince }`), `teams`,
362
+ `defaultTeam`, `resolution`, `declRevision`, `payloadRevision`, `providers` (as typed), `settings` and
363
+ `settingsOrigins`. The decision binds `revision` and `effective.providers` (`E_DECISION_STALE`).
364
+ - Member clone for `worktree | checkout`, first hit wins: `--repo`; `clones:`; `<deployment>/<member>`
365
+ (`agents` → `agents-repo`). None is `E_CLONE_MISSING`; a clone of another repo `E_CLONE_MISMATCH`.
366
+ - `work: workspace`: `./work` is the deployment directory; no branch is recorded.
256
367
 
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".
368
+ ### 6.6 `oats status`
266
369
 
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.
370
+ `--json` adds, per `agents[].instances[]`, `modules` and `soul` drift rows (§5.3) and `identity` from the
371
+ messaging capability's meta. An unreachable workspace gives `workspace: { reachable: false }`. Outside a
372
+ deployment: `E_NO_DEPLOYMENT`.
373
+
374
+ ### 6.7 Capability commands from a deployment
375
+
376
+ Inside a home, `oats <ns> <cmd>` uses the home's modules. From a deployment, `lib/operator-dispatch.mjs`
377
+ resolves as a spawn of `--soul <name>` would (missing `--soul`: `E_BAD_ARGS`). The module whose
378
+ `manifest.command` is `<ns>` is fetched into `<deployment>/.oats/modules/<cap>@<commit12>/`, digest-verified
379
+ (`E_PACKAGE_INTEGRITY { why: "module-store" }`), and run with `OATS_SETTINGS` (its merged payload) and
380
+ `OATS_CLI_BIN`. Two claimants: `E_DUPLICATE_NAMESPACE`; none: `E_UNKNOWN_COMMAND`.
381
+
382
+ ### 6.8 `oats version --json` and removed verbs
383
+
384
+ `workspaceApi: 2`; features are listed only once implemented (`workspace-v2`, `instance-modules`,
385
+ `spawn-provider-payload`, `packages-no-approval`, `package-souls`, `team-model-2`, among others). Removed
386
+ verbs (`prepare`, `create`, `type`, `install`, `restore`, `init`, `use`, `trust`, `list`, `catalog`,
387
+ `remove`, `migrate`, `config`, `inject`) answer `E_UNKNOWN_COMMAND` with `details.removed` and
388
+ `details.replacement`; `session recompose` is also `E_UNKNOWN_COMMAND` (re-spawn instead).
268
389
 
269
390
  ---
270
391
 
271
392
  ## 7. Test fixture — `test/fixtures/northwind/build.mjs`
272
393
 
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)
394
+ `buildNorthwind(baseDir)` builds bare Git repos under `<baseDir>/remotes/`:
395
+
396
+ | Repo | Role |
397
+ |---|---|
398
+ | `agents` | host and member (`release-manager`, `support-triager`; `nw-release-tooling`, `nw-house-style`) |
399
+ | `platform`, `data`, `marketing` | members with souls and capabilities, some executable |
400
+ | `nw-tools` | member that also publishes package `nw.tools` v0.4.0 (`nw-lint`, `nw-deploy`) |
401
+ | `knowledge` | a store, not a member |
402
+ | `experts` | external soul `security-reviewer`, pinned by OID |
403
+ | `pkg-okf`, `pkg-framework` | catalog packages `oats.okf` v2.1.3 (knowledge layer, hooks, a `hostOnly` setting) and `oats.framework` v1.1.3 (`oats.core`) |
404
+
405
+ There are exactly two catalog package repos; `nw.tools` is pinned `git:<nw-tools ref>@v0.4.0`, which
406
+ exercises the non-collapse rule. The workspace declares three shared team labels without ids,
407
+ `defaults.messaging` and `defaults.tasks` as `none`, and `from:` values as `local/<abs-path>` keys.
408
+
409
+ It returns `{ baseDir, remotesDir, refs, urls, keys, commits, tags, catalog, moves }`, is deterministic
410
+ (fixed identity and dates, hermetic git config), and refuses a `baseDir` with whitespace or `@`
411
+ (`E_FIXTURE_BASEDIR`) or an existing `remotes/` (`E_FIXTURE_EXISTS`). Helpers: `moveMember`,
412
+ `dropBacklink` (→ `no-backlink`), `makeUnreadable` (→ `cannot-read`, returns `restore`).
413
+
414
+ ---
415
+
416
+ ## 8. The human sync report
417
+
418
+ `oats sync` and `oats onboard` without `--json` print:
419
+
420
+ ```text
421
+ workspace <name> (<key> @ <commit8>)
422
+ members <name> ✓↔ (@ <commit8>) <name> ✗ (<status>)
423
+ packages <id> <version> ✓ (@ <commit8>)
424
+ changed <id> <from|—> → <to> (@ <commit8>) <id> <from> → removed
425
+ souls N discovered (M members, E external, P package, D disabled here) · K private capabilities
426
+ teams <label>, … (shared) · this deployment's: oats teams
427
+ automations T triggers, S schedules in the members (oats trigger list · oats schedule list)
428
+ problem <code> <member>:<path> <message>
429
+ warning <code> <message>
430
+
431
+ lock <path to oats-lock.json>
287
432
  ```
288
433
 
289
- Every module's tests use this fixture and only this fixture; no test invokes bare `oats setup` (standing rule).
434
+ `changed` reads `(nothing — the lock already described this workspace)` when empty. A standalone view
435
+ prints its note in place of the name. `automations` appears only when there are some; `problem` and
436
+ `warning` repeat per item.
290
437
 
291
438
  ---
292
439
 
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).
310
-
311
- ## Post-0.25.0 clarifications (team review, 2026-09-23)
312
-
313
- **Capability commands outside an instance home (`oats <ns> <cmd>` from the
314
- deployment).** Inside an instance home the dispatcher resolves the namespace from
315
- the home's materialized modules (`instance.json.modules` → `<home>/.oats/modules`);
316
- that shipped in 0.25.0. Outside a home — the operator acts a knowledge layer needs
317
- before any instance exists (`oats okf init`, base migration) — the intended rule
318
- is: **resolve exactly as a spawn of `--soul <name>` would** (`prepareInstance` →
319
- the soul's Resolution), fetch the namespace's capability into the deployment's
320
- module store `<deployment>/.oats/modules/<cap>@<commit>/` (the same per-commit
321
- store capability-defined agents use), and dispatch to that copy with the soul's
322
- merged payload as `OATS_SETTINGS`. Never "the newest instance's copy" (an
323
- instance is not an authority for the deployment) and never an unlocked cache
324
- read (the lock's approval is the gate, as for spawn). `--soul` is required when
325
- the namespace's capability is not a workspace default. **Status: 0.25.x
326
- follow-up** — 0.25.0 still answers `E_CAPABILITY_INACTIVE` there (the pre-v2
327
- chain); the interim is to run the module binary directly with `OATS_SETTINGS`
328
- and `OATS_CLI_BIN`, as the tarball smoke does.
329
-
330
- **`work: workspace` is kept.** A coordination soul's `./work` is the deployment
331
- boundary — the directory holding `oats-local.yaml` (whatever the operator
332
- named it, member clones beside it or named in `clones:`) — read-only across member
333
- clones, no branch recorded. The clone map in `oats-local.yaml` (`clones:`) is
334
- how such a soul finds a member whose clone is elsewhere. **Status: the
335
- directory link is the intent; 0.25.0's kernel still derives the boundary from
336
- the classic `team:` scope (`docs/souls-and-instances.md` open thread) — 0.25.x
337
- follow-up binds it to the `oats-local.yaml` directory.**
338
-
339
- **`identity.source` (oats.aweb) is the absolute path of the `.aw` directory to
340
- retain**, given per spawn (`--provider oats.aweb identity.source=/abs/.aw`) or
341
- per machine (`oats-local.yaml settings.oats.aweb.identity.source`); the kernel
342
- resolves no symbolic seat names. Absolute paths never enter the workspace file
343
- (decision 14).
344
-
345
- **Member capabilities are a code-execution boundary** (decision 2: membership is
346
- the trust; hooks and scripts of every member's default branch run on every
347
- operator's machine at spawn). For a mixed public/private organisation the
348
- recommendation is: **souls only in public members; executable capabilities come
349
- from packages (approved per version) or from private members.** The onboarding
350
- skill (Phase E) states this beside the hosting rule (decision 26).
351
-
352
-
353
- ### 0.25.1 fix round (team review, 2026-09-23) — appended, not edited in place
354
-
355
- Each item names the section it refines and the review finding it closes. The
356
- implementation lands in kernel 0.25.1 (`docs/release-notes/v0.25.1.md`); no
357
- API integer or feature name changes.
358
-
359
- **§3 slot `none` (L1).** A soul's `none` for a slot **empties the slot and drops
360
- any layer-bearing capability the WORKSPACE DEFAULTS contributed for that
361
- layer** — whether it arrived through `defaults.<slot>`, `defaults.capabilities`
362
- or `defaults.byTeam[team].capabilities`. A layer-bearing capability **the soul
363
- itself declares** in its own `capabilities:` alongside `none` for that layer is
364
- `E_SLOT_CONFLICT { reason: "none" }` (spell `<cap>: off` to remove it). This
365
- replaces the Phase B reading under which a layered default arriving via
366
- `defaults.capabilities` was itself a conflict: the workspace's choice is a
367
- default and `none` is the soul's answer to it; only the soul contradicting
368
- itself is loud.
369
-
370
- **§2/§5 per-commit soul cache (M1).** `ensureWorkspaceSoul` fetches a soul's
371
- source at `(repoKey, commit)` into `<deployment>/agents/<name>/souls/<commit12>/`
372
- — **immutable once written** (staged, then renamed in; never removed by the
373
- kernel) — and maintains `<deployment>/agents/<name>/soul` as a **symlink to the
374
- current commit's directory**, swapped atomically (symlink to a temp name +
375
- rename over) so classic readers (`findAgent`, `doctor`, the classic spawn path)
376
- keep seeing "current". A spawned home's `<home>/soul` links **its own commit's
377
- directory** (the realpath of `souls/<commit12>/`), never the swapped pointer:
378
- a running instance's soul never changes under it (decision 7), a `--preview`
379
- may fetch a new commit and swap the pointer without touching any directory an
380
- instance links, and OKF 2's owner pin (`owners.json` = `realpath(<home>/soul)`)
381
- stays valid for the instance that registered it. `.oats-soul-source.json`
382
- remains the stamp of "current". A 0.25.0 layout (`agents/<name>/soul` a real
383
- directory, no `souls/`) is migrated in place on first use: the directory moves
384
- to `souls/<commit from the stamp, else unknown>/` and the pointer replaces it.
385
- A soul symlink whose target lies inside the same `agents/<name>/souls/` is the
386
- one kernel-owned symlink soul readers accept.
387
-
388
- **§1 transport (M2).** `parseRepoRef(ref).key` is unchanged — `<host>/<path>`
389
- is the identity everywhere and every comparison is by key. The **fetch url
390
- honours the form written**: `git@host:org/repo(.git)` and `ssh://…` fetch over
391
- SSH as written; `https://…` fetches over HTTPS; the bare scheme
392
- `git:host/org/repo` fetches over HTTPS by default **unless
393
- `remoteOptions.transport === "ssh"`** (a per-machine choice; `oats-local.yaml`
394
- may carry it once the schema admits it — reported by lane 3, not landed here).
395
- The operator's SSH access is therefore used when the operator wrote an SSH ref,
396
- and a private repo no longer degrades to `not-found` → standalone through an
397
- unintended HTTPS probe. When the standalone fallback engages the discovery
398
- carries `standaloneReason`/`hostFailure { code, reason, url }` so the CLI can
399
- print *why*.
400
-
401
- **§3/§4 approval re-verified at spawn (M3).** For a `from: package` module
402
- `resolveSoul` recomputes `executablesDigest` over the package tree **at the
403
- locked `entry.commit`** and requires equality with `entry.approved.executables`
404
- → else `E_PACKAGE_UNAPPROVED { reason: "digest-mismatch", approved, actual }`.
405
- The one digest definition is `lib/packages.mjs#executablesDigestAt(remote, ref,
406
- commit, path, capabilities)`, shared by `sync` and `resolve`; a hand-edited lock
407
- (same id/version, different commit, copied approval) can no longer materialize
408
- and run unapproved hooks. Cached per `(id, commit)` within a process.
409
-
410
- **§1 peeled commit OIDs (M4).** `observeRemote(ref, { at })` accepts an
411
- annotated tag's OID (or name) but **records the peeled commit** (`<oid>^{commit}`)
412
- as `commit` — in its result, in the lock, in `fetchRemoteTree`'s errors and in
413
- every `instance.json` record. A tag OID is never stored where a commit is
414
- expected.
415
-
416
- **§1 listing hygiene (L3, L4).** `listRemoteTree` filters by depth **before**
417
- asserting entry-name safety, so one unsafe deep name does not blank a member's
418
- souls (unsafe names at the kept depth are still `E_REMOTE_TREE_UNSAFE`). A git
419
- child killed for `maxBuffer` (`ENOBUFS`) is not reported as `timeout`; an
420
- unclassified listing failure is wrapped as `E_REMOTE_UNREADABLE { reason:
421
- "unknown" }` so discovery records a problem row instead of aborting.
422
-
423
- **§2 `validateWorkspace` absolute paths (L2).** The absolute-path refusal
424
- applies to **ref/path fields only** — `members[]`, `packages` values, `stores`
425
- values, `external[].source` / `external[].soul`, `defaults.*.from` — never to
426
- `teams.<label>.description` or to the opaque `messaging` payload.
427
-
428
- **§3 revision (L6).** `Resolution.revision = hash(declRevision, payloadRevision)`
429
- where `declRevision` covers the declarations (member/package commits, the
430
- composed capability set, skills, injects) and `payloadRevision` covers the
431
- payload layers (soul slot payloads ⊕ `oats-local.yaml settings` ⊕
432
- `--provider`). Both are exposed on the Resolution; decision binding keeps using
433
- `revision`, so a settings-only difference still refuses a stale apply, while
434
- `spawn --preview` can say **`changed since: declarations | payload | both`**
435
- instead of a bare `changedSince`.
436
-
437
- **§6 operator-level dispatch (B3, implements the rule stated above).** Outside a
438
- home, with `oats-local.yaml` present, `oats <ns> <cmd> … --soul <name>` runs
439
- `prepareInstance(dir, name)`, picks the module whose `manifest.command === <ns>`,
440
- ensures its tree in `<deployment>/.oats/modules/<cap>@<commit12>/` (member: the
441
- member repo at `module.from.commit`, `module.dir`; package: the lock entry) and
442
- dispatches to that copy with `OATS_SETTINGS = resolution.payloads[cap]` and
443
- `OATS_CLI_BIN`. `--soul` absent → `E_BAD_ARGS` naming it; a namespace no module
444
- provides → `E_UNKNOWN_COMMAND`. Trust is the resolution's (membership;
445
- `E_PACKAGE_UNAPPROVED` for an unapproved package).
446
-
447
- **§5/§6 `work: workspace` under v2 (B2, implements the rule stated above).**
448
- With `prepared` present, a `work: workspace` soul's `./work` links the
449
- deployment directory (`prepared.deployment`, the one holding `oats-local.yaml`);
450
- no branch is recorded; the "needs a declared boundary" remedy names
451
- `oats-local.yaml`, not `oats-config.yaml`. The classic root is unchanged.
452
-
453
- **Decision 13 reach (L7).** "Harnesses start normally" is a property of the
454
- 0.25 **launcher**: every `pi` launch a 0.25 kernel performs — module homes and
455
- classic 0.24 homes alike — starts pi with cwd = home, the composed `AGENTS.md`
456
- appended, and pi's own skill/context discovery intact. Consequently `oats
457
- session recompose` refuses a **module home** (`instance.json.modules` present)
458
- with `E_UNSUPPORTED_MODE` ("re-spawn"); the `session-recompose` feature name
459
- stays advertised because the verb still serves classic homes
460
- (`docs/desktop-cli-api.md`).
461
-
462
- ### 0.25.2 operator-rebuild round (2026-09-24) — appended, not edited in place
463
-
464
- Source: an operator's first rebuild of a real two-team deployment on 0.25.0,
465
- following the 0.25 rebuild guide literally (removed in 0.26.0). **The guide is a contract the
466
- kernel must honour**: where the guide claimed behaviour the kernel lacked, the
467
- kernel changes; where the guide described keys no provider consumes, the guide
468
- changes. Findings R1–R10; kernel side in 0.25.2 (`docs/release-notes/v0.25.2.md`).
469
- No API integer or feature name changes; the only surface additions are
470
- additive fields (`spawn --preview` `providers` / `settings`, `oats status
471
- --json instances[].soul`) and the `sync --approve` flag.
472
-
473
- **§2/§5 member clone lookup (R1).** For a `work: worktree | checkout` soul the
474
- kernel finds the member clone in this order, first hit wins: (1) `oats spawn
475
- --repo <abs path>`; (2) `oats-local.yaml` `clones: { <repo key>: <abs path> }`,
476
- keys normalised through `parseRepoRef(...).key` so any spelling of the same
477
- repo addresses one entry; (3) the convention `<deployment>/<member name>` where
478
- `<member name>` is the last segment of the repo key — **a member named `agents`
479
- is looked for at `<deployment>/agents-repo`** (`<deployment>/agents/` is the
480
- instance root); (4) none → `E_CLONE_MISSING { repoKey, tried: [...], remedies }`
481
- naming the three remedies. A directory found by (2) or (3) whose `origin` remote
482
- resolves to a different repo key → `E_CLONE_MISMATCH { repoKey, path, origin }`
483
- — the kernel never spawns into a clone that is not the member. This order was
484
- stated by the guide and `docs/workspaces.md` before 0.25.2 and not implemented;
485
- it is now normative.
486
-
487
- **§6 `oats sync` creates `agents/` (R2).** `sync` (and therefore `onboard`,
488
- which runs the sync body) creates `<deployment>/agents/` when absent. A
489
- hand-written `oats-local.yaml` needs no `mkdir`.
490
-
491
- **§5 one "You run on OATS" block (R3).** When `oats.core` resolves as a module
492
- the composer suppresses the kernel's legacy `oats:kernel:oats` block; the
493
- module's inject is the one such block. Without `oats.core` (a soul saying `off`)
494
- the legacy block is composed as before, so no instance is left without the
495
- briefing.
496
-
497
- **§5/§6 soul-source drift (R4).** `driftOf` covers `instance.json.workspace.soul`
498
- as well as `modules`: `oats status` prints `soul: <name> from <member> @ <c7>`
499
- with `[member moved since …]` when the member's default branch is past the
500
- recorded commit (`[member unconfirmed]` / `[soul no longer present]` for the
501
- missing cases); `--json` adds `instances[].soul = { repoKey, commit, current:
502
- <commit>|null, status: "current"|"moved"|"missing" }`. A moved soul is
503
- information (decision 17): the instance keeps its own commit directory (M1).
504
-
505
- **§6 preview payload visibility (R5).** `oats spawn --preview` (text and
506
- `--json`) reports `providers` — the `--provider <cap> k=v` map exactly as given,
507
- nested — and `settings.<cap>` — `resolution.payloads[cap]`, the merged payload
508
- the provider's binding receives (`workspace.messaging` base ⊕ `byTeam[team]` ⊕
509
- soul slot payload ⊕ `local.settings[cap]` ⊕ `providers[cap]`). Additive fields;
510
- both empty objects when nothing applies.
511
-
512
- **§5/§6 `work: workspace` (R6, closed in 0.25.1 as B2).** Documented in the
513
- guide's §9: a coordination soul's `./work` is the deployment directory.
514
-
515
- **Provider payload delivery vs provider consumption (R7 — oats.aweb 1.11.2).**
516
- Decision 23 (`messaging.byTeam`) is **kernel semantics**: the kernel merges and
517
- delivers; the provider consumes what its binding declares. oats.aweb 1.11.2's
518
- spawn hook (a) locates the aweb root among `OATS_TEAM_SCOPE`, the home, the
519
- home's git root, `OATS_CONTEXT` and its git root, and `OATS_WORKSPACE` (under
520
- v2: the deployment directory) — none of which is a 0.24 team root; and (b)
521
- resolves the target team from `OATS_TEAM_ID`/`OATS_TEAM_NAME` (the removed
522
- `oats-config.yaml` `team:` block; empty under v2), else the **active team at
523
- the root it found** — it does **not** read `team` from `OATS_SETTINGS`. So for
524
- 1.11.2 `byTeam` is delivered and recorded but a no-op; per-label minting is
525
- obtained only by placing a per-team `.aw` inside each team's member clone
526
- (gitignored) so it is found through the work repo, or one `.aw` at the
527
- deployment directory for a single team. The guide states this (§8b) and
528
- `docs/workspaces.md` states the general rule ("kernel-merged; whether a
529
- provider honours it is the provider's"). **oats.aweb follow-up**: read `team`
530
- (and honour `byTeam`'s result) from the payload; accept the deployment
531
- directory as a first-class root. The kernel does not paper over this with a
532
- `team:` env shim — the env block is removed with `oats-config.yaml`, and a
533
- provider contract is the provider's to grow.
534
-
535
- **OKF 2.1.3 reads `okf.json`, not a soul payload (R8 — corrects §2's `stores`
536
- comment and decision 24's `root` example).** `oats.okf` 2.1.3's spawn hook
537
- reads the soul's knowledge declaration from `<soul>/okf.json` (`{ version: 1,
538
- owner, owns: ["<base>/<node>"], reads: [...] }`, `lib/config.mjs#validateDeclaration`)
539
- and its settings from `OATS_SETTINGS`, admitting **only** `bindings-file`,
540
- `state-dir`, `harvest-runtime`, `harvest-model` (`oats.json#settings`) — any
541
- other key is `E_CONFIG unknown OATS_SETTINGS property`. Where a base lives
542
- inside a store repository is the **bindings file's** `bases.<alias>.repository`
543
- + `root`, not a soul payload key. Therefore: a soul.yaml `knowledge:` payload for
544
- OKF carries binding settings only (usually nothing — the workspace default
545
- fills the slot; `none` opts out); `owns`/`reads`/`store`/`root` examples on
546
- `soul.yaml` are removed from the guide, `workspaces.md`, `souls-and-instances.md`
547
- and `knowledge.md`; `okf.json` stays in `souls/<name>/` and travels with the
548
- soul into the per-commit cache (M1). A soul-payload grammar for OKF is an OKF
549
- follow-up that lands with an `oats.okf` release declaring it in its binding.
550
- The kernel's part — opaque forwarding of the merged payload — is unchanged and
551
- correct. §7b's fresh `state-dir` rule is confirmed by the operator's run.
552
-
553
- **§6 non-interactive approval (R9).** `oats sync --approve <id>@<version>`
554
- (repeatable) approves exactly the entry the current resolution contains for
555
- that id and version: the executables digest is always computed by `sync` over
556
- the fetched tree (`executablesDigestAt`) and recorded — never typed. An
557
- `--approve` naming an id/version the resolution does not contain → `E_BAD_ARGS`
558
- (nothing approved); entries not covered stay unapproved (exit `2`). At the
559
- interactive prompt **Ctrl+D (EOF) is a decline**: exit `2`, entry unapproved —
560
- never treated as "yes", never a hang.
561
-
562
- **§6 onboard next steps (R10).** `oats onboard` lists the workspace **host**
563
- in `next.clone` like any member that lacks a clone at the convention (the host
564
- is a member; a soul that lives in it may need a work clone). Under an explicit
565
- `oats-local.yaml` `standalone:` header the next steps say the view is standalone
566
- and list only that repo.
567
-
568
- ### 0.25.6 — decision 27: served identity is a messaging-layer fact (K1′, K1″, K2)
569
-
570
- - **K1′** `decision.effective.providers` = the resolution's merged per-module payloads
571
- (exactly what reaches `OATS_SETTINGS`), bound by the decision revision. No new flag.
572
- - **K1″** manifest `settings.<key>.hostOnly: true` → the resolver refuses that key in
573
- the workspace base, `byTeam[*]`, the soul's slot and `--provider` layers with
574
- `E_WORKSPACE_SCHEMA { reason: "host-only-key", path, key, capability }`; only
575
- `oats-local.yaml settings.<cap>` may carry it. Generalises decision 23's reserved
576
- `byTeam` into a capability-declared attribute. Schema: `docs/capability-manifest.schema.json`.
577
- - **K2** `oats status --json instances[].identity` and `oats inspect … selected.identity`
578
- copy `capabilityMeta[<cap>].identity` (messaging-layer capability preferred; `provider`
579
- added); text `identity: acts as <address> via grant, expires <t>` / `alias <a> on <team>`.
580
- Layer contract shape in `docs/integrations.md`.
581
- - `features[]` gains `served-identity`.
582
-
583
- ### 0.25.5 — launch-hook `meta` is persisted
584
-
585
- `runLifecycleHooks("launch")` collected each capability's `meta` and the
586
- start/restart path discarded it (only `contributions` and `env` were consumed).
587
- From 0.25.5 a successful start merges `res.meta` per capability into
588
- `instance.json.capabilityMeta` — the record spawn writes and retire reads as
589
- `OATS_META`. A hook answering without `meta` keeps its prior entry; a failed
590
- launch preparation writes nothing. No new field, flag or hook event; this is
591
- the documented hook return finally honoured (decision 27, K3′). Driver: a
592
- provider renewing a session grant at every start would otherwise leave the
593
- original grant id on record and retire would revoke the wrong grant.
594
-
595
- ### 0.25.3 — `OATS_SOUL_ID` (stable soul identity for providers)
596
-
597
- The per-commit soul cache (0.25.1, M1) made `realpath(<home>/soul)` change with every
598
- member commit; a provider that keyed durable state on that path (OKF 2.1.3 `owners.json`)
599
- refused the next spawn (`E_OWNER`). "Members are latest" and "the owner is a path" cannot
600
- both hold, so the kernel now hands hooks a **stable identity**:
601
-
602
- - `OATS_SOUL_ID` in the `spawn` / `retire` / `launch` hook environment: for a workspace soul
603
- `<repo key>#<soul name>` exactly as the canonical key is spelled (e.g.
604
- `github.com/awebai/aweb#aweb-protocol-expert`, local fixtures `local//abs/path.git#name`);
605
- for a classic soul the realpath of `agents/<name>/soul` (today's value — 0.24 deployments
606
- unchanged). Also recorded as `instance.json.workspace.soul.id`.
607
- - `OATS_SOUL` is the **content** the home links — for a workspace soul the per-commit
608
- directory `agents/<name>/souls/<commit12>/`, never the swappable `agents/<name>/soul`
609
- pointer. Providers read content from `OATS_SOUL` and key state on `OATS_SOUL_ID`.
610
- - Provider contract (OKF 2.1.4): `owners[owner] = OATS_SOUL_ID ?? realpath(OATS_SOUL ?? home/soul)`;
611
- a prior row whose value is a path under `agents/<same soul name>/(soul|souls/<commit>)` is
612
- migrated to the id once, not refused; any other mismatch stays `E_OWNER`.
440
+ ## Post-0.25.0 clarifications
613
441
 
442
+ Folded into the sections above: operator-level capability commands (§6.7); `work: workspace` (§6.5);
443
+ host paths and `hostOnly` keys (§3.4); member capabilities as a code-execution boundary (§3.2, §4.3);
444
+ slot `none` (§3.3); the two revisions (§3.6); peeled commits and transport (§1); the per-commit soul
445
+ cache, `OATS_SOUL_ID` and persisted launch `meta` (§5.2); clone lookup and preview payloads (§6.5);
446
+ soul drift (§5.3).