@awebai/oats 0.23.1 → 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 (159) 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.23.2.md +49 -0
  72. package/docs/release-notes/v0.24.0.md +104 -0
  73. package/docs/schedules.md +126 -14
  74. package/docs/soul.schema.json +82 -0
  75. package/injects/oats-portable.md +17 -0
  76. package/injects/portable-instance-boundary.md +39 -0
  77. package/injects/portable-work-directory.md +29 -0
  78. package/lib/artifact-approvals.mjs +120 -0
  79. package/lib/artifact-tree.mjs +141 -0
  80. package/lib/capability-artifacts.mjs +179 -0
  81. package/lib/capability-execution.mjs +15 -0
  82. package/lib/capability-inputs.mjs +39 -0
  83. package/lib/capability-provenance.mjs +231 -0
  84. package/lib/captured-action-shape.mjs +21 -0
  85. package/lib/captured-admission-shape.mjs +20 -0
  86. package/lib/captured-binding-file.mjs +36 -0
  87. package/lib/captured-dispatch.mjs +66 -0
  88. package/lib/captured-instance-index.mjs +277 -0
  89. package/lib/captured-invocation-context.mjs +130 -0
  90. package/lib/captured-launch-request.mjs +46 -0
  91. package/lib/captured-operation-process.mjs +15 -0
  92. package/lib/captured-pi-custody.mjs +29 -0
  93. package/lib/captured-pi-host.mjs +167 -0
  94. package/lib/captured-pi-outcome.mjs +172 -0
  95. package/lib/captured-resolutions.mjs +275 -0
  96. package/lib/captured-scaffold.mjs +87 -0
  97. package/lib/captured-selector.mjs +28 -0
  98. package/lib/captured-session-backend.mjs +52 -0
  99. package/lib/captured-source-receipt-file.mjs +72 -0
  100. package/lib/config-data.mjs +104 -0
  101. package/lib/core.mjs +918 -562
  102. package/lib/errors.mjs +7 -0
  103. package/lib/helper-injection-policy.mjs +98 -0
  104. package/lib/herdr.mjs +18 -7
  105. package/lib/instruction-composition.mjs +31 -0
  106. package/lib/legacy-lock-codec.mjs +106 -0
  107. package/lib/manifest-settings.mjs +84 -0
  108. package/lib/package-closure.mjs +48 -0
  109. package/lib/package-materialization.mjs +83 -0
  110. package/lib/pi-sdk-host.mjs +229 -0
  111. package/lib/portable-artifacts.mjs +115 -0
  112. package/lib/portable-choices.mjs +82 -0
  113. package/lib/portable-composition.mjs +136 -0
  114. package/lib/portable-digest.mjs +105 -0
  115. package/lib/portable-files.mjs +26 -0
  116. package/lib/portable-identity.mjs +40 -0
  117. package/lib/portable-lock.mjs +117 -0
  118. package/lib/portable-migration-artifacts.mjs +135 -0
  119. package/lib/portable-migration-evidence.mjs +305 -0
  120. package/lib/portable-migration-store.mjs +199 -0
  121. package/lib/portable-migration.mjs +104 -0
  122. package/lib/portable-onboarding-acceptance.mjs +66 -0
  123. package/lib/portable-onboarding-request.mjs +49 -0
  124. package/lib/portable-onboarding.mjs +230 -0
  125. package/lib/portable-package-preparation.mjs +188 -0
  126. package/lib/portable-policy.mjs +44 -0
  127. package/lib/portable-shape.mjs +35 -0
  128. package/lib/portable-soul.mjs +38 -0
  129. package/lib/portable-state.mjs +80 -0
  130. package/lib/portable-values.mjs +181 -0
  131. package/lib/prepare-composition.mjs +151 -0
  132. package/lib/prepared-bindings.mjs +78 -0
  133. package/lib/prepared-resources.mjs +127 -0
  134. package/lib/provider-binding-broker.mjs +59 -0
  135. package/lib/provider-binding-wire.mjs +110 -0
  136. package/lib/provider-binding.mjs +22 -0
  137. package/lib/repository-observation.mjs +226 -0
  138. package/lib/resolution-shape.mjs +393 -0
  139. package/lib/schedule-capsule.mjs +206 -0
  140. package/lib/schedule.mjs +259 -38
  141. package/lib/servers.mjs +15 -0
  142. package/lib/soul-constraints.mjs +40 -0
  143. package/lib/source-projection.mjs +84 -0
  144. package/lib/source-spec.mjs +189 -0
  145. package/lib/workspace-definition.mjs +126 -0
  146. package/lib/workspace-discovery.mjs +146 -0
  147. package/package-catalog.json +1 -1
  148. package/package.json +3 -2
  149. package/packages/record/lib/capture-cc.mjs +14 -6
  150. package/packages/record/lib/formats.mjs +14 -3
  151. package/packages/record/lib/native-history.mjs +277 -7
  152. package/packages/record/lib/session-snapshot.mjs +25 -5
  153. package/packages/record/lib/sessions-for-home.mjs +30 -13
  154. package/skills/oats/SKILL.md +12 -7
  155. package/skills/oats-config/SKILL.md +12 -9
  156. package/skills/oats-packages/SKILL.md +12 -8
  157. package/skills/oats-portable/SKILL.md +116 -0
  158. package/skills/oats-portable-artifacts/SKILL.md +63 -0
  159. package/skills/oats-portable-setup/SKILL.md +69 -0
package/README.md CHANGED
@@ -399,7 +399,10 @@ AGENTS.md, Agent Skills, and OKF.
399
399
  [MIT](LICENSE) © 2026 OATS Framework
400
400
 
401
401
  Session backends and unattended launches are described in
