@awebai/oats 0.23.2 → 0.24.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 (158) hide show
  1. package/README.md +7 -4
  2. package/bin/oats-pi-sdk-host.mjs +17 -0
  3. package/bin/oats.mjs +442 -51
  4. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +14 -0
  5. package/capabilities/oats-okf/bin/oats-okf.mjs +79 -11
  6. package/capabilities/oats-okf/lib/binding-wire.mjs +268 -0
  7. package/capabilities/oats-okf/lib/captured-worker.mjs +101 -0
  8. package/capabilities/oats-okf/lib/config.mjs +2 -1
  9. package/capabilities/oats-okf/lib/inspection.mjs +16 -1
  10. package/capabilities/oats-okf/lib/invocation-context.mjs +111 -0
  11. package/capabilities/oats-okf/lib/invocation-shape.mjs +135 -0
  12. package/capabilities/oats-okf/lib/io.mjs +1 -1
  13. package/capabilities/oats-okf/lib/portable-binding.mjs +199 -0
  14. package/capabilities/oats-okf/lib/source-contract.mjs +46 -0
  15. package/capabilities/oats-okf/lib/sources.mjs +123 -3
  16. package/capabilities/oats-okf/lib/stores.mjs +104 -25
  17. package/capabilities/oats-okf/lib/worker.mjs +69 -10
  18. package/capabilities/oats-okf/oats.json +35 -7
  19. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +87 -0
  20. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +113 -0
  21. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +23 -5
  22. package/capabilities/oats-okf/skills/okf/SKILL.md +42 -1
  23. package/docs/artifact-approvals.schema.json +7 -0
  24. package/docs/capability-manifest.schema.json +37 -66
  25. package/docs/captured-invocation-context.schema.json +7 -0
  26. package/docs/captured-resolution.schema.json +7 -0
  27. package/docs/design/2026-09-14-artifact-retention-contract.md +190 -0
  28. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +708 -0
  29. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +85 -0
  30. package/docs/design/2026-09-14-portable-souls-explainer.md +750 -0
  31. package/docs/design/2026-09-15-captured-dispatch.md +127 -0
  32. package/docs/design/2026-09-15-captured-resolution-records.md +143 -0
  33. package/docs/design/2026-09-15-package-preparation.md +100 -0
  34. package/docs/design/2026-09-15-portable-data-contract.md +121 -0
  35. package/docs/design/2026-09-15-portable-declarations.md +189 -0
  36. package/docs/design/2026-09-15-portable-souls-handoff.md +150 -0
  37. package/docs/design/2026-09-15-portable-souls-implementation.md +417 -0
  38. package/docs/design/2026-09-15-selection-lock-and-approval.md +122 -0
  39. package/docs/design/2026-09-15-source-observation.md +119 -0
  40. package/docs/design/2026-09-16-captured-admission.md +77 -0
  41. package/docs/design/2026-09-16-captured-helper-dispatch.md +105 -0
  42. package/docs/design/2026-09-16-captured-launch-inputs.md +42 -0
  43. package/docs/design/2026-09-16-command-profile-preparation.md +86 -0
  44. package/docs/design/2026-09-16-fresh-install-first-rollout.md +47 -0
  45. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +277 -0
  46. package/docs/design/2026-09-16-knowledge-capability-contract.md +61 -0
  47. package/docs/design/2026-09-16-messaging-capability-contract.md +59 -0
  48. package/docs/design/2026-09-16-portable-migration-evidence.md +156 -0
  49. package/docs/design/2026-09-16-portable-onboarding.md +177 -0
  50. package/docs/design/2026-09-16-prepare-request-transport.md +26 -0
  51. package/docs/design/2026-09-16-provider-binding-codecs.md +98 -0
  52. package/docs/design/2026-09-16-provider-binding-wire.md +247 -0
  53. package/docs/design/2026-09-17-capability-helper-input-contract.md +95 -0
  54. package/docs/design/2026-09-17-captured-backend-parity.md +53 -0
  55. package/docs/design/2026-09-17-captured-native-start.md +58 -0
  56. package/docs/design/2026-09-17-portable-boundary-hookup.md +19 -0
  57. package/docs/design/2026-09-17-portable-boundary-resources.md +52 -0
  58. package/docs/design/2026-09-17-public-captured-start.md +108 -0
  59. package/docs/design/2026-09-17-public-prepare-request.md +90 -0
  60. package/docs/design/2026-09-18-captured-pi-host.md +205 -0
  61. package/docs/design/2026-09-18-first-cut-release-checklist.md +131 -0
  62. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +60 -0
  63. package/docs/desktop-cli-api.md +5 -2
  64. package/docs/execution-capsule.schema.json +108 -0
  65. package/docs/execution-targets.md +20 -7
  66. package/docs/oats-lock-v3.schema.json +7 -0
  67. package/docs/oats-member.schema.json +38 -0
  68. package/docs/oats-workspace.schema.json +68 -0
  69. package/docs/portable.schema.json +2512 -0
  70. package/docs/provider-check-input.schema.json +7 -0
  71. package/docs/release-notes/v0.24.0.md +104 -0
  72. package/docs/schedules.md +126 -14
  73. package/docs/soul.schema.json +82 -0
  74. package/injects/oats-portable.md +17 -0
  75. package/injects/portable-instance-boundary.md +39 -0
  76. package/injects/portable-work-directory.md +29 -0
  77. package/lib/artifact-approvals.mjs +120 -0
  78. package/lib/artifact-tree.mjs +141 -0
  79. package/lib/capability-artifacts.mjs +179 -0
  80. package/lib/capability-execution.mjs +15 -0
  81. package/lib/capability-inputs.mjs +39 -0
  82. package/lib/capability-provenance.mjs +231 -0
  83. package/lib/captured-action-shape.mjs +21 -0
  84. package/lib/captured-admission-shape.mjs +20 -0
  85. package/lib/captured-binding-file.mjs +36 -0
  86. package/lib/captured-dispatch.mjs +66 -0
  87. package/lib/captured-instance-index.mjs +277 -0
  88. package/lib/captured-invocation-context.mjs +130 -0
  89. package/lib/captured-launch-request.mjs +46 -0
  90. package/lib/captured-operation-process.mjs +15 -0
  91. package/lib/captured-pi-custody.mjs +29 -0
  92. package/lib/captured-pi-host.mjs +167 -0
  93. package/lib/captured-pi-outcome.mjs +172 -0
  94. package/lib/captured-resolutions.mjs +275 -0
  95. package/lib/captured-scaffold.mjs +87 -0
  96. package/lib/captured-selector.mjs +28 -0
  97. package/lib/captured-session-backend.mjs +52 -0
  98. package/lib/captured-source-receipt-file.mjs +72 -0
  99. package/lib/config-data.mjs +104 -0
  100. package/lib/core.mjs +918 -562
  101. package/lib/errors.mjs +7 -0
  102. package/lib/helper-injection-policy.mjs +98 -0
  103. package/lib/herdr.mjs +18 -7
  104. package/lib/instruction-composition.mjs +31 -0
  105. package/lib/legacy-lock-codec.mjs +106 -0
  106. package/lib/manifest-settings.mjs +84 -0
  107. package/lib/package-closure.mjs +48 -0
  108. package/lib/package-materialization.mjs +83 -0
  109. package/lib/pi-sdk-host.mjs +229 -0
  110. package/lib/portable-artifacts.mjs +115 -0
  111. package/lib/portable-choices.mjs +82 -0
  112. package/lib/portable-composition.mjs +136 -0
  113. package/lib/portable-digest.mjs +105 -0
  114. package/lib/portable-files.mjs +26 -0
  115. package/lib/portable-identity.mjs +40 -0
  116. package/lib/portable-lock.mjs +117 -0
  117. package/lib/portable-migration-artifacts.mjs +135 -0
  118. package/lib/portable-migration-evidence.mjs +305 -0
  119. package/lib/portable-migration-store.mjs +199 -0
  120. package/lib/portable-migration.mjs +104 -0
  121. package/lib/portable-onboarding-acceptance.mjs +66 -0
  122. package/lib/portable-onboarding-request.mjs +49 -0
  123. package/lib/portable-onboarding.mjs +230 -0
  124. package/lib/portable-package-preparation.mjs +188 -0
  125. package/lib/portable-policy.mjs +44 -0
  126. package/lib/portable-shape.mjs +35 -0
  127. package/lib/portable-soul.mjs +38 -0
  128. package/lib/portable-state.mjs +80 -0
  129. package/lib/portable-values.mjs +181 -0
  130. package/lib/prepare-composition.mjs +151 -0
  131. package/lib/prepared-bindings.mjs +78 -0
  132. package/lib/prepared-resources.mjs +127 -0
  133. package/lib/provider-binding-broker.mjs +59 -0
  134. package/lib/provider-binding-wire.mjs +110 -0
  135. package/lib/provider-binding.mjs +22 -0
  136. package/lib/repository-observation.mjs +226 -0
  137. package/lib/resolution-shape.mjs +393 -0
  138. package/lib/schedule-capsule.mjs +206 -0
  139. package/lib/schedule.mjs +259 -38
  140. package/lib/servers.mjs +15 -0
  141. package/lib/soul-constraints.mjs +40 -0
  142. package/lib/source-projection.mjs +84 -0
  143. package/lib/source-spec.mjs +189 -0
  144. package/lib/workspace-definition.mjs +126 -0
  145. package/lib/workspace-discovery.mjs +146 -0
  146. package/package-catalog.json +1 -1
  147. package/package.json +3 -2
  148. package/packages/record/lib/capture-cc.mjs +14 -6
  149. package/packages/record/lib/formats.mjs +14 -3
  150. package/packages/record/lib/native-history.mjs +277 -7
  151. package/packages/record/lib/session-snapshot.mjs +25 -5
  152. package/packages/record/lib/sessions-for-home.mjs +30 -13
  153. package/skills/oats/SKILL.md +12 -7
  154. package/skills/oats-config/SKILL.md +12 -9
  155. package/skills/oats-packages/SKILL.md +12 -8
  156. package/skills/oats-portable/SKILL.md +116 -0
  157. package/skills/oats-portable-artifacts/SKILL.md +63 -0
  158. package/skills/oats-portable-setup/SKILL.md +69 -0
