@awebai/oats 0.22.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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/bin/oats.mjs +3294 -0
  4. package/capabilities/oats-authoring/oats.json +12 -0
  5. package/capabilities/oats-aweb/bin/oats-aweb.mjs +297 -0
  6. package/capabilities/oats-aweb/injects/aweb.md +55 -0
  7. package/capabilities/oats-aweb/oats.json +47 -0
  8. package/capabilities/oats-aweb/package.json +20 -0
  9. package/capabilities/oats-jira/bin/oats-jira.mjs +40 -0
  10. package/capabilities/oats-jira/injects/jira.md +10 -0
  11. package/capabilities/oats-jira/oats.json +22 -0
  12. package/capabilities/oats-jira/package.json +25 -0
  13. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +176 -0
  14. package/capabilities/oats-linear/README.md +234 -0
  15. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +32 -0
  16. package/capabilities/oats-linear/bin/oats-linear.mjs +344 -0
  17. package/capabilities/oats-linear/injects/linear.md +8 -0
  18. package/capabilities/oats-linear/oats.json +24 -0
  19. package/capabilities/oats-linear/package.json +29 -0
  20. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +217 -0
  21. package/capabilities/oats-linear/test/oats-linear.test.mjs +168 -0
  22. package/capabilities/oats-okf/agents/memory-harvest.md +25 -0
  23. package/capabilities/oats-okf/bin/oats-okf.mjs +283 -0
  24. package/capabilities/oats-okf/injects/okf.md +62 -0
  25. package/capabilities/oats-okf/oats.json +20 -0
  26. package/capabilities/oats-okf/package.json +22 -0
  27. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +100 -0
  28. package/capabilities/oats-okf/skills/okf/SKILL.md +107 -0
  29. package/capabilities/oats-okf/skills/okf/scripts/okf-validate.mjs +123 -0
  30. package/capabilities/oats-review/agents/reviewer/AGENTS.md +53 -0
  31. package/capabilities/oats-review/agents/reviewer/soul.yaml +6 -0
  32. package/capabilities/oats-review/injects/review.md +68 -0
  33. package/capabilities/oats-review/oats.json +10 -0
  34. package/capabilities/oats-review/skills/code-review/SKILL.md +44 -0
  35. package/capabilities/oats-review/skills/security-review/SKILL.md +59 -0
  36. package/docs/capabilities.md +505 -0
  37. package/docs/capability-manifest.schema.json +223 -0
  38. package/docs/configuration.md +482 -0
  39. package/docs/conventions.md +73 -0
  40. package/docs/design/desktop-ux-plan.md +362 -0
  41. package/docs/design/package-engine-contract.md +813 -0
  42. package/docs/design/package-runtime-api.md +414 -0
  43. package/docs/desktop-cli-api.md +89 -0
  44. package/docs/desktop-succession.md +51 -0
  45. package/docs/desktop.md +187 -0
  46. package/docs/implementation.md +282 -0
  47. package/docs/integrations.md +123 -0
  48. package/docs/knowledge-theory.md +97 -0
  49. package/docs/knowledge.md +139 -0
  50. package/docs/layers.md +110 -0
  51. package/docs/migration-from-oas.md +122 -0
  52. package/docs/oats-config.schema.json +140 -0
  53. package/docs/oats-lock.schema.json +175 -0
  54. package/docs/oats-package.schema.json +129 -0
  55. package/docs/packages.md +442 -0
  56. package/docs/release-lane.md +132 -0
  57. package/docs/release-notes/v0.18.2.md +79 -0
  58. package/docs/release-notes/v0.18.3.md +63 -0
  59. package/docs/release-notes/v0.18.4.md +65 -0
  60. package/docs/release-notes/v0.18.5.md +66 -0
  61. package/docs/release-notes/v0.18.6.md +87 -0
  62. package/docs/release-notes/v0.19.0.md +186 -0
  63. package/docs/release-notes/v0.19.1.md +24 -0
  64. package/docs/release-notes/v0.19.2.md +32 -0
  65. package/docs/release-notes/v0.19.3.md +16 -0
  66. package/docs/release-notes/v0.19.4.md +12 -0
  67. package/docs/release-notes/v0.20.0.md +89 -0
  68. package/docs/release-notes/v0.22.0.md +180 -0
  69. package/docs/souls-and-instances.md +344 -0
  70. package/injects/framework-workspace.md +7 -0
  71. package/injects/instance-boundary.md +43 -0
  72. package/injects/local-soul.md +19 -0
  73. package/injects/oats.md +11 -0
  74. package/injects/work-attached.md +16 -0
  75. package/injects/work-checkout.md +12 -0
  76. package/injects/work-workspace.md +22 -0
  77. package/injects/work-worktree.md +13 -0
  78. package/lib/core.mjs +6792 -0
  79. package/lib/packages.mjs +1375 -0
  80. package/lib/tmux-config.mjs +35 -0
  81. package/package-catalog.json +44 -0
  82. package/package.json +55 -0
  83. package/packages/record/README.md +151 -0
  84. package/packages/record/bin/capture.mjs +256 -0
  85. package/packages/record/bin/recall.mjs +102 -0
  86. package/packages/record/bin/setup.mjs +281 -0
  87. package/packages/record/bin/turn-record.mjs +47 -0
  88. package/packages/record/docs/turn-record-sot.md +350 -0
  89. package/packages/record/lib/canonical.mjs +151 -0
  90. package/packages/record/lib/capture-aw.mjs +179 -0
  91. package/packages/record/lib/capture-cc.mjs +322 -0
  92. package/packages/record/lib/formats.mjs +296 -0
  93. package/packages/record/lib/ignore.mjs +155 -0
  94. package/packages/record/lib/index-db.mjs +524 -0
  95. package/packages/record/lib/project-aweb.mjs +251 -0
  96. package/packages/record/lib/segments.mjs +186 -0
  97. package/packages/record/lib/store.mjs +447 -0
  98. package/packages/record/lib/tags.mjs +114 -0
  99. package/packages/record/package.json +45 -0
  100. package/packages/record/test/vectors/README.md +34 -0
  101. package/packages/record/test/vectors/aweb-projection-v1.json +231 -0
  102. package/packages/record/test/vectors/journal-merge-v1.json +261 -0
  103. package/packages/record/test/vectors/turn-id-v1.json +152 -0
  104. package/packages/record/test/vectors/validate.mjs +391 -0
  105. package/skills/integration-authoring/SKILL.md +76 -0
  106. package/skills/oats/SKILL.md +151 -0
  107. package/skills/oats-config/SKILL.md +162 -0
  108. package/skills/oats-getting-started/SKILL.md +159 -0
  109. package/skills/oats-packages/SKILL.md +180 -0
  110. package/skills/oats-support/SKILL.md +79 -0
  111. package/skills/skill-craft/SKILL.md +109 -0
  112. package/skills/soul-craft/SKILL.md +109 -0