402
- [execution targets](docs/execution-targets.md). Use `oats spawn <soul> --backend
403
- herdr --yolo` for a Herdr-hosted unattended Codex/Claude session, or put
404
- `yolo: true` in the scope's oats-config.yaml. Aweb owns shared event delivery;
405
- terminal transport alone does not enable a messaging broker.
402
+ [execution targets](docs/execution-targets.md). Claude Code and Codex retain normal
403
+ native context and permissions **alongside the complete resolved OATS instance home**:
404
+ skills, capabilities, instructions, task, metadata, work placement and ordinary
405
+ hooks/approvals are still supplied. Add `--yolo` or select `yolo: true` in configuration
406
+ only for an explicit user opt-in to bypass; unattended execution does not imply it.
407
+ Aweb owns shared event delivery; terminal transport alone does not enable a
408
+ messaging broker.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /** Explicit kernel-owned print adapter; not an alternate interpretation of Pi CLI. */
3
+ import { recordCapturedPiExit, runCapturedPiSdkHost } from "../lib/captured-pi-host.mjs";
4
+
5
+ try {
6
+ const argv = process.argv.slice(2);
7
+ process.exitCode = argv[0] === "--oats-pi-record-exit" ? recordCapturedPiExit(argv) : await runCapturedPiSdkHost(argv);
8
+ } catch (error) {
9
+ // Never echo arbitrary SDK/helper/provider error objects: those can contain
10
+ // native auth material. Native print mode owns its ordinary safe diagnostics.
11
+ const known = new Set(["E_PI_HOST_ARGS", "E_PI_HOST_SELECTION", "E_PI_HOST_MODEL", "E_PI_HOST_TASK", "E_PI_HOST_SDK", "E_PI_HOST_CURRICULUM", "E_PI_HOST_HISTORY", "E_PI_HOST_CUSTODY", "E_PI_HOST_RECORD_UNAVAILABLE", "E_PI_HOST_OUTCOME"]);
12
+ const code = known.has(error?.code) ? error.code : "E_PI_HOST_FAILED";
13
+ console.error(process.argv[2] === "--oats-pi-record-exit"
14
+ ? `${code}: captured Pi process observation refused or failed; completion evidence remains held`
15
+ : `${code}: captured Pi host refused or failed; use the selected harness's native setup for model/auth prerequisites`);
16
+ process.exitCode = 1;
17
+ }
package/bin/oats.mjs CHANGED
@@ -23,11 +23,11 @@ import { enableTmuxMouse, tmuxConfigPath, tmuxMouseEnabled } from "../lib/tmux-c
23
23
  import {
24
24
  LAYERS, WORK_MODES, LEGACY_HOME_CAPABILITIES_DIR, OATS_LOCK_FILE, OATS_VERSION, OAS_SCOPE_REMEDY, RETIRED_CAPABILITIES, detectOasScopes, retiredCapabilityReason, configChain, configCapabilityEntries, manifestOperations,
25
25
  acquireCapability, restoreCapabilities, marketplaceCapabilities,
26
- capabilityManifests, capabilityManifest, capabilityMissingRequires, capabilityIntegrity, capabilityTrust, capabilityExecutablePath,
27
- readCapabilityLocks, writeCapabilityLock,
26
+ capabilityManifests, capabilityManifest, capabilityMissingRequires, capabilityIntegrity, capabilityTrust, capabilityExecutablePath, activateCapturedScaffold, loadCapturedDispatch, prepareCapturedComposition, resolveCapturedHelper, capturedNativeSessionAvailability, scaffoldCapturedInstance, startCapturedInstanceSession, withCapturedBindingFile, withCapturedInvocationContextFile,
27
+ readCapabilityLocks, writeCapabilityLock, admitCapturedAction, beginCapturedIntent, settleCapturedIntent,
28
28
  parsePackageSource, inspectGitSourceRoot, acquirePackage, restorePackages, listInstalledPackages, readPackageLocks, readLockedConfigTemplates,
29
29
  officialCapabilityPackage, officialPackageCatalog,
30
- approveCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
30
+ approveCapability, approveAvailableCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
31
31
  packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir, installedCapabilitiesDir, ownedCapabilitiesDir, loadPackageManifestAt,
32
32
  resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
33
33
  findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, listCapabilityAgents, workspaceOf,
@@ -46,9 +46,18 @@ import { parseEnvelopeText, scheduleScopeOf, listSchedules, describe as describe
46
46
  import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
47
47
  import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
48
48
 
49
+ import { capturedSelector } from "../lib/captured-selector.mjs";
50
+ import { inspectCapturedPiOutcome } from "../lib/captured-pi-host.mjs";
51
+ import { readCapturedResolution } from "../lib/captured-resolutions.mjs";
52
+ import { readPortablePreparationRequest } from "../lib/portable-onboarding-request.mjs";
53
+ import { portableScope } from "../lib/portable-state.mjs";
54
+ import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
55
+ import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
56
+
49
57
  const args = process.argv.slice(2);
50
- const cmd = args[0];
58
+ let cmd = args[0];
51
59
  const HELP_WORDS = new Set(["help", "--help", "-h"]);
60
+ const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
52
61
  const flag = (name) => {
53
62
  const i = args.indexOf(`--${name}`);
54
63
  return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
@@ -86,6 +95,327 @@ const CLI_BIN = realpathSync(fileURLToPath(import.meta.url));
86
95
  const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) } })); process.exit(1); };
87
96
  const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result })); };
88
97
 
