@awebai/oats 0.24.13 → 0.25.1
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 +994 -2837
- package/docs/capabilities.md +136 -323
- package/docs/configuration.md +68 -533
- package/docs/conventions.md +51 -24
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +19 -14
- package/docs/design/2026-09-16-portable-migration-evidence.md +2 -0
- package/docs/design/2026-09-16-portable-onboarding.md +4 -2
- package/docs/design/2026-09-20-redesign-program-board.md +1 -1
- package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +2 -0
- package/docs/design/2026-09-23-simplified-workspace-model.md +711 -0
- package/docs/design/2026-09-23-workspace-module-contracts.md +460 -0
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +65 -0
- package/docs/design/README.md +20 -8
- package/docs/design/operations-contract.md +1 -0
- package/docs/design/package-engine-contract.md +1 -1
- package/docs/design/package-runtime-api.md +1 -1
- package/docs/desktop-cli-api.md +356 -5
- package/docs/desktop-succession.md +12 -6
- package/docs/desktop.md +9 -4
- package/docs/execution-targets.md +16 -4
- package/docs/first-team.md +107 -224
- package/docs/implementation.md +41 -11
- package/docs/integrations.md +50 -47
- package/docs/knowledge-capability-authoring.md +10 -4
- package/docs/knowledge-migration.md +21 -12
- package/docs/knowledge-reference/package-craft.md +11 -3
- package/docs/knowledge.md +60 -18
- package/docs/layers.md +3 -3
- package/docs/migration-from-oas.md +20 -9
- package/docs/oats-local.schema.json +50 -0
- package/docs/oats-membership.schema.json +23 -0
- package/docs/oats-workspace.schema.json +133 -48
- package/docs/official-marketplace.md +9 -6
- package/docs/packages.md +229 -440
- package/docs/rebuild-to-v2.md +347 -0
- package/docs/release-notes/v0.25.0.md +99 -0
- package/docs/release-notes/v0.25.1.md +94 -0
- package/docs/schedules.md +12 -6
- package/docs/soul.schema.json +41 -68
- package/docs/souls-and-instances.md +175 -108
- package/docs/workspace-adoption.md +70 -345
- package/docs/workspaces.md +436 -119
- package/lib/core.mjs +462 -61
- package/lib/instance-resolution.mjs +387 -0
- package/lib/materialize.mjs +580 -0
- package/lib/operator-dispatch.mjs +117 -0
- package/lib/packages.mjs +558 -1269
- package/lib/remote.mjs +718 -0
- package/lib/resolve.mjs +638 -0
- package/lib/schedule.mjs +90 -16
- package/lib/workspace.mjs +654 -0
- package/package.json +1 -1
- package/lib/portable-migration-artifacts.mjs +0 -135
- package/lib/portable-migration-evidence.mjs +0 -305
- package/lib/portable-migration-store.mjs +0 -199
- package/lib/portable-migration.mjs +0 -104
- package/lib/portable-onboarding-acceptance.mjs +0 -66
- package/lib/setup-expert-source.mjs +0 -100
package/lib/schedule.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
28
|
import { Cron } from "croner";
|
|
29
29
|
import { portableScope } from "./portable-state.mjs";
|
|
30
|
+
import { loadLocal } from "./workspace.mjs";
|
|
30
31
|
import { RESERVED_LAUNCH_ENV, ensureRoot, findAgent, findCapabilityAgent, findInstanceHomes, teamAgentRoots, configChain, spawnInstance, inspectInstanceSession, inputInstanceSession, startInstanceSession, retirePendingMarkerPath, loadCapturedDispatch, prepareCapturedComposition } from "./core.mjs";
|
|
31
32
|
import { RECURRENCE_POLICIES, SCHEDULE_ATTEMPT_VERSION, SCHEDULE_DEFINITION_VERSION, admitExecutionTemplate, buildCommandExecutionTemplate, buildWakeExecutionTemplate, capturedDispatchAction, commandArgvWithExecutionBinding, executionContentIntegrity, validateExecutionBinding, validateExecutionCapsule, validateExecutionTemplate, validatePreparationInput } from "./schedule-capsule.mjs";
|
|
32
33
|
|
|
@@ -474,13 +475,97 @@ export function findHomesInScope(ws, instance) {
|
|
|
474
475
|
function scheduleBlock(def, minute) {
|
|
475
476
|
return `\n\n## Scheduled run\n\nThis instance was launched by OATS schedule "${def.id}" for ${minute.toISOString()} (cron "${def.cron}" in ${def.tz}). It is disposable: finish the task above, bring your memory files up to date, and end with \`oats retire --self\`.\n`;
|
|
476
477
|
}
|
|
477
|
-
/**
|
|
478
|
+
/** The environment a scheduler-launched oats child runs with: the caller's
|
|
479
|
+
* frozen instance identity and capability snapshot removed (a tick or run-now
|
|
480
|
+
* may be invoked from inside an unrelated instance; dispatch belongs to the
|
|
481
|
+
* job's cwd and explicit selectors, never to that caller), host configuration
|
|
482
|
+
* such as OATS_HOME_DIR and credentials kept. Shared by command and spawn jobs. */
|
|
483
|
+
function childEnv() {
|
|
484
|
+
const env = { ...process.env };
|
|
485
|
+
for (const key of [...RESERVED_LAUNCH_ENV,
|
|
486
|
+
"OATS_DEPLOYMENT", "OATS_RESOLUTION", "OATS_CAPABILITY", "OATS_LAYER", "OATS_LEVEL", "OATS_META", "OATS_OPERATION",
|
|
487
|
+
"OATS_REPO", "OATS_BRANCH", "OATS_WORK", "OATS_KIND", "OATS_TASK",
|
|
488
|
+
"OATS_RUNTIME", "OATS_PREVIOUS_RUNTIME", "OATS_RETIRE_INTENT",
|
|
489
|
+
"OATS_TEAM_NAME", "OATS_TEAM_ID", "OATS_TEAM_SCOPE",
|
|
490
|
+
]) delete env[key];
|
|
491
|
+
return env;
|
|
492
|
+
}
|
|
493
|
+
/** Is this spawn a WORKSPACE-model spawn (module contract §5, decision 7)?
|
|
494
|
+
* Two marks, either suffices: the deployment realizes a workspace
|
|
495
|
+
* (`oats-local.yaml` walking up from the agents root or the scope — the same
|
|
496
|
+
* detection `oats spawn` uses), or the soul under the agents root was copied
|
|
497
|
+
* from a member repo by ensureWorkspaceSoul (`.oats-soul-source.json` beside
|
|
498
|
+
* it). An unreadable oats-local.yaml is still a workspace deployment: the
|
|
499
|
+
* spawn must go the workspace way and report E_WORKSPACE_SCHEMA, never fall
|
|
500
|
+
* back to a classic home. Returns null for a classic (bare agents root) spawn. */
|
|
501
|
+
export function workspaceSpawnContext(ws, root, agent) {
|
|
502
|
+
for (const dir of [dirname(root), ws]) {
|
|
503
|
+
try { const found = loadLocal(dir); return { kind: "local", local: found.path, dir: dirname(found.path) }; }
|
|
504
|
+
catch (e) { if (e?.code !== "E_LOCAL_MISSING") return { kind: "local", local: null, dir, error: { code: e.code || "E_WORKSPACE_SCHEMA", message: e.message } }; }
|
|
505
|
+
}
|
|
506
|
+
const stamp = agent?._dir ? join(agent._dir, ".oats-soul-source.json") : join(root, agent?.name || "", ".oats-soul-source.json");
|
|
507
|
+
if (agent?.name && existsSync(stamp)) return { kind: "soul-source", local: null, dir: dirname(root), stamp };
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
/** A workspace spawn materializes EXACTLY like `oats spawn` — prepareInstance →
|
|
511
|
+
* ensureWorkspaceSoul → spawnInstanceAsync (resolve over the remotes, copy every
|
|
512
|
+
* module whole into the home, compose AGENTS.md from the modules' injects).
|
|
513
|
+
* That chain is asynchronous (it reads Git remotes); the tick chain is
|
|
514
|
+
* synchronous by design (one short-lived process under the host lock, no
|
|
515
|
+
* daemon). So the tick delegates to the kernel's own CLI as a child process,
|
|
516
|
+
* like a command job: `oats spawn <soul> --json …` in the deployment, and
|
|
517
|
+
* reads back the one JSON envelope. The child holds no host lock (a spawn
|
|
518
|
+
* takes only the scope lock, briefly, for a wake it saves — and the schedule
|
|
519
|
+
* passes no wake flags: the wake is saved here, from the run record).
|
|
520
|
+
* `io.oatsBin`, `io.commandTimeoutMs` and `io.noLaunch` are the test seams. */
|
|
521
|
+
function spawnViaCli(ws, root, agent, opts, context, io) {
|
|
522
|
+
if (context.error) throw scheduleError(context.error.code, `this deployment realizes a workspace (${context.dir}) but its oats-local.yaml cannot be read: ${context.error.message}`);
|
|
523
|
+
const argv = ["spawn", agent.name, "--dir", context.dir, "--agents-root", root, "--purpose", opts.purpose, "--json"];
|
|
524
|
+
if (opts.runtime) argv.push("--runtime", opts.runtime);
|
|
525
|
+
if (opts.model) argv.push("--model", opts.model);
|
|
526
|
+
if (opts.backend) argv.push("--backend", opts.backend);
|
|
527
|
+
if (opts.repo) argv.push("--repo", opts.repo);
|
|
528
|
+
if (opts.yolo === true) argv.push("--yolo"); else if (opts.yolo === false) argv.push("--no-yolo");
|
|
529
|
+
if (opts.launch === false) argv.push("--no-launch");
|
|
530
|
+
// The task is multi-line text of arbitrary size: it travels as a private file, never in argv.
|
|
531
|
+
mkdirSync(join(stateDir(ws), "tasks"), { recursive: true });
|
|
532
|
+
const taskFile = join(stateDir(ws), "tasks", `${opts.purpose}-${process.pid}.md`);
|
|
533
|
+
writeFileSync(taskFile, opts.task, { mode: 0o600 });
|
|
534
|
+
argv.push("--task-file", taskFile);
|
|
535
|
+
let r;
|
|
536
|
+
try { r = spawnSync(process.execPath, [io?.oatsBin || OATS_BIN, ...argv], { cwd: context.dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: io?.commandTimeoutMs || COMMAND_TIMEOUT_MS, killSignal: "SIGTERM", maxBuffer: 16 * 1024 * 1024, env: childEnv() }); }
|
|
537
|
+
finally { try { rmSync(taskFile, { force: true }); } catch { /* best effort */ } }
|
|
538
|
+
const timedOut = r.error?.code === "ETIMEDOUT" || (r.status === null && r.signal === "SIGTERM");
|
|
539
|
+
const envelope = parseEnvelopeText(r.stdout);
|
|
540
|
+
// The child may have created (and launched) a home before dying: unconfirmed, never a confirmed failure.
|
|
541
|
+
// The thrown message reports RETAINED effects so the tick keeps the attempt and its slot for reconcile.
|
|
542
|
+
if (timedOut) throw scheduleError("E_SPAWN_UNCONFIRMED", "workspace spawn timed out; its effects could not be confirmed (a home may have been created) — run oats schedule reconcile");
|
|
543
|
+
if (!envelope || typeof envelope !== "object" || typeof envelope.ok !== "boolean") throw scheduleError("E_SPAWN_UNCONFIRMED", `workspace spawn answered no valid envelope (exit ${r.status ?? r.signal}); its effects could not be confirmed — run oats schedule reconcile${r.stderr ? `: ${String(r.stderr).trim().split("\n").pop()}` : ""}`);
|
|
544
|
+
if (!envelope.ok) {
|
|
545
|
+
const e = envelope.error || {};
|
|
546
|
+
// E_SPAWN_INCOMPLETE: the kernel created a home it could not finish or roll back — retained effects.
|
|
547
|
+
const retained = e.code === "E_SPAWN_INCOMPLETE" || e.details?.home;
|
|
548
|
+
throw scheduleError(e.code || "E_SPAWN_FAILED", retained ? `${e.message} (home retained; effects unconfirmed)` : String(e.message || "spawn failed"), { details: e.details });
|
|
549
|
+
}
|
|
550
|
+
const res = envelope.result || {};
|
|
551
|
+
return { instance: res.instance, home: res.home, launched: res.launched, materialized: "workspace" };
|
|
552
|
+
}
|
|
553
|
+
/** Launch one spawn job. `io.spawn(root, agent, opts)` overrides the whole
|
|
554
|
+
* launcher for tests. Otherwise a workspace deployment goes through the
|
|
555
|
+
* kernel's own `oats spawn` (workspace materialization: `.oats/modules`,
|
|
556
|
+
* module skills and injects — never a classic home); a bare agents root goes
|
|
557
|
+
* through the synchronous classic spawnInstance, whose failures throw here. */
|
|
478
558
|
function launchSpawn(ws, def, minute, io) {
|
|
479
559
|
const { root, agent } = resolveScheduledAgent(ws, def);
|
|
480
560
|
const purpose = `${def.purpose || def.id}-${purposeSuffix(minute)}`;
|
|
481
|
-
const opts = { task: def.task.trimEnd() + scheduleBlock(def, minute), purpose, runtime: def.runtime, model: def.model, yolo: def.yolo, backend: def.backend, repo: def.repo, launch: true };
|
|
482
|
-
|
|
483
|
-
|
|
561
|
+
const opts = { task: def.task.trimEnd() + scheduleBlock(def, minute), purpose, runtime: def.runtime, model: def.model, yolo: def.yolo, backend: def.backend, repo: def.repo, launch: io?.noLaunch === true ? false : true };
|
|
562
|
+
let r;
|
|
563
|
+
if (io?.spawn) r = io.spawn(root, agent, opts);
|
|
564
|
+
else {
|
|
565
|
+
const context = workspaceSpawnContext(ws, root, agent);
|
|
566
|
+
r = context ? spawnViaCli(ws, root, agent, opts, context, io) : spawnInstance(root, agent, opts);
|
|
567
|
+
}
|
|
568
|
+
const run = { instance: r.instance, home: r.home, launched: !!r.instance, kind: "spawn", ...(r.materialized ? { materialized: r.materialized } : {}) };
|
|
484
569
|
if (def.wake && r.home) {
|
|
485
570
|
try { run.wakeSchedule = saveWakeForHome(ws, { instance: r.instance, home: r.home, wake: def.wake }); }
|
|
486
571
|
catch (e) { run.wakeScheduleError = { code: e.code || "E_SCHEDULE_INVALID", message: e.message }; }
|
|
@@ -514,18 +599,7 @@ function launchCommand(ws, def, io) {
|
|
|
514
599
|
let envelope, timedOut = false, raw = "";
|
|
515
600
|
if (io?.command) envelope = io.command({ cwd: def.cwd, argv });
|
|
516
601
|
else {
|
|
517
|
-
const
|
|
518
|
-
// A schedule may be ticked/run-now from inside an unrelated instance.
|
|
519
|
-
// Dispatch belongs to the job's cwd/explicit selectors, never that
|
|
520
|
-
// caller's frozen capability snapshot (including legacy OATS_HOME).
|
|
521
|
-
// Keep host configuration such as OATS_HOME_DIR and credentials intact.
|
|
522
|
-
for (const key of [...RESERVED_LAUNCH_ENV,
|
|
523
|
-
"OATS_DEPLOYMENT", "OATS_RESOLUTION", "OATS_CAPABILITY", "OATS_LAYER", "OATS_LEVEL", "OATS_META", "OATS_OPERATION",
|
|
524
|
-
"OATS_REPO", "OATS_BRANCH", "OATS_WORK", "OATS_KIND", "OATS_TASK",
|
|
525
|
-
"OATS_RUNTIME", "OATS_PREVIOUS_RUNTIME", "OATS_RETIRE_INTENT",
|
|
526
|
-
"OATS_TEAM_NAME", "OATS_TEAM_ID", "OATS_TEAM_SCOPE",
|
|
527
|
-
]) delete env[key];
|
|
528
|
-
const r = spawnSync(process.execPath, [io?.oatsBin || OATS_BIN, ...argv], { cwd: def.cwd, encoding: "utf8", timeout: io?.commandTimeoutMs || COMMAND_TIMEOUT_MS, killSignal: "SIGTERM", maxBuffer: 16 * 1024 * 1024, env });
|
|
602
|
+
const r = spawnSync(process.execPath, [io?.oatsBin || OATS_BIN, ...argv], { cwd: def.cwd, encoding: "utf8", timeout: io?.commandTimeoutMs || COMMAND_TIMEOUT_MS, killSignal: "SIGTERM", maxBuffer: 16 * 1024 * 1024, env: childEnv() });
|
|
529
603
|
timedOut = r.error?.code === "ETIMEDOUT" || (r.status === null && r.signal === "SIGTERM");
|
|
530
604
|
raw = String(r.stdout || "");
|
|
531
605
|
envelope = parseEnvelopeText(raw);
|