@awebai/oats 0.25.8 → 0.26.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 (185) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +576 -1714
  3. package/capabilities/oats-authoring/oats-package.json +2 -2
  4. package/capabilities/oats-authoring/oats.json +2 -2
  5. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
  6. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
  7. package/capabilities/oats-aweb/bin/oats-aweb.mjs +334 -72
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
  10. package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
  11. package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
  12. package/capabilities/oats-aweb/oats.json +14 -4
  13. package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
  14. package/capabilities/oats-jira/oats.json +2 -2
  15. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
  16. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
  17. package/capabilities/oats-linear/oats.json +2 -2
  18. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
  19. package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
  20. package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
  21. package/capabilities/oats-okf/lib/migration.mjs +2 -2
  22. package/capabilities/oats-okf/lib/sources.mjs +5 -4
  23. package/capabilities/oats-okf/lib/stores.mjs +40 -9
  24. package/capabilities/oats-okf/lib/worker.mjs +3 -3
  25. package/capabilities/oats-okf/oats.json +1 -1
  26. package/capabilities/oats-review/oats.json +3 -2
  27. package/docs/capabilities.md +218 -47
  28. package/docs/capability-manifest.schema.json +13 -4
  29. package/docs/configuration.md +17 -5
  30. package/docs/conventions.md +16 -26
  31. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  32. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  33. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  34. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  35. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  36. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  37. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  38. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  39. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
  40. package/docs/design/2026-09-24-phase-d-plan.md +77 -0
  41. package/docs/design/2026-09-25-teams-contract.md +226 -0
  42. package/docs/design/README.md +3 -3
  43. package/docs/design/launch-configurations.md +20 -16
  44. package/docs/design/operations-contract.md +27 -10
  45. package/docs/desktop-cli-api.md +546 -264
  46. package/docs/desktop-instance-start.md +1 -1
  47. package/docs/desktop.md +7 -13
  48. package/docs/execution-targets.md +16 -18
  49. package/docs/first-team.md +14 -17
  50. package/docs/implementation.md +28 -59
  51. package/docs/integrations.md +88 -32
  52. package/docs/knowledge-capability-authoring.md +1 -1
  53. package/docs/knowledge-reference/package-craft.md +10 -8
  54. package/docs/knowledge-theory.md +1 -1
  55. package/docs/knowledge.md +10 -11
  56. package/docs/layers.md +16 -17
  57. package/docs/oats-local.schema.json +29 -1
  58. package/docs/oats-membership.schema.json +5 -3
  59. package/docs/oats-package.schema.json +2 -2
  60. package/docs/oats-workspace.schema.json +1 -1
  61. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  62. package/docs/packages.md +75 -52
  63. package/docs/release-notes/v0.22.0.md +1 -1
  64. package/docs/release-notes/v0.23.1.md +1 -1
  65. package/docs/release-notes/v0.25.9.md +23 -0
  66. package/docs/release-notes/v0.26.0.md +670 -0
  67. package/docs/schedules.md +48 -126
  68. package/docs/soul.schema.json +11 -4
  69. package/docs/souls-and-instances.md +56 -43
  70. package/docs/workspaces.md +80 -58
  71. package/injects/instance-boundary.md +1 -1
  72. package/injects/work-attached.md +1 -1
  73. package/injects/work-workspace.md +2 -2
  74. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  75. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  76. package/lib/capability-contract.mjs +110 -0
  77. package/lib/config-data.mjs +2 -2
  78. package/lib/core.mjs +700 -4824
  79. package/lib/digest.mjs +12 -0
  80. package/lib/instance-inspect.mjs +396 -0
  81. package/lib/instance-lifecycle.mjs +3 -4
  82. package/lib/instance-resolution.mjs +212 -26
  83. package/lib/instruction-composition.mjs +0 -20
  84. package/lib/materialize.mjs +6 -4
  85. package/lib/operator-dispatch.mjs +33 -13
  86. package/lib/packages.mjs +25 -190
  87. package/lib/provider-binding.mjs +4 -2
  88. package/lib/provider-reasons.mjs +3 -68
  89. package/lib/resolve.mjs +204 -68
  90. package/lib/schedule.mjs +97 -272
  91. package/lib/servers.mjs +13 -13
  92. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  93. package/lib/tree-copy.mjs +44 -0
  94. package/lib/workspace.mjs +125 -20
  95. package/package-catalog.json +7 -7
  96. package/package.json +1 -1
  97. package/skills/integration-authoring/SKILL.md +48 -40
  98. package/skills/oats-getting-started/SKILL.md +105 -110
  99. package/skills/oats-support/SKILL.md +2 -2
  100. package/skills/soul-craft/SKILL.md +13 -6
  101. package/bin/oats-pi-sdk-host.mjs +0 -17
  102. package/docs/2026-09-03-architecture-proposal.md +0 -642
  103. package/docs/artifact-approvals.schema.json +0 -7
  104. package/docs/captured-invocation-context.schema.json +0 -7
  105. package/docs/captured-resolution.schema.json +0 -7
  106. package/docs/design/package-engine-contract.md +0 -813
  107. package/docs/design/package-runtime-api.md +0 -588
  108. package/docs/desktop-succession.md +0 -57
  109. package/docs/execution-capsule.schema.json +0 -108
  110. package/docs/first-team-demo.md +0 -92
  111. package/docs/knowledge-migration.md +0 -147
  112. package/docs/migration-from-oas.md +0 -103
  113. package/docs/oats-config.schema.json +0 -172
  114. package/docs/oats-lock-v3.schema.json +0 -7
  115. package/docs/oats-lock.schema.json +0 -175
  116. package/docs/operating-team-migration.md +0 -470
  117. package/docs/portable.schema.json +0 -2512
  118. package/docs/provider-check-input.schema.json +0 -7
  119. package/docs/rebuild-to-v2.md +0 -511
  120. package/docs/workspace-adoption.md +0 -74
  121. package/injects/framework-workspace.md +0 -7
  122. package/injects/local-soul.md +0 -19
  123. package/injects/oats-portable.md +0 -20
  124. package/injects/oats.md +0 -11
  125. package/injects/portable-instance-boundary.md +0 -39
  126. package/injects/portable-work-directory.md +0 -29
  127. package/lib/artifact-approvals.mjs +0 -120
  128. package/lib/artifact-tree.mjs +0 -141
  129. package/lib/capability-artifacts.mjs +0 -179
  130. package/lib/capability-execution.mjs +0 -15
  131. package/lib/capability-inputs.mjs +0 -39
  132. package/lib/capability-provenance.mjs +0 -231
  133. package/lib/captured-action-shape.mjs +0 -21
  134. package/lib/captured-admission-shape.mjs +0 -20
  135. package/lib/captured-binding-file.mjs +0 -36
  136. package/lib/captured-dispatch.mjs +0 -66
  137. package/lib/captured-instance-index.mjs +0 -277
  138. package/lib/captured-invocation-context.mjs +0 -130
  139. package/lib/captured-launch-request.mjs +0 -66
  140. package/lib/captured-operation-process.mjs +0 -15
  141. package/lib/captured-pi-custody.mjs +0 -29
  142. package/lib/captured-pi-host.mjs +0 -167
  143. package/lib/captured-pi-outcome.mjs +0 -172
  144. package/lib/captured-resolutions.mjs +0 -275
  145. package/lib/captured-scaffold.mjs +0 -87
  146. package/lib/captured-selector.mjs +0 -28
  147. package/lib/captured-session-backend.mjs +0 -52
  148. package/lib/captured-source-receipt-file.mjs +0 -72
  149. package/lib/helper-injection-policy.mjs +0 -104
  150. package/lib/legacy-lock-codec.mjs +0 -106
  151. package/lib/manifest-settings.mjs +0 -84
  152. package/lib/package-closure.mjs +0 -48
  153. package/lib/package-materialization.mjs +0 -83
  154. package/lib/pi-sdk-host.mjs +0 -229
  155. package/lib/portable-artifacts.mjs +0 -115
  156. package/lib/portable-choices.mjs +0 -82
  157. package/lib/portable-composition.mjs +0 -136
  158. package/lib/portable-digest.mjs +0 -105
  159. package/lib/portable-identity.mjs +0 -40
  160. package/lib/portable-lock.mjs +0 -117
  161. package/lib/portable-onboarding-request.mjs +0 -49
  162. package/lib/portable-onboarding.mjs +0 -256
  163. package/lib/portable-package-preparation.mjs +0 -188
  164. package/lib/portable-policy.mjs +0 -44
  165. package/lib/portable-soul.mjs +0 -42
  166. package/lib/portable-state.mjs +0 -80
  167. package/lib/prepare-composition.mjs +0 -170
  168. package/lib/prepared-bindings.mjs +0 -92
  169. package/lib/prepared-resources.mjs +0 -127
  170. package/lib/provider-binding-broker.mjs +0 -65
  171. package/lib/provider-binding-wire.mjs +0 -116
  172. package/lib/readiness.mjs +0 -225
  173. package/lib/repository-observation.mjs +0 -226
  174. package/lib/resolution-shape.mjs +0 -393
  175. package/lib/schedule-capsule.mjs +0 -206
  176. package/lib/soul-constraints.mjs +0 -40
  177. package/lib/source-projection.mjs +0 -84
  178. package/lib/source-spec.mjs +0 -189
  179. package/lib/workspace-definition.mjs +0 -126
  180. package/lib/workspace-discovery.mjs +0 -146
  181. package/skills/oats/SKILL.md +0 -162
  182. package/skills/oats-config/SKILL.md +0 -164
  183. package/skills/oats-packages/SKILL.md +0 -184
  184. package/skills/oats-portable/SKILL.md +0 -115
  185. package/skills/oats-portable-artifacts/SKILL.md +0 -63