98
+ function prepareCmd() {
99
+ const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
100
+ const values = new Map(), allowed = new Set(["request", "dir", "source", "revision", "export", "alias", "workspace", "workspace-revision", "work"]);
101
+ for (let index = 1; index < args.length; index++) {
102
+ if (args[index] === "--json") continue;
103
+ const key = args[index].startsWith("--") ? args[index].slice(2) : "";
104
+ if (!allowed.has(key) || values.has(key) || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "prepare needs unique named source/context arguments; use prepare --help");
105
+ values.set(key, args[++index]);
106
+ }
107
+ try {
108
+ let input;
109
+ if (values.has("request")) {
110
+ // The shared leaf owns request bytes/exclusivity; this router alone owns
111
+ // argv and has already refused explicit captured selectors.
112
+ input = readPortablePreparationRequest({ file: values.get("request"),
113
+ inputFlags: Object.fromEntries([...values].filter(([key]) => key !== "request")) });
114
+ } else {
115
+ const deployment = values.get("dir"), alias = values.get("alias"), source = values.get("source");
116
+ if (!deployment || !isAbsolute(deployment) || !alias) fail("E_BAD_ARGS", "prepare needs --dir <absolute deployment> and --alias <name>");
117
+ if (source ? !values.get("revision") || !values.get("export") : !values.get("workspace") || values.has("revision") || values.has("export")) fail("E_BAD_ARGS", "choose a complete source/revision/export reference or a workspace-advertised alias");
118
+ if (values.has("workspace-revision") && !values.has("workspace")) fail("E_BAD_ARGS", "--workspace-revision requires --workspace");
119
+ const origin = { kind: "operator", document: { kind: "operator", id: "oats-prepare" }, pointer: "/source" };
120
+ input = { deployment, source: source ? { source, revision: values.get("revision"), soul: values.get("export"), alias } : alias, origin,
121
+ ...(values.has("work") ? { mode: values.get("work") } : {}),
122
+ ...(values.has("workspace") ? { workspace: { source: values.get("workspace"), origin: { ...origin, pointer: "/workspace" },
123
+ ...(values.has("workspace-revision") ? { revision: values.get("workspace-revision") } : {}) } } : {}) };
124
+ }
125
+ // Pass the whole request to the one public validator/resolver. Unknown
126
+ // fields are refused there, never filtered or filled from ambient state.
127
+ const result = prepareCapturedComposition(input);
128
+ if (!result.resolution) fail("needs-configuration", "preparation is incomplete; no executable resolution was published", result);
129
+ if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
130
+ } catch (error) { fail(error.code || "E_PREPARE_FAILED", error.message); }
131
+ }
132
+
133
+ /** Run one provider operation from immutable captured authority. A home is an
134
+ * explicit target only: its stored binding must name this exact record. */
135
+ function capturedOperation(selector, load, bail) {
136
+ if (args[1] !== "run") bail("E_USAGE", "usage: oats operation run <layer>:<name> --deployment <abs> --resolution <id> [--home <abs>] [--arg k=v ...] [--retry-intent <saved-id>] [--json]");
137
+ const address = args[2], match = typeof address === "string" ? OPERATION_ADDRESS_RE.exec(address) : null;
138
+ if (!match) bail("E_BAD_ARGS", `operation address must be <layer>:<name> with layer one of ${LAYERS.join(", ")} (got ${JSON.stringify(address)})`);
139
+ const [, slot, name] = match, given = Object.create(null);
140
+ let home, retryExecutionId;
141
+ for (let index = 3; index < args.length; index++) {
142
+ const token = args[index];
143
+ if (token === "--json") continue;
144
+ if (token === "--home") {
145
+ const value = args[++index];
146
+ if (home !== undefined || !value || value.startsWith("--") || !isAbsolute(value)) bail("E_BAD_ARGS", "--home needs one absolute instance home");
147
+ home = resolve(value); continue;
148
+ }
149
+ if (token === "--retry-intent") {
150
+ const value = args[++index];
151
+ if (retryExecutionId !== undefined || !value || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) bail("E_BAD_ARGS", "--retry-intent requires one saved executionId");
152
+ retryExecutionId = value; continue;
153
+ }
154
+ if (token === "--arg") {
155
+ const value = args[++index], eq = value?.indexOf("=") ?? -1;
156
+ if (eq < 1) bail("E_BAD_ARGS", "--arg expects name=value");
157
+ const key = value.slice(0, eq);
158
+ if (Object.hasOwn(given, key)) bail("E_BAD_ARGS", `duplicate operation arg ${JSON.stringify(key)}`);
159
+ given[key] = value.slice(eq + 1); continue;
160
+ }
161
+ bail("E_BAD_ARGS", `unsupported captured operation argument ${JSON.stringify(token)}`);
162
+ }
163
+ let meta;
164
+ if (home) {
165
+ const metaFile = join(home, "instance.json");
166
+ if (!existsSync(metaFile)) bail("E_SESSION_UNKNOWN", `${home} is not an OATS instance home (no instance.json)`);
167
+ try { meta = JSON.parse(readFileSync(metaFile, "utf8")); } catch (error) { bail("E_SESSION_UNKNOWN", `${metaFile}: ${error.message}`); }
168
+ if (!meta || typeof meta !== "object" || Array.isArray(meta) || typeof meta.instance !== "string" || !meta.instance) bail("E_SESSION_UNKNOWN", `${metaFile}: invalid instance metadata`);
169
+ const binding = meta.executionBinding;
170
+ if (!binding) bail("migration-required", `${home} has no captured executionBinding; current configuration was not used`);
171
+ if (binding.schemaVersion !== 1 || typeof binding.deployment !== "string" || !isAbsolute(binding.deployment)
172
+ || realOrResolved(binding.deployment) !== realOrResolved(selector.deployment)
173
+ || binding.resolution?.schemaVersion !== 1 || binding.resolution.id !== selector.resolution.id) {
174
+ bail("E_HOME_MISMATCH", `${home} is not bound to captured resolution ${selector.resolution.id} in ${selector.deployment}`);
175
+ }
176
+ }
177
+ // Inspect is static: validate target and arguments before the action load runs
178
+ // the provider's mutable readiness check.
179
+ const inspected = load({ kind: "inspect" }), providerId = inspected.record.bindings[slot]?.capability;
180
+ const provider = providerId ? inspected.manifests.get(providerId) : undefined;
181
+ if (!provider) bail("capability-not-selected", `no captured ${slot} provider is selected`);
182
+ const operation = manifestOperations(provider).find((entry) => entry.name === name);
183
+ if (!operation) bail("operation-not-found", "captured provider does not declare this operation");
184
+ if (operation.context === "home" && !meta) bail("E_OPERATION_UNAVAILABLE", `${address} runs in an instance home; pass --home <abs>`);
185
+ if (operation.context === "scope" && meta) bail("E_BAD_ARGS", `${address} is a scope operation and does not accept --home`);
186
+ const declared = new Map(operation.args.map((entry) => [entry.name, entry]));
187
+ for (const key of Object.keys(given)) if (!declared.has(key)) bail("E_BAD_ARGS", `${address} takes no arg ${JSON.stringify(key)} (declared: ${[...declared.keys()].join(", ") || "none"})`);
188
+ for (const entry of operation.args) if (entry.required && given[entry.name] === undefined) bail("E_BAD_ARGS", `${address} needs --arg ${entry.name}=<value>: ${entry.description || "required"}`);
189
+ const action = { kind: "operation", slot, name };
190
+ const argFlags = operation.args.flatMap((entry) => given[entry.name] === undefined ? [] : [entry.flag, given[entry.name]]), cwd = home || selector.deployment;
191
+ if (operation.kind === "action" && !home) bail("admission-required", "scope mutation has no qualified incarnation/admission path; use an instance-scoped operation");
192
+ if (retryExecutionId !== undefined && operation.kind !== "action") bail("E_BAD_ARGS", "read-only operations do not retry mutation intents");
193
+ const admission = operation.kind === "action" ? admitCapturedAction({ deployment: selector.deployment, resolution: selector.resolution, home, action, input: { arguments: argFlags },
194
+ ...(retryExecutionId !== undefined ? { retryExecutionId } : {}) }) : null;
195
+ let settlement;
196
+ const admittedBail = (code, message, details) => bail(code, message, { ...details, ...(admission ? { intent: admission.intent } : {}),
197
+ ...(settlement ? { settlement, unconfirmed: settlement.state === "unconfirmed" } : {}) });
198
+ if (admission?.replayed) {
199
+ settlement = { state: "completed", receipt: admission.receipt };
200
+ if (!admission.replayable) admittedBail("needs-configuration", "completed operation cannot replay its retained outcome");
201
+ finishOperation({ r: { status: 0, stdout: JSON.stringify(admission.receipt) }, bail: admittedBail, address, provider, op: operation, argFlags, cwd, home, meta, intent: admission.intent }); return;
202
+ }
203
+ let loaded;
204
+ try { loaded = load(action, { invocationTarget: meta ? { home, work: join(home, "work"), name: meta.instance, agent: meta.agent } : null,
205
+ ...(admission ? { intent: admission.intent, priorReceipt: admission.receipt } : {}) }); }
206
+ catch (error) {
207
+ if (admission) {
208
+ settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state: "blocked", receipt: admission.receipt });
209
+ settlement = { state: "blocked", receipt: admission.receipt };
210
+ }
211
+ admittedBail(error.code || "provider-unavailable", error.message);
212
+ }
213
+ const { capability, executable } = loaded;
214
+ const env = { ...process.env };
215
+ for (const key of Object.keys(env)) if (key.startsWith("OATS_") || key.startsWith("PI_AGENT_") || key === "PI_AGENTS_ROOT") delete env[key];
216
+ Object.assign(env, {
217
+ OATS_DEPLOYMENT: selector.deployment, OATS_RESOLUTION: selector.resolution.id,
218
+ OATS_CAPABILITY: capability.id, OATS_CAPABILITY_ROOT: capability.manifest._dir,
219
+ OATS_SETTINGS: JSON.stringify(capability.settings), OATS_CLI_BIN: CLI_BIN,
220
+ OATS_OPERATION: address, OATS_CONTEXT: selector.deployment, OATS_LEVEL: selector.deployment,
221
+ OATS_WORKSPACE: selector.deployment,
222
+ });
223
+ if (meta) Object.assign(env, { OATS_INSTANCE: meta.instance, OATS_INSTANCE_HOME: home, OATS_HOME: home,
224
+ PI_AGENT_INSTANCE: meta.instance, PI_AGENT_HOME: home, ...(meta.agent ? { OATS_AGENT: meta.agent } : {}) });
225
+ const invocation = loaded.invocation;
226
+ let child, cleanupError, started = false;
227
+ try {
228
+ child = withCapturedInvocationContextFile(invocation, contextEnv => withCapturedBindingFile(loaded, bindingEnv => {
229
+ if (admission) { beginCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action }); started = true; }
230
+ return runCapturedOperationProcess({ file: executable.file, args: [...executable.args, ...argFlags, "--json"], cwd, env: { ...env, ...contextEnv, ...bindingEnv } });
231
+ }));
232
+ } catch (error) {
233
+ if (!error?.invocationCompleted) {
234
+ if (admission) {
235
+ settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state: started ? "unconfirmed" : "blocked", receipt: admission.receipt });
236
+ settlement = { state: started ? "unconfirmed" : "blocked", receipt: admission.receipt };
237
+ }
238
+ admittedBail(error.code || "E_OPERATION_RESULT", error.message, { unconfirmed: started });
239
+ }
240
+ child = error.invocationResult; cleanupError = error;
241
+ }
242
+ if (admission) {
243
+ let envelope; try { envelope = JSON.parse(String(child.stdout || "").trim()); } catch { /* unconfirmed below */ }
244
+ const completed = !cleanupError && !child.error && child.status === 0 && envelope?.schemaVersion === 1 && envelope.ok === true;
245
+ const state = completed ? "completed" : "unconfirmed", receipt = envelope ?? parseEnvelopeText(String(child.stdout || "")) ?? admission.receipt;
246
+ try {
247
+ settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state, receipt, replayable: completed });
248
+ settlement = { state, receipt };
249
+ }
250
+ catch (error) { admittedBail("E_OPERATION_RESULT", "operation ran but outcome custody could not be confirmed", { unconfirmed: true, envelope, custody: { code: error.code, message: error.message } }); }
251
+ }
252
+ finishOperation({ r: child, bail: admittedBail, address, provider: capability.manifest, op: operation, argFlags, cwd, home, meta, cleanupError, settlement, ...(admission ? { intent: admission.intent } : {}) });
253
+ }
254
+
255
+ /** Fresh explicit captured scaffold + spawn hooks. Placement is supplied by
256
+ * the operator; launch and non-directory work remain unsupported. */
257
+ function capturedSpawn(selector, load, bail) {
258
+ const subject = args[1]; let home; let noLaunch = false;
259
+ if (!subject || subject.startsWith("-")) bail("E_BAD_ARGS", "captured spawn needs the retained subject name");
260
+ for (let index = 2; index < args.length; index++) {
261
+ const token = args[index];
262
+ if (token === "--json") continue;
263
+ if (token === "--no-launch") { if (noLaunch) bail("E_BAD_ARGS", "duplicate --no-launch"); noLaunch = true; continue; }
264
+ if (token === "--home") {
265
+ const value = args[++index];
266
+ if (home !== undefined || !value || value.startsWith("--") || !isAbsolute(value)) bail("E_BAD_ARGS", "--home needs one absolute new instance home");
267
+ home = resolve(value); continue;
268
+ }
269
+ bail("E_BAD_ARGS", `unsupported captured spawn argument ${JSON.stringify(token)}`);
270
+ }
271
+ if (!home || !noLaunch) bail("E_BAD_ARGS", "captured spawn currently requires --home <absolute new home> and --no-launch");
272
+ const inspected = load({ kind: "inspect" }), expected = inspected.record.subject.kind === "persistent" ? inspected.record.subject.soul.alias : inspected.record.subject.name;
273
+ if (subject !== expected) bail("E_HOME_MISMATCH", `captured resolution subject is ${expected}, not ${subject}`);
274
+ const scaffold = scaffoldCapturedInstance({ deployment: selector.deployment, resolution: selector.resolution, home, instance: basename(home) });
275
+ let activated;
276
+ try {
277
+ activated = activateCapturedScaffold({ deployment: selector.deployment, resolution: selector.resolution, home,
278
+ extraEnv: process.env.OATS_HOME_DIR ? { OATS_HOME_DIR: process.env.OATS_HOME_DIR } : {} });
279
+ } catch (error) {
280
+ if (error?.home) bail(error.code || "E_SPAWN_FAILED", error.message, { home: error.home, cleanupRequired: true, failures: error.provenance || [],
281
+ ...(error.capturedCustody ? { unconfirmed: true, custody: error.capturedCustody } : {}) });
282
+ throw error;
283
+ }
284
+ const result = { ...scaffold, hooksPending: activated.hooksPending, cleanupRequired: activated.cleanupRequired, launchPending: true,
285
+ hookIntents: activated.hooks.intents, hookOrder: activated.hooks.order, warnings: activated.hooks.warnings };
286
+ if (JSON_MODE) jsonOk(result); else console.log(`Scaffolded ${result.instance} at ${result.home}; ${result.hooksPending ? "captured hook custody requires retry/reconciliation" : "captured hooks complete"}, launch pending`);
287
+ }
288
+
289
+ /** Native continuation of an already owned captured home. A helper selector is
290
+ * an exact edge from the SOURCE record, not a name/current-config resolver. */
291
+ function capturedSession(selector, bail) {
292
+ const inspecting = args[1] === "inspect";
293
+ if (!["start", "restart", "inspect"].includes(args[1])) bail("unsupported-action", "captured session supports start/restart or explicit Pi outcome inspect; no current-context fallback was used");
294
+ const values = new Map(), allowed = new Set(inspecting ? ["home", "helper", "native-record"] : ["home", "helper", "request", "retry-intent"]);
295
+ for (let index = 2; index < args.length; index++) {
296
+ if (args[index] === "--json") continue;
297
+ const key = args[index].startsWith("--") ? args[index].slice(2) : "", value = args[index + 1];
298
+ if (!allowed.has(key) || values.has(key) || !value || value.startsWith("--")) bail("E_BAD_ARGS", "captured session needs unique named home/helper/request/retry arguments");
299
+ values.set(key, value); index++;
300
+ }
301
+ const home = values.get("home"), retry = values.get("retry-intent");
302
+ if (!home || !isAbsolute(home) || resolve(home) !== home || home.includes("\0")) bail("E_BAD_ARGS", "captured session needs a normalized absolute --home");
303
+ if (retry !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(retry)) bail("E_BAD_ARGS", "--retry-intent requires one saved executionId");
304
+ if (inspecting && !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(values.get("native-record") ?? "")) bail("E_BAD_ARGS", "captured Pi outcome inspect requires one explicit --native-record UUID");
305
+ const sourceExecutionBinding = { schemaVersion: 1, deployment: portableScope(selector.deployment), resolution: selector.resolution };
306
+ // Pure retained-subject check, before request files, provider or native calls.
307
+ // Dedicated helper IDs remain valid for scaffolding, not edge-less dispatch.
308
+ if (!values.has("helper") && readCapturedResolution(sourceExecutionBinding.deployment, sourceExecutionBinding.resolution).subject.kind === "helper") {
309
+ bail("helper-not-selected", "captured helper session needs SOURCE selectors plus --helper EXACT_SOURCE_HELPER_KEY");
310
+ }
311
+ // Reuse the same bounded strict object-file transport. Preparation and native
312
+ // request schemas remain separate; reject unknown fields before projection.
313
+ let request = {};
314
+ if (values.has("request")) {
315
+ const input = readPortablePreparationRequest({ file: values.get("request") });
316
+ if (input.schemaVersion !== 1 || Object.keys(input).some(key => !["schemaVersion", "backend", "task", "stopGraceMs"].includes(key))) bail("E_BAD_ARGS", "native request must be version1 with only backend/task/stopGraceMs");
317
+ if (Object.hasOwn(input, "backend") && (!input.backend || typeof input.backend !== "object" || Array.isArray(input.backend))) bail("E_BAD_ARGS", "a supplied native backend must be an explicit object, not omission");
318
+ const { schemaVersion, ...fields } = input; request = fields;
319
+ }
320
+ const helperSelection = values.has("helper") ? resolveCapturedHelper({ executionBinding: sourceExecutionBinding, helper: values.get("helper") }) : null;
321
+ const executionBinding = helperSelection?.executionBinding ?? sourceExecutionBinding;
322
+ try {
323
+ if (inspecting) {
324
+ const outcome = inspectCapturedPiOutcome(home, { ...executionBinding, nativeRecordId: values.get("native-record") });
325
+ const result = { outcome, executionBinding, ...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}) };
326
+ if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
327
+ return;
328
+ }
329
+ const native = startCapturedInstanceSession(home, { ...request, deployment: executionBinding.deployment, resolution: executionBinding.resolution,
330
+ restart: args[1] === "restart", ...(retry !== undefined ? { retryExecutionId: retry } : {}) });
331
+ const result = { ...native, executionBinding, ...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}) };
332
+ if (JSON_MODE) jsonOk(result); else console.log(`${result.replayed ? "Replayed captured dispatch receipt for" : "Dispatched captured native session for"} ${home}`);
333
+ } catch (error) {
334
+ bail(error.code || "E_SESSION_START_FAILED", error.message, { home: error.home ?? home, executionBinding,
335
+ ...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}),
336
+ ...(error.capturedCustody ? { custody: error.capturedCustody } : {}),
337
+ ...(error.nativeCustody ? { unconfirmed: true, nativeCustody: error.nativeCustody } : {}) });
338
+ }
339
+ }
340
+
341
+ /** Exact-selector dispatch enters before any current-context resolver. Its
342
+ * child receives the same selector, never an invoking agent's ambient identity. */
343
+ function capturedCommand(selector) {
344
+ const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
345
+ try {
346
+ const end = args.indexOf("--"), head = end < 0 ? args : args.slice(0, end);
347
+ const permitsHome = cmd === "operation" || cmd === "spawn" || cmd === "session";
348
+ const forbiddenContext = permitsHome ? ["--dir", "--server", "--soul", "--agents-root"] : ["--dir", "--home", "--server", "--soul", "--agents-root"];
349
+ if (head.some((arg) => forbiddenContext.includes(arg.split("=")[0]))) {
350
+ fail("E_BAD_ARGS", "captured selectors cannot be mixed with current-context selectors");
351
+ }
352
+ if (selector.artifactSet !== undefined) {
353
+ if (cmd !== "trust" || !args[1] || args[1].startsWith("-") || args.slice(2).some((arg) => arg !== "--json")) fail("E_BAD_ARGS", "artifact-set selectors support only explicit trust of one capability");
354
+ const result = approveAvailableCapability(selector.deployment, selector.artifactSet, args[1], {
355
+ kind: "operator", document: { kind: "operator", id: "oats-trust-artifact-set" }, pointer: "/capability",
356
+ });
357
+ if (JSON_MODE) jsonOk(result); else console.log(`${args[1]}: ${result.status}`);
358
+ return;
359
+ }
360
+ const target = { deployment: selector.deployment, resolution: selector.resolution };
361
+ const load = (action, extra = {}) => loadCapturedDispatch({ ...target, action, ...extra });
362
+ if (cmd === "inspect") {
363
+ let helperKey;
364
+ for (let index = 1; index < args.length; index++) {
365
+ if (["--json", "--composition"].includes(args[index])) continue;
366
+ if (args[index] !== "--helper" || helperKey !== undefined || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "captured inspect accepts --json, --composition and one --helper <exact-map-key>");
367
+ helperKey = args[++index];
368
+ }
369
+ const helperSelection = helperKey === undefined ? null : resolveCapturedHelper({ executionBinding: { schemaVersion: 1, ...target }, helper: helperKey });
370
+ const selected = helperSelection?.executionBinding ?? target;
371
+ const loaded = loadCapturedDispatch({ deployment: selected.deployment, resolution: selected.resolution, action: { kind: args.includes("--composition") ? "compose" : "inspect" } });
372
+ const result = { resolution: loaded.resolution, capture: loaded.record.capture, nativeSession: capturedNativeSessionAvailability(),
373
+ ...(helperSelection ? { helperSelection } : {}),
374
+ capabilities: [...loaded.capabilities.values()].map(({ id, manifest }) => ({ id, version: manifest.version,
375
+ approval: loaded.approvals.find((entry) => entry.artifact.capability === id).status })),
376
+ helpers: Object.entries(loaded.record.helpers).map(([key, resolution]) => ({ key, resolution })),
377
+ hasComposition: !!loaded.record.dispatch.composition,
378
+ ...(loaded.composition ? { composition: loaded.composition } : {}) };
379
+ if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
380
+ return;
381
+ }
382
+ if (cmd === "trust") {
383
+ if (!args[1] || args[1].startsWith("-") || args.slice(2).some((arg) => arg !== "--json")) fail("E_BAD_ARGS", "captured trust needs one capability ID");
384
+ load({ kind: "inspect" }); // complete manifest validation before approval
385
+ const result = approveCapturedCapability(selector.deployment, selector.resolution, args[1], {
386
+ kind: "operator", document: { kind: "operator", id: "oats-trust" }, pointer: "/capability",
387
+ });
388
+ if (JSON_MODE) jsonOk(result); else console.log(`${args[1]}: ${result.status}`);
389
+ return;
390
+ }
391
+ if (cmd === "operation") { capturedOperation(selector, load, fail); return; }
392
+ if (cmd === "spawn") { capturedSpawn(selector, load, fail); return; }
393
+ if (cmd === "session") { capturedSession(selector, fail); return; }
394
+ if (!cmd || cmd.startsWith("-") || KERNEL_COMMANDS.has(cmd)) fail("unsupported-action", "this kernel command has not yet adopted captured selectors; no current-context fallback was used");
395
+ if (!args[1] || args[1] === "--json" || head.some((arg) => HELP_WORDS.has(arg))) {
396
+ const loaded = load({ kind: "inspect" });
397
+ const matches = [...loaded.manifests.values()].filter((manifest) => manifest.command === cmd);
398
+ if (matches.length !== 1) fail("capability-not-selected", "captured command namespace is absent or ambiguous");
399
+ const result = { capability: matches[0].capability, commands: Object.keys(matches[0].commands || {}), help: "manifest only; no executable ran" };
400
+ if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
401
+ return;
402
+ }
403
+ const loaded = load({ kind: "command", namespace: cmd, name: args[1] });
404
+ const env = { ...process.env };
405
+ for (const key of Object.keys(env)) if (key.startsWith("OATS_") || key.startsWith("PI_AGENT_") || key === "PI_AGENTS_ROOT") delete env[key];
406
+ Object.assign(env, { OATS_DEPLOYMENT: selector.deployment, OATS_RESOLUTION: selector.resolution.id,
407
+ OATS_CAPABILITY: loaded.capability.id, OATS_CAPABILITY_ROOT: loaded.capability.manifest._dir,
408
+ OATS_SETTINGS: JSON.stringify(loaded.capability.settings),
409
+ OATS_CLI_BIN: CLI_BIN, OATS_CONTEXT: selector.deployment, OATS_LEVEL: selector.deployment });
410
+ const forwarded = args.slice(2); if (forwarded[0] === "--") forwarded.shift();
411
+ const child = withCapturedInvocationContextFile(loaded.invocation, contextEnv => withCapturedBindingFile(loaded, bindingEnv => spawnSync(process.execPath, [loaded.executable.file, ...loaded.executable.args, ...forwarded], {
412
+ cwd: selector.deployment, env: { ...env, ...contextEnv, ...bindingEnv }, stdio: "inherit",
413
+ })));
414
+ if (child.error) fail("E_CAPABILITY_BROKEN", child.error.message);
415
+ process.exit(child.status ?? 1);
416
+ } catch (error) { fail(error.code || "E_CAPABILITY_BROKEN", error.message); }
417
+ }
418
+
89
419
  /** Level of a directory: laptop (home), repo (.git), else workspace. */
