@awebai/oats 0.24.2 → 0.24.3

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.
package/bin/oats.mjs CHANGED
@@ -49,6 +49,10 @@ import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_
49
49
  import { capturedSelector } from "../lib/captured-selector.mjs";
50
50
  import { inspectCapturedPiOutcome } from "../lib/captured-pi-host.mjs";
51
51
  import { readCapturedResolution } from "../lib/captured-resolutions.mjs";
52
+ import { readLock3 } from "../lib/portable-lock.mjs";
53
+ import { oatsError } from "../lib/errors.mjs";
54
+ import { readPortableBytes } from "../lib/portable-files.mjs";
55
+ import { canonicalJson, parseStrictJson } from "../lib/portable-values.mjs";
52
56
  import { readPortablePreparationRequest } from "../lib/portable-onboarding-request.mjs";
53
57
  import { portableScope } from "../lib/portable-state.mjs";
54
58
  import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
@@ -99,15 +103,40 @@ const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok:
99
103
 
100
104
  function inspectOnboardingCmd() {
101
105
  const fail = (code, message) => JSON_MODE ? jsonFail(code, message) : die(message);
102
- let file;
106
+ const values = new Map();
103
107
  for (let index = 1; index < args.length; index++) {
104
108
  if (args[index] === "--json") continue;
105
- if (args[index] !== "--request" || file !== undefined || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "source inspection accepts one --request <absolute-json> and --json only");
106
- file = args[++index];
109
+ const key = args[index];
110
+ if (!["--request", "--emit-prepare-request"].includes(key) || values.has(key) || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "source inspection accepts one --request <absolute-json>, optional --emit-prepare-request <new-absolute-json>, and --json");
111
+ values.set(key, args[++index]);
107
112
  }
108
113
  try {
109
- const input = readPortablePreparationRequest({ file });
110
- const result = inspectPortableOnboarding(input);
114
+ const output = values.get("--emit-prepare-request");
115
+ let parent;
116
+ const checkOutput = () => {
117
+ if (!isAbsolute(output) || resolve(output) !== output || output.includes("\0")) throw oatsError("E_BAD_ARGS", "prepare-request output needs a normalized absolute path");
118
+ let stat;
119
+ try {
120
+ stat = lstatSync(dirname(output));
121
+ if (!stat.isDirectory() || realpathSync(dirname(output)) !== dirname(output)) throw new Error();
122
+ } catch { throw oatsError("E_BAD_ARGS", "prepare-request output parent must be an existing real directory"); }
123
+ if (parent && (parent.dev !== stat.dev || parent.ino !== stat.ino)) throw oatsError("selection-changed", "prepare-request output parent changed during inspection");
124
+ try { lstatSync(output); }
125
+ catch (error) { if (error.code === "ENOENT") return stat; throw oatsError("E_BAD_ARGS", "prepare-request output could not be checked"); }
126
+ throw oatsError("E_BAD_ARGS", "prepare-request output already exists; choose a new file (nothing overwritten)");
127
+ };
128
+ if (output !== undefined) parent = checkOutput();
129
+ const input = readPortablePreparationRequest({ file: values.get("--request") });
130
+ const { prepareRequest, ...view } = inspectPortableOnboarding(input, { includePrepareRequest: output !== undefined });
131
+ let result = view;
132
+ if (output !== undefined) {
133
+ checkOutput();
134
+ try { writeFileSync(output, canonicalJson(prepareRequest) + "\n", { flag: "wx", mode: 0o600 }); }
135
+ catch { throw oatsError("E_BAD_ARGS", "prepare-request output could not be created exclusively; no existing file was overwritten"); }
136
+ const deployment = view.deployment.deployment.path;
137
+ result = { ...view, prepareRequestFile: output, effects: { ...view.effects, requestFileWrite: true,
138
+ deploymentWrites: output === deployment || output.startsWith(deployment + sep) } };
139
+ }
111
140
  if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
112
141
  } catch (error) { fail(error.code || "E_INSPECT_FAILED", error.message); }
113
142
  }