@@ -8,7 +8,6 @@ contracts the kernel is built against are in
8
8
  [design/2026-09-23-workspace-module-contracts.md](design/2026-09-23-workspace-module-contracts.md);
9
9
  a full worked example (an imaginary company with three teams) is in
10
10
  [design/2026-09-23-simplified-workspace-model.md](design/2026-09-23-simplified-workspace-model.md).
11
- Moving an existing 0.24.x deployment: [rebuild-to-v2.md](rebuild-to-v2.md).
12
11
 
13
12
  ## The rule
14
13
 
@@ -19,7 +18,7 @@ versioned.**
19
18
  | Source kind | `from:` | Versioned | Trust |
20
19
  |---|---|---|---|
21
20
  | Member repo | `<repo key>` or `here` | no — always the member's **latest** default-branch state | membership (the reciprocal handshake) |
22
- | Package | `package` | yes — the version pinned in the workspace's `packages:`; `oats-lock.json` records the exact commit + integrity | executables approved **once per version**, recorded in the lock |
21
+ | Package | `package` | yes — the version pinned in the workspace's `packages:`; `oats-lock.json` records the exact commit + integrity | the declaration in `packages:` (no separate approval) |
23
22
 
24
23
  A soul names each capability **with where it comes from — a location, never a
25
24
  version**. The workspace's `packages:` says which version; materialization
@@ -39,8 +38,8 @@ machine (`oats-local.yaml`); the lock (`oats-lock.json`) sits beside
39
38
  `oats-local.yaml` and is identical on every machine that synced the same
40
39
  workspace commit. Schemas: [`oats-workspace.schema.json`](oats-workspace.schema.json),
41
40
  [`oats-membership.schema.json`](oats-membership.schema.json),
42
- [`soul.schema.json`](soul.schema.json), [`oats-local.schema.json`](oats-local.schema.json),
43
- [`oats-lock-v3.schema.json`](oats-lock-v3.schema.json). The JSON schemas encode
41
+ [`soul.schema.json`](soul.schema.json), [`oats-local.schema.json`](oats-local.schema.json);
42
+ the lock's format is in [packages](packages.md#lock-v3). The JSON schemas encode
44
43
  shapes; domain rules (declared teams, duplicate members, canonical `from:` keys,
45
44
  the two `packages:` value forms) live in the kernel's `validateWorkspace` /
