@awebai/oats 0.30.2 → 0.31.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/bin/oats.mjs +111 -44
- package/docs/configuration.md +21 -1
- package/docs/desktop-cli-api.md +120 -14
- package/docs/desktop-instance-start.md +3 -1
- package/docs/desktop.md +20 -0
- package/docs/execution-targets.md +53 -30
- package/docs/implementation.md +1 -1
- package/docs/integrations.md +1 -1
- package/docs/oats-local.schema.json +8 -0
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +2 -2
- package/docs/release-notes/v0.30.3.md +75 -0
- package/docs/release-notes/v0.31.0.md +130 -0
- package/docs/schedules.md +4 -1
- package/docs/servers.md +94 -25
- package/docs/souls-and-instances.md +8 -3
- package/lib/attachments.mjs +2 -1
- package/lib/core.mjs +233 -216
- package/lib/errors.mjs +17 -0
- package/lib/instance-inspect.mjs +11 -3
- package/lib/schedule.mjs +21 -8
- package/lib/servers.mjs +265 -45
- package/lib/session-input.mjs +9 -23
- package/lib/session-viewer.mjs +0 -5
- package/lib/triggers.mjs +10 -8
- package/package-catalog.json +1 -1
- package/package.json +1 -1
- package/lib/herdr.mjs +0 -106
package/bin/oats.mjs
CHANGED
|
@@ -23,11 +23,12 @@ import { homedir, tmpdir } from "node:os";
|
|
|
23
23
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { runtimeNameWarning, noteRuntimeName } from "../lib/deprecation.mjs";
|
|
26
|
+
import { herdrSettingRemoved } from "../lib/errors.mjs";
|
|
26
27
|
import {
|
|
27
28
|
LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
|
|
28
29
|
capabilityManifests, capabilityTrust, capabilityExecutablePath,
|
|
29
30
|
officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
|
|
30
|
-
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
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, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
31
32
|
} from "../lib/core.mjs";
|
|
32
33
|
import {
|
|
33
34
|
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages, memoizedRemote,
|
|
@@ -78,7 +79,7 @@ const KERNEL_COMMANDS = new Set(["automations", "trigger", "capture", "capabilit
|
|
|
78
79
|
/** Commands whose argv another parser reads (packages/record and packages/experimental parse process.argv). */
|
|
79
80
|
const OWN_ARGV_COMMANDS = new Set(["capture", "recall", "setup", "experimental"]);
|
|
80
81
|
/** Commands `--server <id>` runs on a registered server. */
|
|
81
|
-
const ROUTED_COMMANDS = new Set(["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "launch-config"]);
|
|
82
|
+
const ROUTED_COMMANDS = new Set(["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "launch-config", "readiness", "instance"]);
|
|
82
83
|
const flag = (name) => {
|
|
83
84
|
const i = args.indexOf(`--${name}`);
|
|
84
85
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -92,7 +93,7 @@ function valueFlag(name) {
|
|
|
92
93
|
if (value === true) cmdFail("E_BAD_ARGS", `--${name} needs a value`);
|
|
93
94
|
return value;
|
|
94
95
|
}
|
|
95
|
-
const die = (msg) => { console.error(`oats: ${msg}`); process.exit(
|
|
96
|
+
const die = (msg, exit = 1) => { console.error(`oats: ${msg}`); process.exit(exit); };
|
|
96
97
|
/** A command's harness: --harness, or --runtime, its pre-0.27 name (the released okf worker and
|
|
97
98
|
* a 0.26-era Desktop pass it) — read either, with the deprecation warning. Both, disagreeing,
|
|
98
99
|
* are refused. `get` reads one flag (the command's own reader where it has one). */
|
|
@@ -139,7 +140,7 @@ const withLocalWarnings = (envelope) => {
|
|
|
139
140
|
const sources = [...new Set([...(Array.isArray(same.sources) ? same.sources : []), ...mine.sources])];
|
|
140
141
|
return { ...envelope, warnings: theirs.map((w) => (w === same ? { ...mine, sources, message: mine.message.replace(/\(.*\)/, `(${sources.join("; ")})`) } : w)) };
|
|
141
142
|
};
|
|
142
|
-
const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) }, ...envelopeWarnings() })); process.exit(
|
|
143
|
+
const jsonFail = (code, message, details, exit = 1) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) }, ...envelopeWarnings() })); process.exit(exit); };
|
|
143
144
|
const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result, ...envelopeWarnings() })); };
|
|
144
145
|
// Text mode (or a JSON answer printed before the read): the warning goes to stderr, never stdout.
|
|
145
146
|
process.on("exit", () => { const w = runtimeNameWarning(); if (w && !warningDelivered) process.stderr.write(`oats: warning: ${w.message}\n`); });
|
|
@@ -258,7 +259,7 @@ const INSPECT_TEXT_CAP = 256 * 1024;
|
|
|
258
259
|
/** The agents root a home belongs to, from its path alone:
|
|
259
260
|
* <root>/<agent>/instances/<instance>. */
|
|
260
261
|
function agentsRootOfHome(home) { return dirname(dirname(dirname(home))); }
|
|
261
|
-
const SOUL_FIELDS = ["harness", "model", "yolo", "
|
|
262
|
+
const SOUL_FIELDS = ["harness", "model", "yolo", "description", "launch-config"];
|
|
262
263
|
const realOrResolved = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
263
264
|
/** Every soul of a scope: the persistent souls of every agents root in
|
|
264
265
|
* scope, plus packaged souls (read-only). One enumeration for inspect and
|
|
@@ -348,7 +349,7 @@ function soulEntry(soul, root, { capability } = {}) {
|
|
|
348
349
|
declarationProblems: declared.problems,
|
|
349
350
|
name: soul.name, kind: packaged ? "capability" : (soul.kind || "persistent"), capability: capability || null,
|
|
350
351
|
type: soul.type ?? null, description: soul.description ?? null, repo: soul.repo ?? null, work: soul.work || "checkout",
|
|
351
|
-
harness: soul.harness || "pi", model: soul.model ?? null, yolo: soul.yolo === true || soul.yolo === "true" ? true : soul.yolo === false || soul.yolo === "false" ? false : null, launchConfig: soul["launch-config"] ?? null,
|
|
352
|
+
harness: soul.harness || "pi", model: soul.model ?? null, yolo: soul.yolo === true || soul.yolo === "true" ? true : soul.yolo === false || soul.yolo === "false" ? false : null, launchConfig: soul["launch-config"] ?? null,
|
|
352
353
|
agentsRoot: root, dir: packaged ? soulDir : dir, soulFile: join(soulDir, "soul.yaml"), instructionsFile: join(soulDir, "AGENTS.md"),
|
|
353
354
|
editable: packaged
|
|
354
355
|
? { fields: [], instructions: false, reason: `packaged soul from capability ${capability}: edit the package and update it; scoped bindings still apply through oats use` }
|
|
@@ -1789,9 +1790,12 @@ async function status() {
|
|
|
1789
1790
|
console.log(` ${a.name} [work: ${a.work || "checkout"}, repo: ${soulRepoLabel(a, ws)}]`);
|
|
1790
1791
|
if (a.description) console.log(` ${a.description}`);
|
|
1791
1792
|
for (const i of a.instances) {
|
|
1792
|
-
console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i
|
|
1793
|
+
console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : livenessWord(i)} (branch ${i.branch || "?"}, ${i.work || "?"})${i.runtimeError ? ` ${i.runtimeError}` : ""}`);
|
|
1793
1794
|
const key = i.home ?? `${a.name}/${i.instance}`;
|
|
1794
1795
|
if (i.identity) console.log(` identity: ${servedIdentityLine(i.identity)}`);
|
|
1796
|
+
// The kernel the home's plain `oats` runs (its last launch's), when it is not this one.
|
|
1797
|
+
const launchedBy = i.home ? recordedKernelBin(i.home) : null;
|
|
1798
|
+
if (typeof launchedBy === "string" && (verbose || launchedBy !== CLI_BIN)) console.log(` kernel: ${launchedBy}${launchedBy !== CLI_BIN ? " (not this oats)" : ""}`);
|
|
1795
1799
|
const s = ws?.soul.get(key);
|
|
1796
1800
|
if (s && (verbose || s.status !== "current")) console.log(` ${soulDriftLine(s, a.name)}`);
|
|
1797
1801
|
const rows = ws?.drift.get(key) || [];
|
|
@@ -1803,13 +1807,47 @@ async function status() {
|
|
|
1803
1807
|
}
|
|
1804
1808
|
}
|
|
1805
1809
|
|
|
1810
|
+
/** A status row's liveness in text: `running` null (a server that cannot be read, a Herdr home) is unknown, never idle. */
|
|
1811
|
+
const livenessWord = (i) => i.running === true ? "RUNNING" : i.running === false ? "idle" : "unknown";
|
|
1812
|
+
/** The flags `oats spawn` reads: those taking a value, and switches. `--provider` takes two words.
|
|
1813
|
+
* `--instance` is refused by a local spawn (with its replacement) but still travels to an older
|
|
1814
|
+
* host through `--server`, whose route reads it. */
|
|
1815
|
+
const SPAWN_VALUE_FLAGS = new Set(["agents-root", "backend", "base", "branch", "dir", "expect-decision", "harness", "idempotency-key", "instance", "launch-config", "model", "name", "parent", "purpose", "relation", "relative-root", "relative-to", "repo", "runtime", "task", "task-file", "trigger-event", "wake-cron", "wake-every", "wake-file", "wake-json", "wake-message", "wake-message-file", "wake-tz", "work", "work-dir"]);
|
|
1816
|
+
const SPAWN_SWITCHES = new Set(["allow-child-spawns", "json", "no-child-spawns", "no-launch", "no-yolo", "preview", "yolo"]);
|
|
1817
|
+
/** Why `argv` (after `spawn`, the soul first) is not a spawn, or undefined: a positional after the
|
|
1818
|
+
* soul or a flag spawn does not read is never ignored. A value flag consumes its value exactly as
|
|
1819
|
+
* flag() reads it; a missing value is the flag's own check. */
|
|
1820
|
+
function spawnArgvProblem(argv) {
|
|
1821
|
+
const soul = argv[0];
|
|
1822
|
+
for (let i = 1; i < argv.length; i++) {
|
|
1823
|
+
const a = argv[i];
|
|
1824
|
+
if (a === "--provider") { i += 2; continue; }
|
|
1825
|
+
if (a.startsWith("--")) {
|
|
1826
|
+
const name = a.slice(2);
|
|
1827
|
+
if (SPAWN_SWITCHES.has(name)) continue;
|
|
1828
|
+
if (!SPAWN_VALUE_FLAGS.has(name)) return `oats spawn: unknown flag ${a}`;
|
|
1829
|
+
if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--")) i++;
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
if (/^[A-Za-z0-9_.-]+=/.test(a)) return `oats spawn: unexpected argument ${JSON.stringify(a)} after the soul ${JSON.stringify(soul)}: a capability setting is given as --provider <capability> key=value (here: --provider <capability> ${a})`;
|
|
1833
|
+
return `oats spawn: unexpected argument ${JSON.stringify(a)} after the soul ${JSON.stringify(soul)}: spawn takes one soul`;
|
|
1834
|
+
}
|
|
1835
|
+
return undefined;
|
|
1836
|
+
}
|
|
1837
|
+
/** The Herdr spawn flags (removed in 0.31.0), named for the refusal; undefined when none is given. */
|
|
1838
|
+
function herdrSpawnFlag() {
|
|
1839
|
+
if (flag("backend") === "herdr") return "--backend herdr";
|
|
1840
|
+
if (flag("herdr-socket") !== undefined) return "--herdr-socket";
|
|
1841
|
+
return undefined;
|
|
1842
|
+
}
|
|
1806
1843
|
async function spawnCmd() {
|
|
1807
1844
|
// JSON mode: contract envelope, stable error codes, stderr-only progress.
|
|
1808
1845
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1809
1846
|
const note = (msg) => (JSON_MODE ? console.error(msg) : console.log(msg));
|
|
1810
1847
|
const yolo = yoloFlag();
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1848
|
+
{ const herdr = herdrSpawnFlag(); if (herdr) { const e = herdrSettingRemoved(`${herdr} was given`); bail(e.code, e.message); } }
|
|
1849
|
+
const backend = valueFlag("backend");
|
|
1850
|
+
if (backend !== undefined && backend !== "tmux") bail("E_BAD_ARGS", "--backend must be tmux");
|
|
1813
1851
|
const requestedWork = valueFlag("work");
|
|
1814
1852
|
const workDir = valueFlag("work-dir"), branch = valueFlag("branch"), repo = valueFlag("repo");
|
|
1815
1853
|
const checkDirectoryOptions = (work) => {
|
|
@@ -1817,7 +1855,7 @@ async function spawnCmd() {
|
|
|
1817
1855
|
};
|
|
1818
1856
|
checkDirectoryOptions(requestedWork); // before anything is resolved or written
|
|
1819
1857
|
const name = args[1];
|
|
1820
|
-
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>|--name <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux
|
|
1858
|
+
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>|--name <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--no-launch] [--json]");
|
|
1821
1859
|
// Retired boundary flags (maintainer transport ruling): fail LOUDLY before
|
|
1822
1860
|
// ANY side effect, including root discovery.
|
|
1823
1861
|
// Local souls (local-agents/) are gone with the workspace model: a soul is a member
|
|
@@ -1831,6 +1869,7 @@ async function spawnCmd() {
|
|
|
1831
1869
|
// The slug rule is checked here too, before any side effect (soul fetch).
|
|
1832
1870
|
if (nameFlag !== undefined) { try { explicitInstanceName(String(nameFlag)); } catch (e) { bail(e.code, e.message); throw e; } }
|
|
1833
1871
|
if (args.includes("--ephemeral")) bail("E_BAD_ARGS", "--ephemeral was removed by the runtime-boundary ruling — declare the agent in a capability manifest (agents:) for automatic ephemeral semantics");
|
|
1872
|
+
{ const problem = spawnArgvProblem(args.slice(1)); if (problem) bail("E_BAD_ARGS", problem); }
|
|
1834
1873
|
let root;
|
|
1835
1874
|
// A spawn needs a workspace deployment (lead decision c3-1): no oats-local.yaml
|
|
1836
1875
|
// in reach is E_LOCAL_MISSING, before anything else is read. The agents root is
|
|
@@ -2010,7 +2049,7 @@ async function spawnCmd() {
|
|
|
2010
2049
|
// An attached instance's repository is its work tree owner's (derived by the kernel).
|
|
2011
2050
|
repo: preparedRepo !== undefined ? preparedRepo : ["directory", "attached"].includes(requestedWork || agent.work)
|
|
2012
2051
|
? repo : repo || defaultRepo(workspaceOf(root)) || defaultRepo(process.cwd()),
|
|
2013
|
-
work: requestedWork, workDir, harness: harnessFlag(), backend,
|
|
2052
|
+
work: requestedWork, workDir, harness: harnessFlag(), backend, yolo, model: flag("model"), branch,
|
|
2014
2053
|
launchConfig: valueFlag("launch-config"),
|
|
2015
2054
|
launch: !args.includes("--no-launch"),
|
|
2016
2055
|
...(triggerEvent ? { triggerEvent } : {}),
|
|
@@ -2077,11 +2116,10 @@ async function spawnCmd() {
|
|
|
2077
2116
|
instance: r.instance, agent: r.agent, home: r.home, work: r.work,
|
|
2078
2117
|
branch: r.branch || null, launched: r.launched, warnings: r.warnings || [],
|
|
2079
2118
|
...(wakeSchedule ? { wakeSchedule } : {}), ...(wakeScheduleError ? { wakeScheduleError } : {}),
|
|
2080
|
-
tmux: r.tmux || null, repo: r.repo || null, harness: r.harness || null,
|
|
2119
|
+
tmux: r.tmux || null, backend: "tmux", repo: r.repo || null, harness: r.harness || null,
|
|
2081
2120
|
model: r.model || null, parent: r.parentInstance || null,
|
|
2082
2121
|
sibling: r.siblingInstance || null, relation: r.relation || null,
|
|
2083
2122
|
spawnOrigin: r.spawnOrigin, attach: r.attach,
|
|
2084
|
-
...(r.sessionTarget ? { sessionTarget: r.sessionTarget } : {}),
|
|
2085
2123
|
...(r.yolo !== undefined ? { yolo: r.yolo } : {}),
|
|
2086
2124
|
// K6b/K6c: what bound this spawn, and whether this receipt is a replay of an earlier one.
|
|
2087
2125
|
...(r.decision ? { decision: r.decision } : {}), ...(r.replayed !== undefined ? { replayed: r.replayed } : {}),
|
|
@@ -2090,7 +2128,7 @@ async function spawnCmd() {
|
|
|
2090
2128
|
});
|
|
2091
2129
|
return;
|
|
2092
2130
|
}
|
|
2093
|
-
console.log(`Spawned ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch}` : ""})${r.launched ?
|
|
2131
|
+
console.log(`Spawned ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch}` : ""})${r.launched ? ` — tmux window "${r.tmux.window}"` : " — not launched"}`);
|
|
2094
2132
|
console.log(` home: ${shortPath(r.home)}`);
|
|
2095
2133
|
if (wakeSchedule) console.log(` wake: schedule ${wakeSchedule.id} (${wakeSchedule.cron} ${wakeSchedule.tz}), next ${wakeSchedule.nextRun || "disabled"}`);
|
|
2096
2134
|
if (wakeScheduleError) console.error(` wake: NOT saved — ${wakeScheduleError.message} (the instance is created and launched; add the wake by hand with oats schedule add)`);
|
|
@@ -2823,7 +2861,7 @@ function versionCmd() {
|
|
|
2823
2861
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2824
2862
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2825
2863
|
// never listed.
|
|
2826
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"
|
|
2864
|
+
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"], 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 }));
|
|
2827
2865
|
return;
|
|
2828
2866
|
}
|
|
2829
2867
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -2867,9 +2905,9 @@ async function experimentalCmd() {
|
|
|
2867
2905
|
* an OpenSSH host alias, the remote workspace, the remote oats path. Keys
|
|
2868
2906
|
* and passwords never enter it; ssh owns those. */
|
|
2869
2907
|
function serverCmd() {
|
|
2870
|
-
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
2908
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
2871
2909
|
const sub = args[1];
|
|
2872
|
-
const usage = "usage: oats server add <id> --ssh <host-alias> --workspace </abs/path> [--oats <path>] [--
|
|
2910
|
+
const usage = "usage: oats server add <id> --ssh <host-alias> --workspace </abs/path> [--oats <path>] [--path <dir:dir>] [--label <text>] [--replace] | list | remove <id> | check <id> | roster [--server <id>] | forget <id> --instance <name> [--json]";
|
|
2873
2911
|
if (!["add", "list", "remove", "check", "roster", "forget"].includes(sub)) bail("E_USAGE", usage);
|
|
2874
2912
|
if (sub === "forget") {
|
|
2875
2913
|
// A saved route whose remote instance is gone can be dropped only by
|
|
@@ -2910,15 +2948,16 @@ function serverCmd() {
|
|
|
2910
2948
|
const rows = Object.entries(servers).map(([id, s]) => ({ id, ...s, target: targetOf({ id, ...s }), snapshots: listSnapshots(id).length }));
|
|
2911
2949
|
if (JSON_MODE) { jsonOk({ file: SERVERS_FILE(), servers: rows }); return; }
|
|
2912
2950
|
if (!rows.length) { console.log(`no servers registered (${shortPath(SERVERS_FILE())}) — add one with \`oats server add <id> --ssh <alias> --workspace </path>\``); return; }
|
|
2913
|
-
for (const r of rows) console.log(` ${r.id}${r.label ? ` ${r.label}` : ""}\n ssh ${r.sshHost} workspace ${r.workspace} oats ${r.target.oatsPath}${r.
|
|
2951
|
+
for (const r of rows) console.log(` ${r.id}${r.label ? ` ${r.label}` : ""}\n ssh ${r.sshHost} workspace ${r.workspace} oats ${r.target.oatsPath}${r.snapshots ? ` (${r.snapshots} remote instance${r.snapshots === 1 ? "" : "s"} spawned from here)` : ""}`);
|
|
2914
2952
|
return;
|
|
2915
2953
|
}
|
|
2916
2954
|
const id = args[2];
|
|
2917
2955
|
if (!id || id.startsWith("--")) bail("E_USAGE", usage);
|
|
2918
2956
|
if (sub === "add") {
|
|
2919
2957
|
const val = (name) => { const v = flag(name); return v === true ? bail("E_BAD_ARGS", `--${name} needs a value`) : v; };
|
|
2958
|
+
if (flag("herdr") !== undefined) { const e = herdrSettingRemoved("server add --herdr was given"); bail(e.code, e.message); }
|
|
2920
2959
|
const entry = { sshHost: val("ssh"), workspace: val("workspace") };
|
|
2921
|
-
for (const [k, f] of [["oatsPath", "oats"], ["
|
|
2960
|
+
for (const [k, f] of [["oatsPath", "oats"], ["path", "path"], ["label", "label"]]) { const v = val(f); if (v !== undefined) entry[k] = v; }
|
|
2922
2961
|
if (!entry.sshHost || !entry.workspace) bail("E_USAGE", usage);
|
|
2923
2962
|
try { validateServer(id, entry); } catch (e) { bail(e.code, e.message); }
|
|
2924
2963
|
if (servers[id] && !args.includes("--replace")) bail("E_SERVER_EXISTS", `server ${id} is already registered (pass --replace to overwrite; existing remote instances keep the route they were spawned with)`);
|
|
@@ -2941,27 +2980,30 @@ function serverCmd() {
|
|
|
2941
2980
|
let server; try { server = getServer(id); } catch (e) { bail(e.code, e.message); }
|
|
2942
2981
|
const target = targetOf(server);
|
|
2943
2982
|
try {
|
|
2944
|
-
const remote = checkRemote(target);
|
|
2983
|
+
const remote = checkRemote(target, { serverId: id });
|
|
2945
2984
|
const status = routeCommand(id, "status", [], { server });
|
|
2946
2985
|
const agents = status.envelope.ok ? (status.envelope.result.agents || []).length : undefined;
|
|
2947
2986
|
if (JSON_MODE) { jsonOk({ id, target, remote, workspaceReachable: !!status.envelope.ok, agents, error: status.envelope.ok ? undefined : status.envelope.error }); return; }
|
|
2948
2987
|
console.log(`${id}: ssh ${target.sshHost} ok, remote oats ${remote.version} (envelope v${remote.schemaVersion})`);
|
|
2949
2988
|
console.log(status.envelope.ok ? ` workspace ${target.workspace}: ${agents} agent(s)` : ` workspace ${target.workspace}: ${status.envelope.error?.message || "not usable"}`);
|
|
2950
2989
|
if (!status.envelope.ok) process.exit(1);
|
|
2951
|
-
} catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
2990
|
+
} catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
2952
2991
|
}
|
|
2953
2992
|
|
|
2954
2993
|
/** `oats <spawn|retire|status> --server <id> ...`: run the command on the
|
|
2955
2994
|
* registered server's installed oats, same arguments, same envelope. The
|
|
2956
2995
|
* local side only routes and keeps the route snapshot per remote instance. */
|
|
2957
2996
|
async function serverRouteCmd() {
|
|
2958
|
-
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
2997
|
+
const bail = (code, msg, details, exit) => (JSON_MODE ? jsonFail(code, msg, details, exit) : die(msg, exit));
|
|
2959
2998
|
const id = flag("server");
|
|
2960
2999
|
if (id === true || !id) bail("E_BAD_ARGS", "--server needs a registered server id (oats server list)");
|
|
2961
|
-
// The operations contract
|
|
2962
|
-
// so
|
|
2963
|
-
//
|
|
2964
|
-
|
|
3000
|
+
// The operations contract, launch-config and the host's reads address an
|
|
3001
|
+
// exact member context on the host, so their explicit --dir travels, as
|
|
3002
|
+
// does a retire plan's or guarded apply's (the lifecycle contract's own
|
|
3003
|
+
// arguments); every other routed command takes its scope from the
|
|
3004
|
+
// registration.
|
|
3005
|
+
const explicitScopeOk = ["inspect", "operation", "launch-config", "readiness", "instance"].includes(cmd)
|
|
3006
|
+
|| (cmd === "retire" && ["--plan", "--plan-revision", "--idempotency-key"].some((f) => args.includes(f)));
|
|
2965
3007
|
if (!explicitScopeOk && flag("dir") !== undefined) bail("E_BAD_ARGS", "--dir cannot be combined with --server: the remote workspace comes from the server registration");
|
|
2966
3008
|
if (cmd === "launch-config") {
|
|
2967
3009
|
const action = args[1];
|
|
@@ -2985,7 +3027,7 @@ async function serverRouteCmd() {
|
|
|
2985
3027
|
options.keepEnv = args.includes("--keep-env");
|
|
2986
3028
|
}
|
|
2987
3029
|
let out;
|
|
2988
|
-
try { out = launchConfigRemote(id, options); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3030
|
+
try { out = launchConfigRemote(id, options); } catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
2989
3031
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
2990
3032
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2991
3033
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "launch configuration request failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3002,7 +3044,7 @@ async function serverRouteCmd() {
|
|
|
3002
3044
|
const inst = flag("instance");
|
|
3003
3045
|
if (!inst || inst === true) bail("E_BAD_ARGS", "okf harvest --server needs --instance <name> (spawned from here)");
|
|
3004
3046
|
let routed;
|
|
3005
|
-
try { routed = routeCommand(id, "harvest", [inst]); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3047
|
+
try { routed = routeCommand(id, "harvest", [inst]); } catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
3006
3048
|
if (routed.stderr?.trim()) process.stderr.write(routed.stderr.endsWith("\n") ? routed.stderr : routed.stderr + "\n");
|
|
3007
3049
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(routed.envelope), null, 2)); if (!routed.envelope.ok) process.exit(1); return; }
|
|
3008
3050
|
if (!routed.envelope.ok) die(`${id}: ${routed.envelope.error?.message || "harvest failed"} (${routed.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3030,7 +3072,7 @@ async function serverRouteCmd() {
|
|
|
3030
3072
|
rest.push(a);
|
|
3031
3073
|
}
|
|
3032
3074
|
let out;
|
|
3033
|
-
try { out = scheduleRemote(id, rest); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3075
|
+
try { out = scheduleRemote(id, rest); } catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
3034
3076
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3035
3077
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3036
3078
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "schedule command failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3043,7 +3085,7 @@ async function serverRouteCmd() {
|
|
|
3043
3085
|
// Desktop preflight before a remote attach: the execution host's own
|
|
3044
3086
|
// inspect, relayed as its envelope; a failure is a failure, nonzero.
|
|
3045
3087
|
let out;
|
|
3046
|
-
try { out = inspectRemote(id, addr); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3088
|
+
try { out = inspectRemote(id, addr); } catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
3047
3089
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3048
3090
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3049
3091
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "inspect failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3056,7 +3098,7 @@ async function serverRouteCmd() {
|
|
|
3056
3098
|
const choices = { ...addr, model: value("model"), launchConfig: value("launch-config"), harness: harnessFlag(value), yolo: yoloFlag() };
|
|
3057
3099
|
if (flag("stop-grace") !== undefined) bail("E_BAD_ARGS", "--stop-grace is currently supported on the execution host; omit it to use the remote restart's default wait");
|
|
3058
3100
|
let out;
|
|
3059
|
-
try { out = (args[1] === "restart" ? restartRemote : startRemote)(id, choices); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3101
|
+
try { out = (args[1] === "restart" ? restartRemote : startRemote)(id, choices); } catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
3060
3102
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3061
3103
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3062
3104
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "start failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3070,7 +3112,7 @@ async function serverRouteCmd() {
|
|
|
3070
3112
|
const file = flag("file");
|
|
3071
3113
|
if (!file || file === true) bail("E_BAD_ARGS", "session upload needs --file <local path>");
|
|
3072
3114
|
let r;
|
|
3073
|
-
try { r = uploadAttachment({ file, server: id, ...addr }); } catch (e) { bail(e.code || "E_UPLOAD_FAILED", e.message); }
|
|
3115
|
+
try { r = uploadAttachment({ file, server: id, ...addr }); } catch (e) { bail(e.code || "E_UPLOAD_FAILED", e.message, e.details); }
|
|
3074
3116
|
if (r.stderr) process.stderr.write(r.stderr + "\n");
|
|
3075
3117
|
if (JSON_MODE) { jsonOk(r); return; }
|
|
3076
3118
|
console.log(`Uploaded ${r.name} (${r.bytes} bytes) to ${r.instance || r.home} on ${id}: ${r.path}`);
|
|
@@ -3079,11 +3121,26 @@ async function serverRouteCmd() {
|
|
|
3079
3121
|
if (args[1] !== "attach") bail("E_USAGE", "--server routes `session inspect`, `session start`, `session restart`, `session upload` and `session attach`; input runs on the execution host (the wake broker calls it there)");
|
|
3080
3122
|
let route;
|
|
3081
3123
|
try { route = attachArgv(id, addr, { skipVersionCheck: args.includes("--print") }); }
|
|
3082
|
-
|
|
3124
|
+
// ssh failing before the viewer (the version probe, a name resolved through the host's roster)
|
|
3125
|
+
// is the failure ssh has under it: exit 255, on which a caller reconnects. Every other refusal,
|
|
3126
|
+
// and an ssh that never started (no link can come back), exits 1.
|
|
3127
|
+
catch (e) { bail(e.code || "E_BAD_ARGS", e.message, e.details, e.code === "E_SSH" && e.details?.sshStarted !== false ? 255 : 1); }
|
|
3083
3128
|
if (args.includes("--print")) { console.log(route.argv.map(shellQuote).join(" ")); return; }
|
|
3084
3129
|
const r = spawnSyncProc(route.argv[0], route.argv.slice(1), { stdio: "inherit" });
|
|
3130
|
+
// ssh exits 255 for its own failures: a link that died under the viewer
|
|
3131
|
+
// (keepalives unanswered, the master or the host's sshd gone), or one
|
|
3132
|
+
// never made. Say what is known rather than ending silently.
|
|
3133
|
+
if (r.status === 255) console.error(`\noats: ssh to ${route.target.sshHost} ended with an error (exit 255); if the link was lost, the instance keeps running on ${id}. Reattach with: oats session attach --server ${id} --home ${shellQuote(route.home)}`);
|
|
3085
3134
|
process.exit(r.status ?? 1);
|
|
3086
3135
|
}
|
|
3136
|
+
// A spawn's argv is checked here, before the server is contacted.
|
|
3137
|
+
if (cmd === "spawn") {
|
|
3138
|
+
const herdr = herdrSpawnFlag();
|
|
3139
|
+
if (herdr) { const e = herdrSettingRemoved(`${herdr} was given`); bail(e.code, e.message); }
|
|
3140
|
+
const local = args.slice(1).filter((a, i, all) => a !== "--server" && all[i - 1] !== "--server");
|
|
3141
|
+
const problem = spawnArgvProblem(local);
|
|
3142
|
+
if (problem) bail("E_BAD_ARGS", problem);
|
|
3143
|
+
}
|
|
3087
3144
|
// Everything after the command word travels, minus the routing flags; a
|
|
3088
3145
|
// local --task-file is read here and travels as --task text, since the
|
|
3089
3146
|
// remote cannot read this machine's files.
|
|
@@ -3121,9 +3178,16 @@ async function serverRouteCmd() {
|
|
|
3121
3178
|
}
|
|
3122
3179
|
let routed;
|
|
3123
3180
|
try { routed = routeCommand(id, cmd, rest); }
|
|
3124
|
-
catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3181
|
+
catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
3125
3182
|
const { envelope, stderr } = routed;
|
|
3126
3183
|
if (stderr && stderr.trim()) process.stderr.write(stderr.endsWith("\n") ? stderr : stderr + "\n");
|
|
3184
|
+
// The host's own reads and plans: its envelope, relayed unchanged.
|
|
3185
|
+
if (cmd === "readiness" || cmd === "instance" || (cmd === "retire" && rest.includes("--plan"))) {
|
|
3186
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(envelope), null, 2)); if (!envelope.ok) process.exit(1); return; }
|
|
3187
|
+
if (!envelope.ok) die(`${id}: ${envelope.error?.message || "remote command failed"} (${envelope.error?.code || "E_REMOTE"})`);
|
|
3188
|
+
console.log(JSON.stringify(envelope.result, null, 2));
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3127
3191
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(envelope), null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
|
|
3128
3192
|
if (!envelope.ok && !(cmd === "retire" && envelope.result)) die(`${id}: ${envelope.error?.message || "remote command failed"} (${envelope.error?.code || "E_REMOTE"})`);
|
|
3129
3193
|
const r = envelope.result;
|
|
@@ -3153,7 +3217,7 @@ async function serverRouteCmd() {
|
|
|
3153
3217
|
console.log(`oats status — server ${id} (ssh ${r.target.sshHost}, workspace ${r.target.workspace})\n`);
|
|
3154
3218
|
for (const a of r.agents || []) {
|
|
3155
3219
|
console.log(` ${a.name} [work: ${a.work || "checkout"}, repo: ${a.repo || "?"}]`);
|
|
3156
|
-
for (const i of a.instances || []) console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.
|
|
3220
|
+
for (const i of a.instances || []) console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : livenessWord(i)}${i.runtimeError ? ` ${i.runtimeError}` : ""}`);
|
|
3157
3221
|
}
|
|
3158
3222
|
const snaps = r.snapshots || [];
|
|
3159
3223
|
if (snaps.length) console.log(`\n spawned from this machine: ${snaps.map((s) => s.instance).join(", ")}`);
|
|
@@ -3302,8 +3366,8 @@ Usage:
|
|
|
3302
3366
|
remote instance lives under ~/.oats/remote/)
|
|
3303
3367
|
oats server roster [--server <id>] remote roster grouped by server and saved route
|
|
3304
3368
|
[--budget <ms>] [--per-target <ms>] target: one status pull per group within a total
|
|
3305
|
-
[--json] budget (45 s, 20 s per target);
|
|
3306
|
-
|
|
3369
|
+
[--json] budget (45 s, 20 s per target); every instance the
|
|
3370
|
+
server reports is addressable by --home or name
|
|
3307
3371
|
oats server forget <id> --instance <name> drop a saved route whose remote instance is gone
|
|
3308
3372
|
(the roster shows it as missingRemotely)
|
|
3309
3373
|
oats retire <instance> --home <path> retire exactly that home when two agents own an
|
|
@@ -3311,17 +3375,20 @@ Usage:
|
|
|
3311
3375
|
oats okf harvest --server <id> run the knowledge harvest in a remote instance's
|
|
3312
3376
|
--instance <name> [--json] saved home on its host
|
|
3313
3377
|
oats session inspect|attach --server <id> inspect (envelope) or attach a viewer (ssh PTY) for a
|
|
3314
|
-
--instance <name> | --home <abs> remote instance
|
|
3315
|
-
attach);
|
|
3378
|
+
--instance <name> | --home <abs> remote instance, by its saved route or the server's
|
|
3379
|
+
roster (--print shows attach); oats 0.22.2 or later
|
|
3316
3380
|
oats session start --server <id> start a stopped remote instance in its existing home
|
|
3317
|
-
--instance <name> | --home <abs>
|
|
3381
|
+
--instance <name> | --home <abs> by its saved route or the server's roster; it must advertise
|
|
3318
3382
|
[--model <m>] [--json] session-start (oats 0.22.9 or later)
|
|
3319
3383
|
oats inspect|operation --server <id> the same commands on a registered server over its
|
|
3320
|
-
... [--dir <remote member>] [--home <abs>]
|
|
3384
|
+
... [--dir <remote member>] [--home <abs>] instance home (an explicit --dir travels as is; a --home
|
|
3321
3385
|
is its own context; else the registered workspace);
|
|
3322
3386
|
the server must advertise operations (oats 0.22.16 or later)
|
|
3387
|
+
oats readiness|instance events|git|diff|stop the Desktop's reads and lifecycle plans, and retire --plan,
|
|
3388
|
+
... --server <id> run on the instance's own machine; the host's envelope is
|
|
3389
|
+
relayed unchanged (the host must advertise the feature)
|
|
3323
3390
|
oats session upload --server <id> copy a local file into a remote instance's private
|
|
3324
|
-
--instance <name> | --home <abs> attachments
|
|
3391
|
+
--instance <name> | --home <abs> attachments, by its home or name (bytes stream on
|
|
3325
3392
|
--file <path> [--json] ssh stdin; sha256 verified); the server must
|
|
3326
3393
|
advertise session-upload (oats 0.22.13 or later)
|
|
3327
3394
|
oats onboard [<dir>] --workspace <repo ref> realize a workspace here: writes <dir>/oats-local.yaml
|
|
@@ -3366,14 +3433,14 @@ Usage:
|
|
|
3366
3433
|
[--model <m>] [--json] (same identity, worktree, notes and launch env; no
|
|
3367
3434
|
spawn hooks); --model replaces the recorded model
|
|
3368
3435
|
for this and later starts; a live harness is refused
|
|
3369
|
-
oats spawn <agent> [--task <text> | --task-file <f>] spawn an instance (tmux
|
|
3436
|
+
oats spawn <agent> [--task <text> | --task-file <f>] spawn an instance (tmux; --no-launch
|
|
3370
3437
|
[--purpose <slug>] [--repo <r>] = scaffold only); the agent is a workspace
|
|
3371
3438
|
[--parent <instance>] soul or a capability-defined agent
|
|
3372
3439
|
[--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
|
|
3373
3440
|
[--relative-to <instance>] new instance to an existing one; --parent X
|
|
3374
3441
|
[--relative-root <agents-root>] disambiguates same-named team anchors
|
|
3375
3442
|
[--work worktree|checkout|attached|workspace|directory] = sugar for --relative-to X --relation
|
|
3376
|
-
[--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux
|
|
3443
|
+
[--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
|
|
3377
3444
|
[--no-launch] [--json] without --launch-config/--harness the launch is the
|
|
3378
3445
|
soul's preference: oats-local.yaml souls.launch.<soul>,
|
|
3379
3446
|
then souls.launch."*", then the soul's launch:, then pi
|
package/docs/configuration.md
CHANGED
|
@@ -34,6 +34,8 @@ souls:
|
|
|
34
34
|
|
|
35
35
|
host:
|
|
36
36
|
name: ana-laptop # which workspace triggers and schedules run here
|
|
37
|
+
session: # terminal defaults for NEW launches on this host (0.31)
|
|
38
|
+
tmuxSession: oats-agents # the tmux session new tmux instances open in
|
|
37
39
|
triggers:
|
|
38
40
|
disabled: [platform/nightly-review]
|
|
39
41
|
schedules:
|
|
@@ -64,6 +66,7 @@ refused (`E_WORKSPACE_SCHEMA`).
|
|
|
64
66
|
| `souls.teams` | Which teams each soul joins here: `"*"` applies to every soul; a soul's own entry (its name, or `<package>/<soul>`) adds to it. Every soul is also in its default team. Written by `oats soul teams <soul>\|'*' --add … --remove …`. |
|
|
65
67
|
| `souls.default` | A per-soul override of `defaultTeam`; it must be one of that soul's teams here (`E_TEAM_NOT_ELIGIBLE`). Written by `oats soul teams <soul> --default <label>`. |
|
|
66
68
|
| `souls.disabled` | Souls not run on this machine; a spawn is refused with `E_SOUL_DISABLED`. A bare name disables every soul of that name; `<package>/<soul>` or `<member>/<soul>` disables one. |
|
|
69
|
+
| `session.tmuxSession` | The tmux session new tmux instances open their windows in (0.31). Absent: `OATS_TMUX_SESSION`, else `PI_AGENTS_TMUX_SESSION` (the pre-0.31 variable), else `oats-agents`. `session: { tmuxSession: pi-agents }` keeps the pre-0.31 layout. `oats inspect --json` reports it as `session`. |
|
|
67
70
|
| `host.name` | This machine's name. A workspace trigger or schedule runs only on the host named by its `runsOn` ([schedules.md](schedules.md)). |
|
|
68
71
|
| `automations.trust` | The workspace triggers and schedules (`<member>/<id>`) this host agrees to run, or `"*"` for every one the workspace places here (0.30). Absent or empty: none runs. See [Who runs workspace automations](#who-runs-workspace-automations). |
|
|
69
72
|
| `triggers.disabled`, `schedules.disabled` | Workspace triggers and schedules (`<member>/<id>`) this host does not run, without a commit. Written by `oats trigger disable` / `oats schedule disable`. |
|
|
@@ -151,10 +154,27 @@ source variable must be set on the host (`E_LAUNCH_ENV_MISSING`, before
|
|
|
151
154
|
anything is created or stopped), and only the harness's pane receives it.
|
|
152
155
|
`list` and `preview` redact every environment value, literals included.
|
|
153
156
|
|
|
157
|
+
**The instance's `oats`.** Every launch (`oats spawn`, `session start`,
|
|
158
|
+
`session restart`, locally or through `--server`) writes `<home>/.oats/bin/oats`,
|
|
159
|
+
a link to the launching kernel's `bin/oats.mjs`, and runs the harness with
|
|
160
|
+
`<home>/.oats/bin` first on `PATH` and the rest of `PATH` unchanged. Plain
|
|
161
|
+
`oats` inside an instance is therefore the kernel that launched it, even on a
|
|
162
|
+
machine whose `PATH` finds another kernel first. A launch configuration's own
|
|
163
|
+
`PATH` (literal or `fromEnv`) comes after it. A restart by a different kernel
|
|
164
|
+
re-points the link to that kernel; `spawn --no-launch` writes it too. The
|
|
165
|
+
recipe in `instance.json` records the target as `launch.kernelBin` (no JSON
|
|
166
|
+
answer carries it); `oats status` prints it
|
|
167
|
+
(`kernel:`) under `--verbose`, or when it is not the `oats` running the status.
|
|
168
|
+
A launch that cannot write the link fails with `E_LAUNCH_SHIM` naming the path
|
|
169
|
+
and the cause: a spawn is rolled back, a start starts nothing. The recorded
|
|
170
|
+
`command` does not carry the `PATH`; the kernel adds it when it runs the
|
|
171
|
+
command. Hooks still receive `OATS_CLI_BIN`, unchanged.
|
|
172
|
+
|
|
154
173
|
**The launch recipe.** A spawn records what a start is made of in
|
|
155
174
|
`instance.json` under `launch`: the harness, the configuration and where it
|
|
156
175
|
came from, the executable, args, env, model, yolo, and each capability's
|
|
157
|
-
launch contribution with its settings and trust
|
|
176
|
+
launch contribution with its settings and trust, and the kernel that launched
|
|
177
|
+
it (`kernelBin`, re-written by every start). One renderer turns it into
|
|
158
178
|
the `command`. Configuration `args` go after the harness's own options and
|
|
159
179
|
before capability arguments; every argument is single-quoted.
|
|
160
180
|
|