@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.
Files changed (58) hide show
  1. package/bin/oats.mjs +994 -2837
  2. package/docs/capabilities.md +136 -323
  3. package/docs/configuration.md +68 -533
  4. package/docs/conventions.md +51 -24
  5. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +19 -14
  6. package/docs/design/2026-09-16-portable-migration-evidence.md +2 -0
  7. package/docs/design/2026-09-16-portable-onboarding.md +4 -2
  8. package/docs/design/2026-09-20-redesign-program-board.md +1 -1
  9. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +2 -0
  10. package/docs/design/2026-09-23-simplified-workspace-model.md +711 -0
  11. package/docs/design/2026-09-23-workspace-module-contracts.md +460 -0
  12. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +65 -0
  13. package/docs/design/README.md +20 -8
  14. package/docs/design/operations-contract.md +1 -0
  15. package/docs/design/package-engine-contract.md +1 -1
  16. package/docs/design/package-runtime-api.md +1 -1
  17. package/docs/desktop-cli-api.md +356 -5
  18. package/docs/desktop-succession.md +12 -6
  19. package/docs/desktop.md +9 -4
  20. package/docs/execution-targets.md +16 -4
  21. package/docs/first-team.md +107 -224
  22. package/docs/implementation.md +41 -11
  23. package/docs/integrations.md +50 -47
  24. package/docs/knowledge-capability-authoring.md +10 -4
  25. package/docs/knowledge-migration.md +21 -12
  26. package/docs/knowledge-reference/package-craft.md +11 -3
  27. package/docs/knowledge.md +60 -18
  28. package/docs/layers.md +3 -3
  29. package/docs/migration-from-oas.md +20 -9
  30. package/docs/oats-local.schema.json +50 -0
  31. package/docs/oats-membership.schema.json +23 -0
  32. package/docs/oats-workspace.schema.json +133 -48
  33. package/docs/official-marketplace.md +9 -6
  34. package/docs/packages.md +229 -440
  35. package/docs/rebuild-to-v2.md +347 -0
  36. package/docs/release-notes/v0.25.0.md +99 -0
  37. package/docs/release-notes/v0.25.1.md +94 -0
  38. package/docs/schedules.md +12 -6
  39. package/docs/soul.schema.json +41 -68
  40. package/docs/souls-and-instances.md +175 -108
  41. package/docs/workspace-adoption.md +70 -345
  42. package/docs/workspaces.md +436 -119
  43. package/lib/core.mjs +462 -61
  44. package/lib/instance-resolution.mjs +387 -0
  45. package/lib/materialize.mjs +580 -0
  46. package/lib/operator-dispatch.mjs +117 -0
  47. package/lib/packages.mjs +558 -1269
  48. package/lib/remote.mjs +718 -0
  49. package/lib/resolve.mjs +638 -0
  50. package/lib/schedule.mjs +90 -16
  51. package/lib/workspace.mjs +654 -0
  52. package/package.json +1 -1
  53. package/lib/portable-migration-artifacts.mjs +0 -135
  54. package/lib/portable-migration-evidence.mjs +0 -305
  55. package/lib/portable-migration-store.mjs +0 -199
  56. package/lib/portable-migration.mjs +0 -104
  57. package/lib/portable-onboarding-acceptance.mjs +0 -66
  58. 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
- /** Launch one spawn job. `io.spawn(root, agent, opts)` overrides spawnInstance for tests. */
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
- const r = (io?.spawn || spawnInstance)(root, agent, opts);
483
- const run = { instance: r.instance, home: r.home, launched: !!r.instance, kind: "spawn" };
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 env = { ...process.env };
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);