@awebai/oats 0.25.9 → 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 (177) 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 +279 -93
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
  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 +8 -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-review/oats.json +3 -2
  20. package/docs/capabilities.md +218 -47
  21. package/docs/capability-manifest.schema.json +13 -4
  22. package/docs/configuration.md +17 -5
  23. package/docs/conventions.md +16 -26
  24. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  25. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  26. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  27. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  28. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  29. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  30. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  31. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  32. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
  33. package/docs/design/2026-09-24-phase-d-plan.md +57 -0
  34. package/docs/design/2026-09-25-teams-contract.md +226 -0
  35. package/docs/design/README.md +3 -3
  36. package/docs/design/launch-configurations.md +20 -16
  37. package/docs/design/operations-contract.md +27 -10
  38. package/docs/desktop-cli-api.md +537 -261
  39. package/docs/desktop-instance-start.md +1 -1
  40. package/docs/desktop.md +7 -13
  41. package/docs/execution-targets.md +16 -18
  42. package/docs/first-team.md +14 -17
  43. package/docs/implementation.md +28 -59
  44. package/docs/integrations.md +64 -33
  45. package/docs/knowledge-capability-authoring.md +1 -1
  46. package/docs/knowledge-reference/package-craft.md +10 -8
  47. package/docs/knowledge-theory.md +1 -1
  48. package/docs/knowledge.md +10 -11
  49. package/docs/layers.md +16 -17
  50. package/docs/oats-local.schema.json +29 -1
  51. package/docs/oats-membership.schema.json +5 -3
  52. package/docs/oats-package.schema.json +2 -2
  53. package/docs/oats-workspace.schema.json +1 -1
  54. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  55. package/docs/packages.md +75 -52
  56. package/docs/release-notes/v0.22.0.md +1 -1
  57. package/docs/release-notes/v0.23.1.md +1 -1
  58. package/docs/release-notes/v0.26.0.md +670 -0
  59. package/docs/schedules.md +48 -126
  60. package/docs/soul.schema.json +11 -4
  61. package/docs/souls-and-instances.md +56 -43
  62. package/docs/workspaces.md +80 -58
  63. package/injects/instance-boundary.md +1 -1
  64. package/injects/work-attached.md +1 -1
  65. package/injects/work-workspace.md +2 -2
  66. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  67. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  68. package/lib/capability-contract.mjs +110 -0
  69. package/lib/config-data.mjs +2 -2
  70. package/lib/core.mjs +700 -4824
  71. package/lib/digest.mjs +12 -0
  72. package/lib/instance-inspect.mjs +396 -0
  73. package/lib/instance-lifecycle.mjs +3 -4
  74. package/lib/instance-resolution.mjs +212 -26
  75. package/lib/instruction-composition.mjs +0 -20
  76. package/lib/materialize.mjs +6 -4
  77. package/lib/operator-dispatch.mjs +33 -13
  78. package/lib/packages.mjs +25 -190
  79. package/lib/provider-binding.mjs +4 -2
  80. package/lib/provider-reasons.mjs +3 -68
  81. package/lib/resolve.mjs +204 -68
  82. package/lib/schedule.mjs +97 -272
  83. package/lib/servers.mjs +13 -13
  84. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  85. package/lib/tree-copy.mjs +44 -0
  86. package/lib/workspace.mjs +125 -20
  87. package/package-catalog.json +6 -6
  88. package/package.json +1 -1
  89. package/skills/integration-authoring/SKILL.md +48 -40
  90. package/skills/oats-getting-started/SKILL.md +105 -110
  91. package/skills/oats-support/SKILL.md +2 -2
  92. package/skills/soul-craft/SKILL.md +13 -6
  93. package/bin/oats-pi-sdk-host.mjs +0 -17
  94. package/docs/2026-09-03-architecture-proposal.md +0 -642
  95. package/docs/artifact-approvals.schema.json +0 -7
  96. package/docs/captured-invocation-context.schema.json +0 -7
  97. package/docs/captured-resolution.schema.json +0 -7
  98. package/docs/design/package-engine-contract.md +0 -813
  99. package/docs/design/package-runtime-api.md +0 -588
  100. package/docs/desktop-succession.md +0 -57
  101. package/docs/execution-capsule.schema.json +0 -108
  102. package/docs/first-team-demo.md +0 -92
  103. package/docs/knowledge-migration.md +0 -147
  104. package/docs/migration-from-oas.md +0 -103
  105. package/docs/oats-config.schema.json +0 -172
  106. package/docs/oats-lock-v3.schema.json +0 -7
  107. package/docs/oats-lock.schema.json +0 -175
  108. package/docs/operating-team-migration.md +0 -470
  109. package/docs/portable.schema.json +0 -2512
  110. package/docs/provider-check-input.schema.json +0 -7
  111. package/docs/rebuild-to-v2.md +0 -511
  112. package/docs/workspace-adoption.md +0 -74
  113. package/injects/framework-workspace.md +0 -7
  114. package/injects/local-soul.md +0 -19
  115. package/injects/oats-portable.md +0 -20
  116. package/injects/oats.md +0 -11
  117. package/injects/portable-instance-boundary.md +0 -39
  118. package/injects/portable-work-directory.md +0 -29
  119. package/lib/artifact-approvals.mjs +0 -120
  120. package/lib/artifact-tree.mjs +0 -141
  121. package/lib/capability-artifacts.mjs +0 -179
  122. package/lib/capability-execution.mjs +0 -15
  123. package/lib/capability-inputs.mjs +0 -39
  124. package/lib/capability-provenance.mjs +0 -231
  125. package/lib/captured-action-shape.mjs +0 -21
  126. package/lib/captured-admission-shape.mjs +0 -20
  127. package/lib/captured-binding-file.mjs +0 -36
  128. package/lib/captured-dispatch.mjs +0 -66
  129. package/lib/captured-instance-index.mjs +0 -277
  130. package/lib/captured-invocation-context.mjs +0 -130
  131. package/lib/captured-launch-request.mjs +0 -66
  132. package/lib/captured-operation-process.mjs +0 -15
  133. package/lib/captured-pi-custody.mjs +0 -29
  134. package/lib/captured-pi-host.mjs +0 -167
  135. package/lib/captured-pi-outcome.mjs +0 -172
  136. package/lib/captured-resolutions.mjs +0 -275
  137. package/lib/captured-scaffold.mjs +0 -87
  138. package/lib/captured-selector.mjs +0 -28
  139. package/lib/captured-session-backend.mjs +0 -52
  140. package/lib/captured-source-receipt-file.mjs +0 -72
  141. package/lib/helper-injection-policy.mjs +0 -104
  142. package/lib/legacy-lock-codec.mjs +0 -106
  143. package/lib/manifest-settings.mjs +0 -84
  144. package/lib/package-closure.mjs +0 -48
  145. package/lib/package-materialization.mjs +0 -83
  146. package/lib/pi-sdk-host.mjs +0 -229
  147. package/lib/portable-artifacts.mjs +0 -115
  148. package/lib/portable-choices.mjs +0 -82
  149. package/lib/portable-composition.mjs +0 -136
  150. package/lib/portable-digest.mjs +0 -105
  151. package/lib/portable-identity.mjs +0 -40
  152. package/lib/portable-lock.mjs +0 -117
  153. package/lib/portable-onboarding-request.mjs +0 -49
  154. package/lib/portable-onboarding.mjs +0 -256
  155. package/lib/portable-package-preparation.mjs +0 -188
  156. package/lib/portable-policy.mjs +0 -44
  157. package/lib/portable-soul.mjs +0 -42
  158. package/lib/portable-state.mjs +0 -80
  159. package/lib/prepare-composition.mjs +0 -170
  160. package/lib/prepared-bindings.mjs +0 -92
  161. package/lib/prepared-resources.mjs +0 -127
  162. package/lib/provider-binding-broker.mjs +0 -65
  163. package/lib/provider-binding-wire.mjs +0 -116
  164. package/lib/readiness.mjs +0 -225
  165. package/lib/repository-observation.mjs +0 -226
  166. package/lib/resolution-shape.mjs +0 -393
  167. package/lib/schedule-capsule.mjs +0 -206
  168. package/lib/soul-constraints.mjs +0 -40
  169. package/lib/source-projection.mjs +0 -84
  170. package/lib/source-spec.mjs +0 -189
  171. package/lib/workspace-definition.mjs +0 -126
  172. package/lib/workspace-discovery.mjs +0 -146
  173. package/skills/oats/SKILL.md +0 -162
  174. package/skills/oats-config/SKILL.md +0 -164
  175. package/skills/oats-packages/SKILL.md +0 -184
  176. package/skills/oats-portable/SKILL.md +0 -115
  177. package/skills/oats-portable-artifacts/SKILL.md +0 -63
