@awebai/oats 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -208,7 +208,7 @@ Begin with a small team and a real piece of work:
208
208
  4. Create instances for their assignments.
209
209
  5. Verify that work, communication, learning and handoff behave as intended.
210
210
 
211
- On a machine with Node.js 22+, Git and tmux, and a workspace repository to point at:
211
+ On a machine with Node.js 22+, Git (2.45+ to fetch only what OATS reads; an older git fetches whole trees) and tmux, and a workspace repository to point at:
212
212
 
213
213
  ```bash
214
214
  npm install -g @awebai/oats
package/bin/oats.mjs CHANGED
@@ -10,6 +10,8 @@
10
10
  * oats package add|remove ... edit `packages:` in the workspace file
11
11
  * oats workspace status membership table, packages
12
12
  * oats capabilities | oats souls every visible item of the workspace
13
+ * oats capabilities show <name> [--member <repoKey> | --package <id>] [--file <path>] [--json]
14
+ * what one capability ships (inject text, skill files)
13
15
  *
14
16
  * Workspace model v2 (docs/design/2026-09-23-workspace-module-contracts.md §6):
15
17
  * nothing is installed. `oats-local.yaml` names the workspace, `oats sync`
@@ -28,7 +30,7 @@ import {
28
30
  LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
29
31
  capabilityManifests, capabilityTrust, capabilityExecutablePath,
30
32
  officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
31
- findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, recordedKernelBin, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, validateLaunchConfigDefaults, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
33
+ findInstanceHome, findInstanceHomes, enclosingInstanceHome, logicalCwd, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, recordedKernelBin, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, validateLaunchConfigDefaults, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
32
34
  } from "../lib/core.mjs";
33
35
  import {
34
36
  writeFileAtomic, LOCK_FILE, readLock, readLockIfPresent, writeLock, resolvePackages, memoizedRemote,
@@ -789,7 +791,7 @@ function launchPreview(bail) {
789
791
  let plan;
790
792
  try { plan = planLaunch({ home, instance, meta, contextDir: context, agentLike, selection: sel, resolvedCfg: r, preview: true }); } catch (e) { bail(e.code || "E_BAD_ARGS", e.message); }
791
793
  const { recipe } = plan;
792
- const command = renderLaunchRecipe(recipe, { home, instance, redact: true });
794
+ const command = renderLaunchRecipe(recipe, { home, instance, redact: true, trustHome: plan.trustHome });
793
795
  const d = describeLaunchCommand(command);
794
796
  const environment = d.environment.map((e) => e.reference && recipe.env[e.name]?.fromEnv ? { name: e.name, fromEnv: recipe.env[e.name].fromEnv } : e);
795
797
  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, launchConfigDefault: recipe.launchConfigDefault === true, 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 });
@@ -1027,10 +1029,14 @@ async function readinessCmd() {
1027
1029
  let readSession = null;
1028
1030
  /** The validated `--max-age` seconds (checked once at dispatch: maxAgeRefusal), null when not given. */
1029
1031
  let maxAgeGiven = null;
1032
+ /** The remote budget of this command's reads (ms), or null: only the deployment reads `status` and `workspace
1033
+ * status` have one (remote.mjs READ_REMOTE_BUDGET_MS; OATS_READ_REMOTE_BUDGET_MS overrides it), so they answer
1034
+ * inside a caller's own limit. Spawn, sync and every other verb read with no deadline. */
1035
+ let readBudgetMs = null;
1030
1036
  function commandSession() {
1031
1037
  if (!readSession) {
1032
- readSession = remoteModule.createReadSession({ maxAge: maxAgeGiven ?? 0 });
1033
- process.on("exit", () => readSession.closeNow());
1038
+ readSession = remoteModule.createReadSession({ maxAge: maxAgeGiven ?? 0, ...(readBudgetMs !== null ? { deadline: Date.now() + readBudgetMs } : {}) });
1039
+ process.on("exit", () => { sayReadNotices(); readSession.closeNow(); });
1034
1040
  for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143], ["SIGHUP", 129]]) process.once(signal, () => {
1035
1041
  readSession.closeNow();
1036
1042
  // Another handler (a scheduler lock's release) exits on its own after this one.
@@ -1039,15 +1045,22 @@ function commandSession() {
1039
1045
  }
1040
1046
  return readSession;
1041
1047
  }
1048
+ /** What the read session found worth telling the operator (a remote that cannot serve partial fetches),
1049
+ * once, on stderr when the command ends: stdout, and so every JSON answer, is unchanged. */
1050
+ function sayReadNotices() {
1051
+ for (const notice of readSession?.notices.splice(0) ?? []) process.stderr.write(`oats: warning: ${notice}\n`);
1052
+ }
1042
1053
  /** Which kernel command forms take --max-age: THE allow-list (docs/desktop-cli-api.md "Observation reuse").
1043
1054
  * → null when this form reads with observation reuse, else the E_BAD_ARGS message. `head` is argv before `--`. */
1044
- const MAX_AGE_READS = "status, workspace status, souls, capabilities, inspect --soul|--home, and the read forms of teams and soul teams";
1055
+ const MAX_AGE_READS = "status, workspace status, souls, capabilities, capabilities show, inspect --soul|--home, spawn --preview, and the read forms of teams and soul teams";
1045
1056
  function maxAgeRefusal(command, head) {
1046
1057
  const word = (i) => (head[i] !== undefined && !head[i].startsWith("--") ? head[i] : undefined);
1047
1058
  const refuse = (form) => `--max-age is not accepted by \`oats ${form}\`: only the read verbs reuse observations (${MAX_AGE_READS})`;
