@awebai/oats 0.26.0 → 0.27.1

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
@@ -22,11 +22,12 @@ import { execFileSync, spawnSync } from "node:child_process";
22
22
  import { homedir, tmpdir } from "node:os";
23
23
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
+ import { runtimeNameWarning, noteRuntimeName } from "../lib/deprecation.mjs";
25
26
  import {
26
- LAYERS, OATS_VERSION, manifestOperations,
27
+ LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
27
28
  capabilityManifests, capabilityTrust, capabilityExecutablePath,
28
29
  officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
29
- findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, planLaunch, redactLaunchCommand, restartInstanceSession,
30
+ findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
30
31
  } from "../lib/core.mjs";
31
32
  import {
32
33
  writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages,
@@ -65,6 +66,16 @@ function valueFlag(name) {
65
66
  return value;
66
67
  }
67
68
  const die = (msg) => { console.error(`oats: ${msg}`); process.exit(1); };
69
+ /** A command's harness: --harness, or --runtime, its pre-0.27 name (the released okf worker and
70
+ * a 0.26-era Desktop pass it) — read either, with the deprecation warning. Both, disagreeing,
71
+ * are refused. `get` reads one flag (the command's own reader where it has one). */
72
+ function harnessFlag(get = flag) {
73
+ const harness = get("harness"), runtime = get("runtime");
74
+ if (runtime === undefined) return harness;
75
+ if (harness !== undefined && harness !== runtime) cmdFail("E_BAD_ARGS", `--harness ${harness} and --runtime ${runtime} disagree; --runtime is the pre-0.27 name of --harness — give one`);
76
+ noteRuntimeName("the --runtime flag (use --harness)");
77
+ return runtime;
78
+ }
68
79
  const cmdFail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
69
80
  /** Resolve the --dir flag with central validation: a value-taking flag given
70
81
  * no value (flag() → true) is E_BAD_ARGS inside the JSON boundary, never an
@@ -86,8 +97,25 @@ const JSON_MODE = args.includes("--json");
86
97
  // Canonical absolute path of this CLI executable — the versioned OATS_CLI_BIN
87
98
  // env contract for dispatched package commands (never resolved via PATH).
88
99
  const CLI_BIN = realpathSync(fileURLToPath(import.meta.url));
89
- const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) } })); process.exit(1); };
90
- const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result })); };
100
+ // The one deprecated-name warning (lib/deprecation.mjs) rides the envelope, only when there is one.
101
+ const envelopeWarnings = () => { const w = runtimeNameWarning(); if (w) warningDelivered = true; return w ? { warnings: [w] } : {}; };
102
+ let warningDelivered = false;
103
+ /** A forwarded envelope keeps the host's warnings; this command's own deprecated-name
104
+ * note (a `--runtime` given here) joins the host's into the one warning. */
105
+ const withLocalWarnings = (envelope) => {
106
+ const mine = runtimeNameWarning();
107
+ if (!mine || !envelope || typeof envelope !== "object") return envelope;
108
+ warningDelivered = true;
109
+ const theirs = Array.isArray(envelope.warnings) ? envelope.warnings : [];
110
+ const same = theirs.find((w) => w?.code === mine.code);
111
+ if (!same) return { ...envelope, warnings: [...theirs, mine] };
112
+ const sources = [...new Set([...(Array.isArray(same.sources) ? same.sources : []), ...mine.sources])];
113
+ return { ...envelope, warnings: theirs.map((w) => (w === same ? { ...mine, sources, message: mine.message.replace(/\(.*\)/, `(${sources.join("; ")})`) } : w)) };
114
+ };
115
+ const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) }, ...envelopeWarnings() })); process.exit(1); };
116
+ const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result, ...envelopeWarnings() })); };
117
+ // Text mode (or a JSON answer printed before the read): the warning goes to stderr, never stdout.
118
+ process.on("exit", () => { const w = runtimeNameWarning(); if (w && !warningDelivered) process.stderr.write(`oats: warning: ${w.message}\n`); });
91
119
  const formatBytes = (n) => n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KiB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MiB` : `${(n / 1024 ** 3).toFixed(1)} GiB`;
92
120
  /** A retire recovery's copied outputs (untracked/ignored or directory work), named with their size. */
93
121
  function preservedOutputLines(recovery) {
@@ -203,7 +231,7 @@ const INSPECT_TEXT_CAP = 256 * 1024;
203
231
  /** The agents root a home belongs to, from its path alone:
204
232
  * <root>/<agent>/instances/<instance>. */
205
233
  function agentsRootOfHome(home) { return dirname(dirname(dirname(home))); }
206
- const SOUL_FIELDS = ["runtime", "model", "yolo", "backend", "description", "launch-config"];
234
+ const SOUL_FIELDS = ["harness", "model", "yolo", "backend", "description", "launch-config"];
207
235
  const realOrResolved = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
208
236
  /** Every soul of a scope: the persistent souls of every agents root in
209
237
  * scope, plus packaged souls (read-only). One enumeration for inspect and
@@ -293,7 +321,7 @@ function soulEntry(soul, root, { capability } = {}) {
293
321
  declarationProblems: declared.problems,
294
322
  name: soul.name, kind: packaged ? "capability" : (soul.kind || "persistent"), capability: capability || null,
295
323
  type: soul.type ?? null, description: soul.description ?? null, repo: soul.repo ?? null, work: soul.work || "checkout",
296
- runtime: soul.runtime || "pi", model: soul.model ?? null, yolo: soul.yolo === true || soul.yolo === "true" ? true : soul.yolo === false || soul.yolo === "false" ? false : null, launchConfig: soul["launch-config"] ?? null, backend: soul.backend ?? null,
324
+ harness: soul.harness || "pi", model: soul.model ?? null, yolo: soul.yolo === true || soul.yolo === "true" ? true : soul.yolo === false || soul.yolo === "false" ? false : null, launchConfig: soul["launch-config"] ?? null, backend: soul.backend ?? null,
297
325
  agentsRoot: root, dir: packaged ? soulDir : dir, soulFile: join(soulDir, "soul.yaml"), instructionsFile: join(soulDir, "AGENTS.md"),
298
326
  editable: packaged
299
327
  ? { fields: [], instructions: false, reason: `packaged soul from capability ${capability}: edit the package and update it; scoped bindings still apply through oats use` }
@@ -621,7 +649,7 @@ function serializeLaunchConfigs(map) {
621
649
  const lines = ["launch-configs:"];
622
650
  for (const name of names) {
623
651
  const e = map[name];
624
- lines.push(` ${name}:`, ` runtime: ${e.runtime}`);
652
+ lines.push(` ${name}:`, ` harness: ${e.harness}`);
625
653
  if (e.executable !== undefined) lines.push(` executable: ${yamlQuoted(e.executable)}`);
626
654
  if (e.args?.length) { lines.push(" args:"); for (const a of e.args) lines.push(` - ${yamlQuoted(a)}`); }
627
655
  const envNames = Object.keys(e.env || {}).sort();
@@ -635,9 +663,11 @@ function serializeLaunchConfigs(map) {
635
663
  return lines.join("\n") + "\n";
636
664
  }
637
665
  /** Only the declared keys, in canonical order, from a validated entry. */
666
+ /** A configuration as `launch-config set` writes it: `harness` always — a `runtime` (the
667
+ * pre-0.27 name, read either) is written back under its new name (lead call 6). */
638
668
  function normalizeLaunchConfig(e) {
639
669
  return {
640
- runtime: e.runtime,
670
+ harness: Object.hasOwn(e, "harness") ? e.harness : e.runtime,
641
671
  ...(e.executable !== undefined ? { executable: e.executable } : {}),
642
672
  ...(e.args?.length ? { args: [...e.args] } : {}),
643
673
  ...(e.env && Object.keys(e.env).length ? { env: Object.fromEntries(Object.keys(e.env).sort().map((n) => [n, typeof e.env[n] === "string" ? e.env[n] : { fromEnv: e.env[n].fromEnv }])) } : {}),
@@ -659,7 +689,7 @@ function readLaunchConfigsModel(local) {
659
689
  * `set --keep-env`. */
660
690
  function publicLaunchConfig(e, extra = {}) {
661
691
  const env = Object.fromEntries(Object.keys(e.env || {}).sort().map((n) => [n, typeof e.env[n] === "string" ? { redacted: true } : { fromEnv: e.env[n].fromEnv }]));
662
- return { runtime: e.runtime, executable: e.executable ?? null, args: [...(e.args || [])], env, model: e.model ?? null, yolo: e.yolo ?? null, ...extra };
692
+ return { harness: e.harness, executable: e.executable ?? null, args: [...(e.args || [])], env, model: e.model ?? null, yolo: e.yolo ?? null, ...extra };
663
693
  }
664
694
  /** The scope a launch-config command reads: --dir (or cwd), a running
665
695
  * home's recorded context (--home), or a soul's own member context
@@ -692,16 +722,16 @@ function launchConfigContext(bail) {
692
722
  * configuration, preflighted, read-only; environment values withheld and
693
723
  * the prompt named, never the TASK body. */
694
724
  function launchPreview(bail) {
695
- const sel = { launchConfig: flag("launch-config"), runtime: flag("runtime"), model: flag("model"), yolo: yoloFlag() };
696
- for (const k of ["launch-config", "runtime", "model"]) if (flag(k) === true) bail("E_BAD_ARGS", `--${k} needs a value`);
697
- if (sel.runtime !== undefined && !LAUNCH_RUNTIMES.includes(sel.runtime)) bail("E_BAD_ARGS", `--runtime must be one of ${LAUNCH_RUNTIMES.join(", ")}`);
725
+ for (const k of ["launch-config", "harness", "runtime", "model"]) if (flag(k) === true) bail("E_BAD_ARGS", `--${k} needs a value`);
726
+ const sel = { launchConfig: flag("launch-config"), harness: harnessFlag(), model: flag("model"), yolo: yoloFlag() };
727
+ if (sel.harness !== undefined && !LAUNCH_HARNESSES.includes(sel.harness)) bail("E_BAD_ARGS", `--harness must be one of ${LAUNCH_HARNESSES.join(", ")}`);
698
728
  const { context, selected } = launchConfigContext(bail);
699
729
  if (!selected) bail("E_BAD_ARGS", "preview needs --home <abs> (an existing instance) or --soul <name> [--dir <scope>] (a new instance)");
700
- const selectionGiven = sel.launchConfig !== undefined || sel.runtime !== undefined || sel.model !== undefined || sel.yolo !== undefined;
730
+ const selectionGiven = sel.launchConfig !== undefined || sel.harness !== undefined || sel.model !== undefined || sel.yolo !== undefined;
701
731
  let meta = null, agentLike, home, instance;
702
732
  if (selected.home) {
703
733
  home = selected.home;
704
- try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); } catch (e) { bail("E_HOME_UNKNOWN", `${home}: ${e.message}`); }
734
+ try { meta = upgradeHomeMeta(JSON.parse(readFileSync(join(home, "instance.json"), "utf8")), home); } catch (e) { bail("E_HOME_UNKNOWN", `${home}: ${e.message}`); }
705
735
  instance = meta.instance || basename(home);
706
736
  if (!(meta.launch && typeof meta.launch === "object") && !selectionGiven) {
707
737
  // A home that predates recipes, asked nothing: its frozen command is
@@ -709,15 +739,15 @@ function launchPreview(bail) {
709
739
  // (E_LAUNCH_LEGACY: re-spawn it from the deployment).
710
740
  let d;
711
741
  try { d = describeLaunchCommand(meta.command); } catch (e) { bail(e.code || "E_LAUNCH_COMMAND_UNSUPPORTED", e.message); }
712
- jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, runtime: null, model: null, yolo: null }, runtime: meta.runtime, model: meta.model || null, modelSource: meta.model ? "recorded" : "native default", yolo: meta.yolo ?? null, launchConfig: null, launchConfigSource: null, executable: { path: d.executable, declared: null, resolvedFrom: "recorded" }, argv: d.argv, environment: d.environment, command: redactLaunchCommand(meta.command), prompt: { kind: "task-file", file: "TASK.md" }, hooks: null, preflight: [{ check: "recipe", ok: true, detail: "frozen command; a selection is refused (E_LAUNCH_LEGACY): re-spawn it" }], ok: true });
742
+ jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, harness: null, model: null, yolo: null }, harness: meta.harness, model: meta.model || null, modelSource: meta.model ? "recorded" : "native default", yolo: meta.yolo ?? null, launchConfig: null, launchConfigSource: null, executable: { path: d.executable, declared: null, resolvedFrom: "recorded" }, argv: d.argv, environment: d.environment, command: redactLaunchCommand(meta.command), prompt: { kind: "task-file", file: "TASK.md" }, hooks: null, preflight: [{ check: "recipe", ok: true, detail: "frozen command; a selection is refused (E_LAUNCH_LEGACY): re-spawn it" }], ok: true });
713
743
  return;
714
744
  }
715
745
  const agentsRoot = agentsRootOfHome(home);
716
746
  const agent = (() => { try { return findAgent(agentsRoot, meta.agent); } catch { return undefined; } })();
717
- agentLike = agent || { runtime: meta.runtime, model: meta.model, yolo: meta.yolo };
747
+ agentLike = agent || { harness: meta.harness, model: meta.model, yolo: meta.yolo };
718
748
  } else {
719
749
  const soul = scopeSouls(context).souls.find((x) => x.name === selected.soul && x.agentsRoot === selected.agentsRoot);
720
- agentLike = { runtime: soul.runtime, model: soul.model };
750
+ agentLike = { harness: soul.harness, model: soul.model };
721
751
  instance = `${soul.name}-<purpose>`; home = join(selected.agentsRoot, soul.name, "instances", instance);
722
752
  }
723
753
  // A home's recorded capabilities; a new instance's are its spawn's resolution,
@@ -731,13 +761,13 @@ function launchPreview(bail) {
731
761
  const command = renderLaunchRecipe(recipe, { home, instance, redact: true });
732
762
  const d = describeLaunchCommand(command);
733
763
  const environment = d.environment.map((e) => e.reference && recipe.env[e.name]?.fromEnv ? { name: e.name, fromEnv: recipe.env[e.name].fromEnv } : e);
734
- jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig, runtime: sel.runtime ?? null, model: sel.model ?? null, yolo: sel.yolo ?? null }, runtime: plan.runtime, model: recipe.model, modelSource: plan.modelSource, yolo: recipe.yolo ?? null, launchConfig: recipe.launchConfig, launchConfigSource: recipe.launchConfigSource, executable: { path: plan.executable.path, declared: plan.executable.declared ?? null, resolvedFrom: plan.executable.resolvedFrom }, argv: d.argv, environment, command, prompt: recipe.prompt, hooks: redactLaunchRecipe(recipe).hooks, preflight: plan.preflight, ok: plan.ok });
764
+ jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig, harness: sel.harness ?? null, model: sel.model ?? null, yolo: sel.yolo ?? null }, harness: plan.harness, model: recipe.model, modelSource: plan.modelSource, yolo: recipe.yolo ?? null, launchConfig: recipe.launchConfig, launchConfigSource: recipe.launchConfigSource, executable: { path: plan.executable.path, declared: plan.executable.declared ?? null, resolvedFrom: plan.executable.resolvedFrom }, argv: d.argv, environment, command, prompt: recipe.prompt, hooks: redactLaunchRecipe(recipe).hooks, preflight: plan.preflight, ok: plan.ok });
735
765
  }
736
766
  async function launchConfigCmd() {
737
767
  const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
738
768
  dropAmbientRoot();
739
769
  const sub = args[1];
740
- const usage = "usage: oats launch-config list [--dir <scope> | --home <abs> | --soul <name> [--dir <scope>] [--agents-root <abs>]] [--json] | set <name> --file <json> [--keep-env] [--dir <scope>] [--json] | remove <name> [--dir <scope>] [--json] | preview (--home <abs> | --soul <name> [--dir <scope>]) [--launch-config <name>|none] [--runtime r] [--model m] [--yolo|--no-yolo] --json";
770
+ const usage = "usage: oats launch-config list [--dir <scope> | --home <abs> | --soul <name> [--dir <scope>] [--agents-root <abs>]] [--json] | set <name> --file <json> [--keep-env] [--dir <scope>] [--json] | remove <name> [--dir <scope>] [--json] | preview (--home <abs> | --soul <name> [--dir <scope>]) [--launch-config <name>|none] [--harness r] [--model m] [--yolo|--no-yolo] --json";
741
771
  if (sub === "preview") { launchPreview(bail); return; }
742
772
  if (!["list", "set", "remove"].includes(sub)) bail("E_USAGE", usage);
743
773
  const { context: dir, selected } = sub === "list" ? launchConfigContext(bail) : { context: dirFlag(), selected: null };
@@ -760,7 +790,7 @@ async function launchConfigCmd() {
760
790
  if (!configurations.length) { console.log(`No launch configurations are declared${file ? ` in ${shortPath(file)}` : ` (no oats-local.yaml in reach of ${dir})`}`); return; }
761
791
  for (const c of configurations) {
762
792
  const env = Object.entries(c.env).map(([n, v]) => v.fromEnv ? `${n}=$${v.fromEnv}` : `${n}=<redacted>`).join(" ");
763
- console.log(`${c.name}: ${c.runtime}${c.executable ? ` ${c.executable}` : ""}${c.args.length ? ` ${c.args.map((a) => JSON.stringify(a)).join(" ")}` : ""}${env ? ` [${env}]` : ""}${c.model ? ` model ${c.model}` : ""}${c.yolo !== null ? ` yolo ${c.yolo}` : ""}`);
793
+ console.log(`${c.name}: ${c.harness}${c.executable ? ` ${c.executable}` : ""}${c.args.length ? ` ${c.args.map((a) => JSON.stringify(a)).join(" ")}` : ""}${env ? ` [${env}]` : ""}${c.model ? ` model ${c.model}` : ""}${c.yolo !== null ? ` yolo ${c.yolo}` : ""}`);
764
794
  }
765
795
  return;
766
796
  }
@@ -777,7 +807,7 @@ async function launchConfigCmd() {
777
807
  delete model[name];
778
808
  } else {
779
809
  const f = flag("file");
780
- if (!f || f === true) bail("E_BAD_ARGS", "launch-config set needs --file <json> (an object with runtime and optional executable, args, env, model, yolo)");
810
+ if (!f || f === true) bail("E_BAD_ARGS", "launch-config set needs --file <json> (an object with harness and optional executable, args, env, model, yolo)");
781
811
  let entry;
782
812
  // A parse error is reported without the parser's text: its message can
783
813
  // quote the document, and a definition may carry environment literals.
@@ -790,7 +820,7 @@ async function launchConfigCmd() {
790
820
  } else {
791
821
  try { raw = readFileSync(f, "utf8"); } catch (e) { bail("E_BAD_ARGS", `--file ${f}: ${e.code === "ENOENT" ? "no such file" : e.code || "cannot read"}`); }
792
822
  }
793
- try { entry = JSON.parse(raw); } catch { bail("E_BAD_ARGS", `--file ${f} is not valid JSON (one object with runtime and optional executable, args, env, model, yolo)`); }
823
+ try { entry = JSON.parse(raw); } catch { bail("E_BAD_ARGS", `--file ${f} is not valid JSON (one object with harness and optional executable, args, env, model, yolo)`); }
794
824
  if (args.includes("--keep-env")) {
795
825
  // An editor that saw only redacted values keeps the environment of the
796
826
  // declared definition of that name: a one-time copy into the complete
@@ -801,6 +831,7 @@ async function launchConfigCmd() {
801
831
  if (entry && typeof entry === "object" && Object.keys(current.env || {}).length) entry.env = { ...current.env };
802
832
  }
803
833
  try { validateLaunchConfig(name, entry, `--file ${f}`); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
834
+ if (!Object.hasOwn(entry, "harness")) noteRuntimeName(`runtime in the --file definition (written as harness)`);
804
835
  model[name] = normalizeLaunchConfig(entry);
805
836
  }
806
837
  let next;
@@ -1389,7 +1420,7 @@ async function status() {
1389
1420
  if (s) i.soul = { repoKey: s.repoKey, commit: s.commit, current: s.current?.commit ?? null, status: s.status, ...(s.reason ? { reason: s.reason } : {}) };
1390
1421
  }
1391
1422
  }
1392
- console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}) }, null, 2)); return;
1423
+ console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}), ...envelopeWarnings() }, null, 2)); return;
1393
1424
  }
1394
1425
  console.log(`oats status — agents root ${shortPath(root)}\n`);
1395
1426
  if (ws?.unreachable) console.log(` workspace: unreachable (${ws.unreachable.reason}) — drift unknown\n`);
@@ -1427,7 +1458,7 @@ async function spawnCmd() {
1427
1458
  };
1428
1459
  checkDirectoryOptions(requestedWork); // before anything is resolved or written
1429
1460
  const name = args[1];
1430
- if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>|--name <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--no-launch] [--json]");
1461
+ if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>|--name <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--no-launch] [--json]");
1431
1462
  // Retired boundary flags (maintainer transport ruling): fail LOUDLY before
1432
1463
  // ANY side effect, including root discovery.
1433
1464
  // Local souls (local-agents/) are gone with the workspace model: a soul is a member
@@ -1650,12 +1681,12 @@ async function spawnCmd() {
1650
1681
  // An attached instance's repository is its work tree owner's (derived by the kernel).
1651
1682
  repo: preparedRepo !== undefined ? preparedRepo : ["directory", "attached"].includes(requestedWork || agent.work)
1652
1683
  ? repo : repo || defaultRepo(workspaceOf(root)) || defaultRepo(process.cwd()),
1653
- work: requestedWork, workDir, runtime: flag("runtime"), backend, herdrSocket, yolo, model: flag("model"), branch,
1684
+ work: requestedWork, workDir, harness: harnessFlag(), backend, herdrSocket, yolo, model: flag("model"), branch,
1654
1685
  launchConfig: valueFlag("launch-config"),
1655
1686
  launch: !args.includes("--no-launch"),
1656
1687
  // K6: --preview decides everything and touches nothing; --base <ref>
1657
1688
  // selects a worktree's start point; --model @native-default is the
1658
- // explicit "runtime's own default" (distinct from omitting --model).
1689
+ // explicit "harness's own default" (distinct from omitting --model).
1659
1690
  ...(args.includes("--preview") ? { preview: true, subject: { soul: name, agentsRoot: agentsRootFlag !== undefined ? String(agentsRootFlag) : null, dir: flag("dir") !== undefined && flag("dir") !== true ? String(flag("dir")) : null } } : {}),
1660
1691
  ...(flag("base") !== undefined && flag("base") !== true ? { baseRef: flag("base") } : {}),
1661
1692
  // A confirmed preview binds this apply (K6b): drift → E_DECISION_STALE, nothing created.
@@ -1669,7 +1700,7 @@ async function spawnCmd() {
1669
1700
  // (the deployment's cache had no entry for its commit): the result says so.
1670
1701
  if (prepared) r.soulFetched = soulFetched;
1671
1702
  if (JSON_MODE) { jsonOk(r); return; }
1672
- console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) runtime ${r.runtime}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}; nothing was created${soulFetched ? " (the soul source was fetched to a temporary copy, not kept)" : ""}`);
1703
+ console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) harness ${r.harness}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}; nothing was created${soulFetched ? " (the soul source was fetched to a temporary copy, not kept)" : ""}`);
1673
1704
  return;
1674
1705
  }
1675
1706
  } catch (e) {
@@ -1679,9 +1710,9 @@ async function spawnCmd() {
1679
1710
  // document the message already names. The shared boundary renders it.
1680
1711
  if (TYPED_CLI_FAILURES.has(e?.code)) throw e;
1681
1712
  // A launch refusal (configuration, executable, environment reference,
1682
- // model, runtime) is a fact about the selection, not a spawn-mechanism
1713
+ // model, harness) is a fact about the selection, not a spawn-mechanism
1683
1714
  // failure: it keeps its own code so a GUI can act on it.
1684
- if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_RUNTIME$/.test(e.code)) { bail(e.code, e.message); throw e; }
1715
+ if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_HARNESS$/.test(e.code)) { bail(e.code, e.message); throw e; }
1685
1716
  // An unmet declared requirement is a fact about the soul's configuration
1686
1717
  // (with a remedy), not a spawn-mechanism failure: keep its code and details.
1687
1718
  if (e?.code === "E_REQUIREMENT_INACTIVE") { bail(e.code, e.message, { soul: e.soul, capabilities: e.capabilities, context: e.context, remedy: e.remedy }); throw e; }
@@ -1716,7 +1747,7 @@ async function spawnCmd() {
1716
1747
  instance: r.instance, agent: r.agent, home: r.home, work: r.work,
1717
1748
  branch: r.branch || null, launched: r.launched, warnings: r.warnings || [],
1718
1749
  ...(wakeSchedule ? { wakeSchedule } : {}), ...(wakeScheduleError ? { wakeScheduleError } : {}),
1719
- tmux: r.tmux || null, repo: r.repo || null, runtime: r.runtime || null,
1750
+ tmux: r.tmux || null, repo: r.repo || null, harness: r.harness || null,
1720
1751
  model: r.model || null, parent: r.parentInstance || null,
1721
1752
  sibling: r.siblingInstance || null, relation: r.relation || null,
1722
1753
  spawnOrigin: r.spawnOrigin, attach: r.attach,
@@ -1809,7 +1840,7 @@ function retireCmd() {
1809
1840
  if (r.deferred) {
1810
1841
  if (args.includes("--json")) { console.log(JSON.stringify(r, null, 2)); return; }
1811
1842
  console.log(`Retirement of ${r.retired} (agent ${r.agent}) is ${r.alreadyScheduled ? "already " : ""}scheduled — say any goodbyes now.`);
1812
- console.log(` in ~${r.completesInSec}s a detached completion quiesces this runtime (that is what ends this window), preserves work, runs retire hooks and removes the home`);
1843
+ console.log(` in ~${r.completesInSec}s a detached completion quiesces this harness (that is what ends this window), preserves work, runs retire hooks and removes the home`);
1813
1844
  console.log(` if the completion fails, this window stays, the failure shows in \`oats status\` and at ${shortPath(r.resultPath)}, and \`oats retire ${r.retired}\` retries it`);