46
45
  `validateSoul`, which are the authority.
@@ -61,7 +60,7 @@ members: # repo refs, NO @revision (E_WORKSPAC
61
60
 
62
61
  packages: # the ONLY versioned things
63
62
  oats.framework: v1.1.3 # bare version → resolves through the official catalog
64
- oats.okf: v2.1.4
63
+ oats.okf: v2.1.5
65
64
  acme.tools: git:github.com/acme/tools@v0.4.0 # outside the catalog → git:<repo>@<tag|OID>; still a package
66
65
 
67
66
  teams: # labels, declared once so they cannot drift
@@ -141,8 +140,9 @@ declaration; it travels with the soul into the per-commit soul cache. The
141
140
  carry only the binding's settings keys (`bindings-file`, `state-dir`,
142
141
  `harvest-runtime`, `harvest-model`); `owns`/`reads`/`root` there are refused by
143
142
  the provider, not read (a soul payload grammar is an OKF follow-up). Every
144
- `souls/*/soul.yaml` in a member is discoverable; one that wants to stay
145
- internal says `private: true` (spawnable only from its own repo). A soul's
143
+ `souls/*/soul.yaml` in a member is listed and spawnable — souls have no
144
+ private mode (`private:` in a soul.yaml is ignored since 0.26.0, with a
145
+ `soul-private-ignored` warning). A soul's
146
146
  `name` must equal its directory name; the first of two souls declaring one
147
147
  name (by path) is listed, the second is a problem.
148
148
 
@@ -151,7 +151,8 @@ name (by path) is listed, the second is a problem.
151
151
  The capability manifest is the one file that did not change (see
152
152
  [capabilities.md](capabilities.md)). Discovery relies on `capability` (the same
153
153
  `^[a-z0-9][a-z0-9._-]*$` grammar every `capabilities:` key uses), `version`,
154
- `layer`, and may read `private: true` and `team: <label>`. `version` is
154
+ `layer`, and may read `private: true` (a **repo-owned** capability) and
155
+ `team: <label>`. `version` is
155
156
  informational for member capabilities — a materialized copy is identified by
156
157
  its content digest.
157
158
 
@@ -188,7 +189,14 @@ with the right name, is not admission.
188
189
  model as a repo's committed `.agents/skills/`: whoever can push to the repo
189
190
  decides what runs, and the branch's latest state is what runs. No per-operator
190
191
  trust lists, no per-capability approval for members. Packages come from
191
- *outside* that boundary and keep a one-time executable approval per version.
192
+ *outside* that boundary, and **declaring one in the workspace's `packages:` is
193
+ the trust decision** (human decision, 2026-09-24): people install a package only
194
+ when they trust it, so there is no second, per-version approval step. The lock
195
+ is reproducibility, not approval — it pins the exact commit and content
196
+ integrity, and `oats sync` refuses drift (a moved tag, changed content, an
197
+ edited capability list). A spawn admits only a locked package the workspace
198
+ **still declares**: one removed from `packages:` but left in a stale lock is
199
+ `E_PACKAGE_MISSING { reason: "undeclared" }` until `oats sync` drops it.
192
200
 
193
201
  **The handshake is observed with the operator's own Git read access, in one
194
202
  access context.** The kernel reads both halves over the remotes
@@ -210,10 +218,11 @@ its capabilities unresolvable (`E_NOT_A_MEMBER` / `E_MEMBERSHIP_UNCONFIRMED`).
210
218
  Reading the workspace repo *is* being in the workspace — a workspace's access
211
219
  control is Git's.
212
220
 
213
- **Private items.** `private: true` on a soul or a capability keeps it out of the
214
- workspace listing; a private capability is usable only by souls of the same
215
- repo (`E_CAPABILITY_PRIVATE` otherwise). Owners still see their own private
216
- items.
221
+ **Repo-owned capabilities.** `private: true` in a capability's `oats.json`
222
+ makes it **repo-owned**: it is listed like any other capability (with
223
+ `private: true`; the human table marks it "(repo-owned)"), and it is usable
224
+ only by souls of the same repo (`E_CAPABILITY_PRIVATE` otherwise). Souls have
225
+ no private mode: every soul of a confirmed member is listed and spawnable.
217
226
 
218
227
  **External souls.** `external:` adopts a soul by reference from a repo that is
219
228
  **not** a member, pinned to a full commit. No handshake is asked for and none is
@@ -226,7 +235,7 @@ its slots. An `external[].team` overrides the soul's own `team`.
226
235
  A repository may be a **member** (it completed the handshake; its `souls/*` and
227
236
  `capabilities/*` are member-tier: latest state, trusted by membership) **and** a
228
237
  **package publisher** (its `oats-package/` is consumed only through
229
- `packages:`: versioned, locked, approved). The two never collapse:
238
+ `packages:`: versioned and locked). The two never collapse:
230
239
 
231
240
  - `from: <repo key>` looks **only** under `<repo>/capabilities/<name>/oats.json`
232
241
  at the member's latest state. It never looks inside `oats-package/`. A name
@@ -244,31 +253,32 @@ So the framework's own souls say `oats.okf: { from: package }` even though
244
253
  member soul that is the expert in that capability (`okf-expert`, `aweb-expert`,
245
254
  …), discoverable at latest state like any member soul.
246
255
 
247
- ## Packages, lock, approval, catalog
256
+ ## Packages, lock, catalog
248
257
 
249
258
  `packages:` values have exactly two forms:
250
259
 
251
260
  - a **bare version** (`v2.1.3`, `2.1.3`, `v1.0.0-rc.1`) — resolved through the
