@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 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"), description = flag("description");
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: typeof description === "string" ? description : undefined, runsOn, owner, body });
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
- const { id: _i, enabled: _e, kind, ...rest } = spec; void _i; void _e;
2611
- const r = await addWorkspaceAutomation(scheduleKind({ dep: ws() }), { id: wid, body: { run: kind ?? "spawn", ...rest } });
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": return out({ schedule: updateSchedule(ws(), needId(), readSpec(), io) });
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>|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]");
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
- 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, ...rest }) => rest)(spec);
2747
- const r = await addWorkspaceAutomation(kinds.trigger, { id: wid, body });
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
@@ -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`** (0.40.0): the shared row field is the local definition's
2911
- `description` when it has one, else `null` (a workspace schedule's comes
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`, `owner`, `runsOn`, `runsHere`, `reason` (`null` |
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
- Any kind may carry `description`: what the job is for, in words, for the
56
- people reading `oats schedule list`, `show` and the Desktop. It is one line of
57
- 1 to 200 characters with no control characters (no CR, LF, TAB or any other
58
- C0 or C1 character, nor a Unicode line or paragraph separator); anything else
59
- is `E_SCHEDULE_INVALID` with `field: "description"`. It is stored as given and
60
- is informational only: it never reaches a run's argv, environment, task or
61
- reconcile. A capability that registers jobs (knowledge harvest's `run-source`
62
- jobs) sets it so that its command jobs can be told apart.
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 # or:
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. `oats schedule list` does not list triggers but counts
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 }`). See
200
- [packages.md](packages.md#trigger-templates).
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
@@ -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?`, `enabled?`. A duplicate id within one member and
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
- return { entry: { kind: desc.kind, id: `${member}/${name}`, name, member, description: doc.description ?? null, runsOn: doc.runsOn, owner: `${owner.host}/${owner.login}`, enabled: doc.enabled !== false, origin, source } };
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
- id: a.id, name: a.name, origin: a.origin, description: a.description ?? null,
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
- if (def.description !== undefined) { validateDescription(def.description); out.description = def.description; }
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 ?? { ...localEntry("schedule", { ...def, id: name }, { dep: ws, ...(herdr ? { invalid: { code: herdr.code, message: herdr.message, field: herdr.field } } : {}) }), description: def.description ?? null }, base, ctx);
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.42.1",
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",