@@ -4,7 +4,7 @@
4
4
  * A spawn is: read this machine's `oats-local.yaml` → discover the workspace over
5
5
  * its Git remotes in the operator's access context → find the soul among the
6
6
  * confirmed members (or external souls) → resolve every capability by `from:`
7
- * (member = latest state, package = the locked, approved version) → materialize
7
+ * (member = latest state, package = the locked version) → materialize
8
8
  * each capability WHOLE into the new home (`.oats/modules/`, `.agents/skills/`) →
9
9
  * compose AGENTS.md → launch the harness normally.
10
10
  *
@@ -16,14 +16,16 @@
16
16
  *
17
17
  * Nothing here reads `oats-config.yaml`, an installed-capability directory or a
18
18
  * per-soul `source:` — those do not exist in this model. */
19
- import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
19
+ import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync, readlinkSync, copyFileSync, chmodSync } from "node:fs";
20
20
  import { join, resolve as resolvePath, dirname, basename, relative, isAbsolute, sep } from "node:path";
21
21
  import { oatsError } from "./errors.mjs";
22
- import { loadLocal, discoverWorkspace, discoverRepo, standaloneRepo } from "./workspace.mjs";
23
- import { resolveSoul, packageRef } from "./resolve.mjs";
22
+ import { loadLocal, discoverWorkspace, discoverRepo, standaloneRepo, observeWorkspace, observeSoulLabels } from "./workspace.mjs";
23
+ import { resolveSoul, packageRef, memberRef, manifestDefaultsPayload, mergePayload, payloadOrigins, revisionOf, teamsOf, kernelCompatibility, RESOLUTION_API, SLOTS } from "./resolve.mjs";
24
+ import { declaredSettings, settingValueProblems } from "./capability-contract.mjs";
24
25
  import { materialize, MODULES_DIR, SKILLS_DIR } from "./materialize.mjs";
25
26
  import { fetchRemoteTree } from "./remote.mjs";
26
- import { mkdirSync, renameSync, rmSync, writeFileSync, symlinkSync, statSync } from "node:fs";
27
+ import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, symlinkSync, statSync } from "node:fs";
28
+ import { tmpdir } from "node:os";
27
29
  import { spawnSync } from "node:child_process";
28
30
  import { randomBytes } from "node:crypto";
29
31
  import { readLock, LOCK_FILE, readPackageManifests } from "./packages.mjs";
@@ -99,10 +101,39 @@ export function findSoulEntry(discovery, name) {
99
101
  return hits[0];
100
102
  }
101
103
 
