@awebai/oats 0.42.1 → 0.43.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 +58 -15
- package/docs/desktop-cli-api.md +32 -11
- package/docs/desktop.md +11 -0
- package/docs/packages.md +6 -0
- package/docs/release-notes/v0.43.0.md +93 -0
- package/docs/schedules.md +49 -15
- package/lib/automations.mjs +31 -5
- package/lib/schedule.mjs +21 -8
- package/lib/servers.mjs +5 -0
- package/lib/triggers.mjs +22 -3
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -47,7 +47,7 @@ import { attachArgv, checkRemote, connectServer, forgetSnapshot, getServer, insp
|
|
|
47
47
|
import { spawnSync as spawnSyncProc } from "node:child_process";
|
|
48
48
|
import { tickTriggers } from "../lib/triggers.mjs";
|
|
49
49
|
import * as A from "../lib/automations.mjs";
|
|
50
|
-
import { parseEnvelopeText, unresolvedScheduleAttempts, scheduleScopeOf, listSchedules, describe as describeSchedule, testSchedule, addSchedule, updateSchedule, setEnabled as setScheduleEnabled, removeSchedule, runNow as runScheduleNow, reconcile as reconcileSchedule, tickHost, tickWorkspace, scopeAutomations, scheduleKind, registerWorkspace, unregisterWorkspace, readRegistry, schedulerStatus, saveWakeForHome, removeWakeForHome, wakeFromFlags, withHostLock, scheduleError, SCHEDULE_API } from "../lib/schedule.mjs";
|
|
50
|
+
import { parseEnvelopeText, unresolvedScheduleAttempts, scheduleScopeOf, listSchedules, describe as describeSchedule, testSchedule, addSchedule, updateSchedule, updateScheduleDescription, setEnabled as setScheduleEnabled, removeSchedule, runNow as runScheduleNow, reconcile as reconcileSchedule, tickHost, tickWorkspace, scopeAutomations, scheduleKind, registerWorkspace, unregisterWorkspace, readRegistry, schedulerStatus, saveWakeForHome, removeWakeForHome, wakeFromFlags, withHostLock, scheduleError, SCHEDULE_API } from "../lib/schedule.mjs";
|
|
51
51
|
import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
|
|
52
52
|
import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
|
|
53
53
|
|
|
@@ -69,6 +69,9 @@ const KERNEL_SWITCHES = new Set(["allow-child-spawns", "apply", "check", "clear"
|
|
|
69
69
|
* value is kept out of argv, where it would read as a flag (`--message=--clear` must not
|
|
70
70
|
* clear), and flag() answers it from `inline`. */
|
|
71
71
|
const INLINE_TEXT_FLAGS = new Set(["message"]);
|
|
72
|
+
/** Free-text flags whose inline value is always kept whole, the empty one included:
|
|
73
|
+
* `--description=` clears a trigger's or schedule's description. */
|
|
74
|
+
const INLINE_WHOLE_FLAGS = new Set(["description"]);
|
|
72
75
|
function expandInlineValues(argv) {
|
|
73
76
|
const out = [];
|
|
74
77
|
const inline = new Map();
|
|
@@ -77,7 +80,7 @@ function expandInlineValues(argv) {
|
|
|
77
80
|
const eq = a.indexOf("=");
|
|
78
81
|
if (!a.startsWith("--") || eq <= 2) { out.push(a); continue; }
|
|
79
82
|
const name = a.slice(2, eq), value = a.slice(eq + 1);
|
|
80
|
-
if (INLINE_TEXT_FLAGS.has(name) && value.startsWith("--")) { inline.set(name, value); out.push(`--${name}`); continue; }
|
|
83
|
+
if (INLINE_WHOLE_FLAGS.has(name) || (INLINE_TEXT_FLAGS.has(name) && value.startsWith("--"))) { inline.set(name, value); out.push(`--${name}`); continue; }
|
|
81
84
|
problem ??= KERNEL_SWITCHES.has(name) ? `--${name} takes no value (got ${a})` : value === "" ? `--${name}= needs a value` : value.startsWith("--") ? `--${name}= takes a value, not an option (got ${a})` : undefined;
|
|
82
85
|
out.push(`--${name}`, value);
|
|
83
86
|
}
|
|
@@ -105,6 +108,20 @@ function valueFlag(name) {
|
|
|
105
108
|
if (value === true) cmdFail("E_BAD_ARGS", `--${name} needs a value`);
|
|
106
109
|
return value;
|
|
107
110
|
}
|
|
111
|
+
/** `--description=<text>` on the trigger and schedule verbs: undefined when absent, the text
|
|
112
|
+
* otherwise (`""` clears). The spaced form works too, but only `=` carries an empty value or one
|
|
113
|
+
* that starts with `--`. */
|
|
114
|
+
function descriptionFlag() {
|
|
115
|
+
const value = flag("description");
|
|
116
|
+
if (value === true) throw Object.assign(new Error("--description needs a value: --description=<text> (--description= clears it)"), { code: "E_BAD_ARGS" });
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
/** A spec with the --description flag applied: it sets or overrides `description`; `""` removes it. */
|
|
120
|
+
function withDescription(spec, description) {
|
|
121
|
+
if (description === undefined || !spec || typeof spec !== "object" || Array.isArray(spec)) return spec;
|
|
122
|
+
const { description: _d, ...rest } = spec; void _d;
|
|
123
|
+
return description === "" ? rest : { ...rest, description };
|
|
124
|
+
}
|
|
108
125
|
const die = (msg, exit = 1) => { console.error(`oats: ${msg}`); process.exit(exit); };
|
|
109
126
|
/** A command's harness: --harness, or --runtime, its pre-0.27 name (the released okf worker and
|
|
110
127
|
* a 0.26-era Desktop pass it) — read either, with the deprecation warning. Both, disagreeing,
|
|
@@ -1430,9 +1447,11 @@ async function automationsCmd() {
|
|
|
1430
1447
|
/** `oats trigger|schedule add --workspace <member> --runs-on <host> --owner <host>/<login>`: the
|
|
1431
1448
|
* workspace file, written into the member's checkout when `--dir` (or the cwd) is inside one, else
|
|
1432
1449
|
* printed. What is written must read back as the same automation (parseAutomationFile + expand). */
|
|
1433
|
-
async function addWorkspaceAutomation(desc, { id, body }) {
|
|
1450
|
+
async function addWorkspaceAutomation(desc, { id, body, description }) {
|
|
1434
1451
|
const bail = (code, msg, details) => { throw Object.assign(new Error(msg), { code, details }); };
|
|
1435
|
-
const member = flag("workspace"), runsOn = flag("runs-on"), owner = flag("owner")
|
|
1452
|
+
const member = flag("workspace"), runsOn = flag("runs-on"), owner = flag("owner");
|
|
1453
|
+
// The header's description: refused here when out of the rule (discovery would only warn).
|
|
1454
|
+
if (description !== undefined && description !== "") A.validateDescription(description, desc.kind === "trigger" ? "E_TRIGGER_INVALID" : "E_SCHEDULE_INVALID");
|
|
1436
1455
|
if (typeof member !== "string" || !member) bail("E_BAD_ARGS", "--workspace needs a member name");
|
|
1437
1456
|
if (typeof runsOn !== "string" || !A.HOST_NAME_RE.test(runsOn)) bail("E_BAD_ARGS", "--runs-on <host>: the host.name (oats-local.yaml) of the machine that runs it");
|
|
1438
1457
|
if (!A.parseOwner(owner)) bail("E_BAD_ARGS", "--owner <host>/<login>: the GitHub account it acts as, e.g. github.com/acme-kb-bot");
|
|
@@ -1441,7 +1460,7 @@ async function addWorkspaceAutomation(desc, { id, body }) {
|
|
|
1441
1460
|
const row = (discovery.members || []).find((m) => m.confirmed && (memberLabel(m.key) === member || m.key === member));
|
|
1442
1461
|
if (!row) bail("E_AUTOMATION_MEMBER", `${member} is not a confirmed member of this workspace (members: ${(discovery.members || []).filter((m) => m.confirmed).map((m) => memberLabel(m.key)).join(", ") || "none"})`, { member });
|
|
1443
1462
|
const name = memberLabel(row.key);
|
|
1444
|
-
const content = A.automationFileText(desc, { id, description:
|
|
1463
|
+
const content = A.automationFileText(desc, { id, description: description || undefined, runsOn, owner, body });
|
|
1445
1464
|
const parsed = A.parseAutomationFile(desc, { stem: id, path: `${desc.folder}/${id}.yaml`, bytes: Buffer.from(content), member: name, repoKey: row.key, commit: row.commit });
|
|
1446
1465
|
if (parsed.problem) bail(parsed.problem.code, parsed.problem.message, { path: parsed.problem.path });
|
|
1447
1466
|
await desc.expand(parsed.entry);
|
|
@@ -2603,19 +2622,25 @@ async function scheduleCmd() {
|
|
|
2603
2622
|
case "show": return out({ schedule: describeSchedule(ws(), needId(), io, { ctx: ctx() }) });
|
|
2604
2623
|
case "test": return out({ test: testSchedule(ws(), needId(), io, { ctx: ctx() }) }, (r) => `${r.test.qualifiedId}: ${r.test.placement.runsHere ? "runs here" : `not here (${r.test.placement.reason ?? "disabled here"})`}${r.test.soul ? `; soul ${r.test.soul.name} ${r.test.soul.resolves ? "resolves" : `does NOT resolve (${r.test.soul.error?.code})`}` : ""}; next due ${r.test.nextDue ?? "never"}${r.test.problems.length ? `\n ${r.test.problems.join("\n ")}` : ""}\n(spawned nothing)`);
|
|
2605
2624
|
case "add": {
|
|
2606
|
-
const spec = readSpec();
|
|
2625
|
+
const spec = withDescription(readSpec(), descriptionFlag());
|
|
2607
2626
|
if (flag("workspace") !== undefined) {
|
|
2608
2627
|
const wid = id ?? spec.id;
|
|
2609
2628
|
if (typeof wid !== "string") throw scheduleError("E_BAD_ARGS", "oats schedule add <id> --workspace <member> …: the id names the file (oats-schedules/<id>.yaml)");
|
|
2610
|
-
|
|
2611
|
-
const
|
|
2629
|
+
// A description is the file header's, never the body's.
|
|
2630
|
+
const { id: _i, enabled: _e, kind, description, ...rest } = spec; void _i; void _e;
|
|
2631
|
+
const r = await addWorkspaceAutomation(scheduleKind({ dep: ws() }), { id: wid, body: { run: kind ?? "spawn", ...rest }, description });
|
|
2612
2632
|
return out(r, printWorkspaceAdd);
|
|
2613
2633
|
}
|
|
2614
2634
|
if (id && spec.id === undefined) spec.id = id;
|
|
2615
2635
|
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" });
|
|
2616
2636
|
return out({ schedule: addSchedule(ws(), spec, io) });
|
|
2617
2637
|
}
|
|
2618
|
-
case "update":
|
|
2638
|
+
case "update": {
|
|
2639
|
+
const description = descriptionFlag();
|
|
2640
|
+
// --description alone (no --file/--spec-json) changes only the description.
|
|
2641
|
+
if (description !== undefined && flag("file") === undefined && flag("spec-json") === undefined) return out({ schedule: updateScheduleDescription(ws(), needId(), description, io) });
|
|
2642
|
+
return out({ schedule: updateSchedule(ws(), needId(), withDescription(readSpec(), description), io) });
|
|
2643
|
+
}
|
|
2619
2644
|
case "enable":
|
|
2620
2645
|
case "disable": {
|
|
2621
2646
|
const on = sub === "enable";
|
|
@@ -2658,7 +2683,7 @@ async function scheduleCmd() {
|
|
|
2658
2683
|
if (op === "status") return out({ scheduler: schedulerStatus(ws(), io) });
|
|
2659
2684
|
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
2660
2685
|
}
|
|
2661
|
-
default: throw scheduleError("E_BAD_ARGS", "usage: oats schedule list|show <id>|test <id>|add <id> --file <spec.json> [--workspace <member> --runs-on <host> --owner <host>/<login>]|update <id> --file <spec.json
|
|
2686
|
+
default: throw scheduleError("E_BAD_ARGS", "usage: oats schedule list|show <id>|test <id>|add <id> --file <spec.json> [--description=<text>] [--workspace <member> --runs-on <host> --owner <host>/<login>]|update <id> (--file <spec.json> [--description=<text>] | --description=<text>)|enable <id>|disable <id>|run <id> [--force]|remove <id> [--force]|reconcile <id> [--clear]|tick [--dry-run] [--host]|host install [--max-concurrent <N|default>] [--triggers-max-concurrent <N|none>]|uninstall|status [--dir <workspace>|--server <id>] [--json]");
|
|
2662
2687
|
}
|
|
2663
2688
|
} catch (e) {
|
|
2664
2689
|
// K8b: typed refusal details travel (identity mismatch: key/declared; a refused file: its integrity source).
|
|
@@ -2677,7 +2702,7 @@ async function triggerCmd() {
|
|
|
2677
2702
|
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
2678
2703
|
const out = (result, text) => { if (JSON_MODE) jsonOk(result); else console.log(text ? text(result) : JSON.stringify(result, null, 2)); };
|
|
2679
2704
|
const needId = () => { if (!id) throw T.triggerError("E_BAD_ARGS", `oats trigger ${sub} <id>`); return id; };
|
|
2680
|
-
const usage = "usage: oats trigger add (--file <trigger.json> | --from <package>:<template> [--set <name>=<value>]… [--id <id>]) [--workspace <member> --runs-on <host> --owner <host>/<login>] | list | show <id> | enable <id> | disable <id> | remove <id> | test <id> | status [<id>] [--dir <deployment>] [--json] (<id>: local/<id> or <member>/<id>)";
|
|
2705
|
+
const usage = "usage: oats trigger add (--file <trigger.json> | --from <package>:<template> [--set <name>=<value>]… [--id <id>]) [--description=<text>] [--workspace <member> --runs-on <host> --owner <host>/<login>] | update <id> --description=<text> | list | show <id> | enable <id> | disable <id> | remove <id> | test <id> | status [<id>] [--dir <deployment>] [--json] (<id>: local/<id> or <member>/<id>)";
|
|
2681
2706
|
const where = (t) => (t.origin?.kind === "workspace" ? (t.runsHere ? `runs here as ${t.owner}` : `${t.reason === "assigned-elsewhere" ? `runs on ${t.runsOn}` : t.reason ?? "disabled here"}`) : t.enabledHere ? "local" : "local, disabled");
|
|
2682
2707
|
const line = (t) => `${t.id} ${where(t)} ${t.on?.source ?? "?"} ${t.on?.repo ?? "?"} [${(t.on?.events || []).join(",")}]${t.on?.labels?.length ? ` labels ${t.on.labels.join(",")}` : ""} every ${t.on?.poll ?? "?"} → spawn ${t.spawn?.soul ?? "?"}${t.spawn?.teams?.length ? ` in ${t.spawn.teams.join(",")}` : ""}${t.invalid ? ` INVALID: ${t.invalid.message}` : ""}`;
|
|
2683
2708
|
// The workspace automations of this deployment (the snapshot) placed on this host.
|
|
@@ -2697,6 +2722,15 @@ async function triggerCmd() {
|
|
|
2697
2722
|
actx = undefined;
|
|
2698
2723
|
return out({ trigger: T.describeTrigger(ws(), id, ctx()) }, (r) => line(r.trigger));
|
|
2699
2724
|
}
|
|
2725
|
+
case "update": {
|
|
2726
|
+
// Description only, for now (0.43.0): a full trigger update is remove + add.
|
|
2727
|
+
const known = new Set(["--description", "--dir", "--json"]);
|
|
2728
|
+
const other = args.slice(2).find((a) => a.startsWith("--") && !known.has(a));
|
|
2729
|
+
if (other) throw T.triggerError("E_BAD_ARGS", `oats trigger update: only --description is supported for now (got ${other}); to change anything else, remove the trigger and add it again`);
|
|
2730
|
+
const description = descriptionFlag();
|
|
2731
|
+
if (description === undefined) throw T.triggerError("E_BAD_ARGS", "oats trigger update <id> --description=<text> (--description= clears it)");
|
|
2732
|
+
return out({ trigger: T.updateTriggerDescription(ws(), needId(), description, ctx()) }, (r) => line(r.trigger));
|
|
2733
|
+
}
|
|
2700
2734
|
case "remove": return out(T.removeTrigger(ws(), needId(), ctx()), (r) => `removed trigger ${r.removed}${r.live.length ? ` (its live instances keep running: ${r.live.join(", ")})` : ""}`);
|
|
2701
2735
|
case "status": return out(T.triggerStatus(ws(), id, ctx()), (r) => r.triggers.map((t) => `${t.id} last poll ${t.lastPoll ? `${t.lastPoll.at} ${t.lastPoll.ok ? `ok (${t.lastPoll.matching}/${t.lastPoll.prs} PRs match)` : `FAILED: ${t.lastPoll.error}`}` : "never"} pending ${t.pending.length} fired ${t.firedTotal} live ${t.live.map((l) => l.instance).join(",") || "none"}${t.lastError ? `\n last error ${t.lastError.at}: ${t.lastError.message}` : ""}`).join("\n") || "(no triggers)");
|
|
2702
2736
|
case "test": {
|
|
@@ -2731,6 +2765,7 @@ async function triggerCmd() {
|
|
|
2731
2765
|
}
|
|
2732
2766
|
const idFlag = flag("id");
|
|
2733
2767
|
if (idFlag === true) throw T.triggerError("E_BAD_ARGS", "--id needs a trigger id");
|
|
2768
|
+
const description = descriptionFlag();
|
|
2734
2769
|
let spec;
|
|
2735
2770
|
if (file !== undefined) {
|
|
2736
2771
|
if (file === true || !existsSync(file)) throw T.triggerError("E_BAD_ARGS", `--file ${file === true ? "needs a path" : `not found: ${file}`}`);
|
|
@@ -2743,12 +2778,14 @@ async function triggerCmd() {
|
|
|
2743
2778
|
const kinds = await automationKinds(ws(), readLock(ws()), remoteOptionsFromEnv());
|
|
2744
2779
|
const wid = typeof idFlag === "string" ? idFlag : from !== undefined ? String(from).split(":")[1] : spec?.id;
|
|
2745
2780
|
if (typeof wid !== "string") throw T.triggerError("E_BAD_ARGS", "--workspace: name the trigger with --id (or an id in the file)");
|
|
2746
|
-
|
|
2747
|
-
|
|
2781
|
+
// A description is the file header's (the flag, else the file's), never the body's; made
|
|
2782
|
+
// from a template without one, the row shows the template's.
|
|
2783
|
+
const body = from !== undefined ? { from: String(from), ...(Object.keys(sets).length ? { set: sets } : {}) } : (({ id: _i, kind: _k, enabled: _e, template: _t, createdAt: _c, updatedAt: _u, description: _d, ...rest }) => rest)(spec);
|
|
2784
|
+
const r = await addWorkspaceAutomation(kinds.trigger, { id: wid, body, description: description ?? spec?.description });
|
|
2748
2785
|
return out(r, printWorkspaceAdd);
|
|
2749
2786
|
}
|
|
2750
2787
|
if (spec === undefined) spec = await triggerFromPackage(T, String(from), sets, idFlag);
|
|
2751
|
-
return out({ trigger: T.addTrigger(ws(), spec) }, (r) => `added ${line(r.trigger)}\n(\`oats trigger test ${r.trigger.id}\` checks gh, the repository, the soul and the teams on this host)`);
|
|
2788
|
+
return out({ trigger: T.addTrigger(ws(), withDescription(spec, description)) }, (r) => `added ${line(r.trigger)}\n(\`oats trigger test ${r.trigger.id}\` checks gh, the repository, the soul and the teams on this host)`);
|
|
2752
2789
|
}
|
|
2753
2790
|
default: throw T.triggerError("E_BAD_ARGS", usage);
|
|
2754
2791
|
}
|
|
@@ -3271,7 +3308,7 @@ function versionCmd() {
|
|
|
3271
3308
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
3272
3309
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
3273
3310
|
// never listed.
|
|
3274
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "schedule-host-caps", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-3", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "spawn-preview-max-age", "launch-config-default", "capability-show", "capture-file", "workspace-identity", "server-connect", "capability-route", "servers-per-workspace", "operator-default-soul", "waiting-on-you"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2, capabilityShowApi: 1 }));
|
|
3311
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "schedule-host-caps", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-3", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "spawn-preview-max-age", "launch-config-default", "capability-show", "capture-file", "workspace-identity", "server-connect", "capability-route", "servers-per-workspace", "operator-default-soul", "waiting-on-you", "automation-descriptions"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2, capabilityShowApi: 1 }));
|
|
3275
3312
|
return;
|
|
3276
3313
|
}
|
|
3277
3314
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3575,6 +3612,8 @@ async function serverRouteCmd() {
|
|
|
3575
3612
|
rest.push("--spec-json", readFileSync(f, "utf8"));
|
|
3576
3613
|
continue;
|
|
3577
3614
|
}
|
|
3615
|
+
// An inline --description travels inline (it may be empty, or start with --).
|
|
3616
|
+
if (a === "--description" && inlineValues.has("description")) { rest.push(`--description=${inlineValues.get("description")}`); continue; }
|
|
3578
3617
|
rest.push(a);
|
|
3579
3618
|
}
|
|
3580
3619
|
let out;
|
|
@@ -3947,10 +3986,14 @@ Usage:
|
|
|
3947
3986
|
host install|uninstall|status explicit IANA tz; see docs/schedules.md); --server
|
|
3948
3987
|
routes to that host's workspace
|
|
3949
3988
|
install [--max-concurrent N|default] [--triggers-max-concurrent N|none]
|
|
3989
|
+
oats schedule add|update <id> … --description=<text> a one-line summary (1-200 characters)
|
|
3990
|
+
oats schedule update <id> --description=<text> change only it (--description= clears it)
|
|
3950
3991
|
oats trigger add (--file <json> | --from <package>:<template> [--set k=v]) | list | show | enable
|
|
3951
3992
|
| disable | remove <id> | test <id> | status [<id>] event-driven spawns (github.pull_request
|
|
3952
3993
|
polled with the host's gh by the schedule tick;
|
|
3953
3994
|
see docs/schedules.md#triggers)
|
|
3995
|
+
oats trigger add … --description=<text> a one-line summary (1-200 characters)
|
|
3996
|
+
oats trigger update <id> --description=<text> change only it (--description= clears it)
|
|
3954
3997
|
oats trigger|schedule add … --workspace <member> --runs-on <host> --owner <host>/<login>
|
|
3955
3998
|
a trigger/schedule declared in Git (<member>/<id>):
|
|
3956
3999
|
writes oats-triggers|oats-schedules/<id>.yaml in a
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -108,6 +108,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
108
108
|
| `capability-route` | `oats <namespace> <command> … --server <id>` runs the capability command on the server, OATS 0.39.0 ([Capability commands on a server](#capability-commands-on-a-server)) | |
|
|
109
109
|
| `operator-default-soul` | a capability command from a deployment without `--soul` runs as the first soul that provides its namespace (named on stderr); none is `E_BAD_ARGS`, OATS 0.39.0 ([capabilities.md](capabilities.md)) | |
|
|
110
110
|
| `waiting-on-you` | the `waiting` event kind and the session boundary rule; `oats instance waiting` and `oats instance attention`; `waitingOnYou` (with `message`) on `oats status --json` instance rows, on `oats session inspect --json` and in the events read, OATS 0.40.0 ([Waiting on you](#waiting-on-you)) | `eventsApi: 2` |
|
|
111
|
+
| `automation-descriptions` | a `description` on every trigger and schedule row, local or workspace, by one rule; `oats schedule update <id> --description=<text>` (the description only) and `oats trigger update <id> --description=<text>`; `--description=<text>` on `schedule add` and `trigger add`, OATS 0.43.0 ([Shared row fields](#automations-shared-rows)) | |
|
|
111
112
|
|
|
112
113
|
Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
|
|
113
114
|
`workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
|
|
@@ -2862,6 +2863,7 @@ workspace item of a readable member. Render the rows; never re-derive them.
|
|
|
2862
2863
|
```text
|
|
2863
2864
|
oats schedule list [--dir <d>] --json
|
|
2864
2865
|
oats schedule show <id> --json
|
|
2866
|
+
oats schedule update <id> --description=<text> [--dir <d>] --json
|
|
2865
2867
|
```
|
|
2866
2868
|
|
|
2867
2869
|
```json
|
|
@@ -2888,6 +2890,14 @@ oats schedule show <id> --json
|
|
|
2888
2890
|
- `list`: `{scope, scheduleApi, scheduleHistoryApi, integrity, host,
|
|
2889
2891
|
triggers, snapshot, schedules, scheduler}`; `triggers` counts the trigger
|
|
2890
2892
|
definitions left out. `show <id>`: `{schedule}`, without `integrity`.
|
|
2893
|
+
- `update <id> --description=<text>` without `--file`/`--spec-json` (feature
|
|
2894
|
+
`automation-descriptions`) changes only a local schedule's description,
|
|
2895
|
+
any kind's (a command schedule too), and is allowed while the job runs or
|
|
2896
|
+
has an unresolved attempt. `--description=` (empty) removes it. It answers
|
|
2897
|
+
`{schedule}` (the row `show` answers). A workspace schedule refuses
|
|
2898
|
+
`E_AUTOMATION_WORKSPACE`; out of the rule is `E_SCHEDULE_INVALID {field:
|
|
2899
|
+
"description"}`. Run it as argv, without a shell, always in the `=` form.
|
|
2900
|
+
An older kernel refuses it as a missing `--file`.
|
|
2891
2901
|
- **A readable row**: the definition (`id, kind, cron, tz, enabled, …`, and
|
|
2892
2902
|
`agent/task/purpose/harness` for a spawn, `argv/cwd` for a command, the
|
|
2893
2903
|
message for a wake, the operation for an operation) plus `scope,
|
|
@@ -2907,14 +2917,8 @@ oats schedule show <id> --json
|
|
|
2907
2917
|
limit. Desktop accepts these schedule names for creation, editing and row
|
|
2908
2918
|
actions (enable, disable, test, run and reconcile), including qualified IDs.
|
|
2909
2919
|
A spawn's derived instance name still has a 64-character limit.
|
|
2910
|
-
- **`description
|
|
2911
|
-
|
|
2912
|
-
from its file header). It is one line of at most 200 characters with no
|
|
2913
|
-
control characters; show it in place of the argv when present. Like `task`,
|
|
2914
|
-
it is untrusted text: render it as text. Desktop preserves it unchanged
|
|
2915
|
-
through edits to timing and other fields. A kernel before 0.40.0 sends
|
|
2916
|
-
`null` for every local schedule, and drops a `description` given to `oats
|
|
2917
|
-
schedule add` without refusing it.
|
|
2920
|
+
- **`description`**: the [shared row field](#automations-shared-rows).
|
|
2921
|
+
Desktop preserves it unchanged through edits to timing and other fields.
|
|
2918
2922
|
- **An unreadable row** (`list` only): `{id, scope, scheduleApi,
|
|
2919
2923
|
scheduleHistoryApi, unreadable: {code, message}, history: {status:
|
|
2920
2924
|
"corrupt", stored: null, truncated: false}, recentRuns: []}`. One bad job
|
|
@@ -2964,14 +2968,14 @@ oats schedule show <id> --json
|
|
|
2964
2968
|
### `oats trigger`
|
|
2965
2969
|
|
|
2966
2970
|
```text
|
|
2967
|
-
oats trigger list | show <id> | status [<id>] | test <id> | add (--file <json> | --from <package>:<template> [--set k=v]) | enable <id> | disable <id> | remove <id> --json
|
|
2971
|
+
oats trigger list | show <id> | status [<id>] | test <id> | add (--file <json> | --from <package>:<template> [--set k=v]) [--description=<text>] | update <id> --description=<text> | enable <id> | disable <id> | remove <id> --json
|
|
2968
2972
|
```
|
|
2969
2973
|
|
|
2970
2974
|
Event-driven spawns. Local definitions live in `oats-schedules.json` (`kind:
|
|
2971
2975
|
"trigger"`).
|
|
2972
2976
|
|
|
2973
2977
|
- `list`: `{triggerApi, scope, host, snapshot, triggers, scheduler}`.
|
|
2974
|
-
`show`, `add`, `enable`, `disable` → `{trigger}`; `remove` → `{removed,
|
|
2978
|
+
`show`, `add`, `update`, `enable`, `disable` → `{trigger}`; `remove` → `{removed,
|
|
2975
2979
|
live: [instance]}`. A stored definition that no longer validates carries
|
|
2976
2980
|
`invalid: {code, message, field?}`.
|
|
2977
2981
|
- **A trigger row**: the [shared row fields](#automations-shared-rows) plus
|
|
@@ -3007,6 +3011,12 @@ Event-driven spawns. Local definitions live in `oats-schedules.json` (`kind:
|
|
|
3007
3011
|
cannot reach is a warning.
|
|
3008
3012
|
- A triggered instance records `instance.json.trigger`; its event file is
|
|
3009
3013
|
`OATS_TRIGGER_EVENT_FILE`.
|
|
3014
|
+
- `update <id> --description=<text>` (feature `automation-descriptions`)
|
|
3015
|
+
changes only a local trigger's description; `--description=` (empty)
|
|
3016
|
+
removes it. It sets `updatedAt` and leaves fired and pending events
|
|
3017
|
+
untouched. `<id>` is `local/<id>` or the bare id; a workspace trigger
|
|
3018
|
+
refuses `E_AUTOMATION_WORKSPACE`. Any other flag is `E_BAD_ARGS` ("only
|
|
3019
|
+
--description is supported for now"). An older kernel has no `update`.
|
|
3010
3020
|
- Errors: `E_TRIGGER_INVALID {field}`, `E_TRIGGER_EXISTS`,
|
|
3011
3021
|
`E_TRIGGER_UNKNOWN`, `E_TRIGGER_TEAMS`, `E_TRIGGER_POLL`,
|
|
3012
3022
|
`E_TRIGGER_FAILED`, `E_BAD_ARGS`, `E_PACKAGE_MISSING`,
|
|
@@ -3032,7 +3042,18 @@ registered). `maxConcurrent` is the effective scheduled-job cap (default 5);
|
|
|
3032
3042
|
localPath}` or `{kind: "workspace", repoKey, path, commit, url,
|
|
3033
3043
|
localPath}` (`url` for `github.com` only; `localPath` `null` when the
|
|
3034
3044
|
member is not cloned here).
|
|
3035
|
-
- `description
|
|
3045
|
+
- `description`: what it is for, in words, or `null`. One rule for both kinds
|
|
3046
|
+
and every level: one line of 1 to 200 characters with no control
|
|
3047
|
+
characters (no `\p{Cc}`, U+2028 or U+2029). A local item's comes from its
|
|
3048
|
+
definition; a workspace item's from its file header, and for a trigger made
|
|
3049
|
+
`from:` a package template, the template's when the header has none. A
|
|
3050
|
+
header out of the rule counts as none (the item still runs) and leaves a
|
|
3051
|
+
snapshot problem at `<path>#/description`. Like `task`, it is untrusted
|
|
3052
|
+
text: render it as text. A kernel before 0.43.0 (feature
|
|
3053
|
+
`automation-descriptions`) sends `null` for every local trigger and refuses
|
|
3054
|
+
a trigger `description`; one before 0.40.0 sends `null` for every local
|
|
3055
|
+
schedule too.
|
|
3056
|
+
- `owner`, `runsOn`, `runsHere`, `reason` (`null` |
|
|
3036
3057
|
`host-unnamed` | `assigned-elsewhere` | `owner-mismatch` | `untrusted`),
|
|
3037
3058
|
`reasonDetail`, `enabledHere`.
|
|
3038
3059
|
- `untrusted` (0.30, [automations.trust](configuration.md#who-runs-workspace-automations)):
|
package/docs/desktop.md
CHANGED
|
@@ -211,6 +211,17 @@ whether the host scheduler is enabled. Pause, edit, run now and delete operate
|
|
|
211
211
|
on that workspace's saved jobs. Launching an agent is reported separately from
|
|
212
212
|
the end of its run; neither means its task succeeded.
|
|
213
213
|
|
|
214
|
+
Each schedule and trigger can carry a one-line **summary** (up to 200
|
|
215
|
+
characters), shown under its name in the list. Without one, the list shows the
|
|
216
|
+
first line of the task or wake message in italics, or "No summary" for a
|
|
217
|
+
command or operation. Set it in the schedule form's Summary field, or with
|
|
218
|
+
**Edit summary** on any local schedule or trigger (leave it empty to remove
|
|
219
|
+
it); a workspace item's summary is its file's `description:` in Git. A detail
|
|
220
|
+
page shows everything the item sends (the whole prompt or message, a command's
|
|
221
|
+
arguments as written, an operation and its home), its run state and where it
|
|
222
|
+
comes from. Writing summaries needs OATS 0.43 or later; an older OATS still
|
|
223
|
+
shows the ones it reports.
|
|
224
|
+
|
|
214
225
|
The Spawn dialog also has an optional **Recurring wake-up** setting. It binds
|
|
215
226
|
the schedule to the newly created home, preserving that agent's identity and
|
|
216
227
|
work. A wake starts that same home if it is stopped, then sends the saved
|
package/docs/packages.md
CHANGED
|
@@ -283,6 +283,12 @@ A package may also ship **trigger templates**: `triggers: [{ id,
|
|
|
283
283
|
file }]` in `oats-package.json`, each file `{ parameters, definition }`.
|
|
284
284
|
`oats trigger add --from <package>:<id> --set <name>=<value>` instantiates one
|
|
285
285
|
at the locked commit; see [schedules.md#triggers](schedules.md#triggers).
|
|
286
|
+
A template's `definition` may carry `description`, the one-line summary its
|
|
287
|
+
triggers show (validated like any trigger's: one line of 1 to 200 characters
|
|
288
|
+
without control characters). `add --from` copies it and `--description=<text>`
|
|
289
|
+
overrides it; a workspace trigger made `from:` it shows its file header's
|
|
290
|
+
description, else the template's. A kernel before 0.43.0 refuses the key, so
|
|
291
|
+
a package whose template sets it needs `compatibility.oats: ">=0.43.0"`.
|
|
286
292
|
|
|
287
293
|
## Compatibility floors
|
|
288
294
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# OATS 0.43.0
|
|
2
|
+
|
|
3
|
+
## Added
|
|
4
|
+
|
|
5
|
+
- **A one-line summary for every schedule and trigger** (feature
|
|
6
|
+
`automation-descriptions`). One rule covers every kind and level. A
|
|
7
|
+
`description` is one line of 1 to 200 characters with no control characters
|
|
8
|
+
(no `\p{Cc}`, U+2028 or U+2029). Before this, only local schedules could
|
|
9
|
+
carry one.
|
|
10
|
+
- **Local triggers take `description`.** Out of the rule it is refused as
|
|
11
|
+
`E_TRIGGER_INVALID {field: "description"}`. `oats trigger list` and `show`
|
|
12
|
+
used to send `null` for every local trigger; they now show it.
|
|
13
|
+
- **Package trigger templates may carry `definition.description`.** It is
|
|
14
|
+
validated like any trigger's. `oats trigger add --from` copies it, and
|
|
15
|
+
`--description=<text>` overrides it. A workspace trigger made `from:` a
|
|
16
|
+
template shows its file header's description, or the template's when the
|
|
17
|
+
header has none. A kernel before 0.43.0 refuses the key, so a package
|
|
18
|
+
whose template sets it needs `compatibility.oats: ">=0.43.0"`.
|
|
19
|
+
- **`--description=<text>` on `oats schedule add` and `oats trigger add`**
|
|
20
|
+
sets the description, or overrides the one in the spec. `--description=`
|
|
21
|
+
with an empty value leaves it out. With `--workspace`, the description goes
|
|
22
|
+
to the file header, never into the body.
|
|
23
|
+
- **Change only the description:**
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
oats schedule update <id> --description=<text> # without --file/--spec-json
|
|
27
|
+
oats trigger update <id> --description=<text>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`--description=` clears it. Both work on local schedules and triggers
|
|
31
|
+
only; a workspace one is changed in Git (`E_AUTOMATION_WORKSPACE`). A
|
|
32
|
+
schedule's description may change while the job runs or has an unresolved
|
|
33
|
+
attempt, and the update runs under the same locks as any other. A
|
|
34
|
+
trigger's fired and pending events are untouched. `oats trigger update`
|
|
35
|
+
takes no other flag for now (`E_BAD_ARGS`): a full trigger update is still
|
|
36
|
+
a remove and an add. `oats schedule … --server <id>` with `--description`
|
|
37
|
+
needs the destination to advertise `automation-descriptions`
|
|
38
|
+
(`E_REMOTE_INCOMPATIBLE` otherwise).
|
|
39
|
+
|
|
40
|
+
The Desktop gates its summary writes on the feature. See
|
|
41
|
+
[schedules](../schedules.md#kinds) and the
|
|
42
|
+
[Desktop CLI API](../desktop-cli-api.md#automations-shared-rows).
|
|
43
|
+
|
|
44
|
+
### Desktop
|
|
45
|
+
|
|
46
|
+
- **Schedules and Triggers show each item's summary.** A row shows the item's
|
|
47
|
+
name beside its origin tag (`local`, or the member), in place of
|
|
48
|
+
`local/<id>`, and its summary under it. Without a summary, the row shows the
|
|
49
|
+
first non-empty line of the task or wake message in muted italics, titled "No
|
|
50
|
+
summary set — first line of the prompt". A command or operation row without
|
|
51
|
+
one shows "No summary": the Desktop never makes a label from argv.
|
|
52
|
+
- **A detail page shows everything an item does**
|
|
53
|
+
([#545](https://github.com/awebai/oats/issues/545)). The title is the name,
|
|
54
|
+
with the qualified id under it, and then the summary or "No summary".
|
|
55
|
+
- **What it sends, in full:** a spawn's or trigger's task; a wake's whole
|
|
56
|
+
message and the home it wakes (before, a wake said "No prompt"); a
|
|
57
|
+
command's argv exactly as written, one argument per chip, and its working
|
|
58
|
+
directory; an operation and the home it runs in.
|
|
59
|
+
- **Spawns** also shows a spawn schedule's purpose (never shown before),
|
|
60
|
+
harness and model, launch configuration, permissions, session backend, and
|
|
61
|
+
the spawn's own recurring wake.
|
|
62
|
+
- **Run state** shows a schedule that is running and since when. It also
|
|
63
|
+
shows a run whose state is unknown and since when, with its exit facts,
|
|
64
|
+
whether it still holds a host slot, *Check run state* and the
|
|
65
|
+
`oats schedule reconcile <id> [--clear]` command to paste. A held job lock
|
|
66
|
+
no longer hides an unknown run. It also shows a wake waiting to be delivered.
|
|
67
|
+
- **An Invalid or Unreadable item** gets a card with its code, field and
|
|
68
|
+
message. Before, they appeared only in a tag's tooltip.
|
|
69
|
+
- **Comes from** adds the package template an item was made from (package,
|
|
70
|
+
version, commit) and a local item's created and updated times.
|
|
71
|
+
- **Set a summary from the Desktop** (needs a CLI with
|
|
72
|
+
`automation-descriptions`). The schedule form has an optional **Summary**
|
|
73
|
+
field. **Edit summary** sets or clears the summary of any local schedule
|
|
74
|
+
(command ones included) or local trigger, from the detail page or the row
|
|
75
|
+
menu; it runs `oats <kind> update <id> --description=<text>`. The text is
|
|
76
|
+
saved as typed, and an empty value removes it. A workspace item's summary is
|
|
77
|
+
its file's `description:` in Git. With an older CLI, the Desktop shows the
|
|
78
|
+
summaries it reports and the form keeps a stored summary unchanged.
|
|
79
|
+
|
|
80
|
+
## Changed
|
|
81
|
+
|
|
82
|
+
- **A workspace file header whose `description` breaks the rule is a warning,
|
|
83
|
+
not a refusal.** Before, a non-string header description stopped the entry
|
|
84
|
+
(`E_AUTOMATION_SCHEMA`), and any other value was accepted unchecked. Now the
|
|
85
|
+
entry still loads and runs, with `description: null`, and the snapshot
|
|
86
|
+
reports an `E_AUTOMATION_SCHEMA` problem at `<path>#/description`:
|
|
87
|
+
"description: one line of 1 to 200 characters without control characters —
|
|
88
|
+
shorten it or remove it; the automation keeps running without one". A label
|
|
89
|
+
never stops a job. `oats trigger|schedule add --workspace` refuses an
|
|
90
|
+
out-of-rule description before it writes the file.
|
|
91
|
+
- **The Desktop accepts OATS CLIs `>=0.25.8 <0.44.0`**, so it runs against
|
|
92
|
+
this release's kernel; the Desktop 0.42.x refuses a 0.43 CLI. Upgrade the
|
|
93
|
+
Desktop and the CLI together.
|
package/docs/schedules.md
CHANGED
|
@@ -52,14 +52,21 @@ both are evaluated by the croner library. Schedule IDs use lowercase letters,
|
|
|
52
52
|
digits and dashes, from 1 to 100 characters. Spawn schedules with a long ID
|
|
53
53
|
need an explicit shorter `purpose` to fit the instance-name limit below.
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
people reading `oats schedule list`, `
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
Every schedule and trigger, of any kind, may carry `description`: what it is
|
|
56
|
+
for, in words, for the people reading `oats schedule list`, `oats trigger
|
|
57
|
+
list`, `show` and the Desktop. It is one line of 1 to 200 characters with no
|
|
58
|
+
control characters (no CR, LF, TAB or any other C0 or C1 character, nor a
|
|
59
|
+
Unicode line or paragraph separator). A local schedule or trigger that breaks
|
|
60
|
+
the rule is refused, `E_SCHEDULE_INVALID` or `E_TRIGGER_INVALID` with `field:
|
|
61
|
+
"description"`; a workspace file's header that breaks it is only a warning
|
|
62
|
+
(see [the header](#workspace-triggers-and-schedules)). It is stored as given
|
|
63
|
+
and is informational only: it never reaches a run's argv, environment, task,
|
|
64
|
+
template or reconcile. A capability that registers jobs (knowledge harvest's
|
|
65
|
+
`run-source` jobs) sets it so that its command jobs can be told apart. Set it
|
|
66
|
+
in the definition or with `--description=<text>` on `add`; change only it with
|
|
67
|
+
`update <id> --description=<text>` (see [Commands](#commands)). Local triggers
|
|
68
|
+
take it from OATS 0.43.0 (feature `automation-descriptions`); an older kernel
|
|
69
|
+
refuses the key on a trigger.
|
|
63
70
|
|
|
64
71
|
- **spawn** `{…, agent, agentsRoot?, repo?, backend?, purpose?, task,
|
|
65
72
|
launchConfig?, harness?, model?, yolo?, wake?}` — every due minute launches
|
|
@@ -131,6 +138,7 @@ when the timer cannot reach it.
|
|
|
131
138
|
|
|
132
139
|
```json
|
|
133
140
|
{ "id": "okf-harvest-review", "enabled": true, "kind": "trigger",
|
|
141
|
+
"description": "Review every harvest PR on the knowledge base",
|
|
134
142
|
"on": { "source": "github.pull_request", "repo": "github.com/acme/knowledge",
|
|
135
143
|
"events": ["opened", "reopened", "ready_for_review"],
|
|
136
144
|
"labels": ["okf-harvest"], "base": "main", "poll": "2m" },
|
|
@@ -178,15 +186,19 @@ when the timer cannot reach it.
|
|
|
178
186
|
harness, and is recorded in `instance.json.trigger`.
|
|
179
187
|
|
|
180
188
|
```sh
|
|
181
|
-
oats trigger add --file trigger.json
|
|
182
|
-
oats trigger add --from oats.okf:harvest-review --set repo=github.com/acme/knowledge [--id <id>]
|
|
189
|
+
oats trigger add --file trigger.json [--description=<text>] # or:
|
|
190
|
+
oats trigger add --from oats.okf:harvest-review --set repo=github.com/acme/knowledge [--id <id>] [--description=<text>]
|
|
191
|
+
oats trigger update <id> --description=<text> # the description only; --description= clears it
|
|
183
192
|
oats trigger list | show <id> | enable <id> | disable <id> | remove <id>
|
|
184
193
|
oats trigger test <id> # dry run: gh credentials, repo permissions, the soul, what WOULD fire
|
|
185
194
|
oats trigger status [<id>] # last poll, next due, pending and fired events, live vs max, last error
|
|
186
195
|
```
|
|
187
196
|
|
|
188
197
|
All take `--dir` and `--json` (`triggerApi: 1`). `remove` leaves the instances
|
|
189
|
-
it spawned running. `
|
|
198
|
+
it spawned running. `update` changes only a local trigger's description (any
|
|
199
|
+
other flag is `E_BAD_ARGS`: a full trigger update is a remove and an add); it
|
|
200
|
+
sets `updatedAt` and leaves the trigger's fired and pending events as they
|
|
201
|
+
are. A workspace trigger is changed in Git (`E_AUTOMATION_WORKSPACE`). `oats schedule list` does not list triggers but counts
|
|
190
202
|
them (`triggers: { count, command: "oats trigger list" }`, and a line in text
|
|
191
203
|
mode). Errors: `E_TRIGGER_INVALID { field }`, `E_TRIGGER_EXISTS`,
|
|
192
204
|
`E_TRIGGER_UNKNOWN`, `E_TRIGGER_TEAMS`, `E_BAD_ARGS`.
|
|
@@ -196,8 +208,10 @@ in `oats-package.json`, each file `{ parameters: { <name>: { path, required?,
|
|
|
196
208
|
default?, description? } }, definition }`. `oats trigger add --from
|
|
197
209
|
<package>:<id>` reads it at the locked commit, and `--set <name>=<value>`
|
|
198
210
|
fills a parameter at its dotted `path` (a list value is comma-separated; a
|
|
199
|
-
missing required one is `E_BAD_ARGS { missing }`).
|
|
200
|
-
|
|
211
|
+
missing required one is `E_BAD_ARGS { missing }`). A template's `definition`
|
|
212
|
+
may carry `description` (validated like any trigger's): `add --from` copies
|
|
213
|
+
it, and `--description=<text>` overrides it (`--description=` leaves it out).
|
|
214
|
+
See [packages.md](packages.md#trigger-templates).
|
|
201
215
|
|
|
202
216
|
## Workspace triggers and schedules
|
|
203
217
|
|
|
@@ -246,6 +260,15 @@ owner: github.com/ana
|
|
|
246
260
|
`runsOn` and `owner`, and optionally `id`, `description` and `enabled`. A
|
|
247
261
|
candidate of the wrong kind (a schedule in `oats-triggers/`) or without one
|
|
248
262
|
is an `E_AUTOMATION_SCHEMA` problem, never silently skipped.
|
|
263
|
+
- **The description** follows the [one rule](#kinds), but a header that
|
|
264
|
+
breaks it does not stop the job: the entry still loads and runs as if it
|
|
265
|
+
had none (`description: null`), and the snapshot reports an `E_AUTOMATION_SCHEMA`
|
|
266
|
+
problem at `<path>#/description` ("description: one line of 1 to 200
|
|
267
|
+
characters without control characters — shorten it or remove it; the
|
|
268
|
+
automation keeps running without one"). A trigger made `from:` a package
|
|
269
|
+
template shows the header's description, else the template's. It lives in
|
|
270
|
+
the header only: `add --workspace` writes the flag's (else the spec's) there,
|
|
271
|
+
never into the body.
|
|
249
272
|
- **The id** is `id:`, else the filename stem. The same id twice in one member
|
|
250
273
|
for one kind is `E_AUTOMATION_DUPLICATE`, naming both paths; the second file
|
|
251
274
|
is not listed. Schedule IDs allow 1 to 100 lowercase letters, digits and dashes; trigger
|
|
@@ -312,8 +335,9 @@ check the placement and everything else on this host.
|
|
|
312
335
|
## Commands
|
|
313
336
|
|
|
314
337
|
```sh
|
|
315
|
-
oats schedule add <id> --file spec.json [--dir <deployment>] [--json]
|
|
316
|
-
oats schedule update <id> --file spec.json
|
|
338
|
+
oats schedule add <id> --file spec.json [--description=<text>] [--dir <deployment>] [--json]
|
|
339
|
+
oats schedule update <id> --file spec.json [--description=<text>]
|
|
340
|
+
oats schedule update <id> --description=<text> # the description only; --description= clears it
|
|
317
341
|
oats schedule list | show <id> | enable <id> | disable <id> | remove <id> [--force]
|
|
318
342
|
oats schedule run <id> [--force] # now, under the same lock and bound
|
|
319
343
|
oats schedule test <id> # dry run: where it runs, whether its soul resolves, when it is next due
|
|
@@ -326,6 +350,16 @@ oats schedule host status | uninstall
|
|
|
326
350
|
oats spawn <agent> ... --wake-every 15 --wake-message "Anything new?" # or --wake-file spec.json
|
|
327
351
|
```
|
|
328
352
|
|
|
353
|
+
`--description=<text>` sets the spec's `description`, or overrides it;
|
|
354
|
+
`--description=` (empty) removes it. Take the `=` form: it is the one that
|
|
355
|
+
carries an empty value, or one that starts with `-`. With `--file` or
|
|
356
|
+
`--spec-json`, `update` replaces the whole definition as before (one without a
|
|
357
|
+
description drops it). Without them, `update <id> --description=<text>`
|
|
358
|
+
changes only the description of a local schedule, under the same locks, and
|
|
359
|
+
is allowed while the job runs or has an unresolved attempt. With `--server`,
|
|
360
|
+
the destination must advertise `automation-descriptions`
|
|
361
|
+
(`E_REMOTE_INCOMPATIBLE` otherwise, before anything is forwarded).
|
|
362
|
+
|
|
329
363
|
`<id>` is `local/<id>` (or the bare id) or `<member>/<id>`. `host uninstall`
|
|
330
364
|
unregisters the deployment and removes the timer once none is registered.
|
|
331
365
|
Every `oats schedule` subcommand takes `--server <id>` instead of `--dir` to
|
package/lib/automations.mjs
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* `.git/` or `node_modules/`. One recursive tree listing per member commit serves both kinds.
|
|
20
20
|
* - THE HEADER: `kind: <fileKind>` + `schemaVersion: 1` (a wrong or missing kind is
|
|
21
21
|
* E_AUTOMATION_SCHEMA), `id:` or the filename stem, `runsOn` (a host name), `owner` (a GitHub
|
|
22
|
-
* account, `<host>/<login>`), `description
|
|
22
|
+
* account, `<host>/<login>`), `description?` (out of the one rule, validateDescription, it is a
|
|
23
|
+
* warning and the entry runs without one), `enabled?`. A duplicate id within one member and
|
|
23
24
|
* one kind is E_AUTOMATION_DUPLICATE.
|
|
24
25
|
* - PLACEMENT: a host runs one ONLY when `runsOn` is its `host.name` (oats-local.yaml), its
|
|
25
26
|
* authenticated `gh` account is `owner` AND its `automations.trust` admits it (0.30: both
|
|
@@ -59,6 +60,21 @@ const LOCAL = "local";
|
|
|
59
60
|
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
60
61
|
export function automationError(code, message, details) { return Object.assign(new Error(message), { code, ...(details ? { details } : {}) }); }
|
|
61
62
|
|
|
63
|
+
// ------------------------------------------------------------ descriptions
|
|
64
|
+
|
|
65
|
+
/** A trigger's or schedule's `description`: a one-line label for people (list, show, the
|
|
66
|
+
* Desktop), never read by a run. One rule for every kind and level (0.43.0): local schedules
|
|
67
|
+
* and triggers refuse what breaks it; a workspace file header that breaks it is a warning. */
|
|
68
|
+
export const DESCRIPTION_MAX = 200;
|
|
69
|
+
export const DESCRIPTION_RULE = `one line of 1 to ${DESCRIPTION_MAX} characters, without control characters`;
|
|
70
|
+
/** Throws `code` (the kind's own: E_SCHEDULE_INVALID, E_TRIGGER_INVALID) with field `description`
|
|
71
|
+
* when `description` is not one line of 1 to 200 characters (code points) free of \p{Cc}, U+2028
|
|
72
|
+
* and U+2029. */
|
|
73
|
+
export function validateDescription(description, code = "E_AUTOMATION_SCHEMA") {
|
|
74
|
+
if (typeof description !== "string" || !description.length || [...description].length > DESCRIPTION_MAX || /[\p{Cc}\u2028\u2029]/u.test(description)) throw Object.assign(new Error(`description: ${DESCRIPTION_RULE}`), { code, field: "description", details: { field: "description" } });
|
|
75
|
+
return description;
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
// ------------------------------------------------------------ names
|
|
63
79
|
|
|
64
80
|
/** `local/<id>` for a machine-private trigger or schedule, `<member>/<id>` for a workspace one. */
|
|
@@ -94,7 +110,9 @@ export function candidateOf(path, kinds) {
|
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
/** Parse one candidate file: the shared header here, the body by the kind's descriptor. The
|
|
97
|
-
* definition is not expanded yet. Never throws: → { entry } | { problem }
|
|
113
|
+
* definition is not expanded yet. Never throws: → { entry, warning? } | { problem }. A header
|
|
114
|
+
* `description` out of the rule is a `warning` (the same shape as a problem): the entry still
|
|
115
|
+
* loads and runs, with `description: null` — a label never stops a job. */
|
|
98
116
|
export function parseAutomationFile(desc, { stem, path, bytes, member, repoKey, commit }) {
|
|
99
117
|
const origin = { kind: "workspace", repoKey, path, commit };
|
|
100
118
|
const problem = (message, field) => ({ problem: { code: "E_AUTOMATION_SCHEMA", kind: desc.kind, repoKey, path: field ? `${path}#/${field}` : path, message } });
|
|
@@ -113,12 +131,16 @@ export function parseAutomationFile(desc, { stem, path, bytes, member, repoKey,
|
|
|
113
131
|
if (typeof doc.runsOn !== "string" || !HOST_NAME_RE.test(doc.runsOn)) return problem("runsOn: the host name that runs it (oats-local.yaml host.name: lowercase letters, digits and dashes)", "runsOn");
|
|
114
132
|
const owner = parseOwner(doc.owner);
|
|
115
133
|
if (!owner) return problem("owner: the GitHub account it acts as, <host>/<login> (e.g. github.com/acme-kb-bot)", "owner");
|
|
116
|
-
if (doc.description !== undefined && typeof doc.description !== "string") return problem("description: text", "description");
|
|
117
134
|
if (doc.enabled !== undefined && typeof doc.enabled !== "boolean") return problem("enabled: boolean", "enabled");
|
|
118
135
|
let source;
|
|
119
136
|
try { source = desc.parseBody(Object.fromEntries(Object.entries(doc).filter(([k]) => !HEADER_KEYS.includes(k)))); }
|
|
120
137
|
catch (e) { return problem(e.message, e.field); }
|
|
121
|
-
|
|
138
|
+
let description = null, warning;
|
|
139
|
+
if (doc.description !== undefined) {
|
|
140
|
+
try { description = validateDescription(doc.description); }
|
|
141
|
+
catch { warning = problem(`description: one line of 1 to ${DESCRIPTION_MAX} characters without control characters — shorten it or remove it; the automation keeps running without one`, "description").problem; }
|
|
142
|
+
}
|
|
143
|
+
return { entry: { kind: desc.kind, id: `${member}/${name}`, name, member, description, runsOn: doc.runsOn, owner: `${owner.host}/${owner.login}`, enabled: doc.enabled !== false, origin, source }, ...(warning ? { warning } : {}) };
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
/** Discover the workspace triggers and schedules of the confirmed members: one tree listing per
|
|
@@ -157,6 +179,7 @@ export async function discoverAutomations(discovery, { remote, memberName, kinds
|
|
|
157
179
|
catch (e) { problems.push({ code: e?.code || "E_REMOTE_UNREADABLE", kind: c.kind, repoKey: m.key, path, message: e.message }); continue; }
|
|
158
180
|
const parsed = parseAutomationFile(desc, { stem: c.stem, path, bytes, member: name, repoKey: m.key, commit: m.commit });
|
|
159
181
|
if (parsed.problem) { problems.push(parsed.problem); continue; }
|
|
182
|
+
if (parsed.warning) problems.push(parsed.warning);
|
|
160
183
|
const a = parsed.entry;
|
|
161
184
|
const first = seen[a.kind].get(a.name);
|
|
162
185
|
if (first) { problems.push({ code: "E_AUTOMATION_DUPLICATE", kind: a.kind, repoKey: m.key, path, message: `${a.kind} id ${JSON.stringify(a.name)} is declared by both ${first} and ${path}; ${path} is not listed` }); continue; }
|
|
@@ -387,9 +410,12 @@ export function memberCheckoutFor(dir, desc, id, { sameRepo }) {
|
|
|
387
410
|
/** The fields every trigger row and every schedule row share (docs/desktop-cli-api.md): identity,
|
|
388
411
|
* origin, who and where. Each kind's module adds its own (`kind`, the soul, the task, the event
|
|
389
412
|
* or the cron, the last and next run). */
|
|
413
|
+
const ruledDescription = (d) => { try { return d === undefined || d === null ? null : validateDescription(d); } catch { return null; } };
|
|
390
414
|
export function baseRow(a) {
|
|
391
415
|
return {
|
|
392
|
-
|
|
416
|
+
// The header's (a workspace file), else the definition's (a local one; a package template's),
|
|
417
|
+
// never a stored one that breaks the rule (a hand-edited, invalid definition).
|
|
418
|
+
id: a.id, name: a.name, origin: a.origin, description: a.description ?? ruledDescription(a.definition?.description),
|
|
393
419
|
owner: a.owner ?? null, runsOn: a.runsOn ?? null,
|
|
394
420
|
runsHere: a.placement.runsHere, reason: a.placement.reason, ...(a.placement.detail ? { reasonDetail: a.placement.detail } : {}),
|
|
395
421
|
enabledHere: a.placement.enabledHere,
|
package/lib/schedule.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { herdrSettingRemoved } from "./errors.mjs";
|
|
|
34
34
|
import { withDirLock as withSharedDirLock } from "./dir-lock.mjs";
|
|
35
35
|
import { tickTriggers } from "./triggers.mjs";
|
|
36
36
|
import { resolveMemberClone } from "./instance-resolution.mjs";
|
|
37
|
-
import { SCHEDULE_NAME_RE, readSnapshot, MODEL_RE, automationContext, automationError, baseRow, localEntry, localId, soulOriginOf, splitId } from "./automations.mjs";
|
|
37
|
+
import { SCHEDULE_NAME_RE, readSnapshot, MODEL_RE, automationContext, automationError, baseRow, localEntry, localId, soulOriginOf, splitId, validateDescription } from "./automations.mjs";
|
|
38
38
|
import { RESERVED_LAUNCH_ENV, MAX_INSTANCE_NAME, shq, findAgent, findInstanceHomes, inspectInstanceSession, inputInstanceSession, startInstanceSession, retirePendingMarkerPath } from "./core.mjs";
|
|
39
39
|
|
|
40
40
|
export const SCHEDULE_FILE = "oats-schedules.json";
|
|
@@ -323,11 +323,6 @@ export function validateCron(cron, tz, field = "cron") {
|
|
|
323
323
|
try { return new Cron(cron.trim(), { timezone: tz, paused: true }); }
|
|
324
324
|
catch (e) { throw scheduleError("E_SCHEDULE_INVALID", `${field}: ${e.message}`, { field }); }
|
|
325
325
|
}
|
|
326
|
-
/** A definition's `description`: a label for people (list, show, the Desktop), never read by a run. */
|
|
327
|
-
const DESCRIPTION_MAX = 200;
|
|
328
|
-
function validateDescription(description) {
|
|
329
|
-
if (typeof description !== "string" || !description.length || [...description].length > DESCRIPTION_MAX || /[\p{Cc}\u2028\u2029]/u.test(description)) throw scheduleError("E_SCHEDULE_INVALID", `description: one line of 1 to ${DESCRIPTION_MAX} characters, without control characters`, { field: "description" });
|
|
330
|
-
}
|
|
331
326
|
function validateMessage(message, field) {
|
|
332
327
|
if (typeof message !== "string" || !message.trim() || message.includes("\0") || Buffer.byteLength(message) > MESSAGE_MAX) throw scheduleError("E_SCHEDULE_INVALID", `${field}: non-empty text without NUL, at most 256 KiB`, { field });
|
|
333
328
|
}
|
|
@@ -351,7 +346,8 @@ export function validateDefinition(ws, def, { checkAgent = true, backendSource }
|
|
|
351
346
|
if (typeof enabled !== "boolean") throw scheduleError("E_SCHEDULE_INVALID", "enabled: boolean", { field: "enabled" });
|
|
352
347
|
validateCron(def.cron, def.tz);
|
|
353
348
|
const out = { id, enabled, cron: def.cron.trim(), tz: def.tz.trim(), kind: def.kind };
|
|
354
|
-
|
|
349
|
+
// A label for people (list, show, the Desktop), never read by a run (lib/automations.mjs).
|
|
350
|
+
if (def.description !== undefined) out.description = validateDescription(def.description, "E_SCHEDULE_INVALID");
|
|
355
351
|
if (def.kind === "spawn") {
|
|
356
352
|
if (typeof def.agent !== "string" || !def.agent.trim() || def.agent.trim().startsWith("-")) throw scheduleError("E_SCHEDULE_INVALID", "agent: soul name required (not an option)", { field: "agent" });
|
|
357
353
|
out.agent = def.agent.trim();
|
|
@@ -1190,7 +1186,7 @@ export function describe(ws, qid, io, { defs, st, now = new Date(), ctx = null }
|
|
|
1190
1186
|
const { workspace: _w, ...stored } = def; void _w;
|
|
1191
1187
|
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 } : {}) };
|
|
1192
1188
|
const herdr = entry ? null : storedHerdrBackend(ws, id, def);
|
|
1193
|
-
return scheduleRow(ws, entry ??
|
|
1189
|
+
return scheduleRow(ws, entry ?? localEntry("schedule", { ...def, id: name }, { dep: ws, ...(herdr ? { invalid: { code: herdr.code, message: herdr.message, field: herdr.field } } : {}) }), base, ctx);
|
|
1194
1190
|
}
|
|
1195
1191
|
/** A schedule's list row: its stored facts, the fields every trigger and schedule row share
|
|
1196
1192
|
* (lib/automations.mjs baseRow) and the schedule's own: `kind` is its run (spawn, command, wake,
|
|
@@ -1256,6 +1252,23 @@ export function updateSchedule(ws, qid, spec, io) {
|
|
|
1256
1252
|
return describe(ws, localId(id), io);
|
|
1257
1253
|
}), { retryMs: 3000 });
|
|
1258
1254
|
}
|
|
1255
|
+
/** Change only a local schedule's description (`oats schedule update <id> --description=<text>`),
|
|
1256
|
+
* under the same locks as an update. A label is not what a run is tracked by, so it may change
|
|
1257
|
+
* while the job runs or has an unresolved attempt; nothing else of the stored job is touched or
|
|
1258
|
+
* re-validated. `""` or null removes it. */
|
|
1259
|
+
export function updateScheduleDescription(ws, qid, description, io) {
|
|
1260
|
+
const id = localScheduleId(qid, "update");
|
|
1261
|
+
const clear = description === "" || description === null || description === undefined;
|
|
1262
|
+
if (!clear) validateDescription(description, "E_SCHEDULE_INVALID");
|
|
1263
|
+
return withHostLock(() => withScopeLock(ws, () => {
|
|
1264
|
+
const defs = readDefinitions(ws);
|
|
1265
|
+
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)" : ""}`);
|
|
1266
|
+
const { description: _d, ...rest } = defs.jobs[id]; void _d;
|
|
1267
|
+
defs.jobs[id] = { ...rest, ...(clear ? {} : { description }), updatedAt: new Date().toISOString() };
|
|
1268
|
+
writeDefinitions(ws, defs);
|
|
1269
|
+
return describe(ws, localId(id), io);
|
|
1270
|
+
}), { retryMs: 3000 });
|
|
1271
|
+
}
|
|
1259
1272
|
export function setEnabled(ws, qid, enabled, io) {
|
|
1260
1273
|
const id = localScheduleId(qid, "change");
|
|
1261
1274
|
return withScopeLock(ws, () => {
|
package/lib/servers.mjs
CHANGED
|
@@ -1103,6 +1103,11 @@ export function scheduleRemote(serverId, oatsArgs, io = {}) {
|
|
|
1103
1103
|
throw e;
|
|
1104
1104
|
}
|
|
1105
1105
|
if (setsHostCaps && !remote.features.includes("schedule-host-caps")) throw capsUnsupported();
|
|
1106
|
+
// --description on add/update (0.43.0): an older kernel would ignore it on add, or refuse a
|
|
1107
|
+
// description-only update as a missing --file; refused here before anything is forwarded.
|
|
1108
|
+
if (["add", "update"].includes(oatsArgs[0]) && oatsArgs.some((a) => a === "--description" || a.startsWith("--description=")) && !remote.features.includes("automation-descriptions")) {
|
|
1109
|
+
throw serverError("E_REMOTE_INCOMPATIBLE", `remote oats ${remote.version} at ${target.sshHost} (${serverId}) does not advertise automation-descriptions, so it cannot take --description; upgrade oats on that destination, or put description in the spec`);
|
|
1110
|
+
}
|
|
1106
1111
|
if (["add", "update"].includes(oatsArgs[0])) {
|
|
1107
1112
|
const indexes = oatsArgs.flatMap((arg, index) => arg === "--spec-json" ? [index] : []);
|
|
1108
1113
|
if (indexes.length > 1) throw serverError("E_BAD_ARGS", "remote schedule spec must be unambiguous");
|
package/lib/triggers.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { dirname, join } from "node:path";
|
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { readDefinitions, writeDefinitions, stateDir, withScopeLock, childEnv, parseEnvelopeText, schedulerStatus, readRegistry, definitionsPath } from "./schedule.mjs";
|
|
32
32
|
import { herdrSettingRemoved } from "./errors.mjs";
|
|
33
|
-
import { MODEL_RE, automationError, baseRow, localEntry, localId, parseOwner, qualifiedId, soulOriginOf } from "./automations.mjs";
|
|
33
|
+
import { MODEL_RE, automationError, baseRow, localEntry, localId, parseOwner, qualifiedId, soulOriginOf, validateDescription } from "./automations.mjs";
|
|
34
34
|
import { lockedPackageRef } from "./packages.mjs";
|
|
35
35
|
|
|
36
36
|
export const TRIGGER_API = 1;
|
|
@@ -91,11 +91,13 @@ function checkTemplate(text, field) {
|
|
|
91
91
|
* spawn.backend, for the refusal of `herdr` (Herdr was removed in 0.31.0). */
|
|
92
92
|
export function validateTrigger(def, { backendSource = "the trigger's spawn.backend" } = {}) {
|
|
93
93
|
if (!isObject(def)) throw invalid("definition", "must be an object");
|
|
94
|
-
onlyKeys(def, ["id", "enabled", "kind", "on", "spawn", "concurrency", "template", "createdAt", "updatedAt"], "");
|
|
94
|
+
onlyKeys(def, ["id", "enabled", "kind", "description", "on", "spawn", "concurrency", "template", "createdAt", "updatedAt"], "");
|
|
95
95
|
if (typeof def.id !== "string" || !ID_RE.test(def.id)) throw invalid("id", "lowercase letters, digits and dashes, 1 to 40 characters");
|
|
96
96
|
if (def.kind !== "trigger") throw invalid("kind", "must be \"trigger\"");
|
|
97
97
|
const enabled = def.enabled === undefined ? true : def.enabled;
|
|
98
98
|
if (typeof enabled !== "boolean") throw invalid("enabled", "boolean");
|
|
99
|
+
// A label for people (list, show, the Desktop), never templated or read by a poll or a spawn.
|
|
100
|
+
if (def.description !== undefined) validateDescription(def.description, "E_TRIGGER_INVALID");
|
|
99
101
|
const on = def.on;
|
|
100
102
|
if (!isObject(on)) throw invalid("on", "{ source, repo, events, labels?, base?, poll? }");
|
|
101
103
|
onlyKeys(on, ["source", "repo", "events", "labels", "base", "poll"], "on.");
|
|
@@ -133,7 +135,7 @@ export function validateTrigger(def, { backendSource = "the trigger's spawn.back
|
|
|
133
135
|
for (const [k, v] of [["max", max], ["perKey", perKey]]) if (!Number.isInteger(v) || v < 1 || v > 100) throw invalid(`concurrency.${k}`, "a whole number from 1 to 100");
|
|
134
136
|
if (def.template !== undefined && !isObject(def.template)) throw invalid("template", "the package template this trigger was made from");
|
|
135
137
|
return {
|
|
136
|
-
id: def.id, enabled, kind: "trigger",
|
|
138
|
+
id: def.id, enabled, kind: "trigger", ...(def.description !== undefined ? { description: def.description } : {}),
|
|
137
139
|
on: { source: on.source, repo: repo.key, events: [...on.events], labels: [...labels], ...(on.base !== undefined ? { base: on.base.trim() } : {}), poll },
|
|
138
140
|
spawn: { soul: sp.soul, purpose, task: sp.task, teams: [...teams], ...(sp.launchConfig ? { launchConfig: sp.launchConfig } : {}), ...(sp.harness ? { harness: sp.harness } : {}), ...(sp.model ? { model: sp.model } : {}), ...(sp.yolo !== undefined ? { yolo: sp.yolo } : {}), ...(sp.backend ? { backend: sp.backend } : {}) },
|
|
139
141
|
concurrency: { max, perKey },
|
|
@@ -601,6 +603,23 @@ export function setTriggerEnabled(ws, id, enabled, ctx = null) {
|
|
|
601
603
|
return describeTrigger(ws, e.id, ctx);
|
|
602
604
|
});
|
|
603
605
|
}
|
|
606
|
+
/** Change only a LOCAL trigger's description (`oats trigger update <id> --description=<text>`):
|
|
607
|
+
* `""` or null removes it. Its fired and pending state is untouched; a workspace trigger is
|
|
608
|
+
* changed in Git. */
|
|
609
|
+
export function updateTriggerDescription(ws, id, description, ctx = null) {
|
|
610
|
+
const e = requireTrigger(ws, id, ctx);
|
|
611
|
+
refuseWorkspace(e, "change");
|
|
612
|
+
const clear = description === "" || description === null || description === undefined;
|
|
613
|
+
if (!clear) validateDescription(description, "E_TRIGGER_INVALID");
|
|
614
|
+
return withScopeLock(ws, () => {
|
|
615
|
+
const defs = readDefinitions(ws);
|
|
616
|
+
if (defs.jobs[e.name]?.kind !== "trigger") throw triggerError("E_TRIGGER_UNKNOWN", `no trigger ${JSON.stringify(id)} in ${ws}`, { details: { id: e.id } });
|
|
617
|
+
const { description: _d, ...rest } = defs.jobs[e.name]; void _d;
|
|
618
|
+
defs.jobs[e.name] = { ...rest, ...(clear ? {} : { description }), updatedAt: new Date().toISOString() };
|
|
619
|
+
writeDefinitions(ws, defs);
|
|
620
|
+
return describeTrigger(ws, e.id, ctx);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
604
623
|
/** Remove the definition and its state. Live instances it spawned are untouched. */
|
|
605
624
|
export function removeTrigger(ws, id, ctx = null) {
|
|
606
625
|
const e = requireTrigger(ws, id, ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|