@awebai/oats 0.38.2 → 0.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/oats.mjs +168 -21
- package/docs/capabilities.md +13 -1
- package/docs/desktop-cli-api.md +124 -1
- package/docs/desktop.md +19 -0
- package/docs/integrations.md +13 -3
- package/docs/knowledge.md +3 -2
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +2 -2
- package/docs/release-notes/v0.39.0.md +99 -0
- package/docs/release-notes/v0.39.1.md +42 -0
- package/docs/servers.md +135 -3
- package/lib/dir-lock.mjs +36 -0
- package/lib/instance-inspect.mjs +51 -5
- package/lib/instance-resolution.mjs +55 -17
- package/lib/operator-dispatch.mjs +19 -9
- package/lib/remote.mjs +19 -4
- package/lib/schedule.mjs +3 -26
- package/lib/servers.mjs +320 -2
- package/package-catalog.json +1 -1
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* `init` / `use` / `install` / `restore` / `list` / `catalog` / `remove` /
|
|
20
20
|
* `migrate` / `trust` / `inject` are gone with the installed-capability tier.
|
|
21
21
|
*/
|
|
22
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readSync, realpathSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readSync, readdirSync, realpathSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
24
24
|
import { constants as osConstants, homedir, tmpdir } from "node:os";
|
|
25
25
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
@@ -42,7 +42,7 @@ import { parseConfigData } from "../lib/config-data.mjs";
|
|
|
42
42
|
import * as remoteModule from "../lib/remote.mjs";
|
|
43
43
|
import { activateLocalInputs, localRevision } from "../lib/local-inputs.mjs";
|
|
44
44
|
import YAML from "yaml";
|
|
45
|
-
import { attachArgv, checkRemote, forgetSnapshot, getServer, inspectRemote, startRemote, restartRemote, launchConfigRemote, scheduleRemote, listSnapshots, readServers, rosterGroups, routeCommand, targetOf, validateServer,
|
|
45
|
+
import { attachArgv, checkRemote, connectServer, forgetSnapshot, getServer, inspectRemote, startRemote, restartRemote, launchConfigRemote, scheduleRemote, listSnapshots, learnWorkspaceKey, readServers, redactArgv, reportedWorkspaceKey, rosterGroups, routeCapability, routeCommand, runRemote, serverFlagOf, targetOf, updateServers, validateServer, workspaceKeyOfStatus, workspaceMismatch, SERVERS_FILE } from "../lib/servers.mjs";
|
|
46
46
|
import { spawnSync as spawnSyncProc } from "node:child_process";
|
|
47
47
|
import { tickTriggers } from "../lib/triggers.mjs";
|
|
48
48
|
import * as A from "../lib/automations.mjs";
|
|
@@ -58,7 +58,7 @@ import { readEvents } from "../lib/instance-events.mjs";
|
|
|
58
58
|
|
|
59
59
|
const rawArgs = process.argv.slice(2);
|
|
60
60
|
/** The kernel's switches: a value never rides one (`--yolo=false` must not turn yolo on). */
|
|
61
|
-
const KERNEL_SWITCHES = new Set(["allow-child-spawns", "apply", "check", "clear", "delete-branch", "discard-worktree", "dry-run", "ephemeral", "force", "help", "host", "json", "keep-dir", "keep-env", "no-child-spawns", "no-launch", "no-recursive", "no-yolo", "plan", "policy", "preview", "print", "replace", "self", "verbose", "yes", "yolo"]);
|
|
61
|
+
const KERNEL_SWITCHES = new Set(["allow-child-spawns", "apply", "check", "clear", "delete-branch", "discard-worktree", "dry-run", "ephemeral", "force", "help", "host", "install-oats", "json", "keep-dir", "keep-env", "no-child-spawns", "no-launch", "no-recursive", "no-yolo", "plan", "policy", "preview", "print", "replace", "self", "verbose", "yes", "yolo"]);
|
|
62
62
|
/** `--flag=value` is `--flag value`: every kernel reader (flag(), valueFlag(), the onboard and
|
|
63
63
|
* routed-command loops) then applies the spaced form's validation to it. `problem` is an empty
|
|
64
64
|
* `--flag=`, a switch given a value, or a value that is itself an option (`--model=--yolo`):
|
|
@@ -2746,9 +2746,10 @@ async function onboardCmd() {
|
|
|
2746
2746
|
const bail = (code, message, details) => (JSON_MODE ? jsonFail(code, message, details) : die(message));
|
|
2747
2747
|
const usage = "usage: oats onboard [<dir>] --workspace <repo ref> [--json] (or --dir <dir>)";
|
|
2748
2748
|
let positional, workspaceRef, dirValue;
|
|
2749
|
+
const checkOnly = args.includes("--check");
|
|
2749
2750
|
for (let i = 1; i < args.length; i++) {
|
|
2750
2751
|
const arg = args[i];
|
|
2751
|
-
if (arg === "--json") continue;
|
|
2752
|
+
if (arg === "--json" || arg === "--check") continue;
|
|
2752
2753
|
if (arg === "--dir" || arg === "--workspace") {
|
|
2753
2754
|
const value = args[i + 1];
|
|
2754
2755
|
if (value === undefined || value.startsWith("--")) return bail("E_BAD_ARGS", `--${arg.slice(2)} needs a value\n${usage}`);
|
|
@@ -2761,6 +2762,7 @@ async function onboardCmd() {
|
|
|
2761
2762
|
positional = arg;
|
|
2762
2763
|
}
|
|
2763
2764
|
if (positional !== undefined && dirValue !== undefined) return bail("E_BAD_ARGS", `give the deployment directory once, as <dir> or --dir\n${usage}`);
|
|
2765
|
+
if (checkOnly) return onboardCheck(positional ?? dirValue, workspaceRef, bail);
|
|
2764
2766
|
if (!workspaceRef || !workspaceRef.trim()) return bail("E_BAD_ARGS", `--workspace <repo ref> is required (the repository hosting oats-workspace.yaml)\n${usage}`);
|
|
2765
2767
|
workspaceRef = workspaceRef.trim();
|
|
2766
2768
|
// The ref must be one lib/remote.mjs understands BEFORE anything is written.
|
|
@@ -2867,6 +2869,38 @@ Next:
|
|
|
2867
2869
|
${spawnHint ?? anySoulHint}`);
|
|
2868
2870
|
}
|
|
2869
2871
|
|
|
2872
|
+
/** `oats onboard <dir> [--workspace <ref>] --check`: what onboarding <dir> here would meet, read-only (the
|
|
2873
|
+
* question `oats server connect` asks a host before onboarding there, and `oats server check` asks of a
|
|
2874
|
+
* registered deployment). A leading ~ is this machine's home. `state` is absent | empty | not-empty |
|
|
2875
|
+
* not-a-directory | deployment (oats-local.yaml there; its own workspace is read when --workspace is not
|
|
2876
|
+
* given). `remote` is whether this machine's git reads the workspace remote: an unreadable one is part of
|
|
2877
|
+
* the answer (readable false, with the error, its reason and any hint), never a failure of the check. */
|
|
2878
|
+
async function onboardCheck(dirArg, workspaceRef, bail) {
|
|
2879
|
+
const raw = dirArg ?? process.cwd();
|
|
2880
|
+
const dir = raw === "~" ? homedir() : raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : resolve(raw);
|
|
2881
|
+
let stat = null;
|
|
2882
|
+
try { stat = lstatSync(dir); } catch (e) { if (e.code !== "ENOENT") return bail("E_ONBOARD_FAILED", `cannot inspect ${dir}: ${e.message}`, { dir }); }
|
|
2883
|
+
const state = !stat ? "absent" : !stat.isDirectory() ? "not-a-directory" : existsSync(join(dir, "oats-local.yaml")) ? "deployment" : readdirSync(dir).length ? "not-empty" : "empty";
|
|
2884
|
+
let ref = workspaceRef?.trim();
|
|
2885
|
+
if (!ref && state === "deployment") {
|
|
2886
|
+
try { ref = loadLocal(dir).local.workspace; } catch (e) { return bail(e.code || "E_CONFIG_BROKEN", e.message, e.details); }
|
|
2887
|
+
}
|
|
2888
|
+
if (!ref) return bail("E_BAD_ARGS", `--workspace <repo ref> is required: ${dir} is not a deployment whose workspace could be read instead`, { dir, state });
|
|
2889
|
+
let parsed;
|
|
2890
|
+
try { parsed = remoteModule.parseRepoRef(ref); } catch (e) { return bail(e.code || "E_REPO_REF", e.message, e.details ?? e.provenance); }
|
|
2891
|
+
let remote;
|
|
2892
|
+
try { remote = { readable: true, commit: (await remoteModule.observeRemote(ref, remoteOptionsFromEnv())).commit }; }
|
|
2893
|
+
catch (e) {
|
|
2894
|
+
if (e?.code !== "E_REMOTE_UNREADABLE") throw e;
|
|
2895
|
+
const d = e.details || {};
|
|
2896
|
+
remote = { readable: false, error: { code: e.code, message: e.message, reason: d.reason ?? null, ...(d.hint ? { hint: d.hint, remedy: d.remedy } : {}) } };
|
|
2897
|
+
}
|
|
2898
|
+
const result = { check: true, dir, state, workspace: { ref, key: parsed.key, url: parsed.url }, remote };
|
|
2899
|
+
if (JSON_MODE) { jsonOk(result); return; }
|
|
2900
|
+
console.log(`${shortPath(dir)}: ${state}`);
|
|
2901
|
+
console.log(`workspace ${parsed.key}: ${remote.readable ? `readable (${short(remote.commit)})` : `NOT readable — ${remote.error.message}`}`);
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2870
2904
|
/** The clone URL of a member row: what the remote observed (from the workspace's members: refs;
|
|
2871
2905
|
* standalone, the one repo oats-local.yaml named). */
|
|
2872
2906
|
function memberUrlOf(discovery, key) {
|
|
@@ -2922,6 +2956,9 @@ async function capabilityCommand() {
|
|
|
2922
2956
|
throw e;
|
|
2923
2957
|
}
|
|
2924
2958
|
if (!hit) return NOT_DISPATCHED;
|
|
2959
|
+
// Without --soul the dispatch chose the soul (feature operator-default-soul): say which, on stderr,
|
|
2960
|
+
// so a provider's stdout (its --json envelope included) is untouched.
|
|
2961
|
+
if (hit.defaultSoul) process.stderr.write(`oats ${cmd}: no --soul given; running as soul ${hit.defaultSoul}, the first soul of this deployment that provides "${cmd}" (pass --soul <name> to choose)\n`);
|
|
2925
2962
|
// The same team/workspace facts a spawn hook receives (lead decision c3-7).
|
|
2926
2963
|
const teamCtx = teamEnv(resolvedFromPrepared(hit.prepared, hit.deployment));
|
|
2927
2964
|
// No home, so no recorded soul: OATS_SOUL is the soul's source at the resolved commit, read
|
|
@@ -3142,7 +3179,7 @@ function versionCmd() {
|
|
|
3142
3179
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
3143
3180
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
3144
3181
|
// never listed.
|
|
3145
|
-
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-3", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "spawn-preview-max-age", "launch-config-default", "capability-show", "capture-file", "workspace-identity"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2, capabilityShowApi: 1 }));
|
|
3182
|
+
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-3", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "spawn-preview-max-age", "launch-config-default", "capability-show", "capture-file", "workspace-identity", "server-connect", "capability-route", "servers-per-workspace", "operator-default-soul"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2, capabilityShowApi: 1 }));
|
|
3146
3183
|
return;
|
|
3147
3184
|
}
|
|
3148
3185
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3188,8 +3225,9 @@ async function experimentalCmd() {
|
|
|
3188
3225
|
function serverCmd() {
|
|
3189
3226
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3190
3227
|
const sub = args[1];
|
|
3191
|
-
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]";
|
|
3192
|
-
if (!["add", "list", "remove", "check", "roster", "forget"].includes(sub)) bail("E_USAGE", usage);
|
|
3228
|
+
const usage = "usage: oats server add <id> --ssh <host-alias> --workspace </abs/path> [--oats <path>] [--path <dir:dir>] [--label <text>] [--replace] | connect <id> --ssh <host-alias> [--workspace-ref <ref>] [--dir </abs/path or ~/path on the host>] [--oats <path>] [--path <dir:dir>] [--label <text>] [--install-oats] [--replace] | list [--workspace-ref <ref>] | remove <id> | check <id> | roster [--server <id>] | forget <id> --instance <name> [--json]";
|
|
3229
|
+
if (!["add", "connect", "list", "remove", "check", "roster", "forget"].includes(sub)) bail("E_USAGE", usage);
|
|
3230
|
+
if (sub === "connect") return serverConnectCmd(bail);
|
|
3193
3231
|
if (sub === "forget") {
|
|
3194
3232
|
// A saved route whose remote instance is gone can be dropped only by
|
|
3195
3233
|
// the operator: nothing routed can do it, and the changed-registration
|
|
@@ -3226,10 +3264,21 @@ function serverCmd() {
|
|
|
3226
3264
|
let servers;
|
|
3227
3265
|
try { servers = readServers(); } catch (e) { bail(e.code || "E_SERVERS_UNREADABLE", e.message); }
|
|
3228
3266
|
if (sub === "list") {
|
|
3229
|
-
|
|
3230
|
-
|
|
3267
|
+
let rows = Object.entries(servers).map(([id, s]) => ({ id, ...s, workspaceKey: s.workspaceKey ?? null, target: targetOf({ id, ...s }), snapshots: listSnapshots(id).length }));
|
|
3268
|
+
// --workspace-ref: the servers of one workspace, by canonical key, and the ids whose workspace is not known yet.
|
|
3269
|
+
let unknownWorkspace;
|
|
3270
|
+
if (flag("workspace-ref") !== undefined) {
|
|
3271
|
+
const ref = flag("workspace-ref");
|
|
3272
|
+
if (ref === true) bail("E_BAD_ARGS", "--workspace-ref needs a workspace repository reference");
|
|
3273
|
+
let key;
|
|
3274
|
+
try { key = remoteModule.parseRepoRef(ref).key; } catch (e) { bail(e.code || "E_REPO_REF", e.message, e.details); }
|
|
3275
|
+
unknownWorkspace = rows.filter((r) => r.workspaceKey === null).map((r) => r.id);
|
|
3276
|
+
rows = rows.filter((r) => r.workspaceKey === key);
|
|
3277
|
+
}
|
|
3278
|
+
if (JSON_MODE) { jsonOk({ file: SERVERS_FILE(), servers: rows, ...(unknownWorkspace ? { unknownWorkspace } : {}) }); return; }
|
|
3279
|
+
if (unknownWorkspace?.length) console.log(` (workspace not known yet for ${unknownWorkspace.join(", ")}: oats server check <id> learns it)`);
|
|
3231
3280
|
if (!rows.length) { console.log(`no servers registered (${shortPath(SERVERS_FILE())}) — add one with \`oats server add <id> --ssh <alias> --workspace </path>\``); return; }
|
|
3232
|
-
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)` : ""}`);
|
|
3281
|
+
for (const r of rows) console.log(` ${r.id}${r.label ? ` ${r.label}` : ""}${r.workspaceKey ? ` [${r.workspaceKey}]` : ""}\n ssh ${r.sshHost} workspace ${r.workspace} oats ${r.target.oatsPath}${r.snapshots ? ` (${r.snapshots} remote instance${r.snapshots === 1 ? "" : "s"} spawned from here)` : ""}`);
|
|
3233
3282
|
return;
|
|
3234
3283
|
}
|
|
3235
3284
|
const id = args[2];
|
|
@@ -3241,18 +3290,34 @@ function serverCmd() {
|
|
|
3241
3290
|
for (const [k, f] of [["oatsPath", "oats"], ["path", "path"], ["label", "label"]]) { const v = val(f); if (v !== undefined) entry[k] = v; }
|
|
3242
3291
|
if (!entry.sshHost || !entry.workspace) bail("E_USAGE", usage);
|
|
3243
3292
|
try { validateServer(id, entry); } catch (e) { bail(e.code, e.message); }
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3293
|
+
try {
|
|
3294
|
+
updateServers((current) => {
|
|
3295
|
+
if (current[id] && !args.includes("--replace")) throw Object.assign(new Error(`server ${id} is already registered (pass --replace to overwrite; existing remote instances keep the route they were spawned with)`), { code: "E_SERVER_EXISTS" });
|
|
3296
|
+
current[id] = entry;
|
|
3297
|
+
});
|
|
3298
|
+
} catch (e) { bail(e.code || "E_SERVERS_UNREADABLE", e.message); }
|
|
3299
|
+
// The workspace key is learned from the host, never typed: recorded when the host answers one and
|
|
3300
|
+
// the registration is still this one (it may change while the host is asked).
|
|
3301
|
+
const warnings = [];
|
|
3302
|
+
try {
|
|
3303
|
+
const { key, why } = reportedWorkspaceKey(targetOf(entry), { serverId: id, timeoutMs: 60000 });
|
|
3304
|
+
if (!key) warnings.push(`workspace key unknown: ${why}; oats server check ${id} learns it once the host answers one`);
|
|
3305
|
+
else if (learnWorkspaceKey(id, entry, key) === "recorded") entry.workspaceKey = key;
|
|
3306
|
+
else warnings.push(`workspace key not recorded: registration ${id} changed while the host was asked; oats server check ${id} learns it`);
|
|
3307
|
+
} catch (e) { warnings.push(`workspace key unknown: ${e.message}; oats server check ${id} learns it once the host answers`); }
|
|
3308
|
+
if (JSON_MODE) { jsonOk({ id, ...entry, workspaceKey: entry.workspaceKey ?? null, file: SERVERS_FILE(), ...(warnings.length ? { warnings } : {}) }); return; }
|
|
3309
|
+
console.log(`Registered server ${id} → ssh ${entry.sshHost}, workspace ${entry.workspace}${entry.workspaceKey ? ` (workspace ${entry.workspaceKey})` : ""} (${shortPath(SERVERS_FILE())}). Verify it with \`oats server check ${id}\`.`);
|
|
3310
|
+
for (const w of warnings) console.error(`oats: warning: ${w}`);
|
|
3249
3311
|
return;
|
|
3250
3312
|
}
|
|
3251
3313
|
if (sub === "remove") {
|
|
3252
|
-
|
|
3314
|
+
try {
|
|
3315
|
+
updateServers((current) => {
|
|
3316
|
+
if (!current[id]) throw Object.assign(new Error(`no server registered as ${id}`), { code: "E_SERVER_UNKNOWN" });
|
|
3317
|
+
delete current[id];
|
|
3318
|
+
});
|
|
3319
|
+
} catch (e) { bail(e.code || "E_SERVERS_UNREADABLE", e.message); }
|
|
3253
3320
|
const snaps = listSnapshots(id);
|
|
3254
|
-
delete servers[id];
|
|
3255
|
-
writeServers(servers);
|
|
3256
3321
|
if (JSON_MODE) { jsonOk({ removed: id, remoteInstancesStillTracked: snaps.map((s) => s.instance) }); return; }
|
|
3257
3322
|
console.log(`Removed server ${id}${snaps.length ? ` — ${snaps.length} remote instance(s) spawned from it keep their snapshots and can still be retired with --server ${id}` : ""}`);
|
|
3258
3323
|
return;
|
|
@@ -3264,13 +3329,81 @@ function serverCmd() {
|
|
|
3264
3329
|
const remote = checkRemote(target, { serverId: id });
|
|
3265
3330
|
const status = routeCommand(id, "status", [], { server });
|
|
3266
3331
|
const agents = status.envelope.ok ? (status.envelope.result.agents || []).length : undefined;
|
|
3267
|
-
|
|
3268
|
-
|
|
3332
|
+
// The workspace key: a contradiction is refused (never rewritten), an absent one backfilled.
|
|
3333
|
+
const { key: reported } = workspaceKeyOfStatus(status.envelope);
|
|
3334
|
+
const learned = reported ? learnWorkspaceKey(id, server, reported) : null;
|
|
3335
|
+
if (learned?.mismatch) { const e = workspaceMismatch(id, { ...server, workspaceKey: learned.mismatch }, reported); bail(e.code, e.message, e.details); }
|
|
3336
|
+
const workspaceKey = reported ?? server.workspaceKey ?? null;
|
|
3337
|
+
// Whether git on the host reads the workspace remote: the host's own read-only onboard check
|
|
3338
|
+
// (feature server-connect); null when the host cannot say.
|
|
3339
|
+
let workspaceReadable = null, workspaceReadError;
|
|
3340
|
+
if (status.envelope.ok && remote.features.includes("server-connect")) {
|
|
3341
|
+
const probe = runRemote(target, ["onboard", target.workspace, "--check", "--json"], { serverId: id }).envelope;
|
|
3342
|
+
if (probe.ok && probe.result?.remote) {
|
|
3343
|
+
workspaceReadable = probe.result.remote.readable === true;
|
|
3344
|
+
if (!workspaceReadable) { const { code, message, reason, hint } = probe.result.remote.error || {}; workspaceReadError = { code, message, reason, ...(hint ? { hint } : {}) }; }
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
if (JSON_MODE) { jsonOk({ id, target, remote, workspaceKey, workspaceReachable: !!status.envelope.ok, workspaceReadable, ...(workspaceReadError ? { workspaceReadError } : {}), agents, error: status.envelope.ok ? undefined : status.envelope.error }); return; }
|
|
3348
|
+
console.log(`${id}: ssh ${target.sshHost} ok, remote oats ${remote.version} (envelope v${remote.schemaVersion})${workspaceKey ? `, workspace ${workspaceKey}` : ""}`);
|
|
3349
|
+
if (workspaceReadable === false) console.log(` the host's git cannot read the workspace remote: ${workspaceReadError.message}`);
|
|
3269
3350
|
console.log(status.envelope.ok ? ` workspace ${target.workspace}: ${agents} agent(s)` : ` workspace ${target.workspace}: ${status.envelope.error?.message || "not usable"}`);
|
|
3270
3351
|
if (!status.envelope.ok) process.exit(1);
|
|
3271
3352
|
} catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
|
|
3272
3353
|
}
|
|
3273
3354
|
|
|
3355
|
+
/** `oats server connect <id> --ssh <host>`: this deployment's workspace on a host, registered
|
|
3356
|
+
* (lib/servers.mjs connectServer). Defaults come from the deployment it runs in: its workspace
|
|
3357
|
+
* ref, and ~/Agents/<its directory's name> on the host. */
|
|
3358
|
+
function serverConnectCmd(bail) {
|
|
3359
|
+
const id = args[2];
|
|
3360
|
+
if (!id || id.startsWith("--")) bail("E_USAGE", "usage: oats server connect <id> --ssh <host-alias> [--workspace-ref <ref>] [--dir </abs/path or ~/path on the host>] [--oats <path>] [--path <dir:dir>] [--label <text>] [--install-oats] [--replace] [--json]");
|
|
3361
|
+
const val = (name) => { const v = flag(name); return v === true ? bail("E_BAD_ARGS", `--${name} needs a value`) : v; };
|
|
3362
|
+
const sshHost = val("ssh");
|
|
3363
|
+
if (!sshHost) bail("E_BAD_ARGS", "--ssh <host-alias> is required: the OpenSSH host alias of the machine to connect");
|
|
3364
|
+
let local = null;
|
|
3365
|
+
try { local = loadLocal(process.cwd()); } catch (e) { if (e?.code !== "E_LOCAL_MISSING") bail(e.code || "E_CONFIG_BROKEN", e.message, e.details); }
|
|
3366
|
+
const workspaceRef = val("workspace-ref") ?? local?.local.workspace;
|
|
3367
|
+
if (!workspaceRef) bail("E_BAD_ARGS", "--workspace-ref <ref> is required outside a deployment (inside one it defaults to oats-local.yaml workspace:)");
|
|
3368
|
+
try { remoteModule.parseRepoRef(workspaceRef); } catch (e) { bail(e.code || "E_REPO_REF", e.message, e.details); }
|
|
3369
|
+
const dir = val("dir") ?? (local ? `~/Agents/${basename(dirname(local.path))}` : undefined);
|
|
3370
|
+
if (!dir) bail("E_BAD_ARGS", "--dir <path on the host> is required outside a deployment (inside one it defaults to ~/Agents/<the deployment's directory name>)");
|
|
3371
|
+
let res;
|
|
3372
|
+
try {
|
|
3373
|
+
res = connectServer({ id, sshHost, workspaceRef, dir, oatsPath: val("oats"), path: val("path"), label: val("label"), installOats: args.includes("--install-oats"), replace: args.includes("--replace"), localVersion: OATS_VERSION });
|
|
3374
|
+
} catch (e) {
|
|
3375
|
+
if (!JSON_MODE && e.details?.steps) printConnectSteps(id, sshHost, e.details.steps);
|
|
3376
|
+
bail(e.code || "E_CONNECT", e.message, e.details);
|
|
3377
|
+
}
|
|
3378
|
+
const { registeredAs, ...result } = res;
|
|
3379
|
+
if (JSON_MODE) { jsonOk(result); return; }
|
|
3380
|
+
printConnectSteps(id, sshHost, res.steps);
|
|
3381
|
+
if (res.ready) console.log(`\n${registeredAs} is ready: oats spawn <soul> --server ${registeredAs}`);
|
|
3382
|
+
else { console.log(`\nnot ready yet; for a human, then re-run this command:`); for (const h of res.human) console.log(` - ${h.split("\n").join("\n ")}`); }
|
|
3383
|
+
}
|
|
3384
|
+
function printConnectSteps(id, sshHost, steps) {
|
|
3385
|
+
console.log(`oats server connect ${id} → ${sshHost}`);
|
|
3386
|
+
for (const s of steps) console.log(` ${s.step.padEnd(11)} ${s.status}${s.detail ? ` ${s.detail.split("\n")[0]}` : ""}`);
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
/** `oats <namespace> <command> … --server <id>`: the capability command, as typed minus the routing
|
|
3390
|
+
* flag, on the server's registered deployment (lib/servers.mjs routeCapability). The host's output and
|
|
3391
|
+
* exit status are relayed; under --json its envelope verbatim, or one envelope here when nothing came
|
|
3392
|
+
* back. An argv is printed only with its --invite values redacted. */
|
|
3393
|
+
function capabilityRouteCmd() {
|
|
3394
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3395
|
+
const { id, argv } = serverFlagOf(rawArgs);
|
|
3396
|
+
if (id === true) bail("E_BAD_ARGS", "--server needs a registered server id (oats server list)");
|
|
3397
|
+
let server, r;
|
|
3398
|
+
try { server = getServer(id); r = routeCapability(id, argv, { server, json: JSON_MODE }); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3399
|
+
const shown = `\`oats ${redactArgv(argv).join(" ")}\``;
|
|
3400
|
+
if (JSON_MODE) {
|
|
3401
|
+
if (!r.stdout?.length) bail(r.status === 255 ? "E_SSH" : "E_REMOTE_ENVELOPE", r.status === 255 ? `ssh to ${server.sshHost} failed running ${shown} on server ${id}` : `${shown} on server ${id} exited ${r.status} with no envelope`, { server: id, status: r.status });
|
|
3402
|
+
process.stdout.write(r.stdout);
|
|
3403
|
+
} else if (r.status === 255) console.error(`oats: ${shown} on server ${id} ended with exit 255 (ssh to ${server.sshHost} failed, or the command itself exited 255)`);
|
|
3404
|
+
process.exitCode = r.status;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3274
3407
|
/** `oats <spawn|retire|status> --server <id> ...`: run the command on the
|
|
3275
3408
|
* registered server's installed oats, same arguments, same envelope. The
|
|
3276
3409
|
* local side only routes and keeps the route snapshot per remote instance. */
|
|
@@ -3618,6 +3751,8 @@ else if (cmd && Object.hasOwn(REMOVED_VERBS, cmd)) {
|
|
|
3618
3751
|
console.log(usageText());
|
|
3619
3752
|
process.exit(1);
|
|
3620
3753
|
}
|
|
3754
|
+
// A capability command with --server runs on the server (feature capability-route); kernel commands keep their own table above.
|
|
3755
|
+
else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && serverFlagOf(rawArgs)) capabilityRouteCmd();
|
|
3621
3756
|
else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && await capabilityCommand()) { /* dispatched */ }
|
|
3622
3757
|
// No matching kernel command or capability namespace: in --json mode the help
|
|
3623
3758
|
// text must NOT contaminate stdout — still one envelope object, nonzero exit.
|
|
@@ -3658,10 +3793,20 @@ Usage:
|
|
|
3658
3793
|
oats server add <id> --ssh <alias> register another machine's OATS (OpenSSH alias,
|
|
3659
3794
|
--workspace </abs/path> [--oats <p>] remote workspace, remote oats path; no keys stored;
|
|
3660
3795
|
[--path <dir:dir>] --path = dirs prepended to the remote PATH, e.g. ~/.local/bin)
|
|
3661
|
-
oats server list|remove <id>|check <id> registry; check = reachability
|
|
3796
|
+
oats server list|remove <id>|check <id> registry; check = reachability, version, workspace key
|
|
3797
|
+
[--workspace-ref <ref>] (filled in when missing) and Git readability;
|
|
3798
|
+
list --workspace-ref = one workspace's servers
|
|
3799
|
+
oats server connect <id> --ssh <alias> put this deployment's workspace on that machine and
|
|
3800
|
+
[--workspace-ref <ref>] [--dir <path>] register it: ssh, oats, git, deployment, register,
|
|
3801
|
+
[--oats <p>] [--path <dir:dir>] readiness, each re-checked every run; --install-oats
|
|
3802
|
+
[--label <t>] [--install-oats] installs this version with npm there; human steps
|
|
3803
|
+
[--replace] [--json] come back with the exact remedy (docs/servers.md)
|
|
3662
3804
|
oats spawn|retire|status ... --server <id> run that command on the server's installed oats
|
|
3663
3805
|
(same flags, same envelope; the saved route per
|
|
3664
3806
|
remote instance lives under ~/.oats/remote/)
|
|
3807
|
+
oats <namespace> <command> ... --server <id>
|
|
3808
|
+
a capability command on the server's deployment
|
|
3809
|
+
(argv minus --server, stdin forwarded)
|
|
3665
3810
|
oats server roster [--server <id>] remote roster grouped by server and saved route
|
|
3666
3811
|
[--budget <ms>] [--per-target <ms>] target: one status pull per group within a total
|
|
3667
3812
|
[--json] budget (45 s, 20 s per target); every instance the
|
|
@@ -3694,6 +3839,8 @@ Usage:
|
|
|
3694
3839
|
and prints the
|
|
3695
3840
|
next steps (clone members you work IN, spawn
|
|
3696
3841
|
oats-operator-expert); creates no soul, spawns nothing
|
|
3842
|
+
[--check] --check: write nothing; report <dir> (~ = this home),
|
|
3843
|
+
its state and whether Git reads the workspace remote
|
|
3697
3844
|
oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
|
|
3698
3845
|
oats schedule list|show <id>|test <id>|add <id> --file <spec.json>|update <id> --file <spec.json>
|
|
3699
3846
|
enable|disable|run|remove|reconcile <id> workspace-scoped, host-owned schedules (spawn,
|
package/docs/capabilities.md
CHANGED
|
@@ -68,7 +68,19 @@ A self-contained package has an `oats.json`:
|
|
|
68
68
|
as `aweb.identity`, because that prefix owns the corresponding `AWEB_*`
|
|
69
69
|
namespace.
|
|
70
70
|
- `command` is an optional, unique CLI namespace. The example exposes
|
|
71
|
-
`oats team-chat auth`.
|
|
71
|
+
`oats team-chat auth`. Inside an instance home it runs the home's copy of
|
|
72
|
+
the capability. From a deployment directory (an **operator command**) it
|
|
73
|
+
resolves as a spawn of a soul would: the soul named by `--soul <name>`, or,
|
|
74
|
+
without `--soul`, the first soul of the deployment by name, not disabled
|
|
75
|
+
there, whose resolution provides the namespace (souls whose resolution is
|
|
76
|
+
refused are skipped). The chosen soul is named on stderr (`oats <namespace>:
|
|
77
|
+
no --soul given; running as soul <member>/<soul>, …`), so the command's
|
|
78
|
+
stdout, its `--json` envelope included, is the provider's alone. When no
|
|
79
|
+
soul provides the namespace, the command is refused with `E_BAD_ARGS` (`no
|
|
80
|
+
soul of this deployment provides the <namespace> namespace; pass --soul
|
|
81
|
+
<name>`, with any skipped souls and their codes in `details.skipped`).
|
|
82
|
+
`--soul` without a name is `E_BAD_ARGS`. With `--server <id>` the same
|
|
83
|
+
command runs on that server's deployment ([servers.md](servers.md#run-there)).
|
|
72
84
|
- `compatibility.oats` is the kernel range the capability runs on. The kernel
|
|
73
85
|
refuses to compose a capability whose range does not admit it
|
|
74
86
|
(`E_CAPABILITY_INCOMPATIBLE`, naming capability, range and kernel) wherever a
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -38,7 +38,8 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
38
38
|
"instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
|
|
39
39
|
"workspace-v2","instance-modules","spawn-provider-payload","served-identity","packages-no-approval","spawn-name","settings-origins",
|
|
40
40
|
"team-model-3","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
|
|
41
|
-
"preview-composed-from","observe-max-age","spawn-preview-max-age","capability-show","capture-file","workspace-identity"
|
|
41
|
+
"preview-composed-from","observe-max-age","spawn-preview-max-age","capability-show","capture-file","workspace-identity",
|
|
42
|
+
"server-connect","capability-route","servers-per-workspace","operator-default-soul"],
|
|
42
43
|
"automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
|
|
43
44
|
"readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2,
|
|
44
45
|
"capabilityShowApi":1}
|
|
@@ -101,6 +102,10 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
101
102
|
| `capability-show` | `oats capabilities show <name>` and its `--file` form, OATS 0.34.0 ([`oats capabilities show`](#oats-capabilities-show)) | `capabilityShowApi: 1` |
|
|
102
103
|
| `capture-file` | `oats capture --file <path> --format cc\|pi\|codex --home <instance home> [--json]`: one session file captured as `--home` capture would, with a receipt bound to its bytes, OATS 0.35.0 (the capture USAGE and packages/record/README.md) | |
|
|
103
104
|
| `workspace-identity` | the deployment's workspace identity on `oats status --json` `workspace` (`key`, `ref`, `keyFrom`, `standalone`, `defaultTeam`, `teams`, `teamsFrom`) and each `oats server roster --json` group's relayed `workspace`, OATS 0.36.0 ([Workspace identity](#workspace-identity-feature-workspace-identity-oats-0360)) | |
|
|
105
|
+
| `servers-per-workspace` | `workspaceKey` on registrations and `oats server list --json` rows; `oats server list --workspace-ref <ref>` and its `unknownWorkspace`; `workspaceKey` on `oats server check --json`, OATS 0.39.0 ([Servers per workspace](#servers-per-workspace)) | |
|
|
106
|
+
| `server-connect` | `oats server connect`; `oats onboard --check`; `workspaceReadable` on `oats server check --json`, OATS 0.39.0 ([`oats server connect`](#oats-server-connect)) | |
|
|
107
|
+
| `capability-route` | `oats <namespace> <command> … --server <id>` runs the capability command on the server, OATS 0.39.0 ([Capability commands on a server](#capability-commands-on-a-server)) | |
|
|
108
|
+
| `operator-default-soul` | a capability command from a deployment without `--soul` runs as the first soul that provides its namespace (named on stderr); none is `E_BAD_ARGS`, OATS 0.39.0 ([capabilities.md](capabilities.md)) | |
|
|
104
109
|
|
|
105
110
|
Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
|
|
106
111
|
`workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
|
|
@@ -463,6 +468,17 @@ confirmed member (`evidence: {repoKey, workspace, commit}`); `fail` when not
|
|
|
463
468
|
(the remedy names `oats-membership.yaml`); `unknown` when the workspace could
|
|
464
469
|
not be read. External souls and standalone views are `not-applicable`,
|
|
465
470
|
`required: false` (a standalone item carries `evidence.standaloneReason`).
|
|
471
|
+
A package soul is trusted through the workspace's `packages:` pin, never
|
|
472
|
+
through membership ([the non-collapse rule](workspaces.md#member-tier-vs-package-tier-the-non-collapse-rule)):
|
|
473
|
+
its item is `package <package>/<soul>` (producer `workspace lock`,
|
|
474
|
+
`evidence: {repoKey, workspace, commit, version, integrity, from}` with
|
|
475
|
+
`from.kind: "package"`). It passes when the package is declared and locked
|
|
476
|
+
and ships the soul at the locked commit, and the soul's copy is intact: for
|
|
477
|
+
`--soul`, the copy a spawn would link matches the locked digest; for
|
|
478
|
+
`--home`, the home's copy matches the digest it recorded, and a lock still
|
|
479
|
+
at that commit records the same digest. The copy is digested on every read.
|
|
480
|
+
It fails otherwise, with `code: "E_PACKAGE_INTEGRITY"` on a digest mismatch
|
|
481
|
+
and the remedy naming `oats sync` or a fresh spawn.
|
|
466
482
|
|
|
467
483
|
**`providers`** (producer `provider binding check`): for each module whose
|
|
468
484
|
manifest declares `binding`, the kernel runs its `binding.check` and relays
|
|
@@ -1904,6 +1920,113 @@ route target:
|
|
|
1904
1920
|
target, else `null`; `tmux`, `sessionTarget` (the recorded target of a home
|
|
1905
1921
|
a Herdr-era kernel opened) and `runtimeError` are as the host reports them.
|
|
1906
1922
|
|
|
1923
|
+
<a id="servers-per-workspace"></a>
|
|
1924
|
+
### Servers per workspace (feature `servers-per-workspace`)
|
|
1925
|
+
|
|
1926
|
+
```text
|
|
1927
|
+
oats server list [--workspace-ref <ref>] --json
|
|
1928
|
+
oats server check <id> --json
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
`server list` is an envelope whose `result` is `{file, servers}`; each row is
|
|
1932
|
+
the registration (`id`, `sshHost`, `workspace`, and `oatsPath`, `path`,
|
|
1933
|
+
`label` when set) plus `workspaceKey` (`null` when not known yet), `target`
|
|
1934
|
+
and `snapshots`. With `--workspace-ref <ref>`, `servers` holds only the rows
|
|
1935
|
+
whose `workspaceKey` is the canonical key of `<ref>` (`parseRepoRef`; any
|
|
1936
|
+
spelling of the repository), and `result.unknownWorkspace` lists the ids
|
|
1937
|
+
whose key is not known; an unparsable ref is `E_REPO_REF`.
|
|
1938
|
+
|
|
1939
|
+
```json
|
|
1940
|
+
{"file":"/Users/me/.oats/servers.json",
|
|
1941
|
+
"servers":[{"id":"altair-aweb","sshHost":"altair","workspace":"/Users/me/Agents/aweb","path":"/opt/homebrew/bin","label":"altair",
|
|
1942
|
+
"workspaceKey":"github.com/awebai/ac",
|
|
1943
|
+
"target":{"sshHost":"altair","workspace":"/Users/me/Agents/aweb","oatsPath":"oats","path":"/opt/homebrew/bin"},"snapshots":0}],
|
|
1944
|
+
"unknownWorkspace":["build"]}
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
`server check` adds `workspaceKey` (the host's answer, else the recorded one,
|
|
1948
|
+
else `null`), `workspaceReadable` (`true`, `false`, or `null` when the host
|
|
1949
|
+
does not advertise `server-connect` or cannot say) and, when it is `false`,
|
|
1950
|
+
`workspaceReadError: {code, message, reason, hint?}` beside `id`, `target`,
|
|
1951
|
+
`remote`, `workspaceReachable`, `agents` and `error`. A recorded key the host
|
|
1952
|
+
contradicts fails the check with `E_SERVER_WORKSPACE_MISMATCH`, `details:
|
|
1953
|
+
{recorded, reported}`, and the registration is not rewritten. `server add
|
|
1954
|
+
--json` answers the registration with `workspaceKey` (`null`, and a
|
|
1955
|
+
`warnings` entry, when the host answered none).
|
|
1956
|
+
|
|
1957
|
+
<a id="oats-server-connect"></a>
|
|
1958
|
+
### `oats server connect` (feature `server-connect`)
|
|
1959
|
+
|
|
1960
|
+
```text
|
|
1961
|
+
oats server connect <id> --ssh <host> [--workspace-ref <ref>] [--dir </abs/path or ~/path on the host>]
|
|
1962
|
+
[--oats <path>] [--path <dirs>] [--label <text>] [--install-oats] [--replace] --json
|
|
1963
|
+
```
|
|
1964
|
+
|
|
1965
|
+
An envelope, `ok: true` whenever no step failed, human steps included
|
|
1966
|
+
([servers.md](servers.md#connect-a-machine) has what each step does):
|
|
1967
|
+
|
|
1968
|
+
```json
|
|
1969
|
+
{"id":"altair-aweb","ready":false,
|
|
1970
|
+
"registration":null,
|
|
1971
|
+
"steps":[{"step":"ssh","status":"ok"},
|
|
1972
|
+
{"step":"oats","status":"done","detail":"installed @awebai/oats 0.39.0 (was missing)"},
|
|
1973
|
+
{"step":"git","status":"needs-human","code":"E_REMOTE_UNREADABLE",
|
|
1974
|
+
"detail":"cannot read remote https://github.com/awebai/ac (auth): on macOS a session without a terminal …",
|
|
1975
|
+
"remedy":"on altair: on macOS a session without a terminal … (`gh auth login --insecure-storage`, then `gh auth setup-git`), or use an SSH key the session can reach: …",
|
|
1976
|
+
"hint":"keychain-non-interactive"},
|
|
1977
|
+
{"step":"deployment","status":"skipped","detail":"waits for git"},
|
|
1978
|
+
{"step":"register","status":"skipped","detail":"waits for git"},
|
|
1979
|
+
{"step":"readiness","status":"skipped","detail":"waits for git"}],
|
|
1980
|
+
"human":["on altair: on macOS a session without a terminal … or the desktop login's ssh-agent (point SSH_AUTH_SOCK at it in the shell's startup file)"]}
|
|
1981
|
+
```
|
|
1982
|
+
|
|
1983
|
+
- `steps` is always the six steps `ssh`, `oats`, `git`, `deployment`,
|
|
1984
|
+
`register`, `readiness`, in that order. A step is `{step, status}` plus
|
|
1985
|
+
`detail` when there is something to say; `needs-human` adds `remedy` (and
|
|
1986
|
+
on `git` the error's `code` and any `hint`), in which every command to run
|
|
1987
|
+
is a Markdown code span with each argument quoted for a POSIX shell (a
|
|
1988
|
+
value from a reference or a host path is always one literal argument), and
|
|
1989
|
+
a value in the surrounding text has its backslashes and backticks escaped
|
|
1990
|
+
(`` \\ ``, `` \` ``), so the code spans are exactly the commands (a readiness line relays the provider's own wording); `failed` adds `code` and, for
|
|
1991
|
+
some codes, `details`. `skipped` steps say `waits for <step>`.
|
|
1992
|
+
- `registration` is the registration as written or found (`sshHost`,
|
|
1993
|
+
`workspace`: the absolute path the host resolved for `--dir`, never `~`;
|
|
1994
|
+
`workspaceKey`; and `oatsPath`, `path`,
|
|
1995
|
+
`label` when set), `null` until the `register` step has run.
|
|
1996
|
+
- `human` lists the remedies of the `needs-human` steps, in order. A
|
|
1997
|
+
`readiness` step that needs a human has one line per problem
|
|
1998
|
+
(`<souls>: <subject>: <reason>[ → <remedy>]`, plus `readiness not checked
|
|
1999
|
+
for N souls: …` when the 60 s budget ran out, or `readiness not checked: the
|
|
2000
|
+
host did not list its souls within 60 s; …` when the listing itself did not
|
|
2001
|
+
finish); its `remedy` is those lines joined by `\n`.
|
|
2002
|
+
- `ready` is `true` when no step is `needs-human` or `failed`.
|
|
2003
|
+
- A `failed` step ends the run: `ok: false`, `error.code` is the step's code
|
|
2004
|
+
(`E_SSH`, `E_REMOTE_INSTALL`, `E_SERVER_WORKSPACE_MISMATCH`,
|
|
2005
|
+
`E_DIR_NOT_EMPTY`, `E_SERVER_EXISTS`, `E_SERVERS_BUSY`, or the host's own
|
|
2006
|
+
code relayed),
|
|
2007
|
+
`error.details` is the step's `details` plus `steps`, the steps so far
|
|
2008
|
+
(the failed one last). A mismatch at `deployment` has `details: {expected,
|
|
2009
|
+
reported, dir}`; at `register`, `{recorded, reported}`.
|
|
2010
|
+
|
|
2011
|
+
The host-side read it uses, `oats onboard <dir> [--workspace <ref>] --check
|
|
2012
|
+
--json`, answers `{check: true, dir, state, workspace: {ref, key, url}, remote}`:
|
|
2013
|
+
`state` is `absent`, `empty`, `not-empty`, `not-a-directory` or `deployment`;
|
|
2014
|
+
`remote` is `{readable: true, commit}` or `{readable: false, error: {code,
|
|
2015
|
+
message, reason, hint?, remedy?}}`. It writes nothing.
|
|
2016
|
+
|
|
2017
|
+
<a id="capability-commands-on-a-server"></a>
|
|
2018
|
+
### Capability commands on a server (feature `capability-route`)
|
|
2019
|
+
|
|
2020
|
+
`oats <namespace> <command> … --server <id>` runs `oats <namespace>
|
|
2021
|
+
<command> …`, the argv as typed minus `--server <id>`, from the registered
|
|
2022
|
+
workspace directory on the server. With `--json` the host's stdout (its
|
|
2023
|
+
envelope) is relayed verbatim and the exit status is the host's. When the
|
|
2024
|
+
host gave no output this side answers one envelope: `E_SSH` (`ssh to <host>
|
|
2025
|
+
failed running \`oats …\` on server <id>`) or `E_REMOTE_ENVELOPE`, with
|
|
2026
|
+
`details: {server, status}`; any `--invite` value in the printed argv is
|
|
2027
|
+
`<redacted>`. `--server` with no value is `E_BAD_ARGS`, an unknown id
|
|
2028
|
+
`E_SERVER_UNKNOWN`. Stdin is forwarded untouched unless it is a terminal.
|
|
2029
|
+
|
|
1907
2030
|
<a id="routed-reads-and-plans"></a>
|
|
1908
2031
|
### Routed reads and plans (`--server`, 0.31)
|
|
1909
2032
|
|
package/docs/desktop.md
CHANGED
|
@@ -215,6 +215,25 @@ registered remote workspace the timer and definitions live on that server, so
|
|
|
215
215
|
they do not depend on the Mac staying awake. See [Schedules](schedules.md) for
|
|
216
216
|
the CLI, cron semantics, observed outcomes and recovery commands.
|
|
217
217
|
|
|
218
|
+
## A workspace's machines
|
|
219
|
+
|
|
220
|
+
A window offers only the machines that run its workspace: those whose
|
|
221
|
+
registration reports the same workspace key as the window's deployment on this
|
|
222
|
+
computer (OATS CLI with `servers-per-workspace` and `server-connect`). The
|
|
223
|
+
spawn dialog's **Where to run** lists "This computer" and those machines; the
|
|
224
|
+
Workspace › Setup tab lists them with their OATS version and whether they can
|
|
225
|
+
be reached, with **Check** and **Remove**. Registrations whose key is not known
|
|
226
|
+
yet are checked once in the background after the Desktop starts.
|
|
227
|
+
|
|
228
|
+
**Add a machine to this workspace…** (the last entry of Where to run, and a
|
|
229
|
+
button in Setup) takes an ssh host alias, a name, the folder on that machine
|
|
230
|
+
and whether to install OATS there, and runs `oats server connect` from this
|
|
231
|
+
deployment; when the workspace uses oats.aweb for messaging, it then runs `oats
|
|
232
|
+
aweb connect` to join that machine to the workspace's team. Each step shows as
|
|
233
|
+
a row with what OATS reported, and a step that needs you says what to run
|
|
234
|
+
where, with a copy button. **Check again** re-runs both. The Desktop never asks
|
|
235
|
+
for a password or key, and never runs anything over ssh itself.
|
|
236
|
+
|
|
218
237
|
## Instances on servers
|
|
219
238
|
|
|
220
239
|
Every instance a registered server reports shows in its workspace's roster, whoever spawned it. You
|
package/docs/integrations.md
CHANGED
|
@@ -41,7 +41,7 @@ arrives from.
|
|
|
41
41
|
# oats-workspace.yaml: one default per slot, for every soul
|
|
42
42
|
packages:
|
|
43
43
|
oats.okf: v4.1.1
|
|
44
|
-
oats.aweb: v1.
|
|
44
|
+
oats.aweb: v1.21.1
|
|
45
45
|
oats.linear: v1.0.1
|
|
46
46
|
oats.jira: v1.0.1
|
|
47
47
|
defaults:
|
|
@@ -89,10 +89,20 @@ operations in full.
|
|
|
89
89
|
`knowledge-maintainer` package soul reviews the resulting PRs.
|
|
90
90
|
- **`oats.aweb`** mints a messaging identity for each instance at spawn and
|
|
91
91
|
removes it at retire, contributes the aweb messaging skills, and wires the
|
|
92
|
-
channel so sessions are woken by mail.
|
|
92
|
+
channel so sessions are woken by mail. Under `delivery: channel`, Claude Code
|
|
93
|
+
loads the channel plugin with `--dangerously-load-development-channels`,
|
|
94
|
+
since `aweb-channel` is not on its approved channel list, so every such
|
|
95
|
+
Claude start waits at Claude Code's
|
|
96
|
+
development-channels confirmation until someone answers it in the
|
|
97
|
+
instance's terminal; the start's warnings and readiness say so
|
|
98
|
+
(`channel-dev-confirmation`). `oats aweb roster` lists the team:
|
|
93
99
|
its membership certificates and workspaces, one entry per alias with its
|
|
94
100
|
sources, status and kind (global identities first), and says when either
|
|
95
|
-
source is incomplete.
|
|
101
|
+
source is incomplete. After `oats server connect`, `oats aweb connect
|
|
102
|
+
<server-id>` gives the server's deployment membership in its default team
|
|
103
|
+
through the `--server` capability route, passing the invite token on stdin
|
|
104
|
+
(on the host, `aw id team accept-invite` takes it as an argument for the
|
|
105
|
+
call's duration).
|
|
96
106
|
- **`oats.jira`** teaches the `jira-tasks` protocol and adds an advisory spawn
|
|
97
107
|
hook that names the configured site and project.
|
|
98
108
|
- **`oats.linear`** provides JSON-first `oats linear` commands, the
|
package/docs/knowledge.md
CHANGED
|
@@ -221,8 +221,9 @@ identical copies in both role capabilities.
|
|
|
221
221
|
with the settings recorded at spawn.
|
|
222
222
|
- **From the deployment directory** (holding `oats-local.yaml`), in a shell
|
|
223
223
|
where neither `OATS_INSTANCE_HOME` nor `OATS_HOME` is set (either one pins
|
|
224
|
-
the command to that instance home),
|
|
225
|
-
`--soul <name>`
|
|
224
|
+
the command to that instance home), a capability command runs as the soul
|
|
225
|
+
`--soul <name>` names, or without it as the first soul that provides the
|
|
226
|
+
namespace ([capabilities.md](capabilities.md), `command`). Inside an instance home, a
|
|
226
227
|
`--soul` naming another soul is refused (`E_HOME_MISMATCH`). The kernel resolves the
|
|
227
228
|
soul as a spawn would, fetches its module at the locked commit into
|
|
228
229
|
`<deployment>/.oats/modules/` and runs it with the soul's merged settings
|
package/docs/official-catalog.md
CHANGED
|
@@ -11,7 +11,7 @@ or workspace membership alone does not make a package official.
|
|
|
11
11
|
|---|---|---|---|
|
|
12
12
|
| `oats.framework` | `oats-framework/v1.5.0` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
|
|
13
13
|
| `oats.okf` | `v4.1.1` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
|
|
14
|
-
| `oats.aweb` | `v1.
|
|
14
|
+
| `oats.aweb` | `v1.21.1` | `oats.aweb` (messaging) | |
|
|
15
15
|
| `oats.engineering` | `v1.5.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
|
|
16
16
|
| `oats.authoring` | `v1.0.3` | `oats.authoring` | |
|
|
17
17
|
| `oats.jira` | `v1.0.1` | `oats.jira` (tasks) | |
|
package/docs/packages.md
CHANGED
|
@@ -76,7 +76,7 @@ members:
|
|
|
76
76
|
packages:
|
|
77
77
|
oats.framework: v1.5.0
|
|
78
78
|
oats.okf: v4.1.1
|
|
79
|
-
oats.aweb: v1.
|
|
79
|
+
oats.aweb: v1.21.1
|
|
80
80
|
teams:
|
|
81
81
|
platform: { team: "platform:acme.aweb.ai", description: Platform engineering }
|
|
82
82
|
defaults:
|
|
@@ -134,7 +134,7 @@ Declaring a package in the workspace's `packages:` is the trust decision
|
|
|
134
134
|
## `oats package add | remove`
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
-
oats package add oats.aweb v1.
|
|
137
|
+
oats package add oats.aweb v1.21.1 # a catalog version
|
|
138
138
|
oats package add acme.tools git:github.com/acme/tools@v0.4.0
|
|
139
139
|
oats package remove acme.tools
|
|
140
140
|
```
|