@awebai/oats 0.24.1 → 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
@@ -13,7 +13,7 @@
13
13
  * The kernel resolves per-key closest-wins from wherever agents actually run,
14
14
  * so binding at a level scopes the capability to everything under it.
15
15
  */
16
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readSync, realpathSync, rmSync, writeFileSync } from "node:fs";
16
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readSync, realpathSync, rmSync, writeFileSync } from "node:fs";
17
17
  import { execFileSync, spawnSync } from "node:child_process";
18
18
  import { homedir, tmpdir } from "node:os";
19
19
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
@@ -26,10 +26,10 @@ import {
26
26
  capabilityManifests, capabilityManifest, capabilityMissingRequires, capabilityIntegrity, capabilityTrust, capabilityExecutablePath, activateCapturedScaffold, loadCapturedDispatch, inspectPortableOnboarding, prepareCapturedComposition, resolveCapturedHelper, capturedNativeSessionAvailability, scaffoldCapturedInstance, startCapturedInstanceSession, withCapturedBindingFile, withCapturedInvocationContextFile,
27
27
  readCapabilityLocks, writeCapabilityLock, admitCapturedAction, beginCapturedIntent, settleCapturedIntent,
28
28
  parsePackageSource, inspectGitSourceRoot, acquirePackage, restorePackages, listInstalledPackages, readPackageLocks, readLockedConfigTemplates,
29
- officialCapabilityPackage, officialPackageCatalog,
29
+ officialCapabilityPackage, officialPackageCatalog, DEFAULT_PACKAGE_PATH,
30
30
  approveCapability, approveAvailableCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
31
31
  packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir, installedCapabilitiesDir, ownedCapabilitiesDir, loadPackageManifestAt,
32
- resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
32
+ resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, planInstanceResources, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
33
33
  findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, listCapabilityAgents, workspaceOf,
34
34
  ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
35
35
  spawnInstance, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS, validateLaunchConfig, resolveLaunchSelection, resolveLaunchExecutable, checkLaunchExecutable, missingLaunchEnvRefs, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, LAUNCH_RECIPE_VERSION, parseLaunchCommand, resolveYolo, planLaunch, redactLaunchCommand, restartInstanceSession,
@@ -49,15 +49,21 @@ 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";
55
59
  import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
60
+ import { loadSetupExpertEdition, SETUP_EXPERT, SETUP_CAPABILITIES } from "../lib/setup-expert-source.mjs";
61
+ import { parsePortableSource } from "../lib/source-spec.mjs";
56
62
 
57
63
  const args = process.argv.slice(2);
58
64
  let cmd = args[0];
59
65
  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"]);
66
+ const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
61
67
  const flag = (name) => {
62
68
  const i = args.indexOf(`--${name}`);
63
69
  return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
@@ -97,15 +103,40 @@ const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok:
97
103
 
98
104
  function inspectOnboardingCmd() {
99
105
  const fail = (code, message) => JSON_MODE ? jsonFail(code, message) : die(message);
100
- let file;
106
+ const values = new Map();
101
107
  for (let index = 1; index < args.length; index++) {
102
108
  if (args[index] === "--json") continue;
103
- 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");
104
- 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]);
105
112
  }
106
113
  try {
107
- const input = readPortablePreparationRequest({ file });
108
- 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
+ }
109
140
  if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
110
141
  } catch (error) { fail(error.code || "E_INSPECT_FAILED", error.message); }
111
142
  }