1814
1845
  return;
1815
1846
  }
@@ -1916,9 +1947,9 @@ async function sessionCmd() {
1916
1947
  if (model === true) bad("--model needs a model id; omit it to keep the recorded model");
1917
1948
  const launchConfig = flag("launch-config");
1918
1949
  if (launchConfig === true) bad("--launch-config needs a configuration name, or none");
1919
- const runtime = flag("runtime");
1920
- if (runtime === true || (runtime !== undefined && !LAUNCH_RUNTIMES.includes(runtime))) bad(`--runtime must be one of ${LAUNCH_RUNTIMES.join(", ")}`);
1921
- const opts = { model: model || undefined, launchConfig, runtime, yolo: yoloFlag(), env: process.env, ...(await homeLiveTeams(home)) };
1950
+ const harness = harnessFlag();
1951
+ if (harness === true || (harness !== undefined && !LAUNCH_HARNESSES.includes(harness))) bad(`--harness must be one of ${LAUNCH_HARNESSES.join(", ")}`);
1952
+ const opts = { model: model || undefined, launchConfig, harness, yolo: yoloFlag(), env: process.env, ...(await homeLiveTeams(home)) };
1922
1953
  if (args[1] === "restart") {
1923
1954
  const grace = flag("stop-grace");
1924
1955
  if (grace !== undefined) { if (grace === true || !/^\d+$/.test(String(grace)) || Number(grace) < 1 || Number(grace) > 300) bad("--stop-grace needs a number of seconds (1..300) to wait for the harness after SIGTERM"); opts.stopGraceMs = Number(grace) * 1000; }
@@ -1941,7 +1972,7 @@ async function sessionCmd() {
1941
1972
  const file = flag("file");
1942
1973
  if (!file || file === true) throw Object.assign(new Error("session upload needs --file <local path>"), { code: "E_BAD_ARGS" });
1943
1974
  result = uploadAttachment({ file, home: home === true ? undefined : home });
1944
- } else throw Object.assign(new Error("usage: oats session inspect|input|attach|start|restart|receive|upload --home /absolute/home [--text-file path] [--model id] [--launch-config name|none] [--runtime pi|claude|codex] [--yolo|--no-yolo] [--stop-grace seconds] [--name file] [--file path] [--json]"), { code: "E_BAD_ARGS" });
1975
+ } else throw Object.assign(new Error("usage: oats session inspect|input|attach|start|restart|receive|upload --home /absolute/home [--text-file path] [--model id] [--launch-config name|none] [--harness pi|claude|codex] [--yolo|--no-yolo] [--stop-grace seconds] [--name file] [--file path] [--json]"), { code: "E_BAD_ARGS" });
1945
1976
  if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
1946
1977
  } catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message, e.details); }