1048
1059
  if (head.includes("--server")) return "--max-age cannot be combined with --server: observation reuse is local to this machine";
1049
1060
  switch (command) {
1050
1061
  case "status": case "souls": case "capabilities": case "inspect": return null;
1062
+ // A preview reads (feature spawn-preview-max-age); an apply always observes live.
1063
+ case "spawn": return head.includes("--preview") ? null : refuse("spawn");
1051
1064
  case "workspace": return word(1) === "status" ? null : refuse(["workspace", word(1)].filter(Boolean).join(" "));
1052
1065
  case "teams": return word(1) === undefined ? null : refuse(`teams ${word(1)}`);
1053
1066
  case "soul": {
@@ -1638,14 +1651,25 @@ async function soulCmd() {
1638
1651
  if (doc.teams.length) printTable(["team", "id", "from", "why"], doc.teams.map((t) => [t.default ? `${t.label} (default)` : t.label, t.team ?? "(no id yet)", t.from, t.via.join(",")]));
1639
1652
  }
1640
1653
 
1641
- /** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
1642
- async function itemsCmd(kind) {
1643
- const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
1654
+ /** The catalog's rows of `kind` ("capabilities" | "souls") as `oats capabilities` / `oats souls` list them:
1655
+ * one discovery (honouring --max-age), the lock, workspaceItems. → { ctx, discovery, lock, items }. */
1656
+ async function catalogRows(kind, bail) {
1644
1657
  const ctx = workspaceContext(bail);
1645
1658
  const discovery = await discoverForCli(ctx, bail);
1646
1659
  let lock;
1647
1660
  try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
1648
- const items = workspaceItems(discovery, lock, ctx.local, ctx.deploymentDir)[kind];
1661
+ return { ctx, discovery, lock, items: workspaceItems(discovery, lock, ctx.local, ctx.deploymentDir)[kind] };
1662
+ }
1663
+
1664
+ /** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
1665
+ async function itemsCmd(kind) {
1666
+ const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
1667
+ if (kind === "capabilities") {
1668
+ const { words } = capabilitiesArgv();
1669
+ if (words[0] === "show") return capabilityShowCmd(bail);
1670
+ if (words.length) return bail("E_BAD_ARGS", `unknown capabilities subcommand ${JSON.stringify(words[0])}: \`oats capabilities\` lists the catalog, \`oats capabilities show <name>\` reads one capability`, { subcommand: words[0] });
1671
+ }
1672
+ const { ctx, discovery, lock, items } = await catalogRows(kind, bail);
1649
1673
  // One command's reads at a commit are shared: many souls resolve over the same manifests and listings.
1650
1674
  const remote = memoizedRemote(remoteModule);
1651
1675
  if (kind === "capabilities") await capabilityFacts(items, discovery, lock, ctx, remote);
@@ -1660,6 +1684,74 @@ async function itemsCmd(kind) {
1660
1684
  if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
1661
1685
  }
1662
1686
 
1687
+ /** `oats capabilities` argv after the command: its words (the subcommand first) with every flag and a value
1688
+ * flag's value set aside, and the first flag neither form takes (`unknown`). */
1689
+ function capabilitiesArgv() {
1690
+ const VALUE_FLAGS = new Set(["--member", "--package", "--file", "--dir", "--max-age", "--server"]);
1691
+ const words = [];
1692
+ let unknown;
1693
+ for (let i = 1; i < args.length; i++) {
1694
+ if (VALUE_FLAGS.has(args[i])) { if (args[i + 1] !== undefined && !args[i + 1].startsWith("--")) i++; continue; }
1695
+ if (args[i].startsWith("--")) { if (args[i] !== "--json") unknown ??= args[i]; continue; }
1696
+ words.push(args[i]);
1697
+ }
1698
+ return { words, unknown };
1699
+ }
1700
+
1701
+ /** `oats capabilities show <name> [--member <repoKey> | --package <id>] [--file <path>] [--dir] [--json]`
1702
+ * (feature capability-show, capabilityShowApi 1; lib/capability-show.mjs): what one catalog row ships, read
1703
+ * at that row's commit — the rows are `oats capabilities`'s own (catalogRows). */
1704
+ async function capabilityShowCmd(bail) {
1705
+ const S = await import("../lib/capability-show.mjs");
1706
+ if (flag("server") !== undefined) return bail("E_BAD_ARGS", "oats capabilities show reads this machine's workspace only: --server is not accepted");
1707
+ const { words, unknown } = capabilitiesArgv();
1708
+ if (unknown) return bail("E_BAD_ARGS", `oats capabilities show: unknown flag ${unknown}`, { flag: unknown });
1709
+ const positionals = words.slice(1);
1710
+ if (positionals.length !== 1) return bail("E_BAD_ARGS", positionals.length ? `oats capabilities show takes one capability name, got ${positionals.map((p) => JSON.stringify(p)).join(" ")}` : "oats capabilities show needs a capability name (`oats capabilities` lists them)");
1711
+ const name = positionals[0];
1712
+ const memberArg = valueFlag("member"), packageArg = valueFlag("package"), file = valueFlag("file");
1713
+ if (memberArg !== undefined && packageArg !== undefined) return bail("E_BAD_ARGS", "choose --member <repoKey> or --package <id>, not both");
1714
+ if (file !== undefined && S.unsafeFilePath(file)) return bail("E_CAPABILITY_FILE_UNSAFE", `${JSON.stringify(file)} is not a relative path inside the capability`, { path: file });
1715
+ const { ctx, discovery, lock, items } = await catalogRows("capabilities", bail);
1716
+ // --member takes the repo key a row shows, or any ref spelling of it (compared on the canonical key).
1717
+ let member = memberArg ?? null;
1718
+ if (member !== null && !items.some((r) => r.repoKey === member)) { try { member = remoteModule.parseRepoRef(member).key; } catch { /* matches no row */ } }
1719
+ const remote = memoizedRemote(remoteModule);
1720
+ const catalog = (() => { try { return officialPackageCatalog(); } catch { return null; } })();
1721
+ let doc;
1722
+ try {
1723
+ const row = S.selectCapabilityRow(items, name, { member, package: packageArg ?? null });
1724
+ const source = await S.capabilitySource(row, { discovery, lock, catalog, remote, remoteOptions: ctx.remoteOptions });
1725
+ doc = file === undefined ? await S.capabilityShow(source, { remote, remoteOptions: ctx.remoteOptions }) : await S.capabilityFile(source, file, { remote, remoteOptions: ctx.remoteOptions });
1726
+ } catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance); throw e; }
1727
+ if (JSON_MODE) { jsonOk(withObservation(doc)); return; }
1728
+ if (file !== undefined) {
1729
+ const f = doc.file;
1730
+ if (f.binary) console.log(`(${f.path}: binary, ${formatBytes(f.bytes)} — not shown)`);
1731
+ else {
1732
+ process.stdout.write(f.text.endsWith("\n") || f.text === "" ? f.text : `${f.text}\n`);
1733
+ if (f.truncated) console.log(`(${f.path}: truncated — ${formatBytes(f.bytes)}, the first ${formatBytes(S.TEXT_LIMIT)} shown)`);
1734
+ }
1735
+ return;
1736
+ }
1737
+ console.log(`${doc.name} — ${doc.kind === "member" ? `member ${doc.repoKey}` : `package ${doc.package} v${doc.version} (${doc.repoKey})`} @ ${short(doc.commit)}, ${doc.path}\n`);
1738
+ const size = (bytes) => (bytes === null ? "unreadable" : formatBytes(bytes));
1739
+ console.log(` inject: ${doc.inject ? `${doc.inject.path ?? "(unsafe path)"} (${size(doc.inject.bytes)}${doc.inject.binary ? ", binary" : ""}${doc.inject.truncated ? ", truncated" : ""})` : "(none)"}`);
1740
+ if (doc.skills === null) console.log(" skills: (cannot be listed — see problems)");
1741
+ else if (!doc.skills.length) console.log(" skills: (none)");
1742
+ else {
1743
+ console.log(" skills:");
1744
+ for (const skill of doc.skills) {
1745
+ console.log(` ${skill.name} ${skill.path}`);
1746
+ if (skill.files === null) console.log(" (files cannot be listed — see problems)");
1747
+ for (const f of skill.files ?? []) console.log(` ${f.path} ${size(f.bytes)}`);
1748
+ if (skill.filesTruncated) console.log(` … more files (the first ${S.FILES_PER_SKILL} are listed)`);
1749
+ }
1750
+ }
1751
+ for (const p of doc.problems) console.log(` problem: ${p.code}${p.path ? ` ${p.path}` : ""} — ${p.message}`);
1752
+ console.log(`\n a file's text: oats capabilities show ${doc.name}${doc.kind === "package" ? ` --package ${doc.package}` : ""} --file <path>`);
1753
+ }
1754
+
1663
1755
  /** Capability rows' manifest facts (feature desktop-facts): layer, description, and what each provides
1664
1756
  * (skills, commands, hooks by name) — member rows from discovery's manifest, package rows from the manifest
1665
1757
  * at the locked commit (the sync cache; no network beyond sync's). An unreadable package leaves its rows' facts null. */
