@awebai/oats 0.33.0 → 0.34.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
@@ -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`
@@ -389,7 +391,7 @@ async function workspaceTarget(bail, { command, liveTeams = true }) {
389
391
  try { meta = JSON.parse(readFileSync(join(homeFlag, "instance.json"), "utf8")); } catch (e) { return bail("E_SESSION_UNKNOWN", `${homeFlag} is not an OATS instance home (${e.code === "ENOENT" ? "no instance.json" : e.message})`); }
390
392
  if (isCapturedHome(meta)) { const e = capturedHomeRefusal(homeFlag, "nothing was read"); return bail(e.code, e.message, e.details); }
391
393
  if (!meta || typeof meta.modules !== "object" || meta.modules === null) return bail("E_UNSUPPORTED_MODE", `${homeFlag} is not a workspace-model home (it records no modules): it was spawned by an earlier kernel — re-spawn it from the deployment`);
392
- if (soulFlag && soulFlag !== meta.agent) return bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of ${homeFlag} (${meta.agent})`);
394
+ if (soulFlag && !(await import("../lib/instance-resolution.mjs")).homeSoulMatches(soulFlag, meta)) return bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of ${homeFlag} (${meta.agent})`);
393
395
  const deployment = dirname(dirname(dirname(dirname(realOrResolved(homeFlag)))));
394
396
  // A v2 home lives at <deployment>/agents/<soul>/instances/<name>: its deployment is
395
397
  // derived, so it must hold oats-local.yaml EXACTLY there (never found by walking up).
@@ -535,8 +537,9 @@ async function workspaceOperation(t, { bail, address, layer, opName }) {
535
537
  const settings = lp.settings;
536
538
  const cwd = op.context === "home" ? t.home : t.deployment;
537
539
  const env = { ...lp.env(mod.name, settings), OATS_OPERATION: address, OATS_CONTEXT: t.deployment, OATS_ROOT: t.agentsRoot, PI_AGENTS_ROOT: t.agentsRoot };
538
- if (op.context === "home") Object.assign(env, { OATS_INSTANCE: t.meta.instance, OATS_INSTANCE_HOME: t.home, OATS_HOME: t.home, PI_AGENT_INSTANCE: t.meta.instance, PI_AGENT_HOME: t.home });
539
- else for (const k of ["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "PI_AGENT_INSTANCE", "PI_AGENT_HOME"]) delete env[k];
540
+ // The home's identity, never one inherited from the caller (the reserved PI_AGENT_* names included).
541
+ for (const k of ["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "PI_AGENT_INSTANCE", "PI_AGENT_HOME"]) delete env[k];
542
+ if (op.context === "home") Object.assign(env, { OATS_INSTANCE: t.meta.instance, OATS_INSTANCE_HOME: t.home, OATS_HOME: t.home });
540
543
  await readSession?.closeBatches(); // no idle `git cat-file --batch` child held for the provider's whole run
541
544
  const r = spawnSync("node", [abs, ...rest, ...argFlags, "--json"], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024, timeout: OPERATION_TIMEOUT_MS, killSignal: "SIGTERM" });
542
545
  finishOperation({ r, bail, address, provider, op, argFlags, cwd, home: t.home, meta: t.meta, api: INSPECT_OPERATIONS_API });
@@ -1027,9 +1030,13 @@ async function readinessCmd() {
1027
1030
  let readSession = null;
1028
1031
  /** The validated `--max-age` seconds (checked once at dispatch: maxAgeRefusal), null when not given. */
1029
1032
  let maxAgeGiven = null;
1033
+ /** The remote budget of this command's reads (ms), or null: only the deployment reads `status` and `workspace
1034
+ * status` have one (remote.mjs READ_REMOTE_BUDGET_MS; OATS_READ_REMOTE_BUDGET_MS overrides it), so they answer
1035
+ * inside a caller's own limit. Spawn, sync and every other verb read with no deadline. */
1036
+ let readBudgetMs = null;
1030
1037
  function commandSession() {
1031
1038
  if (!readSession) {
1032
- readSession = remoteModule.createReadSession({ maxAge: maxAgeGiven ?? 0 });
1039
+ readSession = remoteModule.createReadSession({ maxAge: maxAgeGiven ?? 0, ...(readBudgetMs !== null ? { deadline: Date.now() + readBudgetMs } : {}) });
1033
1040
  process.on("exit", () => { sayReadNotices(); readSession.closeNow(); });
1034
1041
  for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143], ["SIGHUP", 129]]) process.once(signal, () => {
1035
1042
  readSession.closeNow();
@@ -1046,7 +1053,7 @@ function sayReadNotices() {
1046
1053
  }
1047
1054
  /** Which kernel command forms take --max-age: THE allow-list (docs/desktop-cli-api.md "Observation reuse").
1048
1055
  * → null when this form reads with observation reuse, else the E_BAD_ARGS message. `head` is argv before `--`. */
1049
- const MAX_AGE_READS = "status, workspace status, souls, capabilities, inspect --soul|--home, spawn --preview, and the read forms of teams and soul teams";
1056
+ 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";
1050
1057
  function maxAgeRefusal(command, head) {
1051
1058
  const word = (i) => (head[i] !== undefined && !head[i].startsWith("--") ? head[i] : undefined);
1052
1059
  const refuse = (form) => `--max-age is not accepted by \`oats ${form}\`: only the read verbs reuse observations (${MAX_AGE_READS})`;
@@ -1645,14 +1652,25 @@ async function soulCmd() {
1645
1652
  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(",")]));
