@awebai/oats 0.38.1 → 0.39.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 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, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
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
- const rows = Object.entries(servers).map(([id, s]) => ({ id, ...s, target: targetOf({ id, ...s }), snapshots: listSnapshots(id).length }));
3230
- if (JSON_MODE) { jsonOk({ file: SERVERS_FILE(), servers: rows }); return; }
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
- 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)`);
3245
- servers[id] = entry;
3246
- writeServers(servers);
3247
- if (JSON_MODE) { jsonOk({ id, ...entry, file: SERVERS_FILE() }); return; }
3248
- console.log(`Registered server ${id} → ssh ${entry.sshHost}, workspace ${entry.workspace} (${shortPath(SERVERS_FILE())}). Verify it with \`oats server check ${id}\`.`);
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
- if (!servers[id]) bail("E_SERVER_UNKNOWN", `no server registered as ${id}`);
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
- if (JSON_MODE) { jsonOk({ id, target, remote, workspaceReachable: !!status.envelope.ok, agents, error: status.envelope.ok ? undefined : status.envelope.error }); return; }
3268
- console.log(`${id}: ssh ${target.sshHost} ok, remote oats ${remote.version} (envelope v${remote.schemaVersion})`);
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 + version, no mutation
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,
@@ -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
@@ -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
@@ -1904,6 +1909,113 @@ route target:
1904
1909
  target, else `null`; `tmux`, `sessionTarget` (the recorded target of a home
1905
1910
  a Herdr-era kernel opened) and `runtimeError` are as the host reports them.
1906
1911
 