@@ -1884,8 +1976,9 @@ async function status() {
1884
1976
  const livenessWord = (i) => i.running === true ? "RUNNING" : i.running === false ? "idle" : "unknown";
1885
1977
  /** The flags `oats spawn` reads: those taking a value, and switches. `--provider` takes two words.
1886
1978
  * `--instance` is refused by a local spawn (with its replacement) but still travels to an older
1887
- * host through `--server`, whose route reads it. */
1888
- const SPAWN_VALUE_FLAGS = new Set(["agents-root", "backend", "base", "branch", "dir", "expect-decision", "harness", "idempotency-key", "instance", "launch-config", "model", "name", "parent", "purpose", "relation", "relative-root", "relative-to", "repo", "runtime", "task", "task-file", "trigger-event", "wake-cron", "wake-every", "wake-file", "wake-json", "wake-message", "wake-message-file", "wake-tz", "work", "work-dir"]);
1979
+ * host through `--server`, whose route reads it. `--max-age` reaches here only on a preview: the
1980
+ * dispatch allow-list (maxAgeRefusal) refuses it on an apply. */
1981
+ const SPAWN_VALUE_FLAGS = new Set(["agents-root", "backend", "base", "branch", "dir", "expect-decision", "harness", "idempotency-key", "instance", "launch-config", "max-age", "model", "name", "parent", "purpose", "relation", "relative-root", "relative-to", "repo", "runtime", "task", "task-file", "trigger-event", "wake-cron", "wake-every", "wake-file", "wake-json", "wake-message", "wake-message-file", "wake-tz", "work", "work-dir"]);
1889
1982
  const SPAWN_SWITCHES = new Set(["allow-child-spawns", "json", "no-child-spawns", "no-launch", "no-yolo", "preview", "yolo"]);
1890
1983
  /** Why `argv` (after `spawn`, the soul first) is not a spawn, or undefined: a positional after the
1891
1984
  * soul or a flag spawn does not read is never ignored. A value flag consumes its value exactly as
@@ -2141,7 +2234,7 @@ async function spawnCmd() {
2141
2234
  // A workspace preview may have fetched the soul's SOURCE to a temporary copy
2142
2235
  // (the deployment's cache had no entry for its commit): the result says so.
2143
2236
  if (prepared) r.soulFetched = soulFetched;
2144
- if (JSON_MODE) { jsonOk(r); return; }
2237
+ if (JSON_MODE) { jsonOk(withObservation(r)); return; }
2145
2238
  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})`}${r.launchConfig ? ` via launch configuration ${r.launchConfig}${r.launchConfigDefault ? ` (this machine's ${r.harness} default)` : ""}` : ""}${r.yolo ? " YOLO" : ""}; nothing was created${soulFetched ? " (the soul source was fetched to a temporary copy, not kept)" : ""}`);
2146
2239
  return;
2147
2240
  }
@@ -2758,8 +2851,9 @@ async function capabilityCommand() {
2758
2851
  let activeIds;
2759
2852
  let context = process.cwd();
2760
2853
  let teamCtx, homeMeta, homeTeamCtx;
2761
- // OATS_INSTANCE_HOME is the canonical identity; the older names still count.
2762
- const instanceHome = process.env.OATS_INSTANCE_HOME || process.env.PI_AGENT_HOME || process.env.OATS_HOME;
2854
+ // OATS_INSTANCE_HOME is the canonical identity; the older names still count. With none set
2855
+ // (a harness that strips the session env), the home enclosing the cwd.
2856
+ const instanceHome = process.env.OATS_INSTANCE_HOME || process.env.PI_AGENT_HOME || process.env.OATS_HOME || enclosingInstanceHome(logicalCwd());
2763
2857
  const metaFile = instanceHome && join(instanceHome, "instance.json");
2764
2858
  // Capability-id keyed — never answer for `constructor`/`toString`. Belt and
2765
2859
  // braces: the ids come from instance.json, which spawn wrote from resolved
@@ -2937,7 +3031,7 @@ function versionCmd() {
2937
3031
  // Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
2938
3032
  // runs on resolve/materialize (contract §6); a feature the binary does not implement is
2939
3033
  // never listed.
2940
- console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], 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", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "launch-config-default"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
3034
+ console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], 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", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "spawn-preview-max-age", "launch-config-default", "capability-show"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2, capabilityShowApi: 1 }));
2941
3035
  return;
2942
3036
  }
2943
3037
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -3340,6 +3434,10 @@ try {
3340
3434
  maxAgeGiven = Number(raw);
3341
3435
  activateLocalInputs(); // observation.localRevision: every local config read from here on is recorded
3342
3436
  }
3437
+ if (kernelArgv && !head.includes("--server") && (cmd === "status" || (cmd === "workspace" && head[1] === "status"))) {
3438
+ const override = Number(process.env.OATS_READ_REMOTE_BUDGET_MS);
3439
+ readBudgetMs = Number.isSafeInteger(override) && override > 0 ? override : remoteModule.READ_REMOTE_BUDGET_MS;
3440
+ }
3343
3441
  const inherited = ["OATS_RESOLUTION", "OATS_DEPLOYMENT"].filter((k) => process.env[k]);
3344
3442
  if (inherited.length && cmd !== "version") refuse(`this environment carries a captured context (${inherited.join(", ")}): the captured/portable path was removed in 0.26, and nothing is run against the current context in its place — retire the captured home and re-spawn it from the deployment`, { inherited });
3345
3443
  if (cmd === "inspect" && head.includes("--request")) {
@@ -3543,6 +3641,9 @@ Usage:
3543
3641
  [--provider <capability> <key>=<value>] a provider setting for this spawn only
3544
3642
  (repeatable; dotted keys nest; recorded in
3545
3643
  instance.json providers.<capability>)
3644
+ [--preview [--max-age <s>]] decide everything, create nothing; the JSON's
3645
+ decision binds an apply (--expect-decision <rev>);
3646
+ --max-age reuses recent heads (preview only)
3546
3647
  oats retire <instance> [--force] retire an instance (window, hooks,
3547
3648
  [--self] [--delete-branch] worktree, home); --self = retire the
3548
3649
  [--keep-dir] [--json] CALLING instance: the window dies, then
@@ -3594,6 +3695,10 @@ Usage:
3594
3695
  oats capabilities [--dir <d>] [--json] every capability of every confirmed member (a
3595
3696
  [--max-age <s>] private one is listed as repo-owned: usable only by
3596
3697
  its own repo's souls) + the locked packages
3698
+ oats capabilities show <name> [--member <repoKey> | --package <id>] [--file <path>]
3699
+ [--dir <d>] [--json] [--max-age <s>] what one capability of that list ships, at its commit:
3700
+ its inject (text) and each skill's files (sizes);
3701
+ --file <path>: one listed file's text
3597
3702
  oats souls [--dir <d>] [--json] every soul of every confirmed member + external souls
3598
3703
  [--max-age <s>] (souls have no private mode), with origin
3599
3704
  (member <key> @ <commit> | package <id> v<ver>) and its
@@ -3660,15 +3765,17 @@ The turn record (core — every conversation captured, searchable, replicated):
3660
3765
 
3661
3766
  Observation reuse (feature observe-max-age):
3662
3767
  --max-age <seconds> on the read verbs only — status, workspace status,
3663
- souls, capabilities, inspect --soul|--home, and the
3664
- read forms of teams and soul teams — reuse a remote
3665
- head observation up to <seconds> old (0–86400; 0 is
3666
- live) instead of asking the remote again; the JSON
3768
+ souls, capabilities, inspect --soul|--home,
3769
+ spawn --preview (feature spawn-preview-max-age),
3770
+ and the read forms of teams and soul teams —
3771
+ reuse a remote head observation up to <seconds>
3772
+ old (0–86400; 0 is live) instead of asking the
3773
+ remote again; the JSON
3667
3774
  then carries observation { observedAt (the oldest
3668
3775
  head used), reused, localRevision (a digest of
3669
3776
  the local configuration read) }. Refused
3670
3777
  (E_BAD_ARGS) by every other command, an edit form,
3671
- and with --server
3778
+ a spawn apply, and with --server
3672
3779
 
3673
3780
  Layers: ${LAYERS.join(", ")}. Workspace model v2: docs/design/2026-09-23-workspace-module-contracts.md.`;
3674
3781
  }
@@ -3681,5 +3788,6 @@ Layers: ${LAYERS.join(", ")}. Workspace model v2: docs/design/2026-09-23-workspa
3681
3788
  die(e.message);
3682
3789
  } finally {
3683
3790
  // The command's read session: every `git cat-file --batch` child ends before the process does.
3791
+ sayReadNotices();
3684
3792
  if (readSession) await readSession.close();
3685
3793
  }
@@ -246,6 +246,23 @@ discovery; the details are in [souls-and-instances.md](souls-and-instances.md).
246
246
  Change a capability's inject or skills in its repository, then spawn a new
247
247
  instance: the generated files are not a source.
248
248
 
249
+ See what a capability ships before any instance has it:
250
+
251
+ ```bash
252
+ oats capabilities show oats.okf # inject path and size, each skill's files, problems
253
+ oats capabilities show oats.okf --file injects/okf.md # one listed file's text
254
+ oats capabilities show nw-house-style --member github.com/nw/agents --json
255
+ ```
256
+
257
+ `oats capabilities show <name>` reads one row of `oats capabilities` at that
258
+ row's commit (a package at its locked commit, after spawn's lock check): the
259
+ inject text exactly as committed, and each skill, enumerated as a spawn
260
+ enumerates it, with its description and files. `--file <path>` prints one
261
+ file the show lists (the inject or a skill file), and no other file of the
262
+ capability. Use `--member <repoKey>` or `--package <id>` when two rows share
263
+ the name. The JSON contract is in
264
+ [desktop-cli-api.md](desktop-cli-api.md#oats-capabilities-show).
265
+
249
266
  Inspect a composition before it exists:
250
267
 
251
268
  ```bash
@@ -355,6 +372,11 @@ for this contract. Hyphenated vendors are also excluded because translating a
355
372
  hyphen to `_` would let `aweb-evil.*` collide with names already inside
356
373
  `aweb.*`'s `AWEB_*` namespace.
357
374
 
375
+ Hook environment values must not be secrets. A codex launch also passes them
376
+ to Codex as command-line arguments (`-c shell_environment_policy.set.<NAME>=…`,
377
+ so its tool commands see them), and any local user can read those. A secret
378
+ reaches a launch through a launch configuration's environment reference.
379
+
358
380
  A manifest's `settings.<key>` may carry `hostOnly: true`. Such a
359
381
  key is a fact about the machine — a custody directory, a state root — and the
360
382
  resolver accepts it only from the deployment's own `oats-local.yaml`
@@ -330,6 +330,6 @@ Environment: `OATS_REMOTE_CACHE` relocates the fetch cache (which also holds
330
330
  the bounded parsed-read cache and the observations `--max-age` reuses; all of
331
331
  it is safe to delete); `OATS_PACKAGE_CATALOG` names an alternative package
332
332
  catalog file. The read verbs (`status`, `workspace status`, `souls`,
333
- `capabilities`, `inspect`, and the read forms of `teams` and `soul teams`)
334
- take `--max-age <seconds>` to reuse a remote head observed that recently
333
+ `capabilities`, `inspect`, the read forms of `teams` and `soul teams`, and
334
+ `spawn --preview`) take `--max-age <seconds>` to reuse a remote head observed that recently
335
335
  ([Observation reuse](desktop-cli-api.md#observation-reuse-feature-observe-max-age-oats-0311)).
@@ -55,8 +55,9 @@ symlink enters as `symlink:<target>`; empty directories and a top-level `.git/`
55
55
  ### 1.3 Access, cache, failures
56
56
 
57
57
  - Git uses the operator's own configuration and credentials; `GIT_TERMINAL_PROMPT=0`,
58
- `GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call.
59
- - A commit is fetched depth 1 (no blob filter) into a bare cache `<cacheDir>/<sha256(key)>/` (default
58
+ `GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call; 10 minutes for the fetch of a commit.
59
+ - A commit is fetched depth 1 with its trees and its blobs up to 64 KiB (larger blobs on demand, when a read
60
+ needs them; whole trees from a server without partial fetches; awebai/oats#384) into a bare cache `<cacheDir>/<sha256(key)>/` (default
60
61
  `~/.cache/oats/remotes`; the CLI honours `OATS_REMOTE_CACHE`) and pinned as `refs/oats/commits/<oid>`.
61
62
  The cache may be wiped at any time. Operations on one cache repo are serialized.
62
63
  - Failures: `E_REMOTE_UNREADABLE { url, key, reason }`, `reason` ∈ `auth`, `not-found`, `network`,