@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 +122 -17
- package/docs/capabilities.md +17 -0
- package/docs/desktop-cli-api.md +184 -4
- package/docs/execution-targets.md +3 -3
- package/docs/implementation.md +36 -0
- package/docs/knowledge.md +4 -2
- package/docs/official-catalog.md +1 -1
- package/docs/release-notes/v0.34.0.md +63 -0
- package/docs/release-notes/v0.34.1.md +57 -0
- package/docs/souls-and-instances.md +15 -8
- package/docs/workspaces.md +12 -1
- package/lib/capability-contract.mjs +1 -1
- package/lib/capability-show.mjs +208 -0
- package/lib/core.mjs +3 -3
- package/lib/instance-resolution.mjs +26 -4
- package/lib/packages.mjs +1 -1
- package/lib/remote.mjs +259 -35
- package/lib/resolve.mjs +56 -14
- package/package-catalog.json +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
539
|
-
|
|
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`
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
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 &&
|
|
2243
|
-
const isSelf = process.env.
|
|
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
|
|
2770
|
-
//
|
|
2771
|
-
|
|
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)
|
|
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
|
package/docs/capabilities.md
CHANGED
|
@@ -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
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -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
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
package/docs/implementation.md
CHANGED
|
@@ -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
|
-
|
|
224
|
-
|
|
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`.
|
package/docs/official-catalog.md
CHANGED
|
@@ -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.
|
|
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) | |
|