@awebai/oats 0.31.0 → 0.32.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 +141 -36
- package/docs/configuration.md +65 -8
- package/docs/desktop-cli-api.md +121 -16
- package/docs/desktop.md +16 -0
- package/docs/implementation.md +58 -0
- package/docs/oats-local.schema.json +2 -1
- package/docs/plans/0.30-close-out.md +7 -4
- package/docs/release-notes/v0.32.0.md +137 -0
- package/lib/automations.mjs +5 -1
- package/lib/core.mjs +75 -29
- package/lib/instance-inspect.mjs +2 -1
- package/lib/instance-resolution.mjs +3 -3
- package/lib/local-inputs.mjs +46 -0
- package/lib/materialize.mjs +3 -2
- package/lib/packages.mjs +26 -6
- package/lib/remote.mjs +792 -36
- package/lib/resolve.mjs +23 -7
- package/lib/servers.mjs +7 -0
- package/lib/workspace.mjs +163 -56
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readSync, realpathSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
22
|
-
import { homedir, tmpdir } from "node:os";
|
|
22
|
+
import { constants as osConstants, homedir, tmpdir } from "node:os";
|
|
23
23
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { runtimeNameWarning, noteRuntimeName } from "../lib/deprecation.mjs";
|
|
@@ -28,16 +28,17 @@ import {
|
|
|
28
28
|
LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
|
|
29
29
|
capabilityManifests, capabilityTrust, capabilityExecutablePath,
|
|
30
30
|
officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
|
|
31
|
-
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, recordedKernelBin, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
31
|
+
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, recordedKernelBin, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, validateLaunchConfigDefaults, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
32
32
|
} from "../lib/core.mjs";
|
|
33
33
|
import {
|
|
34
|
-
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages, memoizedRemote,
|
|
34
|
+
writeFileAtomic, LOCK_FILE, readLock, readLockIfPresent, writeLock, resolvePackages, memoizedRemote,
|
|
35
35
|
classifyPackageValue, parsePackageRequest } from "../lib/packages.mjs";
|
|
36
|
-
import { loadLocal, validateWorkspace, validateLocal, discoverPackageSouls, workspaceWarnings } from "../lib/workspace.mjs";
|
|
36
|
+
import { loadLocal, validateWorkspace, validateLocal, discoverPackageSouls, workspaceWarnings, memberRowByKey } from "../lib/workspace.mjs";
|
|
37
37
|
import { recordedTeams, reportRows, soulKeyOf, soulTeams, teamModel } from "../lib/teams.mjs";
|
|
38
38
|
import { launchLayers } from "../lib/launch-preference.mjs";
|
|
39
39
|
import { parseConfigData } from "../lib/config-data.mjs";
|
|
40
40
|
import * as remoteModule from "../lib/remote.mjs";
|
|
41
|
+
import { activateLocalInputs, localRevision } from "../lib/local-inputs.mjs";
|
|
41
42
|
import YAML from "yaml";
|
|
42
43
|
import { attachArgv, checkRemote, forgetSnapshot, getServer, inspectRemote, startRemote, restartRemote, launchConfigRemote, scheduleRemote, listSnapshots, readServers, rosterGroups, routeCommand, targetOf, validateServer, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
|
|
43
44
|
import { spawnSync as spawnSyncProc } from "node:child_process";
|
|
@@ -367,7 +368,7 @@ async function inspectCmd() {
|
|
|
367
368
|
if (t) {
|
|
368
369
|
if (t.resolutionError) return bail(t.resolutionError.code, t.resolutionError.message, t.resolutionError.details ?? undefined);
|
|
369
370
|
const doc = inspectDocument(t, { kernel: OATS_VERSION });
|
|
370
|
-
if (JSON_MODE) { jsonOk(doc); return; }
|
|
371
|
+
if (JSON_MODE) { jsonOk(withObservation(doc)); return; }
|
|
371
372
|
printWorkspaceInspect(doc); return;
|
|
372
373
|
}
|
|
373
374
|
}
|
|
@@ -536,6 +537,7 @@ async function workspaceOperation(t, { bail, address, layer, opName }) {
|
|
|
536
537
|
const env = { ...lp.env(mod.name, settings), OATS_OPERATION: address, OATS_CONTEXT: t.deployment, OATS_ROOT: t.agentsRoot, PI_AGENTS_ROOT: t.agentsRoot };
|
|
537
538
|
if (op.context === "home") Object.assign(env, { OATS_INSTANCE: t.meta.instance, OATS_INSTANCE_HOME: t.home, OATS_HOME: t.home, PI_AGENT_INSTANCE: t.meta.instance, PI_AGENT_HOME: t.home });
|
|
538
539
|
else for (const k of ["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "PI_AGENT_INSTANCE", "PI_AGENT_HOME"]) delete env[k];
|
|
540
|
+
await readSession?.closeBatches(); // no idle `git cat-file --batch` child held for the provider's whole run
|
|
539
541
|
const r = spawnSync("node", [abs, ...rest, ...argFlags, "--json"], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024, timeout: OPERATION_TIMEOUT_MS, killSignal: "SIGTERM" });
|
|
540
542
|
finishOperation({ r, bail, address, provider, op, argFlags, cwd, home: t.home, meta: t.meta, api: INSPECT_OPERATIONS_API });
|
|
541
543
|
}
|
|
@@ -686,6 +688,7 @@ function serializeLaunchConfigs(map) {
|
|
|
686
688
|
}
|
|
687
689
|
if (e.model !== undefined) lines.push(` model: ${yamlQuoted(e.model)}`);
|
|
688
690
|
if (e.yolo !== undefined) lines.push(` yolo: ${e.yolo}`);
|
|
691
|
+
if (e.default === true) lines.push(" default: true");
|
|
689
692
|
}
|
|
690
693
|
return lines.join("\n") + "\n";
|
|
691
694
|
}
|
|
@@ -700,6 +703,7 @@ function normalizeLaunchConfig(e) {
|
|
|
700
703
|
...(e.env && Object.keys(e.env).length ? { env: Object.fromEntries(Object.keys(e.env).sort().map((n) => [n, typeof e.env[n] === "string" ? e.env[n] : { fromEnv: e.env[n].fromEnv }])) } : {}),
|
|
701
704
|
...(e.model !== undefined ? { model: e.model } : {}),
|
|
702
705
|
...(e.yolo !== undefined ? { yolo: e.yolo } : {}),
|
|
706
|
+
...(e.default === true ? { default: true } : {}),
|
|
703
707
|
};
|
|
704
708
|
}
|
|
705
709
|
function readLaunchConfigsModel(local) {
|
|
@@ -716,7 +720,7 @@ function readLaunchConfigsModel(local) {
|
|
|
716
720
|
* `set --keep-env`. */
|
|
717
721
|
function publicLaunchConfig(e, extra = {}) {
|
|
718
722
|
const env = Object.fromEntries(Object.keys(e.env || {}).sort().map((n) => [n, typeof e.env[n] === "string" ? { redacted: true } : { fromEnv: e.env[n].fromEnv }]));
|
|
719
|
-
return { harness: e.harness, executable: e.executable ?? null, args: [...(e.args || [])], env, model: e.model ?? null, yolo: e.yolo ?? null, ...extra };
|
|
723
|
+
return { harness: e.harness, executable: e.executable ?? null, args: [...(e.args || [])], env, model: e.model ?? null, yolo: e.yolo ?? null, default: e.default === true, ...extra };
|
|
720
724
|
}
|
|
721
725
|
/** The scope a launch-config command reads: --dir (or cwd), a running
|
|
722
726
|
* home's recorded context (--home), or a soul's own member context
|
|
@@ -766,7 +770,7 @@ function launchPreview(bail) {
|
|
|
766
770
|
// (E_LAUNCH_LEGACY: re-spawn it from the deployment).
|
|
767
771
|
let d;
|
|
768
772
|
try { d = describeLaunchCommand(meta.command); } catch (e) { bail(e.code || "E_LAUNCH_COMMAND_UNSUPPORTED", e.message); }
|
|
769
|
-
jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, harness: null, model: null, yolo: null }, harness: meta.harness, model: meta.model || null, modelSource: meta.model ? "recorded" : "native default", yolo: meta.yolo ?? null, launchConfig: null, launchConfigSource: null, executable: { path: d.executable, declared: null, resolvedFrom: "recorded" }, argv: d.argv, environment: d.environment, command: redactLaunchCommand(meta.command), prompt: { kind: "task-file", file: "TASK.md" }, hooks: null, preflight: [{ check: "recipe", ok: true, detail: "frozen command; a selection is refused (E_LAUNCH_LEGACY): re-spawn it" }], ok: true });
|
|
773
|
+
jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, harness: null, model: null, yolo: null }, harness: meta.harness, model: meta.model || null, modelSource: meta.model ? "recorded" : "native default", yolo: meta.yolo ?? null, launchConfig: null, launchConfigSource: null, launchConfigDefault: false, executable: { path: d.executable, declared: null, resolvedFrom: "recorded" }, argv: d.argv, environment: d.environment, command: redactLaunchCommand(meta.command), prompt: { kind: "task-file", file: "TASK.md" }, hooks: null, preflight: [{ check: "recipe", ok: true, detail: "frozen command; a selection is refused (E_LAUNCH_LEGACY): re-spawn it" }], ok: true });
|
|
770
774
|
return;
|
|
771
775
|
}
|
|
772
776
|
const agentsRoot = agentsRootOfHome(home);
|
|
@@ -788,7 +792,7 @@ function launchPreview(bail) {
|
|
|
788
792
|
const command = renderLaunchRecipe(recipe, { home, instance, redact: true });
|
|
789
793
|
const d = describeLaunchCommand(command);
|
|
790
794
|
const environment = d.environment.map((e) => e.reference && recipe.env[e.name]?.fromEnv ? { name: e.name, fromEnv: recipe.env[e.name].fromEnv } : e);
|
|
791
|
-
jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig, harness: sel.harness ?? null, model: sel.model ?? null, yolo: sel.yolo ?? null }, harness: plan.harness, model: recipe.model, modelSource: plan.modelSource, yolo: recipe.yolo ?? null, launchConfig: recipe.launchConfig, launchConfigSource: recipe.launchConfigSource, executable: { path: plan.executable.path, declared: plan.executable.declared ?? null, resolvedFrom: plan.executable.resolvedFrom }, argv: d.argv, environment, command, prompt: recipe.prompt, hooks: redactLaunchRecipe(recipe).hooks, preflight: plan.preflight, ok: plan.ok });
|
|
795
|
+
jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig, harness: sel.harness ?? null, model: sel.model ?? null, yolo: sel.yolo ?? null }, harness: plan.harness, model: recipe.model, modelSource: plan.modelSource, yolo: recipe.yolo ?? null, launchConfig: recipe.launchConfig, launchConfigSource: recipe.launchConfigSource, launchConfigDefault: recipe.launchConfigDefault === true, executable: { path: plan.executable.path, declared: plan.executable.declared ?? null, resolvedFrom: plan.executable.resolvedFrom }, argv: d.argv, environment, command, prompt: recipe.prompt, hooks: redactLaunchRecipe(recipe).hooks, preflight: plan.preflight, ok: plan.ok });
|
|
792
796
|
}
|
|
793
797
|
async function launchConfigCmd() {
|
|
794
798
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
@@ -817,7 +821,7 @@ async function launchConfigCmd() {
|
|
|
817
821
|
if (!configurations.length) { console.log(`No launch configurations are declared${file ? ` in ${shortPath(file)}` : ` (no oats-local.yaml in reach of ${dir})`}`); return; }
|
|
818
822
|
for (const c of configurations) {
|
|
819
823
|
const env = Object.entries(c.env).map(([n, v]) => v.fromEnv ? `${n}=$${v.fromEnv}` : `${n}=<redacted>`).join(" ");
|
|
820
|
-
console.log(`${c.name}: ${c.harness}${c.executable ? ` ${c.executable}` : ""}${c.args.length ? ` ${c.args.map((a) => JSON.stringify(a)).join(" ")}` : ""}${env ? ` [${env}]` : ""}${c.model ? ` model ${c.model}` : ""}${c.yolo !== null ? ` yolo ${c.yolo}` : ""}`);
|
|
824
|
+
console.log(`${c.name}: ${c.harness}${c.default ? ` (this machine's ${c.harness} default)` : ""}${c.executable ? ` ${c.executable}` : ""}${c.args.length ? ` ${c.args.map((a) => JSON.stringify(a)).join(" ")}` : ""}${env ? ` [${env}]` : ""}${c.model ? ` model ${c.model}` : ""}${c.yolo !== null ? ` yolo ${c.yolo}` : ""}`);
|
|
821
825
|
}
|
|
822
826
|
return;
|
|
823
827
|
}
|
|
@@ -860,6 +864,8 @@ async function launchConfigCmd() {
|
|
|
860
864
|
try { validateLaunchConfig(name, entry, `--file ${f}`); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
|
|
861
865
|
if (!Object.hasOwn(entry, "harness")) noteRuntimeName(`runtime in the --file definition (written as harness)`);
|
|
862
866
|
model[name] = normalizeLaunchConfig(entry);
|
|
867
|
+
// One default per harness, over the file as it would be written (no automatic move).
|
|
868
|
+
try { validateLaunchConfigDefaults(model, shortPath(file)); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message, e.details); }
|
|
863
869
|
}
|
|
864
870
|
let next;
|
|
865
871
|
try { next = replaceLaunchConfigsBlock(text, serializeLaunchConfigs(model)); } catch (e) { bail(e.code || "E_CONFIG_BROKEN", `${e.message}; nothing was written`); }
|
|
@@ -1013,11 +1019,60 @@ async function readinessCmd() {
|
|
|
1013
1019
|
// remotes (lib/workspace.mjs), packages resolve to exact commits (lib/packages.mjs,
|
|
1014
1020
|
// lock v3) and the only persisted state is `oats-lock.json` beside oats-local.yaml.
|
|
1015
1021
|
|
|
1022
|
+
/** This command's read session (lib/remote.mjs createReadSession): one per process, created at the
|
|
1023
|
+
* first remote read, closed when the command ends (the `finally` of the dispatch) and, for the
|
|
1024
|
+
* process.exit paths, on exit — no `git cat-file --batch` child outlives the command. Its git
|
|
1025
|
+
* children run as their own process groups, so a terminal's Ctrl-C no longer reaches them: a
|
|
1026
|
+
* SIGINT, SIGTERM or SIGHUP closes the session and exits (128 + the signal number), which kills them. */
|
|
1027
|
+
let readSession = null;
|
|
1028
|
+
/** The validated `--max-age` seconds (checked once at dispatch: maxAgeRefusal), null when not given. */
|
|
1029
|
+
let maxAgeGiven = null;
|
|
1030
|
+
function commandSession() {
|
|
1031
|
+
if (!readSession) {
|
|
1032
|
+
readSession = remoteModule.createReadSession({ maxAge: maxAgeGiven ?? 0 });
|
|
1033
|
+
process.on("exit", () => readSession.closeNow());
|
|
1034
|
+
for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143], ["SIGHUP", 129]]) process.once(signal, () => {
|
|
1035
|
+
readSession.closeNow();
|
|
1036
|
+
// Another handler (a scheduler lock's release) exits on its own after this one.
|
|
1037
|
+
if (process.listenerCount(signal) === 0) process.exit(code);
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
return readSession;
|
|
1041
|
+
}
|
|
1042
|
+
/** Which kernel command forms take --max-age: THE allow-list (docs/desktop-cli-api.md "Observation reuse").
|
|
1043
|
+
* → null when this form reads with observation reuse, else the E_BAD_ARGS message. `head` is argv before `--`. */
|
|
1044
|
+
const MAX_AGE_READS = "status, workspace status, souls, capabilities, inspect --soul|--home, and the read forms of teams and soul teams";
|
|
1045
|
+
function maxAgeRefusal(command, head) {
|
|
1046
|
+
const word = (i) => (head[i] !== undefined && !head[i].startsWith("--") ? head[i] : undefined);
|
|
1047
|
+
const refuse = (form) => `--max-age is not accepted by \`oats ${form}\`: only the read verbs reuse observations (${MAX_AGE_READS})`;
|
|
1048
|
+
if (head.includes("--server")) return "--max-age cannot be combined with --server: observation reuse is local to this machine";
|
|
1049
|
+
switch (command) {
|
|
1050
|
+
case "status": case "souls": case "capabilities": case "inspect": return null;
|
|
1051
|
+
case "workspace": return word(1) === "status" ? null : refuse(["workspace", word(1)].filter(Boolean).join(" "));
|
|
1052
|
+
case "teams": return word(1) === undefined ? null : refuse(`teams ${word(1)}`);
|
|
1053
|
+
case "soul": {
|
|
1054
|
+
if (word(1) !== "teams") return refuse(["soul", word(1)].filter(Boolean).join(" "));
|
|
1055
|
+
const edit = ["--add", "--remove", "--default", "--clear-default"].find((f) => head.includes(f));
|
|
1056
|
+
return edit ? refuse(`soul teams ${edit}`) : null;
|
|
1057
|
+
}
|
|
1058
|
+
default: {
|
|
1059
|
+
const sub = ["package", "schedule", "session", "trigger", "automations", "launch-config", "server", "instance", "operation", "pane"].includes(command) ? word(1) : undefined;
|
|
1060
|
+
return refuse([command, sub].filter(Boolean).join(" "));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
/** The observation block: the heads' oldest observedAt and reuse (the read session) and the revision of
|
|
1065
|
+
* the local configuration this command read (lib/local-inputs.mjs, recording since dispatch). */
|
|
1066
|
+
const observationBlock = () => ({ ...commandSession().observation(), localRevision: localRevision() });
|
|
1067
|
+
/** A read verb's JSON with the observation block — only when --max-age was given (0 included); without
|
|
1068
|
+
* it the document is exactly what it was before the feature (Desktop decodes closed shapes). */
|
|
1069
|
+
const withObservation = (doc) => (maxAgeGiven === null ? doc : { ...doc, observation: observationBlock() });
|
|
1070
|
+
|
|
1016
1071
|
/** Remote options threaded into every remote call. OATS_REMOTE_CACHE relocates
|
|
1017
|
-
* the content-addressed fetch cache (tests never touch ~/.cache). */
|
|
1072
|
+
* the content-addressed fetch cache (tests never touch ~/.cache); `session` is the command's read session. */
|
|
1018
1073
|
function remoteOptionsFromEnv() {
|
|
1019
1074
|
const cacheDir = process.env.OATS_REMOTE_CACHE;
|
|
1020
|
-
return cacheDir ? { cacheDir: resolve(cacheDir) } : {};
|
|
1075
|
+
return { ...(cacheDir ? { cacheDir: resolve(cacheDir) } : {}), session: commandSession() };
|
|
1021
1076
|
}
|
|
1022
1077
|
|
|
1023
1078
|
/** The v2 deployment context at --dir: { dir, localPath, local, deploymentDir, remoteOptions }. */
|
|
@@ -1443,7 +1498,7 @@ async function workspaceCmd() {
|
|
|
1443
1498
|
result.warnings.push({ code: "automation-trust-stale", entry, message: `oats-local.yaml automations.trust names ${entry}, which is no workspace trigger or schedule${actx.snapshot ? "" : " (no automations snapshot yet: run oats sync)"}; its member may not have synced yet` });
|
|
1444
1499
|
}
|
|
1445
1500
|
await workspaceStatusFacts(result, discovery, lock, ctx);
|
|
1446
|
-
if (JSON_MODE) { jsonOk(result); return; }
|
|
1501
|
+
if (JSON_MODE) { jsonOk(withObservation(result)); return; }
|
|
1447
1502
|
console.log(`workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)}) local ${shortPath(ctx.localPath)}\n`);
|
|
1448
1503
|
if (standalone) console.log(` (${standaloneNote(discovery)})\n`);
|
|
1449
1504
|
console.log("Members:");
|
|
@@ -1533,7 +1588,7 @@ async function teamsCmd() {
|
|
|
1533
1588
|
else if (sub === "default") result = V.teamsDefault(ctx, label);
|
|
1534
1589
|
} catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
1535
1590
|
const doc = V.teamsDocument(result ? { ...ctx, local: result.local } : ctx);
|
|
1536
|
-
if (JSON_MODE) { jsonOk(result ? { ...doc, changed: result.changed } : doc); return; }
|
|
1591
|
+
if (JSON_MODE) { jsonOk(result ? { ...doc, changed: result.changed } : withObservation(doc)); return; }
|
|
1537
1592
|
if (result) console.log(result.changed ? `${sub === "add" ? `Declared team ${label}` : sub === "remove" ? `Removed team ${label}` : `The default team is now ${label}`} in ${shortPath(ctx.localPath)}` : "Nothing to change");
|
|
1538
1593
|
console.log(`default ${doc.defaultTeam ?? "(none)"}`);
|
|
1539
1594
|
if (!doc.teams.length) console.log("teams (none: `oats aweb setup` creates them, or `oats teams add <label> --team <id>`)");
|
|
@@ -1560,7 +1615,7 @@ async function soulCmd() {
|
|
|
1560
1615
|
else {
|
|
1561
1616
|
// The soul is named as for spawn: E_SOUL_UNKNOWN / E_SOUL_AMBIGUOUS; package souls come from the lock.
|
|
1562
1617
|
let lock = null;
|
|
1563
|
-
try { lock =
|
|
1618
|
+
try { lock = readLockIfPresent(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
1564
1619
|
let discovery, entry;
|
|
1565
1620
|
try {
|
|
1566
1621
|
const { discoverOrStandalone, findSoulEntry } = await import("../lib/instance-resolution.mjs");
|
|
@@ -1577,7 +1632,7 @@ async function soulCmd() {
|
|
|
1577
1632
|
if (mutating) result = V.soulTeamsEdit(teamsCtx, key, edit);
|
|
1578
1633
|
doc = V.soulTeamsDocument(result ? { ...teamsCtx, local: result.local } : teamsCtx, { soul, key });
|
|
1579
1634
|
} catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
1580
|
-
if (JSON_MODE) { jsonOk(result ? { ...doc, changed: result.changed } : doc); return; }
|
|
1635
|
+
if (JSON_MODE) { jsonOk(result ? { ...doc, changed: result.changed } : withObservation(doc)); return; }
|
|
1581
1636
|
if (result) console.log(result.changed ? `Updated the teams of ${key === "*" ? "every soul" : key} in ${shortPath(teamsCtx.localPath)}` : "Nothing to change");
|
|
1582
1637
|
console.log(`${key === "*" ? "every soul" : key} on this computer: default ${doc.defaultTeam ? `${doc.defaultTeam.label} (${doc.defaultTeam.from})` : "(none)"}`);
|
|
1583
1638
|
if (doc.teams.length) printTable(["team", "id", "from", "why"], doc.teams.map((t) => [t.default ? `${t.label} (default)` : t.label, t.team ?? "(no id yet)", t.from, t.via.join(",")]));
|
|
@@ -1596,7 +1651,7 @@ async function itemsCmd(kind) {
|
|
|
1596
1651
|
if (kind === "capabilities") await capabilityFacts(items, discovery, lock, ctx, remote);
|
|
1597
1652
|
if (kind === "souls") await soulFacts(items, discovery, lock, ctx, remote);
|
|
1598
1653
|
const standalone = discovery.standalone === true;
|
|
1599
|
-
if (JSON_MODE) { jsonOk({ [`${kind}Api`]: 1, standalone: standalone || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, commit: discovery.commit }, [kind]: items, problems: discovery.problems }); return; }
|
|
1654
|
+
if (JSON_MODE) { jsonOk(withObservation({ [`${kind}Api`]: 1, standalone: standalone || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, commit: discovery.commit }, [kind]: items, problems: discovery.problems })); return; }
|
|
1600
1655
|
console.log(`${kind} of workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)})${standalone ? ` — ${standaloneNote(discovery)}` : ""}\n`);
|
|
1601
1656
|
if (!items.length) console.log(" (none)");
|
|
1602
1657
|
else if (kind === "souls") printTable(["name", "origin", "teams here", "work"], items.map((s) => [s.name, s.origin, s.teams === null ? "(invalid: oats teams)" : s.teams.map((t) => (t.default ? `${t.label}*` : t.label)).join(",") || "—", s.work ?? "—"]));
|
|
@@ -1616,14 +1671,24 @@ async function capabilityFacts(rows, discovery, lock, ctx, remote = remoteModule
|
|
|
1616
1671
|
for (const [id, entry] of Object.entries(lock?.packages || {})) {
|
|
1617
1672
|
try { packages.set(id, await lockedPackageCapabilities(id, entry, { catalog, remote, remoteOptions })); } catch { packages.set(id, null); }
|
|
1618
1673
|
}
|
|
1674
|
+
// Indexes built once (a scan of every member per row was O(rows × members)): the member row per key
|
|
1675
|
+
// (memberRowByKey); the member rows per (key, commit), in row order.
|
|
1676
|
+
const capOf = (member, name) => member?.capabilities.find((c) => c.name === name);
|
|
1677
|
+
const rowsAt = new Map();
|
|
1678
|
+
for (const r of rows) {
|
|
1679
|
+
if (r.kind === "package") continue;
|
|
1680
|
+
const k = `${r.repoKey}\0${r.commit}`;
|
|
1681
|
+
if (!rowsAt.has(k)) rowsAt.set(k, []);
|
|
1682
|
+
rowsAt.get(k).push(r);
|
|
1683
|
+
}
|
|
1619
1684
|
// A member capability's fingerprint is its Git tree at the commit (one listing per member commit); a
|
|
1620
1685
|
// package's is the package integrity (the lock).
|
|
1621
1686
|
const trees = new Map();
|
|
1622
1687
|
const treeOf = async ({ repoKey, commit }, ref, dir) => {
|
|
1623
1688
|
const key = `${repoKey}\0${commit}`;
|
|
1624
1689
|
if (!trees.has(key)) {
|
|
1625
|
-
const member = discovery.members
|
|
1626
|
-
const dirs =
|
|
1690
|
+
const member = memberRowByKey(discovery.members, repoKey);
|
|
1691
|
+
const dirs = (rowsAt.get(key) || []).map((r) => capOf(member, r.name)?.path).filter(Boolean);
|
|
1627
1692
|
trees.set(key, remoteModule.remoteTreeOids(ref, commit, [...new Set(dirs)], remoteOptions).catch(() => new Map()));
|
|
1628
1693
|
}
|
|
1629
1694
|
return (await trees.get(key)).get(dir) ?? null;
|
|
@@ -1634,7 +1699,7 @@ async function capabilityFacts(rows, discovery, lock, ctx, remote = remoteModule
|
|
|
1634
1699
|
const read = packages.get(row.package), cap = read?.capabilities.find((c) => c.name === row.name);
|
|
1635
1700
|
if (cap) ({ manifest, dir } = cap), ref = read.ref;
|
|
1636
1701
|
} else {
|
|
1637
|
-
const cap = discovery.members
|
|
1702
|
+
const cap = capOf(memberRowByKey(discovery.members, row.repoKey), row.name);
|
|
1638
1703
|
if (cap) { manifest = cap.manifest; dir = cap.path; ref = memberRef(discovery, remoteModule, row.repoKey); }
|
|
1639
1704
|
}
|
|
1640
1705
|
const provides = manifest ? await capabilityProvides({ ref, commit: row.commit, dir, manifest, remote, remoteOptions }) : { skills: null, commands: null, hooks: null };
|
|
@@ -1649,9 +1714,13 @@ async function capabilityFacts(rows, discovery, lock, ctx, remote = remoteModule
|
|
|
1649
1714
|
* discovery and the lock without spawning (soulSpawnability). */
|
|
1650
1715
|
async function soulFacts(rows, discovery, lock, ctx, remote = remoteModule) {
|
|
1651
1716
|
const { soulSpawnability } = await import("../lib/instance-resolution.mjs");
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1717
|
+
// Indexes built once, each keeping the FIRST match as `find` did (a scan per row was O(souls × members)).
|
|
1718
|
+
const first = (pairs) => { const m = new Map(); for (const [k, v] of pairs) if (!m.has(k)) m.set(k, v); return m; };
|
|
1719
|
+
const packageSoul = first((discovery.packageSouls || []).map((p) => [p.qualifiedName, p]));
|
|
1720
|
+
const externalSoul = first((discovery.external || []).map((e) => e.soul).map((x) => [`${x.name}\0${x.repoKey}`, x]));
|
|
1721
|
+
const entryOf = (row) => row.kind === "package" ? packageSoul.get(row.qualifiedName)
|
|
1722
|
+
: row.kind === "external" ? externalSoul.get(`${row.name}\0${row.repoKey}`)
|
|
1723
|
+
: memberRowByKey(discovery.members, row.repoKey)?.souls.find((x) => x.name === row.name);
|
|
1655
1724
|
for (const row of rows) {
|
|
1656
1725
|
const entry = entryOf(row);
|
|
1657
1726
|
row.file = { path: `${row.path}/soul.yaml`, url: remoteModule.browseUrl(row.repoKey, row.commit, `${row.path}/soul.yaml`) };
|
|
@@ -1689,7 +1758,7 @@ async function statusDrift(data) {
|
|
|
1689
1758
|
if (!anything) return { drift: new Map(), soul: new Map(), souls, unreachable: null };
|
|
1690
1759
|
const deploymentDir = dirname(ctx.path);
|
|
1691
1760
|
let lock = null;
|
|
1692
|
-
try {
|
|
1761
|
+
try { lock = readLockIfPresent(deploymentDir); } catch { lock = null; }
|
|
1693
1762
|
let discovery;
|
|
1694
1763
|
// The standalone view (decisions 10/25) is a discovery too: drift of a standalone
|
|
1695
1764
|
// instance is computed against its member's current state, not reported "unreachable".
|
|
@@ -1707,9 +1776,12 @@ async function statusDrift(data) {
|
|
|
1707
1776
|
if (hasSoul(i)) { try { const row = soulDriftOf(i, discovery); if (row) soul.set(key, row); } catch { /* an unreadable soul record shows as no soul row */ } }
|
|
1708
1777
|
}
|
|
1709
1778
|
// The roster's soul row reflects the CURRENT member commit too (the pointer may lag a moved member).
|
|
1779
|
+
// First match per key, as `find` gave, from indexes built once (not a scan per agent).
|
|
1780
|
+
const packageSoulAt = new Map();
|
|
1781
|
+
for (const p of discovery.packageSouls || []) { const k = `${p.package}\0${p.path}`; if (!packageSoulAt.has(k)) packageSoulAt.set(k, p); }
|
|
1710
1782
|
for (const [, stamp] of souls) {
|
|
1711
|
-
if (typeof stamp.package === "string") { const now =
|
|
1712
|
-
const member = discovery.members
|
|
1783
|
+
if (typeof stamp.package === "string") { const now = packageSoulAt.get(`${stamp.package}\0${stamp.path}`); if (now) stamp.current = now.commit; continue; }
|
|
1784
|
+
const member = memberRowByKey(discovery.members, stamp.repoKey);
|
|
1713
1785
|
if (member && typeof member.commit === "string" && (member.confirmed || (discovery.standalone === true && member.key === discovery.key))) stamp.current = member.commit;
|
|
1714
1786
|
}
|
|
1715
1787
|
return { drift, soul, souls, unreachable: null };
|
|
@@ -1780,7 +1852,8 @@ async function status() {
|
|
|
1780
1852
|
if (s) i.soul = { repoKey: s.repoKey, commit: s.commit, current: s.current?.commit ?? null, status: s.status, ...(s.reason ? { reason: s.reason } : {}), ...(s.package ? { package: s.package, version: s.version, currentVersion: s.current?.version ?? null } : {}) };
|
|
1781
1853
|
}
|
|
1782
1854
|
}
|
|
1783
|
-
|
|
1855
|
+
const observation = maxAgeGiven === null ? {} : { observation: observationBlock() };
|
|
1856
|
+
console.log(JSON.stringify({ root, agents: data, ...observation, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}), ...envelopeWarnings() }, null, 2)); return;
|
|
1784
1857
|
}
|
|
1785
1858
|
console.log(`oats status — agents root ${shortPath(root)}\n`);
|
|
1786
1859
|
if (ws?.unreachable) console.log(` workspace: unreachable (${ws.unreachable.reason}) — drift unknown\n`);
|
|
@@ -2069,7 +2142,7 @@ async function spawnCmd() {
|
|
|
2069
2142
|
// (the deployment's cache had no entry for its commit): the result says so.
|
|
2070
2143
|
if (prepared) r.soulFetched = soulFetched;
|
|
2071
2144
|
if (JSON_MODE) { jsonOk(r); return; }
|
|
2072
|
-
console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) harness ${r.harness}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}; nothing was created${soulFetched ? " (the soul source was fetched to a temporary copy, not kept)" : ""}`);
|
|
2145
|
+
console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) harness ${r.harness}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}${r.launchConfig ? ` via launch configuration ${r.launchConfig}${r.launchConfigDefault ? ` (this machine's ${r.harness} default)` : ""}` : ""}${r.yolo ? " YOLO" : ""}; nothing was created${soulFetched ? " (the soul source was fetched to a temporary copy, not kept)" : ""}`);
|
|
2073
2146
|
return;
|
|
2074
2147
|
}
|
|
2075
2148
|
} catch (e) {
|
|
@@ -2081,7 +2154,7 @@ async function spawnCmd() {
|
|
|
2081
2154
|
// A launch refusal (configuration, executable, environment reference,
|
|
2082
2155
|
// model, harness) is a fact about the selection, not a spawn-mechanism
|
|
2083
2156
|
// failure: it keeps its own code so a GUI can act on it.
|
|
2084
|
-
if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_HARNESS$|^E_HARNESS_UNAVAILABLE$/.test(e.code)) { bail(e.code, e.message, e.details); throw e; }
|
|
2157
|
+
if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_HARNESS$|^E_HARNESS_UNAVAILABLE$|^E_CLAUDE_CONFIG_REMOVED$/.test(e.code)) { bail(e.code, e.message, e.details); throw e; }
|
|
2085
2158
|
// An unmet declared requirement is a fact about the soul's configuration
|
|
2086
2159
|
// (with a remedy), not a spawn-mechanism failure: keep its code and details.
|
|
2087
2160
|
if (e?.code === "E_REQUIREMENT_INACTIVE") { bail(e.code, e.message, { soul: e.soul, capabilities: e.capabilities, context: e.context, remedy: e.remedy }); throw e; }
|
|
@@ -2782,6 +2855,7 @@ async function capabilityCommand() {
|
|
|
2782
2855
|
// OATS_SOUL is the recorded soul or nothing: an ambient value inherited from the
|
|
2783
2856
|
// invoking process names some other soul (a coordinator's own), never this one.
|
|
2784
2857
|
const { OATS_SOUL: _ambientSoul, ...inherited } = process.env;
|
|
2858
|
+
await readSession?.closeBatches(); // no idle `git cat-file --batch` child held for the provider's whole run
|
|
2785
2859
|
const r = spawnSync("node", [abs, ...rest, ...rawArgs.slice(2)], { stdio: "inherit", env: {
|
|
2786
2860
|
...inherited, OATS_CAPABILITY: m.capability,
|
|
2787
2861
|
// Package-runtime boundary: dispatched commands receive the active
|
|
@@ -2803,7 +2877,9 @@ async function capabilityCommand() {
|
|
|
2803
2877
|
} });
|
|
2804
2878
|
// Child never ran (spawn error): nothing reached stdout — keep the envelope contract.
|
|
2805
2879
|
if (r.error) bail("E_CAPABILITY_BROKEN", `oats ${cmd} ${sub}: ${r.error.message || r.error}`);
|
|
2806
|
-
|
|
2880
|
+
// A provider killed by a signal (a Ctrl-C reaches both it and us; our read session's handler
|
|
2881
|
+
// waits for spawnSync) exits as the shell reports a signal death: 128 + its number.
|
|
2882
|
+
process.exit(r.status ?? (r.signal ? 128 + (osConstants.signals[r.signal] ?? 0) : 1));
|
|
2807
2883
|
}
|
|
2808
2884
|
}
|
|
2809
2885
|
|
|
@@ -2861,7 +2937,7 @@ function versionCmd() {
|
|
|
2861
2937
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2862
2938
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2863
2939
|
// never listed.
|
|
2864
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
2940
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "launch-config-default"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
2865
2941
|
return;
|
|
2866
2942
|
}
|
|
2867
2943
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3253,6 +3329,17 @@ try {
|
|
|
3253
3329
|
const selector = head.find((a) => /^--(deployment|resolution|artifact-set)$/.test(a));
|
|
3254
3330
|
const refuse = (message, details) => { if (JSON_MODE) jsonFail("E_UNSUPPORTED_MODE", message, details); die(message); };
|
|
3255
3331
|
if (selector) refuse(`${selector}: a captured selector is refused (the captured/portable path was removed in 0.26); run the command in its workspace deployment or instance home instead`, { selector });
|
|
3332
|
+
// --max-age (feature observe-max-age): ONE allow-list for every kernel command, here — never a
|
|
3333
|
+
// per-command copy. A capability namespace's argv is its provider's.
|
|
3334
|
+
if (kernelArgv && head.includes("--max-age")) {
|
|
3335
|
+
const refusal = maxAgeRefusal(cmd, head);
|
|
3336
|
+
if (refusal) cmdFail("E_BAD_ARGS", refusal);
|
|
3337
|
+
const raw = flag("max-age");
|
|
3338
|
+
if (raw === true) cmdFail("E_BAD_ARGS", `--max-age needs a value: whole seconds from 0 to ${remoteModule.MAX_AGE_LIMIT}`);
|
|
3339
|
+
if (!/^\d{1,5}$/.test(raw) || Number(raw) > remoteModule.MAX_AGE_LIMIT) cmdFail("E_BAD_ARGS", `--max-age takes whole seconds from 0 to ${remoteModule.MAX_AGE_LIMIT}, got ${JSON.stringify(raw)}`);
|
|
3340
|
+
maxAgeGiven = Number(raw);
|
|
3341
|
+
activateLocalInputs(); // observation.localRevision: every local config read from here on is recorded
|
|
3342
|
+
}
|
|
3256
3343
|
const inherited = ["OATS_RESOLUTION", "OATS_DEPLOYMENT"].filter((k) => process.env[k]);
|
|
3257
3344
|
if (inherited.length && cmd !== "version") refuse(`this environment carries a captured context (${inherited.join(", ")}): the captured/portable path was removed in 0.26, and nothing is run against the current context in its place — retire the captured home and re-spawn it from the deployment`, { inherited });
|
|
3258
3345
|
if (cmd === "inspect" && head.includes("--request")) {
|
|
@@ -3329,7 +3416,8 @@ else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && JSON_MODE) json
|
|
|
3329
3416
|
else {
|
|
3330
3417
|
if (cmd && !HELP_WORDS.has(cmd) && !cmd.startsWith("--")) console.error(`oats: unknown command "${cmd}" — no kernel subcommand or active capability namespace matches\n`);
|
|
3331
3418
|
console.log(usageText());
|
|
3332
|
-
|
|
3419
|
+
// exitCode, not exit(): the usage is ~20 KB, and exit() cuts off whatever a pipe has not drained yet.
|
|
3420
|
+
process.exitCode = cmd && !HELP_WORDS.has(cmd) ? 1 : 0;
|
|
3333
3421
|
}
|
|
3334
3422
|
} // end: every command but onboard
|
|
3335
3423
|
|
|
@@ -3357,6 +3445,7 @@ Usage:
|
|
|
3357
3445
|
oats version [--json] kernel version; --json emits the
|
|
3358
3446
|
Desktop CLI API v1 probe payload
|
|
3359
3447
|
oats status [--json] agents, souls, running instances
|
|
3448
|
+
[--max-age <s>] reuse head observations up to <s> s old (below)
|
|
3360
3449
|
oats server add <id> --ssh <alias> register another machine's OATS (OpenSSH alias,
|
|
3361
3450
|
--workspace </abs/path> [--oats <p>] remote workspace, remote oats path; no keys stored;
|
|
3362
3451
|
[--path <dir:dir>] --path = dirs prepended to the remote PATH, e.g. ~/.local/bin)
|
|
@@ -3460,7 +3549,7 @@ Usage:
|
|
|
3460
3549
|
a detached external retirement runs
|
|
3461
3550
|
oats inspect [--dir <scope>] [--soul <name> one authoritative JSON answer for a GUI: souls
|
|
3462
3551
|
[--agents-root <abs>]] [--home <abs>] (harness defaults, editability, instructions),
|
|
3463
|
-
[--json]
|
|
3552
|
+
[--max-age <s>] [--json] installed capabilities with health, effective
|
|
3464
3553
|
layer bindings and activation, declared
|
|
3465
3554
|
operations with availability; --home answers the
|
|
3466
3555
|
running home's recorded modules and their drift
|
|
@@ -3501,19 +3590,20 @@ Usage:
|
|
|
3501
3590
|
| remove <id> [--dir <d>] workspace repo is the current checkout; otherwise
|
|
3502
3591
|
print the line to add (the file travels through Git)
|
|
3503
3592
|
oats workspace status [--dir <d>] [--json] membership table (confirmed / no-backlink /
|
|
3504
|
-
|
|
3593
|
+
[--max-age <s>] cannot-read / backlink-elsewhere), locked packages
|
|
3505
3594
|
oats capabilities [--dir <d>] [--json] every capability of every confirmed member (a
|
|
3506
|
-
|
|
3595
|
+
[--max-age <s>] private one is listed as repo-owned: usable only by
|
|
3507
3596
|
its own repo's souls) + the locked packages
|
|
3508
3597
|
oats souls [--dir <d>] [--json] every soul of every confirmed member + external souls
|
|
3509
|
-
|
|
3598
|
+
[--max-age <s>] (souls have no private mode), with origin
|
|
3510
3599
|
(member <key> @ <commit> | package <id> v<ver>) and its
|
|
3511
3600
|
teams on this deployment
|
|
3512
3601
|
oats teams [--json] | add <label> --team <id> [--description <d>] | remove <label>
|
|
3513
3602
|
| default <label> [--dir <d>] this deployment's teams (shared + local), the
|
|
3514
|
-
|
|
3603
|
+
[--max-age <s>] (the read form only) default; add/remove/default edit oats-local.yaml
|
|
3515
3604
|
oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <l> | --clear-default]
|
|
3516
3605
|
[--dir <d>] [--json] which teams a soul (or every soul) belongs to here
|
|
3606
|
+
[--max-age <s>] (without an edit)
|
|
3517
3607
|
oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
|
|
3518
3608
|
read-only Git observation of the instance's work
|
|
3519
3609
|
tree: branch, status (renames kept), ahead/behind
|
|
@@ -3568,6 +3658,18 @@ The turn record (core — every conversation captured, searchable, replicated):
|
|
|
3568
3658
|
oats <namespace> <command> [args…] run an operational command only when its
|
|
3569
3659
|
capability is active (e.g. oats okf harvest)
|
|
3570
3660
|
|
|
3661
|
+
Observation reuse (feature observe-max-age):
|
|
3662
|
+
--max-age <seconds> on the read verbs only — status, workspace status,
|
|
3663
|
+
souls, capabilities, inspect --soul|--home, and the
|
|
3664
|
+
read forms of teams and soul teams — reuse a remote
|
|
3665
|
+
head observation up to <seconds> old (0–86400; 0 is
|
|
3666
|
+
live) instead of asking the remote again; the JSON
|
|
3667
|
+
then carries observation { observedAt (the oldest
|
|
3668
|
+
head used), reused, localRevision (a digest of
|
|
3669
|
+
the local configuration read) }. Refused
|
|
3670
|
+
(E_BAD_ARGS) by every other command, an edit form,
|
|
3671
|
+
and with --server
|
|
3672
|
+
|
|
3571
3673
|
Layers: ${LAYERS.join(", ")}. Workspace model v2: docs/design/2026-09-23-workspace-module-contracts.md.`;
|
|
3572
3674
|
}
|
|
3573
3675
|
} catch (e) {
|
|
@@ -3577,4 +3679,7 @@ Layers: ${LAYERS.join(", ")}. Workspace model v2: docs/design/2026-09-23-workspa
|
|
|
3577
3679
|
// already names the offending file — the readers re-raise it with one.
|
|
3578
3680
|
if (JSON_MODE) jsonFail(e.code, e.message);
|
|
3579
3681
|
die(e.message);
|
|
3682
|
+
} finally {
|
|
3683
|
+
// The command's read session: every `git cat-file --batch` child ends before the process does.
|
|
3684
|
+
if (readSession) await readSession.close();
|
|
3580
3685
|
}
|
package/docs/configuration.md
CHANGED
|
@@ -50,6 +50,11 @@ launch-configs: # named ways this host starts a har
|
|
|
50
50
|
CLAUDE_CONFIG_DIR: { fromEnv: PERSONAL_CLAUDE_DIR }
|
|
51
51
|
model: opus
|
|
52
52
|
yolo: false
|
|
53
|
+
mine:
|
|
54
|
+
harness: claude
|
|
55
|
+
default: true # this host's baseline for every claude launch (0.32)
|
|
56
|
+
env:
|
|
57
|
+
CLAUDE_CONFIG_DIR: /home/ana/.claude-personal
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
Schema: [`oats-local.schema.json`](oats-local.schema.json). Unknown keys are
|
|
@@ -70,7 +75,7 @@ refused (`E_WORKSPACE_SCHEMA`).
|
|
|
70
75
|
| `host.name` | This machine's name. A workspace trigger or schedule runs only on the host named by its `runsOn` ([schedules.md](schedules.md)). |
|
|
71
76
|
| `automations.trust` | The workspace triggers and schedules (`<member>/<id>`) this host agrees to run, or `"*"` for every one the workspace places here (0.30). Absent or empty: none runs. See [Who runs workspace automations](#who-runs-workspace-automations). |
|
|
72
77
|
| `triggers.disabled`, `schedules.disabled` | Workspace triggers and schedules (`<member>/<id>`) this host does not run, without a commit. Written by `oats trigger disable` / `oats schedule disable`. |
|
|
73
|
-
| `launch-configs.<name>` | A named way to start a harness on this host, chosen at spawn or session start, never by the soul. See [Launch configurations](#launch-configurations). |
|
|
78
|
+
| `launch-configs.<name>` | A named way to start a harness on this host, chosen at spawn or session start, never by the soul. `default: true` makes it this host's baseline for its harness (0.32). See [Launch configurations](#launch-configurations). |
|
|
74
79
|
| `souls.launch` | This machine's launch preference per soul (0.30): `"*"` for every soul, a soul's own entry (its name, or `<package>/<soul>`) over it. A value is a `launch-configs` name or an inline `{ harness, model? }`. It overrides the soul's own `launch:`; explicit spawn flags win over both. See [Launch preferences](#launch-preferences). |
|
|
75
80
|
|
|
76
81
|
How teams are resolved, and what a messaging provider does with them, is in
|
|
@@ -81,8 +86,9 @@ How teams are resolved, and what a messaging provider does with them, is in
|
|
|
81
86
|
An entry has `harness` (`pi` \| `claude` \| `codex`, required), `executable`
|
|
82
87
|
(a bare name looked up on `PATH`, or a path relative to this deployment
|
|
83
88
|
directory), `args` (literal, no shell), `env` (a literal string, or
|
|
84
|
-
`{ fromEnv: NAME }` resolved on the host at start), `model
|
|
85
|
-
|
|
89
|
+
`{ fromEnv: NAME }` resolved on the host at start), `model`, `yolo` and
|
|
90
|
+
`default` (0.32; see [the harness default](#the-harness-default)). A launch
|
|
91
|
+
configuration is a host choice: a soul never names one.
|
|
86
92
|
|
|
87
93
|
- Select one with `--launch-config <name>` on `oats spawn`,
|
|
88
94
|
`oats session start` and `oats session restart`. A named configuration is
|
|
@@ -104,6 +110,50 @@ launch configuration is a host choice: a soul never names one.
|
|
|
104
110
|
- The old key `runtime` is still read as `harness`, with a
|
|
105
111
|
`deprecated-runtime-name` warning.
|
|
106
112
|
|
|
113
|
+
### The harness default
|
|
114
|
+
|
|
115
|
+
`default: true` makes a configuration this host's baseline for its harness
|
|
116
|
+
(0.32, feature `launch-config-default`). Use it for what every launch of a
|
|
117
|
+
harness on this machine needs, whatever soul or preference chose it: an
|
|
118
|
+
account directory (`CLAUDE_CONFIG_DIR`), a wrapper `executable`, an argument.
|
|
119
|
+
|
|
120
|
+
- **When it applies:** a new launch that picks the harness without naming a
|
|
121
|
+
configuration: a soul's `launch:`, an inline `souls.launch` preference,
|
|
122
|
+
`--harness` (on a spawn, or on `session start|restart` of an existing
|
|
123
|
+
home), `--reselect-launch`, and the host default (`pi`). It supplies the
|
|
124
|
+
executable, args, env and `yolo`. A `yolo` recorded from a default stays
|
|
125
|
+
with it: a later `--launch-config none` or another harness does not carry
|
|
126
|
+
it over.
|
|
127
|
+
- **The model** comes from whatever picked the harness (`--model`, then the
|
|
128
|
+
preference); the default's `model` is the last fallback, before the
|
|
129
|
+
harness's own.
|
|
130
|
+
- **A named configuration runs as declared**: `--launch-config <name>` or a
|
|
131
|
+
`souls.launch` name never inherits from the default. `--launch-config none`
|
|
132
|
+
(or a `souls.launch` entry of `none`) asks for the bare harness and
|
|
133
|
+
bypasses it.
|
|
134
|
+
- **One per harness.** A second `default: true` for the same harness is
|
|
135
|
+
refused (`E_LAUNCH_CONFIG_INVALID`, naming both); move it by clearing the
|
|
136
|
+
old one first.
|
|
137
|
+
- **Existing homes keep their launch** until `--reselect-launch` or a
|
|
138
|
+
respawn, like any change of preference. Declaring a default is such a
|
|
139
|
+
change: `oats readiness --home` warns `launch-changed` on each existing
|
|
140
|
+
home the default would now apply to, until it is restarted with
|
|
141
|
+
`--reselect-launch` or respawned.
|
|
142
|
+
- **It is visible.** `oats launch-config list` marks it; `spawn --preview`,
|
|
143
|
+
`launch-config preview`, `instance.json` and `oats inspect --home` say when
|
|
144
|
+
a launch's configuration came from the default (`launchConfigDefault`).
|
|
145
|
+
A default with `yolo: true` turns yolo on for every launch of that harness
|
|
146
|
+
here: the preview shows it.
|
|
147
|
+
- **Every kernel that reads the deployment needs OATS 0.32+.** OATS 0.31
|
|
148
|
+
and older refuse the whole `oats-local.yaml` (`E_WORKSPACE_SCHEMA`) once a
|
|
149
|
+
configuration declares `default`.
|
|
150
|
+
|
|
151
|
+
`oats-claude-config` (a one-line file naming the claude binary, found walking
|
|
152
|
+
up from the deployment) is no longer read. A new claude launch with one in
|
|
153
|
+
reach is refused (`E_CLAUDE_CONFIG_REMOVED`) naming the file: declare the
|
|
154
|
+
name it holds as the claude default (`executable: <name>`, `default: true`)
|
|
155
|
+
and delete the file. Homes launched with it keep their recorded executable.
|
|
156
|
+
|
|
107
157
|
### Launch preferences
|
|
108
158
|
|
|
109
159
|
A soul says what its role should run on, and each machine may override it
|
|
@@ -128,9 +178,11 @@ souls:
|
|
|
128
178
|
harness and replaces only its model.
|
|
129
179
|
- A **launch configuration** name runs that configuration's full recipe. An
|
|
130
180
|
**inline or soul preference** runs its harness the way this host starts it
|
|
131
|
-
without a configuration
|
|
132
|
-
|
|
133
|
-
|
|
181
|
+
without a configuration: [the harness default](#the-harness-default) if
|
|
182
|
+
one is declared, else the executable on `PATH` with no args and no env,
|
|
183
|
+
with the preference's `model`. A preference without `model` uses the
|
|
184
|
+
harness default's model, else the harness's own; it never borrows a lower
|
|
185
|
+
layer's.
|
|
134
186
|
- **A missing harness is refused**, never replaced: `E_HARNESS_UNAVAILABLE`
|
|
135
187
|
names the layer that chose it and the fix (install the harness, or override
|
|
136
188
|
it here in `souls.launch`). `oats souls` still lists the soul, with the
|
|
@@ -274,5 +326,10 @@ oats spawn <soul> --preview # the exact modules, teams and provider payloads
|
|
|
274
326
|
oats doctor # this deployment's files and the lock
|
|
275
327
|
```
|
|
276
328
|
|
|
277
|
-
Environment: `OATS_REMOTE_CACHE` relocates the fetch cache
|
|
278
|
-
|
|
329
|
+
Environment: `OATS_REMOTE_CACHE` relocates the fetch cache (which also holds
|
|
330
|
+
the bounded parsed-read cache and the observations `--max-age` reuses; all of
|
|
331
|
+
it is safe to delete); `OATS_PACKAGE_CATALOG` names an alternative package
|
|
332
|
+
catalog file. The read verbs (`status`, `workspace status`, `souls`,
|
|
333
|
+
`capabilities`, `inspect`, and the read forms of `teams` and `soul teams`)
|
|
334
|
+
take `--max-age <seconds>` to reuse a remote head observed that recently
|
|
335
|
+
([Observation reuse](desktop-cli-api.md#observation-reuse-feature-observe-max-age-oats-0311)).
|