@awebai/oats 0.22.9 → 0.22.12
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 +150 -4
- package/docs/desktop.md +28 -2
- package/docs/release-notes/v0.22.10.md +25 -0
- package/docs/release-notes/v0.22.11.md +49 -0
- package/docs/release-notes/v0.22.12.md +53 -0
- package/docs/schedules.md +143 -0
- package/lib/schedule-host.mjs +150 -0
- package/lib/schedule.mjs +731 -0
- package/lib/servers.mjs +16 -0
- package/package.json +3 -2
package/bin/oats.mjs
CHANGED
|
@@ -39,8 +39,10 @@ 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";
|
|
44
46
|
|
|
45
47
|
const args = process.argv.slice(2);
|
|
46
48
|
const cmd = args[0];
|
|
@@ -2754,6 +2756,34 @@ function spawnCmd() {
|
|
|
2754
2756
|
const relativeRoot = flag("relative-root");
|
|
2755
2757
|
if (relativeRoot !== undefined && (relativeRoot === true || !String(relativeRoot).trim())) bail("E_BAD_ARGS", "--relative-root needs an agents-root path");
|
|
2756
2758
|
if (relativeRoot && !relativeTo) bail("E_BAD_ARGS", "--relative-root only qualifies --relative-to/--parent");
|
|
2759
|
+
// Wake at launch: --wake-file <private JSON {cron, tz, message, enabled}>
|
|
2760
|
+
// is the backend bridge; --wake-every/--wake-cron/--wake-tz/--wake-message
|
|
2761
|
+
// (or --wake-message-file) are the human sugar for the same object. The
|
|
2762
|
+
// wake job is saved AFTER a successful spawn, bound to the returned home.
|
|
2763
|
+
let wake;
|
|
2764
|
+
try {
|
|
2765
|
+
const wakeFile = flag("wake-file");
|
|
2766
|
+
if (wakeFile === true) bail("E_BAD_ARGS", "--wake-file needs a path");
|
|
2767
|
+
const wakeJson = flag("wake-json");
|
|
2768
|
+
if (wakeJson === true) bail("E_BAD_ARGS", "--wake-json needs the wake object as JSON text");
|
|
2769
|
+
if (wakeJson) {
|
|
2770
|
+
let doc; try { doc = JSON.parse(wakeJson); } catch (e) { bail("E_SCHEDULE_INVALID", `--wake-json is not valid JSON: ${e.message}`); }
|
|
2771
|
+
if (!doc || typeof doc !== "object") bail("E_SCHEDULE_INVALID", "--wake-json must hold {cron, tz, message, enabled}");
|
|
2772
|
+
wake = { cron: doc.cron, tz: doc.tz, message: doc.message, enabled: doc.enabled === undefined ? true : doc.enabled };
|
|
2773
|
+
} else if (wakeFile) {
|
|
2774
|
+
if (!existsSync(wakeFile)) bail("E_BAD_ARGS", `--wake-file not found: ${wakeFile}`);
|
|
2775
|
+
let doc; try { doc = JSON.parse(readFileSync(wakeFile, "utf8")); } catch (e) { bail("E_SCHEDULE_INVALID", `--wake-file is not valid JSON: ${e.message}`); }
|
|
2776
|
+
if (!doc || typeof doc !== "object") bail("E_SCHEDULE_INVALID", "--wake-file must hold {cron, tz, message, enabled}");
|
|
2777
|
+
wake = { cron: doc.cron, tz: doc.tz, message: doc.message, enabled: doc.enabled === undefined ? true : doc.enabled };
|
|
2778
|
+
} else if (flag("wake-every") !== undefined || flag("wake-cron") !== undefined) {
|
|
2779
|
+
const mf = flag("wake-message-file");
|
|
2780
|
+
if (mf === true) bail("E_BAD_ARGS", "--wake-message-file needs a path");
|
|
2781
|
+
if (mf && !existsSync(mf)) bail("E_BAD_ARGS", `--wake-message-file not found: ${mf}`);
|
|
2782
|
+
const message = mf ? readFileSync(mf, "utf8") : flag("wake-message") === true ? undefined : flag("wake-message");
|
|
2783
|
+
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 });
|
|
2784
|
+
}
|
|
2785
|
+
if (wake && (typeof wake.message !== "string" || !wake.message.trim())) bail("E_SCHEDULE_INVALID", "wake message: non-empty text is required");
|
|
2786
|
+
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message); throw e; }
|
|
2757
2787
|
let r;
|
|
2758
2788
|
try {
|
|
2759
2789
|
r = spawnInstance(root, agent, {
|
|
@@ -2770,11 +2800,19 @@ function spawnCmd() {
|
|
|
2770
2800
|
if (TYPED_CLI_FAILURES.has(e?.code)) throw e;
|
|
2771
2801
|
bail(e.code === "E_RELATIVE_AMBIGUOUS" ? "E_RELATIVE_AMBIGUOUS" : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
2772
2802
|
}
|
|
2803
|
+
// The instance exists from here on: a failed wake save is reported beside
|
|
2804
|
+
// the full receipt, never hidden, and never causes a second spawn.
|
|
2805
|
+
let wakeSchedule, wakeScheduleError;
|
|
2806
|
+
if (wake) {
|
|
2807
|
+
try { wakeSchedule = saveWakeForHome(scheduleScopeOf(workspaceOf(root)), { instance: r.instance, home: r.home, wake }); }
|
|
2808
|
+
catch (e) { wakeScheduleError = { code: e.code || "E_SCHEDULE_FAILED", message: e.message }; r.warnings = [...(r.warnings || []), `wake schedule NOT saved: ${e.message}`]; }
|
|
2809
|
+
}
|
|
2773
2810
|
if (JSON_MODE) {
|
|
2774
2811
|
// Desktop CLI API v1 spawn result — a FIXED shape (see docs/desktop-cli-api.md).
|
|
2775
2812
|
jsonOk({
|
|
2776
2813
|
instance: r.instance, agent: r.agent, home: r.home, work: r.work,
|
|
2777
2814
|
branch: r.branch || null, launched: r.launched, warnings: r.warnings || [],
|
|
2815
|
+
...(wakeSchedule ? { wakeSchedule } : {}), ...(wakeScheduleError ? { wakeScheduleError } : {}),
|
|
2778
2816
|
tmux: r.tmux || null, repo: r.repo || null, runtime: r.runtime || null,
|
|
2779
2817
|
model: r.model || null, parent: r.parentInstance || null,
|
|
2780
2818
|
sibling: r.siblingInstance || null, relation: r.relation || null,
|
|
@@ -2786,6 +2824,8 @@ function spawnCmd() {
|
|
|
2786
2824
|
}
|
|
2787
2825
|
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
2826
|
console.log(` home: ${shortPath(r.home)}`);
|
|
2827
|
+
if (wakeSchedule) console.log(` wake: schedule ${wakeSchedule.id} (${wakeSchedule.cron} ${wakeSchedule.tz}), next ${wakeSchedule.nextRun || "disabled"}`);
|
|
2828
|
+
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
2829
|
if (!r.launched) console.log(` launch: (cd ${shortPath(r.home)} && ${r.command})`);
|
|
2790
2830
|
for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
|
|
2791
2831
|
console.log(` attach: ${r.attach}`);
|
|
@@ -2809,7 +2849,11 @@ function retireCmd() {
|
|
|
2809
2849
|
// Stdout carries only the envelope in JSON mode (the Desktop parses it).
|
|
2810
2850
|
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
2851
|
}
|
|
2852
|
+
const retiringHome = homeFlag || findInstanceHome(root, name);
|
|
2812
2853
|
const r = retireInstance(root, name, { home: homeFlag, self: isSelf, deleteBranch: args.includes("--delete-branch"), keepDir: args.includes("--keep-dir"), force: args.includes("--force") });
|
|
2854
|
+
// A retired home's wake jobs are forgotten (definitions only; nothing is
|
|
2855
|
+
// stopped by this); a deferred self-retire keeps them until the home is gone.
|
|
2856
|
+
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
2857
|
// Deferred self-retire: nothing has been inspected, run, or removed yet. The
|
|
2814
2858
|
// caller's window dies first; a detached process then retires the instance
|
|
2815
2859
|
// as an external operator and writes its outcome beside the home.
|
|
@@ -2847,6 +2891,56 @@ function retireCmd() {
|
|
|
2847
2891
|
if (isSelf) console.log("This window dies in ~8s — say any goodbyes now.");
|
|
2848
2892
|
}
|
|
2849
2893
|
|
|
2894
|
+
/** `oats schedule ...`: workspace-scoped definitions, host-owned execution
|
|
2895
|
+
* (lib/schedule.mjs). Every subcommand answers the envelope; nothing here
|
|
2896
|
+
* launches unless a job is due or run-now is asked. */
|
|
2897
|
+
function scheduleCmd() {
|
|
2898
|
+
const sub = args[1];
|
|
2899
|
+
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
2900
|
+
// One schedule-owning scope for a directory: the team workspace (the
|
|
2901
|
+
// config level declaring the team), else the outermost config level.
|
|
2902
|
+
const ws = scheduleScopeOf(dirFlag());
|
|
2903
|
+
const io = { hostStatus: () => hostUnitStatus() };
|
|
2904
|
+
const out = (result) => { if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2)); };
|
|
2905
|
+
const readSpec = () => {
|
|
2906
|
+
const inline = flag("spec-json");
|
|
2907
|
+
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" }); } }
|
|
2908
|
+
const f = flag("file");
|
|
2909
|
+
if (!f || f === true) throw scheduleError("E_BAD_ARGS", "--file <spec.json> is required (a private JSON file with the definition)");
|
|
2910
|
+
if (!existsSync(f)) throw scheduleError("E_BAD_ARGS", `spec file not found: ${f}`);
|
|
2911
|
+
try { return JSON.parse(readFileSync(f, "utf8")); } catch (e) { throw scheduleError("E_SCHEDULE_INVALID", `${f} is not valid JSON: ${e.message}`, { field: "file" }); }
|
|
2912
|
+
};
|
|
2913
|
+
const needId = () => { if (!id) throw scheduleError("E_BAD_ARGS", `oats schedule ${sub} <id>`); return id; };
|
|
2914
|
+
try {
|
|
2915
|
+
switch (sub) {
|
|
2916
|
+
case "list": return out(listSchedules(ws, io));
|
|
2917
|
+
case "show": return out({ schedule: describeSchedule(ws, needId(), io) });
|
|
2918
|
+
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) }); }
|
|
2919
|
+
case "update": return out({ schedule: updateSchedule(ws, needId(), readSpec(), io) });
|
|
2920
|
+
case "enable": return out({ schedule: setScheduleEnabled(ws, needId(), true, io) });
|
|
2921
|
+
case "disable": return out({ schedule: setScheduleEnabled(ws, needId(), false, io) });
|
|
2922
|
+
case "run": return out(runScheduleNow(ws, needId(), { io, force: args.includes("--force") }));
|
|
2923
|
+
case "remove": return out(removeSchedule(ws, needId(), { force: args.includes("--force") }));
|
|
2924
|
+
case "reconcile": return out(reconcileSchedule(ws, needId(), { io, clear: args.includes("--clear") }));
|
|
2925
|
+
case "tick": {
|
|
2926
|
+
const dryRun = args.includes("--dry-run");
|
|
2927
|
+
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
2928
|
+
const reg = readRegistry();
|
|
2929
|
+
const considered = withHostLock(() => tickWorkspace(ws, { io, reg, wsList: reg.workspaces.includes(ws) ? reg.workspaces : [...reg.workspaces, ws], dryRun }));
|
|
2930
|
+
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws, io) });
|
|
2931
|
+
}
|
|
2932
|
+
case "host": {
|
|
2933
|
+
const op = args[2];
|
|
2934
|
+
if (op === "install") { registerWorkspace(ws); installHostUnit(); return out({ scheduler: schedulerStatus(ws, io) }); }
|
|
2935
|
+
if (op === "uninstall") { unregisterWorkspace(ws); if (!readRegistry().workspaces.length) uninstallHostUnit(); return out({ scheduler: schedulerStatus(ws, io) }); }
|
|
2936
|
+
if (op === "status") return out({ scheduler: schedulerStatus(ws, io) });
|
|
2937
|
+
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
2938
|
+
}
|
|
2939
|
+
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]");
|
|
2940
|
+
}
|
|
2941
|
+
} catch (e) { cmdFail(e.code || "E_SCHEDULE_FAILED", e.message); }
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2850
2944
|
async function sessionCmd() {
|
|
2851
2945
|
try {
|
|
2852
2946
|
const home = flag("home");
|
|
@@ -3161,7 +3255,7 @@ function versionCmd() {
|
|
|
3161
3255
|
// on it (an older CLI without the surface must fail closed with a
|
|
3162
3256
|
// reason, not an argument error). `features`: kernel abilities a peer
|
|
3163
3257
|
// 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"] }));
|
|
3258
|
+
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"], features: ["retire-home", "session-start", "schedule"], scheduleApi: SCHEDULE_API }));
|
|
3165
3259
|
return;
|
|
3166
3260
|
}
|
|
3167
3261
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3316,6 +3410,32 @@ function serverRouteCmd() {
|
|
|
3316
3410
|
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
3411
|
return;
|
|
3318
3412
|
}
|
|
3413
|
+
if (cmd === "schedule") {
|
|
3414
|
+
// Host-owned: the subcommand runs in the server's registered workspace.
|
|
3415
|
+
// A local --file spec is read here and travels inline; the remote
|
|
3416
|
+
// cannot read this machine's files.
|
|
3417
|
+
const rest = [];
|
|
3418
|
+
for (let i = 1; i < args.length; i++) {
|
|
3419
|
+
const a = args[i];
|
|
3420
|
+
if (a === "--server") { i++; continue; }
|
|
3421
|
+
if (a === "--json") continue;
|
|
3422
|
+
if (a === "--file") {
|
|
3423
|
+
const f = args[++i];
|
|
3424
|
+
if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--file needs a path");
|
|
3425
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `spec file not found: ${f}`);
|
|
3426
|
+
rest.push("--spec-json", readFileSync(f, "utf8"));
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
3429
|
+
rest.push(a);
|
|
3430
|
+
}
|
|
3431
|
+
let out;
|
|
3432
|
+
try { out = scheduleRemote(id, rest); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3433
|
+
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3434
|
+
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3435
|
+
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "schedule command failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
3436
|
+
console.log(JSON.stringify(out.envelope.result, null, 2));
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3319
3439
|
if (cmd === "session") {
|
|
3320
3440
|
const addr = { instance: flag("instance") === true ? undefined : flag("instance"), home: flag("home") === true ? undefined : flag("home") };
|
|
3321
3441
|
if (args[1] === "inspect") {
|
|
@@ -3365,6 +3485,24 @@ function serverRouteCmd() {
|
|
|
3365
3485
|
rest.push("--task", readFileSync(f, "utf8"));
|
|
3366
3486
|
continue;
|
|
3367
3487
|
}
|
|
3488
|
+
// A wake spec for a routed spawn travels as validated JSON text; the
|
|
3489
|
+
// host saves it in its own scope. Local files never travel as paths.
|
|
3490
|
+
if (a === "--wake-file") {
|
|
3491
|
+
const f = args[++i];
|
|
3492
|
+
if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--wake-file needs a path");
|
|
3493
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `wake file not found: ${f}`);
|
|
3494
|
+
let doc; try { doc = JSON.parse(readFileSync(f, "utf8")); } catch (e) { bail("E_SCHEDULE_INVALID", `--wake-file is not valid JSON: ${e.message}`); }
|
|
3495
|
+
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}");
|
|
3496
|
+
rest.push("--wake-json", JSON.stringify({ cron: doc.cron, tz: doc.tz, message: doc.message, enabled: doc.enabled === undefined ? true : doc.enabled }));
|
|
3497
|
+
continue;
|
|
3498
|
+
}
|
|
3499
|
+
if (a === "--wake-message-file") {
|
|
3500
|
+
const f = args[++i];
|
|
3501
|
+
if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--wake-message-file needs a path");
|
|
3502
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `wake message file not found: ${f}`);
|
|
3503
|
+
rest.push("--wake-message", readFileSync(f, "utf8"));
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3368
3506
|
rest.push(a);
|
|
3369
3507
|
}
|
|
3370
3508
|
let routed;
|
|
@@ -3426,10 +3564,10 @@ try {
|
|
|
3426
3564
|
// and exits 0 BEFORE any dispatch: a fresh operator inspects --help before
|
|
3427
3565
|
// using a command, and `install --help` once ran the bare restore while
|
|
3428
3566
|
// `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"]);
|
|
3567
|
+
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
3568
|
const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
|
|
3431
3569
|
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();
|
|
3570
|
+
if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf", "schedule"].includes(cmd)) serverRouteCmd();
|
|
3433
3571
|
else if (cmd === "server") serverCmd();
|
|
3434
3572
|
else if (cmd === "doctor") {
|
|
3435
3573
|
const doctorDir = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
|
|
@@ -3459,6 +3597,7 @@ else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
|
|
|
3459
3597
|
// Same rule as the inner catch: a typed CLI failure surfaces with its own code
|
|
3460
3598
|
// through the shared boundary, never re-badged as a spawn-mechanism failure.
|
|
3461
3599
|
else if (cmd === "session") await sessionCmd();
|
|
3600
|
+
else if (cmd === "schedule") scheduleCmd();
|
|
3462
3601
|
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
3602
|
else if (cmd === "retire") retireCmd();
|
|
3464
3603
|
else if (cmd === "create") createCmd();
|
|
@@ -3530,6 +3669,13 @@ Usage:
|
|
|
3530
3669
|
[--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
|
|
3531
3670
|
[--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
|
|
3532
3671
|
oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
|
|
3672
|
+
oats schedule list|show <id>|add <id> --file <spec.json>|update <id> --file <spec.json>
|
|
3673
|
+
enable|disable|run|remove|reconcile <id> workspace-scoped, host-owned schedules (spawn,
|
|
3674
|
+
tick [--dry-run] [--host] command or wake jobs on a five-field cron with an
|
|
3675
|
+
host install|uninstall|status explicit IANA tz; see docs/schedules.md); --server
|
|
3676
|
+
routes to that host's workspace
|
|
3677
|
+
oats spawn ... --wake-file <json> | --wake-every <N> --wake-message <text> save a wake schedule
|
|
3678
|
+
bound to the new instance's home (docs/schedules.md)
|
|
3533
3679
|
oats session start --home <absolute-home> start a STOPPED instance again in its existing home
|
|
3534
3680
|
[--model <m>] [--json] (same identity, worktree, notes and launch env; no
|
|
3535
3681
|
spawn hooks); --model replaces the recorded model
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# OATS v0.22.10
|
|
2
|
+
|
|
3
|
+
One Desktop defect an operator meets in the sidebar: the instance actions
|
|
4
|
+
control was a native select that did not behave like the rest of the app.
|
|
5
|
+
|
|
6
|
+
## Sidebar instance actions: an app-styled button and popover
|
|
7
|
+
|
|
8
|
+
Each instance row's actions control (Harvest knowledge, Retire instance) was
|
|
9
|
+
a native `<select>` labelled with an ellipsis. It rendered with the
|
|
10
|
+
platform's own look, opened as a native menu, and its keyboard and focus
|
|
11
|
+
behaviour did not match the app's other controls. It is now an app-styled
|
|
12
|
+
button that opens a popover. The menu opens from the keyboard or pointer and
|
|
13
|
+
moves focus into its items. Escape and choosing an action close it and return
|
|
14
|
+
focus to the button; clicking outside also dismisses it.
|
|
15
|
+
|
|
16
|
+
Safety is unchanged. The per-instance pending guard still holds: while a
|
|
17
|
+
harvest or a retirement is in flight for an instance, every control for that
|
|
18
|
+
instance stays disabled until the operation reports, so a second click
|
|
19
|
+
cannot start a duplicate operation, and a roster refresh while an operation
|
|
20
|
+
is pending re-renders the control still disabled. A remote instance with no
|
|
21
|
+
saved route on this machine still has its actions disabled with the same
|
|
22
|
+
explanation.
|
|
23
|
+
|
|
24
|
+
Nothing else changes: no kernel, session start, start dialog or record
|
|
25
|
+
schema change. Tag v0.22.9 and everything it shipped stand.
|
|
@@ -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.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Schedules
|
|
2
|
+
|
|
3
|
+
A schedule launches an agent, runs an oats command, or wakes an existing
|
|
4
|
+
instance on a cron. Definitions belong to a scope, the team workspace (the
|
|
5
|
+
config level that declares the team, else the outermost `oats-config.yaml`
|
|
6
|
+
level), and are committable; every `oats schedule` command run anywhere
|
|
7
|
+
inside that scope, including from an instance home, reads and writes the
|
|
8
|
+
same file. Execution belongs to the host that holds the scope, so a
|
|
9
|
+
schedule on a registered server keeps running while your laptop sleeps.
|
|
10
|
+
|
|
11
|
+
There is no daemon. One host timer (a launchd user agent on macOS, a systemd
|
|
12
|
+
user timer on Linux) runs `oats schedule tick --host` once a minute; the tick
|
|
13
|
+
is a short-lived process that evaluates only the current minute, launches
|
|
14
|
+
what is due through the same `spawn`, `session start` and `session input`
|
|
15
|
+
paths you use by hand, records what it observed, and exits. Minutes missed
|
|
16
|
+
while the machine slept are skipped, never replayed. There are no retries
|
|
17
|
+
and no queue.
|
|
18
|
+
|
|
19
|
+
## Files
|
|
20
|
+
|
|
21
|
+
- `<workspace>/oats-schedules.json` — the definitions (`{version: 1, jobs:
|
|
22
|
+
{<id>: ...}}`). Commit it if you want the schedule shared with the team.
|
|
23
|
+
- `<workspace>/.agents/schedules/state.json` — last attempted minute and
|
|
24
|
+
last run per job (gitignored), plus one lock directory per running job.
|
|
25
|
+
- `~/.oats/schedules/registry.json` — the host registry: which scopes the
|
|
26
|
+
host ticks, `maxConcurrent` (default 1) and the tick interval. One host
|
|
27
|
+
lock serializes ticks, run-now, reconcile and remove; it is never reclaimed
|
|
28
|
+
by another process: a lock whose owner is unreadable or gone is reported
|
|
29
|
+
with the directory to remove, and the holder removes its own lock on exit
|
|
30
|
+
and on SIGINT/SIGTERM. Definition edits take a short per-scope lock.
|
|
31
|
+
|
|
32
|
+
## Kinds
|
|
33
|
+
|
|
34
|
+
- **spawn** `{id, enabled, cron, tz, kind: "spawn", agent, agentsRoot?,
|
|
35
|
+
repo?, backend?, purpose?, task, runtime?, model?, yolo?, wake?}` — every
|
|
36
|
+
due minute launches one disposable instance of `agent` with the same
|
|
37
|
+
options `oats spawn` takes. `agentsRoot` names the exact agents root that
|
|
38
|
+
holds the soul (it must lie inside the workspace and defaults to the
|
|
39
|
+
workspace's own root); it is what tells same-named souls in different
|
|
40
|
+
member repositories apart. `repo` is the work repository, as `--repo`. The task gets a trailing schedule block naming the job and the
|
|
41
|
+
minute and ending with `oats retire --self`. An optional `wake` object
|
|
42
|
+
(`{cron, tz, message}`) attaches a wake schedule to each launched instance;
|
|
43
|
+
nothing is attached unless you ask.
|
|
44
|
+
- **command** `{id, enabled, cron, tz, kind: "command", cwd, argv}` — runs
|
|
45
|
+
an oats-only argv (`argv[0]` is `oats`, no shell) in `cwd`, which must be
|
|
46
|
+
inside the workspace. The runner parses the command's envelope and tracks
|
|
47
|
+
any instance it names, so `["oats", "okf", "harvest"]` run in a source
|
|
48
|
+
instance's home is followed until the harvester it spawned is gone.
|
|
49
|
+
Command return is not task completion.
|
|
50
|
+
- **wake** `{id, enabled, cron, tz, kind: "wake", home, message}` — every
|
|
51
|
+
due minute inspects the instance at `home` through its session receipts.
|
|
52
|
+
Running: `message` is delivered once with `session input`. Not running
|
|
53
|
+
(absent, dead pane or fallback shell): the same home is started with
|
|
54
|
+
`session start` and the message becomes the job's one pending delivery,
|
|
55
|
+
completed on a later tick, any tick, as soon as the session is active; the
|
|
56
|
+
home is started again only at due minutes, never every minute, so a
|
|
57
|
+
harness that keeps exiting is not restarted in a loop. A job holds at most
|
|
58
|
+
one pending delivery: a due minute while one is pending adds nothing.
|
|
59
|
+
Unobservable or still starting: skipped with the reason, delivery kept
|
|
60
|
+
pending. Whether a running harness is busy cannot be seen from the
|
|
61
|
+
terminal: delivery is terminal input (bracketed paste plus Enter), never an
|
|
62
|
+
interrupt, never Ctrl-C, never into a stopped or starting shell. Word wake
|
|
63
|
+
messages so that receiving one again is harmless.
|
|
64
|
+
|
|
65
|
+
`cron` has five fields (minute hour day month weekday) and `tz` is a
|
|
66
|
+
required IANA zone; both are evaluated by the croner library. `--wake-every
|
|
67
|
+
N` at spawn time means `*/N * * * *`: every 7 fires at :00, :07, ... :56 and
|
|
68
|
+
then :00 again, so 1, 5, 10, 15 and 30 give an even cadence.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
oats schedule add <id> --file spec.json --dir <workspace> --json
|
|
74
|
+
oats schedule update <id> --file spec.json
|
|
75
|
+
oats schedule list | show <id> | enable <id> | disable <id> | remove <id>
|
|
76
|
+
oats schedule run <id> # now, under the same lock and bound
|
|
77
|
+
oats schedule tick --dry-run # what would run this minute, launching nothing
|
|
78
|
+
oats schedule reconcile <id> [--clear] # resolve an attempt whose result was never recorded
|
|
79
|
+
oats schedule host install # register this scope and install the ONE host timer (idempotent while active)
|
|
80
|
+
oats schedule host status | uninstall
|
|
81
|
+
oats spawn <agent> ... --wake-every 15 --wake-message "Anything new?" # or --wake-file spec.json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Every subcommand takes `--server <id>` instead of `--dir`: it then runs on
|
|
85
|
+
that host, in its registered workspace, because schedules are host-owned.
|
|
86
|
+
|
|
87
|
+
`list --json` answers `{schedules: [{id, ...definition, nextRun, lastRun,
|
|
88
|
+
running}], scheduler: {installed, active, lastTick, maxConcurrent, ...}}`.
|
|
89
|
+
`active` is what the OS reports about the timer, not whether a file exists.
|
|
90
|
+
|
|
91
|
+
## What a run reports
|
|
92
|
+
|
|
93
|
+
`launched` (spawn or command returned), `active` (the instance is running;
|
|
94
|
+
a home whose retirement is pending still counts, its runtime may be alive),
|
|
95
|
+
`ended` (its home is gone), `stopped` (home present, nothing running: needs
|
|
96
|
+
attention, never removed for you), `launch-failed`, `unknown`, and for wake
|
|
97
|
+
jobs `delivered`, `started` or `skipped`. The kernel never claims a task
|
|
98
|
+
succeeded.
|
|
99
|
+
|
|
100
|
+
`unknown` means the launch's side effects are unconfirmed: a command timed
|
|
101
|
+
out or answered no envelope, an envelope named an instance the roster
|
|
102
|
+
cannot place, or an attempt was never recorded. The job keeps its slot and
|
|
103
|
+
is skipped until `oats schedule reconcile <id>`. Reconcile adopts only an
|
|
104
|
+
attributable receipt: a spawn job's instance is named deterministically for
|
|
105
|
+
its minute, a command job's only by the instance its answer named. Nothing
|
|
106
|
+
is inferred from file times. A command whose answer named nothing stays
|
|
107
|
+
unknown; check the roster and the host by hand, then
|
|
108
|
+
`oats schedule reconcile <id> --clear` records launch-failed and frees the
|
|
109
|
+
slot (or `remove --force` forgets the job).
|
|
110
|
+
|
|
111
|
+
A wake job that starts a stopped home holds a launch slot while that
|
|
112
|
+
runtime is starting, active, retiring or unobservable, and releases it when
|
|
113
|
+
the runtime is proven stopped (the session start receipt's exit marker for
|
|
114
|
+
that launch, or a home that no longer has a session) or the home is gone.
|
|
115
|
+
A persistent home that outlives its process does not keep a slot. Delivering
|
|
116
|
+
a message to a home that is already running takes no slot. The host tick
|
|
117
|
+
observes every registered scope first, then admits due jobs in one
|
|
118
|
+
host-wide order, least recently launched first (only an actual runtime
|
|
119
|
+
launch counts; a skipped or pending job keeps its place at the front), so
|
|
120
|
+
one frequent job in one scope cannot keep the only slot forever. An invalid
|
|
121
|
+
or malformed definition is reported on that job and the rest of the tick
|
|
122
|
+
continues.
|
|
123
|
+
|
|
124
|
+
`disable` never stops anything. `update` never touches a running instance,
|
|
125
|
+
and while a job holds a slot or has an unresolved attempt what its run is
|
|
126
|
+
tracked or reconciled by (kind, agent, agentsRoot, repo, purpose, home, cwd,
|
|
127
|
+
argv) cannot change; cron, tz, task, message, runtime, model and enabled
|
|
128
|
+
can. A cold wake persists its slot before the session start runs and keeps
|
|
129
|
+
it on any start exception, whatever its code (the kernel can refuse while
|
|
130
|
+
recording, after the session exists); the next observation releases it once
|
|
131
|
+
the runtime is proven stopped or absent, one tick at worst.
|
|
132
|
+
`remove` refuses while the job's instance is still tracked (`--force`
|
|
133
|
+
forgets the job without stopping anything). Retiring an instance removes the
|
|
134
|
+
wake jobs bound to its home; a wake whose home is gone otherwise stays
|
|
135
|
+
listed with its skipped reason.
|
|
136
|
+
|
|
137
|
+
## Wake at spawn
|
|
138
|
+
|
|
139
|
+
`oats spawn ... --wake-file <private JSON {cron, tz, message, enabled}>`
|
|
140
|
+
saves a wake job `wake-<instance>` bound to the new home after the spawn
|
|
141
|
+
succeeded. If the spawn succeeds but the save fails, the spawn result still
|
|
142
|
+
carries the full instance receipt, plus `wakeScheduleError` and a warning;
|
|
143
|
+
the instance is neither hidden nor spawned again.
|