@awebai/oats 0.26.0 → 0.27.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 +82 -51
- package/docs/capabilities.md +11 -11
- package/docs/capability-manifest.schema.json +16 -5
- package/docs/configuration.md +2 -2
- package/docs/conventions.md +2 -2
- package/docs/desktop-cli-api.md +70 -13
- package/docs/desktop-instance-start.md +2 -2
- package/docs/first-team.md +1 -1
- package/docs/implementation.md +3 -3
- package/docs/oats-local.schema.json +5 -4
- package/docs/packages.md +1 -1
- package/docs/release-notes/v0.27.0.md +100 -0
- package/docs/schedules.md +6 -6
- package/docs/servers.md +4 -4
- package/docs/souls-and-instances.md +6 -6
- package/injects/instance-boundary.md +1 -1
- package/lib/core.mjs +255 -207
- package/lib/deprecation.mjs +24 -0
- package/lib/instance-inspect.mjs +4 -3
- package/lib/process-group.mjs +1 -1
- package/lib/remote.mjs +1 -1
- package/lib/schedule.mjs +39 -20
- package/lib/servers.mjs +57 -25
- package/lib/workspace.mjs +7 -0
- package/package.json +1 -1
- package/packages/record/lib/session-roots.mjs +8 -6
- package/skills/soul-craft/SKILL.md +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -22,11 +22,12 @@ import { execFileSync, spawnSync } from "node:child_process";
|
|
|
22
22
|
import { homedir, tmpdir } from "node:os";
|
|
23
23
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { runtimeNameWarning, noteRuntimeName } from "../lib/deprecation.mjs";
|
|
25
26
|
import {
|
|
26
|
-
LAYERS, OATS_VERSION, manifestOperations,
|
|
27
|
+
LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
|
|
27
28
|
capabilityManifests, capabilityTrust, capabilityExecutablePath,
|
|
28
29
|
officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
|
|
29
|
-
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe,
|
|
30
|
+
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
30
31
|
} from "../lib/core.mjs";
|
|
31
32
|
import {
|
|
32
33
|
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages,
|
|
@@ -65,6 +66,16 @@ function valueFlag(name) {
|
|
|
65
66
|
return value;
|
|
66
67
|
}
|
|
67
68
|
const die = (msg) => { console.error(`oats: ${msg}`); process.exit(1); };
|
|
69
|
+
/** A command's harness: --harness, or --runtime, its pre-0.27 name (the released okf worker and
|
|
70
|
+
* a 0.26-era Desktop pass it) — read either, with the deprecation warning. Both, disagreeing,
|
|
71
|
+
* are refused. `get` reads one flag (the command's own reader where it has one). */
|
|
72
|
+
function harnessFlag(get = flag) {
|
|
73
|
+
const harness = get("harness"), runtime = get("runtime");
|
|
74
|
+
if (runtime === undefined) return harness;
|
|
75
|
+
if (harness !== undefined && harness !== runtime) cmdFail("E_BAD_ARGS", `--harness ${harness} and --runtime ${runtime} disagree; --runtime is the pre-0.27 name of --harness — give one`);
|
|
76
|
+
noteRuntimeName("the --runtime flag (use --harness)");
|
|
77
|
+
return runtime;
|
|
78
|
+
}
|
|
68
79
|
const cmdFail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
69
80
|
/** Resolve the --dir flag with central validation: a value-taking flag given
|
|
70
81
|
* no value (flag() → true) is E_BAD_ARGS inside the JSON boundary, never an
|
|
@@ -86,8 +97,25 @@ const JSON_MODE = args.includes("--json");
|
|
|
86
97
|
// Canonical absolute path of this CLI executable — the versioned OATS_CLI_BIN
|
|
87
98
|
// env contract for dispatched package commands (never resolved via PATH).
|
|
88
99
|
const CLI_BIN = realpathSync(fileURLToPath(import.meta.url));
|
|
89
|
-
|
|
90
|
-
const
|
|
100
|
+
// The one deprecated-name warning (lib/deprecation.mjs) rides the envelope, only when there is one.
|
|
101
|
+
const envelopeWarnings = () => { const w = runtimeNameWarning(); if (w) warningDelivered = true; return w ? { warnings: [w] } : {}; };
|
|
102
|
+
let warningDelivered = false;
|
|
103
|
+
/** A forwarded envelope keeps the host's warnings; this command's own deprecated-name
|
|
104
|
+
* note (a `--runtime` given here) joins the host's into the one warning. */
|
|
105
|
+
const withLocalWarnings = (envelope) => {
|
|
106
|
+
const mine = runtimeNameWarning();
|
|
107
|
+
if (!mine || !envelope || typeof envelope !== "object") return envelope;
|
|
108
|
+
warningDelivered = true;
|
|
109
|
+
const theirs = Array.isArray(envelope.warnings) ? envelope.warnings : [];
|
|
110
|
+
const same = theirs.find((w) => w?.code === mine.code);
|
|
111
|
+
if (!same) return { ...envelope, warnings: [...theirs, mine] };
|
|
112
|
+
const sources = [...new Set([...(Array.isArray(same.sources) ? same.sources : []), ...mine.sources])];
|
|
113
|
+
return { ...envelope, warnings: theirs.map((w) => (w === same ? { ...mine, sources, message: mine.message.replace(/\(.*\)/, `(${sources.join("; ")})`) } : w)) };
|
|
114
|
+
};
|
|
115
|
+
const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) }, ...envelopeWarnings() })); process.exit(1); };
|
|
116
|
+
const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result, ...envelopeWarnings() })); };
|
|
117
|
+
// Text mode (or a JSON answer printed before the read): the warning goes to stderr, never stdout.
|
|
118
|
+
process.on("exit", () => { const w = runtimeNameWarning(); if (w && !warningDelivered) process.stderr.write(`oats: warning: ${w.message}\n`); });
|
|
91
119
|
const formatBytes = (n) => n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KiB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MiB` : `${(n / 1024 ** 3).toFixed(1)} GiB`;
|
|
92
120
|
/** A retire recovery's copied outputs (untracked/ignored or directory work), named with their size. */
|
|
93
121
|
function preservedOutputLines(recovery) {
|
|
@@ -203,7 +231,7 @@ const INSPECT_TEXT_CAP = 256 * 1024;
|
|
|
203
231
|
/** The agents root a home belongs to, from its path alone:
|
|
204
232
|
* <root>/<agent>/instances/<instance>. */
|
|
205
233
|
function agentsRootOfHome(home) { return dirname(dirname(dirname(home))); }
|
|
206
|
-
const SOUL_FIELDS = ["
|
|
234
|
+
const SOUL_FIELDS = ["harness", "model", "yolo", "backend", "description", "launch-config"];
|
|
207
235
|
const realOrResolved = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
208
236
|
/** Every soul of a scope: the persistent souls of every agents root in
|
|
209
237
|
* scope, plus packaged souls (read-only). One enumeration for inspect and
|
|
@@ -293,7 +321,7 @@ function soulEntry(soul, root, { capability } = {}) {
|
|
|
293
321
|
declarationProblems: declared.problems,
|
|
294
322
|
name: soul.name, kind: packaged ? "capability" : (soul.kind || "persistent"), capability: capability || null,
|
|
295
323
|
type: soul.type ?? null, description: soul.description ?? null, repo: soul.repo ?? null, work: soul.work || "checkout",
|
|
296
|
-
|
|
324
|
+
harness: soul.harness || "pi", model: soul.model ?? null, yolo: soul.yolo === true || soul.yolo === "true" ? true : soul.yolo === false || soul.yolo === "false" ? false : null, launchConfig: soul["launch-config"] ?? null, backend: soul.backend ?? null,
|
|
297
325
|
agentsRoot: root, dir: packaged ? soulDir : dir, soulFile: join(soulDir, "soul.yaml"), instructionsFile: join(soulDir, "AGENTS.md"),
|
|
298
326
|
editable: packaged
|
|
299
327
|
? { fields: [], instructions: false, reason: `packaged soul from capability ${capability}: edit the package and update it; scoped bindings still apply through oats use` }
|
|
@@ -621,7 +649,7 @@ function serializeLaunchConfigs(map) {
|
|
|
621
649
|
const lines = ["launch-configs:"];
|
|
622
650
|
for (const name of names) {
|
|
623
651
|
const e = map[name];
|
|
624
|
-
lines.push(` ${name}:`, `
|
|
652
|
+
lines.push(` ${name}:`, ` harness: ${e.harness}`);
|
|
625
653
|
if (e.executable !== undefined) lines.push(` executable: ${yamlQuoted(e.executable)}`);
|
|
626
654
|
if (e.args?.length) { lines.push(" args:"); for (const a of e.args) lines.push(` - ${yamlQuoted(a)}`); }
|
|
627
655
|
const envNames = Object.keys(e.env || {}).sort();
|
|
@@ -635,9 +663,11 @@ function serializeLaunchConfigs(map) {
|
|
|
635
663
|
return lines.join("\n") + "\n";
|
|
636
664
|
}
|
|
637
665
|
/** Only the declared keys, in canonical order, from a validated entry. */
|
|
666
|
+
/** A configuration as `launch-config set` writes it: `harness` always — a `runtime` (the
|
|
667
|
+
* pre-0.27 name, read either) is written back under its new name (lead call 6). */
|
|
638
668
|
function normalizeLaunchConfig(e) {
|
|
639
669
|
return {
|
|
640
|
-
|
|
670
|
+
harness: Object.hasOwn(e, "harness") ? e.harness : e.runtime,
|
|
641
671
|
...(e.executable !== undefined ? { executable: e.executable } : {}),
|
|
642
672
|
...(e.args?.length ? { args: [...e.args] } : {}),
|
|
643
673
|
...(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 }])) } : {}),
|
|
@@ -659,7 +689,7 @@ function readLaunchConfigsModel(local) {
|
|
|
659
689
|
* `set --keep-env`. */
|
|
660
690
|
function publicLaunchConfig(e, extra = {}) {
|
|
661
691
|
const env = Object.fromEntries(Object.keys(e.env || {}).sort().map((n) => [n, typeof e.env[n] === "string" ? { redacted: true } : { fromEnv: e.env[n].fromEnv }]));
|
|
662
|
-
return {
|
|
692
|
+
return { harness: e.harness, executable: e.executable ?? null, args: [...(e.args || [])], env, model: e.model ?? null, yolo: e.yolo ?? null, ...extra };
|
|
663
693
|
}
|
|
664
694
|
/** The scope a launch-config command reads: --dir (or cwd), a running
|
|
665
695
|
* home's recorded context (--home), or a soul's own member context
|
|
@@ -692,16 +722,16 @@ function launchConfigContext(bail) {
|
|
|
692
722
|
* configuration, preflighted, read-only; environment values withheld and
|
|
693
723
|
* the prompt named, never the TASK body. */
|
|
694
724
|
function launchPreview(bail) {
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
if (sel.
|
|
725
|
+
for (const k of ["launch-config", "harness", "runtime", "model"]) if (flag(k) === true) bail("E_BAD_ARGS", `--${k} needs a value`);
|
|
726
|
+
const sel = { launchConfig: flag("launch-config"), harness: harnessFlag(), model: flag("model"), yolo: yoloFlag() };
|
|
727
|
+
if (sel.harness !== undefined && !LAUNCH_HARNESSES.includes(sel.harness)) bail("E_BAD_ARGS", `--harness must be one of ${LAUNCH_HARNESSES.join(", ")}`);
|
|
698
728
|
const { context, selected } = launchConfigContext(bail);
|
|
699
729
|
if (!selected) bail("E_BAD_ARGS", "preview needs --home <abs> (an existing instance) or --soul <name> [--dir <scope>] (a new instance)");
|
|
700
|
-
const selectionGiven = sel.launchConfig !== undefined || sel.
|
|
730
|
+
const selectionGiven = sel.launchConfig !== undefined || sel.harness !== undefined || sel.model !== undefined || sel.yolo !== undefined;
|
|
701
731
|
let meta = null, agentLike, home, instance;
|
|
702
732
|
if (selected.home) {
|
|
703
733
|
home = selected.home;
|
|
704
|
-
try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); } catch (e) { bail("E_HOME_UNKNOWN", `${home}: ${e.message}`); }
|
|
734
|
+
try { meta = upgradeHomeMeta(JSON.parse(readFileSync(join(home, "instance.json"), "utf8")), home); } catch (e) { bail("E_HOME_UNKNOWN", `${home}: ${e.message}`); }
|
|
705
735
|
instance = meta.instance || basename(home);
|
|
706
736
|
if (!(meta.launch && typeof meta.launch === "object") && !selectionGiven) {
|
|
707
737
|
// A home that predates recipes, asked nothing: its frozen command is
|
|
@@ -709,15 +739,15 @@ function launchPreview(bail) {
|
|
|
709
739
|
// (E_LAUNCH_LEGACY: re-spawn it from the deployment).
|
|
710
740
|
let d;
|
|
711
741
|
try { d = describeLaunchCommand(meta.command); } catch (e) { bail(e.code || "E_LAUNCH_COMMAND_UNSUPPORTED", e.message); }
|
|
712
|
-
jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null,
|
|
742
|
+
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 });
|
|
713
743
|
return;
|
|
714
744
|
}
|
|
715
745
|
const agentsRoot = agentsRootOfHome(home);
|
|
716
746
|
const agent = (() => { try { return findAgent(agentsRoot, meta.agent); } catch { return undefined; } })();
|
|
717
|
-
agentLike = agent || {
|
|
747
|
+
agentLike = agent || { harness: meta.harness, model: meta.model, yolo: meta.yolo };
|
|
718
748
|
} else {
|
|
719
749
|
const soul = scopeSouls(context).souls.find((x) => x.name === selected.soul && x.agentsRoot === selected.agentsRoot);
|
|
720
|
-
agentLike = {
|
|
750
|
+
agentLike = { harness: soul.harness, model: soul.model };
|
|
721
751
|
instance = `${soul.name}-<purpose>`; home = join(selected.agentsRoot, soul.name, "instances", instance);
|
|
722
752
|
}
|
|
723
753
|
// A home's recorded capabilities; a new instance's are its spawn's resolution,
|
|
@@ -731,13 +761,13 @@ function launchPreview(bail) {
|
|
|
731
761
|
const command = renderLaunchRecipe(recipe, { home, instance, redact: true });
|
|
732
762
|
const d = describeLaunchCommand(command);
|
|
733
763
|
const environment = d.environment.map((e) => e.reference && recipe.env[e.name]?.fromEnv ? { name: e.name, fromEnv: recipe.env[e.name].fromEnv } : e);
|
|
734
|
-
jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig,
|
|
764
|
+
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 });
|
|
735
765
|
}
|
|
736
766
|
async function launchConfigCmd() {
|
|
737
767
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
738
768
|
dropAmbientRoot();
|
|
739
769
|
const sub = args[1];
|
|
740
|
-
const usage = "usage: oats launch-config list [--dir <scope> | --home <abs> | --soul <name> [--dir <scope>] [--agents-root <abs>]] [--json] | set <name> --file <json> [--keep-env] [--dir <scope>] [--json] | remove <name> [--dir <scope>] [--json] | preview (--home <abs> | --soul <name> [--dir <scope>]) [--launch-config <name>|none] [--
|
|
770
|
+
const usage = "usage: oats launch-config list [--dir <scope> | --home <abs> | --soul <name> [--dir <scope>] [--agents-root <abs>]] [--json] | set <name> --file <json> [--keep-env] [--dir <scope>] [--json] | remove <name> [--dir <scope>] [--json] | preview (--home <abs> | --soul <name> [--dir <scope>]) [--launch-config <name>|none] [--harness r] [--model m] [--yolo|--no-yolo] --json";
|
|
741
771
|
if (sub === "preview") { launchPreview(bail); return; }
|
|
742
772
|
if (!["list", "set", "remove"].includes(sub)) bail("E_USAGE", usage);
|
|
743
773
|
const { context: dir, selected } = sub === "list" ? launchConfigContext(bail) : { context: dirFlag(), selected: null };
|
|
@@ -760,7 +790,7 @@ async function launchConfigCmd() {
|
|
|
760
790
|
if (!configurations.length) { console.log(`No launch configurations are declared${file ? ` in ${shortPath(file)}` : ` (no oats-local.yaml in reach of ${dir})`}`); return; }
|
|
761
791
|
for (const c of configurations) {
|
|
762
792
|
const env = Object.entries(c.env).map(([n, v]) => v.fromEnv ? `${n}=$${v.fromEnv}` : `${n}=<redacted>`).join(" ");
|
|
763
|
-
console.log(`${c.name}: ${c.
|
|
793
|
+
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}` : ""}`);
|
|
764
794
|
}
|
|
765
795
|
return;
|
|
766
796
|
}
|
|
@@ -777,7 +807,7 @@ async function launchConfigCmd() {
|
|
|
777
807
|
delete model[name];
|
|
778
808
|
} else {
|
|
779
809
|
const f = flag("file");
|
|
780
|
-
if (!f || f === true) bail("E_BAD_ARGS", "launch-config set needs --file <json> (an object with
|
|
810
|
+
if (!f || f === true) bail("E_BAD_ARGS", "launch-config set needs --file <json> (an object with harness and optional executable, args, env, model, yolo)");
|
|
781
811
|
let entry;
|
|
782
812
|
// A parse error is reported without the parser's text: its message can
|
|
783
813
|
// quote the document, and a definition may carry environment literals.
|
|
@@ -790,7 +820,7 @@ async function launchConfigCmd() {
|
|
|
790
820
|
} else {
|
|
791
821
|
try { raw = readFileSync(f, "utf8"); } catch (e) { bail("E_BAD_ARGS", `--file ${f}: ${e.code === "ENOENT" ? "no such file" : e.code || "cannot read"}`); }
|
|
792
822
|
}
|
|
793
|
-
try { entry = JSON.parse(raw); } catch { bail("E_BAD_ARGS", `--file ${f} is not valid JSON (one object with
|
|
823
|
+
try { entry = JSON.parse(raw); } catch { bail("E_BAD_ARGS", `--file ${f} is not valid JSON (one object with harness and optional executable, args, env, model, yolo)`); }
|
|
794
824
|
if (args.includes("--keep-env")) {
|
|
795
825
|
// An editor that saw only redacted values keeps the environment of the
|
|
796
826
|
// declared definition of that name: a one-time copy into the complete
|
|
@@ -801,6 +831,7 @@ async function launchConfigCmd() {
|
|
|
801
831
|
if (entry && typeof entry === "object" && Object.keys(current.env || {}).length) entry.env = { ...current.env };
|
|
802
832
|
}
|
|
803
833
|
try { validateLaunchConfig(name, entry, `--file ${f}`); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
|
|
834
|
+
if (!Object.hasOwn(entry, "harness")) noteRuntimeName(`runtime in the --file definition (written as harness)`);
|
|
804
835
|
model[name] = normalizeLaunchConfig(entry);
|
|
805
836
|
}
|
|
806
837
|
let next;
|
|
@@ -1389,7 +1420,7 @@ async function status() {
|
|
|
1389
1420
|
if (s) i.soul = { repoKey: s.repoKey, commit: s.commit, current: s.current?.commit ?? null, status: s.status, ...(s.reason ? { reason: s.reason } : {}) };
|
|
1390
1421
|
}
|
|
1391
1422
|
}
|
|
1392
|
-
console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}) }, null, 2)); return;
|
|
1423
|
+
console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}), ...envelopeWarnings() }, null, 2)); return;
|
|
1393
1424
|
}
|
|
1394
1425
|
console.log(`oats status — agents root ${shortPath(root)}\n`);
|
|
1395
1426
|
if (ws?.unreachable) console.log(` workspace: unreachable (${ws.unreachable.reason}) — drift unknown\n`);
|
|
@@ -1427,7 +1458,7 @@ async function spawnCmd() {
|
|
|
1427
1458
|
};
|
|
1428
1459
|
checkDirectoryOptions(requestedWork); // before anything is resolved or written
|
|
1429
1460
|
const name = args[1];
|
|
1430
|
-
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>|--name <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--
|
|
1461
|
+
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>|--name <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--no-launch] [--json]");
|
|
1431
1462
|
// Retired boundary flags (maintainer transport ruling): fail LOUDLY before
|
|
1432
1463
|
// ANY side effect, including root discovery.
|
|
1433
1464
|
// Local souls (local-agents/) are gone with the workspace model: a soul is a member
|
|
@@ -1650,12 +1681,12 @@ async function spawnCmd() {
|
|
|
1650
1681
|
// An attached instance's repository is its work tree owner's (derived by the kernel).
|
|
1651
1682
|
repo: preparedRepo !== undefined ? preparedRepo : ["directory", "attached"].includes(requestedWork || agent.work)
|
|
1652
1683
|
? repo : repo || defaultRepo(workspaceOf(root)) || defaultRepo(process.cwd()),
|
|
1653
|
-
work: requestedWork, workDir,
|
|
1684
|
+
work: requestedWork, workDir, harness: harnessFlag(), backend, herdrSocket, yolo, model: flag("model"), branch,
|
|
1654
1685
|
launchConfig: valueFlag("launch-config"),
|
|
1655
1686
|
launch: !args.includes("--no-launch"),
|
|
1656
1687
|
// K6: --preview decides everything and touches nothing; --base <ref>
|
|
1657
1688
|
// selects a worktree's start point; --model @native-default is the
|
|
1658
|
-
// explicit "
|
|
1689
|
+
// explicit "harness's own default" (distinct from omitting --model).
|
|
1659
1690
|
...(args.includes("--preview") ? { preview: true, subject: { soul: name, agentsRoot: agentsRootFlag !== undefined ? String(agentsRootFlag) : null, dir: flag("dir") !== undefined && flag("dir") !== true ? String(flag("dir")) : null } } : {}),
|
|
1660
1691
|
...(flag("base") !== undefined && flag("base") !== true ? { baseRef: flag("base") } : {}),
|
|
1661
1692
|
// A confirmed preview binds this apply (K6b): drift → E_DECISION_STALE, nothing created.
|
|
@@ -1669,7 +1700,7 @@ async function spawnCmd() {
|
|
|
1669
1700
|
// (the deployment's cache had no entry for its commit): the result says so.
|
|
1670
1701
|
if (prepared) r.soulFetched = soulFetched;
|
|
1671
1702
|
if (JSON_MODE) { jsonOk(r); return; }
|
|
1672
|
-
console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""})
|
|
1703
|
+
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)" : ""}`);
|
|
1673
1704
|
return;
|
|
1674
1705
|
}
|
|
1675
1706
|
} catch (e) {
|
|
@@ -1679,9 +1710,9 @@ async function spawnCmd() {
|
|
|
1679
1710
|
// document the message already names. The shared boundary renders it.
|
|
1680
1711
|
if (TYPED_CLI_FAILURES.has(e?.code)) throw e;
|
|
1681
1712
|
// A launch refusal (configuration, executable, environment reference,
|
|
1682
|
-
// model,
|
|
1713
|
+
// model, harness) is a fact about the selection, not a spawn-mechanism
|
|
1683
1714
|
// failure: it keeps its own code so a GUI can act on it.
|
|
1684
|
-
if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^
|
|
1715
|
+
if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_HARNESS$/.test(e.code)) { bail(e.code, e.message); throw e; }
|
|
1685
1716
|
// An unmet declared requirement is a fact about the soul's configuration
|
|
1686
1717
|
// (with a remedy), not a spawn-mechanism failure: keep its code and details.
|
|
1687
1718
|
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; }
|
|
@@ -1716,7 +1747,7 @@ async function spawnCmd() {
|
|
|
1716
1747
|
instance: r.instance, agent: r.agent, home: r.home, work: r.work,
|
|
1717
1748
|
branch: r.branch || null, launched: r.launched, warnings: r.warnings || [],
|
|
1718
1749
|
...(wakeSchedule ? { wakeSchedule } : {}), ...(wakeScheduleError ? { wakeScheduleError } : {}),
|
|
1719
|
-
tmux: r.tmux || null, repo: r.repo || null,
|
|
1750
|
+
tmux: r.tmux || null, repo: r.repo || null, harness: r.harness || null,
|
|
1720
1751
|
model: r.model || null, parent: r.parentInstance || null,
|
|
1721
1752
|
sibling: r.siblingInstance || null, relation: r.relation || null,
|
|
1722
1753
|
spawnOrigin: r.spawnOrigin, attach: r.attach,
|
|
@@ -1809,7 +1840,7 @@ function retireCmd() {
|
|
|
1809
1840
|
if (r.deferred) {
|
|
1810
1841
|
if (args.includes("--json")) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
1811
1842
|
console.log(`Retirement of ${r.retired} (agent ${r.agent}) is ${r.alreadyScheduled ? "already " : ""}scheduled — say any goodbyes now.`);
|
|
1812
|
-
console.log(` in ~${r.completesInSec}s a detached completion quiesces this
|
|
1843
|
+
console.log(` in ~${r.completesInSec}s a detached completion quiesces this harness (that is what ends this window), preserves work, runs retire hooks and removes the home`);
|
|
1813
1844
|
console.log(` if the completion fails, this window stays, the failure shows in \`oats status\` and at ${shortPath(r.resultPath)}, and \`oats retire ${r.retired}\` retries it`);
|
|
1814
1845
|
return;
|
|
1815
1846
|
}
|
|
@@ -1916,9 +1947,9 @@ async function sessionCmd() {
|
|
|
1916
1947
|
if (model === true) bad("--model needs a model id; omit it to keep the recorded model");
|
|
1917
1948
|
const launchConfig = flag("launch-config");
|
|
1918
1949
|
if (launchConfig === true) bad("--launch-config needs a configuration name, or none");
|
|
1919
|
-
const
|
|
1920
|
-
if (
|
|
1921
|
-
const opts = { model: model || undefined, launchConfig,
|
|
1950
|
+
const harness = harnessFlag();
|
|
1951
|
+
if (harness === true || (harness !== undefined && !LAUNCH_HARNESSES.includes(harness))) bad(`--harness must be one of ${LAUNCH_HARNESSES.join(", ")}`);
|
|
1952
|
+
const opts = { model: model || undefined, launchConfig, harness, yolo: yoloFlag(), env: process.env, ...(await homeLiveTeams(home)) };
|
|
1922
1953
|
if (args[1] === "restart") {
|
|
1923
1954
|
const grace = flag("stop-grace");
|
|
1924
1955
|
if (grace !== undefined) { if (grace === true || !/^\d+$/.test(String(grace)) || Number(grace) < 1 || Number(grace) > 300) bad("--stop-grace needs a number of seconds (1..300) to wait for the harness after SIGTERM"); opts.stopGraceMs = Number(grace) * 1000; }
|
|
@@ -1941,7 +1972,7 @@ async function sessionCmd() {
|
|
|
1941
1972
|
const file = flag("file");
|
|
1942
1973
|
if (!file || file === true) throw Object.assign(new Error("session upload needs --file <local path>"), { code: "E_BAD_ARGS" });
|
|
1943
1974
|
result = uploadAttachment({ file, home: home === true ? undefined : home });
|
|
1944
|
-
} else throw Object.assign(new Error("usage: oats session inspect|input|attach|start|restart|receive|upload --home /absolute/home [--text-file path] [--model id] [--launch-config name|none] [--
|
|
1975
|
+
} else throw Object.assign(new Error("usage: oats session inspect|input|attach|start|restart|receive|upload --home /absolute/home [--text-file path] [--model id] [--launch-config name|none] [--harness pi|claude|codex] [--yolo|--no-yolo] [--stop-grace seconds] [--name file] [--file path] [--json]"), { code: "E_BAD_ARGS" });
|
|
1945
1976
|
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
1946
1977
|
} catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message, e.details); }
|
|
1947
1978
|
}
|
|
@@ -2325,7 +2356,7 @@ function versionCmd() {
|
|
|
2325
2356
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2326
2357
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2327
2358
|
// never listed.
|
|
2328
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1,
|
|
2359
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], 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", "teams", "settings-declared", "capabilities-private", "layers-from", "harness"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
2329
2360
|
return;
|
|
2330
2361
|
}
|
|
2331
2362
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -2470,7 +2501,7 @@ async function serverRouteCmd() {
|
|
|
2470
2501
|
const value = (name) => { const v = flag(name); if (v === true) bail("E_BAD_ARGS", `--${name} needs a value`); return v; };
|
|
2471
2502
|
if (!["list", "set", "remove", "preview"].includes(action)) bail("E_BAD_ARGS", "launch-config --server supports list, set, remove and preview");
|
|
2472
2503
|
const options = { action, name: args[2], context: value("dir"), home: value("home"), instance: value("instance"), soul: value("soul"), agentsRoot: value("agents-root") };
|
|
2473
|
-
if (action === "preview") Object.assign(options, { launchConfig: value("launch-config"),
|
|
2504
|
+
if (action === "preview") Object.assign(options, { launchConfig: value("launch-config"), harness: harnessFlag(value), model: value("model"), yolo: yoloFlag() });
|
|
2474
2505
|
if (action === "set") {
|
|
2475
2506
|
const file = value("file");
|
|
2476
2507
|
if (!file) bail("E_BAD_ARGS", "launch-config set needs --file <local JSON file> (or - for stdin)");
|
|
@@ -2489,7 +2520,7 @@ async function serverRouteCmd() {
|
|
|
2489
2520
|
let out;
|
|
2490
2521
|
try { out = launchConfigRemote(id, options); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
2491
2522
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
2492
|
-
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2523
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2493
2524
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "launch configuration request failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
2494
2525
|
console.log(JSON.stringify(out.envelope.result, null, 2));
|
|
2495
2526
|
return;
|
|
@@ -2506,7 +2537,7 @@ async function serverRouteCmd() {
|
|
|
2506
2537
|
let routed;
|
|
2507
2538
|
try { routed = routeCommand(id, "harvest", [inst]); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
2508
2539
|
if (routed.stderr?.trim()) process.stderr.write(routed.stderr.endsWith("\n") ? routed.stderr : routed.stderr + "\n");
|
|
2509
|
-
if (JSON_MODE) { console.log(JSON.stringify(routed.envelope, null, 2)); if (!routed.envelope.ok) process.exit(1); return; }
|
|
2540
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(routed.envelope), null, 2)); if (!routed.envelope.ok) process.exit(1); return; }
|
|
2510
2541
|
if (!routed.envelope.ok) die(`${id}: ${routed.envelope.error?.message || "harvest failed"} (${routed.envelope.error?.code || "E_REMOTE"})`);
|
|
2511
2542
|
const hr = routed.envelope.result;
|
|
2512
2543
|
console.log(`Harvest on ${id} for ${inst}: ${hr.harvest}${hr.reason ? ` (${hr.reason})` : ""}${hr.instance && hr.harvest === "spawned" ? ` — harvester ${hr.instance}` : ""}`);
|
|
@@ -2534,7 +2565,7 @@ async function serverRouteCmd() {
|
|
|
2534
2565
|
let out;
|
|
2535
2566
|
try { out = scheduleRemote(id, rest); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
2536
2567
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
2537
|
-
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2568
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2538
2569
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "schedule command failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
2539
2570
|
console.log(JSON.stringify(out.envelope.result, null, 2));
|
|
2540
2571
|
return;
|
|
@@ -2547,7 +2578,7 @@ async function serverRouteCmd() {
|
|
|
2547
2578
|
let out;
|
|
2548
2579
|
try { out = inspectRemote(id, addr); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
2549
2580
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
2550
|
-
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2581
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2551
2582
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "inspect failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
2552
2583
|
const r = out.envelope.result;
|
|
2553
2584
|
console.log(`${r.instance || r.home} on ${id}: ${r.present ? `present, ${r.state || "unknown"}` : "not present"}${r.backend ? ` (${r.backend})` : ""}`);
|
|
@@ -2555,12 +2586,12 @@ async function serverRouteCmd() {
|
|
|
2555
2586
|
}
|
|
2556
2587
|
if (args[1] === "start" || args[1] === "restart") {
|
|
2557
2588
|
const value = (name) => { const v = flag(name); if (v === true) bail("E_BAD_ARGS", `--${name} needs a value`); return v; };
|
|
2558
|
-
const choices = { ...addr, model: value("model"), launchConfig: value("launch-config"),
|
|
2589
|
+
const choices = { ...addr, model: value("model"), launchConfig: value("launch-config"), harness: harnessFlag(value), yolo: yoloFlag() };
|
|
2559
2590
|
if (flag("stop-grace") !== undefined) bail("E_BAD_ARGS", "--stop-grace is currently supported on the execution host; omit it to use the remote restart's default wait");
|
|
2560
2591
|
let out;
|
|
2561
2592
|
try { out = (args[1] === "restart" ? restartRemote : startRemote)(id, choices); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
2562
2593
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
2563
|
-
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2594
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2564
2595
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "start failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
2565
2596
|
const r = out.envelope.result;
|
|
2566
2597
|
console.log(`Started ${r.instance || r.home} on ${id} (${r.backend}${r.model ? `, model ${r.model}` : ""}, ${r.reused === "pane" ? "in its existing pane" : r.reused === "adopted" ? "adopted the pending session" : "new window"})`);
|
|
@@ -2626,7 +2657,7 @@ async function serverRouteCmd() {
|
|
|
2626
2657
|
catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
2627
2658
|
const { envelope, stderr } = routed;
|
|
2628
2659
|
if (stderr && stderr.trim()) process.stderr.write(stderr.endsWith("\n") ? stderr : stderr + "\n");
|
|
2629
|
-
if (JSON_MODE) { console.log(JSON.stringify(envelope, null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
|
|
2660
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(envelope), null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
|
|
2630
2661
|
if (!envelope.ok && !(cmd === "retire" && envelope.result)) die(`${id}: ${envelope.error?.message || "remote command failed"} (${envelope.error?.code || "E_REMOTE"})`);
|
|
2631
2662
|
const r = envelope.result;
|
|
2632
2663
|
const target = r.target || {};
|
|
@@ -2839,7 +2870,7 @@ Usage:
|
|
|
2839
2870
|
<file> [--json] upload's remote half)
|
|
2840
2871
|
oats session start --home <absolute-home> start a STOPPED instance again in its existing home
|
|
2841
2872
|
[--model m] [--launch-config n|none] (recorded recipe as is; a selection re-resolves it
|
|
2842
|
-
[--
|
|
2873
|
+
[--harness r] [--yolo|--no-yolo] against the scope; a named configuration is a unit)
|
|
2843
2874
|
oats session restart --home <abs-home> stop the running harness (SIGTERM, bounded wait,
|
|
2844
2875
|
[same flags] [--stop-grace <s>] never escalated) and start it again in place under
|
|
2845
2876
|
the same lock; a stop that is not observed is
|
|
@@ -2854,7 +2885,7 @@ Usage:
|
|
|
2854
2885
|
[--relative-to <instance>] new instance to an existing one; --parent X
|
|
2855
2886
|
[--relative-root <agents-root>] disambiguates same-named team anchors
|
|
2856
2887
|
[--work worktree|checkout|attached|workspace|directory] = sugar for --relative-to X --relation
|
|
2857
|
-
[--work-dir <owner-work>] [--
|
|
2888
|
+
[--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
|
|
2858
2889
|
[--no-launch] [--json]
|
|
2859
2890
|
with team: declared, unknown souls
|
|
2860
2891
|
resolve across the team scope's repos
|
|
@@ -2868,7 +2899,7 @@ Usage:
|
|
|
2868
2899
|
[--keep-dir] [--json] CALLING instance: the window dies, then
|
|
2869
2900
|
a detached external retirement runs
|
|
2870
2901
|
oats inspect [--dir <scope>] [--soul <name> one authoritative JSON answer for a GUI: souls
|
|
2871
|
-
[--agents-root <abs>]] [--home <abs>] (
|
|
2902
|
+
[--agents-root <abs>]] [--home <abs>] (harness defaults, editability, instructions),
|
|
2872
2903
|
[--json] installed capabilities with health, effective
|
|
2873
2904
|
layer bindings and activation, declared
|
|
2874
2905
|
operations with availability; --home answers the
|
|
@@ -2882,7 +2913,7 @@ Usage:
|
|
|
2882
2913
|
relayed; a view answers {documents: [...]}
|
|
2883
2914
|
oats launch-config list [--dir <scope> named launch configurations effective at a scope,
|
|
2884
2915
|
| --home <abs> | --soul <name>] a home's recorded context or a soul's own scope:
|
|
2885
|
-
[--agents-root <abs>] [--json]
|
|
2916
|
+
[--agents-root <abs>] [--json] harness, executable, args, env (values redacted,
|
|
2886
2917
|
references shown), model, yolo; the closest
|
|
2887
2918
|
declaring scope provides the whole entry
|
|
2888
2919
|
oats launch-config set <name> --file <j> declare or replace one at this scope from a JSON
|
|
@@ -2890,11 +2921,11 @@ Usage:
|
|
|
2890
2921
|
--keep-env copies the effective definition's env)
|
|
2891
2922
|
oats launch-config remove <name> remove this scope's declaration; an ancestor's,
|
|
2892
2923
|
[--dir <scope>] [--json] if any, becomes effective again
|
|
2893
|
-
oats launch-config preview what a start would run: resolved
|
|
2924
|
+
oats launch-config preview what a start would run: resolved harness, model,
|
|
2894
2925
|
(--home <abs> | --soul <name>) yolo, executable, argv, environment (redacted),
|
|
2895
2926
|
[--launch-config <name>|none] command and preflight; read-only, nothing
|
|
2896
|
-
[--
|
|
2897
|
-
[--yolo | --no-yolo] --json a disagreeing --
|
|
2927
|
+
[--harness r] [--model m] started; a named configuration is a unit, so
|
|
2928
|
+
[--yolo | --no-yolo] --json a disagreeing --harness is refused
|
|
2898
2929
|
oats doctor [dir] [--soul <name>] [--json] resolved targets, trust, requirements;
|
|
2899
2930
|
--soul shows final composed AGENTS.md
|
|
2900
2931
|
oats update [--check] [--yes] check npm for a newer kernel+pi bridge and
|
package/docs/capabilities.md
CHANGED
|
@@ -47,7 +47,7 @@ A self-contained package has an `oats.json`:
|
|
|
47
47
|
"requires": [
|
|
48
48
|
{ "command": "team-chat", "why": "send and receive messages" },
|
|
49
49
|
{
|
|
50
|
-
"
|
|
50
|
+
"harness": "pi",
|
|
51
51
|
"package": "npm:team-chat-pi",
|
|
52
52
|
"why": "real-time push events in pi sessions"
|
|
53
53
|
}
|
|
@@ -134,15 +134,15 @@ A self-contained package has an `oats.json`:
|
|
|
134
134
|
the operator to clean up by hand.
|
|
135
135
|
- `requires` declares what must exist before the capability works. Two kinds:
|
|
136
136
|
- a **host command** (`command`), satisfied by a binary on `PATH`;
|
|
137
|
-
- a **
|
|
138
|
-
satisfied by that
|
|
137
|
+
- a **harness package** (`harness` + `package`, optionally `marketplace`),
|
|
138
|
+
satisfied by that harness's own package manager — `npm:@scope/name` for pi,
|
|
139
139
|
`plugin@marketplace` for Claude Code. It is raised only for deployments that use the named
|
|
140
|
-
|
|
141
|
-
and is verified in the
|
|
140
|
+
harness — a Claude-only deployment is never asked to install a pi package —
|
|
141
|
+
and is verified in the harness's package list, never on `PATH`. A version
|
|
142
142
|
selector is allowed and ignored for identity, so `@latest` and a pinned
|
|
143
143
|
version are one requirement.
|
|
144
|
-
A
|
|
145
|
-
would mutate the operator's
|
|
144
|
+
A harness package is **verified at spawn, never installed there**: installing
|
|
145
|
+
would mutate the operator's harness configuration without asking, in the
|
|
146
146
|
middle of a spawn. A missing, uninstalled or disabled package fails the spawn
|
|
147
147
|
with the consent command that fixes it.
|
|
148
148
|
- OATS never installs a host requirement silently. A missing host command is
|
|
@@ -250,7 +250,7 @@ team: [engineering, reviewers]
|
|
|
250
250
|
discovery warning (`unmapped-team-label`, one per label naming its souls); a
|
|
251
251
|
label not in `teams:` at all is the `E_TEAM_UNKNOWN` problem.
|
|
252
252
|
|
|
253
|
-
## Exact
|
|
253
|
+
## Exact harness composition
|
|
254
254
|
|
|
255
255
|
Every spawned instance receives:
|
|
256
256
|
|
|
@@ -334,7 +334,7 @@ existing manifests load, and change nothing.
|
|
|
334
334
|
Hooks receive `OATS_EVENT`, `OATS_CAPABILITY`, `OATS_LAYER`, `OATS_INSTANCE`,
|
|
335
335
|
`OATS_HOME`, `OATS_AGENT`, `OATS_SOUL`, `OATS_CONTEXT`, `OATS_WORKSPACE`,
|
|
336
336
|
`OATS_ROOT`, `OATS_LEVEL`, `OATS_SETTINGS`, and `OATS_META`. A final JSON line may
|
|
337
|
-
return `meta`, `brief`, `warning`, or
|
|
337
|
+
return `meta`, `brief`, `warning`, or harness-specific `launch` arguments. A
|
|
338
338
|
**spawn hook only** may also return an `env` object for the launched process;
|
|
339
339
|
returning `env` from retire or soul-scaffold is an explicit contract error.
|
|
340
340
|
|
|
@@ -378,7 +378,7 @@ boundary — adding a new launch variable requires a visible manifest change
|
|
|
378
378
|
`OATS_*`, `PI_AGENT_*`, kernel launch variables, and known shell/bootstrap/loader
|
|
379
379
|
names are also rejected as defense in depth. The denylist includes current Node,
|
|
380
380
|
JVM, .NET, Python, Perl, Ruby, Lua, PHP, ELF, and dyld surfaces, but is explicitly
|
|
381
|
-
not the authority boundary:
|
|
381
|
+
not the authority boundary: harness bootstrap names are open-ended, so the
|
|
382
382
|
manifest declaration and trust review enforce what an artifact may contribute.
|
|
383
383
|
Two capabilities claiming the same name is an error even when their values
|
|
384
384
|
match.
|
|
@@ -394,7 +394,7 @@ without a retire hook uses the standard retryable quarantine instead. Ordinary
|
|
|
394
394
|
advisory hook execution failure itself contributes no environment.
|
|
395
395
|
|
|
396
396
|
The environment prefix applies to the initial Pi or Claude process. `--no-launch`
|
|
397
|
-
validates command preparation but
|
|
397
|
+
validates command preparation but launches no harness. The fallback shell
|
|
398
398
|
after that process exits does not inherit command-scoped assignments, and OATS
|
|
399
399
|
has no restart command or replay policy yet. The generated command is persisted
|
|
400
400
|
as before; hooks must contribute locators, selectors, or broker endpoints—not
|
|
@@ -117,17 +117,28 @@
|
|
|
117
117
|
"additionalProperties": false
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
|
-
"title": "
|
|
121
|
-
"description": "A package that must be installed into a specific
|
|
120
|
+
"title": "harness package requirement",
|
|
121
|
+
"description": "A package that must be installed into a specific harness's own package manager. Raised only for deployments using that harness, satisfied by the harness's package list rather than by PATH, and installed only with explicit consent. The harness is named by `harness` (0.27.0) or by `runtime`, its pre-0.27 name that released manifests use: exactly one of the two.",
|
|
122
122
|
"type": "object",
|
|
123
123
|
"required": [
|
|
124
|
-
"runtime",
|
|
125
124
|
"package",
|
|
126
125
|
"why"
|
|
127
126
|
],
|
|
127
|
+
"oneOf": [
|
|
128
|
+
{ "required": ["harness"], "not": { "required": ["runtime"] } },
|
|
129
|
+
{ "required": ["runtime"], "not": { "required": ["harness"] } }
|
|
130
|
+
],
|
|
128
131
|
"properties": {
|
|
132
|
+
"harness": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"enum": [
|
|
135
|
+
"pi",
|
|
136
|
+
"claude"
|
|
137
|
+
]
|
|
138
|
+
},
|
|
129
139
|
"runtime": {
|
|
130
140
|
"type": "string",
|
|
141
|
+
"description": "The pre-0.27 name of `harness`, accepted for released manifests.",
|
|
131
142
|
"enum": [
|
|
132
143
|
"pi",
|
|
133
144
|
"claude"
|
|
@@ -135,7 +146,7 @@
|
|
|
135
146
|
},
|
|
136
147
|
"package": {
|
|
137
148
|
"type": "string",
|
|
138
|
-
"description": "Source spec in that
|
|
149
|
+
"description": "Source spec in that harness's own naming: \"npm:@scope/name\" for pi, \"plugin@marketplace\" for Claude."
|
|
139
150
|
},
|
|
140
151
|
"why": {
|
|
141
152
|
"type": "string"
|
|
@@ -155,7 +166,7 @@
|
|
|
155
166
|
},
|
|
156
167
|
"minVersion": {
|
|
157
168
|
"type": "string",
|
|
158
|
-
"description": "Lowest acceptable installed version of the package, read from the package.json under the install directory the
|
|
169
|
+
"description": "Lowest acceptable installed version of the package, read from the package.json under the install directory the harness's listing names; an older or absent manifest fails the requirement with the install remedy.",
|
|
159
170
|
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+"
|
|
160
171
|
},
|
|
161
172
|
"ifInstalled": {
|
package/docs/configuration.md
CHANGED
|
@@ -37,7 +37,7 @@ souls: # optional — souls this machine d
|
|
|
37
37
|
|
|
38
38
|
launch-configs: # optional — named ways this host starts a harness
|
|
39
39
|
personal:
|
|
40
|
-
|
|
40
|
+
harness: claude
|
|
41
41
|
executable: "./bin/claude-wrapper.sh" # relative → against this deployment directory
|
|
42
42
|
args: ["--verbose"]
|
|
43
43
|
env:
|
|
@@ -55,7 +55,7 @@ refused (`E_WORKSPACE_SCHEMA`).
|
|
|
55
55
|
| `clones` | `<canonical repo key>: <absolute path>` — where a member's clone lives when it is not at `<deployment>/<member name>/`. Only a soul's **work target** (`work: worktree \| checkout`) needs a clone. Lookup order: `spawn --repo`, then this map (keys normalised through `parseRepoRef`, so any ref spelling of the same repo matches), then `<deployment>/<member name>` (a member named `agents` → `<deployment>/agents-repo`, since `agents/` is the instance root); none → `E_CLONE_MISSING`; a directory whose `origin` is another repo → `E_CLONE_MISMATCH`. |
|
|
56
56
|
| `settings.<cap>.<key>` | Host-owned provider values the capability's manifest asks for — absolute paths, state roots, delivery modes. The workspace file **refuses** absolute paths; this is where they go. Merged into the capability's provider payload after the soul's own payload and before any `--provider` flag (see [three homes](workspaces.md#provider-payloads-have-three-homes)). |
|
|
57
57
|
| `souls.disabled` | Soul names not run on this machine; reported by `oats sync` ("disabled here"). |
|
|
58
|
-
| `launch-configs.<name>` | A named way to start a harness on this host (0.26.0; lead decision 2 — a spawn-time host choice, never a soul field): `
|
|
58
|
+
| `launch-configs.<name>` | A named way to start a harness on this host (0.26.0; lead decision 2 — a spawn-time host choice, never a soul field): `harness` (`pi` \| `claude` \| `codex`, required; named `runtime` before 0.27.0, which is still read with a `deprecated-runtime-name` warning), `executable` (a bare name looked up on `PATH`, or a path — relative to this deployment directory), `args` (literal, no shell), `env` (a literal string, non-secret by contract and always redacted, or `{ fromEnv: NAME }` resolved on the host at start), `model`, `yolo`. Selected with `--launch-config <name>` on `oats spawn` and `oats session start \| restart`; explicit flags override its fields. Written by `oats launch-config set <name> --file <json>` / `remove <name>`, which rewrite only this block. Earlier kernels read `launch-configs:` from a scope's `oats-config.yaml`; 0.26.0 refuses it there with a message naming this move. |
|
|
59
59
|
|
|
60
60
|
## Where it sits and how it is found
|
|
61
61
|
|
package/docs/conventions.md
CHANGED
|
@@ -48,8 +48,8 @@ Pi with ambient skill and context discovery disabled and the one instance path
|
|
|
48
48
|
explicit; that exclusion is gone.)* Claude runs provider-native: it reads the
|
|
49
49
|
instance's `.claude/skills` and `CLAUDE.md` symlinks, and the operator's own
|
|
50
50
|
user and project configuration — skills, plugins, settings — stays in effect.
|
|
51
|
-
Neither
|
|
52
|
-
`composition.materialized.
|
|
51
|
+
Neither harness gets a redirected config home.
|
|
52
|
+
`composition.materialized.harnessPosture` in `instance.json` records what each
|
|
53
53
|
instance actually exposes. `oats-getting-started` is the sole pre-workspace
|
|
54
54
|
ambient bootstrap.
|
|
55
55
|
|