@@ -0,0 +1,813 @@
1
+ # Package engine contract (capability materialization, lock v2)
2
+
3
+ Status: **FROZEN** for the capability-materialization delivery. This document is
4
+ the resolver / projection / lock API that the config-and-CLI lane builds
5
+ against. It implements the accepted Decision "Packages materialize capabilities
6
+ while config templates remain explicitly adopted local policy" (2026-07-29) as
7
+ amended by the founder's simplification ruling: the capability-materialization
8
+ `{ packages, capabilities }` lock **replaces** the earlier package-root
9
+ `lockfileVersion: 2` in place and **remains `lockfileVersion: 2`**. There is no
10
+ lock v3, and there is no compatibility path or migration for the earlier
11
+ transitional v2 shape — it is unsupported input, rejected rather than converted.
12
+ Contract changes go through the coordinator to the maintainer.
13
+
14
+ Companion machine-readable schemas:
15
+
16
+ - [`docs/oats-package.schema.json`](../oats-package.schema.json) — `oats-package.json`
17
+ - [`docs/oats-lock.schema.json`](../oats-lock.schema.json) — `oats-lock.json` v2 (+ readable, usable v1)
18
+
19
+ Addendum: [`package-runtime-api.md`](./package-runtime-api.md) — the public
20
+ package-runtime CLI boundary, npm runtime closure semantics, incremental
21
+ transaction guarantees, and runtime-validated schema invariants.
22
+
23
+ ## 0. The model in one paragraph
24
+
25
+ A **package** is transport: the source, dependency, integrity, review and atomic
26
+ update unit. A **capability** is the installed entity: the versioned, targetable,
27
+ activatable, trustable thing a user actually runs. Acquisition stages a package
28
+ closure in a temporary transaction directory, validates the whole selected
29
+ payload, **materializes** each declared capability into a flat, self-contained
30
+ artifact under `.agents/capabilities/installed/<capability-id>/`, writes the
31
+ exact lock, and discards staging. There is **no persistent package root**.
32
+ Acquisition activates nothing and trusts nothing. Config templates are package
33
+ source material that installation never applies.
34
+
35
+ ## 1. Package source grammar and normalized identity
36
+
37
+ *(Unchanged. Restated because the lock's `source` and `path` fields are parsed
38
+ against exactly this grammar.)*
39
+
40
+ A **package source spec** (CLI argument, or an entry in a manifest's
41
+ `dependencies[]`) takes one of four forms:
42
+
43
+ | Form | Examples | Notes |
44
+ |---|---|---|
45
+ | Shorthand git | `git:github.com/org/repo@v1.2.0#<path>`, `git:host/org/repo@<ref>` | `@ref` and `#<path>` both optional at the CLI; resolved once and exact-locked, never advanced on restore |
46
+ | Raw git URL | `https://host/org/repo.git@v1.2.0#dist/oats`, `git@host:org/repo.git@ref#.` | HTTPS or SSH; same `@ref` and `#<path>` rules |
47
+ | Local path | `./pkgs/mypkg`, `/abs/path`, `path:./pkgs/mypkg` | development escape hatch; locked with `commit: "local"` and tree integrity; **exact directory** — no `#<path>` and no default-path heuristic |
48
+ | Official catalog short ID | `oats.okf`, `oats.okf@v1.4.0` | pattern `^[a-z0-9][a-z0-9._-]*$` with optional `@selector`; resolved through the catalog to a git repo **and its `path`**; takes no `#<path>` |
49
+
50
+ Manifest `dependencies[]` entries must be pinnable: an official selector, a
51
+ pinned git tag/commit (`@ref` required), or a local path. There is **no
52
+ general semver solver**.
53
+
54
+ ### 1.1 Contained package root (`path`)
55
+
56
+ A Git repository is not a package: it *contains* one. The **package root** is
57
+ the directory inside the fetched source that carries `oats-package.json`, and it
58
+ is selected by the source contract — never hardcoded at a use site:
59
+
60
+ - **Git specs** select it with a single `#<path>` fragment, split off *before*
61
+ `@ref` parsing so a path can never be mistaken for part of a ref. One
62
+ fragment maximum; a second `#` is `invalid-source`.
63
+ - **Catalog entries** carry it as data: `{ url, ref?, path? }`. The catalog
64
+ owns its packages' roots, so an entry may move one (see §5.5 `updatePackage`).
65
+ - **Omitted** on either: the default is **`oats-package`**.
66
+ - **Local paths** never take one. `oats install /repo/custom-root` treats that
67
+ exact directory as the package root whatever it is named, and locks `.`.
68
+
69
+ **Canonical form.** A path is POSIX-relative with no redundant or trailing
70
+ separators. Every spelling of the source root (`.`, `./`, `./.`, empty)
71
+ normalizes to the single canonical `"."`, so a root selection round-trips
72
+ identically through spec → lock → JSON → doctor/list/update. Absolute paths,
73
+ Windows drive paths, `~` spellings, backslash separators and NUL are
74
+ `invalid-source`; `..` traversal is `path-escape`.
75
+
76
+ **Resolution and containment.** One exact commit is cloned once; the configured
77
+ path is resolved *inside that checkout by realpath*; `oats-package.json` must be
78
+ there; and **only that subtree** is staged, hashed and projected. A path that
79
+ resolves outside the checkout — through a symlink at any depth — and a broken
80
+ link are `path-escape`, decided before any store or lock mutation. Staged
81
+ payload bytes therefore equal the selected subtree: repository docs, CI
82
+ configuration, owner souls and sibling packages never reach `integrity` and can
83
+ never reach an installed artifact. Root source-control metadata (`.git`) is
84
+ always stripped on staging, including direct local roots.
85
+
86
+ One repository may contain several packages selected by different paths. Because
87
+ the closure dedupe key is *source **and** selected path*, two contained roots
88
+ claiming the same OATS package identity still fail `duplicate-package-identity`.
89
+
90
+ **Normalized identity** (what dedupe and lock keys use):
91
+
92
+ - The **package identity** is the `package` field of the staged
93
+ `oats-package.json` — never derived from the source string.
94
+ - The **normalized source** recorded in the lock is one of
95
+ `git:<canonical-url>@<ref>`, `path:<dir>`, `catalog:<id>` for an originally
96
+ bare catalog request, or `catalog:<id>@<selector>` for an originally explicit
97
+ selector. The resolved catalog commit is recorded separately in `commit`.
98
+ - The **selected package root** is recorded in the lock as its own strict
99
+ `path` field — never folded into the source string, stored in canonical form
100
+ only, never normalized or repaired on read (`invalid-lock` otherwise).
101
+ - A lock's `source` is parsed against **exactly** that writer grammar, and
102
+ never carries a `#<path>` fragment. Strictness is load-bearing: `updatePackage`
103
+ and `readLockedConfigTemplates` re-derive a source spec from this string, so a
104
+ payload that merely starts with a known scheme but is invalid for its kind
105
+ (`catalog:../evil`, `path:relative/dir`) would be RECLASSIFIED. Such entries
106
+ are `invalid-lock` at parse, before anything can act on them.
107
+
108
+ **Catalog resolver boundary**: the catalog is a pure mapping *official short ID
109
+ → git repository (+ optional selector → ref translation, + optional package
110
+ root path)*. It authenticates identity and discovery only. It performs **no lock
111
+ advancement** and grants **no executable trust**; after catalog resolution the
112
+ source behaves exactly like a pinned git source. The engine ships a fixture
113
+ catalog for tests.
114
+
115
+ ## 2. Package manifest: what a package must declare
116
+
117
+ ```json
118
+ {
119
+ "package": "example.engineering",
120
+ "version": "3.0.0",
121
+ "description": "Shared agent capabilities and workspace defaults.",
122
+ "compatibility": { "oats": ">=0.20.0" },
123
+ "capabilities": ["capabilities/example-review", "capabilities/example-delivery"],
124
+ "configTemplates": {
125
+ "default": { "path": "config-templates/default/oats-config.yaml", "default": true }
126
+ },
127
+ "dependencies": ["oats.okf@v1.4.0"]
128
+ }
129
+ ```
130
+
131
+ Binding rules (schema + runtime; JSON Schema alone is not complete — see the
132
+ addendum §4):
133
+
134
+ 1. **`capabilities` is REQUIRED and non-empty.** Config-only and empty packages
135
+ are rejected: `invalid-package-manifest`. A package's reason to exist is the
136
+ capabilities it materializes.
137
+ 2. **Dedicated capability roots.** Each entry names a directory carrying one
138
+ `oats.json`. Authoring never emits `"."`; conventional roots are
139
+ `capabilities/<slug>/`.
140
+ 3. **`configTemplates` is the canonical spelling and is OPTIONAL.** A package
141
+ that ships no template is perfectly valid — installation is about
142
+ capabilities. `configs` is accepted as a deprecated read-only alias so
143
+ immutable published tags stay consumable; both spellings normalize to one
144
+ descriptor shape carrying a diagnostic `legacySpelling`. Carrying **both** is
145
+ `invalid-package-manifest`.
146
+ 4. **Legacy `"."` capability roots are read compatibility, and the discriminator
147
+ is `configTemplates` — never `configs`.** Published packages exist with a
148
+ `"."` root and *no* template map at all (`oats.authoring@1.0.0` is
149
+ `capabilities: ["."]` with neither spelling), so keying acceptance on
150
+ `configs` would strand them. The rule is therefore:
151
+ - a manifest **without** `configTemplates` may declare `"."` — the
152
+ compatibility reader accepts it and projects it;
153
+ - a manifest **with** `configTemplates` is unambiguously new, and `"."` is
154
+ `invalid-package-manifest`;
155
+ - authoring tooling rejects and never emits `"."` regardless.
156
+ `"."` remains exclusive with any other capability path (it would nest one
157
+ capability inside another).
158
+ 5. **Self-containment.** Everything a capability declares (`skills`, `inject`,
159
+ `commands`, `hooks`, `agents`) must exist and resolve **inside that
160
+ capability's own root** after symlink resolution. A capability reaching
161
+ package-only paths, sibling capabilities, or outside the package is
162
+ `capability-not-self-contained` — it cannot be materialized, and the engine
163
+ fails rather than silently installing a broken artifact.
164
+ 6. At most one `configTemplates.*.default === true`; `compatibility.oats` is
165
+ required with exactly the grammar `>=x.y.z` / `^x.y.z` / `x.y.z`.
166
+ 7. Two capability paths in one package exporting the same capability ID is
167
+ `duplicate-capability-id`; two packages at one scope exporting the same
168
+ capability ID is `duplicate-capability-id` with both packages as provenance.
169
+
170
+ ## 3. Store layout and the materialized artifact
171
+
172
+ ```text
173
+ <scope>/oats-config.yaml zero or one active config
174
+ <scope>/oats-lock.json lock (v2)
175
+ <scope>/.agents/capabilities/.gitignore contains exactly `installed/`
176
+ <scope>/.agents/capabilities/owned/<id>/ authored; normally committed
177
+ <scope>/.agents/capabilities/installed/<id>/ MATERIALIZED artifact; ignored
178
+ <scope>/.agents/config-templates/adopted/<pkg>/<template>/
179
+ adopted base + adoption.json
180
+ (written by the config lane;
181
+ NEVER ignored)
182
+ ```
183
+
184
+ - There is **no `<scope>/.agents/packages/`** in this model at all. A package
185
+ checkout exists only inside a transaction staging directory, which is created
186
+ under `.agents/capabilities/installed/.staging-<random>/` (same filesystem as
187
+ the destination, so the commit phase is a rename; already gitignored; skipped
188
+ by discovery because it is dot-prefixed) and removed unconditionally when the
189
+ transaction ends.
190
+ - The **materialized artifact** at `installed/<id>/` is the complete validated
191
+ capability root: its `oats.json`, skills, injections, commands, hooks,
192
+ capability-defined agents, and its materialized runtime closure
193
+ (`node_modules`, §6). It additionally carries a generated
194
+ **`.oats-installation.json`** provenance file.
195
+
196
+ ### 3.1 `.oats-installation.json` — deterministic, replayable provenance
197
+
198
+ The file is **inside** the hashed tree, so tampering with it is integrity drift.
199
+ That is only sound if a future kernel reprojecting the same locked bytes
200
+ produces the same file, so it contains **nothing about the writing kernel** —
201
+ only lock-, source- and manifest-derived values:
202
+
203
+ ```json
204
+ {
205
+ "schemaVersion": 1,
206
+ "capability": "example.review",
207
+ "version": "2.1.0",
208
+ "package": "example.engineering",
209
+ "packageVersion": "3.0.0",
210
+ "source": "catalog:example.engineering",
211
+ "commit": "0123456789abcdef0123456789abcdef01234567",
212
+ "packagePath": "oats-package",
213
+ "capabilityPath": "capabilities/example-review"
214
+ }
215
+ ```
216
+
217
+ Binding serialization: exactly these keys, in exactly this order,
218
+ `JSON.stringify(obj, null, 2)` plus a single trailing `"\n"`, written with mode
219
+ `0o644`. `schemaVersion` is a constant of the format, bumped only by an explicit
220
+ contract change (which is itself an integrity change, so it is visible). Every
221
+ field must agree with the lock rows it was projected from; a disagreement is
222
+ `invalid-lock` at read time, and a modified file is `integrity-drift`. Bare
223
+ restore under a newer kernel therefore reproduces the identical artifact hash.
224
+
225
+ ### 3.2 Integrity boundaries
226
+
227
+ - **Capability artifact integrity** (`capabilityArtifactIntegrity`) hashes
228
+ **every byte** under the artifact root — no exclusions, including
229
+ `node_modules` and `.oats-installation.json`. It is the only thing executable
230
+ trust binds to; the runtime closure is *inside* the artifact, so there is no
231
+ separate dependency digest anywhere in this model.
232
+ - **Package payload integrity** (`packageIntegrity`) hashes the staged package
233
+ subtree excluding any `node_modules` and a root `oats-lock.json`. It proves the
234
+ distribution bytes and is what bare restore re-verifies before reprojecting.
235
+ - `.agents/capabilities/owned/<id>/` and `from: path:<dir>` keep their exact
236
+ current semantics, precedence, and structural trust. `from: installed` means
237
+ the flat installed-capability store regardless of which package supplied it.
238
+
239
+ ### 3.3 Git ignore maintenance is part of the transaction
240
+
241
+ `ensureInstalledGitignore` is **not** post-commit convenience. At a Git-backed
242
+ scope the engine **preflights** `.agents/capabilities/.gitignore` before any
243
+ authoritative mutation: it snapshots the prior bytes (or the file's absence),
244
+ ensures the file contains `installed/`, and if it cannot do so **fails before
245
+ any artifact or lock mutation**. If the transaction later fails, the ignore
246
+ bytes are rolled back with everything else. It writes `installed/` and nothing
247
+ else: `owned/` holds authored capabilities and
248
+ `.agents/config-templates/adopted/` holds portable adopted bases, and neither is
249
+ ever ignored or touched. Outside version control it is a no-op: a non-Git scope
250
+ uses the same layout without pretending Git owns its durability.
251
+
252
+ The engine owns this for acquire, restore, update and v1 migration. The CLI lane
253
+ keeps its own outer rollback journal and snapshots the prior ignore state
254
+ alongside its other state; no engine transaction handle or callback is exchanged.
255
+
256
+ ## 4. Lock v2 (capability materialization)
257
+
258
+ ```json
259
+ {
260
+ "lockfileVersion": 2,
261
+ "packages": {
262
+ "example.engineering": {
263
+ "source": "git:https://example.invalid/engineering.git@v3.0.0",
264
+ "path": "oats-package",
265
+ "version": "3.0.0",
266
+ "commit": "0123456789abcdef0123456789abcdef01234567",
267
+ "integrity": "sha256-…",
268
+ "dependencies": ["oats.okf"]
269
+ }
270
+ },
271
+ "capabilities": {
272
+ "example.review": {
273
+ "version": "2.1.0",
274
+ "package": "example.engineering",
275
+ "path": "capabilities/example-review",
276
+ "integrity": "sha256-…",
277
+ "trusted": false
278
+ }
279
+ }
280
+ }
281
+ ```
282
+
283
+ - Both top-level maps are **required**. `packages` rows exact-lock the transport
284
+ unit: `source`, selected package `path`, `version`, `commit`, package payload
285
+ `integrity`, and **package-identity** `dependencies` — always present, an
286
+ empty array when there are none. `capabilities` rows exact-lock the installed
287
+ entity: `version` (from the capability's own `oats.json`), provider `package`,
288
+ dedicated manifest `path` inside that package, materialized artifact
289
+ `integrity`, and boolean `trusted` bound **only** to that artifact integrity.
290
+ - A package row carries **no** capability list and **no** trust: the capability
291
+ rows' `package` back-reference is the single provider truth, so the two levels
292
+ cannot disagree.
293
+ - Semantic invariants validated before any consumption (`invalid-lock`):
294
+ every `capabilities.*.package` is a key of `packages`; every
295
+ `packages.*.dependencies[]` id is a key of `packages`, with no self-dependency
296
+ and no cycle; canonical `path` spellings; `path:` sources require
297
+ `commit: "local"` and `path: "."`; `git:`/`catalog:` require an exact 40-hex
298
+ commit; sha256 digest shapes; array uniqueness.
299
+ - **Prototype safety is global.** Every map is read into a null-prototype
300
+ object with identity-charset-validated keys, and *every* lookup or membership
301
+ check keyed by a package or capability ID uses `Object.hasOwn` or such a map —
302
+ central read, dependency graph, provider resolution, trust, update and remove
303
+ alike. Raw parsed JSON returns inherited `constructor`, `toString` or
304
+ `valueOf` for `map[id]` when no own entry exists, so a prototype-named or
305
+ hostile ID must never be able to impersonate a provider, a dependency or a
306
+ trust entry, or to bypass a membership check.
307
+
308
+ ### 4.1 The unsupported transitional v2 shape
309
+
310
+ The earlier package-root spelling of `lockfileVersion: 2` is **unsupported
311
+ input**. It is detected centrally, **before any discovery or mutation**, and
312
+ rejected with typed `invalid-lock` and an actionable message naming the
313
+ unsupported transitional v2 shape and scope recreation. It is never converted
314
+ and never partially interpreted.
315
+
316
+ Detection uses **direct raw lock-scope reads** — walking ancestor directories
317
+ that own an `oats-lock.json`, *not* `configChain`, so lock-only scopes with no
318
+ config are visible — and this exact OR predicate:
319
+
320
+ 1. a **nonempty** `lockfileVersion: 2` document with no top-level `capabilities`
321
+ map is unsupported transitional v2 **by itself**; **independently**
322
+ 2. any package row for which
323
+ `Object.hasOwn(row, "capabilities") || Object.hasOwn(row, "trustedCapabilities") || Object.hasOwn(row, "depsIntegrity")`
324
+ is forbidden transitional evidence.
325
+
326
+ The row test is **own-property presence, never truthiness or array length**: an
327
+ empty `capabilities: []`, an empty `trustedCapabilities: []`, and a dependency-free
328
+ old row that carries `capabilities`/`trustedCapabilities` but no `depsIntegrity`
329
+ are all still transitional. Package-row `path` and `dependencies` are **never**
330
+ tells — the current shape retains both.
331
+
332
+ An **empty** transitional `{ "lockfileVersion": 2, "packages": {} }` carries no
333
+ state and is semantically identical to an empty current lock, so it normalizes
334
+ to `{ lockfileVersion: 2, packages: {}, capabilities: {} }`. An empty
335
+ `lockfileVersion: 1` file does **not** normalize: it remains pending explicit
336
+ format migration.
337
+
338
+ ## 5. Exported kernel functions (`lib/core.mjs`)
339
+
340
+ All functions are runtime-neutral and dependency-free like the rest of the
341
+ kernel. Errors are thrown `Error`s carrying `code` (§8) and, where relevant,
342
+ `provenance`. Typed engine codes and messages pass through the CLI verbatim.
343
+
344
+ ### 5.1 Source, manifest, integrity
345
+
346
+ ```js
347
+ export function parsePackageSource(spec, { baseDir } = {})
348
+ export const DEFAULT_PACKAGE_PATH // "oats-package"
349
+ export function normalizePackagePath(raw, opts)
350
+ export function inspectGitSourceRoot(spec)
351
+ export function resolvePackageRoot(checkout, packagePath, spec)
352
+ export function gitCheckoutExactRef(dir, ref, spec)
353
+ export function packageIntegrity(dir) // payload hash; excludes node_modules
354
+ export function capabilityArtifactIntegrity(dir) // materialized artifact hash, no exclusions
355
+ export function capabilityIntegrity(dir) // v1 artifact hash (legacy capability store)
356
+
357
+ /** Load + validate an oats-package.json against §2. Returns the manifest plus
358
+ * _dir, _legacySpelling (true when the deprecated `configs` key was used),
359
+ * _configTemplates (normalized { name: { path, description?, default } } from
360
+ * either spelling), and _capabilities: [{ id, rel, dir, manifest }].
361
+ * @throws "invalid-package-manifest", "path-escape", "duplicate-capability-id",
362
+ * "retired-capability"
363
+ */
364
+ export function loadPackageManifestAt(pdir)
365
+
366
+ /** Assert a capability root can be materialized self-contained: every declared
367
+ * resource exists and resolves (after symlink resolution, recursively through
368
+ * contained directory links) inside `capDir`.
369
+ * @throws "capability-not-self-contained", "path-escape"
370
+ */
371
+ export function assertCapabilitySelfContained(capDir, manifest)
372
+ ```
373
+
374
+ ### 5.2 Locks
375
+
376
+ ```js
377
+ /** THE strict lock reader/validator for v1 and v2. Returns
378
+ * { version, packages, capabilities, legacyCapabilities } — all null-prototype
379
+ * and validated — or null when the file is absent. Rejects the unsupported
380
+ * transitional v2 shape (§4.1) centrally, with NO side effects. Old locks are
381
+ * read as they are: never normalized, repaired or rewritten (the one exception
382
+ * is the state-free empty transitional document of §4.1).
383
+ */
384
+ export function parseLockFileStrict(file)
385
+
386
+ /** Read every lock visible from a directory — every ancestor owning an
387
+ * oats-lock.json, plus config-chain levels — closest scope wins per identity.
388
+ * SOLE strict reader; consumers never see an invalid lock as absent or usable.
389
+ * @returns {{
390
+ * packages: Record<pkgId, PackageRow & { _file, _level }>,
391
+ * capabilities: Record<capId, CapabilityRow & { _file, _level }>,
392
+ * legacy: Array<{ file, level, lockfileVersion, capabilities }>, // v1 files
393
+ * migration: Array<{ file, level, lockfileVersion, kind: "v1"|"v1-empty",
394
+ * capabilities: string[] }>
395
+ * }}
396
+ * // `migration` is provenance ONLY: which scopes still need an explicit
397
+ * // conversion and what they hold. Reading never converts anything.
398
+ */
399
+ export function readPackageLocks(startDir)
400
+
401
+ /** Write/replace (entry) or delete (entry === null) one package row / one
402
+ * capability row. Each validates the COMPLETE prospective document before
403
+ * writing. An existing v1 lock — INCLUDING an empty one — is `legacy-lock`:
404
+ * conversion happens only through explicit migration, so only an ABSENT lock is
405
+ * a fresh document.
406
+ */
407
+ export function writePackageLock(levelDir, packageId, entry)
408
+ export function writeCapabilityLockEntry(levelDir, capabilityId, entry)
409
+
410
+ /** Semantic validation of one row against the whole document (§4). */
411
+ export function validateLockEntry(packageId, entry, allPackages, opts)
412
+ export function validateCapabilityLockEntry(capabilityId, entry, allPackages, opts)
413
+ ```
414
+
415
+ ### 5.3 Acquisition and projection
416
+
417
+ ```js
418
+ /** Stage → validate → materialize → lock → discard staging, at one scope.
419
+ *
420
+ * Fetches the root source and the whole dependency closure into a temporary
421
+ * staging directory; validates every manifest (§2), detects cycles, identity
422
+ * collisions and capability-ID collisions (within the closure, and against
423
+ * capabilities already locked at this scope by packages outside it); asserts
424
+ * every capability is self-contained; materializes each capability's runtime
425
+ * closure IN STAGING; calls `opts.assertCommittable` (below); preflights the
426
+ * scope's `.gitignore` (§3.3); then atomically swaps every projected artifact
427
+ * into `.agents/capabilities/installed/<id>/` and writes the lock. On ANY
428
+ * failure — before OR during the commit phase — every already-renamed artifact,
429
+ * the lock bytes and the ignore bytes are rolled back to the pre-operation
430
+ * state. Staging is always removed.
431
+ *
432
+ * ANCHOR DIRECTORIES: staging must live inside the store, so on a scope with no
433
+ * store this necessarily creates `.agents/`, `.agents/capabilities/` and
434
+ * `.agents/capabilities/installed/`. Every refusal and failure path removes
435
+ * exactly the anchors THIS operation created — deepest-first, only while empty,
436
+ * never a pre-existing one and never one holding owned/, adopted/ or any
437
+ * unrelated state. A refused or failed acquisition therefore leaves the scope's
438
+ * tree byte-for-byte and entry-for-entry as it found it, including on a
439
+ * completely clean scope.
440
+ *
441
+ * A v1 lock at the scope is refused BEFORE any source fetch, staging, ignore or
442
+ * artifact work — every v1, INCLUDING AN EMPTY ONE. An empty v1 is still an
443
+ * unconverted scope: converting it as a side effect of `oats install` is the
444
+ * implicit migration §7 forbids, and failing later would make the caller pay for
445
+ * a fetch to learn it.
446
+ *
447
+ * ACTIVATES NOTHING and TRUSTS NOTHING: `trusted` is false for every capability
448
+ * whose artifact integrity is not byte-identical to the one already locked.
449
+ *
450
+ * @param {string} levelDir scope directory owning the store + lock
451
+ * @param {string} spec package source spec
452
+ * @param {{ catalog?, replace?: boolean, expectPackage?: string, rootSnapshot?,
453
+ * assertCommittable?: (preview) => void }} [opts]
454
+ * // `assertCommittable` is the PRE-COMMIT GATE. It is called exactly once,
455
+ * // after the whole closure is projected in staging and BEFORE the ignore
456
+ * // preflight, the artifact swap and the lock write — so the scope is still
457
+ * // completely untouched. It receives the full staged outcome:
458
+ * //
459
+ * // { root, packages: [{ package, version, source, path, commit, integrity,
460
+ * // dependencies, capabilities }],
461
+ * // capabilities: [{ capability, version, package, path, integrity,
462
+ * // trusted, status, layer, executableSurface }],
463
+ * // configTemplates } // identical descriptors to the return value's,
464
+ * // // including `content` and `contentIntegrity`
465
+ * //
466
+ * // It is a PURE GATE: inspect and (optionally) throw, nothing else. A throw
467
+ * // propagates unchanged, staging is discarded, and NOTHING is mutated — no
468
+ * // ignore file, no artifact, no lock byte — so a refusal needs no rollback.
469
+ * // This is what lets guided `oats init --package` present and validate the
470
+ * // complete selected template plan before any engine mutation, and what lets
471
+ * // `oats update` refuse a config-referenced export drop byte-exactly (§5.5).
472
+ * // Staging paths are deliberately NOT exposed: the gate decides, it does not
473
+ * // reach into the transaction.
474
+ * //
475
+ * // `layer` is the capability's DECLARED fundamental layer, normalized to
476
+ * // null when it declares none. It is in the preview because a config
477
+ * // template may bind a fundamental slot to a capability the ROOT PACKAGE
478
+ * // ITSELF supplies: before the commit that capability is not materialized,
479
+ * // is not in the lock, and is not discoverable, so the preview is the only
480
+ * // place the binding can be validated. Validating it after the commit and
481
+ * // unwinding is strictly worse — the gate exists so that case never needs an
482
+ * // outer rollback. The field is the minimum needed for that check; it is not
483
+ * // a staging path and not the manifest.
484
+ * @returns {{
485
+ * root: string,
486
+ * lockFile: string,
487
+ * installed: Array<{ package, version, source, path, commit, integrity,
488
+ * dependencies: string[], capabilities: string[], kept: boolean }>,
489
+ * capabilities: Array<{ capability, version, package, path, integrity, dir,
490
+ * trusted: boolean, status: "installed"|"replaced"|"kept",
491
+ * layer: string|null,
492
+ * executableSurface: { commands: string[], hooks: string[],
493
+ * environment: string[] } }>,
494
+ * configTemplates: Array<{ package, template, path, description?, default: boolean,
495
+ * content: string, contentIntegrity: string,
496
+ * legacySpelling?: boolean }>
497
+ * }}
498
+ * // `configTemplates` carries VALIDATED descriptors AND payload bytes read
499
+ * // from staging before it is discarded, with digests identical to what
500
+ * // `readLockedConfigTemplates` would return, so the config lane can adopt a
501
+ * // template inside the same transaction without a second fetch. Acquisition
502
+ * // itself applies none of them.
503
+ * @throws "invalid-source", "invalid-package-manifest", "path-escape",
504
+ * "capability-not-self-contained", "dependency-cycle",
505
+ * "duplicate-package-identity", "duplicate-capability-id",
506
+ * "incompatible-oats", "integrity-drift", "legacy-lock", "invalid-lock"
507
+ */
508
+ export function acquirePackage(levelDir, spec, opts)
509
+
510
+ /** Bare restore, for every visible lock-owning scope.
511
+ *
512
+ * Preflight parses and caches the COMPLETE visible chain before any fetch,
513
+ * staging or swap. Per capability: a present artifact whose integrity equals the
514
+ * locked integrity is `ok`. Otherwise the provider package's EXACT locked
515
+ * provenance (source + commit + path) is fetched once per package into staging,
516
+ * its payload integrity is verified against the package row, the capability is
517
+ * reprojected, its artifact integrity is verified against the capability row,
518
+ * and only then swapped in. NEVER advances source/version/commit/path, never
519
+ * changes `trusted`, never converts a lock. v1 scopes restore through the
520
+ * existing legacy capability path and are reported with their migration action.
521
+ *
522
+ * @returns {Array<{ package?, capability?, level, status:
523
+ * "ok"|"restored"|"failed"|"legacy", dir?, reason?, code? }>}
524
+ * @throws "invalid-lock" (preflight, before any mutation)
525
+ */
526
+ export function restorePackages(startDir, opts)
527
+
528
+ /** Derive the package/provider view from the lock + the flat capability store —
529
+ * NOT from a package root (there is none). Closest scope wins per package
530
+ * identity; two same-scope packages claiming one capability ID is
531
+ * duplicate-capability-id.
532
+ * @returns {Array<{ package, version, level, source, path, commit, integrity,
533
+ * dependencies: string[],
534
+ * capabilities: Array<{ id, version, path, dir, integrity, trusted,
535
+ * installed: boolean, manifest? }> }>}
536
+ * // `installed:false` + absent `manifest` = locked but not materialized —
537
+ * // exactly what a bare `oats install` repairs. It is reported, never hidden.
538
+ */
539
+ export function listInstalledPackages(startDir)
540
+
541
+ export function installedCapabilityDir(levelDir, capabilityId)
542
+ export const CAPABILITY_INSTALLATION_FILE // ".oats-installation.json"
543
+ ```
544
+
545
+ ### 5.4 Trust
546
+
547
+ ```js
548
+ /** Is this capability's executable surface approved at its CURRENT materialized
549
+ * artifact integrity? Two call shapes (unchanged):
550
+ * capabilityTrust(startDir, capabilityId) // contract shape
551
+ * capabilityTrust(manifest, startDir) // internal resolver/dispatch shape
552
+ * @returns {{ trusted, package, integrity,
553
+ * executableSurface: { commands, hooks, environment },
554
+ * reason? }}
555
+ */
556
+ export function capabilityTrust(a, b)
557
+
558
+ /** Approve executable surfaces at exactly the current artifact integrity.
559
+ * Per-capability by default; `allCapabilities` treats `id` as a PACKAGE identity
560
+ * and approves every capability that package currently supplies (the caller must
561
+ * have displayed the full executable-surface summary first). Writes
562
+ * `trusted: true` on the capability rows. Non-executable capabilities need no
563
+ * approval (no-op, reported in `skipped`). Official identity grants nothing.
564
+ *
565
+ * Two preconditions per target, BOTH required before any flag is set:
566
+ * 1. the materialized artifact hashes to the capability row's `integrity`;
567
+ * 2. its `.oats-installation.json` agrees with that capability row and its
568
+ * provider package row.
569
+ * Integrity alone is not sufficient — a provenance file edited and then
570
+ * re-hashed into its row leaves every byte matching its recorded digest with
571
+ * only the ORIGIN disagreeing, and approval is what unlocks execution. Both are
572
+ * checked for every target before the single lock write, so a bulk approval
573
+ * commits nothing when one capability's origin is disputed.
574
+ * @throws "unknown-capability", "integrity-drift", "invalid-lock"
575
+ */
576
+ export function approveCapability(levelDir, id, { allCapabilities } = {})
577
+ ```
578
+
579
+ ### 5.5 Update and remove
580
+
581
+ ```js
582
+ /** Transactional update of one package: re-resolve the closure from the row's
583
+ * ORIGINAL spec (or opts.spec), validate everything in staging, then replace
584
+ * ALL of that package's exported capability artifacts and lock rows together.
585
+ *
586
+ * - Every export is validated and replaced atomically — never a partial set.
587
+ * - Trust is preserved ONLY for capabilities whose new artifact integrity is
588
+ * byte-identical to the locked one; any change sets `trusted: false`.
589
+ * - Exports that no longer exist are removed ONLY when safe: no config in the
590
+ * chain references them. Otherwise the whole update fails `remove-blocked`
591
+ * and the pre-operation state is restored.
592
+ * @returns {{ package, level, changed, pathChanged, before, after, installed,
593
+ * capabilities, configTemplates, addedCapabilities, removedCapabilities,
594
+ * retiredArtifacts, invalidatedApprovals }}
595
+ */
596
+ export function updatePackage(startDir, packageId, opts)
597
+
598
+ /** Remove one locked package and every capability artifact it supplied.
599
+ * Refuses (`remove-blocked`, with provenance) while another locked package in
600
+ * the TARGET ENTRY'S OWN scope map depends on it, or any config in the chain
601
+ * references one of its capabilities. Transactional: artifacts move to a
602
+ * backup, lock rows are removed, and both sides roll back on failure.
603
+ */
604
+ export function removePackage(startDir, packageId)
605
+ ```
606
+
607
+ ### 5.6 Config templates (read-only, exact-locked)
608
+
609
+ ```js
610
+ /** Read config templates from the EXACT currently locked source of one package
611
+ * — the config lane's `oats config diff` / `oats config sync` / `oats config adopt`
612
+ * input.
613
+ *
614
+ * Stages the locked source (source + commit + path), validates the manifest and
615
+ * its resource containment, verifies the payload integrity against the package
616
+ * row, reads the requested template bytes, and removes staging. It NEVER
617
+ * persists a package root, never exposes a path into one, never mutates the lock
618
+ * or the capability store, and never advances anything. A plain list of what is
619
+ * installed must NOT call this — it is a network operation.
620
+ *
621
+ * @param {{ template?: string, catalog? }} [opts] template omitted = all of them
622
+ * @returns {{ package, source, version, commit, path, integrity, legacySpelling,
623
+ * templates: Array<{ template, path, description?, default, content,
624
+ * contentIntegrity, legacySpelling }> }}
625
+ * // `integrity` is the package PAYLOAD integrity, verified equal to the lock.
626
+ * // The CLI lane owns oats-config schema/policy validation.
627
+ * @throws "unknown-capability" (no such locked package), "invalid-lock",
628
+ * "integrity-drift", "invalid-package-manifest", "invalid-source",
629
+ * "unknown-config-template"
630
+ */
631
+ export function readLockedConfigTemplates(startDir, packageId, opts)
632
+ ```
633
+
634
+ **One template descriptor shape, two readers.** Acquisition (§5.3
635
+ `configTemplates`) and this locked reader produce the SAME descriptor, field for
636
+ field, and both carry `legacySpelling` on **every template item** — the root
637
+ `legacySpelling` here is a package-level convenience duplicate, not the only
638
+ place it appears. A consumer must never have to know which reader produced a
639
+ descriptor in order to read it.
640
+
641
+ **`contentIntegrity` digests the exact FILE BYTES**, not the decoded string:
642
+ `sha256-<64 lowercase hex>` over the bytes on disk. Digesting the decoded string
643
+ would hash U+FFFD replacement characters for any byte sequence that failed to
644
+ decode, yielding a digest nothing can reproduce from the file — and adoption
645
+ compares template bytes. Config templates are UTF-8 text by contract, so the
646
+ decode is **fail-closed**: undecodable bytes are `invalid-package-manifest`, a
647
+ malformed package, never silently repaired. Acquisition and the locked reader
648
+ therefore agree byte-for-byte and digest-for-digest on the same locked source.
649
+
650
+ ### 5.7 Migration (v1 only)
651
+
652
+ ```js
653
+ /** Plan the conversion of one scope's v1 lock. PURE — applies nothing.
654
+ *
655
+ * `marketplace:` entries map through the catalog (aliases first, then identity);
656
+ * `git:`/`path:` entries map to package specs when the source really is a
657
+ * package. Anything the scope cannot convert makes the WHOLE SCOPE
658
+ * unconvertible — the lock has no residue container, so an entry left behind
659
+ * would have nowhere to live and would simply be dropped. `hold`, `manual` AND
660
+ * `retain` therefore all clear `convertible`, and the scope stays v1 in full
661
+ * and keeps working. (`retain` is the guided mode's "keep this custom entry
662
+ * unchanged"; keeping it is only possible by keeping the entire scope.)
663
+ *
664
+ * @returns {{ from: 1|2, convertible: boolean,
665
+ * plan: Array<{ capabilityId?, v1?, package?, spec?,
666
+ * action: "acquire"|"convert-format"|"hold"|"manual"|"retain" ,
667
+ * reason? }>,
668
+ * warnings: string[] }}
669
+ */
670
+ export function migrateLegacyLock(levelDir, opts)
671
+
672
+ /** Apply that plan, transactionally and all-or-nothing per scope. Executable
673
+ * approvals are never carried over — a v1 capability artifact and a materialized
674
+ * artifact are different bytes — and the returned `trust[]` names every surface
675
+ * to re-approve. Any failure restores the original v1 lock BYTE-IDENTICALLY,
676
+ * removes everything the conversion created, and leaves superseded v1 artifacts
677
+ * in place.
678
+ *
679
+ * A MIXED scope — official work beside `retain` entries — is refused
680
+ * `legacy-lock` BEFORE any lock, artifact or ignore mutation: not one official
681
+ * artifact is partially acquired, and the config, store and trust of that scope
682
+ * are byte-identical afterwards.
683
+ *
684
+ * There is NO residue result and no residue container. `retained` appears only
685
+ * on a `skipped` scope — one with no official work at all, left entirely on v1 —
686
+ * and lists the v1 capability ids that were left untouched.
687
+ * @returns {{ migrated, skipped?, retained?, formatConverted?, warnings, file, trust }}
688
+ * @throws "official-mapping-unavailable", "legacy-lock", "invalid-lock", …
689
+ */
690
+ export function applyLegacyLockMigration(levelDir, opts)
691
+ ```
692
+
693
+ ## 6. Runtime closure in the materialized model
694
+
695
+ - **Materialization roots are the declared CAPABILITY roots**, each carrying
696
+ both `package.json` and `package-lock.json`. A package-root-only closure has
697
+ no durable home and is **not** materialized — it is package tooling. If a
698
+ capability actually needs it, self-containment (§2.5) fails and the package is
699
+ rejected. (For a legacy `"."` capability root, capability root == package root,
700
+ so the package-root closure *is* the capability closure.)
701
+ - `npm ci --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund` only;
702
+ no npm lifecycle scripts ever run, at any phase.
703
+ - Transaction-wide platform-invariance preflight over EVERY materialization
704
+ root's lockfile **before** any `npm ci`; post-materialization `.node` native
705
+ binary scan and symlink containment (every link under every materialized
706
+ `node_modules` must realpath-resolve inside the **capability artifact root**)
707
+ **before** any digest or swap.
708
+ - Materialization happens **in staging**; a failure fails the whole transaction
709
+ with the store and lock unchanged.
710
+ - The closure is inside the artifact, so it is covered by the capability's
711
+ `integrity`. Tampering with a materialized dependency invalidates `trusted`
712
+ exactly like source drift, and bare restore reprojects it.
713
+
714
+ ## 7. Compatibility
715
+
716
+ **v1 stays usable without any implicit conversion.** Runtime discovery, exact
717
+ restore, trust checks and approval updates, and `doctor`/`list` diagnosis all
718
+ continue to work against v1 locks and the existing
719
+ `.agents/capabilities/installed/` v1 artifacts. Ordinary use of an unconverted
720
+ deployment must not require migration; only lifecycle *mutation* through the new
721
+ package surface does (`legacy-lock`, naming the explicit command). Guided
722
+ official migration converts a v1 scope **directly** into this flat capability
723
+ materialization.
724
+
725
+ **Immutable published package manifests** using `configs`, or a `"."` capability
726
+ root, remain readable and projectable under §2.3–2.4.
727
+
728
+ **The earlier transitional package-root v2** is unsupported input (§4.1): it
729
+ fails clearly rather than being guessed at or converted. Existing local
730
+ pre-adoption state is recreated by reinstalling; there is no product migration
731
+ path, and there is no compatibility subsystem to maintain.
732
+
733
+ ## 8. Error taxonomy
734
+
735
+ Stable `error.code` values (also the `--json` envelope codes):
736
+
737
+ | code | Meaning |
738
+ |---|---|
739
+ | `invalid-source` | source spec parses to none of the four grammar forms |
740
+ | `invalid-package-manifest` | `oats-package.json` missing/invalid against §2, or a declared path does not identify the expected resource kind |
741
+ | `path-escape` | a declared path resolves outside its containment root after symlink resolution (package root when staging, capability root when projecting, artifact root at runtime) |
742
+ | `capability-not-self-contained` | a declared capability cannot be materialized as a self-contained artifact (a declared resource is missing, or resolves outside its capability root) |
743
+ | `dependency-cycle` | package dependency graph contains a cycle (provenance: the cycle path) |
744
+ | `duplicate-package-identity` | two sources claim the same package identity at one scope (provenance: both sources) |
745
+ | `duplicate-capability-id` | two packages export the same capability ID at one scope, or one package exports it twice (provenance: both) |
746
+ | `integrity-drift` | staged/installed bytes ≠ locked integrity (package payload or capability artifact), or a trust operation against a drifted artifact |
747
+ | `capability-list-mismatch` | a locked capability's provider package no longer exports it at the locked path |
748
+ | `incompatible-oats` | `compatibility.oats` floor not met by the running kernel |
749
+ | `retired-capability` | a package exports / config references a capability the kernel has retired |
750
+ | `legacy-lock` | operation requires the current lock shape but the scope has v1 — run the explicit migration command |
751
+ | `invalid-lock` | lock violates the semantic invariants of §4, **or** is the unsupported transitional v2 shape of §4.1 — fail closed, no normalization, no auto-repair, no side effects |
752
+ | `unknown-capability` | trust/update/remove/template target is not present in the visible locks |
753
+ | `unknown-config-template` | the package has no config template by that name |
754
+ | `remove-blocked` | removal target is still referenced by config or by a dependent locked package (provenance: the blockers) |
755
+ | `official-mapping-unavailable` | guided official migration cannot map a legacy official capability yet; the scope was left unchanged |
756
+ | `unsafe-config-key` | a YAML document uses `__proto__` as a mapping key, or a command was asked to write a key that cannot be one — `__proto__` (assigning it rewrites the parsed object's prototype instead of becoming data, leaving the entry invisible to every key validator), or a `--settings` key / `--soul` / `--type` name carrying a control character, surrounding whitespace, a leading YAML structure indicator, or the `:`/`#` that end a key token. The reader names the mapping key; whoever holds the path re-raises it naming the file, and the CLI renders it as an ordinary typed failure (one `oats:` line, or one `--json` envelope) — never an uncaught stack |
757
+ | `unsafe-config-value` | a command was asked to WRITE config text that cannot survive as one YAML scalar on one line — a control character (the newline is the one that matters: a crafted `oats use --settings k=v` value injected a whole second capability entry), leading/trailing whitespace a read would strip, or a leading YAML structure indicator. Refused before anything is written; the message names the offending input (`--settings value for "mode"`, `--description`) |
758
+ | `E_NO_CONFIG` | the command needs an `oats-config.yaml` it cannot find. TWO emitters, with different conditions and different remedies — stated separately below, because no single sentence is true of both |
759
+
760
+ `E_NO_CONFIG` is shared by two commands that ask different questions. Both are
761
+ diagnosis only: neither writes anything.
762
+
763
+ | emitter | condition | remedy named |
764
+ |---|---|---|
765
+ | `oats use <capability>` | the capability IS present in this scope's own `installed/`/`owned/` store, **and** there is no `oats-config.yaml` at this scope or at any level above it — the chain is empty, so the chain walk never opens this scope's store and the capability would otherwise be reported as never acquired | `oats init --raw --dir <scope>` — offline, deterministic, writes only the minimal config — then the same `oats use` again |
766
+ | `oats config <diff\|sync\|adopt>` | there is no `oats-config.yaml` **at this exact directory**, whatever the chain above it holds: the three-way template lane compares THIS scope's file against its recorded adopted base, so an outer scope's config is not a substitute and the chain is not consulted | `oats init --package <source> --config <name>` — the adopting form, because this lane needs a config that carries a recorded template base |
767
+
768
+ Neither is `E_NO_ADOPTED_BASE`, the next check in the same lane: there the file
769
+ exists but was not adopted from a config template, so no recorded base exists to
770
+ compare against (remedy: `oats config adopt <package> --config <name>`).
771
+
772
+ Fail-closed enforcement points: `parseLockFileStrict`, `readPackageLocks` and
773
+ `listInstalledPackages` RAISE — consumers never see an invalid lock as absent or
774
+ usable data; the writers validate the complete prospective document before
775
+ writing; restore, trust queries, approval, update/remove/migration planning and
776
+ the template reader all validate before acting. Doctor is the only consumer that
777
+ continues past an invalid lock, and it never uses the invalid data.
778
+
779
+ ## 9. Invariants (restated from the Decision — binding)
780
+
781
+ - Acquisition activates nothing, targets nothing and trusts nothing.
782
+ - Every exported capability stays independently addressable, targetable,
783
+ activatable, configurable, excludable and trustable by capability ID;
784
+ capability manifests still cannot carry deployment targets; packages cannot
785
+ make capabilities, family assignments or settings mandatory.
786
+ - Package installation applies **no** config template and creates **no** active
787
+ config. Templates are reported as optional follow-ups.
788
+ - A materialized capability is self-contained: its complete validated local
789
+ production closure and every declared artifact, with all paths and symlinks
790
+ inside the capability root after resolution.
791
+ - Trust binds to capability artifact integrity, never to package identity;
792
+ official catalog identity is not executable approval; any artifact change
793
+ resets `trusted` to false.
794
+ - No silent lock advancement anywhere: bare restore never changes
795
+ source/version/commit/path; only an explicit `oats update <package-id>` may.
796
+ The selected package ROOT advances only where the source owns it: a catalog
797
+ entry supplies `path`, so an update adopts a moved root and reports
798
+ `pathChanged`; a Git spec's `#<path>` is the operator's own selection and
799
+ stays sticky across updates. A path mismatch on acquire therefore names the
800
+ route that can resolve it — `oats update` for catalog sources, `oats remove`
801
+ followed by a re-install with the intended `#<path>` for Git sources (removal
802
+ still refuses while config or dependent packages reference the package).
803
+ - No npm lifecycle scripts, ever; production closure only; platform-invariant
804
+ closures required.
805
+ - Existing config targeting / layer / injection / override / scope-precedence
806
+ semantics are unchanged, including `from: installed`, `from: owned`,
807
+ `from: path:<dir>`, `none` for inherited layers, and injection ejection.
808
+ - `.agents/capabilities/owned/<id>` and `from: path:<dir>` capability development
809
+ are untouched by package paths and keep their existing structural/executable
810
+ trust, targeting, override and composition semantics.
811
+ - Git-backed scopes ignore `installed/` only, ensured transactionally; `owned/`
812
+ and adopted config-template data are never ignored. Non-Git scopes work
813
+ without fake Git state.