@awebai/oats 0.22.10 → 0.22.14
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 +186 -6
- package/docs/design/2026-09-07-architecture-reassessment.md +131 -0
- package/docs/desktop.md +44 -2
- package/docs/execution-targets.md +21 -0
- package/docs/release-notes/v0.22.11.md +49 -0
- package/docs/release-notes/v0.22.12.md +53 -0
- package/docs/release-notes/v0.22.13.md +44 -0
- package/docs/release-notes/v0.22.14.md +48 -0
- package/docs/schedules.md +143 -0
- package/lib/attachments.mjs +164 -0
- package/lib/schedule-host.mjs +150 -0
- package/lib/schedule.mjs +731 -0
- package/lib/servers.mjs +19 -1
- package/lib/session-viewer.mjs +4 -0
- package/package.json +3 -2
package/bin/oats.mjs
CHANGED
|
@@ -39,8 +39,11 @@ import {
|
|
|
39
39
|
assertNoSymlinkedParents, copyFileAtomic, writeFileAtomic,
|
|
40
40
|
runRequirementInstall, selectConfigTemplate, validateConfigTemplate, writeAdoptedTemplate,
|
|
41
41
|
} from "../lib/packages.mjs";
|
|
42
|
-
import { attachArgv, checkRemote, forgetSnapshot, getServer, inspectRemote, startRemote, listSnapshots, readServers, rosterGroups, routeCommand, targetOf, validateServer, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
|
|
42
|
+
import { attachArgv, checkRemote, forgetSnapshot, getServer, inspectRemote, startRemote, scheduleRemote, listSnapshots, readServers, rosterGroups, routeCommand, targetOf, validateServer, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
|
|
43
43
|
import { spawnSync as spawnSyncProc } from "node:child_process";
|
|
44
|
+
import { scheduleScopeOf, listSchedules, describe as describeSchedule, addSchedule, updateSchedule, setEnabled as setScheduleEnabled, removeSchedule, runNow as runScheduleNow, reconcile as reconcileSchedule, tickHost, tickWorkspace, registerWorkspace, unregisterWorkspace, readRegistry, schedulerStatus, saveWakeForHome, removeWakeForHome, wakeFromFlags, withHostLock, scheduleError, SCHEDULE_API } from "../lib/schedule.mjs";
|
|
45
|
+
import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
|
|
46
|
+
import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
|
|
44
47
|
|
|
45
48
|
const args = process.argv.slice(2);
|
|
46
49
|
const cmd = args[0];
|
|
@@ -2754,6 +2757,34 @@ function spawnCmd() {
|
|
|
2754
2757
|
const relativeRoot = flag("relative-root");
|
|
2755
2758
|
if (relativeRoot !== undefined && (relativeRoot === true || !String(relativeRoot).trim())) bail("E_BAD_ARGS", "--relative-root needs an agents-root path");
|
|
2756
2759
|
if (relativeRoot && !relativeTo) bail("E_BAD_ARGS", "--relative-root only qualifies --relative-to/--parent");
|
|
2760
|
+
// Wake at launch: --wake-file <private JSON {cron, tz, message, enabled}>
|
|
2761
|
+
// is the backend bridge; --wake-every/--wake-cron/--wake-tz/--wake-message
|
|
2762
|
+
// (or --wake-message-file) are the human sugar for the same object. The
|
|
2763
|
+
// wake job is saved AFTER a successful spawn, bound to the returned home.
|
|
2764
|
+
let wake;
|
|
2765
|
+
try {
|
|
2766
|
+
const wakeFile = flag("wake-file");
|
|
2767
|
+
if (wakeFile === true) bail("E_BAD_ARGS", "--wake-file needs a path");
|
|
2768
|
+
const wakeJson = flag("wake-json");
|
|
2769
|
+
if (wakeJson === true) bail("E_BAD_ARGS", "--wake-json needs the wake object as JSON text");
|
|
2770
|
+
if (wakeJson) {
|
|
2771
|
+
let doc; try { doc = JSON.parse(wakeJson); } catch (e) { bail("E_SCHEDULE_INVALID", `--wake-json is not valid JSON: ${e.message}`); }
|
|
2772
|
+
if (!doc || typeof doc !== "object") bail("E_SCHEDULE_INVALID", "--wake-json must hold {cron, tz, message, enabled}");
|
|
2773
|
+
wake = { cron: doc.cron, tz: doc.tz, message: doc.message, enabled: doc.enabled === undefined ? true : doc.enabled };
|
|
2774
|
+
} else if (wakeFile) {
|
|
2775
|
+
if (!existsSync(wakeFile)) bail("E_BAD_ARGS", `--wake-file not found: ${wakeFile}`);
|
|
2776
|
+
let doc; try { doc = JSON.parse(readFileSync(wakeFile, "utf8")); } catch (e) { bail("E_SCHEDULE_INVALID", `--wake-file is not valid JSON: ${e.message}`); }
|
|
2777
|
+
if (!doc || typeof doc !== "object") bail("E_SCHEDULE_INVALID", "--wake-file must hold {cron, tz, message, enabled}");
|
|
2778
|
+
wake = { cron: doc.cron, tz: doc.tz, message: doc.message, enabled: doc.enabled === undefined ? true : doc.enabled };
|
|
2779
|
+
} else if (flag("wake-every") !== undefined || flag("wake-cron") !== undefined) {
|
|
2780
|
+
const mf = flag("wake-message-file");
|
|
2781
|
+
if (mf === true) bail("E_BAD_ARGS", "--wake-message-file needs a path");
|
|
2782
|
+
if (mf && !existsSync(mf)) bail("E_BAD_ARGS", `--wake-message-file not found: ${mf}`);
|
|
2783
|
+
const message = mf ? readFileSync(mf, "utf8") : flag("wake-message") === true ? undefined : flag("wake-message");
|
|
2784
|
+
wake = wakeFromFlags({ every: flag("wake-every") === true ? undefined : flag("wake-every"), cron: flag("wake-cron") === true ? undefined : flag("wake-cron"), tz: flag("wake-tz") === true ? undefined : flag("wake-tz"), message });
|
|
2785
|
+
}
|
|
2786
|
+
if (wake && (typeof wake.message !== "string" || !wake.message.trim())) bail("E_SCHEDULE_INVALID", "wake message: non-empty text is required");
|
|
2787
|
+
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message); throw e; }
|
|
2757
2788
|
let r;
|
|
2758
2789
|
try {
|
|
2759
2790
|
r = spawnInstance(root, agent, {
|
|
@@ -2770,11 +2801,19 @@ function spawnCmd() {
|
|
|
2770
2801
|
if (TYPED_CLI_FAILURES.has(e?.code)) throw e;
|
|
2771
2802
|
bail(e.code === "E_RELATIVE_AMBIGUOUS" ? "E_RELATIVE_AMBIGUOUS" : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
2772
2803
|
}
|
|
2804
|
+
// The instance exists from here on: a failed wake save is reported beside
|
|
2805
|
+
// the full receipt, never hidden, and never causes a second spawn.
|
|
2806
|
+
let wakeSchedule, wakeScheduleError;
|
|
2807
|
+
if (wake) {
|
|
2808
|
+
try { wakeSchedule = saveWakeForHome(scheduleScopeOf(workspaceOf(root)), { instance: r.instance, home: r.home, wake }); }
|
|
2809
|
+
catch (e) { wakeScheduleError = { code: e.code || "E_SCHEDULE_FAILED", message: e.message }; r.warnings = [...(r.warnings || []), `wake schedule NOT saved: ${e.message}`]; }
|
|
2810
|
+
}
|
|
2773
2811
|
if (JSON_MODE) {
|
|
2774
2812
|
// Desktop CLI API v1 spawn result — a FIXED shape (see docs/desktop-cli-api.md).
|
|
2775
2813
|
jsonOk({
|
|
2776
2814
|
instance: r.instance, agent: r.agent, home: r.home, work: r.work,
|
|
2777
2815
|
branch: r.branch || null, launched: r.launched, warnings: r.warnings || [],
|
|
2816
|
+
...(wakeSchedule ? { wakeSchedule } : {}), ...(wakeScheduleError ? { wakeScheduleError } : {}),
|
|
2778
2817
|
tmux: r.tmux || null, repo: r.repo || null, runtime: r.runtime || null,
|
|
2779
2818
|
model: r.model || null, parent: r.parentInstance || null,
|
|
2780
2819
|
sibling: r.siblingInstance || null, relation: r.relation || null,
|
|
@@ -2786,6 +2825,8 @@ function spawnCmd() {
|
|
|
2786
2825
|
}
|
|
2787
2826
|
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"}`);
|
|
2788
2827
|
console.log(` home: ${shortPath(r.home)}`);
|
|
2828
|
+
if (wakeSchedule) console.log(` wake: schedule ${wakeSchedule.id} (${wakeSchedule.cron} ${wakeSchedule.tz}), next ${wakeSchedule.nextRun || "disabled"}`);
|
|
2829
|
+
if (wakeScheduleError) console.error(` wake: NOT saved — ${wakeScheduleError.message} (the instance is created and launched; add the wake by hand with oats schedule add)`);
|
|
2789
2830
|
if (!r.launched) console.log(` launch: (cd ${shortPath(r.home)} && ${r.command})`);
|
|
2790
2831
|
for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
|
|
2791
2832
|
console.log(` attach: ${r.attach}`);
|
|
@@ -2809,7 +2850,11 @@ function retireCmd() {
|
|
|
2809
2850
|
// Stdout carries only the envelope in JSON mode (the Desktop parses it).
|
|
2810
2851
|
if (hit && resolve(hit.root) !== resolve(root)) { root = hit.root; (args.includes("--json") ? console.error : console.log)(`(cross-repo: instance homes at ${shortPath(root)})`); }
|
|
2811
2852
|
}
|
|
2853
|
+
const retiringHome = homeFlag || findInstanceHome(root, name);
|
|
2812
2854
|
const r = retireInstance(root, name, { home: homeFlag, self: isSelf, deleteBranch: args.includes("--delete-branch"), keepDir: args.includes("--keep-dir"), force: args.includes("--force") });
|
|
2855
|
+
// A retired home's wake jobs are forgotten (definitions only; nothing is
|
|
2856
|
+
// stopped by this); a deferred self-retire keeps them until the home is gone.
|
|
2857
|
+
if (retiringHome && r.removedDir !== false && !r.deferred) { try { const gone = removeWakeForHome(scheduleScopeOf(workspaceOf(root)), retiringHome); if (gone.length) r.wakeSchedulesRemoved = gone; } catch (e) { r.warnings = [...(r.warnings || []), `wake schedules not cleaned: ${e.message}`]; } }
|
|
2813
2858
|
// Deferred self-retire: nothing has been inspected, run, or removed yet. The
|
|
2814
2859
|
// caller's window dies first; a detached process then retires the instance
|
|
2815
2860
|
// as an external operator and writes its outcome beside the home.
|
|
@@ -2847,6 +2892,56 @@ function retireCmd() {
|
|
|
2847
2892
|
if (isSelf) console.log("This window dies in ~8s — say any goodbyes now.");
|
|
2848
2893
|
}
|
|
2849
2894
|
|
|
2895
|
+
/** `oats schedule ...`: workspace-scoped definitions, host-owned execution
|
|
2896
|
+
* (lib/schedule.mjs). Every subcommand answers the envelope; nothing here
|
|
2897
|
+
* launches unless a job is due or run-now is asked. */
|
|
2898
|
+
function scheduleCmd() {
|
|
2899
|
+
const sub = args[1];
|
|
2900
|
+
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
2901
|
+
// One schedule-owning scope for a directory: the team workspace (the
|
|
2902
|
+
// config level declaring the team), else the outermost config level.
|
|
2903
|
+
const ws = scheduleScopeOf(dirFlag());
|
|
2904
|
+
const io = { hostStatus: () => hostUnitStatus() };
|
|
2905
|
+
const out = (result) => { if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2)); };
|
|
2906
|
+
const readSpec = () => {
|
|
2907
|
+
const inline = flag("spec-json");
|
|
2908
|
+
if (inline && inline !== true) { try { return JSON.parse(inline); } catch (e) { throw scheduleError("E_SCHEDULE_INVALID", `--spec-json is not valid JSON: ${e.message}`, { field: "file" }); } }
|
|
2909
|
+
const f = flag("file");
|
|
2910
|
+
if (!f || f === true) throw scheduleError("E_BAD_ARGS", "--file <spec.json> is required (a private JSON file with the definition)");
|
|
2911
|
+
if (!existsSync(f)) throw scheduleError("E_BAD_ARGS", `spec file not found: ${f}`);
|
|
2912
|
+
try { return JSON.parse(readFileSync(f, "utf8")); } catch (e) { throw scheduleError("E_SCHEDULE_INVALID", `${f} is not valid JSON: ${e.message}`, { field: "file" }); }
|
|
2913
|
+
};
|
|
2914
|
+
const needId = () => { if (!id) throw scheduleError("E_BAD_ARGS", `oats schedule ${sub} <id>`); return id; };
|
|
2915
|
+
try {
|
|
2916
|
+
switch (sub) {
|
|
2917
|
+
case "list": return out(listSchedules(ws, io));
|
|
2918
|
+
case "show": return out({ schedule: describeSchedule(ws, needId(), io) });
|
|
2919
|
+
case "add": { const spec = readSpec(); if (id && spec.id === undefined) spec.id = id; if (id && spec.id !== id) throw scheduleError("E_SCHEDULE_INVALID", `id ${JSON.stringify(spec.id)} in the file does not match ${JSON.stringify(id)}`, { field: "id" }); return out({ schedule: addSchedule(ws, spec, io) }); }
|
|
2920
|
+
case "update": return out({ schedule: updateSchedule(ws, needId(), readSpec(), io) });
|
|
2921
|
+
case "enable": return out({ schedule: setScheduleEnabled(ws, needId(), true, io) });
|
|
2922
|
+
case "disable": return out({ schedule: setScheduleEnabled(ws, needId(), false, io) });
|
|
2923
|
+
case "run": return out(runScheduleNow(ws, needId(), { io, force: args.includes("--force") }));
|
|
2924
|
+
case "remove": return out(removeSchedule(ws, needId(), { force: args.includes("--force") }));
|
|
2925
|
+
case "reconcile": return out(reconcileSchedule(ws, needId(), { io, clear: args.includes("--clear") }));
|
|
2926
|
+
case "tick": {
|
|
2927
|
+
const dryRun = args.includes("--dry-run");
|
|
2928
|
+
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
2929
|
+
const reg = readRegistry();
|
|
2930
|
+
const considered = withHostLock(() => tickWorkspace(ws, { io, reg, wsList: reg.workspaces.includes(ws) ? reg.workspaces : [...reg.workspaces, ws], dryRun }));
|
|
2931
|
+
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws, io) });
|
|
2932
|
+
}
|
|
2933
|
+
case "host": {
|
|
2934
|
+
const op = args[2];
|
|
2935
|
+
if (op === "install") { registerWorkspace(ws); installHostUnit(); return out({ scheduler: schedulerStatus(ws, io) }); }
|
|
2936
|
+
if (op === "uninstall") { unregisterWorkspace(ws); if (!readRegistry().workspaces.length) uninstallHostUnit(); return out({ scheduler: schedulerStatus(ws, io) }); }
|
|
2937
|
+
if (op === "status") return out({ scheduler: schedulerStatus(ws, io) });
|
|
2938
|
+
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
2939
|
+
}
|
|
2940
|
+
default: throw scheduleError("E_BAD_ARGS", "usage: oats schedule list|show <id>|add <id> --file <spec.json>|update <id> --file <spec.json>|enable <id>|disable <id>|run <id> [--force]|remove <id> [--force]|reconcile <id> [--clear]|tick [--dry-run] [--host]|host install|uninstall|status [--dir <workspace>|--server <id>] [--json]");
|
|
2941
|
+
}
|
|
2942
|
+
} catch (e) { cmdFail(e.code || "E_SCHEDULE_FAILED", e.message); }
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2850
2945
|
async function sessionCmd() {
|
|
2851
2946
|
try {
|
|
2852
2947
|
const home = flag("home");
|
|
@@ -2866,7 +2961,19 @@ async function sessionCmd() {
|
|
|
2866
2961
|
if (file === true) throw Object.assign(new Error("--text-file needs a path"), { code: "E_BAD_ARGS" });
|
|
2867
2962
|
if (!file && process.stdin.isTTY) throw Object.assign(new Error("provide --text-file or pipe input on stdin"), { code: "E_BAD_ARGS" });
|
|
2868
2963
|
result = inputInstanceSession(home, readFileSync(file || 0, "utf8"));
|
|
2869
|
-
} else
|
|
2964
|
+
} else if (args[1] === "receive") {
|
|
2965
|
+
// Bytes arrive on stdin (the routed upload pipes them through ssh),
|
|
2966
|
+
// collected event-driven and bounded before anything is written.
|
|
2967
|
+
const name = flag("name");
|
|
2968
|
+
if (!name || name === true) throw Object.assign(new Error("session receive needs --name <file name>"), { code: "E_BAD_ARGS" });
|
|
2969
|
+
if (!home || home === true) throw Object.assign(new Error("session receive needs --home </absolute/instance>"), { code: "E_BAD_ARGS" });
|
|
2970
|
+
if (process.stdin.isTTY) throw Object.assign(new Error("session receive reads the attachment bytes from stdin"), { code: "E_BAD_ARGS" });
|
|
2971
|
+
result = receiveAttachment(home, name, await readStreamBounded(process.stdin, MAX_ATTACHMENT_BYTES));
|
|
2972
|
+
} else if (args[1] === "upload") {
|
|
2973
|
+
const file = flag("file");
|
|
2974
|
+
if (!file || file === true) throw Object.assign(new Error("session upload needs --file <local path>"), { code: "E_BAD_ARGS" });
|
|
2975
|
+
result = uploadAttachment({ file, home: home === true ? undefined : home });
|
|
2976
|
+
} else throw Object.assign(new Error("usage: oats session inspect|input|attach|start|receive|upload --home /absolute/home [--text-file path] [--model id] [--name file] [--file path] [--json]"), { code: "E_BAD_ARGS" });
|
|
2870
2977
|
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
2871
2978
|
} catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message); }
|
|
2872
2979
|
}
|
|
@@ -3161,7 +3268,7 @@ function versionCmd() {
|
|
|
3161
3268
|
// on it (an older CLI without the surface must fail closed with a
|
|
3162
3269
|
// reason, not an argument error). `features`: kernel abilities a peer
|
|
3163
3270
|
// must see before relying on them (retire-home: retire --home).
|
|
3164
|
-
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", "session-start", "roster", "harvest"], features: ["retire-home", "session-start"] }));
|
|
3271
|
+
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", "session-start", "roster", "harvest", "schedule", "session-upload"], features: ["retire-home", "session-start", "schedule", "session-upload"], scheduleApi: SCHEDULE_API }));
|
|
3165
3272
|
return;
|
|
3166
3273
|
}
|
|
3167
3274
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3316,6 +3423,32 @@ function serverRouteCmd() {
|
|
|
3316
3423
|
if (hr.instance && hr.instance !== inst) console.log(` the harvester ${hr.instance} runs on ${id}; retire it there when it is done, or let it self-retire`);
|
|
3317
3424
|
return;
|
|
3318
3425
|
}
|
|
3426
|
+
if (cmd === "schedule") {
|
|
3427
|
+
// Host-owned: the subcommand runs in the server's registered workspace.
|
|
3428
|
+
// A local --file spec is read here and travels inline; the remote
|
|
3429
|
+
// cannot read this machine's files.
|
|
3430
|
+
const rest = [];
|
|
3431
|
+
for (let i = 1; i < args.length; i++) {
|
|
3432
|
+
const a = args[i];
|
|
3433
|
+
if (a === "--server") { i++; continue; }
|
|
3434
|
+
if (a === "--json") continue;
|
|
3435
|
+
if (a === "--file") {
|
|
3436
|
+
const f = args[++i];
|
|
3437
|
+
if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--file needs a path");
|
|
3438
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `spec file not found: ${f}`);
|
|
3439
|
+
rest.push("--spec-json", readFileSync(f, "utf8"));
|
|
3440
|
+
continue;
|
|
3441
|
+
}
|
|
3442
|
+
rest.push(a);
|
|
3443
|
+
}
|
|
3444
|
+
let out;
|
|
3445
|
+
try { out = scheduleRemote(id, rest); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3446
|
+
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3447
|
+
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3448
|
+
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "schedule command failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
3449
|
+
console.log(JSON.stringify(out.envelope.result, null, 2));
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3319
3452
|
if (cmd === "session") {
|
|
3320
3453
|
const addr = { instance: flag("instance") === true ? undefined : flag("instance"), home: flag("home") === true ? undefined : flag("home") };
|
|
3321
3454
|
if (args[1] === "inspect") {
|
|
@@ -3342,7 +3475,19 @@ function serverRouteCmd() {
|
|
|
3342
3475
|
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"})`);
|
|
3343
3476
|
return;
|
|
3344
3477
|
}
|
|
3345
|
-
if (args[1]
|
|
3478
|
+
if (args[1] === "upload") {
|
|
3479
|
+
// Bytes stream to `session receive` on the execution host over the
|
|
3480
|
+
// same route as attach; the answer's checksum is verified here.
|
|
3481
|
+
const file = flag("file");
|
|
3482
|
+
if (!file || file === true) bail("E_BAD_ARGS", "session upload needs --file <local path>");
|
|
3483
|
+
let r;
|
|
3484
|
+
try { r = uploadAttachment({ file, server: id, ...addr }); } catch (e) { bail(e.code || "E_UPLOAD_FAILED", e.message); }
|
|
3485
|
+
if (r.stderr) process.stderr.write(r.stderr + "\n");
|
|
3486
|
+
if (JSON_MODE) { jsonOk(r); return; }
|
|
3487
|
+
console.log(`Uploaded ${r.name} (${r.bytes} bytes) to ${r.instance || r.home} on ${id}: ${r.path}`);
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
if (args[1] !== "attach") bail("E_USAGE", "--server routes `session inspect`, `session start`, `session upload` and `session attach`; input runs on the execution host (the wake broker calls it there)");
|
|
3346
3491
|
let route;
|
|
3347
3492
|
try { route = attachArgv(id, addr, { skipVersionCheck: args.includes("--print") }); }
|
|
3348
3493
|
catch (e) { bail(e.code || "E_BAD_ARGS", e.message); }
|
|
@@ -3365,6 +3510,24 @@ function serverRouteCmd() {
|
|
|
3365
3510
|
rest.push("--task", readFileSync(f, "utf8"));
|
|
3366
3511
|
continue;
|
|
3367
3512
|
}
|
|
3513
|
+
// A wake spec for a routed spawn travels as validated JSON text; the
|
|
3514
|
+
// host saves it in its own scope. Local files never travel as paths.
|
|
3515
|
+
if (a === "--wake-file") {
|
|
3516
|
+
const f = args[++i];
|
|
3517
|
+
if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--wake-file needs a path");
|
|
3518
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `wake file not found: ${f}`);
|
|
3519
|
+
let doc; try { doc = JSON.parse(readFileSync(f, "utf8")); } catch (e) { bail("E_SCHEDULE_INVALID", `--wake-file is not valid JSON: ${e.message}`); }
|
|
3520
|
+
if (!doc || typeof doc !== "object" || !["cron", "tz", "message"].every((k) => typeof doc[k] === "string" && doc[k].trim())) bail("E_SCHEDULE_INVALID", "--wake-file must hold {cron, tz, message, enabled}");
|
|
3521
|
+
rest.push("--wake-json", JSON.stringify({ cron: doc.cron, tz: doc.tz, message: doc.message, enabled: doc.enabled === undefined ? true : doc.enabled }));
|
|
3522
|
+
continue;
|
|
3523
|
+
}
|
|
3524
|
+
if (a === "--wake-message-file") {
|
|
3525
|
+
const f = args[++i];
|
|
3526
|
+
if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--wake-message-file needs a path");
|
|
3527
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `wake message file not found: ${f}`);
|
|
3528
|
+
rest.push("--wake-message", readFileSync(f, "utf8"));
|
|
3529
|
+
continue;
|
|
3530
|
+
}
|
|
3368
3531
|
rest.push(a);
|
|
3369
3532
|
}
|
|
3370
3533
|
let routed;
|
|
@@ -3426,10 +3589,10 @@ try {
|
|
|
3426
3589
|
// and exits 0 BEFORE any dispatch: a fresh operator inspects --help before
|
|
3427
3590
|
// using a command, and `install --help` once ran the bare restore while
|
|
3428
3591
|
// `okf harvest --help` spawned a harvester (BeadHub, 2026-09-05).
|
|
3429
|
-
const KERNEL_COMMANDS = new Set(["capture", "config", "create", "doctor", "experimental", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
3592
|
+
const KERNEL_COMMANDS = new Set(["capture", "config", "create", "doctor", "experimental", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
3430
3593
|
const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
|
|
3431
3594
|
if (cmd && KERNEL_COMMANDS.has(cmd) && wantsHelp) { if (JSON_MODE) { jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); process.exit(0); } usageFor(cmd); process.exit(0); }
|
|
3432
|
-
if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf"].includes(cmd)) serverRouteCmd();
|
|
3595
|
+
if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf", "schedule"].includes(cmd)) serverRouteCmd();
|
|
3433
3596
|
else if (cmd === "server") serverCmd();
|
|
3434
3597
|
else if (cmd === "doctor") {
|
|
3435
3598
|
const doctorDir = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
|
|
@@ -3459,6 +3622,7 @@ else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
|
|
|
3459
3622
|
// Same rule as the inner catch: a typed CLI failure surfaces with its own code
|
|
3460
3623
|
// through the shared boundary, never re-badged as a spawn-mechanism failure.
|
|
3461
3624
|
else if (cmd === "session") await sessionCmd();
|
|
3625
|
+
else if (cmd === "schedule") scheduleCmd();
|
|
3462
3626
|
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; } }
|
|
3463
3627
|
else if (cmd === "retire") retireCmd();
|
|
3464
3628
|
else if (cmd === "create") createCmd();
|
|
@@ -3525,11 +3689,27 @@ Usage:
|
|
|
3525
3689
|
oats session start --server <id> start a stopped remote instance in its existing home
|
|
3526
3690
|
--instance <name> | --home <abs> over its saved route; the server must advertise
|
|
3527
3691
|
[--model <m>] [--json] session-start (oats 0.22.9 or later)
|
|
3692
|
+
oats session upload --server <id> copy a local file into a remote instance's private
|
|
3693
|
+
--instance <name> | --home <abs> attachments over its saved route (bytes stream on
|
|
3694
|
+
--file <path> [--json] ssh stdin; sha256 verified); the server must
|
|
3695
|
+
advertise session-upload (oats 0.22.13 or later)
|
|
3528
3696
|
oats create <name> [--local] create an agent soul; --local = full
|
|
3529
3697
|
[--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
|
|
3530
3698
|
[--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
|
|
3531
3699
|
[--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
|
|
3532
3700
|
oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
|
|
3701
|
+
oats schedule list|show <id>|add <id> --file <spec.json>|update <id> --file <spec.json>
|
|
3702
|
+
enable|disable|run|remove|reconcile <id> workspace-scoped, host-owned schedules (spawn,
|
|
3703
|
+
tick [--dry-run] [--host] command or wake jobs on a five-field cron with an
|
|
3704
|
+
host install|uninstall|status explicit IANA tz; see docs/schedules.md); --server
|
|
3705
|
+
routes to that host's workspace
|
|
3706
|
+
oats spawn ... --wake-file <json> | --wake-every <N> --wake-message <text> save a wake schedule
|
|
3707
|
+
bound to the new instance's home (docs/schedules.md)
|
|
3708
|
+
oats session upload --file <path> store a copy of a local file as a private attachment
|
|
3709
|
+
--home <absolute-home> [--json] in the instance home (.oats-attachments/); answers
|
|
3710
|
+
{path, bytes, sha256}; never types into the session
|
|
3711
|
+
oats session receive --home <abs> --name store attachment bytes read from stdin (the routed
|
|
3712
|
+
<file> [--json] upload's remote half)
|
|
3533
3713
|
oats session start --home <absolute-home> start a STOPPED instance again in its existing home
|
|
3534
3714
|
[--model <m>] [--json] (same identity, worktree, notes and launch env; no
|
|
3535
3715
|
spawn hooks); --model replaces the recorded model
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Architecture reassessment: usable agents with replaceable services
|
|
2
|
+
|
|
3
|
+
Assessment of main `76c0ea4`, September 7, 2026. Requested by Juan after
|
|
4
|
+
operating the Desktop; incorporates Pepe's terminal, shortcut, split and soul
|
|
5
|
+
management feedback. This records the direction and implementation gaps, not a
|
|
6
|
+
claim that every item below has shipped.
|
|
7
|
+
|
|
8
|
+
The requirements authority for this assessment is Juan's September 3
|
|
9
|
+
“Architecture simplification and generlization” in his OATS project KB:
|
|
10
|
+
souls carry instructions and capabilities; a harvester converts ephemeral
|
|
11
|
+
state into knowledge accessible through the selected knowledge capability;
|
|
12
|
+
packages distribute souls and capabilities; OATS constructs and operates
|
|
13
|
+
instances across runtimes and platforms. The private Bookshelf README points
|
|
14
|
+
to September 1's adoption/sovereignty strategy. Its relevant constraint is that
|
|
15
|
+
changing a runtime or service provider must not erase the team's relationships
|
|
16
|
+
or working knowledge. The older August proposals about genomes, clothes and
|
|
17
|
+
turn-record experiments are background, not reasons to expand this release.
|
|
18
|
+
|
|
19
|
+
## Decision
|
|
20
|
+
|
|
21
|
+
Keep the existing package and capability-layer mechanism. Correct the places
|
|
22
|
+
where the CLI and GUI bypass it. The architecture is broadly right; the
|
|
23
|
+
operator experience and some service boundaries are incomplete. More
|
|
24
|
+
abstractions will not fix a terminal that cannot accept a screenshot.
|
|
25
|
+
|
|
26
|
+
| Component | Owns | Must not assume |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| OATS kernel | Soul/instance construction, configuration, lifecycle, host placement, scheduling, capability resolution | A particular knowledge provider, messaging service, or GUI |
|
|
29
|
+
| Session backend | Durable terminal, attach/detach, resize, literal input, observation | OATS soul/package semantics or task success |
|
|
30
|
+
| Knowledge capability | Knowledge access and representation; its harvester and promotion policy | Every installation uses OKF or stores memory in the same files |
|
|
31
|
+
| Messaging capability / aweb | Durable messages, identities, event subscriptions and delivery policy | A particular model harness or an open Desktop window |
|
|
32
|
+
| Task capability | Durable work and task semantics | A particular terminal backend |
|
|
33
|
+
| Desktop | Inspect effective configuration; explicit operator actions; terminal input, files, layout | Provider names, SSH commands, or a second lifecycle implementation |
|
|
34
|
+
|
|
35
|
+
Scheduling belongs to OATS, and a scheduled harvest is one use of scheduling.
|
|
36
|
+
Harvesting behavior belongs to the knowledge capability. Starting an agent,
|
|
37
|
+
writing a prompt, and producing reviewed knowledge are different outcomes.
|
|
38
|
+
The UI must retain those distinctions.
|
|
39
|
+
|
|
40
|
+
## What already fits, and what does not
|
|
41
|
+
|
|
42
|
+
`lib/core.mjs` already selects exclusive knowledge/messaging/tasks layers by
|
|
43
|
+
manifest, resolves scoped capability settings, dispatches lifecycle hooks, and
|
|
44
|
+
materializes runtime instructions. Jira and Linear demonstrate that a layer
|
|
45
|
+
can have alternative implementations. OKF's harvester is already an exported
|
|
46
|
+
capability agent. There is no reason to replace this machinery.
|
|
47
|
+
|
|
48
|
+
The remaining coupling is concrete:
|
|
49
|
+
|
|
50
|
+
- The retire receipt reads `hookResults.meta["oats.okf"].harvested`, although
|
|
51
|
+
current OKF does not supply a retire hook. Remove the dead provider-specific
|
|
52
|
+
inference; do not invent successful harvesting at retirement.
|
|
53
|
+
- Remote capability routing recognizes `okf harvest` specifically in
|
|
54
|
+
`bin/oats.mjs` and `lib/servers.mjs`.
|
|
55
|
+
- Desktop harvest actions and scheduled-harvest definitions construct
|
|
56
|
+
`oats okf harvest`; the editor recognizes that exact argv.
|
|
57
|
+
- The “brain” view presents `STATE.md`, `log.md` and `notes/` as universal
|
|
58
|
+
knowledge structure. These are conventions of the current setup.
|
|
59
|
+
|
|
60
|
+
The next service contract should expose the **effective knowledge provider and
|
|
61
|
+
its supported operations for a specific soul/home**. Discovering a capability
|
|
62
|
+
command namespace alone is insufficient: it does not promise a `harvest` verb
|
|
63
|
+
or a shared file layout. A provider may offer harvest, different operations,
|
|
64
|
+
or read-only access. Desktop must show only declared operations and route
|
|
65
|
+
execution through the selected CLI capability. The same resolution must apply
|
|
66
|
+
locally, remotely, and when a scheduled operation runs.
|
|
67
|
+
|
|
68
|
+
Keep that change small: explicit operation metadata, scoped discovery, and one
|
|
69
|
+
invocation route using the existing capability engine. Do not add a plugin
|
|
70
|
+
framework, generic workflow language, or a second knowledge store. Demonstrate
|
|
71
|
+
replaceability with a tiny alternative-provider fixture that uses a different
|
|
72
|
+
command and storage layout; a full second product is unnecessary.
|
|
73
|
+
|
|
74
|
+
## tmux and Herdr
|
|
75
|
+
|
|
76
|
+
The current kernel defaults to a shared tmux session (`pi-agents`) with a
|
|
77
|
+
window per instance. An agent does not require another tmux server. The GUI
|
|
78
|
+
creates a temporary session linking only the selected window. That isolates
|
|
79
|
+
viewers: switching a terminal elsewhere cannot redirect a GUI tab to another
|
|
80
|
+
agent, and an agent exiting cannot silently expose a sibling under its label.
|
|
81
|
+
The `oatsdesk-*` name in Juan's screenshot is this viewer. Its status bar can
|
|
82
|
+
be hidden with a viewer-local setting. Do not alter global tmux settings or
|
|
83
|
+
merge/move live agent sessions to solve a display defect.
|
|
84
|
+
|
|
85
|
+
Herdr 0.8.2 is a separate terminal runtime, not a tmux-compatible superset.
|
|
86
|
+
Its documented direct terminal attachment, socket API, events and SSH support
|
|
87
|
+
are useful. Its semantic agent state comes from detection and/or integrations;
|
|
88
|
+
it is richer evidence, not proof that a particular message was consumed.
|
|
89
|
+
The [socket API](https://herdr.dev/docs/socket-api/) explicitly distinguishes
|
|
90
|
+
agent-state waits from arbitrary command completion. The
|
|
91
|
+
[remote documentation](https://herdr.dev/docs/persistence-remote/) also
|
|
92
|
+
separates a local thin client from running a client entirely on the server;
|
|
93
|
+
only the former can directly bridge the local desktop clipboard.
|
|
94
|
+
|
|
95
|
+
Keep one OATS session contract and both existing adapters for now. Do not add
|
|
96
|
+
another supervisor above them. Qualify Herdr's actual literal multi-line
|
|
97
|
+
input, exact occupant checks, detach, resize, process exit and remote behavior
|
|
98
|
+
before selecting it as the default for new instances. Inspect `pane run`
|
|
99
|
+
implementation before replacing it merely because its name sounds like shell
|
|
100
|
+
execution. Removing tmux is a later deployment choice, not a prerequisite for
|
|
101
|
+
consistent UI or remote agents. Existing tmux agents stay where they are.
|
|
102
|
+
|
|
103
|
+
## Desktop correction sequence
|
|
104
|
+
|
|
105
|
+
1. **Terminal essentials.** Hide viewer chrome, accept dropped files and pasted
|
|
106
|
+
images without pressing Enter, upload remote files to the execution host,
|
|
107
|
+
preserve the destination tab across async work, and show transfer failures.
|
|
108
|
+
Keep a single GUI and bounded viewers. This is the immediate implementation.
|
|
109
|
+
2. **Keyboard and splits.** Restore Ctrl+Tab navigation inside Mac terminals;
|
|
110
|
+
let an already-open terminal fill an empty split without a second attach;
|
|
111
|
+
expose draggable and keyboard-operable separators. Follow up specific
|
|
112
|
+
keyboard reports with real input tests, especially terminal editing keys
|
|
113
|
+
and non-US layouts. App shortcuts must not silently steal terminal edits.
|
|
114
|
+
3. **Souls & capabilities.** Replace the launch-centric roster with a management
|
|
115
|
+
surface showing souls, installed capabilities, effective layer providers,
|
|
116
|
+
source/version, scoped activation and runtime defaults. Inspect and launch
|
|
117
|
+
are distinct actions. Edits use existing CLI operations with the exact scope
|
|
118
|
+
visible; installing a capability must not silently activate it. This is not
|
|
119
|
+
a marketplace or Team Builder project.
|
|
120
|
+
4. **Provider-neutral operations.** Introduce the scoped operation contract
|
|
121
|
+
above, then use it for manual harvest, schedules and knowledge inspection.
|
|
122
|
+
No second hardcoded default is accepted as a generalization.
|
|
123
|
+
5. **Operating rollout.** Enable schedules per team with the owner's learning
|
|
124
|
+
policy. Capture-lock recovery is a separate reliability issue affecting
|
|
125
|
+
unattended harvesting; it does not block arbitrary scheduled wakes.
|
|
126
|
+
|
|
127
|
+
Acceptance must include real Electron file objects, actual terminal bytes,
|
|
128
|
+
remote transfer hashes, split focus/resize without extra PTYs, and a provider
|
|
129
|
+
replacement fixture. Unit tests of mocked argv alone do not establish these
|
|
130
|
+
boundaries. Reuse the existing agents and GUI sparingly; no model is needed to
|
|
131
|
+
exercise attachment transport or terminal layout.
|
package/docs/desktop.md
CHANGED
|
@@ -74,8 +74,8 @@ app focus, and Retry. Until a compatible CLI is verified, the Soul roster's
|
|
|
74
74
|
**Spawn** buttons are disabled behind one card showing what was detected,
|
|
75
75
|
what is required, **Choose oats…** (pick the binary yourself — the choice
|
|
76
76
|
persists), **Retry**, a docs link, and the copyable install command. The
|
|
77
|
-
app never installs
|
|
78
|
-
CLI boundary
|
|
77
|
+
app never installs the CLI itself. Memory harvest runs through the same
|
|
78
|
+
CLI boundary and is available from an instance's action menu.
|
|
79
79
|
|
|
80
80
|
The probe/mutation contract is specified in
|
|
81
81
|
[desktop-cli-api.md](desktop-cli-api.md).
|
|
@@ -95,6 +95,32 @@ first-class: they appear in the roster with a `local` chip, their brains
|
|
|
95
95
|
and knowledge render, and they spawn like any other soul. Launch flags for
|
|
96
96
|
scripted use: `--dir <workspace>` and `OATS_DESKTOP_PORT`.
|
|
97
97
|
|
|
98
|
+
## Scheduling agents and wake messages
|
|
99
|
+
|
|
100
|
+
Open **Schedules** in the selected workspace to launch a new agent on a cron,
|
|
101
|
+
wake an existing agent with a message, or harvest its knowledge. **Schedule…**
|
|
102
|
+
on a Soul roster card preselects that soul. Choose the task or message, repeat
|
|
103
|
+
pattern and time zone; new agent jobs also offer runtime, model, permissions
|
|
104
|
+
and session backend. The list shows the next run, last observed outcome and
|
|
105
|
+
whether the host scheduler is enabled. Pause, edit, run now and delete operate
|
|
106
|
+
on that workspace's saved jobs. Launching an agent is reported separately from
|
|
107
|
+
the end of its run; neither means its task succeeded.
|
|
108
|
+
|
|
109
|
+
The Spawn dialog also has an optional **Recurring wake-up** setting. It binds
|
|
110
|
+
the schedule to the newly created home, preserving that agent's identity and
|
|
111
|
+
work. A wake starts that same home if it is stopped, then sends the saved
|
|
112
|
+
message through its terminal when ready. It sends no interrupt; the harness
|
|
113
|
+
decides when to process submitted input. Missed cron times are skipped. If the
|
|
114
|
+
agent is created but its wake schedule cannot be saved, the dialog reports both
|
|
115
|
+
facts and directs you to Schedules without spawning another agent.
|
|
116
|
+
|
|
117
|
+
Use **Enable host scheduler** when the view reports that the timer or workspace
|
|
118
|
+
registration is missing. One host timer handles its registered workspaces with
|
|
119
|
+
a shared limit on scheduled agent launches. The GUI can then be closed. For a
|
|
120
|
+
registered remote workspace the timer and definitions live on that server, so
|
|
121
|
+
they do not depend on the Mac staying awake. See [Schedules](schedules.md) for
|
|
122
|
+
the CLI, cron semantics, observed outcomes and recovery commands.
|
|
123
|
+
|
|
98
124
|
## Migrating from the web panel / TUI pane
|
|
99
125
|
|
|
100
126
|
0.18.2 removes the legacy `oats.web` browser panel, `oats pane`, and the
|
|
@@ -185,3 +211,19 @@ certificate auto-discovery disabled) and
|
|
|
185
211
|
marked build-verify mode (inventory + strict codesign verification +
|
|
186
212
|
node-pty ABI, no GUI launch); a local
|
|
187
213
|
interactive run may also exercise the launch phase.
|
|
214
|
+
|
|
215
|
+
### Attach files and screenshots
|
|
216
|
+
|
|
217
|
+
Drop a file onto an agent terminal to insert its path into that agent's draft.
|
|
218
|
+
Pasting an image from the clipboard uses the same attachment path. Neither
|
|
219
|
+
operation presses Enter. Text paste continues to use the terminal's normal
|
|
220
|
+
paste behavior. A drop targets the pane under the pointer, including a visible
|
|
221
|
+
pane in a split.
|
|
222
|
+
|
|
223
|
+
Local files are referenced in place. Clipboard images are saved privately in
|
|
224
|
+
Desktop's application-data `attachments` directory and retained so an agent can
|
|
225
|
+
read them later. For remote terminals, Desktop calls the installed CLI's
|
|
226
|
+
`session upload` operation; it inserts the returned path only after the file
|
|
227
|
+
has reached the execution host. Both CLI installations must advertise
|
|
228
|
+
`session-upload`. A failed transfer leaves the draft unchanged and shows an
|
|
229
|
+
error in the terminal. Each drop/paste accepts up to 16 files totaling 25 MB.
|
|
@@ -181,6 +181,27 @@ pane in that exact window. Herdr additionally verifies the original terminal ID.
|
|
|
181
181
|
The broker owns busy/approval policy and must not interpret `submitted` as
|
|
182
182
|
processing acknowledgement.
|
|
183
183
|
|
|
184
|
+
### Attachments
|
|
185
|
+
|
|
186
|
+
A viewer that drops a file or pastes an image gives the agent a path, never
|
|
187
|
+
terminal input. `oats session upload --file <local> --home <abs>` stores a
|
|
188
|
+
copy as a private file under `<home>/.oats-attachments/` (directory 0700,
|
|
189
|
+
file 0600, `name-2` on collision) and answers `{path, bytes, sha256}`; the
|
|
190
|
+
caller pastes `path` into the still-live session itself. With `--server <id>`
|
|
191
|
+
and `--instance <name>` (or `--home`), the same saved route as attach is
|
|
192
|
+
resolved, the remote must list `session-upload` in both its `remote` and
|
|
193
|
+
`features` probe arrays, and the bytes travel on ssh stdin into `oats session
|
|
194
|
+
receive --home <abs> --name <file>` on the execution host. Each side holds
|
|
195
|
+
the whole file in memory up to the 64 MiB kernel bound (this is a bounded
|
|
196
|
+
transfer, not end-to-end streaming); the receiver reads stdin event-driven,
|
|
197
|
+
refuses above the bound before writing, and allocates the destination
|
|
198
|
+
exclusively (`name-2`, `name-3` when taken), so simultaneous uploads of one
|
|
199
|
+
name never overwrite each other and a planted symlink is never followed. The
|
|
200
|
+
attachments directory must be a real directory inside the home. The local
|
|
201
|
+
side refuses the result unless the remote's size and sha256 equal the local
|
|
202
|
+
file's. A remote file is never a local path over SSH. Attachments live and
|
|
203
|
+
die with the home.
|
|
204
|
+
|
|
184
205
|
Capability spawn hooks register a pending home before runtime allocation;
|
|
185
206
|
inspection becomes available once its receipt is persisted. Retire hooks
|
|
186
207
|
unregister after quiescence. The broker must tolerate this lifecycle order and
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# OATS v0.22.11
|
|
2
|
+
|
|
3
|
+
Schedules: cron-driven agent work that runs on the execution host, from
|
|
4
|
+
the CLI and from Desktop, including while the GUI is closed.
|
|
5
|
+
|
|
6
|
+
## Schedules (kernel)
|
|
7
|
+
|
|
8
|
+
`oats schedule` keeps a per-workspace `oats-schedules.json` of jobs of three
|
|
9
|
+
kinds: **spawn** launches a soul as a fresh disposable instance with a task,
|
|
10
|
+
**command** runs an `oats` command in a directory (a harvest, for example),
|
|
11
|
+
and **wake** starts a stopped instance if needed and sends it a message
|
|
12
|
+
through its terminal. Cron expressions are five-field with an IANA time
|
|
13
|
+
zone, evaluated by croner; missed minutes are skipped, never caught up.
|
|
14
|
+
|
|
15
|
+
One host timer (`oats schedule host install`: a launchd agent or a systemd
|
|
16
|
+
user timer) ticks every registered workspace once a minute under a shared
|
|
17
|
+
limit on concurrent scheduled launches. A remote workspace's schedules live
|
|
18
|
+
and run on that server; `--server` routes every subcommand there.
|
|
19
|
+
`oats spawn --wake-file <json>` binds a wake schedule to the new home.
|
|
20
|
+
|
|
21
|
+
Outcomes are what the kernel observed, not a claim that a task succeeded:
|
|
22
|
+
launched, active, ended, stopped, launch-failed, delivered, started,
|
|
23
|
+
skipped, and unknown. A launch whose effects the kernel cannot confirm (a
|
|
24
|
+
timed-out or envelope-less command, an incomplete rollback) keeps its slot
|
|
25
|
+
as unknown until `oats schedule reconcile <id>` adopts a named receipt or
|
|
26
|
+
the operator clears it with `--clear`. A wake's launch slot follows the
|
|
27
|
+
runtime through the session start receipt, so a persistent home whose
|
|
28
|
+
process exited frees the slot. Locks are never reclaimed automatically;
|
|
29
|
+
an unreadable or dead owner names the directory to remove.
|
|
30
|
+
`oats version --json` advertises feature `schedule` and `scheduleApi: 1`.
|
|
31
|
+
See docs/schedules.md.
|
|
32
|
+
|
|
33
|
+
## Schedules (Desktop)
|
|
34
|
+
|
|
35
|
+
A **Schedules** view per workspace: create, edit, pause, run now, check run
|
|
36
|
+
state and delete jobs through a single JSON spec; the Soul roster's
|
|
37
|
+
**Schedule…** preselects the soul; the spawn dialog offers a recurring wake
|
|
38
|
+
for the new agent, forwarded inline to a remote server. Mutations require
|
|
39
|
+
an explicit workspace. The reconcile result and its recovery instructions
|
|
40
|
+
stay visible across polling. **Enable host scheduler** installs the timer
|
|
41
|
+
and registers the workspace. See docs/desktop.md.
|
|
42
|
+
|
|
43
|
+
## Tests
|
|
44
|
+
|
|
45
|
+
The kernel adds `test/schedule.test.mjs` (fixture homes, injected
|
|
46
|
+
spawn/start/input, dummy commands, fixture-only host unit paths) and
|
|
47
|
+
`test/schedule-session.test.mjs`, which drives a real private tmux socket
|
|
48
|
+
to prove literal delivery, same-minute deduplication and slot release when
|
|
49
|
+
a runtime stops. Nothing in the suites touches a live timer.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# OATS v0.22.12
|
|
2
|
+
|
|
3
|
+
The same reviewed content as the unpublished v0.22.11 tag, whose hosted
|
|
4
|
+
run failed in test fixtures before anything was published; those fixtures
|
|
5
|
+
are fixed here.
|
|
6
|
+
|
|
7
|
+
Schedules: cron-driven agent work that runs on the execution host, from
|
|
8
|
+
the CLI and from Desktop, including while the GUI is closed.
|
|
9
|
+
|
|
10
|
+
## Schedules (kernel)
|
|
11
|
+
|
|
12
|
+
`oats schedule` keeps a per-workspace `oats-schedules.json` of jobs of three
|
|
13
|
+
kinds: **spawn** launches a soul as a fresh disposable instance with a task,
|
|
14
|
+
**command** runs an `oats` command in a directory (a harvest, for example),
|
|
15
|
+
and **wake** starts a stopped instance if needed and sends it a message
|
|
16
|
+
through its terminal. Cron expressions are five-field with an IANA time
|
|
17
|
+
zone, evaluated by croner; missed minutes are skipped, never caught up.
|
|
18
|
+
|
|
19
|
+
One host timer (`oats schedule host install`: a launchd agent or a systemd
|
|
20
|
+
user timer) ticks every registered workspace once a minute under a shared
|
|
21
|
+
limit on concurrent scheduled launches. A remote workspace's schedules live
|
|
22
|
+
and run on that server; `--server` routes every subcommand there.
|
|
23
|
+
`oats spawn --wake-file <json>` binds a wake schedule to the new home.
|
|
24
|
+
|
|
25
|
+
Outcomes are what the kernel observed, not a claim that a task succeeded:
|
|
26
|
+
launched, active, ended, stopped, launch-failed, delivered, started,
|
|
27
|
+
skipped, and unknown. A launch whose effects the kernel cannot confirm (a
|
|
28
|
+
timed-out or envelope-less command, an incomplete rollback) keeps its slot
|
|
29
|
+
as unknown until `oats schedule reconcile <id>` adopts a named receipt or
|
|
30
|
+
the operator clears it with `--clear`. A wake's launch slot follows the
|
|
31
|
+
runtime through the session start receipt, so a persistent home whose
|
|
32
|
+
process exited frees the slot. Locks are never reclaimed automatically;
|
|
33
|
+
an unreadable or dead owner names the directory to remove.
|
|
34
|
+
`oats version --json` advertises feature `schedule` and `scheduleApi: 1`.
|
|
35
|
+
See docs/schedules.md.
|
|
36
|
+
|
|
37
|
+
## Schedules (Desktop)
|
|
38
|
+
|
|
39
|
+
A **Schedules** view per workspace: create, edit, pause, run now, check run
|
|
40
|
+
state and delete jobs through a single JSON spec; the Soul roster's
|
|
41
|
+
**Schedule…** preselects the soul; the spawn dialog offers a recurring wake
|
|
42
|
+
for the new agent, forwarded inline to a remote server. Mutations require
|
|
43
|
+
an explicit workspace. The reconcile result and its recovery instructions
|
|
44
|
+
stay visible across polling. **Enable host scheduler** installs the timer
|
|
45
|
+
and registers the workspace. See docs/desktop.md.
|
|
46
|
+
|
|
47
|
+
## Tests
|
|
48
|
+
|
|
49
|
+
The kernel adds `test/schedule.test.mjs` (fixture homes, injected
|
|
50
|
+
spawn/start/input, dummy commands, fixture-only host unit paths) and
|
|
51
|
+
`test/schedule-session.test.mjs`, which drives a real private tmux socket
|
|
52
|
+
to prove literal delivery, same-minute deduplication and slot release when
|
|
53
|
+
a runtime stops. Nothing in the suites touches a live timer.
|