@awebai/oats 0.30.3 → 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 +76 -44
- package/docs/configuration.md +3 -0
- package/docs/desktop-cli-api.md +112 -11
- package/docs/desktop-instance-start.md +3 -1
- package/docs/desktop.md +20 -0
- package/docs/execution-targets.md +53 -36
- 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.31.0.md +130 -0
- package/docs/schedules.md +4 -1
- package/docs/servers.md +94 -25
- package/docs/souls-and-instances.md +2 -2
- package/lib/attachments.mjs +2 -1
- package/lib/core.mjs +166 -209
- 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 -143
package/bin/oats.mjs
CHANGED
|
@@ -23,6 +23,7 @@ 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,
|
|
@@ -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,7 +1790,7 @@ 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)}`);
|
|
1795
1796
|
// The kernel the home's plain `oats` runs (its last launch's), when it is not this one.
|
|
@@ -1806,10 +1807,12 @@ async function status() {
|
|
|
1806
1807
|
}
|
|
1807
1808
|
}
|
|
1808
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";
|
|
1809
1812
|
/** The flags `oats spawn` reads: those taking a value, and switches. `--provider` takes two words.
|
|
1810
1813
|
* `--instance` is refused by a local spawn (with its replacement) but still travels to an older
|
|
1811
1814
|
* host through `--server`, whose route reads it. */
|
|
1812
|
-
const SPAWN_VALUE_FLAGS = new Set(["agents-root", "backend", "base", "branch", "dir", "expect-decision", "harness", "
|
|
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"]);
|
|
1813
1816
|
const SPAWN_SWITCHES = new Set(["allow-child-spawns", "json", "no-child-spawns", "no-launch", "no-yolo", "preview", "yolo"]);
|
|
1814
1817
|
/** Why `argv` (after `spawn`, the soul first) is not a spawn, or undefined: a positional after the
|
|
1815
1818
|
* soul or a flag spawn does not read is never ignored. A value flag consumes its value exactly as
|
|
@@ -1831,13 +1834,20 @@ function spawnArgvProblem(argv) {
|
|
|
1831
1834
|
}
|
|
1832
1835
|
return undefined;
|
|
1833
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
|
+
}
|
|
1834
1843
|
async function spawnCmd() {
|
|
1835
1844
|
// JSON mode: contract envelope, stable error codes, stderr-only progress.
|
|
1836
1845
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1837
1846
|
const note = (msg) => (JSON_MODE ? console.error(msg) : console.log(msg));
|
|
1838
1847
|
const yolo = yoloFlag();
|
|
1839
|
-
const
|
|
1840
|
-
|
|
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");
|
|
1841
1851
|
const requestedWork = valueFlag("work");
|
|
1842
1852
|
const workDir = valueFlag("work-dir"), branch = valueFlag("branch"), repo = valueFlag("repo");
|
|
1843
1853
|
const checkDirectoryOptions = (work) => {
|
|
@@ -1845,7 +1855,7 @@ async function spawnCmd() {
|
|
|
1845
1855
|
};
|
|
1846
1856
|
checkDirectoryOptions(requestedWork); // before anything is resolved or written
|
|
1847
1857
|
const name = args[1];
|
|
1848
|
-
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]");
|
|
1849
1859
|
// Retired boundary flags (maintainer transport ruling): fail LOUDLY before
|
|
1850
1860
|
// ANY side effect, including root discovery.
|
|
1851
1861
|
// Local souls (local-agents/) are gone with the workspace model: a soul is a member
|
|
@@ -2039,7 +2049,7 @@ async function spawnCmd() {
|
|
|
2039
2049
|
// An attached instance's repository is its work tree owner's (derived by the kernel).
|
|
2040
2050
|
repo: preparedRepo !== undefined ? preparedRepo : ["directory", "attached"].includes(requestedWork || agent.work)
|
|
2041
2051
|
? repo : repo || defaultRepo(workspaceOf(root)) || defaultRepo(process.cwd()),
|
|
2042
|
-
work: requestedWork, workDir, harness: harnessFlag(), backend,
|
|
2052
|
+
work: requestedWork, workDir, harness: harnessFlag(), backend, yolo, model: flag("model"), branch,
|
|
2043
2053
|
launchConfig: valueFlag("launch-config"),
|
|
2044
2054
|
launch: !args.includes("--no-launch"),
|
|
2045
2055
|
...(triggerEvent ? { triggerEvent } : {}),
|
|
@@ -2106,11 +2116,10 @@ async function spawnCmd() {
|
|
|
2106
2116
|
instance: r.instance, agent: r.agent, home: r.home, work: r.work,
|
|
2107
2117
|
branch: r.branch || null, launched: r.launched, warnings: r.warnings || [],
|
|
2108
2118
|
...(wakeSchedule ? { wakeSchedule } : {}), ...(wakeScheduleError ? { wakeScheduleError } : {}),
|
|
2109
|
-
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,
|
|
2110
2120
|
model: r.model || null, parent: r.parentInstance || null,
|
|
2111
2121
|
sibling: r.siblingInstance || null, relation: r.relation || null,
|
|
2112
2122
|
spawnOrigin: r.spawnOrigin, attach: r.attach,
|
|
2113
|
-
...(r.sessionTarget ? { sessionTarget: r.sessionTarget } : {}),
|
|
2114
2123
|
...(r.yolo !== undefined ? { yolo: r.yolo } : {}),
|
|
2115
2124
|
// K6b/K6c: what bound this spawn, and whether this receipt is a replay of an earlier one.
|
|
2116
2125
|
...(r.decision ? { decision: r.decision } : {}), ...(r.replayed !== undefined ? { replayed: r.replayed } : {}),
|
|
@@ -2119,7 +2128,7 @@ async function spawnCmd() {
|
|
|
2119
2128
|
});
|
|
2120
2129
|
return;
|
|
2121
2130
|
}
|
|
2122
|
-
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"}`);
|
|
2123
2132
|
console.log(` home: ${shortPath(r.home)}`);
|
|
2124
2133
|
if (wakeSchedule) console.log(` wake: schedule ${wakeSchedule.id} (${wakeSchedule.cron} ${wakeSchedule.tz}), next ${wakeSchedule.nextRun || "disabled"}`);
|
|
2125
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)`);
|
|
@@ -2852,7 +2861,7 @@ function versionCmd() {
|
|
|
2852
2861
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2853
2862
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2854
2863
|
// never listed.
|
|
2855
|
-
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 }));
|
|
2856
2865
|
return;
|
|
2857
2866
|
}
|
|
2858
2867
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -2896,9 +2905,9 @@ async function experimentalCmd() {
|
|
|
2896
2905
|
* an OpenSSH host alias, the remote workspace, the remote oats path. Keys
|
|
2897
2906
|
* and passwords never enter it; ssh owns those. */
|
|
2898
2907
|
function serverCmd() {
|
|
2899
|
-
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));
|
|
2900
2909
|
const sub = args[1];
|
|
2901
|
-
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]";
|
|
2902
2911
|
if (!["add", "list", "remove", "check", "roster", "forget"].includes(sub)) bail("E_USAGE", usage);
|
|
2903
2912
|
if (sub === "forget") {
|
|
2904
2913
|
// A saved route whose remote instance is gone can be dropped only by
|
|
@@ -2939,15 +2948,16 @@ function serverCmd() {
|
|
|
2939
2948
|
const rows = Object.entries(servers).map(([id, s]) => ({ id, ...s, target: targetOf({ id, ...s }), snapshots: listSnapshots(id).length }));
|
|
2940
2949
|
if (JSON_MODE) { jsonOk({ file: SERVERS_FILE(), servers: rows }); return; }
|
|
2941
2950
|
if (!rows.length) { console.log(`no servers registered (${shortPath(SERVERS_FILE())}) — add one with \`oats server add <id> --ssh <alias> --workspace </path>\``); return; }
|
|
2942
|
-
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)` : ""}`);
|
|
2943
2952
|
return;
|
|
2944
2953
|
}
|
|
2945
2954
|
const id = args[2];
|
|
2946
2955
|
if (!id || id.startsWith("--")) bail("E_USAGE", usage);
|
|
2947
2956
|
if (sub === "add") {
|
|
2948
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); }
|
|
2949
2959
|
const entry = { sshHost: val("ssh"), workspace: val("workspace") };
|
|
2950
|
-
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; }
|
|
2951
2961
|
if (!entry.sshHost || !entry.workspace) bail("E_USAGE", usage);
|
|
2952
2962
|
try { validateServer(id, entry); } catch (e) { bail(e.code, e.message); }
|
|
2953
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)`);
|
|
@@ -2970,27 +2980,30 @@ function serverCmd() {
|
|
|
2970
2980
|
let server; try { server = getServer(id); } catch (e) { bail(e.code, e.message); }
|
|
2971
2981
|
const target = targetOf(server);
|
|
2972
2982
|
try {
|
|
2973
|
-
const remote = checkRemote(target);
|
|
2983
|
+
const remote = checkRemote(target, { serverId: id });
|
|
2974
2984
|
const status = routeCommand(id, "status", [], { server });
|
|
2975
2985
|
const agents = status.envelope.ok ? (status.envelope.result.agents || []).length : undefined;
|
|
2976
2986
|
if (JSON_MODE) { jsonOk({ id, target, remote, workspaceReachable: !!status.envelope.ok, agents, error: status.envelope.ok ? undefined : status.envelope.error }); return; }
|
|
2977
2987
|
console.log(`${id}: ssh ${target.sshHost} ok, remote oats ${remote.version} (envelope v${remote.schemaVersion})`);
|
|
2978
2988
|
console.log(status.envelope.ok ? ` workspace ${target.workspace}: ${agents} agent(s)` : ` workspace ${target.workspace}: ${status.envelope.error?.message || "not usable"}`);
|
|
2979
2989
|
if (!status.envelope.ok) process.exit(1);
|
|
2980
|
-
} catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
2990
|
+
} catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
2981
2991
|
}
|
|
2982
2992
|
|
|
2983
2993
|
/** `oats <spawn|retire|status> --server <id> ...`: run the command on the
|
|
2984
2994
|
* registered server's installed oats, same arguments, same envelope. The
|
|
2985
2995
|
* local side only routes and keeps the route snapshot per remote instance. */
|
|
2986
2996
|
async function serverRouteCmd() {
|
|
2987
|
-
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));
|
|
2988
2998
|
const id = flag("server");
|
|
2989
2999
|
if (id === true || !id) bail("E_BAD_ARGS", "--server needs a registered server id (oats server list)");
|
|
2990
|
-
// The operations contract
|
|
2991
|
-
// so
|
|
2992
|
-
//
|
|
2993
|
-
|
|
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)));
|
|
2994
3007
|
if (!explicitScopeOk && flag("dir") !== undefined) bail("E_BAD_ARGS", "--dir cannot be combined with --server: the remote workspace comes from the server registration");
|
|
2995
3008
|
if (cmd === "launch-config") {
|
|
2996
3009
|
const action = args[1];
|
|
@@ -3014,7 +3027,7 @@ async function serverRouteCmd() {
|
|
|
3014
3027
|
options.keepEnv = args.includes("--keep-env");
|
|
3015
3028
|
}
|
|
3016
3029
|
let out;
|
|
3017
|
-
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); }
|
|
3018
3031
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3019
3032
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3020
3033
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "launch configuration request failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3031,7 +3044,7 @@ async function serverRouteCmd() {
|
|
|
3031
3044
|
const inst = flag("instance");
|
|
3032
3045
|
if (!inst || inst === true) bail("E_BAD_ARGS", "okf harvest --server needs --instance <name> (spawned from here)");
|
|
3033
3046
|
let routed;
|
|
3034
|
-
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); }
|
|
3035
3048
|
if (routed.stderr?.trim()) process.stderr.write(routed.stderr.endsWith("\n") ? routed.stderr : routed.stderr + "\n");
|
|
3036
3049
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(routed.envelope), null, 2)); if (!routed.envelope.ok) process.exit(1); return; }
|
|
3037
3050
|
if (!routed.envelope.ok) die(`${id}: ${routed.envelope.error?.message || "harvest failed"} (${routed.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3059,7 +3072,7 @@ async function serverRouteCmd() {
|
|
|
3059
3072
|
rest.push(a);
|
|
3060
3073
|
}
|
|
3061
3074
|
let out;
|
|
3062
|
-
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); }
|
|
3063
3076
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3064
3077
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3065
3078
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "schedule command failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3072,7 +3085,7 @@ async function serverRouteCmd() {
|
|
|
3072
3085
|
// Desktop preflight before a remote attach: the execution host's own
|
|
3073
3086
|
// inspect, relayed as its envelope; a failure is a failure, nonzero.
|
|
3074
3087
|
let out;
|
|
3075
|
-
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); }
|
|
3076
3089
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3077
3090
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3078
3091
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "inspect failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3085,7 +3098,7 @@ async function serverRouteCmd() {
|
|
|
3085
3098
|
const choices = { ...addr, model: value("model"), launchConfig: value("launch-config"), harness: harnessFlag(value), yolo: yoloFlag() };
|
|
3086
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");
|
|
3087
3100
|
let out;
|
|
3088
|
-
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); }
|
|
3089
3102
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3090
3103
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3091
3104
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "start failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
@@ -3099,7 +3112,7 @@ async function serverRouteCmd() {
|
|
|
3099
3112
|
const file = flag("file");
|
|
3100
3113
|
if (!file || file === true) bail("E_BAD_ARGS", "session upload needs --file <local path>");
|
|
3101
3114
|
let r;
|
|
3102
|
-
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); }
|
|
3103
3116
|
if (r.stderr) process.stderr.write(r.stderr + "\n");
|
|
3104
3117
|
if (JSON_MODE) { jsonOk(r); return; }
|
|
3105
3118
|
console.log(`Uploaded ${r.name} (${r.bytes} bytes) to ${r.instance || r.home} on ${id}: ${r.path}`);
|
|
@@ -3108,13 +3121,22 @@ async function serverRouteCmd() {
|
|
|
3108
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)");
|
|
3109
3122
|
let route;
|
|
3110
3123
|
try { route = attachArgv(id, addr, { skipVersionCheck: args.includes("--print") }); }
|
|
3111
|
-
|
|
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); }
|
|
3112
3128
|
if (args.includes("--print")) { console.log(route.argv.map(shellQuote).join(" ")); return; }
|
|
3113
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)}`);
|
|
3114
3134
|
process.exit(r.status ?? 1);
|
|
3115
3135
|
}
|
|
3116
3136
|
// A spawn's argv is checked here, before the server is contacted.
|
|
3117
3137
|
if (cmd === "spawn") {
|
|
3138
|
+
const herdr = herdrSpawnFlag();
|
|
3139
|
+
if (herdr) { const e = herdrSettingRemoved(`${herdr} was given`); bail(e.code, e.message); }
|
|
3118
3140
|
const local = args.slice(1).filter((a, i, all) => a !== "--server" && all[i - 1] !== "--server");
|
|
3119
3141
|
const problem = spawnArgvProblem(local);
|
|
3120
3142
|
if (problem) bail("E_BAD_ARGS", problem);
|
|
@@ -3156,9 +3178,16 @@ async function serverRouteCmd() {
|
|
|
3156
3178
|
}
|
|
3157
3179
|
let routed;
|
|
3158
3180
|
try { routed = routeCommand(id, cmd, rest); }
|
|
3159
|
-
catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3181
|
+
catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
3160
3182
|
const { envelope, stderr } = routed;
|
|
3161
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
|
+
}
|
|
3162
3191
|
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(envelope), null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
|
|
3163
3192
|
if (!envelope.ok && !(cmd === "retire" && envelope.result)) die(`${id}: ${envelope.error?.message || "remote command failed"} (${envelope.error?.code || "E_REMOTE"})`);
|
|
3164
3193
|
const r = envelope.result;
|
|
@@ -3188,7 +3217,7 @@ async function serverRouteCmd() {
|
|
|
3188
3217
|
console.log(`oats status — server ${id} (ssh ${r.target.sshHost}, workspace ${r.target.workspace})\n`);
|
|
3189
3218
|
for (const a of r.agents || []) {
|
|
3190
3219
|
console.log(` ${a.name} [work: ${a.work || "checkout"}, repo: ${a.repo || "?"}]`);
|
|
3191
|
-
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}` : ""}`);
|
|
3192
3221
|
}
|
|
3193
3222
|
const snaps = r.snapshots || [];
|
|
3194
3223
|
if (snaps.length) console.log(`\n spawned from this machine: ${snaps.map((s) => s.instance).join(", ")}`);
|
|
@@ -3337,8 +3366,8 @@ Usage:
|
|
|
3337
3366
|
remote instance lives under ~/.oats/remote/)
|
|
3338
3367
|
oats server roster [--server <id>] remote roster grouped by server and saved route
|
|
3339
3368
|
[--budget <ms>] [--per-target <ms>] target: one status pull per group within a total
|
|
3340
|
-
[--json] budget (45 s, 20 s per target);
|
|
3341
|
-
|
|
3369
|
+
[--json] budget (45 s, 20 s per target); every instance the
|
|
3370
|
+
server reports is addressable by --home or name
|
|
3342
3371
|
oats server forget <id> --instance <name> drop a saved route whose remote instance is gone
|
|
3343
3372
|
(the roster shows it as missingRemotely)
|
|
3344
3373
|
oats retire <instance> --home <path> retire exactly that home when two agents own an
|
|
@@ -3346,17 +3375,20 @@ Usage:
|
|
|
3346
3375
|
oats okf harvest --server <id> run the knowledge harvest in a remote instance's
|
|
3347
3376
|
--instance <name> [--json] saved home on its host
|
|
3348
3377
|
oats session inspect|attach --server <id> inspect (envelope) or attach a viewer (ssh PTY) for a
|
|
3349
|
-
--instance <name> | --home <abs> remote instance
|
|
3350
|
-
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
|
|
3351
3380
|
oats session start --server <id> start a stopped remote instance in its existing home
|
|
3352
|
-
--instance <name> | --home <abs>
|
|
3381
|
+
--instance <name> | --home <abs> by its saved route or the server's roster; it must advertise
|
|
3353
3382
|
[--model <m>] [--json] session-start (oats 0.22.9 or later)
|
|
3354
3383
|
oats inspect|operation --server <id> the same commands on a registered server over its
|
|
3355
|
-
... [--dir <remote member>] [--home <abs>]
|
|
3384
|
+
... [--dir <remote member>] [--home <abs>] instance home (an explicit --dir travels as is; a --home
|
|
3356
3385
|
is its own context; else the registered workspace);
|
|
3357
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)
|
|
3358
3390
|
oats session upload --server <id> copy a local file into a remote instance's private
|
|
3359
|
-
--instance <name> | --home <abs> attachments
|
|
3391
|
+
--instance <name> | --home <abs> attachments, by its home or name (bytes stream on
|
|
3360
3392
|
--file <path> [--json] ssh stdin; sha256 verified); the server must
|
|
3361
3393
|
advertise session-upload (oats 0.22.13 or later)
|
|
3362
3394
|
oats onboard [<dir>] --workspace <repo ref> realize a workspace here: writes <dir>/oats-local.yaml
|
|
@@ -3401,14 +3433,14 @@ Usage:
|
|
|
3401
3433
|
[--model <m>] [--json] (same identity, worktree, notes and launch env; no
|
|
3402
3434
|
spawn hooks); --model replaces the recorded model
|
|
3403
3435
|
for this and later starts; a live harness is refused
|
|
3404
|
-
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
|
|
3405
3437
|
[--purpose <slug>] [--repo <r>] = scaffold only); the agent is a workspace
|
|
3406
3438
|
[--parent <instance>] soul or a capability-defined agent
|
|
3407
3439
|
[--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
|
|
3408
3440
|
[--relative-to <instance>] new instance to an existing one; --parent X
|
|
3409
3441
|
[--relative-root <agents-root>] disambiguates same-named team anchors
|
|
3410
3442
|
[--work worktree|checkout|attached|workspace|directory] = sugar for --relative-to X --relation
|
|
3411
|
-
[--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)
|
|
3412
3444
|
[--no-launch] [--json] without --launch-config/--harness the launch is the
|
|
3413
3445
|
soul's preference: oats-local.yaml souls.launch.<soul>,
|
|
3414
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`. |
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -30,8 +30,9 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
30
30
|
|
|
31
31
|
```json
|
|
32
32
|
{"schemaVersion":1,"name":"@awebai/oats","version":"0.30.0","desktopApi":1,
|
|
33
|
-
"harnesses":["pi","claude","codex"],"sessionBackends":["tmux"
|
|
34
|
-
"remote":["spawn","retire","status","session","session-start","session-restart","launch-config","roster","harvest","schedule","session-upload","operations"
|
|
33
|
+
"harnesses":["pi","claude","codex"],"sessionBackends":["tmux"],"launchOptions":["yolo"],
|
|
34
|
+
"remote":["spawn","retire","status","session","session-start","session-restart","launch-config","roster","harvest","schedule","session-upload","operations",
|
|
35
|
+
"readiness","instance-events","instance-git","lifecycle-plans"],
|
|
35
36
|
"features":["retire-home","session-start","session-restart","launch-config","schedule","session-upload","operations","instance-git",
|
|
36
37
|
"instance-git-remote","souls-declarations","lifecycle-plans","retire-retention","readiness","spawn-preview","instance-events",
|
|
37
38
|
"instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
|
|
@@ -47,10 +48,14 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
47
48
|
accepted. The real gate is the feature list; its minimum is
|
|
48
49
|
`packages-no-approval`.
|
|
49
50
|
- `harnesses` is what `--harness` accepts; `sessionBackends` what `--backend`
|
|
50
|
-
accepts
|
|
51
|
+
accepts: `["tmux"]` since 0.31.0, when Herdr was removed (`--backend herdr`
|
|
52
|
+
is refused with `E_HERDR_REMOVED`). A host without the `harness` feature
|
|
53
|
+
lists `runtimes` instead.
|
|
51
54
|
- `remote` is the routed surface: the commands `--server <id>` sends to a
|
|
52
55
|
registered server, plus `roster`. The Desktop checks the execution host's
|
|
53
|
-
probe before a routed mutation.
|
|
56
|
+
probe before a routed mutation. From 0.31: `readiness`, `instance-events`,
|
|
57
|
+
`instance-git` and `lifecycle-plans` name the
|
|
58
|
+
[routed reads and plans](#routed-reads-and-plans).
|
|
54
59
|
- In text mode the command prints `@awebai/oats <version> (desktop API v1)`.
|
|
55
60
|
|
|
56
61
|
### Features
|
|
@@ -149,6 +154,20 @@ string for this: to support older kernels, use the spaced form.
|
|
|
149
154
|
| `E_LOCAL_MISSING` | No `oats-local.yaml` in reach of `--dir` or the working directory |
|
|
150
155
|
| `E_UNSUPPORTED_MODE` | A home or selector the kernel no longer runs (below) |
|
|
151
156
|
|
|
157
|
+
<a id="ssh-failures-e_ssh"></a>
|
|
158
|
+
### ssh failures (`E_SSH`)
|
|
159
|
+
|
|
160
|
+
A routed command (`--server`, and `oats server check`) reports ssh's own
|
|
161
|
+
failure as `E_SSH`, message `ssh to <host> failed: …`:
|
|
162
|
+
|
|
163
|
+
- `error.details` is `{"sshStarted": false}` when ssh never started on this
|
|
164
|
+
machine (not installed, not executable): nothing reached the host, and
|
|
165
|
+
retrying cannot help.
|
|
166
|
+
- No `details`: ssh ran and the link failed (unreachable host, refused key,
|
|
167
|
+
lost connection, timeout); a retry may succeed.
|
|
168
|
+
|
|
169
|
+
(0.31.0; before it `E_SSH` never carried details.)
|
|
170
|
+
|
|
152
171
|
Capability dispatch inside a home uses the home's module copies; from a
|
|
153
172
|
deployment it resolves the module as `oats spawn --soul <x>` would and runs it
|
|
154
173
|
with the soul's merged payload. `oats <namespace> --help --json` answers
|
|
@@ -1207,8 +1226,8 @@ with `--expect-decision` records the key and decision in `instance.json`.
|
|
|
1207
1226
|
|
|
1208
1227
|
```json
|
|
1209
1228
|
{"instance":"rm-api","agent":"rm","home":"/w/agents/rm/instances/rm-api","work":"worktree","branch":"agents/rm-api","launched":true,"warnings":[],
|
|
1210
|
-
"tmux":{"session":"
|
|
1211
|
-
"spawnOrigin":"operator","attach":"tmux attach -t
|
|
1229
|
+
"tmux":{"session":"oats-agents","window":"rm-api"},"backend":"tmux","repo":"/w/agents-repo","harness":"pi","model":null,"parent":null,"sibling":null,"relation":null,
|
|
1230
|
+
"spawnOrigin":"operator","attach":"tmux attach -t oats-agents","decision":{"instance":"rm-api","revision":"c557d8ec9a272ba1c1739dc3"},"replayed":false,
|
|
1212
1231
|
"wake":{"requested":false,"saved":null,"error":null},"launchConfig":null,
|
|
1213
1232
|
"launch":{"version":2,"harness":"pi","launchConfig":null,"launchConfigSource":null,"executable":"/usr/local/bin/pi","executableDeclared":null,
|
|
1214
1233
|
"executableResolvedFrom":"PATH","args":[],"env":{},"model":null,"hooks":{"launch":{},"env":{},"contributions":[]},"prompt":{"kind":"task-file","file":"TASK.md"}}}
|
|
@@ -1217,10 +1236,11 @@ with `--expect-decision` records the key and decision in `instance.json`.
|
|
|
1217
1236
|
(`decision` is abridged: it is the full bound decision.)
|
|
1218
1237
|
|
|
1219
1238
|
- Always present: `instance, agent, home, work, branch, launched, warnings
|
|
1220
|
-
(array), tmux ({session, window} | null),
|
|
1239
|
+
(array), tmux ({session, window} | null), backend ("tmux"), repo, harness,
|
|
1240
|
+
model, parent,
|
|
1221
1241
|
sibling, relation, spawnOrigin (operator | instance), attach, launchConfig,
|
|
1222
1242
|
launch` (the redacted recipe).
|
|
1223
|
-
- When they apply: `
|
|
1243
|
+
- When they apply: `yolo`, `decision` and
|
|
1224
1244
|
`replayed` (bound apply), `wake` (keyed apply), `wakeSchedule` and
|
|
1225
1245
|
`wakeScheduleError` (a requested wake).
|
|
1226
1246
|
|
|
@@ -1297,7 +1317,7 @@ workspace-model fields (feature `instance-modules`):
|
|
|
1297
1317
|
```
|
|
1298
1318
|
|
|
1299
1319
|
Abridged: the record also carries the launch recipe and command,
|
|
1300
|
-
composition evidence, the capability runtime, the tmux
|
|
1320
|
+
composition evidence, the capability runtime, the tmux target,
|
|
1301
1321
|
lineage (`parentInstance`, `siblingInstance`, `relation`, `relativeTo`), and
|
|
1302
1322
|
the keyed-spawn fields `decision`, `spawnIdempotencyKey`, `spawnCompleted` and
|
|
1303
1323
|
`wake`; later starts add `restarts` and `restartCount`.
|
|
@@ -1354,8 +1374,13 @@ Not an envelope: `{root, agents, workspace?, problems?, warnings?}`.
|
|
|
1354
1374
|
description, dir, instances}`.
|
|
1355
1375
|
- **Instance rows**: the home's `instance.json` (launch recipe and command
|
|
1356
1376
|
redacted) plus `home` and `instance` (from the directory; a disagreeing
|
|
1357
|
-
claim is kept as `recordedHome`/`recordedInstance`), `running` (
|
|
1358
|
-
|
|
1377
|
+
claim is kept as `recordedHome`/`recordedInstance`), `running` (read from
|
|
1378
|
+
the row's recorded tmux socket and session, never the caller's `$TMUX`;
|
|
1379
|
+
`null` with `runtimeState: "unreachable"` and the tmux error as
|
|
1380
|
+
`runtimeError` when that server cannot be read; `null` for a home a
|
|
1381
|
+
Herdr-era kernel recorded, with `runtimeState: "unsupported"` and
|
|
1382
|
+
`runtimeError: "E_HERDR_REMOVED: …"`, the recorded `sessionTarget` staying
|
|
1383
|
+
in the row),
|
|
1359
1384
|
`identity` when a provider recorded one, `rollbackIncomplete` and
|
|
1360
1385
|
`retirePending` when present, and the Desktop facts below.
|
|
1361
1386
|
- **`modules`** becomes drift rows `{name, from, commit, current, status,
|
|
@@ -1379,6 +1404,82 @@ restart, else `createdAt` for a launched home, else `null`. `modelFrom` is
|
|
|
1379
1404
|
`"harness-default"`, or `null` for an older home. `identityAddress` is the
|
|
1380
1405
|
messaging identity's `address` (else `alias`), or `null`.
|
|
1381
1406
|
|
|
1407
|
+
<a id="the-remote-roster-oats-server-roster---json"></a>
|
|
1408
|
+
### The remote roster (`oats server roster --json`)
|
|
1409
|
+
|
|
1410
|
+
```text
|
|
1411
|
+
oats server roster [--server <id>] [--per-target <ms>] [--budget <ms>] --json
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
An envelope; `result` is `{groups, bounds}` (remote `roster`,
|
|
1415
|
+
[servers.md](servers.md#the-roster-and-harvest)). One group per server id and
|
|
1416
|
+
route target:
|
|
1417
|
+
|
|
1418
|
+
```json
|
|
1419
|
+
{"id":"build:3f2a…","server":"build","label":"Build box","registrationPresent":true,
|
|
1420
|
+
"target":{"sshHost":"build-host","workspace":"/srv/team","oatsPath":"oats"},
|
|
1421
|
+
"probe":{"ok":true},"agentsRoot":"/srv/team/agents",
|
|
1422
|
+
"souls":[{"name":"dev","harness":"claude","work":"worktree","agentsRoot":"/srv/team/agents"}],
|
|
1423
|
+
"instances":[{"server":"build","instance":"dev-a","agent":"dev","home":"/srv/team/agents/dev/instances/dev-a",
|
|
1424
|
+
"agentsRoot":"/srv/team/agents","harness":"claude","backend":"tmux","tmux":{"session":"oats-agents","window":"dev-a"},
|
|
1425
|
+
"running":true,"identity":{"alias":"dev-a","address":"acme/dev-a"},"identityAddress":"acme/dev-a",
|
|
1426
|
+
"teams":[{"label":"default","team":"acme:team"}],"startedAt":"2026-09-29T10:00:00.000Z","createdAt":"2026-09-29T09:58:12.004Z",
|
|
1427
|
+
"model":"opus","runtimeState":null,"parentInstance":"lead","siblingInstance":null,"relation":"child","relativeTo":"lead",
|
|
1428
|
+
"spawnOrigin":"instance","retirePending":false,"rollbackIncomplete":false,
|
|
1429
|
+
"savedRoute":false,"addressable":true,"missingRemotely":false}],
|
|
1430
|
+
"retireFailures":[]}
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
- **Instance rows** relay the host's own `status --json` row: `identity`,
|
|
1434
|
+
`identityAddress`, `teams`, `startedAt`, `createdAt`, `model`,
|
|
1435
|
+
`runtimeState`, `parentInstance`, `siblingInstance`, `relation`,
|
|
1436
|
+
`relativeTo` and `spawnOrigin` are always present, `null` when the host
|
|
1437
|
+
does not supply them (a host before 0.31, a fact it never recorded, or a
|
|
1438
|
+
saved route the host no longer lists). Nothing is derived on this side.
|
|
1439
|
+
- **`addressable`** (0.31): `true` for every row the host reports. Routed
|
|
1440
|
+
session and lifecycle commands reach it by `--home`, or by name when the
|
|
1441
|
+
name is unique on the host ([addressing](servers.md#run-there); a shared
|
|
1442
|
+
name is `E_AMBIGUOUS` with `error.details.candidates: [{agent, home}]`). A
|
|
1443
|
+
saved-route row the host did not list is addressable only while the host's
|
|
1444
|
+
answer is unknown (`missingRemotely: false`).
|
|
1445
|
+
- **`savedRoute`**: the instance was spawned from this machine and has a
|
|
1446
|
+
saved route here. Information only; no action depends on it.
|
|
1447
|
+
- `running` is `null` when unknown; `backend` is `tmux` for a row with a tmux
|
|
1448
|
+
target, else `null`; `tmux`, `sessionTarget` (the recorded target of a home
|
|
1449
|
+
a Herdr-era kernel opened) and `runtimeError` are as the host reports them.
|
|
1450
|
+
|
|
1451
|
+
<a id="routed-reads-and-plans"></a>
|
|
1452
|
+
### Routed reads and plans (`--server`, 0.31)
|
|
1453
|
+
|
|
1454
|
+
The Desktop's per-instance reads and the lifecycle plans run on the
|
|
1455
|
+
instance's own machine: the local command, with `--server <id>` added.
|
|
1456
|
+
|
|
1457
|
+
| Command | `remote` entry | The host must advertise |
|
|
1458
|
+
|---|---|---|
|
|
1459
|
+
| `oats readiness --server <id> (--home <abs> \| --soul <n>) …` | `readiness` | `readiness`, `readinessApi: 2` |
|
|
1460
|
+
| `oats instance events <name> --server <id> …` | `instance-events` | `instance-events-2`, `eventsApi: 2` |
|
|
1461
|
+
| `oats instance git\|diff <name> --server <id> …` | `instance-git` | `instance-git`, `instanceGitApi: 1` |
|
|
1462
|
+
| `oats instance stop <name> --server <id> (--plan \| --apply …)` | `lifecycle-plans` | `lifecycle-plans`, `lifecycleApi: 1` |
|
|
1463
|
+
| `oats retire <name> --server <id> --plan`, and the guarded apply (`--plan-revision`, `--idempotency-key`) | `lifecycle-plans` | `lifecycle-plans`, `lifecycleApi: 1` |
|
|
1464
|
+
|
|
1465
|
+
- The flags are the local command's. The instance is addressed like every
|
|
1466
|
+
routed instance command ([servers.md](servers.md#run-there)): `--home` as
|
|
1467
|
+
given, else the name through its saved route or the host's roster, sent
|
|
1468
|
+
as `--home`. `--dir` names a directory on the host and travels as is;
|
|
1469
|
+
without it the registered workspace is sent (not for `readiness --home`,
|
|
1470
|
+
whose home is its own context). A retire plan and its guarded apply take
|
|
1471
|
+
`--dir` like the rest; an unguarded `retire --server` refuses it.
|
|
1472
|
+
- An instance with a saved route is reached through it, registration or not.
|
|
1473
|
+
A guarded retire apply whose name the host no longer lists is sent by name,
|
|
1474
|
+
so a repeated key gets the host's recorded receipt (or its refusal).
|
|
1475
|
+
- The host's envelope is relayed unchanged, success or failure: the same
|
|
1476
|
+
document the local command answers, with no routing keys added. The
|
|
1477
|
+
guarded retire apply is the routed `retire`, whose result carries
|
|
1478
|
+
`server` and `target` as before.
|
|
1479
|
+
- A host that does not advertise the feature and API number is refused with
|
|
1480
|
+
`E_REMOTE_INCOMPATIBLE`, naming both and the host's version, before
|
|
1481
|
+
anything is sent. A name two homes share on the host is `E_AMBIGUOUS`.
|
|
1482
|
+
|
|
1382
1483
|
<a id="instance-git-state-oats-instance-gitdiff-instancegitapi-1-oats-0247"></a>
|
|
1383
1484
|
## Git and diff
|
|
1384
1485
|
|
|
@@ -67,4 +67,6 @@ before launch. Desktop does not scaffold a home or execute a launcher itself.
|
|
|
67
67
|
Status collection reads each instance's recorded tmux socket and session,
|
|
68
68
|
with one query per socket per collection. A launcher shell with a harness
|
|
69
69
|
child remains running; a fallback shell or dead pane is stopped. Errors that
|
|
70
|
-
prevent a reliable observation remain unknown.
|
|
70
|
+
prevent a reliable observation remain unknown. A row that records a Herdr
|
|
71
|
+
target (Herdr was removed in 0.31.0) is never observed: it shows the kernel's
|
|
72
|
+
`E_HERDR_REMOVED` text, and Open and Start are disabled.
|