1947
1978
  }
@@ -2325,7 +2356,7 @@ function versionCmd() {
2325
2356
  // Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
2326
2357
  // runs on resolve/materialize (contract §6); a feature the binary does not implement is
2327
2358
  // never listed.
2328
- 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", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "teams", "settings-declared", "capabilities-private", "layers-from"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
2359
+ console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["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", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "teams", "settings-declared", "capabilities-private", "layers-from", "harness"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
2329
2360
  return;
2330
2361
  }
2331
2362
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -2470,7 +2501,7 @@ async function serverRouteCmd() {
2470
2501
  const value = (name) => { const v = flag(name); if (v === true) bail("E_BAD_ARGS", `--${name} needs a value`); return v; };
2471
2502
  if (!["list", "set", "remove", "preview"].includes(action)) bail("E_BAD_ARGS", "launch-config --server supports list, set, remove and preview");
2472
2503
  const options = { action, name: args[2], context: value("dir"), home: value("home"), instance: value("instance"), soul: value("soul"), agentsRoot: value("agents-root") };
2473
- if (action === "preview") Object.assign(options, { launchConfig: value("launch-config"), runtime: value("runtime"), model: value("model"), yolo: yoloFlag() });
2504
+ if (action === "preview") Object.assign(options, { launchConfig: value("launch-config"), harness: harnessFlag(value), model: value("model"), yolo: yoloFlag() });
2474
2505
  if (action === "set") {
2475
2506
  const file = value("file");
2476
2507
  if (!file) bail("E_BAD_ARGS", "launch-config set needs --file <local JSON file> (or - for stdin)");
@@ -2489,7 +2520,7 @@ async function serverRouteCmd() {
2489
2520
  let out;
2490
2521
  try { out = launchConfigRemote(id, options); } catch (e) { bail(e.code || "E_SSH", e.message); }
2491
2522
  if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
2492
- if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
2523
+ if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
2493
2524
  if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "launch configuration request failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
2494
2525
  console.log(JSON.stringify(out.envelope.result, null, 2));
2495
2526
  return;
@@ -2506,7 +2537,7 @@ async function serverRouteCmd() {
2506
2537
  let routed;
2507
2538
  try { routed = routeCommand(id, "harvest", [inst]); } catch (e) { bail(e.code || "E_SSH", e.message); }
2508
2539
  if (routed.stderr?.trim()) process.stderr.write(routed.stderr.endsWith("\n") ? routed.stderr : routed.stderr + "\n");
2509
- if (JSON_MODE) { console.log(JSON.stringify(routed.envelope, null, 2)); if (!routed.envelope.ok) process.exit(1); return; }
2540
+ if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(routed.envelope), null, 2)); if (!routed.envelope.ok) process.exit(1); return; }
2510
2541
  if (!routed.envelope.ok) die(`${id}: ${routed.envelope.error?.message || "harvest failed"} (${routed.envelope.error?.code || "E_REMOTE"})`);
2511
2542
  const hr = routed.envelope.result;
2512
2543
  console.log(`Harvest on ${id} for ${inst}: ${hr.harvest}${hr.reason ? ` (${hr.reason})` : ""}${hr.instance && hr.harvest === "spawned" ? ` — harvester ${hr.instance}` : ""}`);
@@ -2534,7 +2565,7 @@ async function serverRouteCmd() {
2534
2565
  let out;
2535
2566
  try { out = scheduleRemote(id, rest); } catch (e) { bail(e.code || "E_SSH", e.message); }
2536
2567
  if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
2537
- if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
2568
+ if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
2538
2569
  if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "schedule command failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
2539
2570
  console.log(JSON.stringify(out.envelope.result, null, 2));
2540
2571
  return;
@@ -2547,7 +2578,7 @@ async function serverRouteCmd() {
2547
2578
  let out;
2548
2579
  try { out = inspectRemote(id, addr); } catch (e) { bail(e.code || "E_SSH", e.message); }
2549
2580
  if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
2550
- if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
2581
+ if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
2551
2582
  if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "inspect failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
2552
2583
  const r = out.envelope.result;
2553
2584
  console.log(`${r.instance || r.home} on ${id}: ${r.present ? `present, ${r.state || "unknown"}` : "not present"}${r.backend ? ` (${r.backend})` : ""}`);
@@ -2555,12 +2586,12 @@ async function serverRouteCmd() {
2555
2586
  }
2556
2587
  if (args[1] === "start" || args[1] === "restart") {
2557
2588
  const value = (name) => { const v = flag(name); if (v === true) bail("E_BAD_ARGS", `--${name} needs a value`); return v; };
2558
- const choices = { ...addr, model: value("model"), launchConfig: value("launch-config"), runtime: value("runtime"), yolo: yoloFlag() };
2589
+ const choices = { ...addr, model: value("model"), launchConfig: value("launch-config"), harness: harnessFlag(value), yolo: yoloFlag() };
2559
2590
  if (flag("stop-grace") !== undefined) bail("E_BAD_ARGS", "--stop-grace is currently supported on the execution host; omit it to use the remote restart's default wait");
2560
2591
  let out;
2561
2592
  try { out = (args[1] === "restart" ? restartRemote : startRemote)(id, choices); } catch (e) { bail(e.code || "E_SSH", e.message); }
2562
2593
  if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
2563
- if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
2594
+ if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
2564
2595
  if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "start failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
2565
2596
  const r = out.envelope.result;
2566
2597
  console.log(`Started ${r.instance || r.home} on ${id} (${r.backend}${r.model ? `, model ${r.model}` : ""}, ${r.reused === "pane" ? "in its existing pane" : r.reused === "adopted" ? "adopted the pending session" : "new window"})`);
@@ -2626,7 +2657,7 @@ async function serverRouteCmd() {
2626
2657
  catch (e) { bail(e.code || "E_SSH", e.message); }
2627
2658
  const { envelope, stderr } = routed;
2628
2659
  if (stderr && stderr.trim()) process.stderr.write(stderr.endsWith("\n") ? stderr : stderr + "\n");
2629
- if (JSON_MODE) { console.log(JSON.stringify(envelope, null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
2660
+ if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(envelope), null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
2630
2661
  if (!envelope.ok && !(cmd === "retire" && envelope.result)) die(`${id}: ${envelope.error?.message || "remote command failed"} (${envelope.error?.code || "E_REMOTE"})`);
2631
2662
  const r = envelope.result;
2632
2663
  const target = r.target || {};
@@ -2839,7 +2870,7 @@ Usage:
2839
2870
  <file> [--json] upload's remote half)
2840
2871
  oats session start --home <absolute-home> start a STOPPED instance again in its existing home
2841
2872
  [--model m] [--launch-config n|none] (recorded recipe as is; a selection re-resolves it
2842
- [--runtime r] [--yolo|--no-yolo] against the scope; a named configuration is a unit)
2873
+ [--harness r] [--yolo|--no-yolo] against the scope; a named configuration is a unit)
2843
2874
  oats session restart --home <abs-home> stop the running harness (SIGTERM, bounded wait,
2844
2875
  [same flags] [--stop-grace <s>] never escalated) and start it again in place under
2845
2876
  the same lock; a stop that is not observed is
@@ -2854,7 +2885,7 @@ Usage:
2854
2885
  [--relative-to <instance>] new instance to an existing one; --parent X
2855
2886
  [--relative-root <agents-root>] disambiguates same-named team anchors
2856
2887
  [--work worktree|checkout|attached|workspace|directory] = sugar for --relative-to X --relation
2857
- [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
2888
+ [--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
2858
2889
  [--no-launch] [--json]
2859
2890
  with team: declared, unknown souls
2860
2891
  resolve across the team scope's repos
@@ -2868,7 +2899,7 @@ Usage:
2868
2899
  [--keep-dir] [--json] CALLING instance: the window dies, then
2869
2900
  a detached external retirement runs
2870
2901
  oats inspect [--dir <scope>] [--soul <name> one authoritative JSON answer for a GUI: souls
2871
- [--agents-root <abs>]] [--home <abs>] (runtime defaults, editability, instructions),
2902
+ [--agents-root <abs>]] [--home <abs>] (harness defaults, editability, instructions),
2872
2903
  [--json] installed capabilities with health, effective
2873
2904
  layer bindings and activation, declared
2874
2905
  operations with availability; --home answers the
@@ -2882,7 +2913,7 @@ Usage:
2882
2913
  relayed; a view answers {documents: [...]}
2883
2914
  oats launch-config list [--dir <scope> named launch configurations effective at a scope,
2884
2915
  | --home <abs> | --soul <name>] a home's recorded context or a soul's own scope:
2885
- [--agents-root <abs>] [--json] runtime, executable, args, env (values redacted,
2916
+ [--agents-root <abs>] [--json] harness, executable, args, env (values redacted,
2886
2917
  references shown), model, yolo; the closest
2887
2918
  declaring scope provides the whole entry
2888
2919
  oats launch-config set <name> --file <j> declare or replace one at this scope from a JSON
@@ -2890,11 +2921,11 @@ Usage:
2890
2921
  --keep-env copies the effective definition's env)
2891
2922
  oats launch-config remove <name> remove this scope's declaration; an ancestor's,
2892
2923
  [--dir <scope>] [--json] if any, becomes effective again
2893
- oats launch-config preview what a start would run: resolved runtime, model,
2924
+ oats launch-config preview what a start would run: resolved harness, model,
2894
2925
  (--home <abs> | --soul <name>) yolo, executable, argv, environment (redacted),
2895
2926
  [--launch-config <name>|none] command and preflight; read-only, nothing
2896
- [--runtime r] [--model m] started; a named configuration is a unit, so
2897
- [--yolo | --no-yolo] --json a disagreeing --runtime is refused
2927
+ [--harness r] [--model m] started; a named configuration is a unit, so
2928
+ [--yolo | --no-yolo] --json a disagreeing --harness is refused
2898
2929
  oats doctor [dir] [--soul <name>] [--json] resolved targets, trust, requirements;
2899
2930
  --soul shows final composed AGENTS.md
2900
2931
  oats update [--check] [--yes] check npm for a newer kernel+pi bridge and