252
261
  official catalog (`package-catalog.json` in the `oats` repo; the reviewed
253
- marketplace, see [official-marketplace.md](official-marketplace.md)). This is
262
+ list, see [official-catalog.md](official-catalog.md)). This is
254
263
  the only way a package becomes *pinnable by id*.
255
264
  - **`git:<repo>@<ref>`** — a direct package ref: `<repo>` is any ref the kernel
256
265
  understands (`github.com/org/repo`, `https://…`, `git@host:…`, `/abs/bare.git`,
257
266
  `file:///…`), `<ref>` a tag name or a full commit OID. The package is read at
258
267
  `oats-package/` inside that repo.
259
268
 
260
- Both are packages: versioned, locked, approved. A ref that resolves to a
261
- **branch** is refused (`E_PACKAGE_INTEGRITY { why: "branch" }`) — versions are
262
- immutable. A tag that moved (same version string, different commit) fails
263
- integrity on the next `oats sync` and asks again.
264
-
265
- `oats sync` confirms membership, resolves every `packages:` entry to a commit +
266
- content digest, asks (on a terminal) for any missing per-version executable
267
- approval — or takes it from repeatable `--approve <id>@<version>` flags for
268
- unattended runs (each approves exactly the entry the resolution contains; the
269
- digest is always computed, never typed; Ctrl+D at the prompt is a decline,
270
- exit `2`) — writes `oats-lock.json` (lockfileVersion 3), creates `agents/` if
271
- absent and reports what changed. `oats package add <id> <version|git:…@…>` / `oats package remove <id>` edit
269
+ Both are packages: versioned and locked. A ref that resolves to a **branch** is
270
+ refused (`E_PACKAGE_INTEGRITY { why: "branch" }`) — versions are immutable. A
271
+ tag that moved (same version string, different commit), or content that no
272
+ longer matches the locked integrity, fails with `E_PACKAGE_INTEGRITY` on the
273
+ next `oats sync`.
274
+
275
+ **There is no package approval** (human decision, 2026-09-24). Declaring a
276
+ package in `packages:` is the trust decision; `oats sync` asks nothing and
277
+ `--approve` is `E_BAD_ARGS`. `oats sync` confirms membership, resolves every
278
+ `packages:` entry to a commit + content digest, writes `oats-lock.json`
279
+ (lockfileVersion 3), creates `agents/` if absent, reports what changed and
280
+ exits `0`. A lock written by an earlier kernel may still carry an `approved`
281
+ record per entry: it is ignored, and the next write drops it. `oats package add <id> <version|git:…@…>` / `oats package remove <id>` edit
272
282
  `packages:` in the workspace file when it is tracked by the current checkout,
273
283
  else print the line to add — the workspace file is shared through Git. Details:
274
284
  [packages.md](packages.md).
@@ -282,8 +292,8 @@ the workspace's slot default):
282
292
 
283
293
  ```
284
294
  from: package → some locked package provides `name` else E_PACKAGE_MISSING (run `oats sync`)
285
- → that package version is approved else E_PACKAGE_UNAPPROVED
286
- → read its capability manifest at the locked commit; copy; record package/version/commit/digest
295
+ → read its manifests at the locked commit; the lock's capability list must match else E_PACKAGE_INTEGRITY
296
+ → copy; record package/version/commit/digest
287
297
  from: <repo> → <repo> is a CONFIRMED member else E_NOT_A_MEMBER / E_MEMBERSHIP_UNCONFIRMED
288
298
  (or `here`) → it has capabilities/<name>/oats.json else E_CAPABILITY_MISSING
289
299
  → not private, unless <repo> is the soul's own else E_CAPABILITY_PRIVATE
@@ -308,13 +318,12 @@ Nothing is symlinked, nothing is shared between instances.
308
318
  ```
309
319
  <agents-root>/<soul>/instances/<instance>/
310
320
  ├── AGENTS.md # composed: soul AGENTS.md + kernel/work-mode blocks + each module's inject
311
- │ # (with oats.core resolved, the module's "You run on OATS" block is the only one — the kernel's legacy copy is suppressed)
321
+ │ # (the "You run on OATS" block is oats.core's inject; the kernel ships no copy)
312
322
  ├── CLAUDE.md → AGENTS.md
313
323
  ├── .agents/skills/<capability>/<skill>/SKILL.md # full copies; where pi/codex look
314
324
  ├── .claude/skills → ../.agents/skills
315
325
  ├── .oats/modules/<capability>/ # the full capability copy: oats.json, bin/, injects/, skills/
316
326
  ├── instance.json # modules{}, providers{}, workspace{} recorded here
317
- ├── soul → <agents-root>/<soul>/soul # read-only reference
318
327
  ├── TASK.md
319
328
  └── work/
