@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/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
+ }
@@ -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
- /** Validate and normalize one definition against its scope. */
299
- export function validateDefinition(ws, def, { checkAgent = true } = {}) {
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 !== undefined) { if (!BACKENDS.has(def.backend)) throw scheduleError("E_SCHEDULE_INVALID", "backend: tmux or herdr", { field: "backend" }); out.backend = 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 shape = shapeError(def);
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
- if (cronError) { if (!dryRun) js.lastRun = { ...(js.lastRun || {}), outcome: "invalid", error: `definition cannot be evaluated: ${cronError}` }; record(id, "invalid", { error: cronError }); continue; }
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
- return scheduleRow(ws, entry ?? localEntry("schedule", { ...def, id: name }, { dep: ws }), base, ctx);
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,