@awebai/oats 0.42.0 → 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 +66 -18
- package/docs/desktop-cli-api.md +109 -23
- package/docs/desktop.md +17 -2
- package/docs/packages.md +6 -0
- package/docs/release-notes/v0.42.1.md +110 -0
- package/docs/release-notes/v0.43.0.md +93 -0
- package/docs/schedules.md +49 -15
- package/docs/servers.md +4 -1
- package/docs/souls-and-instances.md +126 -6
- package/injects/instance-boundary.md +4 -3
- package/injects/work-checkout.md +25 -3
- package/injects/work-worktree.md +30 -7
- package/lib/automations.mjs +31 -5
- package/lib/core.mjs +181 -18
- package/lib/instance-git.mjs +22 -0
- package/lib/instance-lifecycle.mjs +17 -2
- package/lib/retire-output.mjs +14 -1
- package/lib/schedule.mjs +21 -8
- package/lib/servers.mjs +10 -2
- package/lib/triggers.mjs +22 -3
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -47,13 +47,13 @@ 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
|
|
|
54
54
|
import { observeInstanceGit, diffInstanceFile } from "../lib/instance-git.mjs";
|
|
55
55
|
import { planStop, applyStop, planRetire, resolveInstance as resolveInstanceForCli } from "../lib/instance-lifecycle.mjs";
|
|
56
|
-
import { formatBytes, workRecoveryLines } from "../lib/retire-output.mjs";
|
|
56
|
+
import { extraWorktreeLines, formatBytes, workRecoveryLines } from "../lib/retire-output.mjs";
|
|
57
57
|
const await_import_lifecycle = () => ({ resolveInstance: resolveInstanceForCli });
|
|
58
58
|
import { homeTarget, soulTarget, isWorkspaceContext, inspectDocument, readinessDocument, policyOf, policySoul, manifestMissingRequires, INSPECT_OPERATIONS_API } from "../lib/instance-inspect.mjs";
|
|
59
59
|
import { readEvents, setWaiting, incarnationOf } from "../lib/instance-events.mjs";
|
|
@@ -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);
|
|
@@ -2498,7 +2517,7 @@ function retireCmd() {
|
|
|
2498
2517
|
const planRev = flag("plan-revision"), idemKey = flag("idempotency-key");
|
|
2499
2518
|
if (planRev === true || idemKey === true) die("--plan-revision and --idempotency-key need values");
|
|
2500
2519
|
if ((planRev !== undefined) !== (idemKey !== undefined)) die("--plan-revision and --idempotency-key go together");
|
|
2501
|
-
let replayPath = null, childrenStopped = null;
|
|
2520
|
+
let replayPath = null, childrenStopped = null, plannedExtraWorktrees;
|
|
2502
2521
|
if (planRev !== undefined) {
|
|
2503
2522
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(idemKey)) die("--idempotency-key: 1-128 chars of [A-Za-z0-9._:-]");
|
|
2504
2523
|
// Replay first: after a successful retire the home is gone, so the receipt
|
|
@@ -2508,6 +2527,9 @@ function retireCmd() {
|
|
|
2508
2527
|
let fresh;
|
|
2509
2528
|
try { fresh = planRetire(dirFlag(), root, name, { home: homeFlag }); } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message, e.details) : die(e.message); }
|
|
2510
2529
|
replayPath = join(dirname(fresh.home), `.oats-retire-receipt.${idemKey}.json`);
|
|
2530
|
+
// The extra trees the confirmed plan names, as it names them: the retire refuses as stale rather than
|
|
2531
|
+
// move or remove one that no longer reads that way when it gets to them.
|
|
2532
|
+
plannedExtraWorktrees = fresh.facts.extraWorktrees;
|
|
2511
2533
|
if (fresh.planRevision !== planRev) return args.includes("--json") ? jsonFail("E_PLAN_STALE", `the retire plan changed since it was shown (${planRev} → ${fresh.planRevision}); review the fresh plan`, { plan: fresh }) : die(`the retire plan changed since it was shown; re-run oats retire ${name} --plan`);
|
|
2512
2534
|
// The plan promised: recorded children are STOPPED first (bounded, never
|
|
2513
2535
|
// escalated) and retained. A child still running after the grace refuses
|
|
@@ -2521,7 +2543,7 @@ function retireCmd() {
|
|
|
2521
2543
|
if (running.length) return args.includes("--json") ? jsonFail("E_CHILDREN_RUNNING", `${running.map((k) => k.instance).join(", ")} ${running.length === 1 ? "is" : "are"} still running after a bounded stop; nothing was retired and nothing was escalated`, { childrenStopped, plan: fresh }) : die(`children still running: ${running.map((k) => k.instance).join(", ")}; nothing retired`);
|
|
2522
2544
|
}
|
|
2523
2545
|
let r;
|
|
2524
|
-
try { r = retireInstance(root, name, { home: homeFlag, self: isSelf, discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force") }); }
|
|
2546
|
+
try { r = retireInstance(root, name, { home: homeFlag, self: isSelf, discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force"), ...(plannedExtraWorktrees ? { plannedExtraWorktrees } : {}) }); }
|
|
2525
2547
|
catch (e) { if (!e?.code) throw e; return args.includes("--json") ? jsonFail(e.code, e.message, e.candidates ? { ...e.details, candidates: e.candidates } : e.details) : die(e.message); }
|
|
2526
2548
|
if (childrenStopped) r.childrenStopped = childrenStopped;
|
|
2527
2549
|
if (replayPath) { r.planRevision = planRev; r.idempotencyKey = idemKey; r.replayed = false; try { writeFileAtomic(replayPath, JSON.stringify(r, null, 2)); } catch { /* receipt is evidence, not authority */ } }
|
|
@@ -2563,6 +2585,7 @@ function retireCmd() {
|
|
|
2563
2585
|
// Preserving work and not saying so leaves the operator believing it is gone,
|
|
2564
2586
|
// which is most of the harm of deleting it. Name the classes and the path.
|
|
2565
2587
|
for (const line of workRecoveryLines(r)) console.log(line);
|
|
2588
|
+
for (const line of extraWorktreeLines(r)) console.log(line);
|
|
2566
2589
|
for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
|
|
2567
2590
|
if (isSelf) console.log("This window dies in ~8s — say any goodbyes now.");
|
|
2568
2591
|
}
|
|
@@ -2599,19 +2622,25 @@ async function scheduleCmd() {
|
|
|
2599
2622
|
case "show": return out({ schedule: describeSchedule(ws(), needId(), io, { ctx: ctx() }) });
|
|
2600
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)`);
|
|
2601
2624
|
case "add": {
|
|
2602
|
-
const spec = readSpec();
|
|
2625
|
+
const spec = withDescription(readSpec(), descriptionFlag());
|
|
2603
2626
|
if (flag("workspace") !== undefined) {
|
|
2604
2627
|
const wid = id ?? spec.id;
|
|
2605
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)");
|
|
2606
|
-
|
|
2607
|
-
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 });
|
|
2608
2632
|
return out(r, printWorkspaceAdd);
|
|
2609
2633
|
}
|
|
2610
2634
|
if (id && spec.id === undefined) spec.id = id;
|
|
2611
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" });
|
|
2612
2636
|
return out({ schedule: addSchedule(ws(), spec, io) });
|
|
2613
2637
|
}
|
|
2614
|
-
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
|
+
}
|
|
2615
2644
|
case "enable":
|
|
2616
2645
|
case "disable": {
|
|
2617
2646
|
const on = sub === "enable";
|
|
@@ -2654,7 +2683,7 @@ async function scheduleCmd() {
|
|
|
2654
2683
|
if (op === "status") return out({ scheduler: schedulerStatus(ws(), io) });
|
|
2655
2684
|
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
2656
2685
|
}
|
|
2657
|
-
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]");
|
|
2658
2687
|
}
|
|
2659
2688
|
} catch (e) {
|
|
2660
2689
|
// K8b: typed refusal details travel (identity mismatch: key/declared; a refused file: its integrity source).
|
|
@@ -2673,7 +2702,7 @@ async function triggerCmd() {
|
|
|
2673
2702
|
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
2674
2703
|
const out = (result, text) => { if (JSON_MODE) jsonOk(result); else console.log(text ? text(result) : JSON.stringify(result, null, 2)); };
|
|
2675
2704
|
const needId = () => { if (!id) throw T.triggerError("E_BAD_ARGS", `oats trigger ${sub} <id>`); return id; };
|
|
2676
|
-
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>)";
|
|
2677
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");
|
|
2678
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}` : ""}`;
|
|
2679
2708
|
// The workspace automations of this deployment (the snapshot) placed on this host.
|
|
@@ -2693,6 +2722,15 @@ async function triggerCmd() {
|
|
|
2693
2722
|
actx = undefined;
|
|
2694
2723
|
return out({ trigger: T.describeTrigger(ws(), id, ctx()) }, (r) => line(r.trigger));
|
|
2695
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
|
+
}
|
|
2696
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(", ")})` : ""}`);
|
|
2697
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)");
|
|
2698
2736
|
case "test": {
|
|
@@ -2727,6 +2765,7 @@ async function triggerCmd() {
|
|
|
2727
2765
|
}
|
|
2728
2766
|
const idFlag = flag("id");
|
|
2729
2767
|
if (idFlag === true) throw T.triggerError("E_BAD_ARGS", "--id needs a trigger id");
|
|
2768
|
+
const description = descriptionFlag();
|
|
2730
2769
|
let spec;
|
|
2731
2770
|
if (file !== undefined) {
|
|
2732
2771
|
if (file === true || !existsSync(file)) throw T.triggerError("E_BAD_ARGS", `--file ${file === true ? "needs a path" : `not found: ${file}`}`);
|
|
@@ -2739,12 +2778,14 @@ async function triggerCmd() {
|
|
|
2739
2778
|
const kinds = await automationKinds(ws(), readLock(ws()), remoteOptionsFromEnv());
|
|
2740
2779
|
const wid = typeof idFlag === "string" ? idFlag : from !== undefined ? String(from).split(":")[1] : spec?.id;
|
|
2741
2780
|
if (typeof wid !== "string") throw T.triggerError("E_BAD_ARGS", "--workspace: name the trigger with --id (or an id in the file)");
|
|
2742
|
-
|
|
2743
|
-
|
|
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 });
|
|
2744
2785
|
return out(r, printWorkspaceAdd);
|
|
2745
2786
|
}
|
|
2746
2787
|
if (spec === undefined) spec = await triggerFromPackage(T, String(from), sets, idFlag);
|
|
2747
|
-
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)`);
|
|
2748
2789
|
}
|
|
2749
2790
|
default: throw T.triggerError("E_BAD_ARGS", usage);
|
|
2750
2791
|
}
|
|
@@ -3267,7 +3308,7 @@ function versionCmd() {
|
|
|
3267
3308
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
3268
3309
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
3269
3310
|
// never listed.
|
|
3270
|
-
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 }));
|
|
3271
3312
|
return;
|
|
3272
3313
|
}
|
|
3273
3314
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3571,6 +3612,8 @@ async function serverRouteCmd() {
|
|
|
3571
3612
|
rest.push("--spec-json", readFileSync(f, "utf8"));
|
|
3572
3613
|
continue;
|
|
3573
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; }
|
|
3574
3617
|
rest.push(a);
|
|
3575
3618
|
}
|
|
3576
3619
|
let out;
|
|
@@ -3714,6 +3757,7 @@ async function serverRouteCmd() {
|
|
|
3714
3757
|
}
|
|
3715
3758
|
console.log(`Retired ${r.retired} on ${id}${r.deferred ? " (deferred completion scheduled there)" : ""}${r.rollbackIncomplete ? " — cleanup INCOMPLETE on the server, home retained there" : ""}`);
|
|
3716
3759
|
for (const line of workRecoveryLines(r, { host: target.sshHost })) console.log(line);
|
|
3760
|
+
for (const line of extraWorktreeLines(r, { host: target.sshHost })) console.log(line);
|
|
3717
3761
|
if (r.rollbackIncomplete) {
|
|
3718
3762
|
for (const f of r.rollbackIncomplete) console.error(` ${f}`);
|
|
3719
3763
|
const branch = failedSpawnBranchLine(r, r.rollbackIncomplete, { host: target.sshHost });
|
|
@@ -3942,10 +3986,14 @@ Usage:
|
|
|
3942
3986
|
host install|uninstall|status explicit IANA tz; see docs/schedules.md); --server
|
|
3943
3987
|
routes to that host's workspace
|
|
3944
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)
|
|
3945
3991
|
oats trigger add (--file <json> | --from <package>:<template> [--set k=v]) | list | show | enable
|
|
3946
3992
|
| disable | remove <id> | test <id> | status [<id>] event-driven spawns (github.pull_request
|
|
3947
3993
|
polled with the host's gh by the schedule tick;
|
|
3948
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)
|
|
3949
3997
|
oats trigger|schedule add … --workspace <member> --runs-on <host> --owner <host>/<login>
|
|
3950
3998
|
a trigger/schedule declared in Git (<member>/<id>):
|
|
3951
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
|
|
@@ -1923,7 +1924,11 @@ route target:
|
|
|
1923
1924
|
"running":true,"identity":{"alias":"dev-a","address":"acme/dev-a"},"identityAddress":"acme/dev-a",
|
|
1924
1925
|
"teams":[{"label":"default","team":"acme:team"}],"startedAt":"2026-09-29T10:00:00.000Z","createdAt":"2026-09-29T09:58:12.004Z",
|
|
1925
1926
|
"model":"opus","runtimeState":null,"parentInstance":"lead","siblingInstance":null,"relation":"child","relativeTo":"lead",
|
|
1926
|
-
"spawnOrigin":"instance","
|
|
1927
|
+
"spawnOrigin":"instance","work":"worktree","repo":"/srv/team/ws","branch":"agents/dev-a","modelFrom":"soul",
|
|
1928
|
+
"soul":{"repoKey":"github.com/acme/team","commit":"66566512…","current":"9c1e04ab…","status":"moved"},
|
|
1929
|
+
"modules":[{"name":"oats.okf","from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"},
|
|
1930
|
+
"commit":"ab897841…","current":{"commit":"ab897841…","version":"2.1.3"},"status":"current"}],
|
|
1931
|
+
"retirePending":false,"rollbackIncomplete":false,
|
|
1927
1932
|
"savedRoute":false,"addressable":true,"missingRemotely":false}],
|
|
1928
1933
|
"retireFailures":[]}
|
|
1929
1934
|
```
|
|
@@ -1940,9 +1945,16 @@ route target:
|
|
|
1940
1945
|
- **Instance rows** relay the host's own `status --json` row: `identity`,
|
|
1941
1946
|
`identityAddress`, `teams`, `startedAt`, `createdAt`, `model`,
|
|
1942
1947
|
`runtimeState`, `parentInstance`, `siblingInstance`, `relation`,
|
|
1943
|
-
`relativeTo` and `spawnOrigin
|
|
1944
|
-
|
|
1945
|
-
|
|
1948
|
+
`relativeTo` and `spawnOrigin`, and (relayed from 0.42.1) `work`, `repo`,
|
|
1949
|
+
`branch`, `modelFrom`, `soul` and `modules`, are always present, `null`
|
|
1950
|
+
when the host does not supply them (an older host, a fact it never
|
|
1951
|
+
recorded, or a saved route the host no longer lists). Each is the
|
|
1952
|
+
[local row's](#the-roster-oats-status---json) fact of the same name,
|
|
1953
|
+
relayed as the host answered it. Nothing is derived on this side: `repo`
|
|
1954
|
+
is a path on the host, never read here, and `soul` and `modules` are the
|
|
1955
|
+
host's own drift observation (against its own members and lock), not
|
|
1956
|
+
recomputed: `modules` is the drift rows, or the recorded map when the host
|
|
1957
|
+
could not read its workspace, as on a local row.
|
|
1946
1958
|
- **`waitingOnYou`** (0.40.2, [Waiting on you](#waiting-on-you)) is on a row
|
|
1947
1959
|
only when the host's kernel reports it: a row from a host before 0.40.0,
|
|
1948
1960
|
and a saved route the host did not list, have no such key. Absent means
|
|
@@ -2219,6 +2231,15 @@ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--
|
|
|
2219
2231
|
`--delete-branch` (since 0.41.0: `this self-retire was requested with
|
|
2220
2232
|
--delete-branch by an older OATS; retirement no longer deletes branches, so
|
|
2221
2233
|
the branch and the worktree were left`).
|
|
2234
|
+
- `worktree-retained` (`data: {movedTo, branch, recordedBranch}`) and
|
|
2235
|
+
`worktree-removed` (`data: {branch}`) are written to the workspace log only,
|
|
2236
|
+
by the retire's worktree step. Since 0.42.1 the retire also writes one per
|
|
2237
|
+
extra tree it handled
|
|
2238
|
+
([extra trees at retire](souls-and-instances.md#extra-trees-at-retire)),
|
|
2239
|
+
with `extra: true` and the tree's absolute `path` added:
|
|
2240
|
+
`worktree-retained` `{movedTo, branch, recordedBranch: null, extra: true,
|
|
2241
|
+
path}` and `worktree-removed` `{branch, extra: true, path}`. A row without
|
|
2242
|
+
`extra` is about `work/`.
|
|
2222
2243
|
- **Incarnation.** Each row carries the writing home's `createdAt` (or
|
|
2223
2244
|
`null` for old rows); the top-level `incarnation` is the current home's (or
|
|
2224
2245
|
`null`). Earlier incarnations are returned as this address's history.
|
|
@@ -2438,7 +2459,9 @@ oats retire <instance> --plan [--home <abs>] [--dir <d>] --json
|
|
|
2438
2459
|
"upstream":{"ref":null,"ahead":null,"behind":null},"base":{"ref":null,"ahead":null,"behind":null},"remote":null},
|
|
2439
2460
|
"workMode":"worktree","repo":"/w/one","recordedBranch":"agents/dev-1",
|
|
2440
2461
|
"children":[{"instance":"dev-1-child","agent":"dev","home":"/w/agents/dev/instances/dev-1-child","session":{"state":"shell","present":true,"backend":"tmux","established":true}}],
|
|
2441
|
-
"ambiguous":[],"pullRequest":"unknown"
|
|
2462
|
+
"ambiguous":[],"pullRequest":"unknown",
|
|
2463
|
+
"extraWorktrees":[{"path":"/w/agents/dev/instances/dev-1/.work-docs","repo":"/w/docs","branch":"agents/dev-1-docs","detachedAt":null,
|
|
2464
|
+
"disposition":"retain","movedTo":"/w/.agents/worktrees/docs/agents-dev-1-docs","reason":"…"}]},
|
|
2442
2465
|
"defaults":{"retainWorktree":true,"deleteBranch":false,"stopChildren":true,"retainChildren":true},
|
|
2443
2466
|
"planRevision":"4e5f6a7b8c9d0e1f2a3b4c5d","notes":["the worktree is on feat/x, not the recorded agents/dev-1; …"]}
|
|
2444
2467
|
```
|
|
@@ -2476,10 +2499,35 @@ oats retire <instance> --plan [--home <abs>] [--dir <d>] --json
|
|
|
2476
2499
|
there when it is not empty` in directory mode, and absent in checkout,
|
|
2477
2500
|
attached and workspace modes. They are strings in `notes`: no other key
|
|
2478
2501
|
changes, and `planRevision` is unaffected.
|
|
2502
|
+
- `facts.extraWorktrees` (0.42.1) lists the instance's
|
|
2503
|
+
[extra trees](souls-and-instances.md#extra-trees-at-retire): linked
|
|
2504
|
+
worktrees at `<home>/.work-*` that Git confirms. It is an array, empty when
|
|
2505
|
+
there are none, in every work mode. Each row is `{path, repo, branch,
|
|
2506
|
+
detachedAt, disposition, movedTo, reason}`:
|
|
2507
|
+
- `path`: the tree's absolute path in the home.
|
|
2508
|
+
- `repo`: its repository, the first entry of that repository's `git
|
|
2509
|
+
worktree list` (the main worktree, or the bare repository).
|
|
2510
|
+
- `branch`: the branch its HEAD is on, or `null`. `detachedAt`: the commit
|
|
2511
|
+
when HEAD is detached, else `null`.
|
|
2512
|
+
- `disposition`: `"remove"` (the tree is clean: it would be removed, its
|
|
2513
|
+
branch kept), `"retain"` (it would be moved to `movedTo`, as `work/` is
|
|
2514
|
+
retained) or `"refuse"` (the retire would refuse with
|
|
2515
|
+
`E_WORK_PRESERVATION_FAILED` and keep the home).
|
|
2516
|
+
- `movedTo`: the target for `"retain"`, else `null`.
|
|
2517
|
+
- `reason`: `null` for `"remove"`, why the tree is not clean for
|
|
2518
|
+
`"retain"`, why it is refused for `"refuse"`.
|
|
2519
|
+
|
|
2520
|
+
`notes` also carries one string per tree that says the same. The trees are
|
|
2521
|
+
part of `planRevision`: a tree created, removed, dirtied or cleaned between
|
|
2522
|
+
the plan and the apply, or a change of its disposition or target (another
|
|
2523
|
+
directory taking the `<leaf>-N` it would move to, for example), refuses a
|
|
2524
|
+
guarded apply with `E_PLAN_STALE` before anything runs. A reader that does
|
|
2525
|
+
not know the key can ignore it; the `notes` strings say the same.
|
|
2479
2526
|
|
|
2480
2527
|
Plain `retire` keeps a worktree-mode instance's work: the worktree is moved
|
|
2481
2528
|
(`git worktree move`) to `<deployment>/.agents/worktrees/<repo>/<branch>` (a
|
|
2482
|
-
`-2` suffix if taken; `detached-<oid12>` when detached), state intact.
|
|
2529
|
+
`-2` suffix if taken; `detached-<oid12>` when detached), state intact. An
|
|
2530
|
+
extra tree that is not clean is moved the same way; a clean one is removed.
|
|
2483
2531
|
|
|
2484
2532
|
```text
|
|
2485
2533
|
oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--home <abs>] --json
|
|
@@ -2535,6 +2583,21 @@ A first retire prints the **raw receipt**, not an envelope:
|
|
|
2535
2583
|
retire (hooks run, a recovery copied), and neither says that one happened:
|
|
2536
2584
|
an error here is not proof that nothing happened, nor that a recovery
|
|
2537
2585
|
exists.
|
|
2586
|
+
- `extraWorktrees` (0.42.1): the extra trees the retire handled, present only
|
|
2587
|
+
when it handled at least one. Each row is the plan's row (`{path, repo,
|
|
2588
|
+
branch, detachedAt, disposition, movedTo, reason}`) plus `outcome`:
|
|
2589
|
+
`"removed"` or `"retained"`, with `movedTo` where a retained tree went. The
|
|
2590
|
+
step runs only when the home is removed (not with `--keep-dir`, not when the
|
|
2591
|
+
home is kept for a retry), after the hooks and before the worktree step of
|
|
2592
|
+
`work/`. `--discard-worktree` does not apply to it. A locked tree
|
|
2593
|
+
(`"refuse"` in the plan) stops the retire with `E_WORK_PRESERVATION_FAILED`
|
|
2594
|
+
naming the tree before anything runs (no session stop, no retire hook);
|
|
2595
|
+
a lock that appears during the hooks, or a move or removal Git refuses,
|
|
2596
|
+
stops it at the step, after the hooks. `--force` does not bypass either;
|
|
2597
|
+
the home and `work/` are kept, and trees already handled stay handled. A tree
|
|
2598
|
+
that no longer matches what the applied plan said refuses with
|
|
2599
|
+
`E_PLAN_STALE`, the home kept, rather than be moved or removed unplanned.
|
|
2600
|
+
The key is additive.
|
|
2538
2601
|
- `workRecovery`: `{path, classes, bytes, home, outputs?, repoCopy?,
|
|
2539
2602
|
notCopied?, afterHooks?}`, present when a recovery was written. One retire
|
|
2540
2603
|
writes at most one recovery directory, and `path` is that directory.
|
|
@@ -2560,10 +2623,12 @@ A first retire prints the **raw receipt**, not an envelope:
|
|
|
2560
2623
|
names: a prefix that matches several entries lists each one, and a
|
|
2561
2624
|
declared entry that does not exist is not listed. `owner` is the
|
|
2562
2625
|
declaring capability; when two declare the same entry it is the first in
|
|
2563
|
-
capability-name order.
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2626
|
+
capability-name order. Since 0.42.1 the list also holds each verified
|
|
2627
|
+
extra tree (`.work-<purpose>`), with `owner: "kernel:extra-worktree"`: the
|
|
2628
|
+
retire handles it at its own step, never in the copy. Each entry has
|
|
2629
|
+
exactly these three keys: names and owners only, no sizes, hashes, modes
|
|
2630
|
+
or contents of what was left out. `scope` is always `"home"` in this
|
|
2631
|
+
release. Present only when there is at least one.
|
|
2567
2632
|
- `afterHooks`: `{home: boolean, work: boolean}`, saying which parts were
|
|
2568
2633
|
copied again under `after-hooks/`: `after-hooks/home/` when a retire hook
|
|
2569
2634
|
changed the home (its bytes and permission bits, the kernel's own
|
|
@@ -2615,8 +2680,9 @@ A first retire prints the **raw receipt**, not an envelope:
|
|
|
2615
2680
|
`idempotencyKey` and `replayed: false`.
|
|
2616
2681
|
|
|
2617
2682
|
Refusals (envelopes): `E_PLAN_STALE`, `E_CHILDREN_RUNNING`,
|
|
2618
|
-
`E_WORK_PRESERVATION_FAILED` (the home is kept; retry or
|
|
2619
|
-
`--discard-worktree`
|
|
2683
|
+
`E_WORK_PRESERVATION_FAILED` (the home is kept; retry, or
|
|
2684
|
+
`--discard-worktree` when it is `work/` that could not be re-homed: it does
|
|
2685
|
+
not apply to an extra tree), `E_WORK_INSPECTION_FAILED` (the home is kept; the
|
|
2620
2686
|
message names the entry or the state that could not be read),
|
|
2621
2687
|
`E_SESSION_UNKNOWN`, `E_AMBIGUOUS_INSTANCE`,
|
|
2622
2688
|
`E_NO_ROOT`, `E_LIFECYCLE_FAILED`. A recovery whose Git status disagrees with
|
|
@@ -2797,6 +2863,7 @@ workspace item of a readable member. Render the rows; never re-derive them.
|
|
|
2797
2863
|
```text
|
|
2798
2864
|
oats schedule list [--dir <d>] --json
|
|
2799
2865
|
oats schedule show <id> --json
|
|
2866
|
+
oats schedule update <id> --description=<text> [--dir <d>] --json
|
|
2800
2867
|
```
|
|
2801
2868
|
|
|
2802
2869
|
```json
|
|
@@ -2823,6 +2890,14 @@ oats schedule show <id> --json
|
|
|
2823
2890
|
- `list`: `{scope, scheduleApi, scheduleHistoryApi, integrity, host,
|
|
2824
2891
|
triggers, snapshot, schedules, scheduler}`; `triggers` counts the trigger
|
|
2825
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`.
|
|
2826
2901
|
- **A readable row**: the definition (`id, kind, cron, tz, enabled, …`, and
|
|
2827
2902
|
`agent/task/purpose/harness` for a spawn, `argv/cwd` for a command, the
|
|
2828
2903
|
message for a wake, the operation for an operation) plus `scope,
|
|
@@ -2842,14 +2917,8 @@ oats schedule show <id> --json
|
|
|
2842
2917
|
limit. Desktop accepts these schedule names for creation, editing and row
|
|
2843
2918
|
actions (enable, disable, test, run and reconcile), including qualified IDs.
|
|
2844
2919
|
A spawn's derived instance name still has a 64-character limit.
|
|
2845
|
-
- **`description
|
|
2846
|
-
|
|
2847
|
-
from its file header). It is one line of at most 200 characters with no
|
|
2848
|
-
control characters; show it in place of the argv when present. Like `task`,
|
|
2849
|
-
it is untrusted text: render it as text. Desktop preserves it unchanged
|
|
2850
|
-
through edits to timing and other fields. A kernel before 0.40.0 sends
|
|
2851
|
-
`null` for every local schedule, and drops a `description` given to `oats
|
|
2852
|
-
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.
|
|
2853
2922
|
- **An unreadable row** (`list` only): `{id, scope, scheduleApi,
|
|
2854
2923
|
scheduleHistoryApi, unreadable: {code, message}, history: {status:
|
|
2855
2924
|
"corrupt", stored: null, truncated: false}, recentRuns: []}`. One bad job
|
|
@@ -2899,14 +2968,14 @@ oats schedule show <id> --json
|
|
|
2899
2968
|
### `oats trigger`
|
|
2900
2969
|
|
|
2901
2970
|
```text
|
|
2902
|
-
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
|
|
2903
2972
|
```
|
|
2904
2973
|
|
|
2905
2974
|
Event-driven spawns. Local definitions live in `oats-schedules.json` (`kind:
|
|
2906
2975
|
"trigger"`).
|
|
2907
2976
|
|
|
2908
2977
|
- `list`: `{triggerApi, scope, host, snapshot, triggers, scheduler}`.
|
|
2909
|
-
`show`, `add`, `enable`, `disable` → `{trigger}`; `remove` → `{removed,
|
|
2978
|
+
`show`, `add`, `update`, `enable`, `disable` → `{trigger}`; `remove` → `{removed,
|
|
2910
2979
|
live: [instance]}`. A stored definition that no longer validates carries
|
|
2911
2980
|
`invalid: {code, message, field?}`.
|
|
2912
2981
|
- **A trigger row**: the [shared row fields](#automations-shared-rows) plus
|
|
@@ -2942,6 +3011,12 @@ Event-driven spawns. Local definitions live in `oats-schedules.json` (`kind:
|
|
|
2942
3011
|
cannot reach is a warning.
|
|
2943
3012
|
- A triggered instance records `instance.json.trigger`; its event file is
|
|
2944
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`.
|
|
2945
3020
|
- Errors: `E_TRIGGER_INVALID {field}`, `E_TRIGGER_EXISTS`,
|
|
2946
3021
|
`E_TRIGGER_UNKNOWN`, `E_TRIGGER_TEAMS`, `E_TRIGGER_POLL`,
|
|
2947
3022
|
`E_TRIGGER_FAILED`, `E_BAD_ARGS`, `E_PACKAGE_MISSING`,
|
|
@@ -2967,7 +3042,18 @@ registered). `maxConcurrent` is the effective scheduled-job cap (default 5);
|
|
|
2967
3042
|
localPath}` or `{kind: "workspace", repoKey, path, commit, url,
|
|
2968
3043
|
localPath}` (`url` for `github.com` only; `localPath` `null` when the
|
|
2969
3044
|
member is not cloned here).
|
|
2970
|
-
- `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` |
|
|
2971
3057
|
`host-unnamed` | `assigned-elsewhere` | `owner-mismatch` | `untrusted`),
|
|
2972
3058
|
`reasonDetail`, `enabledHere`.
|
|
2973
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
|
|
@@ -249,14 +260,18 @@ for a password or key, and never runs anything over ssh itself.
|
|
|
249
260
|
|
|
250
261
|
Every instance a registered server reports shows in its workspace's roster, whoever spawned it. You
|
|
251
262
|
can open its terminal, start, restart, stop and remove it, and read its readiness, activity, Git
|
|
252
|
-
and diffs, as for a local one.
|
|
263
|
+
and diffs, as for a local one. The context panel's Soul tab and its Messaging & Teams list read
|
|
264
|
+
through the server too (`oats inspect --server <id> --home <path>`), and its Work card, "model
|
|
265
|
+
from" line and "older build" chip show the facts the server relays. Only its pull request is not read here, since the forge reads this
|
|
253
266
|
computer's clones. Every command goes to the server by the instance's home (`--server <id> --home
|
|
254
267
|
<path>`), never by a bare name. Stop and Remove show the plan the server makes, and confirm
|
|
255
268
|
against it.
|
|
256
269
|
|
|
257
270
|
A read waits for the server: the view says "Reading from <server>…", and gives up after about
|
|
258
271
|
45 seconds with "Couldn't reach <server>." When the server refuses, you see its code and message;
|
|
259
|
-
nothing is read from this computer in its place.
|
|
272
|
+
nothing is read from this computer in its place. When the server's OATS, or this computer's, lacks
|
|
273
|
+
what a part of the panel needs, that part names the server and says which OATS to update, rather
|
|
274
|
+
than showing nothing. A row that can't be opened says why on the row:
|
|
260
275
|
Herdr no longer supported, gone from <server>, not reachable on <server>, or <server> not reached.
|
|
261
276
|
For an instance a server no longer lists, the reason names the command that removes it from this
|
|
262
277
|
computer (`oats server forget <server> --instance <name>`).
|
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
|
|