@@ -2446,6 +2475,28 @@ function trust() {
2446
2475
  if (!id || id.startsWith("--")) { cmdFail("E_USAGE", "usage: oats trust <capability> [--dir <dir>] | oats trust <package> --all-capabilities [--dir <dir>]"); return; }
2447
2476
  const dir = dirFlag();
2448
2477
  const all = args.includes("--all-capabilities");
2478
+ // A v3 deployment needs an EXPLICIT immutable artifact-set selection. Never
2479
+ // guess one or reinterpret its lock through the classic approval engine.
2480
+ try {
2481
+ for (const scope of lockLevelsUp(dir).reverse()) {
2482
+ // Discriminate with the shared bounded decoder only. Classic readers
2483
+ // retain all v1/v2 validation, including implicit (versionless) v1 locks.
2484
+ let version;
2485
+ try { version = parseStrictJson(readPortableBytes(join(scope, OATS_LOCK_FILE)))?.lockfileVersion; }
2486
+ catch { continue; } // Let the existing classic reader diagnose its input.
2487
+ if (version !== 3) continue;
2488
+ const portable = readLock3(scope).lock;
2489
+ if (!portable) throw oatsError("selection-changed", "selection lock disappeared; repeat explicit trust selection");
2490
+ const sets = [...new Set(Object.values(portable.selections).map(row => row.available).filter(key => key && Object.hasOwn(portable.artifactSets[key].capabilities, id)))].sort();
2491
+ const commands = sets.map(artifactSet => ({ artifactSet,
2492
+ command: `oats trust ${shellQuote(id)} --deployment ${shellQuote(scope)} --artifact-set ${shellQuote(artifactSet)}` }));
2493
+ const message = commands.length && !all
2494
+ ? `lock v3 requires exact artifact-set approval; run ${commands.map(item => item.command).join(" or ")}`
2495
+ : `lock v3 approvals are per capability; use prepare's selections[].artifactSet for each capability in approvalRequired[]: oats trust <capability> --deployment ${shellQuote(scope)} --artifact-set <sha256-artifact-set>`;
2496
+ if (JSON_MODE) jsonFail("needs-configuration", message, { deployment: scope, commands });
2497
+ die(message);
2498
+ }
2499
+ } catch (error) { cmdFail(error.code || "invalid-lock", error.message); return; }
2449
2500
  // Package-backed approval path (per-capability, or explicit bulk on a package id).
2450
2501
  let pkgs, locks;
2451
2502
  try { pkgs = listInstalledPackages(dir); locks = readPackageLocks(dir); } catch (e) { cmdFail(e.code || "invalid-lock", e.message || e); return; }
@@ -5361,8 +5412,10 @@ The turn record (core — every conversation captured, searchable, replicated):
5361
5412
  packages/experimental/README.md
5362
5413
 
5363
5414
  oats inspect --request <absolute-json-file> [--json]
5364
- read-only fresh source/workspace/member metadata;
5365
- no current config, provider execution or preparation authority
5415
+ [--emit-prepare-request <new-absolute-json-file>]
5416
+ fresh source/workspace/member metadata; optional private
5417
+ request export uses the existing fresh-request builder;
5418
+ no provider execution or preparation authority
5366
5419
  oats prepare --request <absolute-json-file> [--json]
5367
5420
  complete public preparation input; no mixed flags,
5368
5421
  inherited binding, implicit setup or launch authority
@@ -5374,6 +5427,10 @@ The turn record (core — every conversation captured, searchable, replicated):
5374
5427
  same preparation through workspace imports
5375
5428
  oats inspect --deployment <abs> --resolution <id> [--composition] [--json]
5376
5429
  [--helper <exact-map-key>] inspect retained source/helper inputs, not today's configuration
5430
+ oats trust <capability> --deployment <abs> --artifact-set <sha256-…> [--json]
5431
+ approve one exact prepared artifact; use each
5432
+ selections[].artifactSet for capability ids
5433
+ in prepare's approvalRequired[] at that selection
5377
5434
  oats trust <capability> --deployment <abs> --resolution <id> [--json]
5378
5435
  explicitly approve that exact captured artifact
5379
5436
  oats <namespace> <command> --deployment <abs> --resolution <id> -- [args…]
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
4
4
 
5
- **Last update:** 2026-09-21 04:10Z · main `feef2a2f`+ · **OATS v0.24.2 tagged (CI)** · OKF v2.1.1 · oats-framework/v1.1.1 · aweb v1.11.0 · oats-knowledge 8d67eab4
5
+ **Last update:** 2026-09-21 05:00Z · main `04e930e7` · **OATS v0.24.2 published** · OKF v2.1.1 · oats-framework/v1.1.1 · aweb v1.11.0 · oats-knowledge 8d67eab4
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -11,7 +11,7 @@ Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, n
11
11
  | # | Stream | State | Owner | Next action |
12
12
  |---|---|---|---|---|
13
13
  | S1 | Knowledge capability contract rework (kernel↔provider boundary, OKF 2.x) | ✅ OATS 0.24.1 / OKF 2.1.1 published | P | done for this phase |
14
- | S2 | Workspace/Portable Souls adoption of the OATS repos | ✅ workspace + seven indexes + **six imports** (five experts @caa341f3, setup expert @0aad753c) · ✅ second-operator gate run · 🔄 five seams → L, target 0.24.3 | lead, L, Antares | seams PR; Antares re-run on 0.24.2 (aweb 1.11.0) |
14
+ | S2 | Workspace/Portable Souls adoption of the OATS repos | ✅ workspace + seven indexes + six imports · ✅ **0.24.2 published** (onboard verified from the npm tarball) · 🔄 Antares re-run requested · 🔄 five seams → L, 0.24.3 | lead, L, Antares | re-run report; seams PR |
15
15
  | S3 | Messaging capability readiness on the new infrastructure (aweb) | ✅ aweb 1.11.0 released · ✅ **catalog + six editions pin v1.11.0 (0.24.2)** · ⬜ second-operator re-run | P, lead | Antares re-run |
16
16
  | S4 | Official capabilities `oats.core` / `oats.setup` + explicit default + onboarding `oats-setup-expert` | ✅ D1, D2, **D3 merged (PR35)**: `oats onboard` verified live (acquire 1.1.1 → setup expert with both caps → scaffold composes the five capability skills, no legacy) · `oats.framework` 1.1.1 tagged | P, L | done; Desktop surfaces → S8 |
17
17
  | S5 | Official marketplace = reviewed list in oats repo | ✅ D4 merged · ✅ `oats.framework` 1.1.1 listed (`oats.core`, `oats.setup`, `oats.knowledge-theory` aliases) | M | Desktop view → S8 |
@@ -38,7 +38,7 @@ Fresh dir, local `@awebai/oats@0.24.1`, no prior state. `inspect --request` →
38
38
  2. `prepare` on the absent deployment inspect blessed → raw `ENOENT` + host path through the JSON envelope.
39
39
  3. `prepare` writes lock v3; `oats trust <cap> --dir` rejects it (`unsupported lockfileVersion 3`) → dead end from `--help`.
40
40
  4. The working `trust --deployment --artifact-set <sha256>` route is absent from `--help`.
41
- 5. Problems carry `origins: []` and no slot/capability; aweb's missing interface masks OKF diagnostics — a valid and a bogus `stores.oats` binding produce byte-identical output. **Fix first.**
41
+ 5. Problems carry `origins: []` and no slot/capability, so a valid and a bogus `stores.oats` binding produce byte-identical output. **Root cause (L's trace):** both OKF `normalize` calls refused because the operator request had no OKF runtime settings (`bindings-file`, `state-dir` — absolute host paths, no defaults); the message never said so. Fix = attribute problems by slot/capability and name the missing item; the identical pair is correct output for that input and stays identical. Guidance for the required settings → P (oats-workspace-setup).
42
42
 
43
43
  ## S3 — Messaging (aweb) on the new infrastructure
44
44
  - ✅ **aweb PR3 merged → v1.11.0 (93f8ab96)**: `binding {normalize,bind,check}` on the existing wire; `check` = HOME-route operational custody only (explicit private team, `delivery: session`, kernel ≥0.24.2 via caller-owned `OATS_CLI_BIN`, retained `launchSelection` must be input-capable Claude/Codex; strict-Pi print → `needs-configuration`, never downgraded). Native adapter over existing `aw` commands with physical identity-dir custody and redacted tokens. Standalone 30/0; coupling 14/0 vs kernel b92f0d07. PR33 (launchSelection projection, OATS_CLI_BIN in codec env) merged b92f0d07.
@@ -9,9 +9,11 @@ owned; production capability/profile readiness is not established by the fixture
9
9
 
10
10
  ```sh
11
11
  oats inspect --request /absolute/inspection.json --json
12
+ # Optional explicit request export (new private file; never overwrite):
13
+ oats inspect --request /absolute/inspection.json --emit-prepare-request /absolute/preparation.json --json
12
14
  ```
13
15
 
14
- This mode accepts only one request file and `--json`. Explicit captured selectors
16
+ This mode accepts one request file, optional `--emit-prepare-request`, and `--json`. Explicit captured selectors
15
17
  or current-context flags conflict before file reads; inherited captured environment
16
18
  is not new-work input. Other existing inspect modes are unchanged. The shared
17
19
  bounded strict JSON request reader feeds the existing inspection validator intact:
@@ -66,21 +68,47 @@ have not been classified by their owner and are omitted. Import summaries expose
66
68
  `payloadOmitted`. Top-level `omitted:{providerPayloads:true,adoptionValues:true}`
67
69
  states that this is a metadata view, not a lossless request or a safe-payload claim.
68
70
  It is not an issued `buildFreshPreparationRequest` witness, even in the same
69
- process. Keep the original authored input for an explicit preparation request.
71
+ process. The opt-in `--emit-prepare-request` route calls that existing builder
72
+ on the real in-process inspection before dropping the private witness. It writes
73
+ only `.preparation` to a new mode-0600 file at an explicit normalized absolute
74
+ path with an existing real parent; existing files/symlinks are refused, never
75
+ overwritten. JSON output names `prepareRequestFile` and records the explicit
76
+ request-file write in `effects`; it does not echo the request contents. The
77
+ export can carry unclassified adoption declarations and must remain private;
78
+ requests must contain nonsecret values or credential references, never secrets.
79
+ A held inspection cannot emit a fresh preparation request. Core callers can
80
+ explicitly request this data via `{includePrepareRequest:true}`; the default
81
+ metadata projection and its omissions are unchanged.
82
+
83
+ The file is reusable new-work input, NOT a stored resolution, approval, admission,
84
+ or serialized ready-inspection permission. Preparation performs fresh validation
85
+ and observations, including re-resolving any mutable source selectors. Provider
86
+ configuration still requires explicit operator choices; conversion invents none.
70
87
 
71
88
  Existing managed deployment state is preserved and reported, not repaired or
72
89
  migrated. An absent selected path requires explicit operator provisioning and
73
- reinspection. The serialized inspection does not lock the filesystem or authorize
90
+ reinspection. Prepare refuses it with typed `needs-configuration` and provisioning
91
+ guidance before fetching or writing managed state, not a raw ENOENT. Inspection's
92
+ `ready` means the path is eligible for fresh setup, not already provisioned.
93
+ The serialized inspection does not lock the filesystem or authorize
74
94
  later mutation; preparation retains its own existing validation/custody rules.
75
95
  The inspected work target does not become source identity or an implied placement
76
96
  choice. Supported captured directory scaffolds own their separate H/work.
77
97
 
78
98
  ## Existing preparation and retained execution
79
99
 
80
- `oats prepare --request` already accepts deployment/source/origin, workspace/member
81
- OR standalone context, operator policy/bindings, mode/local-input authorization,
82
- launch and helperLaunches. Do not pass the inspection result or workTarget/catalog
83
- wrapper. Exact executable approval is separate. A required provider whose binding
100
+ `oats prepare --request` accepts deployment/source/origin, optional `workTarget`,
101
+ workspace/member OR standalone context, operator policy/bindings,
102
+ mode/local-input authorization, launch and helperLaunches. The original minimal
103
+ inspection request (without inspection-only `catalogIndexes`) is also accepted;
104
+ use the converter rather than stripping fields from a metadata/result wrapper.
105
+ Explicit `workTarget` is validated with the same physical existing-directory
106
+ validator as inspection and returned as work-context metadata. It takes precedence
107
+ over any caller assumption about cwd: no cwd/config fallback selects its value.
108
+ Omitting it preserves prior preparation behavior without inventing a placement.
109
+ It does not change source identity, `operator.localBase`, work mode, or captured
110
+ H/work placement. Do not pass an inspection result/catalog wrapper or private
111
+ scratch `directory` to preparation. Exact executable approval is separate. A required provider whose binding
84
112
  code is unapproved may return `needs-configuration` with an `approval-required`
85
113
  problem and exact artifact-set/capability requests, before any record exists:
86
114
 
@@ -92,6 +120,44 @@ oats spawn <subject> --deployment <D> --resolution <R> --home <new-H> --no-launc
92
120
  oats session start --deployment <D> --resolution <R> --home <H> --request /absolute/native.json --json
93
121
  ```
94
122
 
123
+ Each `selections[]` row carries an `artifactSet` id and an `approvalRequired[]`
124
+ array of **capability ids**. Pair them: pass the capability to `trust` and that
125
+ row's artifact-set id to `--artifact-set`. These are not interchangeable ids.
126
+ `trust <capability> --dir <D>` against a v3 deployment now refuses with typed
127
+ `needs-configuration` and exact available-set commands; it never picks or approves
128
+ a set automatically. Classic v1/v2 trust remains the classic route.
129
+
130
+ Provider preparation problems carry `slot` and `capability`, plus original
131
+ provenance where available. A no-interface provider is identified with kernel-known
132
+ manifest/version facts. Other supported slots still normalize, resolve through
133
+ the same choice engine, and bind if their own choices are resolved; any required
134
+ slot problem still prevents publication. Provider free text is not passed through.
135
+ Different opaque inputs need not produce different public errors if both fail the
136
+ same provider prerequisite. In particular, missing OKF host runtime settings can
137
+ hold both syntactically valid Git locators; preparation does not test whether a
138
+ remote repository exists. Required readiness checks remain later, with the provider.
139
+
140
+ ### Operator input
141
+
142
+ When present, `operator` requires both `policy` (object; `{}` is valid) and
143
+ `document` (`{ "kind": "operator", "id": "setup-attempt" }`). Its ONLY optional
144
+ fields are `localBase`, `allowLocalPaths`, `sourceContext`, and `bindings`:
145
+
146
+ - `policy`: explicit provider/additive selections with their selected sources and
147
+ settings, using the existing policy grammar. It cannot erase soul requirements.
148
+ - `localBase`: explicit absolute base for relative local policy sources. Work
149
+ context/cwd is not a substitute.
150
+ - `allowLocalPaths`: explicit boolean authorization for those local policy sources.
151
+ Top-level local acquisition authorization remains a separate input.
152
+ - `sourceContext`: existing qualified repository anchor for `repo:` policy sources;
153
+ not a new repository inferred from workTarget.
154
+ - `bindings`: provider-owned map. Kernel preserves it and its document pointers;
155
+ it does not interpret store names, Git destinations, credentials or private teams.
156
+
157
+ Selecting an inherited store does not replace required provider runtime settings.
158
+ Use the selected provider's instructions for those settings; kernel must not guess
159
+ host-owned durable paths or copy native authentication.
160
+
95
161
  Repreparation after explicit approval is ordinary continuation in the selected,
96
162
  now-managed deployment; do not delete its state to make fresh preflight pass.
97
163
  Required hooks still run under their admitted custody with `--no-launch`; a parsed
@@ -0,0 +1,13 @@
1
+ # OATS v0.24.3 — second-operator `prepare` seams
2
+
3
+ Kernel/Pi/Desktop **0.24.3**. Fixes the five usability seams an independent second operator hit adopting the shared workspace definition on 0.24.1/0.24.2 (see the [program board](../design/2026-09-20-redesign-program-board.md)). No contract, schema or authority change.
4
+
5
+ - **Attributed provider problems.** Every preparation problem now carries `slot`, `capability` and its `origins`, with kernel-fixed text that names the missing item (for example `oats.okf knowledge normalize binding could not be prepared` / `oats.aweb@1.10.3 declares no binding interface; messaging cannot be prepared`). One unqualified slot no longer masks another slot's diagnostics: every slot that has a binding interface is normalized, and the single resolver reports each invalid choice under its slot. Provider free text still never crosses the wire.
6
+ - **`oats trust <capability> --dir <deployment>` on a lock v3 deployment** returns a typed problem with the exact working command (`oats trust <cap> --deployment <abs> --artifact-set <sha256-…>`) instead of `unsupported lockfileVersion 3`. No auto-approval.
7
+ - **Help** documents `--artifact-set` and how `prepare`'s `selections[].artifactSet` / `approvalRequired[]` pair with it.
8
+ - **`prepare` on an absent deployment path** returns a typed explicit-provisioning hold, not a raw `ENOENT` with a host path.
9
+ - **One request file for both commands.** `prepare --request` accepts the explicit `workTarget` that `inspect --request` requires (explicit beats cwd; it never implies captured placement or current config), and `oats inspect --request <f> --emit-prepare-request <out>` writes the converted preparation request through the existing builder.
10
+
11
+ The second operator's recorded requests and results are preserved verbatim as regression fixtures (`test/fixtures/second-operator/`). Their valid-vs-bogus `stores.oats` pair remains identical by design: both refused on missing OKF runtime settings (`bindings-file`, `state-dir`), which the messages now say.
12
+
13
+ Setup guidance for those settings lands in the next `oats.framework` payload.
package/lib/core.mjs CHANGED
@@ -62,7 +62,7 @@ import { validateCapturedLaunchRequest } from "./captured-launch-request.mjs";
62
62
  import { validateCapabilityInputDeclarations } from "./capability-inputs.mjs";
63
63
  import { validateCapturedSessionBackend, validateCapturedSessionTarget, assertCapturedSessionPlacement } from "./captured-session-backend.mjs";
64
64
  import { createRepositoryTransaction } from "./repository-observation.mjs";
65
- import { inspectPortableOnboarding as inspectOnboarding, describePortableOnboarding } from "./portable-onboarding.mjs";
65
+ import { inspectPortableOnboarding as inspectOnboarding, describePortableOnboarding, buildFreshPreparationRequest, inspectWorkTarget } from "./portable-onboarding.mjs";
66
66
  import { readLock3 } from "./portable-lock.mjs";
67
67
  import { portableScope, portableStateDirectory } from "./portable-state.mjs";
68
68
  import { objectAt } from "./portable-shape.mjs";
@@ -1211,13 +1211,17 @@ export function approveAvailableCapability(deployment, artifactSet, capability,
1211
1211
  /** Public read-only source/workspace inspection through the existing facade.
1212
1212
  * Owns only transient repository scratch, never deployment state, provisioning,
1213
1213
  * provider code, approvals or a serialized mutation witness. */
1214
- export function inspectPortableOnboarding(input, { repositoryOptions = {} } = {}) {
1214
+ export function inspectPortableOnboarding(input, { repositoryOptions = {}, includePrepareRequest = false } = {}) {
1215
1215
  canonicalJson(input);
1216
+ if (typeof includePrepareRequest !== "boolean") throw oatsError("invalid-declaration", "request conversion requires an explicit boolean option");
1216
1217
  const directory = mkdtempSync(join(realpathSync(tmpdir()), "oats-source-inspection-")), owned = lstatSync(directory);
1217
1218
  let repositories, primary;
1218
1219
  try {
1219
1220
  repositories = createRepositoryTransaction({ ...repositoryOptions, directory, accessContextKey: repositoryOptions.accessContextKey ?? "native" });
1220
- return describePortableOnboarding(inspectOnboarding(input, { repositories }));
1221
+ const inspection = inspectOnboarding(input, { repositories }), result = describePortableOnboarding(inspection);
1222
+ // Opt-in data export, not a serialized custody witness or executable grant.
1223
+ // Default metadata continues to omit unclassified adoption/provider values.
1224
+ return includePrepareRequest ? { ...result, prepareRequest: buildFreshPreparationRequest(inspection).preparation } : result;
1221
1225
  } catch (error) { primary = error; throw error; }
1222
1226
  finally {
1223
1227
  try {
@@ -1239,21 +1243,28 @@ export function inspectPortableOnboarding(input, { repositoryOptions = {} } = {}
1239
1243
  * a fabricated complete record. No lifecycle side effects run here. */
1240
1244
  export function prepareCapturedComposition(input, { repositoryOptions = {} } = {}) {
1241
1245
  canonicalJson(input);
1242
- objectAt(input, ["deployment", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "source"]);
1246
+ objectAt(input, ["deployment", "workTarget", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "source"]);
1243
1247
  if (input.launch !== undefined) validateCapturedLaunchRequest(input.launch, validateLaunchConfig);
1244
1248
  if (input.helperLaunches !== undefined) {
1245
1249
  objectAt(input.helperLaunches, null, []);
1246
1250
  for (const request of Object.values(input.helperLaunches)) validateCapturedLaunchRequest(request, validateLaunchConfig);
1247
1251
  }
1248
1252
  if (input.mode !== undefined && !WORK_MODES.includes(input.mode)) throw oatsError("invalid-declaration", "invalid preparation work mode");
1249
- const deployment = portableScope(input.deployment);
1253
+ const { workTarget, ...preparationInput } = input;
1254
+ const target = Object.hasOwn(input, "workTarget") ? inspectWorkTarget(workTarget) : null;
1255
+ let deployment;
1256
+ try { deployment = portableScope(input.deployment); }
1257
+ catch (error) {
1258
+ if (error.code === "ENOENT") throw oatsError("needs-configuration", "deployment directory is absent; provision the explicitly selected directory and inspect again before preparation");
1259
+ throw error;
1260
+ }
1250
1261
  const previous = readLock3(deployment); // Old state refuses before scratch/fetch.
1251
1262
  const directory = mkdtempSync(join(portableStateDirectory(deployment, true), ".prepare-")), owned = lstatSync(directory);
1252
1263
  const origin = input.origin ?? { kind: "operator", document: { kind: "operator", id: "oats-prepare" }, pointer: "/source" };
1253
1264
  let repositories, primary, catalog;
1254
1265
  try {
1255
1266
  repositories = createRepositoryTransaction({ ...repositoryOptions, directory, accessContextKey: repositoryOptions.accessContextKey ?? "native" });
1256
- return prepareComposition({ ...input, deployment, origin, directory }, { repositories, previous, kernel: {
1267
+ const result = prepareComposition({ ...preparationInput, deployment, origin, directory }, { repositories, previous, kernel: {
1257
1268
  loadPackageManifestAt, capabilityCompatibility, assertPlatformInvariantLocks, materializeCapability,
1258
1269
  manifest: loadRetainedManifest,
1259
1270
  binding: runCapturedProviderBinding,
@@ -1266,6 +1277,8 @@ export function prepareCapturedComposition(input, { repositoryOptions = {} } = {
1266
1277
  settings: assertCapabilitySettingValues, skills: skillEntriesIn, hooks: manifestHookDeclarations,
1267
1278
  validateLaunchConfig, runtimeRequirements: applicableRequirements }),
1268
1279
  } });
1280
+ // Explicit work context never selects source/config, cwd, or H/work placement.
1281
+ return target ? { ...result, workTarget: target } : result;
1269
1282
  } catch (error) { primary = error; throw error; }
1270
1283
  finally {
1271
1284
  try {
@@ -108,7 +108,7 @@ export function preflightFreshDeployment({ deployment, maxEntries = 4096 }) {
108
108
  return result;
109
109
  }
110
110
 
111
- function workTarget(path) {
111
+ export function inspectWorkTarget(path) {
112
112
  const canonical = canonicalExistingDirectory(path, "work target"), marker = present(join(canonical, ".git"));
113
113
  return { path: canonical, state: "existing", git: marker ? { present: true, kind: marker.isDirectory() ? "directory" : marker.isFile() ? "file" : "other" } : { present: false, kind: null } };
114
114
  }
@@ -146,7 +146,7 @@ export function inspectPortableOnboarding(input, { repositories } = {}) {
146
146
  validateOrigin(input.origin);
147
147
  const standalone = explicitContext(input);
148
148
  const deployment = preflightFreshDeployment({ deployment: input.deployment });
149
- const target = workTarget(input.workTarget), discovery = createWorkspaceDiscovery(repositories);
149
+ const target = inspectWorkTarget(input.workTarget), discovery = createWorkspaceDiscovery(repositories);
150
150
  const witness = { deployment: preflightWitnesses.get(deployment), work: directoryIdentity(target.path) };
151
151
  const workspace = Object.hasOwn(input, "workspace") ? discovery.readWorkspace(input.workspace) : null;
152
152
  const selected = typeof input.source === "string" ? sourceOrigin(workspace, input.source) : { reference: input.source, origin: input.origin };
@@ -236,7 +236,7 @@ export function buildFreshPreparationRequest(inspection, options = {}) {
236
236
  const { operator, mode, allowLocalPaths = false } = options;
237
237
  if (typeof allowLocalPaths !== "boolean") throw oatsError("invalid-declaration", "allowLocalPaths must be boolean");
238
238
  if (mode !== undefined && (typeof mode !== "string" || !mode.trim())) throw oatsError("invalid-declaration", "preparation mode must be non-empty text");
239
- const input = { deployment: inspection.deployment.deployment.path,
239
+ const input = { deployment: inspection.deployment.deployment.path, workTarget: inspection.workTarget.path,
240
240
  source: inspection.source.reference,
241
241
  origin: inspection.source.revision.provenance[0], allowLocalPaths,
242
242
  ...(inspection.workspace ? { workspace: inspection.workspace.request } : { standaloneContextKey: inspection.context.key }),
@@ -37,24 +37,34 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
37
37
  const normalized=new Map(),problems=[],requirements=[...plan.requirements],candidates=[...plan.candidates];
38
38
  const settingsFor=id=>Object.fromEntries(Object.entries(plan.settings[id] ?? {}).map(([name,key])=>[name,plan.choices[key].value]));
39
39
  const run=(id,phase,input)=>invoke({artifacts:seed.artifacts,capability:id,phase,settings:settingsFor(id),input});
40
- const refuse=error=>({code:safeCodes.has(error?.code)?error.code:'provider-not-qualified',message:'provider binding could not be prepared',origins:[]});
40
+ const originsFor=id=>[...new Map((plan.capabilities[id]?.choiceKeys ?? []).flatMap(key=>{
41
+ const choice=plan.choices[key];return choice?.selectedBy?[choice.selectedBy]:[];
42
+ }).map(origin=>[canonicalJson(origin),origin])).values()];
43
+ const problem=(slot,id,code,message,origins=originsFor(id))=>({code,message,origins,slot,capability:id});
44
+ const refuse=(slot,id,phase,error)=>problem(slot,id,safeCodes.has(error?.code)?error.code:'provider-not-qualified',
45
+ `${id} ${slot} ${phase} binding could not be prepared`); // Never provider free text.
41
46
  for (const [id,manifest] of manifests) {
42
47
  if (!Object.hasOwn(seed.artifacts.capabilities,id) || !manifest.layer) continue;
43
- if (plan.providers[manifest.layer] !== id) problems.push({code:'needs-configuration',message:'fundamental capability must be selected in its own layer',origins:[]});
48
+ if (plan.providers[manifest.layer] !== id) problems.push(problem(manifest.layer,id,'needs-configuration','fundamental capability must be selected in its own layer'));
44
49
  }
45
- if (problems.length) return {plan,seed,problems};
46
50
  for (const [slot,id] of Object.entries(plan.providers)) {
47
51
  if (id === null) continue;
48
52
  const manifest=manifests.get(id);
49
- if (manifest?.layer !== slot || !manifest.binding) { problems.push({code:'provider-not-qualified',message:'selected provider has no matching binding interface',origins:[]});continue; }
53
+ if (manifest?.layer !== slot) { problems.push(problem(slot,id,'provider-not-qualified',`${id} does not declare the selected ${slot} layer`));continue; }
54
+ if (!manifest.binding) { problems.push(problem(slot,id,'provider-not-qualified',`${id}@${manifest.version} declares no binding interface; ${slot} cannot be prepared`));continue; }
50
55
  try {
51
56
  const value=run(id,'normalize',{declarations,context:seed.context});
52
57
  normalized.set(slot,{id,value}); requirements.push(...value.requirements);candidates.push(...value.candidates);
53
- } catch(error) { problems.push(refuse(error)); }
58
+ } catch(error) { problems.push(refuse(slot,id,'normalize',error)); }
54
59
  }
55
- if (problems.length) return {plan,seed,problems};
60
+ // One unsupported slot must not mask another slot's normalized requirements
61
+ // or invalid choices. The same resolver still decides every value.
56
62
  const resolved=resolveChoices({requirements,candidates});
57
- if (resolved.status !== 'resolved') return {plan:{...plan,...resolved},seed,problems:resolved.problems};
63
+ for (const item of resolved.problems) {
64
+ const slot=Object.keys(plan.providers).find(slot=>bindingField(item.key,slot));
65
+ if (!slot) throw oatsError('invalid-binding-output','provider choice problem has no owned binding slot');
66
+ problems.push({...item,slot,capability:plan.providers[slot]});
67
+ }
58
68
  const nextPlan={...plan,...resolved,requirements,candidates},bindings=Object.create(null);
59
69
  let messagingChoice={schemaVersion:1,enabled:false};
60
70
  const known=[];
@@ -64,6 +74,7 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
64
74
  }
65
75
  const witnessed=origin=>bindingOriginWitnessed(origin,declarations) || known.some(candidate=>same(candidate,origin));
66
76
  for (const [slot,{id,value}] of normalized) {
77
+ if (resolved.problems.some(item=>bindingField(item.key,slot))) continue;
67
78
  try {
68
79
  const choices=Object.fromEntries(Object.entries(resolved.choices).filter(([key])=>bindingField(key,slot)));
69
80
  const bound=run(id,'bind',{model:value.model,choices,context:seed.context});
@@ -72,7 +83,9 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
72
83
  }
73
84
  bindings[slot]=bound.binding;
74
85
  if (slot === 'messaging') messagingChoice=bound.messagingChoice;
75
- } catch(error) { problems.push(refuse(error)); }
86
+ } catch(error) { problems.push(refuse(slot,id,'bind',error)); }
76
87
  }
88
+ nextPlan.problems=problems;
89
+ nextPlan.status=problems.some(item=>item.code==='requirement-conflict')?'conflict':problems.length?'needs-configuration':'resolved';
77
90
  return {plan:nextPlan,seed:{...seed,choices:resolved.choices,bindings,messagingChoice},problems};
78
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.24.2",
3
+ "version": "0.24.3",
4
4
  "description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
5
5
  "keywords": [
6
6
  "agents",