@@ -385,6 +416,9 @@ function capturedCommand(selector) {
385
416
  const selected = helperSelection?.executionBinding ?? target;
386
417
  const loaded = loadCapturedDispatch({ deployment: selected.deployment, resolution: selected.resolution, action: { kind: args.includes("--composition") ? "compose" : "inspect" } });
387
418
  const result = { resolution: loaded.resolution, capture: loaded.record.capture, nativeSession: capturedNativeSessionAvailability(),
419
+ launchSelection: loaded.record.dispatch.launch === null ? null : {
420
+ runtime: loaded.record.dispatch.launch.runtime, model: loaded.record.dispatch.launch.model,
421
+ },
388
422
  ...(helperSelection ? { helperSelection } : {}),
389
423
  capabilities: [...loaded.capabilities.values()].map(({ id, manifest }) => ({ id, version: manifest.version,
390
424
  approval: loaded.approvals.find((entry) => entry.artifact.capability === id).status })),
@@ -505,6 +539,10 @@ function resolveForDoctor(ctx, soulName, { json } = {}) {
505
539
  die(`${e.message}`);
506
540
  }
507
541
  }
542
+ function operationalKnowledgeNote(composition, soulName) {
543
+ return composition && !composition.oatsCoreDeclared
544
+ ? `soul ${soulName} has no oats.core capability; kernel-shipped operational skills are deprecated` : null;
545
+ }
508
546
  function doctorComposition(ctx, soulName) {
509
547
  if (!soulName) return undefined;
510
548
  const root = findRoot(ctx);
@@ -1259,7 +1297,8 @@ function doctorJson(dir) {
1259
1297
  hooks: Object.keys(r.layers[l].hooks || {}), missingRequires: r.layers[l].missingRequires,
1260
1298
  provenance: r.provenance[l],
1261
1299
  } : { provenance: r.provenance[l] || null }])),
1262
- kernelInjection: r.kernelInjection,
1300
+ kernelInjection: composition?.resolved.kernelInjection ?? r.kernelInjection,
1301
+ information: operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : [],
1263
1302
  injects: r.injects,
1264
1303
  capabilities: r.capabilities.map((c) => ({ id: c.id, layer: c.layer, command: c.command, origin: c.origin, provenance: c.provenance, settings: c.settings, skills: c.skills, inject: c.inject, hooks: Object.keys(c.hooks || {}), trust: c.trust })),
1265
1304
  acquired: Object.fromEntries(Object.entries(mans).map(([n, m]) => [n, { layer: m.layer, command: m.command, version: m.version, dir: m._dir, origin: m._origin, description: m.description }])),
@@ -1288,6 +1327,7 @@ function doctor(dir) {
1288
1327
  const soulName = flag("soul");
1289
1328
  const chain = configChain(ctx);
1290
1329
  const r = resolveForDoctor(ctx, soulName);
1330
+ const composition = doctorComposition(ctx, soulName);
1291
1331
  console.log(`oats doctor — resolved from ${shortPath(ctx)}\n`);
1292
1332
 
1293
1333
  // Kernel/bridge version skew (published in lockstep from one tag).
@@ -1332,7 +1372,8 @@ function doctor(dir) {
1332
1372
  }
1333
1373
 
1334
1374
  console.log("\nKernel injection:");
1335
- console.log(` oats: ${r.kernelInjection?.inject ? shortPath(r.kernelInjection.inject) : "none"} [${r.kernelInjection?.provenance || "default"}]`);
1375
+ const kernelInjection = composition?.resolved.kernelInjection ?? r.kernelInjection;
1376
+ console.log(` oats: ${kernelInjection?.inject ? shortPath(kernelInjection.inject) : "none"} [${kernelInjection?.provenance || "default"}]`);
1336
1377
 
1337
1378
  console.log("\nUnconditional injections (outermost→innermost):");
1338
1379
  if (r.injects.length === 0) console.log(" (none)");
@@ -1440,7 +1481,8 @@ function doctor(dir) {
1440
1481
  }
1441
1482
 
1442
1483
  if (soulName) {
1443
- const composition = doctorComposition(ctx, soulName);
1484
+ const information = operationalKnowledgeNote(composition, soulName);
1485
+ if (information) console.log(`\nINFO: ${information}`);
1444
1486
  console.log(`\nFinal composed AGENTS.md for ${soulName}:\n\n${composition.text}`);
1445
1487
  } else console.log("\nPass --soul <name> to inspect final composed AGENTS.md.");
1446
1488
  }
@@ -2433,6 +2475,28 @@ function trust() {
2433
2475
  if (!id || id.startsWith("--")) { cmdFail("E_USAGE", "usage: oats trust <capability> [--dir <dir>] | oats trust <package> --all-capabilities [--dir <dir>]"); return; }
2434
2476
  const dir = dirFlag();
2435
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; }
2436
2500
  // Package-backed approval path (per-capability, or explicit bulk on a package id).
2437
2501
  let pkgs, locks;
2438
2502
  try { pkgs = listInstalledPackages(dir); locks = readPackageLocks(dir); } catch (e) { cmdFail(e.code || "invalid-lock", e.message || e); return; }