320
329
  ```
@@ -347,10 +356,16 @@ not exclude anything.
347
356
  ## Teams
348
357
 
349
358
  `teams:` declares labels once (`global`, `engineering`, …) so they cannot drift
350
- into typos. A soul or capability carries `team:`, else its repo's default from
351
- `oats-membership.yaml`, else `unassigned`. A label not declared in `teams:` is
352
- `E_TEAM_UNKNOWN` (the item is still listed). `defaults.byTeam.<team>.capabilities`
353
- adds capabilities additively for souls with that label (`off` removes). **A
359
+ into typos. A soul carries `team:` — one label or a list (`team: [engineering,
360
+ reviewers]`, the first the primary) — else its repo's default from
361
+ `oats-membership.yaml` (same shape), else `unassigned`; a capability carries one
362
+ label. A label not declared in `teams:` is `E_TEAM_UNKNOWN` (the item is still
363
+ listed); a declared label without a `messaging.byTeam` entry is the
364
+ `unmapped-team-label` warning. `defaults.byTeam.<team>.capabilities` adds
365
+ capabilities additively for souls with that label, for each label in order
366
+ (`off` removes; two labels that disagree are `E_TEAM_CONFLICT`). Each label is
367
+ an *eligible* messaging team the provider may join on request — see
368
+ [capabilities.md](capabilities.md#several-team-labels). **A
354
369
  label never gates, restricts, changes trust or partitions the knowledge
355
370
  store** — it organises and can supply defaults. The messaging provider's payload
356
371
  (private teams, channels) lives under `messaging:`, so "team" means one thing.
@@ -364,9 +379,13 @@ store** — it organises and can supply defaults. The messaging provider's paylo
364
379
  | A fact about **this spawn** | `oats spawn … --provider <cap> key=value` (repeatable; dotted keys nest) → `instance.json.providers.<cap>` | `--provider oats.aweb identity.source=/abs/path/to/retained/.aw` |
365
380
 
366
381
  The merged payload is `workspace.messaging` (messaging slot only; its base
367
- keys ⊕ `byTeam[<soul's team>]`, with `byTeam` itself stripped) ⊕ soul slot
368
- payload ⊕ `local.settings[cap]` ⊕ `spawn.providers[cap]` — objects deep-merge,
369
- later wins on scalars and arrays. The provider's own `binding` contract
382
+ keys, with `byTeam` stripped) ⊕ soul slot payload ⊕ `local.settings[cap]` ⊕
383
+ `spawn.providers[cap]` — objects deep-merge, later wins on scalars and arrays.
384
+ **No `byTeam[<label>]` is merged into it, the primary's included** (teams
385
+ amendment K): each label's `base ⊕ byTeam[label]` reaches the provider only as
386
+ that label's entry in `OATS_TEAMS` (the preview's `teams`). So `settings.team`
387
+ (and `OATS_TEAM_ID`) is the personal team if the host, the soul or the spawn
388
+ set one; empty means the provider's own default. The provider's own `binding` contract
370
389
  (`normalize → bind → check`) runs over the merged payload exactly as before.
371
390
  Two teams, two messaging identities, one workspace:
372
391
 
@@ -378,18 +397,21 @@ messaging:
378
397
  cloud: { team: aweb:example.cloud }
379
398
  ```
380
399
 
381
- A soul with `team: cloud` hands its messaging provider `{ team: aweb:example.cloud, … }`;
382
- a label under `byTeam` that is not declared in `teams:` is `E_WORKSPACE_SCHEMA`.
383
- **`byTeam` is kernel-merged; whether a provider honours what arrives is the
384
- provider's.** `spawn --preview` shows the merged `settings.<cap>` so the
385
- delivery is verifiable, and `instance.json.providers.<cap>` records it — but
386
- **oats.aweb 1.12.0 reads `team` from its payload** and mints into exactly that
387
- team (`--team-id`), warning when the payload disagrees with an `OATS_TEAM_*`
388
- value — so `byTeam.<label>.team` IS the per-label identity. What the payload
389
- does not change is **where the `.aw` root is found**: the hook still searches
390
- the bounded candidates in
391
- [rebuild-to-v2.md §8b](rebuild-to-v2.md#8b-where-the-team-aw-lives-now-and-what-byteam-does-today)
392
- and that root must hold a membership of the named team (the deployment's `.aw`
400
+ A soul with `team: cloud` hands its messaging provider the eligible team
401
+ `{ label: cloud, team: aweb:example.cloud, mapped: true, payload: { team: aweb:example.cloud, … } }`
402
+ in `OATS_TEAMS`; its settings carry no `team` unless the host, soul or spawn set
403
+ one. A label under `byTeam` that is not declared in `teams:` is
404
+ `E_WORKSPACE_SCHEMA`. **Joining an eligible team is the provider's explicit
405
+ act.** `spawn --preview` shows the merged `settings.<cap>` and the `teams`, so
406
+ the delivery is verifiable, and `instance.json` records both. With oats.aweb
407
+ 1.13.1 (which reads `team` from its settings and ignores `OATS_TEAMS`) the
408
+ primary identity therefore mints into the personal team: the `.aw` root's
409
+ active team, or the one the host set. What the payload does not change is
410
+ **where the `.aw` root is found**: the hook still searches
411
+ bounded candidates, first hit wins — the instance home, the Git repository
412
+ containing it, the soul's work repository and the Git repository containing
413
+ it, then the deployment directory (`OATS_WORKSPACE`); never the user home or
414
+ above the deployment — and that root must hold a membership of the named team (the deployment's `.aw`
393
415
  joined to every team its labels name is the simple layout). On oats.aweb
394
416
  1.11.2 `team` was ignored (the root's active team won), so `byTeam` there is
395
417
  a recorded intent only.
@@ -428,7 +450,7 @@ The deployment directory is **yours to choose** (decision 9) — an existing fol
428
450
  ```
429
451
  ~/acme/ ← the directory you chose
430
452
  ├── oats-local.yaml ← which workspace this machine realizes + host paths + disabled souls
431
- ├── oats-lock.json ← exact commit + integrity + per-version approval per package
453
+ ├── oats-lock.json ← exact commit + integrity per package
432
454
  ├── agents/ ← instance homes (each self-contained) + fetched soul sources
433
455
  ├── platform/ ← clone of github.com/acme/platform (only if someone works IN it; may live elsewhere — see clones:)
434
456
  └── tools/
@@ -454,8 +476,8 @@ the host repo readable (it holds declarations, no secrets) or grant access.
454
476
 
455
477
  Two things keep the standalone spawn useful rather than hollow: `oats.core`