@@ -0,0 +1,229 @@
1
+ /** Explicit, zero-plugin Pi SDK host. No auth wrapper, default resource loader,
2
+ * private SDK imports, model substitution or interpretation of legacy Pi argv. */
3
+ import { lstatSync, realpathSync } from "node:fs";
4
+ import { isAbsolute, join, resolve, sep } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { readPortableBytes } from "./portable-files.mjs";
7
+ import { parseStrictJson } from "./portable-values.mjs";
8
+ import { oatsError } from "./errors.mjs";
9
+
10
+ export const PI_SDK_HOST_VERSION = "1";
11
+ export const PI_SDK_VERSION = "0.85.1";
12
+ export const PI_SDK_HOST = fileURLToPath(new URL("../bin/oats-pi-sdk-host.mjs", import.meta.url));
13
+ const fail = (code, message) => { throw oatsError(code, message); };
14
+ const absolute = value => typeof value === "string" && value.length > 0 && !value.includes("\0") && isAbsolute(value) && resolve(value) === value;
15
+ export function isPiSdkHost(recipe) {
16
+ return recipe?.runtime === "pi" && recipe.executable === PI_SDK_HOST;
17
+ }
18
+
19
+ export function parsePiHostModel(value) {
20
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*\/[^\s\0*?\\]+$/.test(value)
21
+ || value.includes("!") || value.includes("$") || value.includes("..")) {
22
+ fail("E_PI_HOST_MODEL", "captured Pi host requires an exact provider/model identifier");
23
+ }
24
+ const slash = value.indexOf("/");
25
+ return { provider: value.slice(0, slash), id: value.slice(slash + 1) };
26
+ }
27
+
28
+ /** Only operator-owned options; internal home/session/model/task are NEVER here. */
29
+ export function parsePiHostRecipeArgs(args) {
30
+ const keys = ["--oats-pi-host", "--mode", "--thinking", "--sdk-root", "--sdk-version"];
31
+ if (!Array.isArray(args) || args.length !== keys.length * 2
32
+ || args.some(value => typeof value !== "string" || value.includes("\0"))
33
+ || keys.some((key, i) => args[i * 2] !== key)
34
+ || args[1] !== PI_SDK_HOST_VERSION || args[3] !== "print" || args[5] !== "medium"
35
+ || !absolute(args[7]) || args[9] !== PI_SDK_VERSION) {
36
+ fail("E_PI_HOST_ARGS", "unsupported captured Pi SDK host profile or arguments");
37
+ }
38
+ return { hostVersion: args[1], mode: args[3], thinkingLevel: args[5], sdkRoot: args[7], sdkVersion: args[9] };
39
+ }
40
+
41
+ export function validatePiHostRecipe(recipe) {
42
+ if (!isPiSdkHost(recipe) || recipe.executableResource || (recipe.executableResolvedFrom !== undefined && recipe.executableResolvedFrom !== "explicit-host")) {
43
+ fail("E_PI_HOST_SELECTION", "Pi SDK host requires its exact explicit kernel executable");
44
+ }
45
+ const options = parsePiHostRecipeArgs(recipe.args);
46
+ parsePiHostModel(recipe.model);
47
+ if (!recipe.env || typeof recipe.env !== "object" || Array.isArray(recipe.env) || Object.keys(recipe.env).length
48
+ || recipe.yolo !== false) fail("E_PI_HOST_SELECTION", "Pi host-v1 has no launch environment contributions or yolo policy");
49
+ if (recipe.hooks && (Object.keys(recipe.hooks.env ?? {}).length || Object.keys(recipe.hooks.launch ?? {}).length || (recipe.hooks.contributions ?? []).length)) {
50
+ fail("E_PI_HOST_SELECTION", "Pi host-v1 does not accept launch-hook contributions");
51
+ }
52
+ return options;
53
+ }
54
+
55
+ export function piHostArgv(recipe, { home, sessionDir, taskFile = join(home, "TASK.md") }) {
56
+ validatePiHostRecipe(recipe);
57
+ if (![home, sessionDir, taskFile].every(absolute) || taskFile !== join(home, "TASK.md")) fail("E_PI_HOST_ARGS", "invalid kernel Pi host paths");
58
+ return ["--home", home, "--session-dir", sessionDir, "--model", recipe.model, "--task-file", taskFile, ...recipe.args];
59
+ }
60
+
61
+ export function parsePiHostArgv(argv) {
62
+ const keys = ["--home", "--session-dir", "--model", "--task-file"];
63
+ if (!Array.isArray(argv) || argv.length !== 18 || keys.some((key, i) => argv[i * 2] !== key)) {
64
+ fail("E_PI_HOST_ARGS", "Pi host accepts only the kernel-rendered argv prefix and closed host profile");
65
+ }
66
+ const [home, sessionDir, model, taskFile] = keys.map((_, i) => argv[i * 2 + 1]);
67
+ if (![home, sessionDir, taskFile].every(absolute) || taskFile !== join(home, "TASK.md")) fail("E_PI_HOST_ARGS", "invalid kernel Pi host paths");
68
+ const modelSelection = parsePiHostModel(model);
69
+ return { home, sessionDir, model, modelSelection, taskFile, ...parsePiHostRecipeArgs(argv.slice(8)) };
70
+ }
71
+
72
+ /** Package metadata only; no SDK import, auth inspection, refresh or model call. */
73
+ export function resolvePiSdkEntry({ sdkRoot, sdkVersion }) {
74
+ if (!absolute(sdkRoot) || sdkVersion !== PI_SDK_VERSION || !lstatSync(sdkRoot).isDirectory() || realpathSync(sdkRoot) !== sdkRoot) {
75
+ fail("E_PI_HOST_SDK", "Pi SDK root must be the exact physical selected installation");
76
+ }
77
+ // OATS-managed payloads must use their own retained-resource approval surface.
78
+ if (sdkRoot.split(sep).includes(".agents")) fail("E_PI_HOST_SDK", "managed OATS resources cannot be used as an external SDK installation");
79
+ const pkg = parseStrictJson(readPortableBytes(join(sdkRoot, "package.json")));
80
+ const entry = pkg.exports?.["."]?.import;
81
+ if (pkg.name !== "@earendil-works/pi-coding-agent" || pkg.version !== sdkVersion || typeof entry !== "string" || !entry.startsWith("./")) {
82
+ fail("E_PI_HOST_SDK", "selected SDK package/name/version/public export does not match the host profile");
83
+ }
84
+ const file = resolve(sdkRoot, entry);
85
+ if (!file.startsWith(sdkRoot + sep) || realpathSync(file) !== file || !lstatSync(file).isFile()) fail("E_PI_HOST_SDK", "Pi public SDK export escapes its selected installation");
86
+ return pathToFileURL(file).href;
87
+ }
88
+
89
+ export function readPiHostTask(taskFile) {
90
+ const bytes = readPortableBytes(taskFile);
91
+ if (bytes.length > 512 * 1024) fail("E_PI_HOST_TASK", "captured Pi task exceeds the launch bound");
92
+ let task;
93
+ try { task = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
94
+ catch { fail("E_PI_HOST_TASK", "captured Pi task is not UTF-8"); }
95
+ if (!task.trim() || task.includes("\0")) fail("E_PI_HOST_TASK", "captured Pi print requires a nonempty task");
96
+ return { bytes, task };
97
+ }
98
+
99
+ /** Public ResourceLoader interface. verify() reopens retained authority on EVERY
100
+ * reload, before loadSkills reads anything. No SDK discovery/loader is copied. */
101
+ export function createPiResourceLoader(sdk, { cwd, agentDir, verify }) {
102
+ let agents, skills, extensions;
103
+ const ready = () => { if (!agents) fail("E_PI_HOST_CURRICULUM", "Pi curriculum has not been verified"); };
104
+ return {
105
+ getExtensions() { ready(); return extensions; },
106
+ getSkills() { ready(); return skills; },
107
+ getPrompts: () => ({ prompts: [], diagnostics: [] }),
108
+ getThemes: () => ({ themes: [], diagnostics: [] }),
109
+ getAgentsFiles() { ready(); return { agentsFiles: [agents] }; },
110
+ getSystemPrompt: () => undefined,
111
+ getSystemPromptSource: () => undefined,
112
+ getAppendSystemPrompt: () => [],
113
+ getAppendSystemPromptSources: () => [],
114
+ extendResources(paths) {
115
+ if (!paths || typeof paths !== "object" || Array.isArray(paths)
116
+ || Object.keys(paths).some(key => !["skillPaths", "promptPaths", "themePaths"].includes(key))
117
+ || Object.values(paths).some(value => !Array.isArray(value) || value.length)) {
118
+ fail("E_PI_HOST_CURRICULUM", "unselected Pi resource additions are not supported");
119
+ }
120
+ },
121
+ async reload() {
122
+ agents = skills = extensions = undefined; // failed reload cannot expose a stale verified view
123
+ const selected = await verify();
124
+ if (selected.cwd !== cwd || typeof selected.text !== "string" || !Array.isArray(selected.skills)) fail("E_PI_HOST_CURRICULUM", "Pi retained curriculum context changed");
125
+ const result = sdk.loadSkills({ cwd, agentDir, skillPaths: selected.skills.map(skill => skill.path), includeDefaults: false });
126
+ if (result.diagnostics.length || result.skills.length !== selected.skills.length) fail("E_PI_HOST_CURRICULUM", "Pi skill loader did not consume the exact retained skill set");
127
+ for (const wanted of selected.skills) {
128
+ const found = result.skills.filter(skill => skill.name === wanted.name);
129
+ if (found.length !== 1 || realpathSync(found[0].filePath) !== realpathSync(wanted.path)) fail("E_PI_HOST_CURRICULUM", "Pi skill identity differs from its retained selection");
130
+ }
131
+ await verify(); // no selected-file drift during SDK parsing
132
+ agents = { path: selected.agentsPath, content: selected.text };
133
+ skills = result;
134
+ extensions = { extensions: [], errors: [], runtime: sdk.createExtensionRuntime() };
135
+ },
136
+ };
137
+ }
138
+
139
+ /** Print exposes no history UI/import/resume/branch operations. The proxy is a
140
+ * before-delegation boundary around the real public SDK runtime, not an engine
141
+ * replacement. It never calls a late factory after opening an unowned target. */
142
+ export function guardPiPrintRuntime(runtime) {
143
+ const unsupported = new Set(["switchSession", "importFromJsonl", "fork", "newSession"]);
144
+ return new Proxy(runtime, {
145
+ get(target, key) {
146
+ if (unsupported.has(key)) return () => fail("E_PI_HOST_HISTORY", "session replacement/import is not supported by this explicit print profile");
147
+ const value = Reflect.get(target, key, target);
148
+ return typeof value === "function" ? value.bind(target) : value;
149
+ },
150
+ });
151
+ }
152
+
153
+ /** Non-authorizing metadata from actual PUBLIC SDK/session objects. No prompt,
154
+ * content, raw error, credential/profile or request-as-result material is copied. */
155
+ export function observePiSdkSession(session, sdkVersion) {
156
+ const text = value => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") ? value : null;
157
+ const manager = session.sessionManager, header = manager.getHeader();
158
+ const messages = session.messages, last = messages.at(-1);
159
+ const entry = manager.getBranch().findLast(value => value.type === "message");
160
+ const native = entry?.message;
161
+ const matchesLast = last?.role === "assistant" && native?.role === "assistant"
162
+ && native.timestamp === last.timestamp && native.provider === last.provider
163
+ && native.model === last.model && native.stopReason === last.stopReason;
164
+ const finalAssistant = matchesLast && text(entry.id) && text(native.provider) && text(native.model)
165
+ && Number.isFinite(native.timestamp) && native.timestamp >= 0
166
+ && ["stop", "length", "toolUse", "error", "aborted"].includes(native.stopReason)
167
+ ? { entryId: entry.id, provider: native.provider, model: native.model,
168
+ responseModel: text(native.responseModel), stopReason: native.stopReason, timestamp: native.timestamp } : null;
169
+ return {
170
+ sdkVersion: text(sdkVersion),
171
+ header: header?.type === "session" && Number.isSafeInteger(header.version) && text(header.id) && text(header.cwd) && text(header.timestamp)
172
+ ? { type: "session", version: header.version, id: header.id, cwd: header.cwd, timestamp: header.timestamp } : null,
173
+ sessionId: text(manager.getSessionId()), sessionFile: text(manager.getSessionFile()),
174
+ model: text(session.model?.provider) && text(session.model?.id) ? { provider: session.model.provider, id: session.model.id } : null,
175
+ finalAssistant,
176
+ };
177
+ }
178
+
179
+ /** Production uses native services in the user's UNCHANGED profile. No options
180
+ * are passed to ModelRuntime to override stores/auth/config/refresh behavior.
181
+ * Dependencies are injectable for UNIT orchestration checks, not readiness. */
182
+ export async function runPiSdkHost(argv, { verifyContext, onOutcome, importSdk = url => import(url), cwd = process.cwd() } = {}) {
183
+ const options = parsePiHostArgv(argv);
184
+ if (typeof verifyContext !== "function" || realpathSync(cwd) !== options.home) fail("E_PI_HOST_CUSTODY", "Pi host requires its admitted captured home context");
185
+ const verify = () => verifyContext(options);
186
+ await verify();
187
+ const { task } = readPiHostTask(options.taskFile);
188
+ const entry = resolvePiSdkEntry(options);
189
+ const sdk = await importSdk(entry); // the verified PUBLIC root export only
190
+ if (sdk.VERSION !== options.sdkVersion) fail("E_PI_HOST_SDK", "loaded Pi SDK version differs from selection");
191
+ await verify();
192
+ const agentDir = sdk.getAgentDir(); // native profile resolution, never sessionDir
193
+ const modelRuntime = await sdk.ModelRuntime.create();
194
+ const model = modelRuntime.getModel(options.modelSelection.provider, options.modelSelection.id);
195
+ if (!model || model.provider !== options.modelSelection.provider || model.id !== options.modelSelection.id) fail("E_PI_HOST_MODEL", "selected native Pi model is unavailable; no default was selected");
196
+ const factory = async ({ cwd: target, agentDir: profile, sessionManager, sessionStartEvent }) => {
197
+ await verify();
198
+ if (target !== options.home || profile !== agentDir || sessionManager.getCwd() !== target || sessionManager.getSessionDir() !== options.sessionDir) fail("E_PI_HOST_HISTORY", "Pi runtime context differs from its owned history");
199
+ const settingsManager = sdk.SettingsManager.create(target, agentDir);
200
+ const resourceLoader = createPiResourceLoader(sdk, { cwd: target, agentDir, verify });
201
+ await resourceLoader.reload();
202
+ const services = { cwd: target, agentDir, modelRuntime, settingsManager, resourceLoader, diagnostics: [] };
203
+ const built = await sdk.createAgentSessionFromServices({ services, sessionManager, sessionStartEvent, model, thinkingLevel: options.thinkingLevel });
204
+ if (built.session.model?.id !== model.id || built.session.model?.provider !== model.provider || built.modelFallbackMessage) fail("E_PI_HOST_MODEL", "Pi substituted the captured model");
205
+ return { ...built, services, diagnostics: [] };
206
+ };
207
+ await verify();
208
+ const sessionManager = sdk.SessionManager.create(options.home, options.sessionDir);
209
+ const runtime = await sdk.createAgentSessionRuntime(factory, { cwd: options.home, agentDir, sessionManager });
210
+ let handedToPrint = false, observation = null;
211
+ try {
212
+ if (onOutcome) runtime.setBeforeSessionInvalidate(() => {
213
+ // Public synchronous hook: capture real session data BEFORE disposal, never
214
+ // synthesize a header/model/final message from the requested launch values.
215
+ try { observation = observePiSdkSession(runtime.session, sdk.VERSION); }
216
+ catch { observation = null; } // unavailable evidence cannot qualify success
217
+ });
218
+ await verify();
219
+ if (readPiHostTask(options.taskFile).task !== task) fail("E_PI_HOST_TASK", "captured task changed during Pi startup");
220
+ handedToPrint = true; // native runPrintMode owns normal disposal/signals/errors
221
+ const exitCode = await sdk.runPrintMode(guardPiPrintRuntime(runtime), { mode: "text", initialMessage: task });
222
+ const verified = await verify(); // no successful host outcome after observed custody drift
223
+ if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) fail("E_PI_HOST_OUTCOME", "native print mode returned no valid status");
224
+ if (onOutcome) await onOutcome(options, { exitCode, observation }, verified);
225
+ return exitCode;
226
+ } finally {
227
+ if (!handedToPrint) await runtime.dispose();
228
+ }
229
+ }
@@ -0,0 +1,115 @@
1
+ /** New-format immutable tree storage. Selection, source identity observation,
2
+ * provenance/requirements, approval and dispatch remain separate upper layers.
3
+ * No ambient lock/config/network lookup and no migration of legacy evidence. */
4
+ import { lstatSync, mkdirSync, realpathSync } from "node:fs";
5
+ import { isAbsolute, join, relative, resolve } from "node:path";
6
+ import { assertRetainableTree, ensureStoreIgnore } from "./capability-artifacts.mjs";
7
+ import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
8
+ import { publishArtifactTree } from "./artifact-tree.mjs";
9
+ import { canonicalJson } from "./portable-values.mjs";
10
+ import { TREE_FORMAT, jsonIntegrity, treeIntegrity, validateIntegrity } from "./portable-digest.mjs";
11
+ import { oatsError } from "./errors.mjs";
12
+
13
+ const exists = (path) => {
14
+ try { return lstatSync(path); }
15
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
16
+ };
17
+ const outside = (root, target) => {
18
+ const path = relative(root, target);
19
+ return path === ".." || path.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(path);
20
+ };
21
+
22
+ function referenceParts(reference) {
23
+ canonicalJson(reference, { maxBytes: 64 * 1024, maxDepth: 16, maxEntries: 1024 });
24
+ if (!reference || typeof reference !== "object" || Array.isArray(reference)) {
25
+ throw oatsError("invalid-artifact-reference", "expected a portable artifact reference");
26
+ }
27
+ validateIntegrity(reference.integrity, [TREE_FORMAT]);
28
+ const keys = Object.keys(reference).sort().join(",");
29
+ let root, extra = [];
30
+ if (reference.kind === "capability" && keys === "capability,integrity,kind" && isMaterializedCapabilityId(reference.capability)) {
31
+ root = [".agents", "capabilities", "artifacts"]; extra = [reference.capability];
32
+ } else if (reference.kind === "soul" && keys === "identity,integrity,kind"
33
+ && reference.identity && typeof reference.identity === "object" && !Array.isArray(reference.identity)
34
+ && Object.keys(reference.identity).length > 0) {
35
+ // The source/record codec qualifies the identity and witness. Storage only
36
+ // derives an opaque safe key from that exact identity; aliases are not keys.
37
+ root = [".agents", "soul-artifacts"]; extra = [jsonIntegrity(reference.identity).value];
38
+ } else if (reference.kind === "resource" && keys === "integrity,kind") {
39
+ root = [".agents", "resource-artifacts"];
40
+ } else throw oatsError("invalid-artifact-reference", "invalid portable artifact reference kind/fields");
41
+ return { root, tail: [...extra, "tree-exec-v1", reference.integrity.value] };
42
+ }
43
+ function explicitScope(scope) {
44
+ if (typeof scope !== "string" || !isAbsolute(scope)) {
45
+ throw oatsError("invalid-artifact-store", "artifact deployment scope must be explicit and absolute");
46
+ }
47
+ try {
48
+ const actual = realpathSync(scope);
49
+ if (!lstatSync(actual).isDirectory()) throw oatsError("invalid-artifact-store", "artifact scope is not a directory");
50
+ return actual;
51
+ } catch (error) {
52
+ if (error.code === "ENOENT") throw oatsError("artifact-not-found", "artifact deployment scope is absent");
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ /** Lexical path only. This does not verify the store, bytes, provenance or trust. */
58
+ export function portableArtifactDir(scope, reference) {
59
+ if (typeof scope !== "string" || !isAbsolute(scope)) {
60
+ throw oatsError("invalid-artifact-store", "artifact deployment scope must be explicit and absolute");
61
+ }
62
+ const { root, tail } = referenceParts(reference);
63
+ return join(resolve(scope), ...root, ...tail);
64
+ }
65
+ function storeDirectory(scope, reference, create) {
66
+ const parts = referenceParts(reference), prefix = [...parts.root, ...parts.tail.slice(0, -1)];
67
+ let path = explicitScope(scope);
68
+ for (let index = 0; index < prefix.length; index++) {
69
+ path = join(path, prefix[index]);
70
+ if (create) {
71
+ try { mkdirSync(path, { mode: 0o700 }); }
72
+ catch (error) { if (error.code !== "EEXIST") throw error; }
73
+ }
74
+ const stat = exists(path);
75
+ if (!stat) throw oatsError("artifact-not-found", "portable artifact store is absent");
76
+ if (!stat.isDirectory()) throw oatsError("invalid-artifact-store", "managed artifact store component is not a real directory");
77
+ if (create && index === parts.root.length - 1) ensureStoreIgnore(path);
78
+ }
79
+ return path;
80
+ }
81
+ function verifyTree(path, reference) {
82
+ const stat = exists(path);
83
+ if (!stat) throw oatsError("artifact-not-found", "portable artifact tree is absent");
84
+ if (!stat.isDirectory()) throw oatsError("invalid-artifact", "portable artifact root must be a real directory");
85
+ // Digest traversal is bounded and does not follow links. Then qualify link
86
+ // containment; neither check uses the original source or an ambient package row.
87
+ const actual = treeIntegrity(path);
88
+ if (actual.value !== reference.integrity.value) throw oatsError("integrity-drift", "portable artifact digest does not match its reference");
89
+ assertRetainableTree(path);
90
+ return path;
91
+ }
92
+
93
+ /** Byte/containment verification only, not a complete captured-resolution proof. */
94
+ export function verifyPortableArtifact(scope, reference) {
95
+ const root = storeDirectory(scope, reference, false);
96
+ const dir = join(root, reference.integrity.value);
97
+ verifyTree(dir, reference);
98
+ return { reference, dir };
99
+ }
100
+ export function retainPortableArtifact(scope, sourceDir, reference) {
101
+ referenceParts(reference);
102
+ const deployment = explicitScope(scope);
103
+ if (typeof sourceDir !== "string" || !isAbsolute(sourceDir)) {
104
+ throw oatsError("invalid-artifact", "artifact input must be an explicit materialized directory");
105
+ }
106
+ verifyTree(sourceDir, reference);
107
+ const candidate = portableArtifactDir(deployment, reference), source = realpathSync(sourceDir);
108
+ if (!outside(source, candidate) && source !== candidate) {
109
+ throw oatsError("invalid-artifact", "artifact source contains its destination store");
110
+ }
111
+ const parent = storeDirectory(deployment, reference, true), dir = join(parent, reference.integrity.value);
112
+ if (exists(dir)) { verifyTree(dir, reference); return { reference, dir, status: "kept" }; }
113
+ const status = publishArtifactTree(sourceDir, dir, (tree) => verifyTree(tree, reference));
114
+ return { reference, dir, status };
115
+ }
@@ -0,0 +1,82 @@
1
+ /** The single bounded choice resolver: equality/presence constraints plus
2
+ * fallback values. No expressions, source fetching, provider internals or trust.
3
+ * Field codecs represent a disabled/unbound selection as null; false/[] may be
4
+ * legitimate DATA values. This engine does not guess meaning from strings. */
5
+ import { canonicalJson, compareUtf8 } from "./portable-values.mjs";
6
+ import { invalidShape, objectAt, stringAt } from "./portable-shape.mjs";
7
+
8
+ const PRIORITY = Object.freeze({ "manifest-default": -1, "workspace-default": 0, "soul-default": 1, "import-adoption": 2, operator: 3 });
9
+ export const CHOICE_KINDS = Object.freeze(Object.keys(PRIORITY));
10
+ const equal = (a, b) => canonicalJson(a) === canonicalJson(b);
11
+ const originOrder = (a, b) => compareUtf8(canonicalJson(a.origin), canonicalJson(b.origin))
12
+ || compareUtf8(canonicalJson(a), canonicalJson(b));
13
+
14
+ export function resolveChoices({ requirements = [], candidates = [] } = {}) {
15
+ canonicalJson({ requirements, candidates });
16
+ if (!Array.isArray(requirements) || !Array.isArray(candidates)) invalidShape("", "choice inputs must be arrays");
17
+ const groups = new Map(), choices = Object.create(null), problems = [];
18
+ const group = (key) => {
19
+ stringAt(key, "/key");
20
+ if (!key.startsWith("/")) invalidShape("/key", "choice keys are absolute JSON pointers");
21
+ if (!groups.has(key)) groups.set(key, { requirements: [], candidates: [] });
22
+ return groups.get(key);
23
+ };
24
+ for (const [index, requirement] of requirements.entries()) {
25
+ const at = `/requirements/${index}`;
26
+ objectAt(requirement, ["key", "kind", "value", "origin"], ["key", "kind", "origin"], at);
27
+ if (!["equals", "required"].includes(requirement.kind)) invalidShape(`${at}/kind`, "unsupported constraint kind");
28
+ if ((requirement.kind === "equals") !== Object.hasOwn(requirement, "value")) invalidShape(at, "constraint value does not match its kind");
29
+ objectAt(requirement.origin, null, [], `${at}/origin`);
30
+ group(requirement.key).requirements.push(requirement);
31
+ }
32
+ for (const [index, candidate] of candidates.entries()) {
33
+ const at = `/candidates/${index}`;
34
+ objectAt(candidate, ["key", "kind", "value", "origin"], ["key", "kind", "value", "origin"], at);
35
+ if (!Object.hasOwn(PRIORITY, candidate.kind)) invalidShape(`${at}/kind`, "unsupported choice origin kind");
36
+ objectAt(candidate.origin, null, [], `${at}/origin`);
37
+ group(candidate.key).candidates.push(candidate);
38
+ }
39
+ const conflict = (key, a, b, message) => {
40
+ problems.push({ code: "requirement-conflict", key, message, origins: [a.origin, b.origin] });
41
+ };
42
+ for (const key of [...groups.keys()].sort(compareUtf8)) {
43
+ const input = groups.get(key);
44
+ input.requirements.sort(originOrder);
45
+ input.candidates.sort((a, b) => PRIORITY[a.kind] - PRIORITY[b.kind] || originOrder(a, b));
46
+ const fixed = input.requirements.filter((item) => item.kind === "equals");
47
+ const presence = input.requirements.find((item) => item.kind === "required");
48
+ const seed = fixed[0];
49
+ let value = seed ? seed.value : null, selectedBy = seed ? seed.origin : null;
50
+ for (const other of fixed.slice(1)) if (!equal(seed.value, other.value)) conflict(key, seed, other, "hard requirements disagree");
51
+ if (seed && presence && seed.value === null) conflict(key, presence, seed, "required value is explicitly absent");
52
+ const considered = [];
53
+ let previous;
54
+ for (const candidate of input.candidates) {
55
+ if (previous && PRIORITY[candidate.kind] === PRIORITY[previous.kind] && !equal(candidate.value, previous.value)) {
56
+ conflict(key, previous, candidate, "equal-authority choices disagree");
57
+ }
58
+ previous = candidate;
59
+ const incompatible = (seed && !equal(candidate.value, seed.value)) || (presence && candidate.value === null);
60
+ if (incompatible) {
61
+ if (PRIORITY[candidate.kind] >= PRIORITY["import-adoption"]) {
62
+ conflict(key, seed ?? presence, candidate, "explicit choice cannot erase a hard requirement");
63
+ }
64
+ considered.push({ kind: candidate.kind, value: candidate.value, origin: candidate.origin, disposition: "overridden" });
65
+ continue;
66
+ }
67
+ if (!seed || PRIORITY[candidate.kind] >= PRIORITY["import-adoption"]) {
68
+ value = candidate.value; selectedBy = candidate.origin;
69
+ }
70
+ considered.push({ kind: candidate.kind, value: candidate.value, origin: candidate.origin, disposition: "overridden" });
71
+ }
72
+ for (const entry of considered) {
73
+ if (selectedBy && equal(entry.origin, selectedBy) && equal(entry.value, value)) entry.disposition = "selected";
74
+ }
75
+ if (presence && value === null && !problems.some((item) => item.key === key)) {
76
+ problems.push({ code: "needs-configuration", key, message: "required value has no concrete binding", origins: [presence.origin] });
77
+ }
78
+ choices[key] = { value, selectedBy, constraints: input.requirements.map(({ key: _, ...constraint }) => constraint), considered };
79
+ }
80
+ return { contractVersion: 1, status: problems.some((item) => item.code === "requirement-conflict")
81
+ ? "conflict" : problems.length ? "needs-configuration" : "resolved", choices, problems };
82
+ }
@@ -0,0 +1,136 @@
1
+ /** Source-aware software choice planning. Consumes validated parser results;
2
+ * qualifies no hosting identity and performs no acquisition, approval or enrollment.
3
+ * All precedence/conflicts still go through the single resolveChoices engine. */
4
+ import { canonicalJson, compareUtf8 } from "./portable-values.mjs";
5
+ import { invalidShape, objectAt, pointerKey } from "./portable-shape.mjs";
6
+ import { validateOrigin } from "./resolution-shape.mjs";
7
+ import { sameIdentity, validateSoulIdentity } from "./portable-identity.mjs";
8
+ import { FUNDAMENTAL_SLOTS, validatePolicyChoices } from "./portable-policy.mjs";
9
+ import { capabilityChoiceKey, layerChoiceKey, settingChoiceKey, soulConstraints, sourceSelectionValue } from "./soul-constraints.mjs";
10
+ import { resolveChoices } from "./portable-choices.mjs";
11
+ import { validateRepoAnchor, validateSelectionSource } from "./source-spec.mjs";
12
+ import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
13
+
14
+ function originAt(parsed, pointer, kind) {
15
+ const locator = parsed.origins[pointer];
16
+ if (!locator) invalidShape(pointer, "planning requires the original declaration origin");
17
+ const origin = { ...locator, kind };
18
+ validateOrigin(origin);
19
+ return origin;
20
+ }
21
+
22
+ export function planSoftwareChoices(input) {
23
+ canonicalJson(input);
24
+ objectAt(input, ["identity", "soul", "workspace", "adoptions", "operator", "providerRequirements"], ["identity", "soul"]);
25
+ const { identity, soul, workspace = null, adoptions = [], operator = null, providerRequirements = [] } = input;
26
+ validateSoulIdentity(identity);
27
+ const requirements = soulConstraints(soul), candidates = [], settingsCandidates = [];
28
+ if (!Array.isArray(adoptions) || !Array.isArray(providerRequirements)) invalidShape("", "adoptions and provider constraints must be arrays");
29
+ const add = (parsed, key, value, pointer, kind) => candidates.push({ key, value, kind, origin: originAt(parsed, pointer, kind) });
30
+ const collect = (parsed, policy, pointer, kind, sourceContext) => {
31
+ const selection = (id, item, at, key) => {
32
+ const selectedSource = item === false || item === "none" ? null : sourceSelectionValue(id, parsed, at, sourceContext);
33
+ add(parsed, key, selectedSource, at, kind);
34
+ if (item && typeof item === "object") for (const [name, value] of Object.entries(item.settings ?? {})) {
35
+ settingsCandidates.push({ owner: { key, value: selectedSource }, candidate: { key: settingChoiceKey(id, name), value, kind,
36
+ origin: originAt(parsed, `${at}/settings/${pointerKey(name)}`, kind) } });
37
+ }
38
+ };
39
+ for (const [id, item] of Object.entries(policy.capabilities ?? {})) selection(id, item, `${pointer}/capabilities/${pointerKey(id)}`, capabilityChoiceKey(id));
40
+ for (const slot of FUNDAMENTAL_SLOTS) if (Object.hasOwn(policy, slot)) {
41
+ const item = policy[slot]; selection(item === "none" ? null : item.capability, item, `${pointer}/${slot}`, layerChoiceKey(slot));
42
+ }
43
+ };
44
+ if (workspace?.declaration.defaults) collect(workspace, workspace.declaration.defaults, "/defaults", "workspace-default");
45
+ if (soul.declaration.defaults) collect(soul, soul.declaration.defaults, "/defaults", "soul-default");
46
+
47
+ // Discovery supplies qualified identity. An alias never performs this lookup.
48
+ // Several aliases may contribute compatible fields; conflicting equal-authority
49
+ // fields are reported by resolveChoices with both original document pointers.
50
+ for (const adoption of adoptions) {
51
+ objectAt(adoption, ["identity", "parsed", "origins", "pointer"], ["identity", "parsed", "origins", "pointer"]);
52
+ validateSoulIdentity(adoption.identity);
53
+ if (!sameIdentity(adoption.identity, identity)) continue;
54
+ const imported = adoption.parsed.reference;
55
+ if (identity.kind !== "git-soul" || imported.soul !== identity.exportPath) invalidShape("/adoptions", "qualified adoption does not match its exported source path");
56
+ if (identity.repository.kind === "canonical-remote" && imported.source !== identity.repository.remote) invalidShape("/adoptions", "import source contradicts its qualified canonical identity");
57
+ const parsed = { sources: adoption.parsed.sources, origins: adoption.origins };
58
+ const root = `${adoption.pointer}/adoption`, value = imported.adoption ?? {};
59
+ if (value.providers) collect(parsed, value.providers, `${root}/providers`, "import-adoption");
60
+ for (const field of ["bindings", "teamAliases"]) for (const [key, item] of Object.entries(value[field] ?? {})) {
61
+ add(parsed, `/adoption/${field}/${pointerKey(key)}`, item, `${root}/${field}/${pointerKey(key)}`, "import-adoption");
62
+ }
63
+ }
64
+ if (operator) {
65
+ objectAt(operator, ["policy", "document", "localBase", "allowLocalPaths", "sourceContext", "bindings"], ["policy", "document"]);
66
+ if (operator.bindings !== undefined) objectAt(operator.bindings, null, [], "/operator/bindings");
67
+ if (operator.sourceContext !== undefined) validateRepoAnchor(operator.sourceContext);
68
+ if (operator.allowLocalPaths !== undefined && typeof operator.allowLocalPaths !== "boolean") invalidShape("/operator/allowLocalPaths", "expected explicit boolean local-source authorization");
69
+ validateOrigin({ kind: "operator", document: operator.document, pointer: "/policy" });
70
+ if (operator.document.kind !== "operator") invalidShape("/operator/document", "operator choices need an operator input locator");
71
+ const origins = Object.create(null);
72
+ const mark = (value, pointer) => {
73
+ origins[pointer] = { document: operator.document, pointer };
74
+ if (value && typeof value === "object") for (const [key, child] of Object.entries(value)) mark(child, `${pointer}/${pointerKey(key)}`);
75
+ };
76
+ mark(operator.policy, "/policy");
77
+ const sources = validatePolicyChoices(operator.policy, { pointer: "/policy", origins,
78
+ localBase: operator.localBase, allowLocalPaths: operator.allowLocalPaths === true });
79
+ collect({ sources, origins }, operator.policy, "/policy", "operator", operator.sourceContext);
80
+ }
81
+ for (const requirement of [...requirements, ...providerRequirements]) validateOrigin(requirement.origin);
82
+ // Provider-owned fixed fields join the SAME constraint pipeline. This function
83
+ // neither interprets a payload nor executes a provider to obtain these inputs.
84
+ const allRequirements = [...requirements, ...providerRequirements];
85
+ // Eligibility follows the selected owning field AND source. Reuse the same
86
+ // engine for this deterministic source pass, then final field resolution;
87
+ // nothing is fetched/re-observed and no second precedence policy is introduced.
88
+ const sourceChoices = resolveChoices({ requirements: allRequirements, candidates }).choices;
89
+ const excludedSettings = [];
90
+ for (const { owner, candidate } of settingsCandidates) {
91
+ if (sameIdentity(sourceChoices[owner.key]?.value ?? null, owner.value)) candidates.push(candidate);
92
+ else excludedSettings.push({ ...candidate, owner, reason: "owning-source-overridden" });
93
+ }
94
+ const result = resolveChoices({ requirements: allRequirements, candidates });
95
+ const capabilities = Object.create(null), providers = Object.create(null), settings = Object.create(null);
96
+ const problems = [...result.problems];
97
+ const select = (key) => {
98
+ const choice = result.choices[key];
99
+ if (!choice || choice.value === null) return null;
100
+ objectAt(choice.value, ["capability", "source"], ["capability", "source"], key);
101
+ const { capability, source } = choice.value;
102
+ if (!isMaterializedCapabilityId(capability)) invalidShape(key, "invalid selected capability identity");
103
+ if (key.startsWith("/capabilities/") && key !== capabilityChoiceKey(capability)) invalidShape(key, "capability choice key differs from its selected identity");
104
+ validateSelectionSource(source);
105
+ if (Object.hasOwn(capabilities, capability)) {
106
+ const prior = capabilities[capability];
107
+ if (!sameIdentity(prior.source, source)) problems.push({ code: "requirement-conflict", key,
108
+ message: "one capability identity has incompatible selected sources", origins: [result.choices[prior.choiceKeys[0]].selectedBy, choice.selectedBy] });
109
+ prior.choiceKeys.push(key);
110
+ } else capabilities[capability] = { source, choiceKeys: [key] };
111
+ return capability;
112
+ };
113
+ for (const key of Object.keys(result.choices).sort(compareUtf8)) if (key.startsWith("/capabilities/")) select(key);
114
+ for (const slot of FUNDAMENTAL_SLOTS) {
115
+ const key = layerChoiceKey(slot);
116
+ if (Object.hasOwn(result.choices, key)) providers[slot] = select(key);
117
+ }
118
+ // Only selected software receives settings; alternative/default history remains
119
+ // in choices for explanation, not an ambient configuration to execute later.
120
+ for (const id of Object.keys(capabilities)) settings[id] = Object.create(null);
121
+ const unescape = (part) => part.replace(/~1/g, "/").replace(/~0/g, "~");
122
+ for (const key of Object.keys(result.choices)) {
123
+ if (key.startsWith("/layers/") && !FUNDAMENTAL_SLOTS.some((slot) => key === layerChoiceKey(slot))) invalidShape(key, "unknown fundamental slot choice");
124
+ if (!key.startsWith("/settings/")) continue;
125
+ const separator = key.indexOf("/", "/settings/".length);
126
+ if (separator < 0) invalidShape(key, "setting choice must name a capability and setting");
127
+ const id = unescape(key.slice("/settings/".length, separator)), name = unescape(key.slice(separator + 1));
128
+ if (!isMaterializedCapabilityId(id) || settingChoiceKey(id, name) !== key) invalidShape(key, "noncanonical setting choice key");
129
+ if (Object.hasOwn(settings, id)) settings[id][name] = key;
130
+ }
131
+ return { ...result, status: problems.some((problem) => problem.code === "requirement-conflict") ? "conflict" : result.status,
132
+ requirements: allRequirements, candidates, capabilities, providers, settings, excludedSettings, problems,
133
+ providerInputs: { knowledge: soul.declaration.knowledge ?? null, stores: workspace?.declaration.knowledge?.stores ?? [] },
134
+ // Advertisements/adoption aliases are data, never wider-team consent.
135
+ advertisedTeams: soul.declaration.teams ?? [] };
136
+ }