90
420
  function levelOf(dir) {
91
421
  const d = resolve(dir);
@@ -637,7 +967,55 @@ const OPERATION_ADDRESS_RE = /^(knowledge|messaging|tasks):([a-z][a-z0-9-]*)$/;
637
967
  const reportsRetainedEffectsText = (message) => /INCOMPLETE|quarantin|retain|could not (?:be )?(?:verif|confirm)/i.test(String(message || ""));
638
968
  // Comfortably below the scheduler's 5-minute command bound and any GUI
639
969
  // proxy, so the receipt always reaches the caller before a wrapper gives up.
640
- const OPERATION_TIMEOUT_MS = 4 * 60 * 1000;
970
+ const OPERATION_TIMEOUT_MS = CAPTURED_OPERATION_TIMEOUT_MS;
971
+ function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home, meta, cleanupError, intent, settlement }) {
972
+ const stderr = String(r.stderr || "").trim();
973
+ const timedOut = r.error?.code === "ETIMEDOUT" || (r.status === null && ["SIGTERM", "SIGKILL"].includes(r.signal) && (!settlement || !r.error));
974
+ const base = { operation: address, capability: provider.capability, version: provider.version || null, argv: [provider.command, op.command, ...argFlags], cwd, target: home ? { home, instance: meta.instance } : null };
975
+ // Unconfirmed outcomes (a timeout, no valid receipt, a receipt contradicted
976
+ // by the exit status) carry what WAS observed in error.details, so a
977
+ // scheduler can keep the slot as unknown and reconcile by any name the
978
+ // provider managed to answer; they are never confirmed failures.
979
+ const observed = (envelope) => ({ exit: r.status, signal: r.signal || null, unconfirmed: true, ...(envelope && typeof envelope === "object" ? { envelope } : {}),
980
+ ...(stderr ? { stderr: stderr.slice(0, 2000) } : {}), ...(cleanupError ? { cleanup: { code: cleanupError.code || "E_OPERATION_CLEANUP", message: String(cleanupError.message || cleanupError).slice(0, 1000) } } : {}) });
981
+ if (r.error && !timedOut) bail("E_CAPABILITY_BROKEN", `${address}: ${r.error.message || r.error}`, settlement ? observed(parseEnvelopeText(String(r.stdout || ""))) : undefined);
982
+ if (timedOut) bail("E_OPERATION_TIMEOUT", `${address} (${provider.capability} ${op.command}) did not finish within ${OPERATION_TIMEOUT_MS / 1000} s; its effects are unconfirmed`, observed(parseEnvelopeText(String(r.stdout || ""))));
983
+ // Exactly one JSON-v1 envelope on stdout, nothing else, and an exit status
984
+ // that agrees with it: contaminated output or a success envelope from a
985
+ // process that then failed is not a receipt.
986
+ let envelope;
987
+ try { envelope = JSON.parse(String(r.stdout || "").trim()); } catch { envelope = undefined; }
988
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope) || envelope.schemaVersion !== 1 || typeof envelope.ok !== "boolean") bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) did not answer exactly one JSON-v1 envelope on stdout (exit ${r.status}); its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(parseEnvelopeText(String(r.stdout || ""))));
989
+ if (cleanupError) bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) answered, but private invocation cleanup could not be confirmed; its effects are unconfirmed`, observed(envelope));
990
+ // A provider's own failure is relayed with its code; its WHOLE envelope
991
+ // (a partial receipt such as result.instance of something it launched
992
+ // before failing, and any details it gave) travels in error.details so a
993
+ // scheduler can keep an unconfirmed outcome and reconcile that target.
994
+ if (!envelope.ok) bail(envelope.error?.code || "E_OPERATION_FAILED", `${address}: ${envelope.error?.message || "failed"}`, { exit: r.status, envelope, ...(reportsRetainedEffectsText(envelope.error?.message) ? { unconfirmed: true } : {}) });
995
+ if (r.status !== 0) bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) answered ok but exited ${r.status}; the receipt is not trusted and its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(envelope));
996
+ const result = envelope.result && typeof envelope.result === "object" ? envelope.result : {};
997
+ if (op.kind === "view") {
998
+ const docs = result.documents;
999
+ const bad = !Array.isArray(docs) || docs.some((d) => !d || typeof d !== "object" || typeof d.label !== "string" || !d.label
1000
+ || (d.kind !== undefined && !["markdown", "text"].includes(d.kind))
1001
+ || (d.path !== undefined && d.path !== null && (typeof d.path !== "string" || !isAbsolute(d.path)))
1002
+ || (d.text !== undefined && d.text !== null && typeof d.text !== "string"));
1003
+ if (bad) bail("E_OPERATION_RESULT", `${address} is a view operation but ${provider.capability} ${op.command} did not answer { documents: [{label, kind?, path?, text?}] }`);
1004
+ }
1005
+ // A launch receipt in the delegated result (a harvester the provider
1006
+ // spawned) is surfaced as top-level instance/home so the scheduler tracks
1007
+ // it exactly as it tracks a command job's launch; the source home itself
1008
+ // is `target`, never a launch.
1009
+ const receipt = {};
1010
+ if (typeof result.instance === "string" && result.instance !== meta?.instance) receipt.instance = result.instance;
1011
+ if (typeof result.home === "string" && result.home !== home) receipt.home = result.home;
1012
+ const out = { ...base, ...receipt, result, ...(intent ? { intent } : {}), ...(stderr ? { stderr: stderr.slice(0, 2000) } : {}) };
1013
+ if (JSON_MODE) { jsonOk(out); return; }
1014
+ console.log(`${address} via ${provider.capability}@${provider.version || "?"} (${provider.command} ${op.command}) in ${shortPath(cwd)}: ok`);
1015
+ if (op.kind === "view") for (const d of result.documents) console.log(` - ${d.label}${d.path ? ` (${shortPath(d.path)})` : ""}${d.text ? `: ${String(d.text).split("\n")[0].slice(0, 100)}` : ""}`);
1016
+ else console.log(JSON.stringify(result, null, 2));
1017
+ if (stderr) console.error(stderr);
1018
+ }
641
1019
  function operationCmd() {
642
1020
  const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
643
1021
  dropAmbientRoot();
@@ -754,50 +1132,7 @@ function operationCmd() {
754
1132
  });
755
1133
  if (op.context === "home") Object.assign(env, { OATS_INSTANCE: meta.instance, OATS_INSTANCE_HOME: home, OATS_HOME: home, PI_AGENT_INSTANCE: meta.instance, PI_AGENT_HOME: home });
756
1134
  const r = spawnSync("node", [abs, ...rest, ...argFlags, "--json"], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024, timeout: OPERATION_TIMEOUT_MS, killSignal: "SIGTERM" });
757
- const stderr = String(r.stderr || "").trim();
758
- const timedOut = r.error?.code === "ETIMEDOUT" || (r.status === null && r.signal === "SIGTERM");
759
- if (r.error && !timedOut) bail("E_CAPABILITY_BROKEN", `${address}: ${r.error.message || r.error}`);
760
- const base = { operation: address, capability: provider.capability, version: provider.version || null, argv: [provider.command, op.command, ...argFlags], cwd, target: home ? { home, instance: meta.instance } : null };
761
- // Unconfirmed outcomes (a timeout, no valid receipt, a receipt contradicted
762
- // by the exit status) carry what WAS observed in error.details, so a
763
- // scheduler can keep the slot as unknown and reconcile by any name the
764
- // provider managed to answer; they are never confirmed failures.
765
- const observed = (envelope) => ({ exit: r.status, signal: r.signal || null, unconfirmed: true, ...(envelope && typeof envelope === "object" ? { envelope } : {}), ...(stderr ? { stderr: stderr.slice(0, 2000) } : {}) });
766
- if (timedOut) bail("E_OPERATION_TIMEOUT", `${address} (${provider.capability} ${op.command}) did not finish within ${OPERATION_TIMEOUT_MS / 1000} s; its effects are unconfirmed`, observed(parseEnvelopeText(String(r.stdout || ""))));
767
- // Exactly one JSON-v1 envelope on stdout, nothing else, and an exit status
768
- // that agrees with it: contaminated output or a success envelope from a
769
- // process that then failed is not a receipt.
770
- let envelope;
771
- try { envelope = JSON.parse(String(r.stdout || "").trim()); } catch { envelope = undefined; }
772
- if (!envelope || typeof envelope !== "object" || Array.isArray(envelope) || envelope.schemaVersion !== 1 || typeof envelope.ok !== "boolean") bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) did not answer exactly one JSON-v1 envelope on stdout (exit ${r.status}); its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(parseEnvelopeText(String(r.stdout || ""))));
773
- // A provider's own failure is relayed with its code; its WHOLE envelope
774
- // (a partial receipt such as result.instance of something it launched
775
- // before failing, and any details it gave) travels in error.details so a
776
- // scheduler can keep an unconfirmed outcome and reconcile that target.
777
- if (!envelope.ok) bail(envelope.error?.code || "E_OPERATION_FAILED", `${address}: ${envelope.error?.message || "failed"}`, { exit: r.status, envelope, ...(reportsRetainedEffectsText(envelope.error?.message) ? { unconfirmed: true } : {}) });
778
- if (r.status !== 0) bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) answered ok but exited ${r.status}; the receipt is not trusted and its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(envelope));
779
- const result = envelope.result && typeof envelope.result === "object" ? envelope.result : {};
780
- if (op.kind === "view") {
781
- const docs = result.documents;
782
- const bad = !Array.isArray(docs) || docs.some((d) => !d || typeof d !== "object" || typeof d.label !== "string" || !d.label
783
- || (d.kind !== undefined && !["markdown", "text"].includes(d.kind))
784
- || (d.path !== undefined && d.path !== null && (typeof d.path !== "string" || !isAbsolute(d.path)))
785
- || (d.text !== undefined && d.text !== null && typeof d.text !== "string"));
786
- if (bad) bail("E_OPERATION_RESULT", `${address} is a view operation but ${provider.capability} ${op.command} did not answer { documents: [{label, kind?, path?, text?}] }`);
787
- }
788
- // A launch receipt in the delegated result (a harvester the provider
789
- // spawned) is surfaced as top-level instance/home so the scheduler tracks
790
- // it exactly as it tracks a command job's launch; the source home itself
791
- // is `target`, never a launch.
792
- const receipt = {};
793
- if (typeof result.instance === "string" && result.instance !== meta?.instance) receipt.instance = result.instance;
794
- if (typeof result.home === "string" && result.home !== home) receipt.home = result.home;
795
- const out = { ...base, ...receipt, result, ...(stderr ? { stderr: stderr.slice(0, 2000) } : {}) };
796
- if (JSON_MODE) { jsonOk(out); return; }
797
- console.log(`${address} via ${provider.capability}@${provider.version || "?"} (${provider.command} ${op.command}) in ${shortPath(cwd)}: ok`);
798
- if (op.kind === "view") for (const d of result.documents) console.log(` - ${d.label}${d.path ? ` (${shortPath(d.path)})` : ""}${d.text ? `: ${String(d.text).split("\n")[0].slice(0, 100)}` : ""}`);
799
- else console.log(JSON.stringify(result, null, 2));
800
- if (stderr) console.error(stderr);
1135
+ finishOperation({ r, bail, address, provider, op, argFlags, cwd, home, meta });
801
1136
  }
802
1137
 
803
1138
  // ---------- soul set: runtime defaults and instructions of an editable soul ----------
@@ -3822,6 +4157,7 @@ function scheduleCmd() {
3822
4157
 
3823
4158
  async function sessionCmd() {
3824
4159
  try {
4160
+ if (flag("native-record") !== undefined) throw Object.assign(new Error("--native-record outcome inspection requires exact captured deployment/resolution selectors"), { code: "E_BAD_ARGS" });
3825
4161
  const home = flag("home");
3826
4162
  let result;
3827
4163
  if (args[1] === "attach") {
@@ -4158,7 +4494,7 @@ function versionCmd() {
4158
4494
  // on it (an older CLI without the surface must fail closed with a
4159
4495
  // reason, not an argument error). `features`: kernel abilities a peer
4160
4496
  // must see before relying on them (retire-home: retire --home).
4161
- console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations"], scheduleApi: SCHEDULE_API, operationsApi: 1 }));
4497
+ console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations"], scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
4162
4498
  return;
4163
4499
  }
4164
4500
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -4523,11 +4859,38 @@ async function serverRouteCmd() {
4523
4859
  // blame` pointing at the commit that last changed each command.