456
478
  (the framework's own operational package) is the kernel's default here as
457
- well, resolved from the official catalog through the operator's own lock and
458
- approved like any package (a soul may say `oats.core: off`); and the
479
+ well, resolved from the official catalog through the operator's own lock like any
480
+ package (a soul may say `oats.core: off`); and the
459
481
  operator's `oats-local.yaml` may name the repo directly (`workspace: <member
460
482
  ref>` — the kernel notices it is a member whose workspace it cannot read and
461
483
  falls back to the standalone view — or `standalone: <repo ref>` to ask for
@@ -467,8 +489,8 @@ an explicit `standalone:` header its next steps say so and name that one repo.
467
489
  member capability's hooks and command scripts run on every operator's machine at
468
490
  spawn, gated by nothing but the handshake. In a mixed public/private
469
491
  organisation keep **souls only** in public members and let executable
470
- capabilities come from packages (approved per version in the lock) or from
471
- private members.
492
+ capabilities come from packages (declared in `packages:`, pinned by the lock)
493
+ or from private members.
472
494
 
473
495
  **Hosting the workspace file when some members are private.** Everyone who
474
496
  can read the workspace file sees the member list. So: a public member never
@@ -502,12 +524,12 @@ installed-capability tier (`.agents/capabilities/installed/`) and
502
524
  `E_UNKNOWN_COMMAND` naming its replacement); per-soul `stores.<x>.inherit`;
503
525
  ambient-skill exclusion at launch. Lock v1/v2 files are `E_LOCK_SCHEMA`.
504
526
  There is no converter and no dual-schema reader: a 0.24.x kernel keeps
505
- spawning 0.24.x deployments; see [rebuild-to-v2.md](rebuild-to-v2.md).
527
+ spawning 0.24.x deployments.
506
528
 
507
529
  ## Related
508
530
 
509
531
  - [Souls and instances](souls-and-instances.md) · [Packages](packages.md) ·
510
- [Configuration (`oats-local.yaml`)](configuration.md) · [Rebuild guide](rebuild-to-v2.md)
532
+ [Configuration (`oats-local.yaml`)](configuration.md)
511
533
  - [Capability manifests](capabilities.md) · [Contracts](layers.md) ·
512
534
  [Desktop CLI API — workspace model](desktop-cli-api.md#workspace-model-workspaceapi-2)
513
535
  - [Design navigation](design/README.md)
@@ -20,7 +20,7 @@ root, and not the work tree. Anything that says "your home" means this directory
20
20
  - **Soul work is repository work.** If your TASK is to change soul content that
21
21
  lives in this repository, that is ordinary code work — do it on tracked paths
22
22
  under `work/`, reviewed like the rest. How your own learnings reach your soul
23
- is your knowledge layer's business, and its instructions below say so if you
23
+ is your knowledge capability's business, and its instructions below say so if you
24
24
  have one.
25
25
 
26
26
  **`<instance-home>/work` is your repository or workspace view** — whatever your
@@ -8,7 +8,7 @@ their branch and their uncommitted state. You are a guest in their workspace.
8
8
  - Keep your changes and commits **small and clearly attributable** (your
9
9
  instance name in commit messages where ambiguity is possible).
10
10
  - Do not touch files the owner is mid-editing unless your task says so; when
11
- in doubt, coordinate through your messaging layer or your spawner.
11
+ in doubt, coordinate through your messaging capability or your spawner.
12
12
  - Retiring you never removes the shared tree — cleanup of the tree is the
13
13
  owner's concern, not yours.
14
14
 
@@ -6,8 +6,8 @@ cross-repo coordinator: your product is routing, analysis, and coordination —
6
6
  not code changes.
7
7
 
8
8
  - **Read freely across all member repos; never edit or commit inside them.**
9
- Repo changes are routed to that repo's own agents (see `oats status --team`,
10
- your task layer, or messaging) or to the human.
9
+ Repo changes are routed to that repo's own agents (see `oats status` in the
10
+ deployment, your tasks capability, or messaging) or to the human.
11
11
  - No git state operations in any member repo: no branch switching, no
12
12
  commits, no worktrees, no resets.
13
13
  - Your own working state lives in your instance home, not in any member repo,
@@ -1,4 +1,4 @@
1
- /** Bounded descriptor-backed reads shared by private portable metadata stores. */
1
+ /** Bounded descriptor-backed reads of kernel-owned files (manifests, instance records). */
2
2
  import { constants, closeSync, fstatSync, lstatSync, openSync, readSync } from "node:fs";
3
3
  import { oatsError } from "./errors.mjs";
4
4
 
@@ -8,19 +8,19 @@ export function readPortableBytes(path, { missingCode = "resource-not-found", in
8
8
  catch (error) {
9
9
  if (error.code !== "ENOENT") throw error;
10
10
  if (allowMissing) return null;
11
- throw oatsError(missingCode, "portable metadata is absent");
11
+ throw oatsError(missingCode, "metadata file is absent");
12
12
  }
13
- if (!stat.isFile()) throw oatsError(invalidCode, "portable metadata must be a regular file");
14
- if (stat.size > 8 * 1024 * 1024) throw oatsError("resource-limit", "portable metadata byte limit exceeded");
13
+ if (!stat.isFile()) throw oatsError(invalidCode, "metadata file must be a regular file");
14
+ if (stat.size > 8 * 1024 * 1024) throw oatsError("resource-limit", "metadata file byte limit exceeded");
15
15
  const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
16
16
  try {
17
17
  const before = fstatSync(fd);
18
- if (!before.isFile() || before.dev !== stat.dev || before.ino !== stat.ino || before.size !== stat.size) throw oatsError("integrity-drift", "portable metadata changed during opening");
18
+ if (!before.isFile() || before.dev !== stat.dev || before.ino !== stat.ino || before.size !== stat.size) throw oatsError("integrity-drift", "metadata file changed during opening");
19
19
  const buffer = Buffer.alloc(stat.size + 1);
20
20
  let count = 0, size;
21
21
  while (count < buffer.length && (size = readSync(fd, buffer, count, buffer.length - count, null)) > 0) count += size;
22
22
  const after = fstatSync(fd);
23
- if (count !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) throw oatsError("integrity-drift", "portable metadata changed during reading");
23
+ if (count !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) throw oatsError("integrity-drift", "metadata file changed during reading");
24
24
  return buffer.subarray(0, count);
25
25
  } finally { closeSync(fd); }
26
26
  }
@@ -1,5 +1,6 @@
1
- /** Bounded data codecs shared by portable declarations and retained records.
2
- * No source resolution, filesystem access, getters, or caller serialization hooks. */
1
+ /** Bounded data codecs: strict JSON decoding and canonical JSON (config documents,
2
+ * instance records, provider answers). No source resolution, filesystem access,
3
+ * getters, or caller serialization hooks. */
3
4
  import { oatsError } from "./errors.mjs";
4
5
 
5
6
  const DEFAULTS = Object.freeze({ maxBytes: 8 * 1024 * 1024, maxDepth: 64, maxEntries: 100_000 });
@@ -39,16 +40,6 @@ export function decodeUtf8(input, where = "input") {
39
40
  }
40
41
  export const compareUtf8 = (a, b) => Buffer.compare(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
41
42
 
42
- /** Freeze a bounded JSON observation, never caller accessors or cyclic objects. */
43
- export function freezeJson(value) {
44
- canonicalJson(value);
45
- const seen = new WeakSet();
46
- const freeze = (item) => {
47
- if (!item || typeof item !== "object" || seen.has(item)) return;
48
- seen.add(item); for (const child of Object.values(item)) freeze(child); Object.freeze(item);
49
- };
50
- freeze(value); return value;
51
- }
52
43
 
53
44
  /** Canonical JSON includes exactly one final LF. Emit sorted keys directly:
54
45
  * JSON.stringify on a rebuilt object would reorder integer-looking keys. */
@@ -0,0 +1,110 @@
1
+ /** The capability manifest's kernel contract, shared by every reader: workspace
2
+ * discovery (member capabilities, E_WORKSPACE_SCHEMA), package manifests
3
+ * (E_PACKAGE_MANIFEST), the resolver (payload values) and the kernel's own
4
+ * manifest loader. One rule set, so a manifest a workspace accepts is one the
5
+ * kernel can run.
6
+ *
7
+ * Covers what the kernel enforces when it RUNS a capability, checked where the
8
+ * manifest is READ instead: the launch environment a capability may declare
9
+ * (names, namespaces), its hooks (approved events, declaration shape, `required`
10
+ * only on spawn, the script inside the capability), and a provider payload's
11
+ * value against the manifest's `settings.<key>.values`. Dependency-free. */
12
+
13
+ export const APPROVED_HOOKS = new Set(["soul-scaffold", "spawn", "retire", "launch"]);
14
+ export const PORTABLE_ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
15
+ export const CAPABILITY_ENV_ID_RE = /^[a-z][a-z0-9]*\.[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
16
+ export const CORE_LAUNCH_ENV = new Set(["OATS_INSTANCE", "OATS_INSTANCE_HOME", "PI_AGENT_INSTANCE", "PI_AGENT_HOME"]);
17
+ export const PROCESS_BOOTSTRAP_ENV = new Set([
18
+ "PATH", "HOME", "SHELL", "TMPDIR", "TMP", "TEMP", "PWD", "OLDPWD", "SHLVL", "_",
19
+ "ENV", "BASH_ENV", "BASHOPTS", "SHELLOPTS", "CDPATH", "IFS", "PROMPT_COMMAND", "PS4", "ZDOTDIR",
20
+ "NODE_OPTIONS", "NODE_PATH", "_JAVA_OPTIONS", "GCONV_PATH", "GLIBC_TUNABLES", "ELECTRON_RUN_AS_NODE",
21
+ ]);
22
+ export const PROCESS_BOOTSTRAP_PREFIXES = [
23
+ "NODE_", "LD_", "DYLD_", "PYTHON", "PERL", "RUBY", "JAVA_", "JDK_JAVA_",
24
+ "DOTNET_", "COMPlus_", "COREHOST_", "LUA_", "PHP_", "ELECTRON_", "GLIBC_",
25
+ ];
26
+
27
+ const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
28
+ const pointerKey = (k) => String(k).replace(/~/g, "~0").replace(/\//g, "~1");
29
+
30
+ /** A hook's script, the first word of its command: relative to the capability root and inside it. */
31
+ export function hookScriptEscapes(command) {
32
+ const script = String(command).trim().split(/\s+/)[0] || "";
33
+ return script.startsWith("/") || script.startsWith("~") || /^[A-Za-z]:[\\/]/.test(script) || script.split(/[\\/]/).includes("..");
34
+ }
35
+
36
+ /** Every contract problem of one manifest → [{ pointer, message }] (empty when it is sound).
37
+ * Messages name the capability; `pointer` is the JSON pointer inside oats.json. */
38
+ export function manifestContractProblems(m) {
39
+ const problems = [];
40
+ const id = m?.capability;
41
+ const bad = (pointer, message) => problems.push({ pointer, message: `capability ${id} ${message}` });
42
+
43
+ if (m.environmentNamespaces !== undefined && (!Array.isArray(m.environmentNamespaces) || m.environmentNamespaces.some((ns) => typeof ns !== "string"))) {
44
+ bad("/environmentNamespaces", "manifest environmentNamespaces must be an array of prefixes");
45
+ }
46
+ if (m.environment !== undefined) {
47
+ if (!Array.isArray(m.environment) || m.environment.some((name) => typeof name !== "string")) {
48
+ bad("/environment", "manifest environment must be an array of exact variable names");
49
+ } else if (new Set(m.environment).size !== m.environment.length) {
50
+ bad("/environment", "manifest environment contains duplicate names");
51
+ } else if (m.environment.length) {
52
+ const vendor = CAPABILITY_ENV_ID_RE.test(id) ? id.match(/^([a-z][a-z0-9]*)\./)?.[1] : undefined;
53
+ if (!vendor) bad("/capability", "must use a lowercase dotted ID (vendor.name) without package or path syntax to declare launch environment");
54
+ else {
55
+ const extra = Array.isArray(m.environmentNamespaces) ? m.environmentNamespaces.filter((ns) => typeof ns === "string") : [];
56
+ extra.forEach((ns, i) => {
57
+ if (!/^[A-Z][A-Z0-9]*_$/.test(ns)) bad(`/environmentNamespaces/${i}`, `manifest environmentNamespaces entry ${JSON.stringify(ns)} must be an uppercase prefix ending in an underscore`);
58
+ else if (ns === "OATS_" || ns === "PI_AGENT_" || PROCESS_BOOTSTRAP_PREFIXES.some((reserved) => ns.startsWith(reserved) || reserved.startsWith(ns))) bad(`/environmentNamespaces/${i}`, `manifest environmentNamespaces entry ${ns} is a reserved namespace`);
59
+ });
60
+ const allowed = [`${vendor.toUpperCase()}_`, ...extra];
61
+ m.environment.forEach((name, i) => {
62
+ const at = `/environment/${i}`;
63
+ if (!PORTABLE_ENV_NAME_RE.test(name)) bad(at, `manifest environment name ${JSON.stringify(name)} is invalid`);
64
+ else if (CORE_LAUNCH_ENV.has(name) || name.startsWith("OATS_") || name.startsWith("PI_AGENT_")) bad(at, `manifest environment name ${name} collides with a reserved core variable`);
65
+ else if (PROCESS_BOOTSTRAP_ENV.has(name) || PROCESS_BOOTSTRAP_PREFIXES.some((reserved) => name.startsWith(reserved))) bad(at, `manifest environment name ${name} collides with a reserved process bootstrap variable`);
66
+ else if (!allowed.some((ns) => name.startsWith(ns))) bad(at, `manifest environment name ${name} is outside its ${allowed.join(", ")} namespace${allowed.length > 1 ? "s" : ""} (declare another in environmentNamespaces)`);
67
+ });
68
+ }
69
+ }
70
+ }
71
+
72
+ if (m.hooks !== undefined && !isObject(m.hooks)) bad("/hooks", "manifest hooks must be an object of event → command");
73
+ const hooks = isObject(m.hooks) ? Object.entries(m.hooks) : [];
74
+ // A hook's launch environment is claimed under the capability's dotted vendor
75
+ // component; an undotted id has none, so its hooks cannot run.
76
+ if (hooks.length && !/^[a-z][a-z0-9]*\./.test(String(id))) bad("/hooks", "declares hooks but its id has no dotted lowercase vendor component (e.g. acme.tool); only a dotted id can carry hooks");
77
+ for (const [event, value] of hooks) {
78
+ const at = `/hooks/${pointerKey(event)}`;
79
+ if (!APPROVED_HOOKS.has(event)) { bad(at, `declares unsupported hook "${event}" (${[...APPROVED_HOOKS].join(", ")})`); continue; }
80
+ const extraKeys = isObject(value) ? Object.keys(value).filter((k) => !["command", "required", "inputs"].includes(k)) : [];
81
+ const command = typeof value === "string" ? value : isObject(value) && typeof value.command === "string" ? value.command : undefined;
82
+ if (command === undefined || !command.trim() || extraKeys.length) { bad(at, `hook "${event}" must be a command string or { command, required, inputs }${extraKeys.length ? ` (unknown: ${extraKeys.join(", ")})` : ""}`); continue; }
83
+ if (isObject(value) && value.required !== undefined && typeof value.required !== "boolean") bad(`${at}/required`, `hook "${event}": "required" must be a boolean`);
84
+ // Only a spawn hook can fail a spawn; marking others required would promise
85
+ // an enforcement that has no defined moment to act.
86
+ else if (isObject(value) && value.required === true && event !== "spawn") bad(`${at}/required`, `hook "${event}" cannot be required — only the spawn hook is enforced (retire, launch and soul-scaffold run outside a spawn transaction)`);
87
+ if (hookScriptEscapes(command)) bad(isObject(value) ? `${at}/command` : at, `hook "${event}" script ${JSON.stringify(command.trim().split(/\s+/)[0])} escapes the capability directory; a hook script is a path inside it`);
88
+ }
89
+ return problems;
90
+ }
91
+
92
+ /** The setting keys a manifest declares (`settings.<key>`), sorted: names only, never their
93
+ * descriptions or defaults. The spawn preview and inspect expose them so a client can gate
94
+ * a choice on a declared key (e.g. a messaging provider's `join`). */
95
+ export function declaredSettings(manifest) {
96
+ return isObject(manifest?.settings) ? Object.keys(manifest.settings).sort() : [];
97
+ }
98
+
99
+ /** Top-level payload keys whose value is outside the manifest's `settings.<key>.values`
100
+ * → [{ key, value, values }]. A conditional `requires` row reads these values, so a
101
+ * misspelled one would silently skip every row instead of failing. */
102
+ export function settingValueProblems(manifest, payload) {
103
+ const out = [];
104
+ if (!isObject(manifest?.settings) || !isObject(payload)) return out;
105
+ for (const [key, decl] of Object.entries(manifest.settings)) {
106
+ if (!isObject(decl) || !Array.isArray(decl.values) || !Object.hasOwn(payload, key) || payload[key] === undefined) continue;
107
+ if (!decl.values.some((v) => String(v) === String(payload[key]))) out.push({ key, value: payload[key], values: decl.values });
108
+ }
109
+ return out;
110
+ }
@@ -2,8 +2,8 @@
2
2
  * object-construction language: no tags, anchors, aliases, merges or coercive keys.
3
3
  * Legacy core readers are unchanged until the explicit consumer migration. */
4
4
  import { Composer, CST, Lexer, Parser, isAlias, isMap, isScalar, isSeq } from "yaml";
5
- import { bytesIntegrity } from "./portable-digest.mjs";
6
- import { byteView, canonicalJson, dataLimits, decodeUtf8, parseStrictJson } from "./portable-values.mjs";
5
+ import { bytesIntegrity } from "./digest.mjs";
6
+ import { byteView, canonicalJson, dataLimits, decodeUtf8, parseStrictJson } from "./canonical-json.mjs";
7
7
  import { oatsError } from "./errors.mjs";
8
8
 
9
9
  const pointerKey = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");