104
+ /**
105
+ * A home's LIVE eligible teams (teams contract 2026-09-25, decision 6). Teams are messaging
106
+ * state, not frozen composition: the soul's labels are read from its repository NOW and each
107
+ * label's payload from the workspace's current `messaging`. The home's modules and skills are
108
+ * untouched. No workspace discovery: exactly two repository reads — the workspace host at its
109
+ * current commit (observeWorkspace) and the soul's own repo (observeSoulLabels).
110
+ * → { teams, source: "live" } — or, when that cannot answer (the host or the soul's repo is
111
+ * unreachable, the soul is no longer listed, a standalone deployment), the spawn-time record:
112
+ * { teams: <record, or null when none>, source: "recorded", reason }.
113
+ * `source` travels as OATS_TEAMS_SOURCE: a provider leaves a joined team only on a `live` answer.
114
+ */
115
+ export async function liveTeams(home, meta, { remoteOptions, remote } = {}) {
116
+ const recorded = (reason, error) => ({ teams: Array.isArray(meta?.teams) ? meta.teams : null, source: "recorded", reason, ...(error ? { error } : {}) });
117
+ const soul = meta?.workspace?.soul;
118
+ // A capability agent's soul is its providing module's: no team labels, nothing to re-read.
119
+ if (!soul || typeof soul.repoKey !== "string" || typeof meta.agent !== "string") return recorded("no-workspace-soul");
120
+ const deployment = dirname(dirname(dirname(dirname(resolvePath(home)))));
121
+ try {
122
+ const found = loadLocal(deployment);
123
+ if (!found.path || resolvePath(dirname(found.path)) !== resolvePath(deployment)) return recorded("no-deployment");
124
+ if (typeof found.local.standalone === "string" && found.local.standalone) return recorded("standalone");
125
+ const wsObs = await observeWorkspace(found.local.workspace, { remoteOptions, remote });
126
+ const labels = await observeSoulLabels(wsObs, { name: meta.agent, repoKey: soul.repoKey }, { remoteOptions, remote });
127
+ if (labels === null) return recorded("soul-not-listed");
128
+ return { teams: teamsOf(wsObs.workspace, labels), source: "live" };
129
+ } catch (e) { return recorded("unreadable", { code: e?.code || "E_REMOTE_UNREADABLE", message: e?.message ?? String(e) }); }
130
+ }
131
+
102
132
  /** The per-commit soul cache under an agent directory: `<agentDir>/souls/<commit12>/`.
103
133
  * Each entry is IMMUTABLE once written (fetched to staging, renamed in) and is never
104
- * removed by the kernel — a running instance's `<home>/soul` links straight at its
105
- * own commit's directory (realpath), so nothing can change under it. */
134
+ * removed by the kernel — a running instance records its own commit's directory
135
+ * (realpath, instance.json `soulDir`, handed to hooks as OATS_SOUL), so nothing can
136
+ * change under it. */
106
137
  export const SOULS_DIR = "souls";
107
138
  export const SOUL_SOURCE_STAMP = ".oats-soul-source.json";
108
139
  const commit12 = (c) => String(c || "").slice(0, 12) || "unknown";
@@ -128,13 +159,40 @@ function swapSoulPointer(soulDir, target) {
128
159
  } catch (x) { try { rmSync(tmp, { force: true }); } catch { /* nothing */ } throw x; }
129
160
  }
130
161
 
162
+ /** Fetch a prepared soul's source tree (soul.yaml + AGENTS.md, CLAUDE.md → AGENTS.md) into `dest`. */
163
+ async function fetchSoulSource(prepared, dest) {
164
+ const e = prepared.soulEntry;
165
+ const ref = e.repoKey.startsWith("local/") ? e.repoKey.slice("local/".length) : `git:${e.repoKey}`;
166
+ // A soul's CLAUDE.md → AGENTS.md alias is the one symlink a soul source may carry.
167
+ await fetchRemoteTree(ref, e.commit, e.path, dest, { ...(prepared.remoteOptions || {}), allowSymlinks: (p) => p === "CLAUDE.md" });
168
+ if (!existsSync(join(dest, "soul.yaml")) || !existsSync(join(dest, "AGENTS.md"))) throw err("E_SOUL_INCOMPLETE", `soul ${e.name} at ${e.repoKey}@${String(e.commit).slice(0, 12)} lacks soul.yaml or AGENTS.md`, { repoKey: e.repoKey, commit: e.commit, path: e.path });
169
+ if (!existsSync(join(dest, "CLAUDE.md"))) symlinkSync("AGENTS.md", join(dest, "CLAUDE.md"));
170
+ }
171
+
172
+ /** A preview's soul source. A preview writes NOTHING in the deployment (K6b), so it
173
+ * never populates the per-commit cache or moves the `soul` pointer: it reads the
174
+ * cache entry for the prepared commit when one is already complete, else fetches
175
+ * the source into a temporary directory outside the deployment. `cleanup` removes
176
+ * that temporary copy; `fetched` says whether the preview had to fetch. */
177
+ export async function previewWorkspaceSoul(prepared, agentsRoot) {
178
+ const e = prepared.soulEntry;
179
+ const commitDir = join(agentsRoot, e.name, SOULS_DIR, commit12(e.commit));
180
+ if (existsSync(join(commitDir, "soul.yaml")) && existsSync(join(commitDir, "AGENTS.md"))) return { soulDir: realpathSync(commitDir), fetched: false, cleanup: () => {} };
181
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), "oats-preview-soul-")));
182
+ const cleanup = () => rmSync(tmp, { recursive: true, force: true });
183
+ try { await fetchSoulSource(prepared, join(tmp, "soul")); }
184
+ catch (x) { cleanup(); throw x; }
185
+ return { soulDir: join(tmp, "soul"), fetched: true, cleanup };
186
+ }
187
+
131
188
  /** Materialize a workspace soul's SOURCE (soul.yaml, AGENTS.md, skills/…) into the
132
189
  * per-commit cache `<agentsRoot>/<name>/souls/<commit12>/` and point the classic
133
190
  * `<agentsRoot>/<name>/soul` at it (a SYMLINK, swapped atomically), so the classic
134
191
  * spawn skeleton, findAgent, doctor … keep reading "current" from the usual place
135
192
  * while every spawned home links its OWN commit's directory (decision 7: an
136
- * instance never changes under itself — a preview or a later spawn may fetch a new
137
- * commit and move the pointer, but no directory an instance links is ever touched).
193
+ * instance never changes under itself — a later spawn may fetch a new commit and
194
+ * move the pointer, but no directory an instance links is ever touched; a preview
195
+ * never calls this, see previewWorkspaceSoul).
138
196
  *
139
197
  * Idempotent per (repo, commit): an existing complete entry is reused (never
140
198
  * refetched, never rewritten); an entry that lost its soul.yaml is moved aside and
@@ -173,13 +231,9 @@ export async function ensureWorkspaceSoul(prepared, agentsRoot) {
173
231
  renameSync(commitDir, `${commitDir}.damaged-${process.pid}-${randomBytes(4).toString("hex")}`);
174
232
  }
175
233
  if (!complete) {
176
- const ref = e.repoKey.startsWith("local/") ? e.repoKey.slice("local/".length) : `git:${e.repoKey}`;
177
234
  const staging = join(soulsDir, `.soul-staging-${process.pid}-${randomBytes(4).toString("hex")}`);
178
235
  try {
179
- // A soul's CLAUDE.md → AGENTS.md alias is the one symlink a soul source may carry.
180
- await fetchRemoteTree(ref, e.commit, e.path, staging, { ...(prepared.remoteOptions || {}), allowSymlinks: (p) => p === "CLAUDE.md" });
181
- if (!existsSync(join(staging, "soul.yaml")) || !existsSync(join(staging, "AGENTS.md"))) throw err("E_SOUL_INCOMPLETE", `soul ${e.name} at ${e.repoKey}@${String(e.commit).slice(0, 12)} lacks soul.yaml or AGENTS.md`, { repoKey: e.repoKey, commit: e.commit, path: e.path });
182
- if (!existsSync(join(staging, "CLAUDE.md"))) symlinkSync("AGENTS.md", join(staging, "CLAUDE.md"));
236
+ await fetchSoulSource(prepared, staging);
183
237
  try { renameSync(staging, commitDir); }
184
238
  catch (x) {
185
239
  // Lost a race with a concurrent fetch of the same commit: theirs is the same bytes.
@@ -397,7 +451,114 @@ export async function discoverOrStandalone(local, { remoteOptions, remote } = {}
397
451
  /** Materialize a prepared resolution into `home`. Called by spawnInstance after
398
452
  * the home directory exists and before the harness is launched. */