4524
4860
  const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
4525
4861
  try {
4862
+ // Inspect explicit selectors with the existing parser before new-work routing,
4863
+ // including selectors before the command. Inherited captures are not prepare inputs.
4864
+ let captured;
4865
+ try { captured = capturedSelector(args, {}); }
4866
+ catch (error) {
4867
+ if (JSON_MODE) jsonFail(error.code || "E_BAD_ARGS", error.message);
4868
+ die(error.message);
4869
+ }
4870
+ if (cmd === "prepare" || captured?.args[0] === "prepare") {
4871
+ if (captured) {
4872
+ if (JSON_MODE) jsonFail("E_BAD_ARGS", "prepare is explicit new work and cannot use captured selectors");
4873
+ die("prepare is explicit new work and cannot use captured selectors");
4874
+ }
4875
+ if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
4876
+ prepareCmd(); process.exit(0);
4877
+ }
4878
+ // Other commands retain their existing explicit/inherited selection rules.
4879
+ try { captured ??= capturedSelector(args); }
4880
+ catch (error) {
4881
+ if (JSON_MODE) jsonFail(error.code || "E_BAD_ARGS", error.message);
4882
+ die(error.message);
4883
+ }
4884
+ if (captured) {
4885
+ args.splice(0, args.length, ...captured.args); cmd = args[0];
4886
+ // Host protocol negotiation describes this executable, not a mutable
4887
+ // configuration. An inherited capture must not break `oats version` probes.
4888
+ if (captured.explicit || cmd !== "version") { capturedCommand(captured); process.exit(0); }
4889
+ }
4526
4890
  // `--help`/`-h` anywhere after a kernel command prints that command's usage
4527
4891
  // and exits 0 BEFORE any dispatch: a fresh operator inspects --help before
4528
4892
  // using a command, and `install --help` once ran the bare restore while
4529
4893
  // `okf harvest --help` spawned a harvester (BeadHub, 2026-09-05).
4530
- const KERNEL_COMMANDS = new Set(["capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
4531
4894
  const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
4532
4895
  if (cmd && KERNEL_COMMANDS.has(cmd) && wantsHelp) { if (JSON_MODE) { jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); process.exit(0); } usageFor(cmd); process.exit(0); }
4533
4896
  if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "use", "soul", "launch-config"].includes(cmd)) await serverRouteCmd();