1646
1653
  }
1647
1654
 
1648
- /** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
1649
- async function itemsCmd(kind) {
1650
- const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
1655
+ /** The catalog's rows of `kind` ("capabilities" | "souls") as `oats capabilities` / `oats souls` list them:
1656
+ * one discovery (honouring --max-age), the lock, workspaceItems. → { ctx, discovery, lock, items }. */
1657
+ async function catalogRows(kind, bail) {
1651
1658
  const ctx = workspaceContext(bail);
1652
1659
  const discovery = await discoverForCli(ctx, bail);
1653
1660
  let lock;
1654
1661
  try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
1655
- const items = workspaceItems(discovery, lock, ctx.local, ctx.deploymentDir)[kind];
1662
+ return { ctx, discovery, lock, items: workspaceItems(discovery, lock, ctx.local, ctx.deploymentDir)[kind] };
1663
+ }
1664
+
1665
+ /** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
1666
+ async function itemsCmd(kind) {
1667
+ const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
1668
+ if (kind === "capabilities") {
1669
+ const { words } = capabilitiesArgv();
1670
+ if (words[0] === "show") return capabilityShowCmd(bail);
1671
+ 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] });
1672
+ }
1673
+ const { ctx, discovery, lock, items } = await catalogRows(kind, bail);
1656
1674
  // One command's reads at a commit are shared: many souls resolve over the same manifests and listings.
1657
1675
  const remote = memoizedRemote(remoteModule);
1658
1676
  if (kind === "capabilities") await capabilityFacts(items, discovery, lock, ctx, remote);
@@ -1667,6 +1685,74 @@ async function itemsCmd(kind) {
1667
1685
  if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
1668
1686
  }
1669
1687
 
1688
+ /** `oats capabilities` argv after the command: its words (the subcommand first) with every flag and a value
1689
+ * flag's value set aside, and the first flag neither form takes (`unknown`). */
1690
+ function capabilitiesArgv() {
1691
+ const VALUE_FLAGS = new Set(["--member", "--package", "--file", "--dir", "--max-age", "--server"]);
1692
+ const words = [];
1693
+ let unknown;
1694
+ for (let i = 1; i < args.length; i++) {
1695
+ if (VALUE_FLAGS.has(args[i])) { if (args[i + 1] !== undefined && !args[i + 1].startsWith("--")) i++; continue; }
1696
+ if (args[i].startsWith("--")) { if (args[i] !== "--json") unknown ??= args[i]; continue; }
1697
+ words.push(args[i]);
1698
+ }
1699
+ return { words, unknown };
1700
+ }
1701
+
1702
+ /** `oats capabilities show <name> [--member <repoKey> | --package <id>] [--file <path>] [--dir] [--json]`
1703
+ * (feature capability-show, capabilityShowApi 1; lib/capability-show.mjs): what one catalog row ships, read
1704
+ * at that row's commit — the rows are `oats capabilities`'s own (catalogRows). */
1705
+ async function capabilityShowCmd(bail) {
1706
+ const S = await import("../lib/capability-show.mjs");
1707
+ if (flag("server") !== undefined) return bail("E_BAD_ARGS", "oats capabilities show reads this machine's workspace only: --server is not accepted");
1708
+ const { words, unknown } = capabilitiesArgv();
1709
+ if (unknown) return bail("E_BAD_ARGS", `oats capabilities show: unknown flag ${unknown}`, { flag: unknown });
1710
+ const positionals = words.slice(1);
1711
+ 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)");
1712
+ const name = positionals[0];
1713
+ const memberArg = valueFlag("member"), packageArg = valueFlag("package"), file = valueFlag("file");
1714
+ if (memberArg !== undefined && packageArg !== undefined) return bail("E_BAD_ARGS", "choose --member <repoKey> or --package <id>, not both");
1715
+ 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 });
1716
+ const { ctx, discovery, lock, items } = await catalogRows("capabilities", bail);
1717
+ // --member takes the repo key a row shows, or any ref spelling of it (compared on the canonical key).
1718
+ let member = memberArg ?? null;
1719
+ if (member !== null && !items.some((r) => r.repoKey === member)) { try { member = remoteModule.parseRepoRef(member).key; } catch { /* matches no row */ } }
1720
+ const remote = memoizedRemote(remoteModule);
1721
+ const catalog = (() => { try { return officialPackageCatalog(); } catch { return null; } })();
1722
+ let doc;
1723
+ try {
1724
+ const row = S.selectCapabilityRow(items, name, { member, package: packageArg ?? null });
1725
+ const source = await S.capabilitySource(row, { discovery, lock, catalog, remote, remoteOptions: ctx.remoteOptions });
1726
+ doc = file === undefined ? await S.capabilityShow(source, { remote, remoteOptions: ctx.remoteOptions }) : await S.capabilityFile(source, file, { remote, remoteOptions: ctx.remoteOptions });
1727
+ } catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance); throw e; }
1728
+ if (JSON_MODE) { jsonOk(withObservation(doc)); return; }
1729
+ if (file !== undefined) {
1730
+ const f = doc.file;
1731
+ if (f.binary) console.log(`(${f.path}: binary, ${formatBytes(f.bytes)} — not shown)`);
1732
+ else {
1733
+ process.stdout.write(f.text.endsWith("\n") || f.text === "" ? f.text : `${f.text}\n`);
1734
+ if (f.truncated) console.log(`(${f.path}: truncated — ${formatBytes(f.bytes)}, the first ${formatBytes(S.TEXT_LIMIT)} shown)`);
1735
+ }
1736
+ return;
1737
+ }
1738
+ console.log(`${doc.name} — ${doc.kind === "member" ? `member ${doc.repoKey}` : `package ${doc.package} v${doc.version} (${doc.repoKey})`} @ ${short(doc.commit)}, ${doc.path}\n`);
1739
+ const size = (bytes) => (bytes === null ? "unreadable" : formatBytes(bytes));
1740
+ console.log(` inject: ${doc.inject ? `${doc.inject.path ?? "(unsafe path)"} (${size(doc.inject.bytes)}${doc.inject.binary ? ", binary" : ""}${doc.inject.truncated ? ", truncated" : ""})` : "(none)"}`);
1741
+ if (doc.skills === null) console.log(" skills: (cannot be listed — see problems)");
1742
+ else if (!doc.skills.length) console.log(" skills: (none)");
1743
+ else {
1744
+ console.log(" skills:");
1745
+ for (const skill of doc.skills) {
1746
+ console.log(` ${skill.name} ${skill.path}`);
1747
+ if (skill.files === null) console.log(" (files cannot be listed — see problems)");
1748
+ for (const f of skill.files ?? []) console.log(` ${f.path} ${size(f.bytes)}`);
1749
+ if (skill.filesTruncated) console.log(` … more files (the first ${S.FILES_PER_SKILL} are listed)`);
1750
+ }
1751
+ }
1752
+ for (const p of doc.problems) console.log(` problem: ${p.code}${p.path ? ` ${p.path}` : ""} — ${p.message}`);
1753
+ console.log(`\n a file's text: oats capabilities show ${doc.name}${doc.kind === "package" ? ` --package ${doc.package}` : ""} --file <path>`);
1754
+ }
1755
+
1670
1756
  /** Capability rows' manifest facts (feature desktop-facts): layer, description, and what each provides
1671
1757
  * (skills, commands, hooks by name) — member rows from discovery's manifest, package rows from the manifest
1672
1758
  * at the locked commit (the sync cache; no network beyond sync's). An unreadable package leaves its rows' facts null. */
