@awebai/oats 0.28.0 → 0.29.1
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 +298 -107
- package/capabilities/oats-okf/bin/oats-okf.mjs +28 -8
- package/capabilities/oats-okf/injects/okf.md +33 -33
- package/capabilities/oats-okf/lib/binding-wire.mjs +4 -1
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/consult.mjs +1 -5
- package/capabilities/oats-okf/lib/harvest-status.mjs +88 -0
- package/capabilities/oats-okf/lib/harvest-switch.mjs +81 -0
- package/capabilities/oats-okf/lib/okf-validate.mjs +123 -0
- package/capabilities/oats-okf/lib/sources.mjs +28 -3
- package/capabilities/oats-okf/lib/stores.mjs +9 -4
- package/capabilities/oats-okf/lib/worker.mjs +82 -8
- package/capabilities/oats-okf/oats.json +14 -8
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +8 -6
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +1 -1
- package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +104 -0
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +140 -0
- package/capabilities/oats-okf-harvest/injects/harvester.md +12 -0
- package/capabilities/oats-okf-harvest/oats.json +26 -0
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +168 -0
- package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +192 -0
- package/capabilities/{oats-okf/skills/okf → oats-okf-harvest/skills/okf-authoring}/SKILL.md +15 -30
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +149 -0
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +12 -0
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +45 -0
- package/capabilities/oats-okf-maintenance/oats.json +21 -0
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +144 -0
- package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +192 -0
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +151 -0
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +123 -0
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +146 -0
- package/capabilities/oats-review/injects/review.md +3 -2
- package/capabilities/oats-review/oats.json +3 -4
- package/docs/capabilities.md +41 -9
- package/docs/capability-manifest.schema.json +0 -7
- package/docs/desktop-cli-api.md +277 -15
- package/docs/implementation.md +1 -1
- package/docs/knowledge-capability-authoring.md +8 -2
- package/docs/knowledge-reference/package-craft.md +8 -5
- package/docs/knowledge.md +101 -0
- package/docs/oats-local.schema.json +31 -1
- package/docs/official-catalog.md +7 -4
- package/docs/packages.md +11 -5
- package/docs/release-lane.md +1 -1
- package/docs/release-notes/v0.29.0.md +240 -0
- package/docs/release-notes/v0.29.1.md +79 -0
- package/docs/schedules.md +133 -5
- package/docs/souls-and-instances.md +4 -6
- package/docs/workspaces.md +11 -2
- package/lib/automations.mjs +369 -0
- package/lib/core.mjs +65 -154
- package/lib/instance-git.mjs +30 -1
- package/lib/instance-inspect.mjs +12 -4
- package/lib/instance-resolution.mjs +31 -182
- package/lib/materialize.mjs +5 -7
- package/lib/operator-dispatch.mjs +1 -2
- package/lib/packages.mjs +17 -0
- package/lib/remote.mjs +21 -1
- package/lib/resolve.mjs +51 -7
- package/lib/schedule.mjs +211 -41
- package/lib/triggers.mjs +182 -49
- package/lib/workspace.mjs +1 -1
- package/package-catalog.json +6 -4
- package/package.json +1 -1
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +0 -26
- package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +0 -5
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +0 -285
- package/capabilities/oats-review/agents/reviewer/AGENTS.md +0 -53
- package/capabilities/oats-review/agents/reviewer/soul.yaml +0 -6
- /package/capabilities/{oats-okf/skills/okf → oats-okf-harvest/skills/okf-authoring}/scripts/okf-validate.mjs +0 -0
package/lib/schedule.mjs
CHANGED
|
@@ -30,6 +30,8 @@ import { Cron } from "croner";
|
|
|
30
30
|
import { loadLocal } from "./workspace.mjs";
|
|
31
31
|
import { noteRuntimeName } from "./deprecation.mjs";
|
|
32
32
|
import { tickTriggers } from "./triggers.mjs";
|
|
33
|
+
import { resolveMemberClone } from "./instance-resolution.mjs";
|
|
34
|
+
import { AUTOMATION_ID_RE, automationContext, automationError, baseRow, localEntry, localId, soulOriginOf, splitId } from "./automations.mjs";
|
|
33
35
|
import { RESERVED_LAUNCH_ENV, MAX_INSTANCE_NAME, findAgent, findInstanceHomes, inspectInstanceSession, inputInstanceSession, startInstanceSession, retirePendingMarkerPath } from "./core.mjs";
|
|
34
36
|
|
|
35
37
|
export const SCHEDULE_FILE = "oats-schedules.json";
|
|
@@ -319,6 +321,7 @@ export function validateDefinition(ws, def, { checkAgent = true } = {}) {
|
|
|
319
321
|
noteRuntimeName(`schedule ${id}: runtime (written as harness)`);
|
|
320
322
|
def = { ...def, harness: def.runtime };
|
|
321
323
|
}
|
|
324
|
+
if (def.launchConfig !== undefined) { if (typeof def.launchConfig !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(def.launchConfig)) throw scheduleError("E_SCHEDULE_INVALID", "launchConfig: the name of a launch configuration on the running host (oats-local.yaml launch-configs)", { field: "launchConfig" }); out.launchConfig = def.launchConfig; }
|
|
322
325
|
if (def.harness !== undefined) { if (!HARNESSES.has(def.harness)) throw scheduleError("E_SCHEDULE_INVALID", "harness: pi, claude or codex", { field: "harness" }); out.harness = def.harness; }
|
|
323
326
|
if (def.model !== undefined) { if (typeof def.model !== "string") throw scheduleError("E_SCHEDULE_INVALID", "model: string", { field: "model" }); out.model = def.model; }
|
|
324
327
|
if (def.yolo !== undefined) { if (typeof def.yolo !== "boolean") throw scheduleError("E_SCHEDULE_INVALID", "yolo: boolean", { field: "yolo" }); out.yolo = def.yolo; }
|
|
@@ -354,11 +357,65 @@ export function validateDefinition(ws, def, { checkAgent = true } = {}) {
|
|
|
354
357
|
return out;
|
|
355
358
|
}
|
|
356
359
|
|
|
360
|
+
/** The filesystem-safe key a workspace schedule is stored under in the local run state (job locks
|
|
361
|
+
* are directories named by it): `<member>~<id>`. Repository names never carry `~`; local ids
|
|
362
|
+
* (SCHEDULE_ID_RE) never do either. */
|
|
363
|
+
export const scheduleStateKey = (qid) => String(qid).replace("/", "~");
|
|
364
|
+
/** The jobs a tick, run or reconcile works on: the deployment's local jobs (stored under their
|
|
365
|
+
* bare id) and, from the automations context (its `schedules`), the workspace schedules THIS host runs (stored
|
|
366
|
+
* under scheduleStateKey(<member>/<id>), a filesystem-safe key: job locks are directories). */
|
|
367
|
+
export function effectiveDefinitions(ws, ctx) {
|
|
368
|
+
const defs = readDefinitions(ws);
|
|
369
|
+
for (const a of ctx?.schedules || []) {
|
|
370
|
+
if (!a.placement.runsHere) continue;
|
|
371
|
+
const key = scheduleStateKey(a.id);
|
|
372
|
+
defs.jobs[key] = { ...a.definition, id: key, enabled: true, purpose: a.definition.purpose ?? a.name, workspace: { qid: a.id, name: a.name, member: a.member, origin: a.origin, runsOn: a.runsOn, owner: a.owner } };
|
|
373
|
+
}
|
|
374
|
+
return defs;
|
|
375
|
+
}
|
|
376
|
+
/** The stored key of a schedule argument (`local/<id>`, a bare local id, or `<member>/<id>`). */
|
|
377
|
+
export function scheduleKeyOf(id) { const { scope, name } = splitId(id); return scope === "local" ? name : scheduleStateKey(`${scope}/${name}`); }
|
|
378
|
+
/** A workspace schedule file (0.29.0; the shared header is lib/automations.mjs's):
|
|
379
|
+
* `oats-schedules/<id>.yaml` or `*.oats-schedule.yaml`, `kind: oats-schedule`. Its body is the
|
|
380
|
+
* run (`run: spawn | command`, cron, tz, …); a wake or an operation targets an instance home on
|
|
381
|
+
* one machine and stays local. `expand` checks it against the deployment `dep`. */
|
|
382
|
+
export function scheduleKind({ dep = null } = {}) {
|
|
383
|
+
return {
|
|
384
|
+
kind: "schedule", folder: "oats-schedules", suffix: "oats-schedule", fileKind: "oats-schedule",
|
|
385
|
+
bodyKeys: ["run", "cron", "tz", "agent", "task", "purpose", "launchConfig", "harness", "model", "yolo", "backend", "wake", "argv", "cwd"],
|
|
386
|
+
parseBody: (body) => {
|
|
387
|
+
const bad = (message) => Object.assign(new Error(message), { field: "run" });
|
|
388
|
+
const { run = "spawn", ...rest } = body;
|
|
389
|
+
if (run === "wake" || run === "operation") throw bad(`run: ${run} targets an instance home on one machine; declare it locally (oats schedule add), not in Git`);
|
|
390
|
+
if (run !== "spawn" && run !== "command") throw bad("run: spawn or command");
|
|
391
|
+
return { definition: { ...rest, kind: run } };
|
|
392
|
+
},
|
|
393
|
+
expand: (a) => validateWorkspaceSchedule(dep, a),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/** A workspace schedule, as a validated kernel definition for the snapshot: the file's run facts
|
|
397
|
+
* checked like `oats schedule add` (the soul is resolved when it runs, through discovery); a
|
|
398
|
+
* command's cwd is relative to the deployment. */
|
|
399
|
+
export function validateWorkspaceSchedule(dep, a) {
|
|
400
|
+
const d = a.source?.definition || {};
|
|
401
|
+
const spec = { ...d, id: a.name, enabled: true };
|
|
402
|
+
if (spec.kind === "command") {
|
|
403
|
+
if (d.cwd !== undefined && (typeof d.cwd !== "string" || isAbsolute(d.cwd))) throw scheduleError("E_SCHEDULE_INVALID", "cwd: a directory relative to the deployment (a workspace schedule never names a machine path)", { field: "cwd" });
|
|
404
|
+
spec.cwd = resolve(dep, d.cwd ?? ".");
|
|
405
|
+
}
|
|
406
|
+
const out = validateDefinition(dep, spec, { checkAgent: false });
|
|
407
|
+
delete out.enabled;
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
357
411
|
/** The soul a spawn job launches, under the deployment's one agents root
|
|
358
412
|
* (a definition's agentsRoot, when given, must be that root). `repo` is the
|
|
359
413
|
* work repository and never selects a soul. */
|
|
360
414
|
export function resolveScheduledAgent(ws, def) {
|
|
361
415
|
const [root] = scopeRoots(ws);
|
|
416
|
+
// A workspace schedule (0.29.0) names its soul as `oats spawn <soul>` does: discovery resolves
|
|
417
|
+
// it (a member or package soul), whether or not this deployment has spawned it before.
|
|
418
|
+
if (def.workspace) return { root, agent: { name: def.agent }, discover: true };
|
|
362
419
|
if (def.agentsRoot && resolve(def.agentsRoot) !== root) throw scheduleError("E_SCHEDULE_INVALID", `agentsRoot: ${def.agentsRoot} is not this deployment's agents root (${root})`, { field: "agentsRoot" });
|
|
363
420
|
const agent = findAgent(root, def.agent);
|
|
364
421
|
if (!agent) throw scheduleError("E_SCHEDULE_INVALID", `agent: no soul named ${JSON.stringify(def.agent)} under ${root}`, { field: "agent" });
|
|
@@ -427,7 +484,7 @@ export function findHomesInScope(ws, instance) {
|
|
|
427
484
|
// --------------------------------------------------------------- runner
|
|
428
485
|
|
|
429
486
|
function scheduleBlock(def, minute) {
|
|
430
|
-
return `\n\n## Scheduled run\n\nThis instance was launched by OATS schedule "${def.id}" for ${minute.toISOString()} (cron "${def.cron}" in ${def.tz}). It is disposable: finish the task above, bring your memory files up to date, and end with \`oats retire --self\`.\n`;
|
|
487
|
+
return `\n\n## Scheduled run\n\nThis instance was launched by OATS schedule "${def.workspace?.qid ?? def.id}" for ${minute.toISOString()} (cron "${def.cron}" in ${def.tz}). It is disposable: finish the task above, bring your memory files up to date, and end with \`oats retire --self\`.\n`;
|
|
431
488
|
}
|
|
432
489
|
/** The environment a scheduler-launched oats child runs with: the caller's
|
|
433
490
|
* frozen instance identity and capability snapshot removed (a tick or run-now
|
|
@@ -464,7 +521,8 @@ export function workspaceSpawnContext(ws) {
|
|
|
464
521
|
* `io.oatsBin`, `io.commandTimeoutMs` and `io.noLaunch` are the test seams. */
|
|
465
522
|
function spawnViaCli(ws, root, agent, opts, context, io) {
|
|
466
523
|
if (context.error) throw scheduleError(context.error.code, `this deployment realizes a workspace (${context.dir}) but its oats-local.yaml cannot be read: ${context.error.message}`);
|
|
467
|
-
const argv = ["spawn", agent.name, "--dir", context.dir, "--agents-root", root, "--purpose", opts.purpose, "--json"];
|
|
524
|
+
const argv = ["spawn", agent.name, "--dir", context.dir, ...(opts.discover ? [] : ["--agents-root", root]), "--purpose", opts.purpose, "--json"];
|
|
525
|
+
if (opts.launchConfig) argv.push("--launch-config", opts.launchConfig);
|
|
468
526
|
if (opts.harness) argv.push("--harness", opts.harness);
|
|
469
527
|
if (opts.model) argv.push("--model", opts.model);
|
|
470
528
|
if (opts.backend) argv.push("--backend", opts.backend);
|
|
@@ -499,9 +557,9 @@ function spawnViaCli(ws, root, agent, opts, context, io) {
|
|
|
499
557
|
* deployment (workspace materialization: `.oats/modules`, module skills and
|
|
500
558
|
* injects). */
|
|
501
559
|
function launchSpawn(ws, def, minute, io) {
|
|
502
|
-
const { root, agent } = resolveScheduledAgent(ws, def);
|
|
560
|
+
const { root, agent, discover } = resolveScheduledAgent(ws, def);
|
|
503
561
|
const purpose = `${def.purpose || def.id}-${purposeSuffix(minute)}`;
|
|
504
|
-
const opts = { task: def.task.trimEnd() + scheduleBlock(def, minute), purpose, harness: def.harness, model: def.model, yolo: def.yolo, backend: def.backend, repo: def.repo, launch: io?.noLaunch === true ? false : true };
|
|
562
|
+
const opts = { discover, task: def.task.trimEnd() + scheduleBlock(def, minute), purpose, launchConfig: def.launchConfig, harness: def.harness, model: def.model, yolo: def.yolo, backend: def.backend, repo: def.repo, launch: io?.noLaunch === true ? false : true };
|
|
505
563
|
let r;
|
|
506
564
|
if (io?.spawn) r = io.spawn(root, agent, opts);
|
|
507
565
|
else r = spawnViaCli(ws, root, agent, opts, workspaceSpawnContext(ws), io);
|
|
@@ -659,13 +717,13 @@ function observePrevious(ws, id, def, js, io, now, { mutate }) {
|
|
|
659
717
|
* reads only: no lock, state or definition is touched. Due jobs are
|
|
660
718
|
* visited least-recently-launched first so one job cannot monopolize the
|
|
661
719
|
* slot. An invalid definition is recorded on that job and the rest continue. */
|
|
662
|
-
export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun = false, only, observeOnly = false, candidates } = {}) {
|
|
720
|
+
export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun = false, only, observeOnly = false, candidates, ctx = null } = {}) {
|
|
663
721
|
const minute = minuteStart(now);
|
|
664
722
|
const key = minuteKey(minute);
|
|
665
723
|
const registry = reg || readRegistry();
|
|
666
724
|
const bound = registry.maxConcurrent || 1;
|
|
667
725
|
const wsAll = wsList || [ws];
|
|
668
|
-
const defs =
|
|
726
|
+
const defs = effectiveDefinitions(ws, ctx);
|
|
669
727
|
const st = readState(ws);
|
|
670
728
|
const considered = [];
|
|
671
729
|
const stateOf = (id) => (st.jobs[id] ||= {});
|
|
@@ -768,6 +826,15 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
|
|
|
768
826
|
return considered;
|
|
769
827
|
}
|
|
770
828
|
|
|
829
|
+
/** One scope's automations context (lib/automations.mjs automationContext) with its oats-local.yaml;
|
|
830
|
+
* an unreadable oats-local.yaml means no workspace automations (the local jobs still run). */
|
|
831
|
+
export function scopeAutomations(ws, { io, now = new Date(), refresh = false, cache = new Map() } = {}) {
|
|
832
|
+
let local = null;
|
|
833
|
+
try { local = loadLocal(ws).local; } catch { local = null; }
|
|
834
|
+
const clonePathOf = (repoKey) => resolveMemberClone({ deployment: ws, soulEntry: { repoKey }, local });
|
|
835
|
+
const ctx = automationContext(ws, { local, io: { ...(io || {}), childEnv }, now, refresh, cache, clonePathOf });
|
|
836
|
+
return Object.assign(ctx, { ws, local });
|
|
837
|
+
}
|
|
771
838
|
/** The host tick: every registered scope under the host lock. */
|
|
772
839
|
export function tickHost({ now = new Date(), io, dryRun = false } = {}) {
|
|
773
840
|
const reg = readRegistry();
|
|
@@ -779,21 +846,27 @@ export function tickHost({ now = new Date(), io, dryRun = false } = {}) {
|
|
|
779
846
|
// every-minute job in the first registered scope cannot monopolize the
|
|
780
847
|
// bound. An unreadable scope is recorded and the rest continue.
|
|
781
848
|
const jobs = [];
|
|
849
|
+
// Each scope's workspace automations, placed on this host once per tick (one gh identity
|
|
850
|
+
// lookup per GitHub host), the snapshot refreshed first when it is ten minutes old.
|
|
851
|
+
const cache = new Map(), contexts = new Map();
|
|
852
|
+
for (const ws of wsList) contexts.set(ws, scopeAutomations(ws, { io, now, refresh: !dryRun, cache }));
|
|
853
|
+
for (const ctx of contexts.values()) if (ctx.refresh && !ctx.refresh.ok) considered.push({ workspace: ctx.ws, action: "error", error: `automations refresh: ${ctx.refresh.error}` });
|
|
782
854
|
for (const ws of wsList) {
|
|
855
|
+
const ctx = contexts.get(ws);
|
|
783
856
|
try {
|
|
784
|
-
considered.push(...tickWorkspace(ws, { now, io, reg, wsList, dryRun, observeOnly: true }));
|
|
785
|
-
const defs =
|
|
857
|
+
considered.push(...tickWorkspace(ws, { now, io, reg, wsList, dryRun, observeOnly: true, ctx }));
|
|
858
|
+
const defs = effectiveDefinitions(ws, ctx), st = readState(ws);
|
|
786
859
|
for (const [id, def] of Object.entries(defs.jobs)) if (def?.kind !== "trigger") jobs.push({ ws, id, key: launchOrderKey(st.jobs[id]) });
|
|
787
860
|
} catch (e) { considered.push({ workspace: ws, action: "error", error: e.message }); }
|
|
788
861
|
}
|
|
789
862
|
jobs.sort((a, b) => a.key.localeCompare(b.key) || a.ws.localeCompare(b.ws) || a.id.localeCompare(b.id));
|
|
790
863
|
for (const j of jobs) {
|
|
791
|
-
try { considered.push(...tickWorkspace(j.ws, { now, io, reg, wsList, dryRun, candidates: [j.id] })); }
|
|
864
|
+
try { considered.push(...tickWorkspace(j.ws, { now, io, reg, wsList, dryRun, candidates: [j.id], ctx: contexts.get(j.ws) })); }
|
|
792
865
|
catch (e) { considered.push({ workspace: j.ws, id: j.id, action: "error", error: e.message }); }
|
|
793
866
|
}
|
|
794
867
|
// Triggers: each scope's event-driven spawns, polled at their own interval (lib/triggers.mjs).
|
|
795
868
|
for (const ws of wsList) {
|
|
796
|
-
try { considered.push(...tickTriggers(ws, { now, io, dryRun })); }
|
|
869
|
+
try { considered.push(...tickTriggers(ws, { now, io, dryRun, ctx: contexts.get(ws) })); }
|
|
797
870
|
catch (e) { considered.push({ workspace: ws, action: "error", error: `triggers: ${e.message}` }); }
|
|
798
871
|
}
|
|
799
872
|
if (!dryRun) writeHostState({ ...readHostState(), lastTick: now.toISOString(), minute: minuteKey(minuteStart(now)) });
|
|
@@ -804,10 +877,11 @@ export function tickHost({ now = new Date(), io, dryRun = false } = {}) {
|
|
|
804
877
|
|
|
805
878
|
/** Run one job now, ignoring its cron, under the same lock and bound; the
|
|
806
879
|
* previous run is observed under the host lock before any lock is judged. */
|
|
807
|
-
export function runNow(ws,
|
|
808
|
-
const
|
|
880
|
+
export function runNow(ws, qid, { now = new Date(), io, force = false, ctx = null } = {}) {
|
|
881
|
+
const id = scheduleKeyOf(qid);
|
|
882
|
+
const defs = effectiveDefinitions(ws, ctx);
|
|
809
883
|
const def = defs.jobs[id];
|
|
810
|
-
if (!def || def.kind === "trigger") throw
|
|
884
|
+
if (!def || def.kind === "trigger") throw notHere(ws, qid, ctx);
|
|
811
885
|
if (!def.enabled && !force) throw scheduleError("E_SCHEDULE_DISABLED", `schedule ${id} is disabled; enable it or pass --force`);
|
|
812
886
|
const reg = readRegistry();
|
|
813
887
|
const wsList = reg.workspaces.includes(resolve(ws)) ? reg.workspaces : [...reg.workspaces, resolve(ws)];
|
|
@@ -819,10 +893,10 @@ export function runNow(ws, id, { now = new Date(), io, force = false } = {}) {
|
|
|
819
893
|
if (js.attempt) throw scheduleError("E_SCHEDULE_UNRESOLVED", `schedule ${id} has a launch attempt without a recorded result; run oats schedule reconcile ${id} first`);
|
|
820
894
|
if (def.kind !== "wake" && jobLockInfo(ws, id)) throw scheduleError("E_SCHEDULE_RUNNING", `schedule ${id} is still running`);
|
|
821
895
|
if (def.kind !== "wake" && liveLocks(wsList) >= reg.maxConcurrent) throw scheduleError("E_SCHEDULER_BUSY", `host concurrency ${reg.maxConcurrent} reached`);
|
|
822
|
-
const considered = tickWorkspace(ws, { now, io, reg, wsList, only: id });
|
|
896
|
+
const considered = tickWorkspace(ws, { now, io, reg, wsList, only: id, ctx });
|
|
823
897
|
const after = readState(ws);
|
|
824
898
|
if (after.jobs[id]) { after.jobs[id].lastAttemptedMinute = minuteKey(minuteStart(now)); writeState(ws, after); }
|
|
825
|
-
return { schedule: describe(ws,
|
|
899
|
+
return { schedule: describe(ws, qid, io, { ctx }), run: after.jobs[id]?.lastRun || null, considered };
|
|
826
900
|
});
|
|
827
901
|
}
|
|
828
902
|
|
|
@@ -831,15 +905,17 @@ export function runNow(ws, id, { now = new Date(), io, force = false } = {}) {
|
|
|
831
905
|
* identity), a command attempt only the instance its answer NAMED. Nothing
|
|
832
906
|
* is inferred from file times. An unnamed command effect stays unknown
|
|
833
907
|
* until the operator has checked by hand and passes `clear`. */
|
|
834
|
-
export function reconcile(ws,
|
|
835
|
-
const
|
|
908
|
+
export function reconcile(ws, qid, { io, now = new Date(), clear = false, ctx = null } = {}) {
|
|
909
|
+
const id = scheduleKeyOf(qid);
|
|
910
|
+
const defs = effectiveDefinitions(ws, ctx);
|
|
836
911
|
const def = defs.jobs[id];
|
|
837
|
-
if (!def || def.kind === "trigger") throw
|
|
912
|
+
if (!def || def.kind === "trigger") throw notHere(ws, qid, ctx);
|
|
913
|
+
const describeHere = () => describe(ws, qid, io, { ctx });
|
|
838
914
|
return withHostLock(() => {
|
|
839
915
|
const st = readState(ws);
|
|
840
916
|
const js = st.jobs[id] || (st.jobs[id] = {});
|
|
841
917
|
const attempt = js.attempt || (js.lastRun?.outcome === "unknown" ? { scheduledFor: js.lastRun.scheduledFor, startedAt: js.lastRun.startedAt, ...(js.lastRun.execution !== undefined ? { execution: js.lastRun.execution } : {}) } : undefined);
|
|
842
|
-
if (!attempt) return { schedule:
|
|
918
|
+
if (!attempt) return { schedule: describeHere(), reconciled: "nothing to reconcile" };
|
|
843
919
|
try { validateAttemptExecution(attempt, jobLockInfo(ws, id)); }
|
|
844
920
|
catch (error) { throw scheduleError("E_SCHEDULE_INVALID", error.message); }
|
|
845
921
|
const attemptKind = def.kind;
|
|
@@ -859,9 +935,9 @@ export function reconcile(ws, id, { io, now = new Date(), clear = false } = {})
|
|
|
859
935
|
if (js.lastRun?.instance) named = findHomesInScope(ws, js.lastRun.instance).map((home) => ({ instance: js.lastRun.instance, home }));
|
|
860
936
|
if (named.length === 1) adopted = named[0];
|
|
861
937
|
else if (named.length > 1) ambiguous = `${named.length} homes named ${js.lastRun.instance}: ${named.map((c) => c.home).join(", ")}`;
|
|
862
|
-
else if (!clear) return { schedule:
|
|
938
|
+
else if (!clear) return { schedule: describeHere(), reconciled: "unknown", remedy: `the command's effects cannot be proven from a matching admitted receipt; check the roster and the host by hand, then run oats schedule reconcile ${id} --clear to record launch-failed and free the slot, or oats schedule remove --force ${id}` };
|
|
863
939
|
}
|
|
864
|
-
if (ambiguous && !clear) return { schedule:
|
|
940
|
+
if (ambiguous && !clear) return { schedule: describeHere(), reconciled: "unknown", remedy: `${ambiguous}; retire or adopt by hand, then run oats schedule reconcile ${id} --clear or oats schedule remove --force ${id}` };
|
|
865
941
|
if (ambiguous) adopted = undefined;
|
|
866
942
|
delete js.attempt;
|
|
867
943
|
if (adopted) {
|
|
@@ -873,13 +949,61 @@ export function reconcile(ws, id, { io, now = new Date(), clear = false } = {})
|
|
|
873
949
|
releaseJobLock(ws, id);
|
|
874
950
|
}
|
|
875
951
|
writeState(ws, st);
|
|
876
|
-
return { schedule:
|
|
952
|
+
return { schedule: describeHere(), reconciled: adopted ? "adopted" : "cleared" };
|
|
877
953
|
});
|
|
878
954
|
}
|
|
879
955
|
|
|
880
956
|
// -------------------------------------------------------- definitions CRUD
|
|
881
957
|
|
|
882
958
|
export const SCHEDULE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
959
|
+
/** A schedule id this host cannot run: unknown, or a workspace schedule placed elsewhere. */
|
|
960
|
+
/** `oats schedule test <id>`: what running it on this host would do, without running it — its
|
|
961
|
+
* placement, whether its soul resolves (the kernel's own `oats spawn <soul> --preview`, which
|
|
962
|
+
* writes nothing) and when it is next due. Spawns nothing, records nothing, takes no lock. */
|
|
963
|
+
export function testSchedule(ws, qid, io, { now = new Date(), ctx = null } = {}) {
|
|
964
|
+
const row = describe(ws, qid, io, { now, ctx });
|
|
965
|
+
const { scope, name } = splitId(qid);
|
|
966
|
+
const entry = scope === "local" ? null : (ctx?.schedules || []).find((x) => x.id === `${scope}/${name}`);
|
|
967
|
+
const def = entry ? { ...entry.definition, workspace: { qid: entry.id } } : readDefinitions(ws).jobs[name];
|
|
968
|
+
const problems = [];
|
|
969
|
+
if (row.invalid) problems.push(`${row.qualifiedId} is invalid: ${row.invalid.message}`);
|
|
970
|
+
if (!row.runsHere) problems.push(`${row.qualifiedId} does not run on this host (${row.reason ?? "disabled here"}${row.reasonDetail ? `: ${row.reasonDetail}` : ""})`);
|
|
971
|
+
if (!row.enabled) problems.push(`${row.qualifiedId} is disabled (oats schedule enable ${row.qualifiedId})`);
|
|
972
|
+
// The next cron match, whether or not this host runs it (placement says that).
|
|
973
|
+
let nextDue = null; try { nextDue = def?.cron ? nextRunAfter(def, now) : null; } catch (e) { problems.push(`cron: ${e.message}`); }
|
|
974
|
+
let soul = null;
|
|
975
|
+
if (def?.kind === "spawn") {
|
|
976
|
+
soul = { name: def.agent, origin: row.soul?.origin ?? null, resolves: false, error: null };
|
|
977
|
+
try {
|
|
978
|
+
const { root, discover } = resolveScheduledAgent(ws, def);
|
|
979
|
+
const context = workspaceSpawnContext(ws);
|
|
980
|
+
if (context.error) throw scheduleError(context.error.code, context.error.message);
|
|
981
|
+
const argv = ["spawn", def.agent, "--dir", context.dir, ...(discover ? [] : ["--agents-root", root]), "--preview", "--json"];
|
|
982
|
+
if (def.launchConfig) argv.push("--launch-config", def.launchConfig);
|
|
983
|
+
if (def.harness) argv.push("--harness", def.harness);
|
|
984
|
+
if (def.model) argv.push("--model", def.model);
|
|
985
|
+
const r = spawnSync(process.execPath, [io?.oatsBin || OATS_BIN, ...argv], { cwd: context.dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: io?.commandTimeoutMs || COMMAND_TIMEOUT_MS, killSignal: "SIGTERM", maxBuffer: 16 * 1024 * 1024, env: childEnv() });
|
|
986
|
+
const envelope = parseEnvelopeText(r.stdout);
|
|
987
|
+
if (!envelope || typeof envelope.ok !== "boolean") throw scheduleError("E_SPAWN_FAILED", `the spawn preview answered no valid envelope (exit ${r.status ?? r.signal})`);
|
|
988
|
+
if (!envelope.ok) throw scheduleError(envelope.error?.code || "E_SPAWN_FAILED", String(envelope.error?.message || "spawn preview failed"));
|
|
989
|
+
soul.resolves = true;
|
|
990
|
+
} catch (e) {
|
|
991
|
+
soul.error = { code: e.code || "E_SPAWN_FAILED", message: e.message };
|
|
992
|
+
problems.push(`soul ${def.agent} does not resolve: ${e.message}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
id: row.id, qualifiedId: row.qualifiedId, kind: row.kind,
|
|
997
|
+
placement: { runsHere: row.runsHere, reason: row.reason, ...(row.reasonDetail ? { reasonDetail: row.reasonDetail } : {}), enabledHere: row.enabledHere, runsOn: row.runsOn, owner: row.owner, host: ctx?.host?.name ?? null },
|
|
998
|
+
soul, nextDue, spawned: false, problems, ok: problems.length === 0,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function notHere(ws, qid, ctx) {
|
|
1002
|
+
const { scope } = splitId(qid);
|
|
1003
|
+
const a = scope !== "local" ? (ctx?.schedules || []).find((x) => x.id === qid) : null;
|
|
1004
|
+
if (a) return automationError("E_AUTOMATION_NOT_HERE", `${qid} does not run on this host (${a.placement.reason ?? (a.invalid ? "invalid" : "disabled here")}${a.placement.detail ? `: ${a.placement.detail}` : a.invalid ? `: ${a.invalid.message}` : ""})`, { id: qid, reason: a.placement.reason, runsOn: a.runsOn, owner: a.owner });
|
|
1005
|
+
return scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(qid)} in ${ws}${scope !== "local" && !ctx?.snapshot ? " (no automations snapshot yet: run oats sync)" : ""}`);
|
|
1006
|
+
}
|
|
883
1007
|
function incarnationOfHome(home) { try { const m = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); return typeof m?.createdAt === "string" ? m.createdAt : null; } catch { return null; } }
|
|
884
1008
|
/** K8b — session PROVENANCE of a run (facts the recorder had), never a
|
|
885
1009
|
* transcript: there is no reader behind this block. */
|
|
@@ -903,12 +1027,22 @@ function projectHistory(js) {
|
|
|
903
1027
|
}
|
|
904
1028
|
return { history: { status: "ok", stored: stored.length, truncated: stored.length > RECENT_RUNS_MAX }, recentRuns: rows };
|
|
905
1029
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1030
|
+
/** One schedule's row: the stored job's facts (a workspace schedule this host runs is a job
|
|
1031
|
+
* under its state key) plus the automations row (docs/desktop-cli-api.md "Automations"). A
|
|
1032
|
+
* workspace schedule another host runs has no local facts: its definition and placement only. */
|
|
1033
|
+
export function describe(ws, qid, io, { defs, st, now = new Date(), ctx = null } = {}) {
|
|
1034
|
+
if (typeof qid !== "string" || !qid) throw scheduleError("E_BAD_ARGS", `oats schedule show <id> (local/<id> or <member>/<id>)`);
|
|
1035
|
+
const { scope, name } = splitId(qid);
|
|
1036
|
+
if (scope !== "local" && (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(scope) || !AUTOMATION_ID_RE.test(name))) throw scheduleError("E_BAD_ARGS", `a workspace schedule id is <member>/<id> (got ${JSON.stringify(qid)})`);
|
|
1037
|
+
const entry = scope === "local" ? null : (ctx?.schedules || []).find((x) => x.id === `${scope}/${name}`);
|
|
1038
|
+
if (scope !== "local" && !entry) throw notHere(ws, `${scope}/${name}`, ctx);
|
|
1039
|
+
if (entry && !entry.placement.runsHere) return scheduleRow(ws, entry, { ...(entry.definition || {}), scope: ws, scheduleApi: 2, scheduleHistoryApi: SCHEDULE_HISTORY_API, nextRun: null, lastRun: null, running: false }, ctx);
|
|
1040
|
+
const id = scope === "local" ? name : scheduleStateKey(entry.id);
|
|
1041
|
+
if (scope === "local" && !SCHEDULE_ID_RE.test(id)) throw scheduleError("E_BAD_ARGS", `schedule id must match ${SCHEDULE_ID_RE} (got ${JSON.stringify(qid)})`);
|
|
1042
|
+
defs ||= effectiveDefinitions(ws, ctx); st ||= readState(ws);
|
|
909
1043
|
const def = defs.jobs[id];
|
|
910
|
-
if (!def) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(
|
|
911
|
-
if (def.kind === "trigger") throw scheduleError("E_SCHEDULE_UNKNOWN", `${
|
|
1044
|
+
if (!def) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(qid)} in ${ws}`);
|
|
1045
|
+
if (def.kind === "trigger") throw scheduleError("E_SCHEDULE_UNKNOWN", `${qid} is a trigger, not a schedule — use oats trigger show ${qid}`);
|
|
912
1046
|
// Subject truth: the definition's own id must be the key it is stored under.
|
|
913
1047
|
if (def.id !== undefined && def.id !== id) throw scheduleError("E_SCHEDULE_IDENTITY", `schedule stored under ${JSON.stringify(id)} declares id ${JSON.stringify(def.id)}`, { key: id, declared: def.id });
|
|
914
1048
|
const js = st.jobs[id] || {};
|
|
@@ -916,18 +1050,44 @@ export function describe(ws, id, io, { defs, st, now = new Date() } = {}) {
|
|
|
916
1050
|
const lock = jobLockInfo(ws, id);
|
|
917
1051
|
const intent = js.attempt || (lock ? (js.lastRun?.execution !== undefined ? { execution: js.lastRun.execution } : {}) : undefined);
|
|
918
1052
|
const { history, recentRuns } = projectHistory(js);
|
|
919
|
-
|
|
1053
|
+
const { workspace: _w, ...stored } = def; void _w;
|
|
1054
|
+
const base = { ...stored, id, scope: ws, scheduleApi: 2, scheduleHistoryApi: SCHEDULE_HISTORY_API, executionStatus: scheduleExecutionStatus(def, intent, lock), nextRun, lastRun: js.lastRun ? { ...js.lastRun, runId: runIdOf(js.lastRun), session: sessionProvenanceOf(js.lastRun) } : null, history, recentRuns, running: !!lock, ...(js.attempt ? { attempt: js.attempt } : {}), ...(js.pendingWake ? { pendingWake: js.pendingWake } : {}) };
|
|
1055
|
+
return scheduleRow(ws, entry ?? localEntry("schedule", { ...def, id: name }, { dep: ws }), base, ctx);
|
|
1056
|
+
}
|
|
1057
|
+
/** A schedule's list row: its stored facts, the fields every trigger and schedule row share
|
|
1058
|
+
* (lib/automations.mjs baseRow) and the schedule's own: `kind` is its run (spawn, command, wake,
|
|
1059
|
+
* operation), the soul it spawns, the task, the cron and tz, the last run and the next due. */
|
|
1060
|
+
function scheduleRow(ws, entry, base, ctx) {
|
|
1061
|
+
const { enabled: _e, ...rest } = base; void _e;
|
|
1062
|
+
const def = entry.definition || {};
|
|
1063
|
+
const soul = def.kind === "spawn" ? def.agent ?? null : null;
|
|
1064
|
+
const local = entry.member === "local";
|
|
1065
|
+
return {
|
|
1066
|
+
...rest, ...baseRow(entry),
|
|
1067
|
+
// A local schedule keeps its bare id (the 0.28 contract every consumer reads); its qualified
|
|
1068
|
+
// form is qualifiedId. A workspace schedule's id IS qualified: <member>/<id>.
|
|
1069
|
+
id: local ? entry.name : entry.id, qualifiedId: entry.id,
|
|
1070
|
+
kind: base.kind ?? def.kind ?? null,
|
|
1071
|
+
soul: soul ? { name: soul, origin: soulOriginOf(ctx?.snapshot?.souls, soul) } : null,
|
|
1072
|
+
task: def.task ?? null, cron: def.cron ?? null, tz: def.tz ?? null,
|
|
1073
|
+
teams: [], launchConfig: def.launchConfig ?? null, harness: def.harness ?? null, model: def.model ?? null, concurrency: null,
|
|
1074
|
+
lastRun: base.lastRun ?? null, nextDue: entry.placement.runsHere ? base.nextRun ?? null : null,
|
|
1075
|
+
enabled: entry.enabled !== false,
|
|
1076
|
+
};
|
|
920
1077
|
}
|
|
921
|
-
export function listSchedules(ws, io, { now = new Date() } = {}) {
|
|
1078
|
+
export function listSchedules(ws, io, { now = new Date(), ctx = null } = {}) {
|
|
922
1079
|
const defs = readDefinitions(ws), st = readState(ws);
|
|
1080
|
+
const all = effectiveDefinitions(ws, ctx);
|
|
923
1081
|
// One job's bad identity or history does not fail the others: it is reported as its own row.
|
|
924
|
-
const
|
|
925
|
-
try { return describe(ws,
|
|
926
|
-
catch (e) { return { id, scope: ws, scheduleApi: 2, scheduleHistoryApi: SCHEDULE_HISTORY_API, unreadable: { code: e.code || "E_SCHEDULE_INVALID", message: e.message }, history: { status: "corrupt", stored: null, truncated: false }, recentRuns: [] }; }
|
|
927
|
-
}
|
|
1082
|
+
const row = (qid) => {
|
|
1083
|
+
try { return describe(ws, qid, io, { defs: all, st, now, ctx }); }
|
|
1084
|
+
catch (e) { return { id: splitId(qid).scope === "local" ? splitId(qid).name : qid, qualifiedId: qid, name: splitId(qid).name, scope: ws, scheduleApi: 2, scheduleHistoryApi: SCHEDULE_HISTORY_API, unreadable: { code: e.code || "E_SCHEDULE_INVALID", message: e.message }, history: { status: "corrupt", stored: null, truncated: false }, recentRuns: [] }; }
|
|
1085
|
+
};
|
|
1086
|
+
const local = Object.keys(defs.jobs).filter((id) => defs.jobs[id]?.kind !== "trigger").sort().map((id) => row(localId(id)));
|
|
1087
|
+
const workspace = (ctx?.schedules || []).map((a) => row(a.id));
|
|
928
1088
|
// Triggers share the definitions file but not this listing: a pointer, so they are never invisible.
|
|
929
|
-
const triggers = Object.values(defs.jobs).filter((d) => d?.kind === "trigger").length;
|
|
930
|
-
return { scope: ws, scheduleApi: 2, scheduleHistoryApi: SCHEDULE_HISTORY_API, integrity: scheduleIntegrity(ws), schedules, triggers: { count: triggers, command: "oats trigger list" }, scheduler: schedulerStatus(ws, io) };
|
|
1089
|
+
const triggers = Object.values(defs.jobs).filter((d) => d?.kind === "trigger").length + (ctx?.triggers || []).length;
|
|
1090
|
+
return { scope: ws, scheduleApi: 2, scheduleHistoryApi: SCHEDULE_HISTORY_API, integrity: scheduleIntegrity(ws), host: { name: ctx?.host?.name ?? null, ghUser: ctx?.ghUsers ? ctx.ghUsers() : {} }, schedules: [...local, ...workspace], triggers: { count: triggers, command: "oats trigger list" }, snapshot: ctx?.snapshot ? { takenAt: ctx.snapshot.takenAt, problems: (ctx.snapshot.problems || []).length } : null, scheduler: schedulerStatus(ws, io) };
|
|
931
1091
|
}
|
|
932
1092
|
export function addSchedule(ws, spec, io) {
|
|
933
1093
|
const def = validateDefinition(ws, spec);
|
|
@@ -937,14 +1097,15 @@ export function addSchedule(ws, spec, io) {
|
|
|
937
1097
|
const now = new Date().toISOString();
|
|
938
1098
|
defs.jobs[def.id] = { ...def, createdAt: now, updatedAt: now };
|
|
939
1099
|
writeDefinitions(ws, defs);
|
|
940
|
-
return describe(ws, def.id, io);
|
|
1100
|
+
return describe(ws, localId(def.id), io);
|
|
941
1101
|
});
|
|
942
1102
|
}
|
|
943
1103
|
/** Update under the host lock (so the busy check cannot race a tick's
|
|
944
1104
|
* admission) and the scope lock. What a run is tracked or reconciled by
|
|
945
1105
|
* (kind, agent, agentsRoot, repo, purpose, home, cwd, argv) cannot change
|
|
946
1106
|
* while the job holds a slot or has an unresolved attempt; timing and text can. */
|
|
947
|
-
export function updateSchedule(ws,
|
|
1107
|
+
export function updateSchedule(ws, qid, spec, io) {
|
|
1108
|
+
const id = localScheduleId(qid, "update");
|
|
948
1109
|
const def = validateDefinition(ws, { ...spec, id });
|
|
949
1110
|
return withHostLock(() => withScopeLock(ws, () => {
|
|
950
1111
|
const defs = readDefinitions(ws);
|
|
@@ -954,21 +1115,23 @@ export function updateSchedule(ws, id, spec, io) {
|
|
|
954
1115
|
if (busy && identity(defs.jobs[id]) !== identity(def)) throw scheduleError("E_SCHEDULE_RUNNING", `schedule ${id} is running or has an unresolved attempt; its execution identity, kind, target and captured policy cannot change until it ends (cron, tz and enabled can)`);
|
|
955
1116
|
defs.jobs[id] = { ...def, createdAt: defs.jobs[id].createdAt, updatedAt: new Date().toISOString() };
|
|
956
1117
|
writeDefinitions(ws, defs);
|
|
957
|
-
return describe(ws, id, io);
|
|
1118
|
+
return describe(ws, localId(id), io);
|
|
958
1119
|
}), { retryMs: 3000 });
|
|
959
1120
|
}
|
|
960
|
-
export function setEnabled(ws,
|
|
1121
|
+
export function setEnabled(ws, qid, enabled, io) {
|
|
1122
|
+
const id = localScheduleId(qid, "change");
|
|
961
1123
|
return withScopeLock(ws, () => {
|
|
962
1124
|
const defs = readDefinitions(ws);
|
|
963
1125
|
if (!defs.jobs[id] || defs.jobs[id].kind === "trigger") throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}${defs.jobs[id] ? " (it is a trigger: use oats trigger)" : ""}`);
|
|
964
1126
|
defs.jobs[id] = { ...defs.jobs[id], enabled, updatedAt: new Date().toISOString() };
|
|
965
1127
|
writeDefinitions(ws, defs);
|
|
966
|
-
return describe(ws, id, io);
|
|
1128
|
+
return describe(ws, localId(id), io);
|
|
967
1129
|
});
|
|
968
1130
|
}
|
|
969
1131
|
/** Remove under the host lock (state changes under a live tick are refused
|
|
970
1132
|
* by the lock, not raced). */
|
|
971
|
-
export function removeSchedule(ws,
|
|
1133
|
+
export function removeSchedule(ws, qid, { force = false } = {}) {
|
|
1134
|
+
const id = localScheduleId(qid, "remove");
|
|
972
1135
|
return withHostLock(() => withScopeLock(ws, () => {
|
|
973
1136
|
const defs = readDefinitions(ws);
|
|
974
1137
|
if (!defs.jobs[id] || defs.jobs[id].kind === "trigger") throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}${defs.jobs[id] ? " (it is a trigger: use oats trigger)" : ""}`);
|
|
@@ -981,6 +1144,13 @@ export function removeSchedule(ws, id, { force = false } = {}) {
|
|
|
981
1144
|
return { removed: id };
|
|
982
1145
|
}));
|
|
983
1146
|
}
|
|
1147
|
+
/** The stored id of a LOCAL schedule argument; a workspace schedule is changed in Git (enabled or
|
|
1148
|
+
* disabled on this host through oats-local.yaml schedules.disabled). */
|
|
1149
|
+
function localScheduleId(qid, verb) {
|
|
1150
|
+
const { scope, name } = splitId(qid);
|
|
1151
|
+
if (scope !== "local") throw automationError("E_AUTOMATION_WORKSPACE", `${qid} is a workspace schedule, defined in Git: ${verb} its file there${verb === "remove" ? `, or stop it on this host: oats schedule disable ${qid}` : ""}`, { id: qid });
|
|
1152
|
+
return name;
|
|
1153
|
+
}
|
|
984
1154
|
/** Save the wake job for a freshly spawned instance (id wake-<instance>). */
|
|
985
1155
|
export function saveWakeForHome(ws, { instance, home, wake }) {
|
|
986
1156
|
const id = `wake-${instance}`.slice(0, 40).replace(/-+$/, "");
|