399
453
  export async function materializePrepared(prepared, home) {
400
- return materialize(prepared.resolution, home, { lock: prepared.lock, remoteOptions: prepared.remoteOptions, soulAgentsMd: prepared.soulAgentsMd, soulDir: prepared.soulDir });
454
+ const localModules = prepared.localModules || null;
455
+ // A capability agent's module copied from its anchor's verified copy (prepareCapabilityAgent):
456
+ // materialize still checks the copy's digest against the one the anchor recorded.
457
+ const fetch = localModules ? async (ref, commit, dir, dest, options) => {
458
+ const src = localModules[basename(dest)];
459
+ if (!src) return defaultRemote.fetchRemoteTree(ref, commit, dir, dest, options);
460
+ copyLocalModule(src, dest);
461
+ return { digest: defaultRemote.contentDigest(dest, { allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK }) };
462
+ } : undefined;
463
+ return materialize(prepared.resolution, home, { lock: prepared.lock, remoteOptions: prepared.remoteOptions, soulAgentsMd: prepared.soulAgentsMd, soulDir: prepared.soulDir, ...(fetch ? { fetch } : {}) });
464
+ }
465
+ /** Copy a materialized module tree (regular files, directories, the CLAUDE.md alias) — nothing else. */
466
+ function copyLocalModule(src, dest, rel = "") {
467
+ mkdirSync(dest, { recursive: true });
468
+ for (const name of readdirSync(src).sort()) {
469
+ const from = join(src, name), to = join(dest, name), at = rel ? `${rel}/${name}` : name, st = lstatSync(from);
470
+ if (st.isSymbolicLink()) {
471
+ if (!defaultRemote.OATS_ALIAS_SYMLINK(at) || readlinkSync(from) !== "AGENTS.md") throw err("E_REMOTE_TREE_UNSAFE", `${from} is a symlink`, { path: from, why: "symlink" });
472
+ symlinkSync("AGENTS.md", to);
473
+ } else if (st.isDirectory()) copyLocalModule(from, to, at);
474
+ else if (st.isFile()) { copyFileSync(from, to); chmodSync(to, (st.mode & 0o111) ? 0o755 : 0o644); }
475
+ else throw err("E_REMOTE_TREE_UNSAFE", `${from} is not a regular file`, { path: from, why: "device" });
476
+ }
477
+ }
478
+
479
+ /**
480
+ * The prepared resolution of a CAPABILITY AGENT (a capability's `agents:` soul, e.g.
481
+ * OKF's memory-harvest worker) — lead decision c3 Q1. It composes its providing
482
+ * capability's module and NOTHING else: no workspace defaults, no knowledge,
483
+ * messaging or tasks module, so no provider hook of any slot runs for it, and the
484
+ * providing module's own hooks do not run either (a manifest has no way to declare
485
+ * them for an agent). A knowledge-layer provider's inject is left out: a service
486
+ * agent carries no memory protocol.
487
+ *
488
+ * The module comes from `agent._manifestSource` — the instance home whose module
489
+ * copy declared the agent (the --parent/--relative-to anchor first): its recorded
490
+ * `modules[<cap>]` (copied from that home's verified copy, pinned to the recorded
491
+ * digest) and its recorded `providers[<cap>]` payload. Without such a home (the
492
+ * source instance is gone), `pkg` — the deployment lock's package entry that
493
+ * declares the agent (resolvePackageCapabilityAgent) — supplies the module, and the
494
+ * payload is the manifest defaults ⊕ the workspace's messaging payload (for a
495
+ * messaging-layer provider) ⊕ oats-local.yaml `settings.<cap>`.
496
+ */
497
+ export async function prepareCapabilityAgent(contextDir, agent, { discovery, pkg = null, remoteOptions, catalog = null, remote = defaultRemote } = {}) {
498
+ const found = loadLocal(contextDir);
499
+ const local = found.local;
500
+ const deployment = dirname(found.path);
501
+ const lock = existsSync(join(deployment, LOCK_FILE)) ? readLock(deployment) : null;
502
+ const view = discovery ?? await discoverOrStandalone(local, { remoteOptions });
503
+ const cap = agent.capability;
504
+ let module, payload, origins, localModules = null;
505
+ const anchor = pkg ? null : agent._manifestSource;
506
+ if (anchor) {
507
+ let meta;
508
+ try { meta = JSON.parse(readFileSync(join(anchor, "instance.json"), "utf8")); } catch { meta = null; }
509
+ const recorded = meta?.modules?.[cap];
510
+ const moduleDir = join(anchor, MODULES_DIR, cap);
511
+ if (!recorded?.from || typeof recorded.digest !== "string" || !existsSync(join(moduleDir, "oats.json"))) {
512
+ throw err("E_CAPABILITY_BROKEN", `capability agent ${agent.name}: ${anchor} does not record a verified copy of ${cap} (instance.json modules.${cap} with a digest, and ${join(MODULES_DIR, cap)}); respawn that instance`, { capability: cap, anchor });
513
+ }
514
+ const manifest = JSON.parse(readFileSync(join(moduleDir, "oats.json"), "utf8"));
515
+ module = { name: cap, from: { ...recorded.from }, manifest, layer: SLOTS.includes(manifest.layer) ? manifest.layer : null, private: manifest.private === true, digest: recorded.digest };
516
+ payload = meta.providers?.[cap] && typeof meta.providers[cap] === "object" ? JSON.parse(JSON.stringify(meta.providers[cap])) : {};
517
+ origins = Object.fromEntries(Object.keys(payload).map((k) => [`/${k.replace(/~/g, "~0").replace(/\//g, "~1")}`, { kind: "anchor", at: `${join(anchor, "instance.json")}#/providers/${cap}` }]));
518
+ localModules = { [cap]: moduleDir };
519
+ } else if (pkg) {
520
+ const manifest = pkg.manifest;
521
+ if (pkg.kind === "member") {
522
+ module = { name: cap, from: { kind: "member", repoKey: pkg.repoKey, commit: pkg.commit }, manifest, layer: SLOTS.includes(manifest.layer) ? manifest.layer : null, private: false, dir: pkg.capDir };
523
+ } else {
524
+ const entry = lock?.packages?.[pkg.package];
525
+ if (!entry) throw err("E_PACKAGE_MISSING", `capability agent ${agent.name}: package ${pkg.package} is not in ${LOCK_FILE}`, { capability: cap, package: pkg.package });
526
+ const ref = packageRef(pkg.package, entry, catalog, remote);
527
+ module = {
528
+ name: cap, from: { kind: "package", package: pkg.package, version: entry.version, commit: entry.commit, integrity: entry.integrity, repoKey: remote.parseRepoRef(ref).key },
529
+ manifest, layer: SLOTS.includes(manifest.layer) ? manifest.layer : null, private: manifest.private === true, dir: pkg.capDir,
530
+ };
531
+ }
532
+ const layers = [{ payload: manifestDefaultsPayload(manifest), origin: { kind: "manifest-default", at: "oats.json#/settings" } }];
533
+ if (module.layer === "messaging" && view?.workspace?.messaging && typeof view.workspace.messaging === "object") {
534
+ const { byTeam, ...base } = view.workspace.messaging;
535
+ layers.push({ payload: base, origin: { kind: "workspace", at: "oats-workspace.yaml#/messaging" } });
536
+ }
537
+ const host = local.settings?.[cap];
538
+ if (host && typeof host === "object") layers.push({ payload: host, origin: { kind: "host", at: `oats-local.yaml#/settings/${cap}` } });
539
+ payload = mergePayload(...layers.map((l) => l.payload));
540
+ origins = payloadOrigins(layers);
541
+ } else {
542
+ throw new TypeError("prepareCapabilityAgent: the agent names no source home, and no package entry was given");
543
+ }
544
+ // The capability's own kernel range, as resolveSoul checks every soul's modules.
545
+ const compat = kernelCompatibility({ ...module.manifest, capability: cap });
546
+ if (!compat.ok) throw err("E_CAPABILITY_INCOMPATIBLE", `capability agent ${agent.name}: ${cap} requires oats ${compat.range}; this kernel is ${compat.kernel}`, { capability: cap, range: compat.range, kernel: compat.kernel, from: module.from });
547
+ for (const { key, value, values } of settingValueProblems(module.manifest, payload)) {
548
+ throw err("E_WORKSPACE_SCHEMA", `${cap}: setting ${JSON.stringify(key)} is ${JSON.stringify(value)}, not one of ${values.map((v) => JSON.stringify(v)).join(", ")}`, { capability: cap, key, value, values, reason: "setting-value" });
549
+ }
550
+ const slots = { knowledge: null, messaging: null, tasks: null };
551
+ if (module.layer) slots[module.layer] = cap;
552
+ if (module.layer === "knowledge") module.inject = false;
553
+ const injects = module.inject !== false && typeof module.manifest.inject === "string" && module.manifest.inject ? [{ module: cap, path: module.manifest.inject }] : [];
554
+ const repoKey = module.from.repoKey ?? `package:${module.from.package}`;
555
+ const soulEntry = { name: agent.name, repoKey, commit: module.from.commit, team: null, path: null, capability: cap };
556
+ const soul = { name: agent.name, repoKey, commit: module.from.commit, team: null, path: null };
557
+ const decl = { resolutionApi: RESOLUTION_API, soul, modules: [module], slots, skills: [], injects };
558
+ const payloads = { [cap]: payload };
559
+ const declRevision = revisionOf(decl), payloadRevision = revisionOf(payloads);
560
+ const resolution = { ...decl, payloads, payloadOrigins: { [cap]: origins }, teams: [], declRevision, payloadRevision, revision: revisionOf({ declRevision, payloadRevision }) };
561
+ return { local, deployment, lock: anchor ? null : lock, discovery: view, soulEntry, resolution, remoteOptions, spawn: { providers: {} }, capabilityAgent: true, ...(localModules ? { localModules } : {}) };
401
562
  }