1912
+ <a id="servers-per-workspace"></a>
1913
+ ### Servers per workspace (feature `servers-per-workspace`)
1914
+
1915
+ ```text
1916
+ oats server list [--workspace-ref <ref>] --json
1917
+ oats server check <id> --json
1918
+ ```
1919
+
1920
+ `server list` is an envelope whose `result` is `{file, servers}`; each row is
1921
+ the registration (`id`, `sshHost`, `workspace`, and `oatsPath`, `path`,
1922
+ `label` when set) plus `workspaceKey` (`null` when not known yet), `target`
1923
+ and `snapshots`. With `--workspace-ref <ref>`, `servers` holds only the rows
1924
+ whose `workspaceKey` is the canonical key of `<ref>` (`parseRepoRef`; any
1925
+ spelling of the repository), and `result.unknownWorkspace` lists the ids
1926
+ whose key is not known; an unparsable ref is `E_REPO_REF`.
1927
+
1928
+ ```json
1929
+ {"file":"/Users/me/.oats/servers.json",
1930
+ "servers":[{"id":"altair-aweb","sshHost":"altair","workspace":"/Users/me/Agents/aweb","path":"/opt/homebrew/bin","label":"altair",
1931
+ "workspaceKey":"github.com/awebai/ac",
1932
+ "target":{"sshHost":"altair","workspace":"/Users/me/Agents/aweb","oatsPath":"oats","path":"/opt/homebrew/bin"},"snapshots":0}],
1933
+ "unknownWorkspace":["build"]}
1934
+ ```
1935
+
1936
+ `server check` adds `workspaceKey` (the host's answer, else the recorded one,
1937
+ else `null`), `workspaceReadable` (`true`, `false`, or `null` when the host
1938
+ does not advertise `server-connect` or cannot say) and, when it is `false`,
1939
+ `workspaceReadError: {code, message, reason, hint?}` beside `id`, `target`,
1940
+ `remote`, `workspaceReachable`, `agents` and `error`. A recorded key the host
1941
+ contradicts fails the check with `E_SERVER_WORKSPACE_MISMATCH`, `details:
1942
+ {recorded, reported}`, and the registration is not rewritten. `server add
1943
+ --json` answers the registration with `workspaceKey` (`null`, and a
1944
+ `warnings` entry, when the host answered none).
1945
+
1946
+ <a id="oats-server-connect"></a>
1947
+ ### `oats server connect` (feature `server-connect`)
1948
+
1949
+ ```text
1950
+ oats server connect <id> --ssh <host> [--workspace-ref <ref>] [--dir </abs/path or ~/path on the host>]
1951
+ [--oats <path>] [--path <dirs>] [--label <text>] [--install-oats] [--replace] --json
1952
+ ```
1953
+
1954
+ An envelope, `ok: true` whenever no step failed, human steps included
1955
+ ([servers.md](servers.md#connect-a-machine) has what each step does):
1956
+
1957
+ ```json
1958
+ {"id":"altair-aweb","ready":false,
1959
+ "registration":null,
1960
+ "steps":[{"step":"ssh","status":"ok"},
1961
+ {"step":"oats","status":"done","detail":"installed @awebai/oats 0.39.0 (was missing)"},
1962
+ {"step":"git","status":"needs-human","code":"E_REMOTE_UNREADABLE",
1963
+ "detail":"cannot read remote https://github.com/awebai/ac (auth): on macOS a session without a terminal …",
1964
+ "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: …",
1965
+ "hint":"keychain-non-interactive"},
1966
+ {"step":"deployment","status":"skipped","detail":"waits for git"},
1967
+ {"step":"register","status":"skipped","detail":"waits for git"},
1968
+ {"step":"readiness","status":"skipped","detail":"waits for git"}],
1969
+ "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)"]}
1970
+ ```
1971
+
1972
+ - `steps` is always the six steps `ssh`, `oats`, `git`, `deployment`,
1973
+ `register`, `readiness`, in that order. A step is `{step, status}` plus
1974
+ `detail` when there is something to say; `needs-human` adds `remedy` (and
1975
+ on `git` the error's `code` and any `hint`), in which every command to run
1976
+ is a Markdown code span with each argument quoted for a POSIX shell (a
1977
+ value from a reference or a host path is always one literal argument), and
1978
+ a value in the surrounding text has its backslashes and backticks escaped
1979
+ (`` \\ ``, `` \` ``), so the code spans are exactly the commands (a readiness line relays the provider's own wording); `failed` adds `code` and, for
1980
+ some codes, `details`. `skipped` steps say `waits for <step>`.
1981
+ - `registration` is the registration as written or found (`sshHost`,
1982
+ `workspace`: the absolute path the host resolved for `--dir`, never `~`;
1983
+ `workspaceKey`; and `oatsPath`, `path`,
1984
+ `label` when set), `null` until the `register` step has run.
1985
+ - `human` lists the remedies of the `needs-human` steps, in order. A
1986
+ `readiness` step that needs a human has one line per problem
1987
+ (`<souls>: <subject>: <reason>[ → <remedy>]`, plus `readiness not checked
1988
+ for N souls: …` when the 60 s budget ran out, or `readiness not checked: the
1989
+ host did not list its souls within 60 s; …` when the listing itself did not
1990
+ finish); its `remedy` is those lines joined by `\n`.
1991
+ - `ready` is `true` when no step is `needs-human` or `failed`.
1992
+ - A `failed` step ends the run: `ok: false`, `error.code` is the step's code
1993
+ (`E_SSH`, `E_REMOTE_INSTALL`, `E_SERVER_WORKSPACE_MISMATCH`,
1994
+ `E_DIR_NOT_EMPTY`, `E_SERVER_EXISTS`, `E_SERVERS_BUSY`, or the host's own
1995
+ code relayed),
1996
+ `error.details` is the step's `details` plus `steps`, the steps so far
1997
+ (the failed one last). A mismatch at `deployment` has `details: {expected,
1998
+ reported, dir}`; at `register`, `{recorded, reported}`.
1999
+
2000
+ The host-side read it uses, `oats onboard <dir> [--workspace <ref>] --check
2001
+ --json`, answers `{check: true, dir, state, workspace: {ref, key, url}, remote}`:
2002
+ `state` is `absent`, `empty`, `not-empty`, `not-a-directory` or `deployment`;
2003
+ `remote` is `{readable: true, commit}` or `{readable: false, error: {code,
2004
+ message, reason, hint?, remedy?}}`. It writes nothing.
2005
+
2006
+ <a id="capability-commands-on-a-server"></a>
2007
+ ### Capability commands on a server (feature `capability-route`)
2008
+
2009
+ `oats <namespace> <command> … --server <id>` runs `oats <namespace>
2010
+ <command> …`, the argv as typed minus `--server <id>`, from the registered
2011
+ workspace directory on the server. With `--json` the host's stdout (its
2012
+ envelope) is relayed verbatim and the exit status is the host's. When the
2013
+ host gave no output this side answers one envelope: `E_SSH` (`ssh to <host>
2014
+ failed running \`oats …\` on server <id>`) or `E_REMOTE_ENVELOPE`, with
2015
+ `details: {server, status}`; any `--invite` value in the printed argv is
2016
+ `<redacted>`. `--server` with no value is `E_BAD_ARGS`, an unknown id
2017
+ `E_SERVER_UNKNOWN`. Stdin is forwarded untouched unless it is a terminal.
2018
+
1907
2019
  <a id="routed-reads-and-plans"></a>
1908
2020
  ### Routed reads and plans (`--server`, 0.31)
1909
2021
 
@@ -17,14 +17,20 @@ Enter a model or leave the field blank to use the selected launch's default.
17
17
  An old harness's model is not carried to a different harness. Available local model
18
18
  suggestions are advisory; a model ID can also be typed. Start uses the saved
19
19
  briefing and state in a new harness conversation; it does not resume an old
20
- harness conversation ID. After the launch appears in the roster, Desktop
21
- opens the instance's terminal.
20
+ harness conversation ID. Once the kernel accepts the start or restart, the
21
+ dialog closes and Desktop opens the instance's terminal as soon as its row is
22
+ running with a terminal session, so whatever the harness shows first (a
23
+ confirmation such as Claude Code's development-channels prompt, an error, the
24
+ session) is visible there. If the terminal is not ready within the wait, a
25
+ notice says so and the instance's row opens it later. A refused launch keeps
26
+ the dialog open with the kernel's error.
22
27
 
23
28
  If an ordinary Start dialog finds the instance already running, its action becomes
24
29
  **Open terminal**. A failed or timed-out start requires **Refresh status** before
25
30
  another attempt, because the launch may have succeeded before the reply was
26
31
  lost. Changing workspaces dismisses the dialog and prevents a delayed launch
27
- reply from opening a terminal in the wrong workspace.
32
+ reply, or a terminal that becomes ready later, from opening in the wrong
33
+ workspace.
28
34
 
29
35
  **Restart with…** is explicit: after validating the new configuration, the
30
36
  kernel stops the current harness and starts the selected one. It does not
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
@@ -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.20.0
44
+ oats.aweb: v1.21.0
45
45
  oats.linear: v1.0.1
46
46
  oats.jira: v1.0.1
47
47
  defaults:
@@ -92,7 +92,11 @@ operations in full.
92
92
  channel so sessions are woken by mail. `oats aweb roster` lists the team:
93
93
  its membership certificates and workspaces, one entry per alias with its
94
94
  sources, status and kind (global identities first), and says when either
95
- source is incomplete.
95
+ source is incomplete. After `oats server connect`, `oats aweb connect
96
+ <server-id>` gives the server's deployment membership in its default team
97
+ through the `--server` capability route, passing the invite token on stdin
98
+ (on the host, `aw id team accept-invite` takes it as an argument for the
99
+ call's duration).
96
100
  - **`oats.jira`** teaches the `jira-tasks` protocol and adds an advisory spawn
97
101
  hook that names the configured site and project.
98
102
  - **`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), every capability command needs
225
- `--soul <name>` (`E_BAD_ARGS` without it). Inside an instance home, a
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
@@ -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.20.0` | `oats.aweb` (messaging) | |
14
+ | `oats.aweb` | `v1.21.0` | `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.20.0
79
+ oats.aweb: v1.21.0
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.20.0 # a catalog version
137
+ oats package add oats.aweb v1.21.0 # 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
  ```
@@ -0,0 +1,13 @@
1
+ # OATS 0.38.2
2
+
3
+ ## Fixed
4
+
5
+ - **The Desktop's Start and Restart dialog no longer stays open after the
6
+ launch** (awebai/oats#525). Since 0.36.0 the dialog's wait for the
7
+ terminal never recognised the started instance's row, so every Start or
8
+ Restart ended on "no running harness was observed" with the dialog left
9
+ open, while the harness (for Claude Code under channel delivery, its
10
+ development-channels confirmation) waited unseen. The dialog now closes
11
+ as soon as the launch is accepted, and the instance's terminal opens once
12
+ its row is ready, showing whatever the harness shows first. A refused
13
+ launch keeps the dialog with its error.