@@ -3928,16 +3992,17 @@ function spawnCmd() {
3928
3992
  if (!agent && !instrFile && !defFile) {
3929
3993
  const def = listAgentDefs(process.cwd()).find((d) => d.name === name);
3930
3994
  if (!def) bail("E_UNKNOWN_AGENT", `unknown agent "${name}" (known: ${listAgents(root).map((a) => a.name).join(", ") || "none"}; importable defs: ${listAgentDefs(process.cwd()).map((d) => d.name).join(", ") || "none"}) — pass --instructions-file or --def-file to create a local agent`);
3931
- agent = upsertLocalAgent(root, { name: def.name, file: def.path, repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model") });
3995
+ agent = upsertLocalAgent(root, { name: def.name, file: def.path, repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), oatsCore: !args.includes("--no-oats-core") });
3932
3996
  } else if (!agent || agent.kind === "local") {
3933
3997
  agent = upsertLocalAgent(root, {
3934
- name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
3998
+ name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined, oatsCore: !args.includes("--no-oats-core"),
3935
3999
  repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo: yoloFlag(),
3936
4000
  });
3937
4001
  } else {
3938
4002
  bail("E_BAD_ARGS", `"${name}" is a persistent agent — spawn it without --instructions-file/--def-file`);
3939
4003
  }
3940
4004
  }
4005
+ for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
3941
4006
  // Lineage is explicit: --relation child|sibling|parent|unrelated anchors the new
3942
4007
  // instance to --relative-to <instance>. --parent X is sugar for
3943
4008
  // --relative-to X --relation child (agents spawning sub-agents pass their own
@@ -4221,10 +4286,134 @@ async function paneCmd() {
4221
4286
  die("`oats pane` has been retired — the OATS Desktop app (packages/desktop) is the control panel now.");
4222
4287
  }
4223
4288
 
4289
+ function onboardCmd() {
4290
+ const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
4291
+ const values = new Map();
4292
+ for (let i = 1; i < args.length; i++) {
4293
+ const arg = args[i];
4294
+ if (["--json", "--force-existing"].includes(arg)) { values.set(arg.slice(2), true); continue; }
4295
+ if (!["--dir", "--workspace"].includes(arg) || values.has(arg.slice(2)) || !args[i + 1] || args[i + 1].startsWith("--")) {
4296
+ fail("E_BAD_ARGS", "usage: oats onboard [--dir <deployment>] [--workspace <git:source[@revision]>] [--force-existing] [--json]");
4297
+ }
4298
+ values.set(arg.slice(2), args[++i]);
4299
+ }
4300
+ dropAmbientRoot();
4301
+ let deployment, root, acquired, created, configFile, configBefore, configWritten;
4302
+ try {
4303
+ // Like create: an existing enclosing roster wins, otherwise bootstrap at
4304
+ // the enclosing Git root or explicit directory. Canonicalize existing parents.
4305
+ const requested = resolve(values.get("dir") || process.cwd()), missing = [];
4306
+ let parent = requested;
4307
+ while (!existsSync(parent)) { missing.unshift(basename(parent)); parent = dirname(parent); }
4308
+ const start = join(realpathSync(parent), ...missing);
4309
+ root = findRoot(start) || join(defaultRepo(start) || start, "agents");
4310
+ deployment = dirname(root);
4311
+ assertNoSymlinkedParents(deployment, root, "onboard agents root");
4312
+ assertNoSymlinkedParents(deployment, join(deployment, "local-agents", SETUP_EXPERT), "local setup soul");
4313
+ const assertSetupAbsent = () => {
4314
+ for (const candidate of [join(deployment, "local-agents", SETUP_EXPERT), join(root, SETUP_EXPERT), join(root, "local-agents", SETUP_EXPERT), join(root, "tmp-agents", SETUP_EXPERT)]) {
4315
+ let present = false;
4316
+ try { lstatSync(candidate); present = true; } catch (error) { if (error.code !== "ENOENT") throw error; }
4317
+ if (present) throw Object.assign(new Error(`existing or incomplete setup expert is preserved at ${candidate}; it will not be overwritten`), { code: "E_AGENT_EXISTS" });
4318
+ }
4319
+ };
4320
+ assertSetupAbsent();
4321
+ const agents = listAgents(root);
4322
+ if (agents.length && !values.get("force-existing")) throw Object.assign(new Error("deployment already has agents; use --force-existing to add the setup expert without replacing them"), { code: "E_DEPLOYMENT_NOT_EMPTY" });
4323
+ if (findAgent(root, SETUP_EXPERT)) throw Object.assign(new Error("oats-setup-expert already exists; it will not be overwritten"), { code: "E_AGENT_EXISTS" });
4324
+ const edition = loadSetupExpertEdition(values.get("workspace"));
4325
+ for (const key of ["description", "runtime", "model"]) if (edition.declaration[key] !== undefined) assertSafeConfigValue(edition.declaration[key], `setup edition ${key}`);
4326
+ const catalog = officialPackageCatalog(), entry = catalog["oats.framework"];
4327
+ if (!Object.hasOwn(catalog, "oats.framework") || !entry?.url || !entry.ref
4328
+ || SETUP_CAPABILITIES.some(id => { const m = officialCapabilityPackage(id); return !m.available || m.package !== "oats.framework" || m.migratedCapability !== id; })) {
4329
+ throw Object.assign(new Error("official oats.framework with core/setup aliases and a published revision is required"), { code: "needs-configuration" });
4330
+ }
4331
+ const catalogSource = parsePortableSource(`git:${entry.url}@${entry.ref}#${entry.path ?? DEFAULT_PACKAGE_PATH}`);
4332
+ const file = join(deployment, "oats-config.yaml");
4333
+ if (existsSync(file) && !lstatSync(file).isFile()) throw Object.assign(new Error("onboard will not replace a non-regular deployment configuration"), { code: "E_CONFIG_BROKEN" });
4334
+ const before = existsSync(file) ? readFileSync(file, "utf8") : null;
4335
+ configFile = file; configBefore = before;
4336
+ const caps = readCapabilitiesModel(file), previous = resolveOatsConfig(deployment, SETUP_EXPERT);
4337
+ // Exclusions are for the NEW soul only. Never turn off an existing root's
4338
+ // global provider/layer just to make bootstrap work under --force-existing.
4339
+ for (const cap of previous.capabilities) {
4340
+ if (SETUP_CAPABILITIES.includes(cap.id)) continue;
4341
+ let target;
4342
+ if (cap.layer) {
4343
+ const existing = caps.layers[cap.layer];
4344
+ if (existing && (existing === "none" || existing.capability !== cap.id)) throw Object.assign(new Error(`cannot safely exclude ${cap.id} for the setup soul at this level; choose a fresh deployment`), { code: "needs-configuration" });
4345
+ target = caps.layers[cap.layer] ||= { capability: cap.id };
4346
+ } else target = caps.additive[cap.id] ||= {};
4347
+ target.souls = { ...target.souls, [SETUP_EXPERT]: false };
4348
+ }
4349
+ if (before === null && !agents.length) for (const layer of LAYERS) caps.layers[layer] ??= "none";
4350
+ for (const id of SETUP_CAPABILITIES) {
4351
+ const target = caps.additive[id] ||= {};
4352
+ if (target.from && target.from !== "installed") throw Object.assign(new Error(`${id} already selects another provenance; choose a fresh deployment`), { code: "needs-configuration" });
4353
+ target.from = "installed"; target.souls = { ...target.souls, [SETUP_EXPERT]: true };
4354
+ }
4355
+ const text = replaceCapabilitiesBlock(before ?? `name: ${scaffoldConfigName(deployment)}\n`, caps);
4356
+ mkdirSync(deployment, { recursive: true });
4357
+ acquired = acquirePackage(deployment, "oats.framework", { expectPackage: "oats.framework",
4358
+ catalog(id, selector) { const selected = Object.hasOwn(catalog, id) ? catalog[id] : null; return selected?.url ? { url: selected.url, ref: selector || selected.ref, path: selected.path } : undefined; },
4359
+ assertCommittable(plan) {
4360
+ const pkg = plan.packages.find(p => p.package === "oats.framework");
4361
+ if (edition.packageIntegrity && pkg?.integrity !== edition.packageIntegrity) throw Object.assign(new Error("selected edition's same-repository package differs from the official acquisition; align the reviewed source and catalog explicitly"), { code: "integrity-drift" });
4362
+ for (const id of SETUP_CAPABILITIES) {
4363
+ const cap = plan.capabilities.find(c => c.capability === id);
4364
+ if (!cap || cap.package !== "oats.framework" || cap.layer || Object.values(cap.executableSurface || {}).some(value => Array.isArray(value) && value.length)) {
4365
+ throw Object.assign(new Error(`setup bootstrap needs resources-only ${id}; executable surfaces require a separate explicit approval path`), { code: "approval-required" });
4366
+ }
4367
+ }
4368
+ } });
4369
+ if ((existsSync(file) ? readFileSync(file, "utf8") : null) !== before) throw Object.assign(new Error("deployment configuration changed during acquisition; nothing was activated"), { code: "E_CONFIG_CHANGED" });
4370
+ writeFileAtomic(file, text); configWritten = text;
4371
+ const selected = resolveOatsConfig(deployment, SETUP_EXPERT);
4372
+ if (selected.capabilities.length !== 2 || SETUP_CAPABILITIES.some(id => !selected.capabilities.some(c => c.id === id))) {
4373
+ throw Object.assign(new Error("setup expert's effective configuration contains other capabilities; no soul or hook was created"), { code: "needs-configuration" });
4374
+ }
4375
+ // Required capabilities were checked resources-only before acquisition: no
4376
+ // unrelated or executable soul-scaffold hooks can run during local creation.
4377
+ assertNoSymlinkedParents(deployment, root, "onboard agents root");
4378
+ assertNoSymlinkedParents(deployment, join(deployment, "local-agents", SETUP_EXPERT), "local setup soul");
4379
+ assertSetupAbsent();
4380
+ mkdirSync(root, { recursive: true });
4381
+ created = coreCreateAgent(root, { name: SETUP_EXPERT, local: true, oatsCore: false, repo: deployment, work: "directory",
4382
+ runtime: edition.declaration.runtime, model: edition.declaration.model, yolo: false,
4383
+ description: edition.declaration.description, instructions: edition.instructions });
4384
+ const pkg = acquired.installed.find(p => p.package === "oats.framework");
4385
+ const source = parsePortableSource(`git:${catalogSource.url}@${pkg.commit}#${pkg.path}`);
4386
+ const requires = { capabilities: Object.fromEntries(SETUP_CAPABILITIES.map(id => [id, { source: `${source.source}#${source.path}` }])) };
4387
+ const soulFile = join(created.soul, "soul.yaml");
4388
+ writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`);
4389
+ const agent = findAgent(root, SETUP_EXPERT), composition = composeInstanceAgentsMd(created.soul, deployment, SETUP_EXPERT, "directory", "local");
4390
+ planInstanceResources({ resolved: composition.resolved, soulDir: created.soul, agent, contextDir: deployment, composition });
4391
+ const argv = [process.execPath, CLI_BIN, "spawn", SETUP_EXPERT, "--dir", deployment, "--no-yolo", "--task", "Help me configure this deployment and adopt my workspace with explicit approvals."];
4392
+ const result = { mode: "classic", captured: false, deployment, agentsRoot: root, ...created, source: edition.source,
4393
+ package: { id: pkg.package, version: pkg.version, commit: pkg.commit, path: pkg.path }, lockFile: acquired.lockFile,
4394
+ capabilities: [...SETUP_CAPABILITIES], launched: false, next: { argv, command: argv.map(shellQuote).join(" ") } };
4395
+ if (JSON_MODE) jsonOk(result);
4396
+ else { console.log(`Created local ${SETUP_EXPERT} in ${deployment} (classic bootstrap, not captured preparation).`); console.log(`No model was launched. Next:\n${result.next.command}`); }
4397
+ } catch (error) {
4398
+ // Roll back only configuration bytes still exactly owned by this attempt.
4399
+ // Acquired artifacts/locks and any incomplete new soul remain visible evidence.
4400
+ let configRestored = false;
4401
+ if (configWritten !== undefined) {
4402
+ try {
4403
+ if (lstatSync(configFile).isFile() && readFileSync(configFile, "utf8") === configWritten) {
4404
+ if (configBefore === null) rmSync(configFile); else writeFileAtomic(configFile, configBefore);
4405
+ configRestored = true;
4406
+ }
4407
+ } catch { /* never erase another writer's change or hide a failed rollback */ }
4408
+ }
4409
+ fail(error.code || "E_ONBOARD_FAILED", error.message, { deployment, agentsRoot: root, packageAcquired: !!acquired, soul: created?.soul, configRestored, launched: false });
4410
+ }
4411
+ }
4412
+
4224
4413
  function createCmd() {
4225
4414
  const yolo = yoloFlag();
4226
4415
  const name = args[1];
4227
- if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--runtime pi|claude|codex] [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]");
4416
+ if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--no-oats-core] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--runtime pi|claude|codex] [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]");
4228
4417
  const local = args.includes("--local");
4229
4418
  const startDir = dirFlag();
4230
4419
  // `create` BOOTSTRAPS a deployment: with no agents/ or local-agents/ yet,
@@ -4243,11 +4432,12 @@ function createCmd() {
4243
4432
  }
4244
4433
  const instrFile = flag("instructions-file");
4245
4434
  const r = coreCreateAgent(root, {
4246
- name, local, description: flag("description"), type: flag("type"), repo: flag("repo") || (flag("work") === "directory" ? undefined : defaultRepo(process.cwd())),
4435
+ name, local, oatsCore: !args.includes("--no-oats-core"), description: flag("description"), type: flag("type"), repo: flag("repo") || (flag("work") === "directory" ? undefined : defaultRepo(process.cwd())),
4247
4436
  work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
4248
4437
  instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
4249
4438
  });
4250
4439
  if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
4440
+ for (const information of r.notes || []) console.error(`[${information.code}] ${information.message}`);
4251
4441
  if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
4252
4442
  console.log(`Created ${r.kind === "local" ? "LOCAL agent (uncommitted — soul lives in local-agents/, gitignored)" : "agent"} "${r.agent}" — soul at ${shortPath(r.soul)}`);
4253
4443
  console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then: oats spawn ${r.agent} --task "..."`);
@@ -4900,6 +5090,14 @@ if (cmd === "prepare" || captured?.args[0] === "prepare") {
4900
5090
  if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
4901
5091
  prepareCmd(); process.exit(0);
4902
5092
  }
5093
+ if (cmd === "onboard" || captured?.args[0] === "onboard") {
5094
+ if (captured) {
5095
+ if (JSON_MODE) jsonFail("E_BAD_ARGS", "onboard is explicit classic bootstrap and cannot use captured selectors");
5096
+ die("onboard is explicit classic bootstrap and cannot use captured selectors");
5097
+ }
5098
+ if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
5099
+ onboardCmd(); process.exit(0);
5100
+ }
4903
5101
  // Other commands retain their existing explicit/inherited selection rules.
4904
5102
  try { captured ??= capturedSelector(args); }
4905
5103
  catch (error) {
@@ -5028,7 +5226,10 @@ Usage:
5028
5226
  --instance <name> | --home <abs> attachments over its saved route (bytes stream on
5029
5227
  --file <path> [--json] ssh stdin; sha256 verified); the server must
5030
5228
  advertise session-upload (oats 0.22.13 or later)
5031
- oats create <name> [--local] create an agent soul; --local = full
5229
+ oats onboard [--dir <deployment>] bootstrap a LOCAL setup expert from official
5230
+ [--workspace <git:source[@revision]>] capabilities; classic path, not captured prepare;
5231
+ [--force-existing] [--json] prints the next spawn command, never launches
5232
+ oats create <name> [--local] [--no-oats-core] create an agent soul; --local = full
5032
5233
  [--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
5033
5234
  [--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
5034
5235
  [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
@@ -5058,6 +5259,7 @@ Usage:
5058
5259
  oats spawn <agent> [--task <text>] spawn an instance (tmux/Herdr; --no-launch
5059
5260
  [--purpose <slug>] [--repo <r>] = scaffold only); --instructions-file/
5060
5261
  [--parent <instance>] --def-file creates a local agent;
5262
+ [--no-oats-core] omit the default only on NEW local souls;
5061
5263
  [--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
5062
5264
  [--relative-to <instance>] new instance to an existing one; --parent X
5063
5265
  [--relative-root <agents-root>] disambiguates same-named team anchors
@@ -5210,8 +5412,10 @@ The turn record (core — every conversation captured, searchable, replicated):
5210
5412
  packages/experimental/README.md
5211
5413
 
5212
5414
  oats inspect --request <absolute-json-file> [--json]
5213
- read-only fresh source/workspace/member metadata;
5214
- 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
5215
5419
  oats prepare --request <absolute-json-file> [--json]
5216
5420
  complete public preparation input; no mixed flags,
5217
5421
  inherited binding, implicit setup or launch authority
@@ -5223,6 +5427,10 @@ The turn record (core — every conversation captured, searchable, replicated):
5223
5427
  same preparation through workspace imports
5224
5428
  oats inspect --deployment <abs> --resolution <id> [--composition] [--json]
5225
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
5226
5434
  oats trust <capability> --deployment <abs> --resolution <id> [--json]
5227
5435
  explicitly approve that exact captured artifact
5228
5436
  oats <namespace> <command> --deployment <abs> --resolution <id> -- [args…]
@@ -0,0 +1,11 @@
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 aweb provider binding phase (manifest-owned JSON stdin/stdout)\n');
7
+ } else {
8
+ const phase=args[0];
9
+ if(args.length!==1) await runBindingWire(phase,[],process.stdout);
10
+ else await runBindingWire(phase);
11
+ }
@@ -40,6 +40,10 @@ import { execFileSync } from "node:child_process";
40
40
  import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
41
41
  import { hostname } from "node:os";
42
42
  import { join, dirname, resolve, delimiter } from "node:path";
43
+ import { loadCapturedAwebExecution, requireCapturedAwebAction } from "../lib/captured-execution.mjs";
44
+ import { assessCapturedSessionReadiness, querySelectedKernel } from "../lib/session-readiness.mjs";
45
+ import { runCapturedNative } from "../lib/captured-native.mjs";
46
+ import { parseBindingJson } from "../lib/binding-wire.mjs";
43
47
 
44
48
  /** Run a command as ARGV — never a shell string. Team ids, aliases, instance
45
49
  * names and invite tokens all flow through here; quoting them correctly is a
@@ -110,6 +114,28 @@ const warn = (m) => out({ warning: `oats-aweb: ${String(m).slice(0, 300)}` });
110
114
  * state already exists (e.g. a joined identity) so retire can undo it. */
111
115
  const fatal = (m, meta) => out({ ...(meta ? { meta } : {}), warning: `oats-aweb: ${String(m).slice(0, 300)}` }, 1);
112
116
 
117
+ // Any selected snapshot enters the captured consumer BEFORE legacy settings,
118
+ // root discovery or identity handling. Invalid-present never falls back.
119
+ try {
120
+ const loaded = loadCapturedAwebExecution();
121
+ if (loaded.kind === "captured") {
122
+ const event = process.argv[2] || process.env.OATS_EVENT;
123
+ if ((process.env.OATS_EVENT && process.env.OATS_EVENT !== event) || process.argv.slice(3).some(arg => arg !== "--json")) throw new Error("captured entrypoint arguments differ from the selected action");
124
+ const manifest = JSON.parse(readFileSync(new URL("../oats.json", import.meta.url), "utf8"));
125
+ const selected = requireCapturedAwebAction(loaded, event, manifest);
126
+ const settings = parseBindingJson(Buffer.from(process.env.OATS_SETTINGS || "{}"));
127
+ if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k => k !== "delivery")) throw new Error("captured settings support delivery only; no identity copying or ambient fallback");
128
+ const checked = assessCapturedSessionReadiness({ binding: selected.binding, invocation: selected.context, settings }, {
129
+ query(args, options) { selected.assertCurrent(); const result = querySelectedKernel(args, options); selected.assertCurrent(); return result; },
130
+ });
131
+ if (checked.status !== "ready") out({ ...checked, warning: `oats-aweb: ${checked.problems[0].message}` }, 1);
132
+ const result = runCapturedNative({ selected, event, settings, run });
133
+ out(result.output, result.exitCode);
134
+ }
135
+ } catch (error) {
136
+ out({ status: "needs-configuration", problems: [{ code: "invalid-binding", message: "invalid or changed captured aweb execution input" }], warning: "oats-aweb: invalid or changed captured aweb execution input; no legacy fallback was used" }, 1);
137
+ }
138
+
113
139
  const event = process.env.OATS_EVENT || process.argv[2];
114
140
  const instance = process.env.OATS_INSTANCE;
115
141
  const home = process.env.OATS_HOME || process.cwd();