402
563
 
403
564
  /** Turn a Resolution's modules (already materialized under `home`) into the
@@ -417,14 +578,14 @@ export function toCapabilityRows(resolution, home) {
417
578
  level: home, origin: m.from.kind === "package" ? `package:${m.from.package}@${m.from.version}` : `member:${m.from.repoKey}@${m.from.commit}`,
418
579
  provenance: [m.from.kind === "package" ? `package ${m.from.package} v${m.from.version}` : `member ${m.from.repoKey} @ ${String(m.from.commit).slice(0, 12)}`],
419
580
  settings: { ...(resolution.payloads?.[m.name] && typeof resolution.payloads[m.name] === "object" ? resolution.payloads[m.name] : {}) },
420
- skills, inject: inject && existsSync(inject) ? inject : undefined,
581
+ skills, inject: m.inject !== false && inject && existsSync(inject) ? inject : undefined,
421
582
  skillsDeclared: manifest.skills || [], injectDeclared: manifest.inject,
422
583
  // Member capabilities are trusted by membership (decision 2); package
423
- // capabilities were approved per version at sync (E_PACKAGE_UNAPPROVED
424
- // otherwise, so reaching here means approved).
584
+ // capabilities by their declaration in the workspace's packages: (human
585
+ // decision 2026-09-24: declaring a package is the trust decision).
425
586
  hooks: hookCommandsOf(manifest, dir), requiredHooks: requiredHooksOf(manifest),
426
587
  environment: [...(manifest.environment || [])], environmentNamespaces: [...(manifest.environmentNamespaces || [])],
427
- missingRequires: [], compatibility: { ok: true }, trust: { trusted: true, reason: m.from.kind === "package" ? "approved package version" : "workspace member" },
588
+ missingRequires: [], compatibility: kernelCompatibility({ ...manifest, capability: m.name }), trust: { trusted: true, reason: m.from.kind === "package" ? "declared workspace package" : "workspace member" },
428
589
  executable: !!manifest.commands && Object.keys(manifest.commands).length > 0,
429
590
  retirement: manifest.retirement,
430
591
  dir, from: m.from, _scope: 0,
@@ -466,7 +627,7 @@ export function modulesPreview(resolution, agentsRoot, soulName) {
466
627
  return resolution.modules.map((m) => {
467
628
  const prev = previous?.modules?.[m.name];
468
629
  const changedSince = !previous ? null : !prev ? { instance: previous.name, was: null } : (prev.commit !== m.from.commit ? { instance: previous.name, was: prev.commit } : false);
469
- return { name: m.name, from: m.from, layer: m.manifest.layer ?? null, private: !!m.private, changedSince };
630
+ return { name: m.name, from: m.from, layer: m.manifest.layer ?? null, private: !!m.private, declares: declaredSettings(m.manifest), changedSince };
470
631
  });
471
632
  }
472
633
 
@@ -479,23 +640,48 @@ export const INSTANCE_MODULES_DIR = MODULES_DIR;
479
640
  /**
480
641
  * Workspace model: a capability-defined agent (a package capability's `agents:`
481
642
  * soul — OKF's memory-harvest worker, say) resolves from the deployment's LOCK,
482
- * not from any instance: every approved package's capability manifests are read
643
+ * not from any instance: every locked package's capability manifests are read
483
644
  * over the remote; the first `agents/<name>` match has its capability tree fetched
484
645
  * into the deployment's module store `<deployment>/.oats/modules/<cap>@<commit>/`
485
646
  * (a per-commit cache shared by every such spawn) so the classic capability-agent
486
647
  * machinery can read it exactly as it reads a materialized instance module.
487
648
  * → { capability, dir, commit, package, version, manifest, rel } | undefined.
488
- * Unapproved packages are skipped (they could not have materialized anywhere).
489
649
  */
