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