@@ -2239,8 +2325,8 @@ function retireCmd() {
2239
2325
  }
2240
2326
  // The calling instance knows its own home: self-retire never needs to
2241
2327
  // disambiguate a same-named twin by hand.
2242
- if (homeFlag === undefined && process.env.OATS_INSTANCE_HOME && (process.env.PI_AGENT_INSTANCE === name || process.env.OATS_INSTANCE === name)) homeFlag = process.env.OATS_INSTANCE_HOME;
2243
- const isSelf = process.env.PI_AGENT_INSTANCE === name || process.env.OATS_INSTANCE === name;
2328
+ if (homeFlag === undefined && process.env.OATS_INSTANCE_HOME && process.env.OATS_INSTANCE === name) homeFlag = process.env.OATS_INSTANCE_HOME;
2329
+ const isSelf = process.env.OATS_INSTANCE === name;
2244
2330
  if (isSelf && !args.includes("--self")) die(`"${name}" is the calling instance — self-retire is irreversible; if your task is complete and you were told to retire, re-run with --self (finish your memory files FIRST; your session dies ~8s after)`);
2245
2331
  if (!isSelf && args.includes("--self")) die(`--self given but "${name}" is not the calling instance`);
2246
2332
  const root = ensureRoot(dirFlag());
@@ -2766,9 +2852,12 @@ async function capabilityCommand() {
2766
2852
  let activeIds;
2767
2853
  let context = process.cwd();
2768
2854
  let teamCtx, homeMeta, homeTeamCtx;
2769
- // OATS_INSTANCE_HOME is the canonical identity; the older names still count. With none set
2770
- // (a harness that strips the session env), the home enclosing the cwd.
2771
- const instanceHome = process.env.OATS_INSTANCE_HOME || process.env.PI_AGENT_HOME || process.env.OATS_HOME || enclosingInstanceHome(logicalCwd());
2855
+ // OATS_INSTANCE_HOME is the identity; OATS_HOME still counts. With neither set (a harness that
2856
+ // strips the session env), the home enclosing the cwd. What chose the home is named in every
2857
+ // refusal about it.
2858
+ const homeVariable = ["OATS_INSTANCE_HOME", "OATS_HOME"].find((name) => process.env[name]);
2859
+ const instanceHome = homeVariable ? process.env[homeVariable] : enclosingInstanceHome(logicalCwd());
2860
+ const chosenBy = homeVariable ?? "the working directory";
2772
2861
  const metaFile = instanceHome && join(instanceHome, "instance.json");
2773
2862
  // Capability-id keyed — never answer for `constructor`/`toString`. Belt and
2774
2863
  // braces: the ids come from instance.json, which spawn wrote from resolved
@@ -2787,11 +2876,19 @@ async function capabilityCommand() {
2787
2876
  if (!isWorkspaceHome(meta)) { const e = preWorkspaceHome(instanceHome, "nothing was dispatched"); bail(e.code, e.message); }
2788
2877
  context = meta.repo || context;
2789
2878
  soulDir = instanceSoulDir(instanceHome, meta);
2879
+ const ws = meta.workspace && typeof meta.workspace === "object" ? meta.workspace : {};
2880
+ // Inside a home the namespace is the home's, so a --soul for another soul is refused, never
2881
+ // ignored (as inspect's --soul against --home is). The home's own soul, by any of its names, is fine.
2882
+ const soulFlag = flag("soul");
2883
+ if (soulFlag === true) bail("E_BAD_ARGS", "--soul needs a soul name", { flag: "--soul" });
2884
+ if (typeof soulFlag === "string" && !(await import("../lib/instance-resolution.mjs")).homeSoulMatches(soulFlag, meta)) {
2885
+ bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of the instance home ${instanceHome} (${meta.agent}), which ${chosenBy} chose; to run "${cmd}" as a spawn of ${soulFlag} would, run it outside the instance home with OATS_INSTANCE_HOME and OATS_HOME unset`,
2886
+ { home: instanceHome, soul: meta.agent, chosenBy, flag: "--soul" });
2887
+ }
2790
2888
  // The team/workspace facts the home recorded at spawn, as its hooks got them, with the
2791
2889
  // recorded eligible teams (OATS_TEAMS_SOURCE=recorded). Only the home's MESSAGING module
2792
2890
  // gets them live (below): its team verbs (join/leave/teams) must see what the workspace
2793
2891
  // allows now, and no other command pays a remote read for them.
2794
- const ws = meta.workspace && typeof meta.workspace === "object" ? meta.workspace : {};
2795
2892
  const messaging = (meta.capabilities || []).find((c) => c.layer === "messaging")?.id;
2796
2893
  homeMeta = { meta, messaging };
2797
2894
  homeTeamCtx = (t) => teamEnv({ workspace: { key: ws.key, name: ws.name, deployment: ws.deployment }, teams: t.teams, defaultTeam: t.defaultTeam, teamsSource: t.source });
@@ -2809,7 +2906,7 @@ async function capabilityCommand() {
2809
2906
  // Workspace model: an instance's own materialized modules are the command
2810
2907
  // namespaces available to it (instance.json.modules → <home>/.oats/modules).
2811
2908
  const mans = Object.values(capabilityManifests(instanceHome)).filter((m) => m.command === cmd && m.commands);
2812
- if (!mans.length) return NOT_DISPATCHED;
2909
+ if (!mans.length) bail("E_UNKNOWN_COMMAND", `oats ${cmd}: no capability of the instance home ${instanceHome} (soul ${homeMeta.meta.agent}), which ${chosenBy} chose, provides "${cmd}"`, { home: instanceHome, chosenBy, namespace: cmd });
2813
2910
  if (mans.length > 1) bail("E_DUPLICATE_NAMESPACE", `duplicate operational command namespace "${cmd}": ${mans.map((m) => m.capability).join(", ")}`);
2814
2911
  const m = mans[0];
2815
2912
  if (!activeIds.includes(m.capability)) bail("E_CAPABILITY_INACTIVE", `${m.capability} command namespace is not active in the current context/instance`);
@@ -2946,7 +3043,7 @@ function versionCmd() {
2946
3043
  // Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
2947
3044
  // runs on resolve/materialize (contract §6); a feature the binary does not implement is
2948
3045
  // never listed.
2949
- 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"], 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 }));
3046
+ 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 }));
2950
3047
  return;
2951
3048
  }
2952
3049
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -3349,6 +3446,10 @@ try {
3349
3446
  maxAgeGiven = Number(raw);
3350
3447
  activateLocalInputs(); // observation.localRevision: every local config read from here on is recorded
3351
3448
  }
3449
+ if (kernelArgv && !head.includes("--server") && (cmd === "status" || (cmd === "workspace" && head[1] === "status"))) {
3450
+ const override = Number(process.env.OATS_READ_REMOTE_BUDGET_MS);
3451
+ readBudgetMs = Number.isSafeInteger(override) && override > 0 ? override : remoteModule.READ_REMOTE_BUDGET_MS;
3452
+ }
3352
3453
  const inherited = ["OATS_RESOLUTION", "OATS_DEPLOYMENT"].filter((k) => process.env[k]);
3353
3454
  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 });
3354
3455
  if (cmd === "inspect" && head.includes("--request")) {
@@ -3606,6 +3707,10 @@ Usage:
3606
3707
  oats capabilities [--dir <d>] [--json] every capability of every confirmed member (a
3607
3708
  [--max-age <s>] private one is listed as repo-owned: usable only by
3608
3709
  its own repo's souls) + the locked packages
3710
+ oats capabilities show <name> [--member <repoKey> | --package <id>] [--file <path>]
3711
+ [--dir <d>] [--json] [--max-age <s>] what one capability of that list ships, at its commit:
3712
+ its inject (text) and each skill's files (sizes);
3713
+ --file <path>: one listed file's text
3609
3714
  oats souls [--dir <d>] [--json] every soul of every confirmed member + external souls
3610
3715
  [--max-age <s>] (souls have no private mode), with origin
3611
3716
  (member <key> @ <commit> | package <id> v<ver>) and its
@@ -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
@@ -38,9 +38,10 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
38
38
  "instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
39
39
  "workspace-v2","instance-modules","spawn-provider-payload","served-identity","packages-no-approval","spawn-name","settings-origins",
40
40
  "team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
41
- "preview-composed-from","observe-max-age","spawn-preview-max-age"],
41
+ "preview-composed-from","observe-max-age","spawn-preview-max-age","capability-show"],
42
42
  "automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
43
- "readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2}
43
+ "readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2,
44
+ "capabilityShowApi":1}
44
45
  ```
45
46
 
46
47
  - The Desktop accepts `desktopApi === 1` and a released `version` inside
@@ -97,6 +98,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
97
98
  | `preview-composed-from` | `composedFrom` on preview `modules[]` ([Composition](#the-preview)) | |
98
99
  | `observe-max-age` | `--max-age <s>` on the read verbs and their `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311)) | |
99
100
  | `spawn-preview-max-age` | `--max-age <s>` on `spawn --preview` and its `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311), [The preview](#the-preview)) | |
101
+ | `capability-show` | `oats capabilities show <name>` and its `--file` form, OATS 0.34.0 ([`oats capabilities show`](#oats-capabilities-show)) | `capabilityShowApi: 1` |
100
102
 
101
103
  Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
102
104
  `workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
@@ -196,6 +198,7 @@ it and answer live, without the block.
196
198
  oats status | workspace status | souls | capabilities | inspect --soul|--home
197
199
  | teams | soul teams <soul> … --max-age <seconds> --json
198
200
  oats spawn <soul> … --preview --max-age <seconds> --json (feature spawn-preview-max-age)
201
+ oats capabilities show <name> … --max-age <seconds> --json (feature capability-show)
199
202
  ```
200
203
 
201
204
  - **Values:** whole seconds, `0` to `86400`. `0` is live: it reuses nothing.
@@ -253,8 +256,8 @@ oats spawn <soul> … --preview --max-age <seconds> --json (feature spawn-pre
253
256
  invocation refuse the flag before reading or writing anything, with
254
257
  `E_BAD_ARGS` "--max-age is not accepted by \`oats <form>\`: only the read
255
258
  verbs reuse observations (status, workspace status, souls, capabilities,
256
- inspect --soul|--home, spawn --preview, and the read forms of teams and soul
257
- teams)" and,
259
+ capabilities show, inspect --soul|--home, spawn --preview, and the read
260
+ forms of teams and soul teams)" and,
258
261
  with `--server`, "--max-age cannot be combined with --server: observation
259
262
  reuse is local to this machine".
260
263
  A capability command's argv (`oats <namespace> …`) is its provider's: the
@@ -734,6 +737,14 @@ Read-only (it writes no lock):
734
737
  ```
735
738
 
736
739
  - `members[]` and `packages[]` are the sync rows (packages from the lock).
740
+ - **Remote budget (0.33.1).** `oats workspace status` and `oats status` finish
741
+ their remote work within 12 s of their first remote read, whatever the
742
+ machine's load or another process holding a remote's cache. A member not
743
+ read by then is a `cannot-read` row whose `detail` ends `(timeout)`, and its
744
+ git is ended; the command still answers. The workspace definition itself
745
+ (the host) not read by then fails the command as any unreadable host does
746
+ (`E_REMOTE_UNREADABLE`, `reason: "timeout"`). Spawn, sync and every other
747
+ verb have no such budget.
737
748
  - `declaredPackages`: the ids in `packages:` (standalone: the kernel's
738
749
  default). `unsynced`: declared, not locked. `stale`: locked, no longer
739
750
  declared. `external[]`: `{source, soul}`.
@@ -810,6 +821,175 @@ problem`, and (feature `launch-preference`) `key` and `launch`.
810
821
  This document keeps `soulsApi: 1`; the probe's `soulsApi: 2` is the inspect
811
822
  soul row's.
812
823
 
824
+ ### `oats capabilities show`
825
+
826
+ Feature `capability-show`, `capabilityShowApi: 1`, OATS 0.34.0.
827
+
828
+ ```text
829
+ oats capabilities show <name> [--member <repoKey> | --package <id>] [--dir <d>] [--max-age <s>] --json
830
+ oats capabilities show <name> [--member <repoKey> | --package <id>] --file <path> [--dir <d>] [--max-age <s>] --json
831
+ ```
832
+
833
+ What one capability ships: its inject text and each skill's files, and one
834
+ file's text on request. The Desktop's capability page shows them without
835
+ reading clones or caches itself.
836
+
837
+ > **Every `text` and `description` is untrusted repository content.** Render
838
+ > it as plain text (`textContent`), or through a sanitising Markdown renderer
839
+ > that allows no raw HTML, no scripts and no remote images.
840
+
841
+ **Selection.** The rows are exactly the rows of `oats capabilities --json`
842
+ (one discovery, honouring `--max-age`), so the answer's `commit` equals that
843
+ row's `commit`.
844
+
845
+ - `<name>` alone selects the one row with that name.
846
+ - `--member <repoKey>` selects a member row of that repository: the key as a
847
+ row shows it, or any ref spelling of the same repository.
848
+ - `--package <id>` selects that package's row.
849
+ - No row: `E_CAPABILITY_UNKNOWN`, `details: {name}` plus `member` or
850
+ `package` when one was given. An unsynced package is not in the catalog, so
851
+ its capabilities are `E_CAPABILITY_UNKNOWN` until `oats sync`.
852
+ - More than one row: `E_CAPABILITY_AMBIGUOUS`, `details: {name, candidates}`,
853
+ each candidate `{kind, repoKey, origin}` (member) or `{kind, package,
854
+ origin}` (package). Choose one with `--member` or `--package`.
855
+ - `E_BAD_ARGS` for `--member` with `--package`, a missing or second name, an
856
+ unknown flag, and `--server` (the verb reads this machine's workspace only).
857
+ An unknown word after `oats capabilities` (`oats capabilities foo`) is
858
+ `E_BAD_ARGS` too.
859
+
860
+ **Trust path.** Every read goes through the remote cache at a full commit id;
861
+ nothing reads a working clone.
862
+
863
+ - A member capability is read from its member repository at the row's commit.
864
+ - A package capability is read at the locked commit, the same trust path
865
+ spawn uses. First comes spawn's lock check: when the package manifest at
866
+ that commit does not list the capability, or its capability list differs
867
+ from the lock's, the show refuses `E_PACKAGE_INTEGRITY` with spawn's
868
+ details. The full-tree content digest is not recomputed per show: `oats
869
+ sync` proved the lock's integrity over the tree of exactly that commit, and
870
+ the commit id content-addresses the tree.
871
+
872
+ **The show:**
873
+
874
+ ```json
875
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","repoKey":"github.com/awebai/oats-okf","package":"oats.okf","version":"4.0.5",
876
+ "commit":"26d8216f…","path":"oats-package/capabilities/oats-okf",
877
+ "inject":{"path":"injects/okf.md","bytes":2422,"text":"## Knowledge: OKF\n\nYou have two kinds of knowledge. …","binary":false,"truncated":false},
878
+ "skills":[{"name":"okf-consultation","path":"skills/okf-consultation","description":"Consulting your soul's knowledge with the `oats okf` CLI: …",
879
+ "files":[{"path":"skills/okf-consultation/SKILL.md","bytes":6947},{"path":"skills/okf-consultation/references/consult.md","bytes":4465}],
880
+ "filesTruncated":false},
881
+ {"name":"okf-instance-knowledge","path":"skills/okf-instance-knowledge","description":"Keeping this instance's own knowledge …",
882
+ "files":[{"path":"skills/okf-instance-knowledge/SKILL.md","bytes":4787}],"filesTruncated":false}],
883
+ "problems":[]}
884
+ ```
885
+
886
+ - `kind` is `member` or `package`. `repoKey` is set for both kinds; for a
887
+ package it is the repository the package is read from. `package` and
888
+ `version` are the package id and locked version, `null` for a member.
889
+ - `commit` is 40 hex and equals the catalog row's `commit`. `path` is the
890
+ capability directory, repository-relative.
891
+ - `inject` is `{path, bytes, text, binary, truncated}` or `null`. `skills`
892
+ is a list of `{name, path, description, files, filesTruncated}`, each file
893
+ `{path, bytes}`, or `null`. `problems` is a list of `{code, message,
894
+ path}`.
895
+ - With `--max-age` (`0` included) both the show and the `--file` answer gain
896
+ the [`observation`](#observation-reuse-feature-observe-max-age-oats-0311)
897
+ block `{observedAt, reused, localRevision}`.
898
+
899
+ **The `--file` answer:**
900
+
901
+ ```json
902
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"26d8216f…",
903
+ "file":{"path":"skills/okf-instance-knowledge/SKILL.md","bytes":4787,"text":"---\nname: okf-instance-knowledge\n…","binary":false,"truncated":false}}
904
+ ```
905
+
906
+ **Rules.**
907
+
908
+ - **Paths.** Every `path` in `inject`, `skills`, `file` and `problems` is
909
+ POSIX and relative to the capability directory, never the repository.
910
+ Every non-null `path` is a safe relative path: no `.`, `..` or `.git`
911
+ component (any case), no empty component, no `\`. A manifest's inject and
912
+ skill paths are reported as the module install reads them: a `\` is a
913
+ separator, and a leading `./` and trailing slashes are dropped
914
+ (`injects\guide.md` is `injects/guide.md`).
915
+ - **The inject** is the committed file exactly: untrimmed and untemplated.
916
+ (Spawn composes it raw and trimmed, with no settings substitution.)
917
+ - `inject: null`: the manifest declares no inject.
918
+ - Declared but unreadable (missing, a symlink, a directory, over the read
919
+ budget): `inject: {path, bytes: null, text: null, binary: false,
920
+ truncated: false}` and a `problems[]` entry with the remote's code
921
+ (`E_REMOTE_PATH_MISSING`, `E_REMOTE_TREE_UNSAFE`, `E_REMOTE_FILE_OVERSIZE`,
922
+ …). The show still answers ok.
923
+ - Declared as a path that is not a safe relative path (spawn refuses it):
924
+ `inject: {path: null, bytes: null, text: null, binary: false, truncated:
925
+ false}` and a problem with spawn's code (`E_CAPABILITY_MISSING` for a
926
+ member, `E_PACKAGE_MANIFEST` for a package) and `path: null`. The raw
927
+ manifest value appears only inside `message`, JSON-quoted. So
928
+ `inject.path` is `null` only with a problem.
929
+ - **Skills** are in the catalog row's order (by name, in codepoint order),
930
+ enumerated exactly as a spawn enumerates them. `skills` is `null` exactly
931
+ when the catalog row's `skills` is `null`, with a problem carrying spawn's
932
+ code (`E_CAPABILITY_MISSING` or `E_PACKAGE_MANIFEST`) or an `E_REMOTE_*`
933
+ code, and `path: null`. A skill whose path is not safe (a `.git`
934
+ directory) makes the skills unlistable the same way, in both answers.
935
+ - **Files.** `files` is every regular file under the skill directory,
936
+ recursively (no symlinks, no submodules), sorted by path in codepoint order.
937
+ At most 200 per skill; beyond that `filesTruncated` is `true`. `bytes` is
938
+ the blob size. When a skill's files cannot be listed (an unsafe entry name
939
+ in the tree, an unreadable remote), `files` is `null`, `filesTruncated` is
940
+ `false`, and a problem's `path` is the skill's `path`: "could not list"
941
+ never collapses into "listed nothing".
942
+ - **`description`** is the `description` key of SKILL.md's leading `---` YAML
943
+ front matter, when it is a string. It is parsed from the whole SKILL.md,
944
+ not from its 262144-byte text cut. It is `null` when the front matter is
945
+ absent or does not parse, the key is absent or not a string, or SKILL.md is
946
+ unreadable or binary (no problem is reported for it). It is cut to at most
947
+ 1024 UTF-8 bytes on a code point boundary.
948
+ - **Text.** A file is `binary: true, text: null` when it contains a NUL byte
949
+ or is not valid UTF-8. Only the first 262144 + 3 bytes are examined, so the
950
+ cut is decided on the same bytes; when the file is longer, a valid sequence
951
+ they end inside of is not held against it. Otherwise `text` is the content cut to at
952
+ most 262144 UTF-8 bytes on a code point boundary, with `truncated: true`
953
+ when cut. A byte order mark is kept in the text. `bytes` is always the real
954
+ size.
955
+ - **Invariants** (pinned for the Desktop):
956
+ - `binary: true` ⇒ `text: null` and `truncated: false` (`truncated` is a
957
+ text-only flag).
958
+ - `truncated: true` ⇒ `text` is a string and `binary: false`.
959
+ - `bytes` is `null` only for an unreadable declared inject (with its
960
+ problem). In a `--file` answer it is always an integer.
961
+ - **Large files.** A file over the read budget (4 MiB) is listed with its
962
+ size. `--file` refuses it with `E_REMOTE_FILE_OVERSIZE`, passed through
963
+ unchanged: its `details.path` is repository-relative, not
964
+ capability-relative.
965
+
966
+ **`--file <path>`** reads one file the show lists, and nothing else.
967
+
968
+ - A syntactically unsafe path is `E_CAPABILITY_FILE_UNSAFE`, `details:
969
+ {path}`, before anything is read: absolute, empty, a `.`, `..` or `.git`
970
+ component (any case), an empty component, a trailing slash, a `\` or a NUL.
971
+ - Otherwise the path must be the inject's `path` or a path in some skill's
972
+ `files` as the show lists it (the 200 cap included). Anything else is
973
+ `E_CAPABILITY_FILE_UNKNOWN`, `details: {path, name}`. No other file of the
974
+ capability (`oats.json`, scripts, `bin/`) is readable through this verb.
975
+ - A file the show does not list (beyond the 200 cap, a symlink, under a skill
976
+ whose files cannot be listed) is `E_CAPABILITY_FILE_UNKNOWN` by design:
977
+ show "not available", not an error.
978
+ - A listed file the remote cannot read answers the remote's own code
979
+ (`E_REMOTE_FILE_OVERSIZE`, `E_REMOTE_PATH_MISSING`, `E_REMOTE_TREE_UNSAFE`,
980
+ …).
981
+
982
+ **Failures.** Every failure is exactly one error envelope on stdout with a
983
+ nonzero exit, as for every command: `E_BAD_ARGS`, `E_CAPABILITY_UNKNOWN`,
984
+ `E_CAPABILITY_AMBIGUOUS`, `E_PACKAGE_INTEGRITY`, `E_CAPABILITY_FILE_UNSAFE`,
985
+ `E_CAPABILITY_FILE_UNKNOWN`, an `E_REMOTE_*` code, and the workspace's own
986
+ (`E_LOCAL_MISSING`, `E_LOCK_SCHEMA`, …).
987
+
988
+ **Without `--json`** the show prints a short listing for an operator: the
989
+ inject's path and size, each skill with its files and sizes, and the
990
+ problems. `--file` prints the text; a binary file prints a one-line note
991
+ instead, and a truncated file prints its text followed by a one-line note.
992
+
813
993
  <a id="desktop-facts-feature-desktop-facts-oats-0290"></a>
814
994
  ### Desktop facts
815
995
 
@@ -27,9 +27,9 @@ named launch configuration (`--launch-config <name>`); see
27
27
  [configuration.md](configuration.md#launch-configurations).
28
28
  `--model @native-default` uses the harness's own default model.
29
29
 
30
- The launch command sets `OATS_INSTANCE`, `OATS_INSTANCE_HOME`,
31
- `PI_AGENT_INSTANCE` and `PI_AGENT_HOME`, plus the environment that the launch
32
- configuration and capabilities contribute. The home's layout and what those
30
+ The launch command sets `OATS_INSTANCE` and `OATS_INSTANCE_HOME`, plus the
31
+ environment that the launch configuration and capabilities contribute. No
32
+ `PI_AGENT_*` name is set, for any harness. The home's layout and what those
33
33
  variables point at are described in
34
34
  [souls-and-instances.md](souls-and-instances.md#instance-anatomy).
35
35
 
@@ -48,6 +48,7 @@ published to npm. Its developer docs are in
48
48
  | `workspace.mjs` | workspace, membership and soul files; discovery |
49
49
  | `resolve.mjs` | a soul's resolution: capabilities, slots, provenance |
50
50
  | `packages.mjs` | `packages:`, the catalog, `oats sync`, `oats-lock.json` |
51
+ | `capability-show.mjs` | `oats capabilities show`: one catalog row's inject and skill files, read at its commit |
51
52
  | `materialize.mjs` | copying modules into a home and composing it |
52
53
  | `core.mjs` | spawn, retire, sessions, hooks, launch recipes, instance metadata |
53
54
  | `instruction-composition.mjs` | the generated `AGENTS.md` |
@@ -71,6 +72,27 @@ Every CLI command owns one read session (`createReadSession` in
71
72
  CLI closes it when the command ends). A library caller without a session
72
73
  gets the plain per-call behaviour. Within a session:
73
74
 
75
+ - a HEAD observation speaks protocol v0 when the operator has not pinned
76
+ `protocol.version` (`git config --get`, read once per command): the whole
77
+ ref advertisement in one round trip, resolved exactly as v2's filtered
78
+ answer. `V0_ADVERTISEMENT_BUDGET` (4 MiB, git's `maxBuffer`) bounds what
79
+ is kept, not the transfer: git reads the whole advertisement before it
80
+ prints a ref, so a remote over budget costs its advertisement once, then
81
+ git is killed, the remote observed again under v2 and recorded. A v0
82
+ timeout stays today's error (no retry) and is recorded too, unless the
83
+ session's `deadline` cut that read's timeout (the deadline, not the remote,
84
+ may have ended it). The
85
+ v0 read and its `protocol.version` check go through `sessionExec` like every
86
+ other git call. The record is
87
+ `<cacheRoot>/.ls-remote/<sha256(key)>.<reason>.json`, `{ protocol: "v2",
88
+ reason: "overflow" | "timeout", recordedAt }`, one file per reason (an
89
+ in-flight timeout never replaces an overflow), written atomically with no lock;
90
+ `overflow` is permanent, `timeout` expires after 7 days, and an unreadable,
91
+ corrupt or expired record is no record (v0 is tried). Another v0 failure
92
+ that is not final (auth, not-found, cache, an abort) is retried once under
93
+ v2. Each is a session notice. The v2 argv (`lsRemoteArgs`) stays the
94
+ observation's identity: records and memo keys do not depend on the
95
+ protocol;
74
96
  - a head is observed once per (cache repo, ref), and a commit peeled once; at
75
97
  most eight observations run at once (`OBSERVE_LIMIT`), each holding its slot
76
98
  for all its git work (the `ls-remote` and the fetch of the commit it names,
@@ -104,6 +126,20 @@ gets the plain per-call behaviour. Within a session:
104
126
  command that ends normally awaits the close, so its readers are reaped
105
127
  before it exits; a `process.exit` (every refusal) ends them in the
106
128
  exit hook (`closeNow`), and the system reaps them once the process is gone;
129
+ - a session may have a `deadline` (`READ_REMOTE_BUDGET_MS`, 12 s after it
130
+ starts): the CLI gives one to `status` and `workspace status` only
131
+ (`readBudgetMs`; `OATS_READ_REMOTE_BUDGET_MS` is a test and ops override,
132
+ not a contract). Every
133
+ remote step then gets what is left of it instead of its own default: each
134
+ git call's timeout (`sessionExec`: ls-remote, fetch, ls-tree, the cache's
135
+ plumbing; none starts once nothing is left), the git version probe
136
+ (`readVersion`), the batch readers' answers, the cache write lock's wait,
137
+ the half-initialised cache's wait and the lock-race backoff. What the
138
+ deadline ends is a `timeout` (a peel or version it ended is never read as a
139
+ missing commit or an older git), so an unread member degrades as any
140
+ unreadable one. A cut wait never changes what it judges: past the deadline
141
+ no lock is taken or reclaimed (live, stale or unreadable), and a cache
142
+ directory waited for less than in full is not taken for a crash's leftover;
107
143
  - every git child is ended with SIGTERM first and SIGKILL only after a
108
144
  grace (`terminateGroup`): git removes its own lock files on SIGTERM, and
109
145
  a git killed outright leaves one that blocks every later write. The
package/docs/knowledge.md CHANGED
@@ -220,8 +220,10 @@ identical copies in both role capabilities.
220
220
  - **From an instance home**, `oats okf …` runs the home's copy of the module
221
221
  with the settings recorded at spawn.
222
222
  - **From the deployment directory** (holding `oats-local.yaml`), in a shell
223
- without another instance's `OATS_*` identity, every capability command
224
- needs `--soul <name>` (`E_BAD_ARGS` without it). The kernel resolves the
223
+ where neither `OATS_INSTANCE_HOME` nor `OATS_HOME` is set (either one pins
224
+ the command to that instance home), every capability command needs
225
+ `--soul <name>` (`E_BAD_ARGS` without it). Inside an instance home, a
226
+ `--soul` naming another soul is refused (`E_HOME_MISMATCH`). The kernel resolves the
225
227
  soul as a spawn would, fetches its module at the locked commit into
226
228
  `<deployment>/.oats/modules/` and runs it with the soul's merged settings.
227
229
  An unlocked package is `E_PACKAGE_MISSING` until `oats sync`.
@@ -12,7 +12,7 @@ or workspace membership alone does not make a package official.
12
12
  | `oats.framework` | `oats-framework/v1.4.1` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
13
13
  | `oats.okf` | `v4.0.5` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
14
14
  | `oats.aweb` | `v1.17.5` | `oats.aweb` (messaging) | |
15
- | `oats.engineering` | `v1.4.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
15
+ | `oats.engineering` | `v1.5.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
16
16
  | `oats.authoring` | `v1.0.3` | `oats.authoring` | |
17
17
  | `oats.jira` | `v1.0.1` | `oats.jira` (tasks) | |
18
18
  | `oats.linear` | `v1.0.1` | `oats.linear` (tasks) | |