@awebai/oats 0.30.2 → 0.31.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 +111 -44
- package/docs/configuration.md +21 -1
- package/docs/desktop-cli-api.md +120 -14
- package/docs/desktop-instance-start.md +3 -1
- package/docs/desktop.md +20 -0
- package/docs/execution-targets.md +53 -30
- package/docs/implementation.md +1 -1
- package/docs/integrations.md +1 -1
- package/docs/oats-local.schema.json +8 -0
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +2 -2
- package/docs/release-notes/v0.30.3.md +75 -0
- package/docs/release-notes/v0.31.0.md +130 -0
- package/docs/schedules.md +4 -1
- package/docs/servers.md +94 -25
- package/docs/souls-and-instances.md +8 -3
- package/lib/attachments.mjs +2 -1
- package/lib/core.mjs +233 -216
- package/lib/errors.mjs +17 -0
- package/lib/instance-inspect.mjs +11 -3
- package/lib/schedule.mjs +21 -8
- package/lib/servers.mjs +265 -45
- package/lib/session-input.mjs +9 -23
- package/lib/session-viewer.mjs +0 -5
- package/lib/triggers.mjs +10 -8
- package/package-catalog.json +1 -1
- package/package.json +1 -1
- package/lib/herdr.mjs +0 -106
package/lib/errors.mjs
CHANGED
|
@@ -5,3 +5,20 @@ export function oatsError(code, message, provenance) {
|
|
|
5
5
|
if (provenance) e.provenance = provenance;
|
|
6
6
|
return e;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/** Herdr was removed in 0.31.0: every place that meets it refuses with E_HERDR_REMOVED, this stem
|
|
10
|
+
* and what to do. */
|
|
11
|
+
export const HERDR_REMOVED = "Herdr is no longer supported by OATS (removed in 0.31.0); tmux is the only session backend.";
|
|
12
|
+
/** A flag or configuration value that selects Herdr: `what` names the flag (`--backend herdr was
|
|
13
|
+
* given`), or the file and key (`<file> sets spawn.backend: herdr`). */
|
|
14
|
+
export function herdrSettingRemoved(what) {
|
|
15
|
+
return oatsError("E_HERDR_REMOVED", `${HERDR_REMOVED} ${what}. Remove it (or use tmux).`);
|
|
16
|
+
}
|
|
17
|
+
/** A session operation on an instance OATS opened in Herdr. */
|
|
18
|
+
export function herdrInstanceRemoved(name) {
|
|
19
|
+
return oatsError("E_HERDR_REMOVED", `${HERDR_REMOVED} ${name} was opened in Herdr. Retire it (\`oats retire ${name}\`) and spawn a new instance; it opens in tmux.`);
|
|
20
|
+
}
|
|
21
|
+
/** Retiring an instance OATS opened in Herdr while processes still work in its home. */
|
|
22
|
+
export function herdrInstanceBusy(name, pids) {
|
|
23
|
+
return oatsError("E_HERDR_REMOVED", `${HERDR_REMOVED} ${name} was opened in Herdr. A process still works in this home (pid ${pids.join(", ")}); stop its Herdr pane (for example \`herdr --session oats server stop\`), then retire.`);
|
|
24
|
+
}
|
package/lib/instance-inspect.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { spawnSync } from "node:child_process";
|
|
|
14
14
|
import { accessSync, constants as fsConstants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
15
15
|
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { capabilityManifests, homeLaunchLayers, instanceSoulDir, launchConfigsAt, launchReportFor, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
17
|
+
import { capabilityManifests, sessionDefaults, homeLaunchLayers, instanceSoulDir, launchConfigsAt, launchReportFor, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
18
18
|
import { agentDirOf, discoverOrStandalone, ensureWorkspaceSoul, findSoulEntry, liveTeams, prepareInstance } from "./instance-resolution.mjs";
|
|
19
19
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
20
20
|
import { kernelCompatibility } from "./resolve.mjs";
|
|
@@ -127,7 +127,7 @@ export async function homeTarget(home, meta, { remoteOptions, discover = true, l
|
|
|
127
127
|
try { launchCurrent = launchReportFor({ layers: homeLaunchLayers(realHome, meta), launchConfigs: launchConfigsAt(deployment), contextDir: deployment }); }
|
|
128
128
|
catch { launchCurrent = null; }
|
|
129
129
|
return {
|
|
130
|
-
kind: "instance", home: realHome, meta, deployment, agentsRoot, teams: live.teams, defaultTeam: live.defaultTeam ?? null, teamsSource: live.source, launch, launchCurrent,
|
|
130
|
+
kind: "instance", home: realHome, meta, deployment, agentsRoot, teams: live.teams, defaultTeam: live.defaultTeam ?? null, teamsSource: live.source, launch, launchCurrent, session: newSessionDefaults(deployment),
|
|
131
131
|
recordedDefaultTeam: recordedTeams(meta).defaultTeam, teamModel: model, teamKey,
|
|
132
132
|
subject: { kind: "instance", instance: meta.instance, home, soul: meta.agent },
|
|
133
133
|
soul: { name: meta.agent, repoKey: ws.soul?.repoKey ?? null, commit: ws.soul?.commit ?? null, external, path: null, soulDir, definition, problems: [...problems, ...loadProblems] },
|
|
@@ -183,7 +183,7 @@ export async function soulTarget(contextDir, soul, { remoteOptions } = {}) {
|
|
|
183
183
|
try { launchConfigs = launchConfigsAt(deployment); } catch { launchConfigs = {}; }
|
|
184
184
|
const launch = launchReportFor({ layers: prepared?.launch ?? launchLayers({ definition: soulEntry.definition, entry: soulEntry, key: teamKey, local: found.local }), launchConfigs, contextDir: deployment });
|
|
185
185
|
return {
|
|
186
|
-
kind: "soul", home: null, meta: null, deployment, agentsRoot: join(deployment, "agents"), teams, defaultTeam, teamsSource: "live", teamModel: model, teamKey, launch,
|
|
186
|
+
kind: "soul", home: null, meta: null, deployment, agentsRoot: join(deployment, "agents"), teams, defaultTeam, teamsSource: "live", teamModel: model, teamKey, launch, session: newSessionDefaults(deployment),
|
|
187
187
|
subject: { kind: "soul", soul: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null },
|
|
188
188
|
soul: { name: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null, external: soulEntry.external === true, path: soulEntry.path ?? null,
|
|
189
189
|
soulDir, definition: soulEntry.definition ?? null, problems: [], ...(soulCopyError ? { copyError: soulCopyError } : {}) },
|
|
@@ -195,6 +195,12 @@ export async function soulTarget(contextDir, soul, { remoteOptions } = {}) {
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/** The tmux session a NEW tmux spawn here would open in ({tmuxSession}, 0.31); null when the
|
|
199
|
+
* deployment's oats-local.yaml cannot be read. */
|
|
200
|
+
function newSessionDefaults(deployment) {
|
|
201
|
+
try { return sessionDefaults(deployment); } catch { return null; }
|
|
202
|
+
}
|
|
203
|
+
|
|
198
204
|
/** Is `dir` a v2 context (an oats-local.yaml in reach)? Only a MISSING file means
|
|
199
205
|
* "not a workspace"; an unreadable or invalid one is the caller's error to report,
|
|
200
206
|
* never a silent fallback to the classic chain. */
|
|
@@ -256,6 +262,8 @@ export function inspectDocument(t, { kernel }) {
|
|
|
256
262
|
// Feature launch-preference: a soul's launch here; a home's recorded launch and launchCurrent.
|
|
257
263
|
launch: t.launch ?? null,
|
|
258
264
|
...(t.home ? { launchCurrent: t.launchCurrent ?? null } : {}),
|
|
265
|
+
// What a new tmux spawn here would open in: {tmuxSession} (0.31).
|
|
266
|
+
session: t.session ?? null,
|
|
259
267
|
knowledge: kcap ? { provider: kcap.id, version: kcap.version, operations: kcap.operations.map((o) => ({ name: o.name, kind: o.kind, available: o.available, reason: o.reason })) } : { provider: null, version: null, operations: [] },
|
|
260
268
|
instance: m ? { home: t.home, instance: m.instance, agent: m.agent, harness: m.harness || null, model: m.model ?? null, yolo: m.yolo ?? null, launched: !!m.launched, createdAt: m.createdAt || null,
|
|
261
269
|
resolution: m.workspace?.resolution ?? null, soulDir: t.soul.soulDir, instructions: { ...readTextCapped(join(t.home, "AGENTS.md")), sources: m.instructions || [] } } : null,
|
package/lib/schedule.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from "node:url";
|
|
|
29
29
|
import { Cron } from "croner";
|
|
30
30
|
import { loadLocal } from "./workspace.mjs";
|
|
31
31
|
import { noteRuntimeName } from "./deprecation.mjs";
|
|
32
|
+
import { herdrSettingRemoved } from "./errors.mjs";
|
|
32
33
|
import { tickTriggers } from "./triggers.mjs";
|
|
33
34
|
import { resolveMemberClone } from "./instance-resolution.mjs";
|
|
34
35
|
import { AUTOMATION_ID_RE, MODEL_RE, automationContext, automationError, baseRow, localEntry, localId, soulOriginOf, splitId } from "./automations.mjs";
|
|
@@ -38,7 +39,6 @@ export const SCHEDULE_FILE = "oats-schedules.json";
|
|
|
38
39
|
export const SCHEDULE_API = 2;
|
|
39
40
|
const ID_RE = /^[a-z0-9-]{1,40}$/;
|
|
40
41
|
const HARNESSES = new Set(["pi", "claude", "codex"]);
|
|
41
|
-
const BACKENDS = new Set(["tmux", "herdr"]);
|
|
42
42
|
const MESSAGE_MAX = 256 * 1024;
|
|
43
43
|
const OATS_BIN = join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "oats.mjs");
|
|
44
44
|
const COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -295,8 +295,17 @@ function validateMessage(message, field) {
|
|
|
295
295
|
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 });
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
/**
|
|
299
|
-
|
|
298
|
+
/** The refusal of a spawn job's `backend: herdr` (Herdr was removed in 0.31.0): `where` names the
|
|
299
|
+
* file and key that set it. */
|
|
300
|
+
export function herdrBackendRefusal(where) { return Object.assign(herdrSettingRemoved(`${where}: herdr`), { field: "backend" }); }
|
|
301
|
+
/** A stored local spawn job that selects Herdr: its refusal, naming this scope's definitions file. */
|
|
302
|
+
function storedHerdrBackend(ws, id, def) {
|
|
303
|
+
return def?.kind === "spawn" && !def.workspace && def.backend === "herdr" ? herdrBackendRefusal(`${definitionsPath(ws)} sets jobs.${id}.backend`) : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Validate and normalize one definition against its scope. `backendSource` names the file and key
|
|
307
|
+
* a definition read from elsewhere sets its backend in (default: this scope's definitions file). */
|
|
308
|
+
export function validateDefinition(ws, def, { checkAgent = true, backendSource } = {}) {
|
|
300
309
|
if (!def || typeof def !== "object" || Array.isArray(def)) throw scheduleError("E_SCHEDULE_INVALID", "definition must be an object", { field: "definition" });
|
|
301
310
|
if (def.kind === "trigger") throw scheduleError("E_SCHEDULE_INVALID", "kind: a trigger is added with `oats trigger add`, not `oats schedule add`", { field: "kind" });
|
|
302
311
|
const id = def.id;
|
|
@@ -314,7 +323,8 @@ export function validateDefinition(ws, def, { checkAgent = true } = {}) {
|
|
|
314
323
|
out.agentsRoot = resolve(def.agentsRoot);
|
|
315
324
|
}
|
|
316
325
|
if (def.repo !== undefined) { if (typeof def.repo !== "string" || !def.repo.trim() || def.repo.startsWith("-")) throw scheduleError("E_SCHEDULE_INVALID", "repo: the work repository path, as oats spawn --repo", { field: "repo" }); out.repo = def.repo; }
|
|
317
|
-
if (def.backend
|
|
326
|
+
if (def.backend === "herdr") throw herdrBackendRefusal(backendSource ?? `${definitionsPath(ws)} sets jobs.${id}.backend`);
|
|
327
|
+
if (def.backend !== undefined) { if (def.backend !== "tmux") throw scheduleError("E_SCHEDULE_INVALID", "backend: tmux", { field: "backend" }); out.backend = def.backend; }
|
|
318
328
|
if (def.purpose !== undefined) { if (typeof def.purpose !== "string" || !/^[a-z0-9-]{1,40}$/.test(def.purpose)) throw scheduleError("E_SCHEDULE_INVALID", "purpose: lowercase letters, digits and dashes", { field: "purpose" }); out.purpose = def.purpose; }
|
|
319
329
|
// A run spawns <agent>-<purpose|id>-<YYYYMMDDHHMM>; instance names are at most
|
|
320
330
|
// MAX_INSTANCE_NAME characters, so refuse here what would fail at every firing.
|
|
@@ -413,7 +423,7 @@ export function validateWorkspaceSchedule(dep, a) {
|
|
|
413
423
|
const real = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
414
424
|
if (!inside(real(dep), real(spec.cwd))) throw scheduleError("E_SCHEDULE_INVALID", `cwd: ${JSON.stringify(d.cwd)} leaves the deployment; a workspace command's cwd is a directory inside it`, { field: "cwd" });
|
|
415
425
|
}
|
|
416
|
-
const out = validateDefinition(dep, spec, { checkAgent: false });
|
|
426
|
+
const out = validateDefinition(dep, spec, { checkAgent: false, backendSource: `${a.origin?.path ?? a.name} sets backend` });
|
|
417
427
|
delete out.enabled;
|
|
418
428
|
return out;
|
|
419
429
|
}
|
|
@@ -756,11 +766,13 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
|
|
|
756
766
|
const def = defs.jobs[id];
|
|
757
767
|
const js = stateOf(id);
|
|
758
768
|
let due, cronError;
|
|
759
|
-
const
|
|
769
|
+
const invalidShape = shapeError(def), herdr = invalidShape ? null : storedHerdrBackend(ws, id, def);
|
|
770
|
+
const shape = invalidShape ?? herdr?.message;
|
|
760
771
|
if (!shape) validateActiveExecution(js, jobLockInfo(ws, id)); // Also covers host pre-observed candidates.
|
|
761
772
|
if (shape) { cronError = shape; due = false; }
|
|
762
773
|
else { try { due = only ? true : dueAt(def, minute); } catch (e) { cronError = e.message; due = false; } }
|
|
763
|
-
|
|
774
|
+
const errorCode = herdr ? { errorCode: herdr.code } : {};
|
|
775
|
+
if (cronError) { if (!dryRun) js.lastRun = { ...(js.lastRun || {}), outcome: "invalid", error: `definition cannot be evaluated: ${cronError}`, ...errorCode }; record(id, "invalid", { error: cronError, ...errorCode }); continue; }
|
|
764
776
|
if (def.kind === "wake" && js.pendingWake && def.enabled && !dryRun && !only) {
|
|
765
777
|
const dueNow = due && js.lastAttemptedMinute !== key;
|
|
766
778
|
const held = !!jobLockInfo(ws, id);
|
|
@@ -1066,7 +1078,8 @@ export function describe(ws, qid, io, { defs, st, now = new Date(), ctx = null }
|
|
|
1066
1078
|
const { history, recentRuns } = projectHistory(js);
|
|
1067
1079
|
const { workspace: _w, ...stored } = def; void _w;
|
|
1068
1080
|
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 } : {}) };
|
|
1069
|
-
|
|
1081
|
+
const herdr = entry ? null : storedHerdrBackend(ws, id, def);
|
|
1082
|
+
return scheduleRow(ws, entry ?? localEntry("schedule", { ...def, id: name }, { dep: ws, ...(herdr ? { invalid: { code: herdr.code, message: herdr.message, field: herdr.field } } : {}) }), base, ctx);
|
|
1070
1083
|
}
|
|
1071
1084
|
/** A schedule's list row: its stored facts, the fields every trigger and schedule row share
|
|
1072
1085
|
* (lib/automations.mjs baseRow) and the schedule's own: `kind` is its run (spawn, command, wake,
|