@awebai/oats 0.22.1 → 0.22.2
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/README.md +10 -3
- package/bin/oats.mjs +231 -16
- package/capabilities/oats-aweb/injects/aweb.md +4 -3
- package/capabilities/oats-aweb/oats.json +1 -1
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +3 -1
- package/capabilities/oats-okf/bin/oats-okf.mjs +125 -9
- package/capabilities/oats-okf/injects/okf.md +7 -0
- package/capabilities/oats-okf/oats.json +2 -2
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +42 -1
- package/docs/execution-targets.md +181 -0
- package/docs/implementation.md +14 -1
- package/docs/migration-from-oas.md +1 -1
- package/docs/oats-config.schema.json +1 -0
- package/docs/operating-team-migration.md +217 -0
- package/docs/release-notes/v0.22.2.md +69 -0
- package/docs/servers.md +94 -0
- package/docs/souls-and-instances.md +30 -3
- package/lib/core.mjs +379 -60
- package/lib/herdr.mjs +95 -0
- package/lib/servers.mjs +436 -0
- package/lib/session-input.mjs +78 -0
- package/lib/session-viewer.mjs +51 -0
- package/package-catalog.json +2 -2
- package/package.json +1 -1
- package/packages/record/bin/capture.mjs +59 -3
- package/packages/record/bin/recall.mjs +67 -1
- package/packages/record/lib/sessions-for-home.mjs +130 -0
- package/skills/oats/SKILL.md +6 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ specialist, a maintainer, a reviewer, a package owner, or any other role, each
|
|
|
14
14
|
with a precise curriculum, durable knowledge, and a full provider-native
|
|
15
15
|
session you can enter and steer.
|
|
16
16
|
|
|
17
|
-
OATS
|
|
17
|
+
OATS launches **Pi**, **Claude Code**, and **Codex**. A team may mix providers and models
|
|
18
18
|
while sharing the same souls, package and config contracts, instance
|
|
19
19
|
lifecycle, and coordination topology. On machines where `oats setup` has run,
|
|
20
20
|
the append-only, searchable **turn record** captures supported local transcripts
|
|
@@ -45,7 +45,7 @@ designs.
|
|
|
45
45
|
instantiated many times without losing its identity or accumulated
|
|
46
46
|
expertise.
|
|
47
47
|
- **Instances are real sessions, not hidden subagent calls.** Each instance is
|
|
48
|
-
a disposable incarnation with a full Pi
|
|
48
|
+
a disposable incarnation with a full Pi, Claude Code, or Codex session hosted in
|
|
49
49
|
tmux, an explicit task, its own home, and a repository or workspace view.
|
|
50
50
|
You can attach to it, steer it, message it, stop it, and inspect exactly
|
|
51
51
|
what it received.
|
|
@@ -145,7 +145,8 @@ template discovery curtailed while operator-configured extensions remain
|
|
|
145
145
|
enabled. Claude Code keeps the operator's settings, skills, plugins, MCP,
|
|
146
146
|
hooks, and memory, and OATS adds its canonical composed resources. The
|
|
147
147
|
guarantee is an exact OATS-managed curriculum, not identical ambient behavior
|
|
148
|
-
across providers.
|
|
148
|
+
across providers. Codex uses native instructions, skills and approval settings;
|
|
149
|
+
its launch support currently requires agents to check `aw` themselves for new messages.
|
|
149
150
|
|
|
150
151
|
### Configuration and layers
|
|
151
152
|
|
|
@@ -361,3 +362,9 @@ AGENTS.md, Agent Skills, and OKF.
|
|
|
361
362
|
## License
|
|
362
363
|
|
|
363
364
|
[MIT](LICENSE) © 2026 OATS Framework
|
|
365
|
+
|
|
366
|
+
Session backends and unattended launches are described in
|
|
367
|
+
[execution targets](docs/execution-targets.md). Use `oats spawn <soul> --backend
|
|
368
|
+
herdr --yolo` for a Herdr-hosted unattended Codex/Claude session, or put
|
|
369
|
+
`yolo: true` in the scope's oats-config.yaml. Aweb owns shared event delivery;
|
|
370
|
+
terminal transport alone does not enable a messaging broker.
|
package/bin/oats.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
|
|
32
32
|
findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, listCapabilityAgents, workspaceOf,
|
|
33
33
|
ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
|
|
34
|
-
spawnInstance, retireInstance, upsertLocalAgent, defaultRepo, RELATIONS,
|
|
34
|
+
spawnInstance, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS,
|
|
35
35
|
} from "../lib/core.mjs";
|
|
36
36
|
import {
|
|
37
37
|
aggregateMissingRequirements, applyFromOasScope, beginRunJournal, discoverMigrationScopes, discoverOasScopes, discoverWorkspaceScopes, planFromOasScope,
|
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
assertNoSymlinkedParents, copyFileAtomic, writeFileAtomic,
|
|
40
40
|
runRequirementInstall, selectConfigTemplate, validateConfigTemplate, writeAdoptedTemplate,
|
|
41
41
|
} from "../lib/packages.mjs";
|
|
42
|
+
import { attachArgv, checkRemote, getServer, inspectRemote, listSnapshots, readServers, routeCommand, targetOf, validateServer, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
|
|
43
|
+
import { spawnSync as spawnSyncProc } from "node:child_process";
|
|
42
44
|
|
|
43
45
|
const args = process.argv.slice(2);
|
|
44
46
|
const cmd = args[0];
|
|
@@ -47,6 +49,15 @@ const flag = (name) => {
|
|
|
47
49
|
const i = args.indexOf(`--${name}`);
|
|
48
50
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
49
51
|
};
|
|
52
|
+
function yoloFlag() {
|
|
53
|
+
if (args.includes("--yolo") && args.includes("--no-yolo")) cmdFail("E_BAD_ARGS", "choose --yolo or --no-yolo, not both");
|
|
54
|
+
return args.includes("--yolo") ? true : args.includes("--no-yolo") ? false : undefined;
|
|
55
|
+
}
|
|
56
|
+
function valueFlag(name) {
|
|
57
|
+
const value = flag(name);
|
|
58
|
+
if (value === true) cmdFail("E_BAD_ARGS", `--${name} needs a value`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
50
61
|
const die = (msg) => { console.error(`oats: ${msg}`); process.exit(1); };
|
|
51
62
|
/** Resolve the --dir flag with central validation: a value-taking flag given
|
|
52
63
|
* no value (flag() → true) is E_BAD_ARGS inside the JSON boundary, never an
|
|
@@ -2602,7 +2613,10 @@ function status() {
|
|
|
2602
2613
|
console.log(` ${a.name}${a.kind === "local" ? " (local)" : ""} [work: ${a.work || "checkout"}, repo: ${a.repo || "?"}]`);
|
|
2603
2614
|
if (a.description) console.log(` ${a.description}`);
|
|
2604
2615
|
for (const i of a.instances) {
|
|
2605
|
-
console.log(` • ${i.instance} ${i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
|
|
2616
|
+
console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
|
|
2617
|
+
}
|
|
2618
|
+
for (const f of a.retireFailures || []) {
|
|
2619
|
+
console.log(` ! deferred retirement of ${f.instance} FAILED${f.completedAt ? ` at ${f.completedAt}` : ""}: ${f.error || (f.incomplete || []).join("; ") || "see result file"} — retry with \`oats retire ${f.instance}\``);
|
|
2606
2620
|
}
|
|
2607
2621
|
}
|
|
2608
2622
|
const defs = listAgentDefs(process.cwd());
|
|
@@ -2624,7 +2638,8 @@ function statusTeam() {
|
|
|
2624
2638
|
if (!agents.length) { console.log(" (no agents)"); continue; }
|
|
2625
2639
|
for (const a of agents) {
|
|
2626
2640
|
console.log(` ${a.name}${a.kind === "local" ? " (local)" : ""}${a.description ? ` — ${a.description}` : ""}`);
|
|
2627
|
-
for (const i of a.instances) console.log(` • ${i.instance} ${i.running ? "RUNNING" : "idle"}`);
|
|
2641
|
+
for (const i of a.instances) console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"}`);
|
|
2642
|
+
for (const f of a.retireFailures || []) console.log(` ! deferred retirement of ${f.instance} FAILED: ${f.error || (f.incomplete || []).join("; ") || "see result file"} — retry with \`oats retire ${f.instance}\``);
|
|
2628
2643
|
}
|
|
2629
2644
|
}
|
|
2630
2645
|
}
|
|
@@ -2633,8 +2648,11 @@ function spawnCmd() {
|
|
|
2633
2648
|
// JSON mode: contract envelope, stable error codes, stderr-only progress.
|
|
2634
2649
|
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
2635
2650
|
const note = (msg) => (JSON_MODE ? console.error(msg) : console.log(msg));
|
|
2651
|
+
const yolo = yoloFlag();
|
|
2652
|
+
const backend = valueFlag("backend"), herdrSocket = valueFlag("herdr-socket");
|
|
2653
|
+
if (backend !== undefined && !["tmux", "herdr"].includes(backend)) bail("E_BAD_ARGS", "--backend must be tmux or herdr");
|
|
2636
2654
|
const name = args[1];
|
|
2637
|
-
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace] [--work-dir <owner-work>] [--runtime pi|claude] [--model <m>] [--branch <b>] [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]");
|
|
2655
|
+
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]");
|
|
2638
2656
|
// Retired boundary flags (maintainer transport ruling): fail LOUDLY before
|
|
2639
2657
|
// ANY side effect — including root discovery and local-agent upsert (an
|
|
2640
2658
|
// --instructions-file spawn must not scaffold/overwrite a local soul before
|
|
@@ -2676,7 +2694,7 @@ function spawnCmd() {
|
|
|
2676
2694
|
} else if (!agent || agent.kind === "local") {
|
|
2677
2695
|
agent = upsertLocalAgent(root, {
|
|
2678
2696
|
name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
2679
|
-
repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"),
|
|
2697
|
+
repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo: yoloFlag(),
|
|
2680
2698
|
});
|
|
2681
2699
|
} else {
|
|
2682
2700
|
bail("E_BAD_ARGS", `"${name}" is a persistent agent — spawn it without --instructions-file/--def-file`);
|
|
@@ -2725,7 +2743,7 @@ function spawnCmd() {
|
|
|
2725
2743
|
r = spawnInstance(root, agent, {
|
|
2726
2744
|
purpose: flag("purpose"), task: taskText, taskFile: taskFileFlag, relation, relativeTo, relativeRoot,
|
|
2727
2745
|
repo: flag("repo") || agent.repo || defaultRepo(workspaceOf(root)) || defaultRepo(process.cwd()),
|
|
2728
|
-
work: flag("work"), workDir: flag("work-dir"), runtime: flag("runtime"), model: flag("model"), branch: flag("branch"),
|
|
2746
|
+
work: flag("work"), workDir: flag("work-dir"), runtime: flag("runtime"), backend, herdrSocket, yolo, model: flag("model"), branch: flag("branch"),
|
|
2729
2747
|
launch: !args.includes("--no-launch"),
|
|
2730
2748
|
});
|
|
2731
2749
|
} catch (e) {
|
|
@@ -2745,10 +2763,12 @@ function spawnCmd() {
|
|
|
2745
2763
|
model: r.model || null, parent: r.parentInstance || null,
|
|
2746
2764
|
sibling: r.siblingInstance || null, relation: r.relation || null,
|
|
2747
2765
|
spawnOrigin: r.spawnOrigin, attach: r.attach,
|
|
2766
|
+
...(r.sessionTarget ? { sessionTarget: r.sessionTarget } : {}),
|
|
2767
|
+
...(r.yolo !== undefined ? { yolo: r.yolo } : {}),
|
|
2748
2768
|
});
|
|
2749
2769
|
return;
|
|
2750
2770
|
}
|
|
2751
|
-
console.log(`Spawned ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch}` : ""})${r.launched ? ` — tmux window "${r.tmux.window}"` : " — not launched"}`);
|
|
2771
|
+
console.log(`Spawned ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch}` : ""})${r.launched ? r.sessionTarget ? ` — Herdr pane "${r.sessionTarget.paneId}"` : ` — tmux window "${r.tmux.window}"` : " — not launched"}`);
|
|
2752
2772
|
console.log(` home: ${shortPath(r.home)}`);
|
|
2753
2773
|
if (!r.launched) console.log(` launch: (cd ${shortPath(r.home)} && ${r.command})`);
|
|
2754
2774
|
for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
|
|
@@ -2768,6 +2788,16 @@ function retireCmd() {
|
|
|
2768
2788
|
if (hit && resolve(hit.root) !== resolve(root)) { root = hit.root; console.log(`(cross-repo: instance homes at ${shortPath(root)})`); }
|
|
2769
2789
|
}
|
|
2770
2790
|
const r = retireInstance(root, name, { self: isSelf, deleteBranch: args.includes("--delete-branch"), keepDir: args.includes("--keep-dir"), force: args.includes("--force") });
|
|
2791
|
+
// Deferred self-retire: nothing has been inspected, run, or removed yet. The
|
|
2792
|
+
// caller's window dies first; a detached process then retires the instance
|
|
2793
|
+
// as an external operator and writes its outcome beside the home.
|
|
2794
|
+
if (r.deferred) {
|
|
2795
|
+
if (args.includes("--json")) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
2796
|
+
console.log(`Retirement of ${r.retired} (agent ${r.agent}) is ${r.alreadyScheduled ? "already " : ""}scheduled — say any goodbyes now.`);
|
|
2797
|
+
console.log(` in ~${r.completesInSec}s a detached completion quiesces this runtime (that is what ends this window), preserves work, runs retire hooks and removes the home`);
|
|
2798
|
+
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`);
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2771
2801
|
// Forced removal past an incomplete cleanup: the home is gone because the
|
|
2772
2802
|
// operator said so, but the external state it owed is still out there and
|
|
2773
2803
|
// nobody else will mention it again.
|
|
@@ -2795,13 +2825,34 @@ function retireCmd() {
|
|
|
2795
2825
|
if (isSelf) console.log("This window dies in ~8s — say any goodbyes now.");
|
|
2796
2826
|
}
|
|
2797
2827
|
|
|
2828
|
+
async function sessionCmd() {
|
|
2829
|
+
try {
|
|
2830
|
+
const home = flag("home");
|
|
2831
|
+
let result;
|
|
2832
|
+
if (args[1] === "attach") {
|
|
2833
|
+
if (JSON_MODE) throw Object.assign(new Error("session attach is interactive; omit --json"), { code: "E_BAD_ARGS" });
|
|
2834
|
+
process.exitCode = await attachInstanceSession(home);
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
if (args[1] === "inspect") result = inspectInstanceSession(home);
|
|
2838
|
+
else if (args[1] === "input") {
|
|
2839
|
+
const file = flag("text-file");
|
|
2840
|
+
if (file === true) throw Object.assign(new Error("--text-file needs a path"), { code: "E_BAD_ARGS" });
|
|
2841
|
+
if (!file && process.stdin.isTTY) throw Object.assign(new Error("provide --text-file or pipe input on stdin"), { code: "E_BAD_ARGS" });
|
|
2842
|
+
result = inputInstanceSession(home, readFileSync(file || 0, "utf8"));
|
|
2843
|
+
} else throw Object.assign(new Error("usage: oats session inspect|input|attach --home /absolute/home [--text-file path] [--json]"), { code: "E_BAD_ARGS" });
|
|
2844
|
+
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
2845
|
+
} catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message); }
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2798
2848
|
async function paneCmd() {
|
|
2799
2849
|
die("`oats pane` has been retired — the OATS Desktop app (packages/desktop) is the control panel now.");
|
|
2800
2850
|
}
|
|
2801
2851
|
|
|
2802
2852
|
function createCmd() {
|
|
2853
|
+
const yolo = yoloFlag();
|
|
2803
2854
|
const name = args[1];
|
|
2804
|
-
if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace] [--runtime pi|claude] [--model <m>] [--instructions-file <f>]");
|
|
2855
|
+
if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace] [--runtime pi|claude|codex] [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]");
|
|
2805
2856
|
const local = args.includes("--local");
|
|
2806
2857
|
const startDir = dirFlag();
|
|
2807
2858
|
// `create` BOOTSTRAPS a deployment: with no agents/ or local-agents/ yet,
|
|
@@ -2819,7 +2870,7 @@ function createCmd() {
|
|
|
2819
2870
|
const instrFile = flag("instructions-file");
|
|
2820
2871
|
const r = coreCreateAgent(root, {
|
|
2821
2872
|
name, local, description: flag("description"), type: flag("type"), repo: flag("repo") || defaultRepo(process.cwd()),
|
|
2822
|
-
work: flag("work"), runtime: flag("runtime"), model: flag("model"),
|
|
2873
|
+
work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
|
|
2823
2874
|
instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
2824
2875
|
});
|
|
2825
2876
|
if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
|
|
@@ -3067,7 +3118,10 @@ function versionCmd() {
|
|
|
3067
3118
|
if (JSON_MODE) {
|
|
3068
3119
|
// EXACT Desktop API v1 probe payload — one JSON object, nothing else on
|
|
3069
3120
|
// stdout. Desktop accepts desktopApi === 1 and a compatible semver range.
|
|
3070
|
-
|
|
3121
|
+
// `remote`: the commands this kernel routes to a registered server with
|
|
3122
|
+
// --server; a Desktop gates its remote path on it (an older CLI without
|
|
3123
|
+
// the surface must fail closed with a reason, not an argument error).
|
|
3124
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session"] }));
|
|
3071
3125
|
return;
|
|
3072
3126
|
}
|
|
3073
3127
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3106,6 +3160,152 @@ async function experimentalCmd() {
|
|
|
3106
3160
|
await import(url);
|
|
3107
3161
|
}
|
|
3108
3162
|
|
|
3163
|
+
// ---------- servers: registry and remote routing (docs/execution-targets.md) ----------
|
|
3164
|
+
/** `oats server add|list|remove|check`. A registration is where and how:
|
|
3165
|
+
* an OpenSSH host alias, the remote workspace, the remote oats path. Keys
|
|
3166
|
+
* and passwords never enter it; ssh owns those. */
|
|
3167
|
+
function serverCmd() {
|
|
3168
|
+
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
3169
|
+
const sub = args[1];
|
|
3170
|
+
const usage = "usage: oats server add <id> --ssh <host-alias> --workspace </abs/path> [--oats <path>] [--herdr <path>] [--path <dir:dir>] [--label <text>] [--replace] | list | remove <id> | check <id> [--json]";
|
|
3171
|
+
if (!["add", "list", "remove", "check"].includes(sub)) bail("E_USAGE", usage);
|
|
3172
|
+
let servers;
|
|
3173
|
+
try { servers = readServers(); } catch (e) { bail(e.code || "E_SERVERS_UNREADABLE", e.message); }
|
|
3174
|
+
if (sub === "list") {
|
|
3175
|
+
const rows = Object.entries(servers).map(([id, s]) => ({ id, ...s, target: targetOf({ id, ...s }), snapshots: listSnapshots(id).length }));
|
|
3176
|
+
if (JSON_MODE) { jsonOk({ file: SERVERS_FILE(), servers: rows }); return; }
|
|
3177
|
+
if (!rows.length) { console.log(`no servers registered (${shortPath(SERVERS_FILE())}) — add one with \`oats server add <id> --ssh <alias> --workspace </path>\``); return; }
|
|
3178
|
+
for (const r of rows) console.log(` ${r.id}${r.label ? ` ${r.label}` : ""}\n ssh ${r.sshHost} workspace ${r.workspace} oats ${r.target.oatsPath}${r.target.herdrPath ? ` herdr ${r.target.herdrPath}` : ""}${r.snapshots ? ` (${r.snapshots} remote instance${r.snapshots === 1 ? "" : "s"} spawned from here)` : ""}`);
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
const id = args[2];
|
|
3182
|
+
if (!id || id.startsWith("--")) bail("E_USAGE", usage);
|
|
3183
|
+
if (sub === "add") {
|
|
3184
|
+
const val = (name) => { const v = flag(name); return v === true ? bail("E_BAD_ARGS", `--${name} needs a value`) : v; };
|
|
3185
|
+
const entry = { sshHost: val("ssh"), workspace: val("workspace") };
|
|
3186
|
+
for (const [k, f] of [["oatsPath", "oats"], ["herdrPath", "herdr"], ["path", "path"], ["label", "label"]]) { const v = val(f); if (v !== undefined) entry[k] = v; }
|
|
3187
|
+
if (!entry.sshHost || !entry.workspace) bail("E_USAGE", usage);
|
|
3188
|
+
try { validateServer(id, entry); } catch (e) { bail(e.code, e.message); }
|
|
3189
|
+
if (servers[id] && !args.includes("--replace")) bail("E_SERVER_EXISTS", `server ${id} is already registered (pass --replace to overwrite; existing remote instances keep the route they were spawned with)`);
|
|
3190
|
+
servers[id] = entry;
|
|
3191
|
+
writeServers(servers);
|
|
3192
|
+
if (JSON_MODE) { jsonOk({ id, ...entry, file: SERVERS_FILE() }); return; }
|
|
3193
|
+
console.log(`Registered server ${id} → ssh ${entry.sshHost}, workspace ${entry.workspace} (${shortPath(SERVERS_FILE())}). Verify it with \`oats server check ${id}\`.`);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
if (sub === "remove") {
|
|
3197
|
+
if (!servers[id]) bail("E_SERVER_UNKNOWN", `no server registered as ${id}`);
|
|
3198
|
+
const snaps = listSnapshots(id);
|
|
3199
|
+
delete servers[id];
|
|
3200
|
+
writeServers(servers);
|
|
3201
|
+
if (JSON_MODE) { jsonOk({ removed: id, remoteInstancesStillTracked: snaps.map((s) => s.instance) }); return; }
|
|
3202
|
+
console.log(`Removed server ${id}${snaps.length ? ` — ${snaps.length} remote instance(s) spawned from it keep their snapshots and can still be retired with --server ${id}` : ""}`);
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
// check: reachability and compatibility, no mutation
|
|
3206
|
+
let server; try { server = getServer(id); } catch (e) { bail(e.code, e.message); }
|
|
3207
|
+
const target = targetOf(server);
|
|
3208
|
+
try {
|
|
3209
|
+
const remote = checkRemote(target);
|
|
3210
|
+
const status = routeCommand(id, "status", [], { server });
|
|
3211
|
+
const agents = status.envelope.ok ? (status.envelope.result.agents || []).length : undefined;
|
|
3212
|
+
if (JSON_MODE) { jsonOk({ id, target, remote, workspaceReachable: !!status.envelope.ok, agents, error: status.envelope.ok ? undefined : status.envelope.error }); return; }
|
|
3213
|
+
console.log(`${id}: ssh ${target.sshHost} ok, remote oats ${remote.version} (envelope v${remote.schemaVersion})`);
|
|
3214
|
+
console.log(status.envelope.ok ? ` workspace ${target.workspace}: ${agents} agent(s)` : ` workspace ${target.workspace}: ${status.envelope.error?.message || "not usable"}`);
|
|
3215
|
+
if (!status.envelope.ok) process.exit(1);
|
|
3216
|
+
} catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
/** `oats <spawn|retire|status> --server <id> ...`: run the command on the
|
|
3220
|
+
* registered server's installed oats, same arguments, same envelope. The
|
|
3221
|
+
* local side only routes and keeps the route snapshot per remote instance. */
|
|
3222
|
+
function serverRouteCmd() {
|
|
3223
|
+
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
3224
|
+
const id = flag("server");
|
|
3225
|
+
if (id === true || !id) bail("E_BAD_ARGS", "--server needs a registered server id (oats server list)");
|
|
3226
|
+
if (flag("dir") !== undefined || args.some((a) => a.startsWith("--dir="))) bail("E_BAD_ARGS", "--dir cannot be combined with --server: the remote workspace comes from the server registration");
|
|
3227
|
+
// Interactive viewer: `oats session attach --server <id> --instance <name>`
|
|
3228
|
+
// (or --home </abs/remote/home>) runs the execution host's own attach
|
|
3229
|
+
// through an ssh PTY with this terminal's stdio; nothing is captured.
|
|
3230
|
+
if (cmd === "session") {
|
|
3231
|
+
const addr = { instance: flag("instance") === true ? undefined : flag("instance"), home: flag("home") === true ? undefined : flag("home") };
|
|
3232
|
+
if (args[1] === "inspect") {
|
|
3233
|
+
// Desktop preflight before a remote attach: the execution host's own
|
|
3234
|
+
// inspect, relayed as its envelope; a failure is a failure, nonzero.
|
|
3235
|
+
let out;
|
|
3236
|
+
try { out = inspectRemote(id, addr); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3237
|
+
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3238
|
+
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3239
|
+
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "inspect failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
3240
|
+
const r = out.envelope.result;
|
|
3241
|
+
console.log(`${r.instance || r.home} on ${id}: ${r.present ? `present, ${r.state || "unknown"}` : "not present"}${r.backend ? ` (${r.backend})` : ""}`);
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
if (args[1] !== "attach") bail("E_USAGE", "--server routes `session inspect` and `session attach`; input runs on the execution host (the wake broker calls it there)");
|
|
3245
|
+
let route;
|
|
3246
|
+
try { route = attachArgv(id, addr, { skipVersionCheck: args.includes("--print") }); }
|
|
3247
|
+
catch (e) { bail(e.code || "E_BAD_ARGS", e.message); }
|
|
3248
|
+
if (args.includes("--print")) { console.log(route.argv.map(shellQuote).join(" ")); return; }
|
|
3249
|
+
const r = spawnSyncProc(route.argv[0], route.argv.slice(1), { stdio: "inherit" });
|
|
3250
|
+
process.exit(r.status ?? 1);
|
|
3251
|
+
}
|
|
3252
|
+
// Everything after the command word travels, minus the routing flags; a
|
|
3253
|
+
// local --task-file is read here and travels as --task text, since the
|
|
3254
|
+
// remote cannot read this machine's files.
|
|
3255
|
+
const rest = [];
|
|
3256
|
+
for (let i = 1; i < args.length; i++) {
|
|
3257
|
+
const a = args[i];
|
|
3258
|
+
if (a === "--server") { i++; continue; }
|
|
3259
|
+
if (a === "--json") continue;
|
|
3260
|
+
if (a === "--task-file") {
|
|
3261
|
+
const f = args[++i];
|
|
3262
|
+
if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--task-file needs a path");
|
|
3263
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `task file not found: ${f}`);
|
|
3264
|
+
rest.push("--task", readFileSync(f, "utf8"));
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
rest.push(a);
|
|
3268
|
+
}
|
|
3269
|
+
let routed;
|
|
3270
|
+
try { routed = routeCommand(id, cmd, rest); }
|
|
3271
|
+
catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3272
|
+
const { envelope, stderr } = routed;
|
|
3273
|
+
if (stderr && stderr.trim()) process.stderr.write(stderr.endsWith("\n") ? stderr : stderr + "\n");
|
|
3274
|
+
if (JSON_MODE) { console.log(JSON.stringify(envelope, null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
|
|
3275
|
+
if (!envelope.ok && !(cmd === "retire" && envelope.result)) die(`${id}: ${envelope.error?.message || "remote command failed"} (${envelope.error?.code || "E_REMOTE"})`);
|
|
3276
|
+
const r = envelope.result;
|
|
3277
|
+
const target = r.target || {};
|
|
3278
|
+
if (cmd === "spawn") {
|
|
3279
|
+
console.log(`Spawned ${r.instance} on ${id} (${r.work}${r.branch ? `, branch ${r.branch}` : ""})${r.launched ? ` — tmux window "${r.tmux?.window}" on ${r.target.sshHost}` : " — not launched"}`);
|
|
3280
|
+
console.log(` remote home: ${r.home}`);
|
|
3281
|
+
console.log(` route snapshot: ${shortPath(r.snapshot)}`);
|
|
3282
|
+
for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
|
|
3283
|
+
console.log(` attach: ssh -t ${r.target.sshHost} tmux attach -t ${r.tmux?.session || "oats"}`);
|
|
3284
|
+
} else if (cmd === "retire") {
|
|
3285
|
+
// Everything the local retireCmd tells the operator, for a remote home
|
|
3286
|
+
// they cannot see: forced-incomplete state now theirs to remove by hand
|
|
3287
|
+
// there, work preserved there and where, and incomplete cleanup.
|
|
3288
|
+
if (r.forcedIncomplete) {
|
|
3289
|
+
console.error(`Removed ${r.retired} on ${id} under --force with cleanup INCOMPLETE — this external state was NOT cleaned up and is now yours to remove by hand on ${target.sshHost}:`);
|
|
3290
|
+
for (const f of r.forcedIncomplete) console.error(` ${f}`);
|
|
3291
|
+
}
|
|
3292
|
+
console.log(`Retired ${r.retired} on ${id}${r.deferred ? " (deferred completion scheduled there)" : ""}${r.rollbackIncomplete ? " — cleanup INCOMPLETE on the server, home retained there" : ""}`);
|
|
3293
|
+
for (const recovery of r.workRecoveries || (r.workRecovery ? [r.workRecovery] : [])) {
|
|
3294
|
+
console.log(`Work that was not committed has been preserved on ${target.sshHost}: ${(recovery.classes || []).join(", ")}`);
|
|
3295
|
+
console.log(` ${recovery.path}`);
|
|
3296
|
+
}
|
|
3297
|
+
if (r.rollbackIncomplete) { for (const f of r.rollbackIncomplete) console.error(` ${f}`); console.error(`Fix the cause there and re-run \`oats retire ${r.retired} --server ${id}\`.`); process.exit(1); }
|
|
3298
|
+
} else {
|
|
3299
|
+
console.log(`oats status — server ${id} (ssh ${r.target.sshHost}, workspace ${r.target.workspace})\n`);
|
|
3300
|
+
for (const a of r.agents || []) {
|
|
3301
|
+
console.log(` ${a.name} [work: ${a.work || "checkout"}, repo: ${a.repo || "?"}]`);
|
|
3302
|
+
for (const i of a.instances || []) console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"}`);
|
|
3303
|
+
}
|
|
3304
|
+
const snaps = r.snapshots || [];
|
|
3305
|
+
if (snaps.length) console.log(`\n spawned from this machine: ${snaps.map((s) => s.instance).join(", ")}`);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3109
3309
|
// ---------- main ----------
|
|
3110
3310
|
// Typed config-shape failures are DEPLOYMENT state the operator can fix, not
|
|
3111
3311
|
// kernel bugs: an unsafe mapping key anywhere in the visible config chain is
|
|
@@ -3121,7 +3321,9 @@ async function experimentalCmd() {
|
|
|
3121
3321
|
// blame` pointing at the commit that last changed each command.
|
|
3122
3322
|
const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
|
|
3123
3323
|
try {
|
|
3124
|
-
if (
|
|
3324
|
+
if (flag("server") !== undefined && ["spawn", "retire", "status", "session"].includes(cmd)) serverRouteCmd();
|
|
3325
|
+
else if (cmd === "server") serverCmd();
|
|
3326
|
+
else if (cmd === "doctor") {
|
|
3125
3327
|
const doctorDir = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
|
|
3126
3328
|
args.includes("--json") ? doctorJson(doctorDir) : doctor(doctorDir);
|
|
3127
3329
|
}
|
|
@@ -3142,6 +3344,7 @@ else if (cmd === "pane") await paneCmd();
|
|
|
3142
3344
|
else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
|
|
3143
3345
|
// Same rule as the inner catch: a typed CLI failure surfaces with its own code
|
|
3144
3346
|
// through the shared boundary, never re-badged as a spawn-mechanism failure.
|
|
3347
|
+
else if (cmd === "session") await sessionCmd();
|
|
3145
3348
|
else if (cmd === "spawn") { try { spawnCmd(); } catch (e) { if (TYPED_CLI_FAILURES.has(e?.code)) throw e; if (JSON_MODE) jsonFail("E_SPAWN_FAILED", e.message || e); throw e; } }
|
|
3146
3349
|
else if (cmd === "retire") retireCmd();
|
|
3147
3350
|
else if (cmd === "create") createCmd();
|
|
@@ -3163,24 +3366,36 @@ Usage:
|
|
|
3163
3366
|
Desktop CLI API v1 probe payload
|
|
3164
3367
|
oats status [--json] agents, souls, running instances
|
|
3165
3368
|
oats status --team [--json] whole-team roster across the team scope's repos
|
|
3369
|
+
oats server add <id> --ssh <alias> register another machine's OATS (OpenSSH alias,
|
|
3370
|
+
--workspace </abs/path> [--oats <p>] remote workspace, remote oats path; no keys stored;
|
|
3371
|
+
[--path <dir:dir>] --path = dirs prepended to the remote PATH, e.g. ~/.local/bin)
|
|
3372
|
+
oats server list|remove <id>|check <id> registry; check = reachability + version, no mutation
|
|
3373
|
+
oats spawn|retire|status ... --server <id> run that command on the server's installed oats
|
|
3374
|
+
(same flags, same envelope; the saved route per
|
|
3375
|
+
remote instance lives under ~/.oats/remote/)
|
|
3376
|
+
oats session inspect|attach --server <id> inspect (envelope) or attach a viewer (ssh PTY) for a
|
|
3377
|
+
--instance <name> | --home <abs> remote instance over its saved route (--print shows
|
|
3378
|
+
attach); the server needs oats 0.22.2 or later
|
|
3166
3379
|
oats create <name> [--local] create an agent soul; --local = full
|
|
3167
3380
|
[--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
|
|
3168
|
-
[--work <mode>] [--runtime pi|claude] gitignored; same memory + lifecycle)
|
|
3169
|
-
[--model <m>] [--instructions-file <f>]
|
|
3170
|
-
oats
|
|
3381
|
+
[--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
|
|
3382
|
+
[--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
|
|
3383
|
+
oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
|
|
3384
|
+
oats spawn <agent> [--task <text>] spawn an instance (tmux/Herdr; --no-launch
|
|
3171
3385
|
[--purpose <slug>] [--repo <r>] = scaffold only); --instructions-file/
|
|
3172
3386
|
[--parent <instance>] --def-file creates a local agent;
|
|
3173
3387
|
[--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
|
|
3174
3388
|
[--relative-to <instance>] new instance to an existing one; --parent X
|
|
3175
3389
|
[--relative-root <agents-root>] disambiguates same-named team anchors
|
|
3176
3390
|
[--work worktree|checkout|attached|workspace] = sugar for --relative-to X --relation
|
|
3177
|
-
[--work-dir <owner-work>] [--runtime pi|claude] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
|
|
3391
|
+
[--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
|
|
3178
3392
|
[--instructions-file <f>|--def-file <f>] [--no-launch] [--json]
|
|
3179
3393
|
with team: declared, unknown local souls
|
|
3180
3394
|
resolve across the team scope's repos
|
|
3181
3395
|
oats retire <instance> [--force] retire an instance (window, hooks,
|
|
3182
3396
|
[--self] [--delete-branch] worktree, home); --self = retire the
|
|
3183
|
-
[--keep-dir] [--json] CALLING instance
|
|
3397
|
+
[--keep-dir] [--json] CALLING instance: the window dies, then
|
|
3398
|
+
a detached external retirement runs
|
|
3184
3399
|
oats doctor [dir] [--soul <name>] [--json] resolved targets, trust, requirements;
|
|
3185
3400
|
--soul shows final composed AGENTS.md
|
|
3186
3401
|
oats update [--check] [--yes] check npm for a newer kernel+pi bridge and
|
|
@@ -41,9 +41,10 @@ Aliases are instance names (e.g. `dev-coordinator-1`). Discovery:
|
|
|
41
41
|
lists the aweb team across machines.
|
|
42
42
|
|
|
43
43
|
**Never sleep, poll, or busy-wait for another agent's reply.** Send your
|
|
44
|
-
message, finish your turn, and go idle
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
message, finish your turn, and go idle when a delivery channel is configured
|
|
45
|
+
(you saw `✓ aweb connected` at startup). Native Codex has no aweb channel:
|
|
46
|
+
check inbox and pending chat at task boundaries or when the operator asks,
|
|
47
|
+
as described in TASK.md; incoming messages alone will not wake that session. A `sleep N; aw mail inbox` loop burns tokens, delays the reply,
|
|
47
48
|
and adds nothing. An empty `aw mail inbox` means no UNREAD mail — not that
|
|
48
49
|
messages were lost.
|
|
49
50
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# memory-harvest — soul promotion from live instances
|
|
2
2
|
|
|
3
3
|
You are a memory-harvest instance. You were spawned because a live agent
|
|
4
|
-
instance committed work while holding pending notes
|
|
4
|
+
instance committed work while holding pending notes, or because its own
|
|
5
|
+
captured session turns hold candidates nobody has judged yet (your briefing
|
|
6
|
+
says which, and names the exact record windows when it is the latter).
|
|
5
7
|
|
|
6
8
|
**Your briefing (TASK.md) is the authority on your situation**: the source
|
|
7
9
|
notes dir, the soul to update, the work tree you were given, and how your
|