@awebai/oats 0.27.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/oats.mjs +185 -26
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +8 -3
- package/capabilities/oats-okf/bin/oats-okf.mjs +33 -28
- package/capabilities/oats-okf/injects/okf.md +29 -21
- package/capabilities/oats-okf/lib/config.mjs +5 -1
- package/capabilities/oats-okf/lib/consult.mjs +500 -0
- package/capabilities/oats-okf/lib/inspection.mjs +11 -3
- package/capabilities/oats-okf/lib/io.mjs +9 -2
- package/capabilities/oats-okf/lib/sources.mjs +15 -53
- package/capabilities/oats-okf/lib/stores.mjs +10 -7
- package/capabilities/oats-okf/lib/worker.mjs +9 -1
- package/capabilities/oats-okf/oats.json +13 -4
- package/capabilities/oats-okf/skills/okf/SKILL.md +14 -6
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +142 -0
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
- package/docs/design/2026-09-24-phase-d-plan.md +11 -0
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +241 -0
- package/docs/design/2026-09-26-okf-knowledge-operations.md +389 -0
- package/docs/desktop-cli-api.md +93 -7
- package/docs/oats-local.schema.json +2 -1
- package/docs/oats-package.schema.json +39 -0
- package/docs/packages.md +67 -3
- package/docs/release-notes/v0.28.0.md +144 -0
- package/docs/schedules.md +99 -1
- package/docs/souls-and-instances.md +7 -3
- package/docs/workspaces.md +8 -2
- package/lib/core.mjs +22 -4
- package/lib/instance-inspect.mjs +4 -4
- package/lib/instance-resolution.mjs +62 -18
- package/lib/materialize.mjs +13 -0
- package/lib/packages.mjs +90 -6
- package/lib/resolve.mjs +20 -2
- package/lib/schedule.mjs +24 -11
- package/lib/triggers.mjs +545 -0
- package/lib/workspace.mjs +80 -3
- package/package-catalog.json +1 -1
- package/package.json +1 -1
package/lib/schedule.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from "node:url";
|
|
|
29
29
|
import { Cron } from "croner";
|
|
30
30
|
import { loadLocal } from "./workspace.mjs";
|
|
31
31
|
import { noteRuntimeName } from "./deprecation.mjs";
|
|
32
|
+
import { tickTriggers } from "./triggers.mjs";
|
|
32
33
|
import { RESERVED_LAUNCH_ENV, MAX_INSTANCE_NAME, findAgent, findInstanceHomes, inspectInstanceSession, inputInstanceSession, startInstanceSession, retirePendingMarkerPath } from "./core.mjs";
|
|
33
34
|
|
|
34
35
|
export const SCHEDULE_FILE = "oats-schedules.json";
|
|
@@ -65,6 +66,7 @@ const CAPTURED_SCHEDULE_REMOVED = "captured schedules are refused (the captured/
|
|
|
65
66
|
export const HARNESS_CONFLICT = "harness and runtime (its pre-0.27 name) disagree: keep harness: and remove runtime:";
|
|
66
67
|
export function shapeError(def) {
|
|
67
68
|
if (!def || typeof def !== "object" || Array.isArray(def)) return "definition is not an object";
|
|
69
|
+
if (def.kind === "trigger") return "a trigger is managed by oats trigger, not oats schedule";
|
|
68
70
|
if (!["spawn", "command", "wake", "operation"].includes(def.kind)) return `kind ${JSON.stringify(def.kind)} is not spawn, command, wake or operation`;
|
|
69
71
|
if (typeof def.cron !== "string" || typeof def.tz !== "string") return "cron and tz must be strings";
|
|
70
72
|
if (def.kind === "spawn" && typeof def.agent !== "string") return "agent must be a string";
|
|
@@ -246,7 +248,7 @@ export function withHostLock(fn, { retryMs = 0 } = {}) { return withDirLock(join
|
|
|
246
248
|
/** Registry read-modify-write is serialized on its own short lock. */
|
|
247
249
|
function withRegistryLock(fn) { return withDirLock(join(hostScheduleDir(), "registry.lock"), "the host schedule registry", fn, { retryMs: 3000 }); }
|
|
248
250
|
/** Definitions and state are read-modify-write; CRUD serializes here briefly. */
|
|
249
|
-
function withScopeLock(ws, fn) { return withDirLock(join(stateDir(ws), "scope.lock"), `schedules of ${ws}`, fn, { retryMs: 3000 }); }
|
|
251
|
+
export function withScopeLock(ws, fn) { return withDirLock(join(stateDir(ws), "scope.lock"), `schedules of ${ws}`, fn, { retryMs: 3000 }); }
|
|
250
252
|
|
|
251
253
|
export function acquireJobLock(ws, id, owner) {
|
|
252
254
|
const dir = lockDir(ws, id);
|
|
@@ -288,6 +290,7 @@ function validateMessage(message, field) {
|
|
|
288
290
|
/** Validate and normalize one definition against its scope. */
|
|
289
291
|
export function validateDefinition(ws, def, { checkAgent = true } = {}) {
|
|
290
292
|
if (!def || typeof def !== "object" || Array.isArray(def)) throw scheduleError("E_SCHEDULE_INVALID", "definition must be an object", { field: "definition" });
|
|
293
|
+
if (def.kind === "trigger") throw scheduleError("E_SCHEDULE_INVALID", "kind: a trigger is added with `oats trigger add`, not `oats schedule add`", { field: "kind" });
|
|
291
294
|
const id = def.id;
|
|
292
295
|
if (typeof id !== "string" || !ID_RE.test(id)) throw scheduleError("E_SCHEDULE_INVALID", "id: lowercase letters, digits and dashes, 1 to 40 characters", { field: "id" });
|
|
293
296
|
const enabled = def.enabled === undefined ? true : def.enabled;
|
|
@@ -431,13 +434,13 @@ function scheduleBlock(def, minute) {
|
|
|
431
434
|
* may be invoked from inside an unrelated instance; dispatch belongs to the
|
|
432
435
|
* job's cwd and explicit selectors, never to that caller), host configuration
|
|
433
436
|
* such as OATS_HOME_DIR and credentials kept. Shared by command and spawn jobs. */
|
|
434
|
-
function childEnv() {
|
|
437
|
+
export function childEnv() {
|
|
435
438
|
const env = { ...process.env };
|
|
436
439
|
for (const key of [...RESERVED_LAUNCH_ENV,
|
|
437
440
|
"OATS_DEPLOYMENT", "OATS_RESOLUTION", "OATS_CAPABILITY", "OATS_LAYER", "OATS_LEVEL", "OATS_META", "OATS_OPERATION",
|
|
438
441
|
"OATS_REPO", "OATS_BRANCH", "OATS_WORK", "OATS_KIND", "OATS_TASK",
|
|
439
442
|
"OATS_HARNESS", "OATS_PREVIOUS_HARNESS", "OATS_RUNTIME", "OATS_PREVIOUS_RUNTIME", "OATS_RETIRE_INTENT",
|
|
440
|
-
"OATS_TEAM_NAME", "OATS_TEAM_ID", "OATS_TEAM_SCOPE",
|
|
443
|
+
"OATS_TEAM_NAME", "OATS_TEAM_ID", "OATS_TEAM_SCOPE", "OATS_TRIGGER_EVENT_FILE",
|
|
441
444
|
]) delete env[key];
|
|
442
445
|
return env;
|
|
443
446
|
}
|
|
@@ -671,6 +674,8 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
|
|
|
671
674
|
// are gone), then decide launches: a job ordered earlier must not see a
|
|
672
675
|
// slot still held by a home that has already ended.
|
|
673
676
|
// `candidates` (the host tick) means the host already observed every scope.
|
|
677
|
+
// Triggers (kind "trigger") live in the same file and are evaluated by tickTriggers, not here.
|
|
678
|
+
for (const [id, def] of Object.entries(defs.jobs)) if (def && def.kind === "trigger") delete defs.jobs[id];
|
|
674
679
|
if (!candidates) for (const [id, def] of Object.entries(defs.jobs)) observePrevious(ws, id, def, stateOf(id), io, now, { mutate: !dryRun });
|
|
675
680
|
if (observeOnly) { if (!dryRun) writeState(ws, st); return considered; }
|
|
676
681
|
// Least recently LAUNCHED first: only an actual harness launch counts, a
|
|
@@ -778,7 +783,7 @@ export function tickHost({ now = new Date(), io, dryRun = false } = {}) {
|
|
|
778
783
|
try {
|
|
779
784
|
considered.push(...tickWorkspace(ws, { now, io, reg, wsList, dryRun, observeOnly: true }));
|
|
780
785
|
const defs = readDefinitions(ws), st = readState(ws);
|
|
781
|
-
for (const id of Object.
|
|
786
|
+
for (const [id, def] of Object.entries(defs.jobs)) if (def?.kind !== "trigger") jobs.push({ ws, id, key: launchOrderKey(st.jobs[id]) });
|
|
782
787
|
} catch (e) { considered.push({ workspace: ws, action: "error", error: e.message }); }
|
|
783
788
|
}
|
|
784
789
|
jobs.sort((a, b) => a.key.localeCompare(b.key) || a.ws.localeCompare(b.ws) || a.id.localeCompare(b.id));
|
|
@@ -786,6 +791,11 @@ export function tickHost({ now = new Date(), io, dryRun = false } = {}) {
|
|
|
786
791
|
try { considered.push(...tickWorkspace(j.ws, { now, io, reg, wsList, dryRun, candidates: [j.id] })); }
|
|
787
792
|
catch (e) { considered.push({ workspace: j.ws, id: j.id, action: "error", error: e.message }); }
|
|
788
793
|
}
|
|
794
|
+
// Triggers: each scope's event-driven spawns, polled at their own interval (lib/triggers.mjs).
|
|
795
|
+
for (const ws of wsList) {
|
|
796
|
+
try { considered.push(...tickTriggers(ws, { now, io, dryRun })); }
|
|
797
|
+
catch (e) { considered.push({ workspace: ws, action: "error", error: `triggers: ${e.message}` }); }
|
|
798
|
+
}
|
|
789
799
|
if (!dryRun) writeHostState({ ...readHostState(), lastTick: now.toISOString(), minute: minuteKey(minuteStart(now)) });
|
|
790
800
|
return { tickedAt: now.toISOString(), minute: minuteKey(minuteStart(now)), considered, scheduler: schedulerStatus(undefined, io) };
|
|
791
801
|
};
|
|
@@ -797,7 +807,7 @@ export function tickHost({ now = new Date(), io, dryRun = false } = {}) {
|
|
|
797
807
|
export function runNow(ws, id, { now = new Date(), io, force = false } = {}) {
|
|
798
808
|
const defs = readDefinitions(ws);
|
|
799
809
|
const def = defs.jobs[id];
|
|
800
|
-
if (!def) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
810
|
+
if (!def || def.kind === "trigger") throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
801
811
|
if (!def.enabled && !force) throw scheduleError("E_SCHEDULE_DISABLED", `schedule ${id} is disabled; enable it or pass --force`);
|
|
802
812
|
const reg = readRegistry();
|
|
803
813
|
const wsList = reg.workspaces.includes(resolve(ws)) ? reg.workspaces : [...reg.workspaces, resolve(ws)];
|
|
@@ -824,7 +834,7 @@ export function runNow(ws, id, { now = new Date(), io, force = false } = {}) {
|
|
|
824
834
|
export function reconcile(ws, id, { io, now = new Date(), clear = false } = {}) {
|
|
825
835
|
const defs = readDefinitions(ws);
|
|
826
836
|
const def = defs.jobs[id];
|
|
827
|
-
if (!def) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
837
|
+
if (!def || def.kind === "trigger") throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
828
838
|
return withHostLock(() => {
|
|
829
839
|
const st = readState(ws);
|
|
830
840
|
const js = st.jobs[id] || (st.jobs[id] = {});
|
|
@@ -898,6 +908,7 @@ export function describe(ws, id, io, { defs, st, now = new Date() } = {}) {
|
|
|
898
908
|
defs ||= readDefinitions(ws); st ||= readState(ws);
|
|
899
909
|
const def = defs.jobs[id];
|
|
900
910
|
if (!def) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
911
|
+
if (def.kind === "trigger") throw scheduleError("E_SCHEDULE_UNKNOWN", `${id} is a trigger, not a schedule — use oats trigger show ${id}`);
|
|
901
912
|
// Subject truth: the definition's own id must be the key it is stored under.
|
|
902
913
|
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 });
|
|
903
914
|
const js = st.jobs[id] || {};
|
|
@@ -910,11 +921,13 @@ export function describe(ws, id, io, { defs, st, now = new Date() } = {}) {
|
|
|
910
921
|
export function listSchedules(ws, io, { now = new Date() } = {}) {
|
|
911
922
|
const defs = readDefinitions(ws), st = readState(ws);
|
|
912
923
|
// One job's bad identity or history does not fail the others: it is reported as its own row.
|
|
913
|
-
const schedules = Object.keys(defs.jobs).sort().map((id) => {
|
|
924
|
+
const schedules = Object.keys(defs.jobs).filter((id) => defs.jobs[id]?.kind !== "trigger").sort().map((id) => {
|
|
914
925
|
try { return describe(ws, id, io, { defs, st, now }); }
|
|
915
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: [] }; }
|
|
916
927
|
});
|
|
917
|
-
|
|
928
|
+
// 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) };
|
|
918
931
|
}
|
|
919
932
|
export function addSchedule(ws, spec, io) {
|
|
920
933
|
const def = validateDefinition(ws, spec);
|
|
@@ -935,7 +948,7 @@ export function updateSchedule(ws, id, spec, io) {
|
|
|
935
948
|
const def = validateDefinition(ws, { ...spec, id });
|
|
936
949
|
return withHostLock(() => withScopeLock(ws, () => {
|
|
937
950
|
const defs = readDefinitions(ws);
|
|
938
|
-
if (!defs.jobs[id]) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
951
|
+
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)" : ""}`);
|
|
939
952
|
const identity = (d) => JSON.stringify([d.kind, d.agent, d.agentsRoot, d.repo, d.purpose, d.home, d.cwd, d.argv, d.operation, d.definitionVersion, d.recurrencePolicy, d.execution, d.preparation]);
|
|
940
953
|
const busy = !!jobLockInfo(ws, id) || !!readState(ws).jobs[id]?.attempt;
|
|
941
954
|
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)`);
|
|
@@ -947,7 +960,7 @@ export function updateSchedule(ws, id, spec, io) {
|
|
|
947
960
|
export function setEnabled(ws, id, enabled, io) {
|
|
948
961
|
return withScopeLock(ws, () => {
|
|
949
962
|
const defs = readDefinitions(ws);
|
|
950
|
-
if (!defs.jobs[id]) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
963
|
+
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)" : ""}`);
|
|
951
964
|
defs.jobs[id] = { ...defs.jobs[id], enabled, updatedAt: new Date().toISOString() };
|
|
952
965
|
writeDefinitions(ws, defs);
|
|
953
966
|
return describe(ws, id, io);
|
|
@@ -958,7 +971,7 @@ export function setEnabled(ws, id, enabled, io) {
|
|
|
958
971
|
export function removeSchedule(ws, id, { force = false } = {}) {
|
|
959
972
|
return withHostLock(() => withScopeLock(ws, () => {
|
|
960
973
|
const defs = readDefinitions(ws);
|
|
961
|
-
if (!defs.jobs[id]) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
974
|
+
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)" : ""}`);
|
|
962
975
|
const st = readState(ws), job = st.jobs[id];
|
|
963
976
|
if ((jobLockInfo(ws, id) || job?.attempt || job?.lastRun?.outcome === "unknown") && !force) throw scheduleError("E_SCHEDULE_RUNNING", `schedule ${id} is running or has unresolved effects; reconcile first, or --force forgets the job without stopping anything`);
|
|
964
977
|
delete defs.jobs[id];
|