@@ -4821,6 +5184,34 @@ The turn record (core — every conversation captured, searchable, replicated):
4821
5184
  design, repo checkout only; see
4822
5185
  packages/experimental/README.md
4823
5186
 
5187
+ oats prepare --request <absolute-json-file> [--json]
5188
+ complete public preparation input; no mixed flags,
5189
+ inherited binding, implicit setup or launch authority
5190
+ oats prepare --dir <abs> --source <git repo> --revision <ref> --export <path>
5191
+ --alias <name> [--work <mode>] [--json] prepare retained commands and curriculum,
5192
+ no launch; provider gaps report incomplete
5193
+ oats prepare --dir <abs> --workspace <git repo> --alias <advertised alias>
5194
+ [--workspace-revision <ref>] [--work <mode>] [--json]
5195
+ same preparation through workspace imports
5196
+ oats inspect --deployment <abs> --resolution <id> [--composition] [--json]
5197
+ [--helper <exact-map-key>] inspect retained source/helper inputs, not today's configuration
5198
+ oats trust <capability> --deployment <abs> --resolution <id> [--json]
5199
+ explicitly approve that exact captured artifact
5200
+ oats <namespace> <command> --deployment <abs> --resolution <id> -- [args…]
5201
+ run the approved retained command; no ambient fallback
5202
+ oats operation run <layer>:<name> --deployment <abs> --resolution <id>
5203
+ [--home <abs>] [--arg k=v ...] [--retry-intent <saved-id>] [--json]
5204
+ home actions admit distinct requests; explicit retry reuses intent
5205
+ scope views stay read-only; scope mutation is not yet qualified
5206
+ oats spawn <captured subject> --deployment <abs> --resolution <id>
5207
+ --home <abs> --no-launch [--json] create a fresh directory scaffold and run captured hooks
5208
+ oats session inspect --deployment <abs> --resolution <id> --home <abs> --native-record <UUID> [--helper <exact-key>] --json
5209
+ read-only Pi completion observation; not admission or readiness
5210
+ oats session start|restart --deployment <abs> --resolution <id> --home <abs>
5211
+ [--helper <exact-source-map-key>] [--request <abs-json>] [--retry-intent <saved-id>] [--json]
5212
+ dispatch the owned captured home via existing native custody;
5213
+ version1 request: backend/task/stopGraceMs, no model override
5214
+
4824
5215
  oats <namespace> <command> [args…] run an operational command only when its
4825
5216
  capability is active (e.g. oats okf harvest)
4826
5217
 
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { runBindingWire } from '../lib/binding-wire.mjs';
3
+
4
+ const args=process.argv.slice(2);
5
+ if(args.includes('--help') || args.includes('-h')) {
6
+ process.stdout.write('oats okf provider binding phase (manifest-owned JSON stdin/stdout)\n');
7
+ } else {
8
+ const phase=args[0];
9
+ if(args.length!==1) {
10
+ await runBindingWire(phase,[],process.stdout);
11
+ } else {
12
+ await runBindingWire(phase);
13
+ }
14
+ }