490
- export async function resolvePackageCapabilityAgent(contextDir, name, { remoteOptions, catalog = null } = {}) {
650
+ export async function resolvePackageCapabilityAgent(contextDir, name, { remoteOptions, catalog = null, discovery = undefined } = {}) {
491
651
  if (typeof name !== "string" || !name) return undefined;
492
652
  const found = loadLocal(contextDir);
493
653
  const deployment = found.path ? dirname(found.path) : resolvePath(contextDir);
654
+ const remote = defaultRemote;
655
+ const view = discovery === undefined ? await discoverOrStandalone(found.local, { remoteOptions }) : discovery;
656
+ // A confirmed member's own (non-private) capability first: membership is its trust
657
+ // decision, exactly as for a soul's `from: <member>` module.
658
+ for (const row of view?.members || []) {
659
+ if (!row?.confirmed && view?.standalone !== true) continue;
660
+ for (const cap of row.capabilities || []) {
661
+ if (cap.private) continue;
662
+ const agents = Array.isArray(cap.manifest?.agents) ? cap.manifest.agents : [];
663
+ const rel = agents.find((a) => typeof a === "string" && basename(a) === name);
664
+ if (!rel) continue;
665
+ const commit = cap.commit ?? row.commit;
666
+ const store = join(deployment, MODULES_DIR);
667
+ const dir = join(store, `${cap.name}@${String(commit).slice(0, 12)}`);
668
+ if (!existsSync(join(dir, "oats.json"))) {
669
+ mkdirSync(store, { recursive: true });
670
+ await fetchRemoteTree(memberRef(view, remote, cap.repoKey), commit, cap.path, dir, { ...(remoteOptions || {}), allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK });
671
+ }
672
+ return { kind: "member", capability: cap.name, dir, capDir: cap.path, commit, repoKey: cap.repoKey, manifest: cap.manifest, rel };
673
+ }
674
+ }
494
675
  if (!existsSync(join(deployment, LOCK_FILE))) return undefined;
495
676
  const lock = readLock(deployment);
496
- const remote = defaultRemote;
677
+ // Declaring a package in packages: is the trust decision: a workspace view
678
+ // admits only locked packages the workspace still declares (a stale lock entry
679
+ // is never a capability-agent source). A standalone view's lock holds only what
680
+ // a standalone sync wrote.
681
+ const declared = view?.standalone === true ? null : (view?.workspace?.packages && typeof view.workspace.packages === "object" ? view.workspace.packages : {});
497
682
  for (const [id, entry] of Object.entries(lock.packages || {})) {
498
- if (!entry || typeof entry !== "object" || !entry.approved || typeof entry.approved.executables !== "string") continue;
683
+ if (!entry || typeof entry !== "object") continue;
684
+ if (declared && !Object.hasOwn(declared, id)) continue;
499
685
  let ref;
500
686
  try { ref = packageRef(id, entry, catalog, remote); } catch { continue; }
501
687
  let read;
@@ -510,7 +696,7 @@ export async function resolvePackageCapabilityAgent(contextDir, name, { remoteOp
510
696
  mkdirSync(store, { recursive: true });
511
697
  await fetchRemoteTree(ref, entry.commit, cap.dir, dir, { ...(remoteOptions || {}), allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK });
512
698
  }
513
- return { capability: cap.name, dir, commit: entry.commit, package: id, version: entry.version, manifest: cap.manifest, rel };
699
+ return { kind: "package", capability: cap.name, dir, capDir: cap.dir, commit: entry.commit, package: id, version: entry.version, manifest: cap.manifest, rel };
514
700
  }
515
701
  }
516
702
  return undefined;
@@ -1,7 +1,5 @@
1
1
  /** Formatting shared by current preparation and retained instruction projection.
2
2
  * Selection/order is supplied by the caller; this module never discovers config. */
3
- import { readPortableBytes } from "./portable-files.mjs";
4
- import { oatsError } from "./errors.mjs";
5
3
 
6
4
  export function renderInstructionText(body, blocks) {
7
5
  let text = body.replace(/\n*$/, "\n");
@@ -11,21 +9,3 @@ export function renderInstructionText(body, blocks) {
11
9
  return text;
12
10
  }
13
11
 
14
- /** Input is the result of exact record verification, not a current config or
15
- * a mutable soul directory. All paths come from its verified resource inventory. */
16
- export function composeCapturedInstructions(verified) {
17
- const composition = verified.record.dispatch.composition;
18
- if (!composition) throw oatsError("resolution-incomplete", "captured instruction composition is absent");
19
- const read = (key) => {
20
- const file = verified.resources.get(key);
21
- if (!file) throw oatsError("resolution-incomplete", "captured instruction resource is absent");
22
- let content;
23
- try { content = new TextDecoder("utf-8", { fatal: true }).decode(readPortableBytes(file)); }
24
- catch (cause) { throw oatsError("invalid-resolution", "captured instructions are not readable UTF-8", { cause }); }
25
- return { file, content };
26
- };
27
- const body = read(composition.body);
28
- const blocks = composition.blocks.map(({ source, resource }) => ({ source, ...read(resource) }));
29
- const skills = composition.skills.map(({ name, resource }) => ({ name, path: verified.resources.get(resource) }));
30
- return { text: renderInstructionText(body.content, blocks), blocks, skills, omissions: composition.omissions };
31
- }
@@ -1,7 +1,7 @@
1
1
  /** lib/materialize.mjs — copy whole, compose, record (workspace model v2, module contract §5).
2
2
  *
3
3
  * Supersedes the module/skill assembly in core.mjs spawn (`prepared` copies, symlinked
4
- * skill sets, ambient exclusion) and capability-artifacts.mjs.
4
+ * skill sets, ambient exclusion) and the captured path's artifact store.
5
5
  *
6
6
  * materialize(resolution, home, options)
7
7
  * For every module of the resolution: fetch its capability directory whole into
@@ -240,8 +240,11 @@ function moduleSkills(resolution, module, moduleDir) {
240
240
  return rows;
241
241
  }
242
242
 
243
- /** The inject files a module contributes → [relative path]. resolution.injects rows first, else manifest.inject. */
243
+ /** The inject files a module contributes → [relative path]. resolution.injects rows first, else manifest.inject;
244
+ * none for a module row marked `inject: false` (a capability agent's knowledge-layer provider carries no
245
+ * memory protocol: lib/instance-resolution.mjs#prepareCapabilityAgent). */
244
246
  function moduleInjects(resolution, module, moduleDir) {
247
+ if (module.inject === false) return [];
245
248
  const declared = (resolution.injects || []).filter((i) => i && i.module === module.name);
246
249
  const rels = declared.length
247
250
  ? declared.map((i) => modulePath(i.path, module.name, "inject"))
@@ -280,7 +283,6 @@ function soulBody(home, options) {
280
283
  if (typeof options.soulAgentsMd === "string") return { text: options.soulAgentsMd, file: null };
281
284
  const candidates = [];
282
285
  if (typeof options.soulDir === "string") candidates.push(join(resolve(options.soulDir), "AGENTS.md"));
283
- candidates.push(join(home, "soul", "AGENTS.md"));
284
286
  for (const file of candidates) {
285
287
  try { if (statSync(file).isFile()) return { text: readFileSync(file, "utf8"), file }; } catch {}
286
288
  }
@@ -309,7 +311,7 @@ function ensureOatsDir(home) {
309
311
  * remoteOptions threaded into fetch (cacheDir, exec, …)
310
312
  * lock oats-lock.json v3 object: package repo/path/commit for from.kind === "package"
311
313
  * catalog { <id>: { url, ref, path } } for lock entries whose source is "catalog:<id>"
312
- * soulAgentsMd the soul's canonical AGENTS.md text (else read from options.soulDir or <home>/soul/AGENTS.md)
314
+ * soulAgentsMd the soul's canonical AGENTS.md text (else read from options.soulDir)
313
315
  * blocks pre-rendered blocks [{ source, file, content }] placed BEFORE the capability injects
314
316
  * (the kernel's own kernel:… and work-mode:… blocks, composed by core.mjs)
315
317
  * now() ISO timestamp source (tests)
@@ -12,13 +12,13 @@
12
12
  *
13
13
  * Never "the newest instance's copy" (an instance is not an authority for the
14
14
  * deployment) and never an unlocked cache read: trust is exactly spawn's —
15
- * members by membership, packages by the lock's approval (`prepareInstance`
16
- * already refuses E_PACKAGE_UNAPPROVED). A module in the Resolution IS active.
15
+ * members by membership, packages by their declaration in the workspace's
16
+ * packages:. A module in the Resolution IS active.
17
17
  *
18
18
  * `--soul` is required: without a soul there is no Resolution to answer from
19
19
  * (E_BAD_ARGS naming --soul). Nothing here reads oats-config.yaml. */
20
- import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
21
- import { dirname, join, resolve as resolvePath } from "node:path";
20
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
21
+ import { basename, dirname, join, resolve as resolvePath } from "node:path";
22
22
  import { randomBytes } from "node:crypto";
23
23
  import { oatsError } from "./errors.mjs";
24
24
  import { loadLocal } from "./workspace.mjs";
@@ -58,25 +58,45 @@ export function moduleStoreDir(deployment, module) {
58
58
  }
59
59
 
60
60
  /** Ensure the module's capability tree is in the deployment store; returns its dir.
61
- * A tree already there (oats.json present) is reused — the commit is in the name,
62
- * so it cannot be stale. The fetch lands in a sibling staging dir and is renamed
63
- * into place, so a failed or interrupted fetch never leaves a half tree that a
64
- * later call would trust. */
61
+ * Nothing that differs from the locked commit is ever handed out to run:
62
+ * - a fetch's content digest must match what the fetch read at the commit (and
63
+ * the resolution's `module.digest` when it pins one), else E_PACKAGE_INTEGRITY;
64
+ * the verified digest is recorded beside the tree (`.<cap>@<commit12>.digest`,
65
+ * in the same operator-owned deployment as oats-lock.json);
66
+ * - a tree already there is reused only while its content still digests to that
67
+ * record (the commit in the name does not stop a local edit); a drifted tree,
68
+ * or one with no record, is removed and fetched again.
69
+ * The fetch lands in a sibling staging dir and is renamed into place, so a failed
70
+ * or interrupted fetch never leaves a half tree that a later call would trust. */
65
71
  export async function ensureModuleTree(deployment, module, lock, { catalog = null, remote = defaultRemote, remoteOptions, fetch = defaultRemote.fetchRemoteTree } = {}) {
66
72
  const store = join(deployment, MODULES_DIR);
67
73
  const dir = moduleStoreDir(deployment, module);
68
- if (existsSync(join(dir, "oats.json"))) return dir;
74
+ const record = join(store, `.${basename(dir)}.digest`);
75
+ const digestOf = (tree) => { try { return (remote.contentDigest ?? defaultRemote.contentDigest)(tree, { allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK }); } catch { return null; } };
76
+ const expected = () => { if (module.digest) return module.digest; try { return readFileSync(record, "utf8").trim() || null; } catch { return null; } };
77
+ const verified = (tree) => { const want = expected(); return !!want && digestOf(tree) === want; };
78
+ if (existsSync(join(dir, "oats.json"))) {
79
+ if (verified(dir)) return dir;
80
+ rmSync(dir, { recursive: true, force: true }); // drifted or unrecorded: re-materialize, never run it
81
+ }
69
82
  const ref = moduleRef(module, lock, { catalog, remote });
70
83
  if (typeof module.dir !== "string" || !module.dir) throw err("E_CAPABILITY_BROKEN", `module ${module.name}: resolution records no capability directory`, { module: module.name });
71
84
  mkdirSync(store, { recursive: true });
72
85
  const staging = join(store, `.staging-${module.name}-${process.pid}-${randomBytes(4).toString("hex")}`);
73
86
  try {
74
- await fetch(ref, module.from.commit, module.dir, staging, { ...(remoteOptions || {}), allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK });
87
+ const fetched = await fetch(ref, module.from.commit, module.dir, staging, { ...(remoteOptions || {}), allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK });
75
88
  if (!existsSync(join(staging, "oats.json"))) throw err("E_CAPABILITY_BROKEN", `module ${module.name}: ${module.dir} at ${String(module.from.commit).slice(0, 12)} has no oats.json`, { module: module.name, dir: module.dir, commit: module.from.commit });
76
- // A concurrent call may have won; its tree is the same commit — keep it.
77
- if (existsSync(join(dir, "oats.json"))) { rmSync(staging, { recursive: true, force: true }); return dir; }
89
+ const digest = digestOf(staging);
90
+ const reported = fetched && typeof fetched.digest === "string" ? fetched.digest : null;
91
+ if (!digest || (reported && reported !== digest) || (module.digest && module.digest !== digest)) {
92
+ throw err("E_PACKAGE_INTEGRITY", `module ${module.name}: ${module.dir} at ${String(module.from.commit).slice(0, 12)} digests ${digest ?? "unreadably"}, not ${module.digest ?? reported}`,
93
+ { module: module.name, commit: module.from.commit, expected: module.digest ?? reported, actual: digest, why: "module-store" });
94
+ }
95
+ // A concurrent call may have won; its tree is the same commit — keep it if it matches.
96
+ if (existsSync(join(dir, "oats.json")) && digestOf(dir) === digest) { rmSync(staging, { recursive: true, force: true }); writeFileSync(record, `${digest}\n`); return dir; }
78
97
  if (existsSync(dir)) rmSync(dir, { recursive: true, force: true }); // a half tree from an interrupted direct write
79
98
  renameSync(staging, dir);
99
+ writeFileSync(record, `${digest}\n`);
80
100
  return dir;
81
101
  } catch (x) { try { rmSync(staging, { recursive: true, force: true }); } catch { /* nothing */ } throw x; }
82
102
  }
@@ -93,7 +113,7 @@ export async function ensureModuleTree(deployment, module, lock, { catalog = nul
93
113
  * ensureTree(): Promise<dir> } — or null when no module of the soul's
94
114
  * resolution claims the namespace (the caller answers E_UNKNOWN_COMMAND).
95
115
  * Two modules claiming one namespace → E_DUPLICATE_NAMESPACE. Everything
96
- * prepareInstance can throw (E_SOUL_UNKNOWN, E_PACKAGE_UNAPPROVED,
116
+ * prepareInstance can throw (E_SOUL_UNKNOWN,
97
117
  * E_REMOTE_UNREADABLE, …) propagates untouched.
98
118
  */
99
119
  export async function resolveOperatorDispatch(contextDir, namespace, soulName, { remoteOptions, remote, catalog = null, fetch, prepare = prepareInstance } = {}) {