@awebai/oats 0.22.0 → 0.22.2

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 (60) hide show
  1. package/README.md +40 -50
  2. package/bin/oats.mjs +242 -22
  3. package/capabilities/oats-authoring/LICENSE +21 -0
  4. package/capabilities/oats-authoring/oats-package.json +11 -0
  5. package/capabilities/oats-authoring/oats.json +4 -4
  6. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +63 -0
  7. package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +109 -0
  8. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +109 -0
  9. package/capabilities/oats-aweb/injects/aweb.md +4 -3
  10. package/capabilities/oats-aweb/oats.json +7 -7
  11. package/capabilities/oats-aweb/skills/LICENSE +21 -0
  12. package/capabilities/oats-aweb/skills/VENDORED.md +26 -0
  13. package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +201 -0
  14. package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +161 -0
  15. package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +61 -0
  16. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +328 -0
  17. package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +74 -0
  18. package/capabilities/oats-jira/oats.json +1 -1
  19. package/capabilities/oats-linear/oats.json +1 -1
  20. package/capabilities/oats-okf/agents/{memory-harvest.md → memory-harvest/AGENTS.md} +3 -1
  21. package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +6 -0
  22. package/capabilities/oats-okf/bin/oats-okf.mjs +201 -54
  23. package/capabilities/oats-okf/injects/okf.md +7 -0
  24. package/capabilities/oats-okf/oats.json +5 -2
  25. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +42 -1
  26. package/capabilities/oats-review/oats.json +1 -1
  27. package/docs/2026-09-03-architecture-proposal.md +642 -0
  28. package/docs/execution-targets.md +181 -0
  29. package/docs/first-team-demo.md +87 -0
  30. package/docs/first-team.md +179 -0
  31. package/docs/implementation.md +14 -1
  32. package/docs/integrations.md +83 -65
  33. package/docs/layers.md +356 -80
  34. package/docs/migration-from-oas.md +80 -116
  35. package/docs/oats-config.schema.json +1 -0
  36. package/docs/operating-team-migration.md +217 -0
  37. package/docs/release-notes/v0.22.1.md +106 -0
  38. package/docs/release-notes/v0.22.2.md +69 -0
  39. package/docs/servers.md +94 -0
  40. package/docs/souls-and-instances.md +30 -3
  41. package/lib/core.mjs +626 -415
  42. package/lib/herdr.mjs +95 -0
  43. package/lib/servers.mjs +436 -0
  44. package/lib/session-input.mjs +78 -0
  45. package/lib/session-viewer.mjs +51 -0
  46. package/package-catalog.json +2 -2
  47. package/package.json +1 -1
  48. package/packages/record/README.md +76 -16
  49. package/packages/record/bin/capture.mjs +59 -3
  50. package/packages/record/bin/recall.mjs +67 -1
  51. package/packages/record/docs/turn-record-sot.md +1 -1
  52. package/packages/record/lib/sessions-for-home.mjs +130 -0
  53. package/packages/record/lib/store.mjs +207 -43
  54. package/skills/oats/SKILL.md +6 -2
  55. package/capabilities/oats-aweb/package.json +0 -20
  56. package/capabilities/oats-jira/package.json +0 -25
  57. package/capabilities/oats-linear/README.md +0 -234
  58. package/capabilities/oats-linear/package.json +0 -29
  59. package/capabilities/oats-linear/test/oats-linear.test.mjs +0 -168
  60. package/capabilities/oats-okf/package.json +0 -22
package/README.md CHANGED
@@ -14,11 +14,12 @@ specialist, a maintainer, a reviewer, a package owner, or any other role, each
14
14
  with a precise curriculum, durable knowledge, and a full provider-native
15
15
  session you can enter and steer.
16
16
 
17
- OATS works with **Pi** and **Claude Code**. A team may mix providers and models
17
+ OATS launches **Pi**, **Claude Code**, and **Codex**. A team may mix providers and models
18
18
  while sharing the same souls, package and config contracts, instance
19
- lifecycle, and coordination topology. Every conversation an agent has is
20
- captured into an append-only, searchable **turn record** that outlives models,
21
- harnesses, and this repository's own designs.
19
+ lifecycle, and coordination topology. On machines where `oats setup` has run,
20
+ the append-only, searchable **turn record** captures supported local transcripts
21
+ and aw client logs. It outlives models, harnesses, and this repository's own
22
+ designs.
22
23
 
23
24
  ## Contents
24
25
 
@@ -44,7 +45,7 @@ harnesses, and this repository's own designs.
44
45
  instantiated many times without losing its identity or accumulated
45
46
  expertise.
46
47
  - **Instances are real sessions, not hidden subagent calls.** Each instance is
47
- a disposable incarnation with a full Pi or Claude Code session hosted in
48
+ a disposable incarnation with a full Pi, Claude Code, or Codex session hosted in
48
49
  tmux, an explicit task, its own home, and a repository or workspace view.
49
50
  You can attach to it, steer it, message it, stop it, and inspect exactly
50
51
  what it received.
@@ -63,56 +64,34 @@ harnesses, and this repository's own designs.
63
64
  `sibling` relationships, can carry cross-machine identities through a
64
65
  messaging layer such as `oats.aweb`, and are visible together in OATS
65
66
  Desktop.
66
- - **Everything is on the record.** Claude Code, Pi, and Codex sessions, plus
67
- aweb mail and chat, are captured as signed turns with exact provenance and
68
- searched locally with `oats recall`.
67
+ - **Supported conversations stay on the record.** On machines where `oats
68
+ setup` has run, OATS captures Claude Code, Pi, and Codex transcripts plus aw
69
+ client logs. It skips sources matched by the local record's ignore list.
70
+ Native session turns are content-addressed, not signed, and carry exact
71
+ provenance. Projected aweb mail and chat keep their original message
72
+ signatures verbatim. Search the captured content locally with `oats recall`.
69
73
 
70
74
  ## Quick start
71
75
 
72
- Requires Node.js 22 or newer and tmux.
76
+ Follow [Run your first OATS team](docs/first-team.md) for the tested path:
77
+ install the kernel and runtimes, adopt a development configuration, select
78
+ an available harvester model, connect your team, and complete a real task
79
+ through review, harvest, and retirement.
73
80
 
74
81
  ```bash
75
82
  npm install -g @awebai/oats@latest
76
- pi install npm:@awebai/oats-pi@latest # only if you run agents in Pi
77
- ```
78
-
79
- Initialize a workspace and check it:
80
-
81
- ```bash
82
- cd my-workspace
83
- oats init
84
- oats doctor
85
- ```
86
-
87
- Create a specialist and put it to work:
88
-
89
- ```bash
90
- oats create backend-expert --type developers --repo . --work worktree
91
- oats spawn backend-expert --purpose implement --task "Add rate limiting to the public API"
92
- oats status --team
93
- oats retire <instance>
94
- ```
95
-
96
- Or adopt a complete reference configuration from an official package:
97
-
98
- ```bash
83
+ pi install npm:@awebai/oats-pi@latest
84
+ cd /path/to/project
99
85
  oats init --package oats.dev --config default
100
- oats install
101
86
  ```
102
87
 
103
- `oats init --package` acquires and exact-locks the full closure, validates the
104
- chosen template against its providers, writes it as your local
105
- `oats-config.yaml`, and records the adopted base so `oats config diff` and
106
- `oats config sync` can compare against it later.
88
+ Continue with the guide's model, team, and executable-trust setup before
89
+ spawning. Initialization acquires packages; it does not authenticate a
90
+ runtime or join a messaging team.
107
91
 
108
- Start capturing the turn record on this machine:
109
-
110
- ```bash
111
- oats setup
112
- oats recall "rate limiting"
113
- ```
114
-
115
- A Pi agent can also load the `oats-getting-started` skill and guide the setup.
92
+ See [the first-team example](docs/first-team-demo.md) for the real Pi and
93
+ Claude tasks behind the guide. Existing OAS users: start with
94
+ [the migration command](docs/migration-from-oas.md).
116
95
 
117
96
  ## How it works
118
97
 
@@ -166,7 +145,8 @@ template discovery curtailed while operator-configured extensions remain
166
145
  enabled. Claude Code keeps the operator's settings, skills, plugins, MCP,
167
146
  hooks, and memory, and OATS adds its canonical composed resources. The
168
147
  guarantee is an exact OATS-managed curriculum, not identical ambient behavior
169
- across providers.
148
+ across providers. Codex uses native instructions, skills and approval settings;
149
+ its launch support currently requires agents to check `aw` themselves for new messages.
170
150
 
171
151
  ### Configuration and layers
172
152
 
@@ -207,10 +187,13 @@ Bare `oats install` restores the exact lock and never advances source state.
207
187
 
208
188
  ## The turn record
209
189
 
210
- `packages/record` is the load-bearing layer. Every conversation an agent has
211
- is captured as signed turns in an append-only, content-addressed, replicated
212
- record with exact provenance, and searched locally through a SQLite full-text
213
- index. It has no runtime dependencies beyond Node.
190
+ `packages/record` is the load-bearing layer. On each machine where `oats setup`
191
+ has run, it captures Claude Code, Pi, and Codex transcripts plus aw client logs.
192
+ It skips sources matched by that record root's ignore list. Native session
193
+ turns are content-addressed, not signed, and carry exact provenance. Projected
194
+ aweb mail and chat keep their original message signatures verbatim. The
195
+ append-only record can be replicated and searched locally through a SQLite
196
+ full-text index. It has no runtime dependencies beyond Node.
214
197
 
215
198
  ```bash
216
199
  oats setup # install capture hooks and the background watcher
@@ -323,6 +306,7 @@ forms. Do not hand-edit the lock or installed stores.
323
306
  - [OATS Desktop](docs/desktop.md)
324
307
  - [Migration from OAS](docs/migration-from-oas.md)
325
308
  - [Release notes](docs/release-notes/)
309
+ - [Architecture proposal, 2026-09-03](docs/2026-09-03-architecture-proposal.md): components, contracts, and what may be replaced (proposal, not shipped behavior)
326
310
 
327
311
  ## Contributing
328
312
 
@@ -378,3 +362,9 @@ AGENTS.md, Agent Skills, and OKF.
378
362
  ## License
379
363
 
380
364
  [MIT](LICENSE) © 2026 OATS Framework
365
+
366
+ Session backends and unattended launches are described in
367
+ [execution targets](docs/execution-targets.md). Use `oats spawn <soul> --backend
368
+ herdr --yolo` for a Herdr-hosted unattended Codex/Claude session, or put
369
+ `yolo: true` in the scope's oats-config.yaml. Aweb owns shared event delivery;
370
+ terminal transport alone does not enable a messaging broker.
package/bin/oats.mjs CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
32
32
  findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, listCapabilityAgents, workspaceOf,
33
33
  ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
34
- spawnInstance, retireInstance, upsertLocalAgent, defaultRepo, RELATIONS,
34
+ spawnInstance, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS,
35
35
  } from "../lib/core.mjs";
36
36
  import {
37
37
  aggregateMissingRequirements, applyFromOasScope, beginRunJournal, discoverMigrationScopes, discoverOasScopes, discoverWorkspaceScopes, planFromOasScope,
@@ -39,6 +39,8 @@ import {
39
39
  assertNoSymlinkedParents, copyFileAtomic, writeFileAtomic,
40
40
  runRequirementInstall, selectConfigTemplate, validateConfigTemplate, writeAdoptedTemplate,
41
41
  } from "../lib/packages.mjs";
42
+ import { attachArgv, checkRemote, getServer, inspectRemote, listSnapshots, readServers, routeCommand, targetOf, validateServer, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
43
+ import { spawnSync as spawnSyncProc } from "node:child_process";
42
44
 
43
45
  const args = process.argv.slice(2);
44
46
  const cmd = args[0];
@@ -47,6 +49,15 @@ const flag = (name) => {
47
49
  const i = args.indexOf(`--${name}`);
48
50
  return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
49
51
  };
52
+ function yoloFlag() {
53
+ if (args.includes("--yolo") && args.includes("--no-yolo")) cmdFail("E_BAD_ARGS", "choose --yolo or --no-yolo, not both");
54
+ return args.includes("--yolo") ? true : args.includes("--no-yolo") ? false : undefined;
55
+ }
56
+ function valueFlag(name) {
57
+ const value = flag(name);
58
+ if (value === true) cmdFail("E_BAD_ARGS", `--${name} needs a value`);
59
+ return value;
60
+ }
50
61
  const die = (msg) => { console.error(`oats: ${msg}`); process.exit(1); };
51
62
  /** Resolve the --dir flag with central validation: a value-taking flag given
52
63
  * no value (flag() → true) is E_BAD_ARGS inside the JSON boundary, never an
@@ -2602,7 +2613,10 @@ function status() {
2602
2613
  console.log(` ${a.name}${a.kind === "local" ? " (local)" : ""} [work: ${a.work || "checkout"}, repo: ${a.repo || "?"}]`);
2603
2614
  if (a.description) console.log(` ${a.description}`);
2604
2615
  for (const i of a.instances) {
2605
- console.log(` • ${i.instance} ${i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
2616
+ console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
2617
+ }
2618
+ for (const f of a.retireFailures || []) {
2619
+ console.log(` ! deferred retirement of ${f.instance} FAILED${f.completedAt ? ` at ${f.completedAt}` : ""}: ${f.error || (f.incomplete || []).join("; ") || "see result file"} — retry with \`oats retire ${f.instance}\``);
2606
2620
  }
2607
2621
  }
2608
2622
  const defs = listAgentDefs(process.cwd());
@@ -2624,7 +2638,8 @@ function statusTeam() {
2624
2638
  if (!agents.length) { console.log(" (no agents)"); continue; }
2625
2639
  for (const a of agents) {
2626
2640
  console.log(` ${a.name}${a.kind === "local" ? " (local)" : ""}${a.description ? ` — ${a.description}` : ""}`);
2627
- for (const i of a.instances) console.log(` • ${i.instance} ${i.running ? "RUNNING" : "idle"}`);
2641
+ for (const i of a.instances) console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"}`);
2642
+ for (const f of a.retireFailures || []) console.log(` ! deferred retirement of ${f.instance} FAILED: ${f.error || (f.incomplete || []).join("; ") || "see result file"} — retry with \`oats retire ${f.instance}\``);
2628
2643
  }
2629
2644
  }
2630
2645
  }
@@ -2633,8 +2648,11 @@ function spawnCmd() {
2633
2648
  // JSON mode: contract envelope, stable error codes, stderr-only progress.
2634
2649
  const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
2635
2650
  const note = (msg) => (JSON_MODE ? console.error(msg) : console.log(msg));
2651
+ const yolo = yoloFlag();
2652
+ const backend = valueFlag("backend"), herdrSocket = valueFlag("herdr-socket");
2653
+ if (backend !== undefined && !["tmux", "herdr"].includes(backend)) bail("E_BAD_ARGS", "--backend must be tmux or herdr");
2636
2654
  const name = args[1];
2637
- if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace] [--work-dir <owner-work>] [--runtime pi|claude] [--model <m>] [--branch <b>] [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]");
2655
+ if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]");
2638
2656
  // Retired boundary flags (maintainer transport ruling): fail LOUDLY before
2639
2657
  // ANY side effect — including root discovery and local-agent upsert (an
2640
2658
  // --instructions-file spawn must not scaffold/overwrite a local soul before
@@ -2676,7 +2694,7 @@ function spawnCmd() {
2676
2694
  } else if (!agent || agent.kind === "local") {
2677
2695
  agent = upsertLocalAgent(root, {
2678
2696
  name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
2679
- repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"),
2697
+ repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo: yoloFlag(),
2680
2698
  });
2681
2699
  } else {
2682
2700
  bail("E_BAD_ARGS", `"${name}" is a persistent agent — spawn it without --instructions-file/--def-file`);
@@ -2725,7 +2743,7 @@ function spawnCmd() {
2725
2743
  r = spawnInstance(root, agent, {
2726
2744
  purpose: flag("purpose"), task: taskText, taskFile: taskFileFlag, relation, relativeTo, relativeRoot,
2727
2745
  repo: flag("repo") || agent.repo || defaultRepo(workspaceOf(root)) || defaultRepo(process.cwd()),
2728
- work: flag("work"), workDir: flag("work-dir"), runtime: flag("runtime"), model: flag("model"), branch: flag("branch"),
2746
+ work: flag("work"), workDir: flag("work-dir"), runtime: flag("runtime"), backend, herdrSocket, yolo, model: flag("model"), branch: flag("branch"),
2729
2747
  launch: !args.includes("--no-launch"),
2730
2748
  });
2731
2749
  } catch (e) {
@@ -2745,10 +2763,12 @@ function spawnCmd() {
2745
2763
  model: r.model || null, parent: r.parentInstance || null,
2746
2764
  sibling: r.siblingInstance || null, relation: r.relation || null,
2747
2765
  spawnOrigin: r.spawnOrigin, attach: r.attach,
2766
+ ...(r.sessionTarget ? { sessionTarget: r.sessionTarget } : {}),
2767
+ ...(r.yolo !== undefined ? { yolo: r.yolo } : {}),
2748
2768
  });
2749
2769
  return;
2750
2770
  }
2751
- console.log(`Spawned ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch}` : ""})${r.launched ? ` — tmux window "${r.tmux.window}"` : " — not launched"}`);
2771
+ console.log(`Spawned ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch}` : ""})${r.launched ? r.sessionTarget ? ` — Herdr pane "${r.sessionTarget.paneId}"` : ` — tmux window "${r.tmux.window}"` : " — not launched"}`);
2752
2772
  console.log(` home: ${shortPath(r.home)}`);
2753
2773
  if (!r.launched) console.log(` launch: (cd ${shortPath(r.home)} && ${r.command})`);
2754
2774
  for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
@@ -2768,6 +2788,16 @@ function retireCmd() {
2768
2788
  if (hit && resolve(hit.root) !== resolve(root)) { root = hit.root; console.log(`(cross-repo: instance homes at ${shortPath(root)})`); }
2769
2789
  }
2770
2790
  const r = retireInstance(root, name, { self: isSelf, deleteBranch: args.includes("--delete-branch"), keepDir: args.includes("--keep-dir"), force: args.includes("--force") });
2791
+ // Deferred self-retire: nothing has been inspected, run, or removed yet. The
2792
+ // caller's window dies first; a detached process then retires the instance
2793
+ // as an external operator and writes its outcome beside the home.
2794
+ if (r.deferred) {
2795
+ if (args.includes("--json")) { console.log(JSON.stringify(r, null, 2)); return; }
2796
+ console.log(`Retirement of ${r.retired} (agent ${r.agent}) is ${r.alreadyScheduled ? "already " : ""}scheduled — say any goodbyes now.`);
2797
+ console.log(` in ~${r.completesInSec}s a detached completion quiesces this runtime (that is what ends this window), preserves work, runs retire hooks and removes the home`);
2798
+ console.log(` if the completion fails, this window stays, the failure shows in \`oats status\` and at ${shortPath(r.resultPath)}, and \`oats retire ${r.retired}\` retries it`);
2799
+ return;
2800
+ }
2771
2801
  // Forced removal past an incomplete cleanup: the home is gone because the
2772
2802
  // operator said so, but the external state it owed is still out there and
2773
2803
  // nobody else will mention it again.
@@ -2795,30 +2825,56 @@ function retireCmd() {
2795
2825
  if (isSelf) console.log("This window dies in ~8s — say any goodbyes now.");
2796
2826
  }
2797
2827
 
2828
+ async function sessionCmd() {
2829
+ try {
2830
+ const home = flag("home");
2831
+ let result;
2832
+ if (args[1] === "attach") {
2833
+ if (JSON_MODE) throw Object.assign(new Error("session attach is interactive; omit --json"), { code: "E_BAD_ARGS" });
2834
+ process.exitCode = await attachInstanceSession(home);
2835
+ return;
2836
+ }
2837
+ if (args[1] === "inspect") result = inspectInstanceSession(home);
2838
+ else if (args[1] === "input") {
2839
+ const file = flag("text-file");
2840
+ if (file === true) throw Object.assign(new Error("--text-file needs a path"), { code: "E_BAD_ARGS" });
2841
+ if (!file && process.stdin.isTTY) throw Object.assign(new Error("provide --text-file or pipe input on stdin"), { code: "E_BAD_ARGS" });
2842
+ result = inputInstanceSession(home, readFileSync(file || 0, "utf8"));
2843
+ } else throw Object.assign(new Error("usage: oats session inspect|input|attach --home /absolute/home [--text-file path] [--json]"), { code: "E_BAD_ARGS" });
2844
+ if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
2845
+ } catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message); }
2846
+ }
2847
+
2798
2848
  async function paneCmd() {
2799
2849
  die("`oats pane` has been retired — the OATS Desktop app (packages/desktop) is the control panel now.");
2800
2850
  }
2801
2851
 
2802
2852
  function createCmd() {
2853
+ const yolo = yoloFlag();
2803
2854
  const name = args[1];
2804
- if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace] [--runtime pi|claude] [--model <m>] [--instructions-file <f>]");
2855
+ if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace] [--runtime pi|claude|codex] [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]");
2805
2856
  const local = args.includes("--local");
2806
2857
  const startDir = dirFlag();
2807
- // --local can BOOTSTRAP a deployment: with no agents/ or local-agents/ yet,
2808
- // anchor at the enclosing git repo (else the start dir) — people can use OATS
2809
- // with local agents alone.
2858
+ // `create` BOOTSTRAPS a deployment: with no agents/ or local-agents/ yet,
2859
+ // anchor at the enclosing git repo (else the start dir). It is the command
2860
+ // that populates the roster root, so it must not demand that the root
2861
+ // already exist — that demand was the first thing a new user hit after
2862
+ // `oats init` (a raw stack trace from ensureRoot). Local and committed souls
2863
+ // anchor the same way; writeSoul creates the directories.
2810
2864
  let root = findRoot(startDir);
2865
+ let bootstrapped = false;
2811
2866
  if (!root) {
2812
- if (!local) root = ensureRoot(startDir); // keeps the pointed error for committed souls
2813
- else root = join(defaultRepo(startDir) || resolve(startDir), "agents");
2867
+ root = join(defaultRepo(startDir) || resolve(startDir), "agents");
2868
+ bootstrapped = true;
2814
2869
  }
2815
2870
  const instrFile = flag("instructions-file");
2816
2871
  const r = coreCreateAgent(root, {
2817
2872
  name, local, description: flag("description"), type: flag("type"), repo: flag("repo") || defaultRepo(process.cwd()),
2818
- work: flag("work"), runtime: flag("runtime"), model: flag("model"),
2873
+ work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
2819
2874
  instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
2820
2875
  });
2821
- if (args.includes("--json")) { console.log(JSON.stringify(r, null, 2)); return; }
2876
+ if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
2877
+ if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
2822
2878
  console.log(`Created ${r.kind === "local" ? "LOCAL agent (uncommitted — soul lives in local-agents/, gitignored)" : "agent"} "${r.agent}" — soul at ${shortPath(r.soul)}`);
2823
2879
  console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then: oats spawn ${r.agent} --task "..."`);
2824
2880
  }
@@ -3062,7 +3118,10 @@ function versionCmd() {
3062
3118
  if (JSON_MODE) {
3063
3119
  // EXACT Desktop API v1 probe payload — one JSON object, nothing else on
3064
3120
  // stdout. Desktop accepts desktopApi === 1 and a compatible semver range.
3065
- console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1 }));
3121
+ // `remote`: the commands this kernel routes to a registered server with
3122
+ // --server; a Desktop gates its remote path on it (an older CLI without
3123
+ // the surface must fail closed with a reason, not an argument error).
3124
+ console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session"] }));
3066
3125
  return;
3067
3126
  }
3068
3127
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -3101,6 +3160,152 @@ async function experimentalCmd() {
3101
3160
  await import(url);
3102
3161
  }
3103
3162
 
3163
+ // ---------- servers: registry and remote routing (docs/execution-targets.md) ----------
3164
+ /** `oats server add|list|remove|check`. A registration is where and how:
3165
+ * an OpenSSH host alias, the remote workspace, the remote oats path. Keys
3166
+ * and passwords never enter it; ssh owns those. */
3167
+ function serverCmd() {
3168
+ const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
3169
+ const sub = args[1];
3170
+ const usage = "usage: oats server add <id> --ssh <host-alias> --workspace </abs/path> [--oats <path>] [--herdr <path>] [--path <dir:dir>] [--label <text>] [--replace] | list | remove <id> | check <id> [--json]";
3171
+ if (!["add", "list", "remove", "check"].includes(sub)) bail("E_USAGE", usage);
3172
+ let servers;
3173
+ try { servers = readServers(); } catch (e) { bail(e.code || "E_SERVERS_UNREADABLE", e.message); }
3174
+ if (sub === "list") {
3175
+ const rows = Object.entries(servers).map(([id, s]) => ({ id, ...s, target: targetOf({ id, ...s }), snapshots: listSnapshots(id).length }));
3176
+ if (JSON_MODE) { jsonOk({ file: SERVERS_FILE(), servers: rows }); return; }
3177
+ if (!rows.length) { console.log(`no servers registered (${shortPath(SERVERS_FILE())}) — add one with \`oats server add <id> --ssh <alias> --workspace </path>\``); return; }
3178
+ for (const r of rows) console.log(` ${r.id}${r.label ? ` ${r.label}` : ""}\n ssh ${r.sshHost} workspace ${r.workspace} oats ${r.target.oatsPath}${r.target.herdrPath ? ` herdr ${r.target.herdrPath}` : ""}${r.snapshots ? ` (${r.snapshots} remote instance${r.snapshots === 1 ? "" : "s"} spawned from here)` : ""}`);
3179
+ return;
3180
+ }
3181
+ const id = args[2];
3182
+ if (!id || id.startsWith("--")) bail("E_USAGE", usage);
3183
+ if (sub === "add") {
3184
+ const val = (name) => { const v = flag(name); return v === true ? bail("E_BAD_ARGS", `--${name} needs a value`) : v; };
3185
+ const entry = { sshHost: val("ssh"), workspace: val("workspace") };
3186
+ for (const [k, f] of [["oatsPath", "oats"], ["herdrPath", "herdr"], ["path", "path"], ["label", "label"]]) { const v = val(f); if (v !== undefined) entry[k] = v; }
3187
+ if (!entry.sshHost || !entry.workspace) bail("E_USAGE", usage);
3188
+ try { validateServer(id, entry); } catch (e) { bail(e.code, e.message); }
3189
+ if (servers[id] && !args.includes("--replace")) bail("E_SERVER_EXISTS", `server ${id} is already registered (pass --replace to overwrite; existing remote instances keep the route they were spawned with)`);
3190
+ servers[id] = entry;
3191
+ writeServers(servers);
3192
+ if (JSON_MODE) { jsonOk({ id, ...entry, file: SERVERS_FILE() }); return; }
3193
+ console.log(`Registered server ${id} → ssh ${entry.sshHost}, workspace ${entry.workspace} (${shortPath(SERVERS_FILE())}). Verify it with \`oats server check ${id}\`.`);
3194
+ return;
3195
+ }
3196
+ if (sub === "remove") {
3197
+ if (!servers[id]) bail("E_SERVER_UNKNOWN", `no server registered as ${id}`);
3198
+ const snaps = listSnapshots(id);
3199
+ delete servers[id];
3200
+ writeServers(servers);
3201
+ if (JSON_MODE) { jsonOk({ removed: id, remoteInstancesStillTracked: snaps.map((s) => s.instance) }); return; }
3202
+ console.log(`Removed server ${id}${snaps.length ? ` — ${snaps.length} remote instance(s) spawned from it keep their snapshots and can still be retired with --server ${id}` : ""}`);
3203
+ return;
3204
+ }
3205
+ // check: reachability and compatibility, no mutation
3206
+ let server; try { server = getServer(id); } catch (e) { bail(e.code, e.message); }
3207
+ const target = targetOf(server);
3208
+ try {
3209
+ const remote = checkRemote(target);
3210
+ const status = routeCommand(id, "status", [], { server });
3211
+ const agents = status.envelope.ok ? (status.envelope.result.agents || []).length : undefined;
3212
+ if (JSON_MODE) { jsonOk({ id, target, remote, workspaceReachable: !!status.envelope.ok, agents, error: status.envelope.ok ? undefined : status.envelope.error }); return; }
3213
+ console.log(`${id}: ssh ${target.sshHost} ok, remote oats ${remote.version} (envelope v${remote.schemaVersion})`);
3214
+ console.log(status.envelope.ok ? ` workspace ${target.workspace}: ${agents} agent(s)` : ` workspace ${target.workspace}: ${status.envelope.error?.message || "not usable"}`);
3215
+ if (!status.envelope.ok) process.exit(1);
3216
+ } catch (e) { bail(e.code || "E_SSH", e.message); }
3217
+ }
3218
+
3219
+ /** `oats <spawn|retire|status> --server <id> ...`: run the command on the
3220
+ * registered server's installed oats, same arguments, same envelope. The
3221
+ * local side only routes and keeps the route snapshot per remote instance. */
3222
+ function serverRouteCmd() {
3223
+ const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
3224
+ const id = flag("server");
3225
+ if (id === true || !id) bail("E_BAD_ARGS", "--server needs a registered server id (oats server list)");
3226
+ if (flag("dir") !== undefined || args.some((a) => a.startsWith("--dir="))) bail("E_BAD_ARGS", "--dir cannot be combined with --server: the remote workspace comes from the server registration");
3227
+ // Interactive viewer: `oats session attach --server <id> --instance <name>`
3228
+ // (or --home </abs/remote/home>) runs the execution host's own attach
3229
+ // through an ssh PTY with this terminal's stdio; nothing is captured.
3230
+ if (cmd === "session") {
3231
+ const addr = { instance: flag("instance") === true ? undefined : flag("instance"), home: flag("home") === true ? undefined : flag("home") };
3232
+ if (args[1] === "inspect") {
3233
+ // Desktop preflight before a remote attach: the execution host's own
3234
+ // inspect, relayed as its envelope; a failure is a failure, nonzero.
3235
+ let out;
3236
+ try { out = inspectRemote(id, addr); } catch (e) { bail(e.code || "E_SSH", e.message); }
3237
+ if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
3238
+ if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
3239
+ if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "inspect failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
3240
+ const r = out.envelope.result;
3241
+ console.log(`${r.instance || r.home} on ${id}: ${r.present ? `present, ${r.state || "unknown"}` : "not present"}${r.backend ? ` (${r.backend})` : ""}`);
3242
+ return;
3243
+ }
3244
+ if (args[1] !== "attach") bail("E_USAGE", "--server routes `session inspect` and `session attach`; input runs on the execution host (the wake broker calls it there)");
3245
+ let route;
3246
+ try { route = attachArgv(id, addr, { skipVersionCheck: args.includes("--print") }); }
3247
+ catch (e) { bail(e.code || "E_BAD_ARGS", e.message); }
3248
+ if (args.includes("--print")) { console.log(route.argv.map(shellQuote).join(" ")); return; }
3249
+ const r = spawnSyncProc(route.argv[0], route.argv.slice(1), { stdio: "inherit" });
3250
+ process.exit(r.status ?? 1);
3251
+ }
3252
+ // Everything after the command word travels, minus the routing flags; a
3253
+ // local --task-file is read here and travels as --task text, since the
3254
+ // remote cannot read this machine's files.
3255
+ const rest = [];
3256
+ for (let i = 1; i < args.length; i++) {
3257
+ const a = args[i];
3258
+ if (a === "--server") { i++; continue; }
3259
+ if (a === "--json") continue;
3260
+ if (a === "--task-file") {
3261
+ const f = args[++i];
3262
+ if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--task-file needs a path");
3263
+ if (!existsSync(f)) bail("E_BAD_ARGS", `task file not found: ${f}`);
3264
+ rest.push("--task", readFileSync(f, "utf8"));
3265
+ continue;
3266
+ }
3267
+ rest.push(a);
3268
+ }
3269
+ let routed;
3270
+ try { routed = routeCommand(id, cmd, rest); }
3271
+ catch (e) { bail(e.code || "E_SSH", e.message); }
3272
+ const { envelope, stderr } = routed;
3273
+ if (stderr && stderr.trim()) process.stderr.write(stderr.endsWith("\n") ? stderr : stderr + "\n");
3274
+ if (JSON_MODE) { console.log(JSON.stringify(envelope, null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
3275
+ if (!envelope.ok && !(cmd === "retire" && envelope.result)) die(`${id}: ${envelope.error?.message || "remote command failed"} (${envelope.error?.code || "E_REMOTE"})`);
3276
+ const r = envelope.result;
3277
+ const target = r.target || {};
3278
+ if (cmd === "spawn") {
3279
+ console.log(`Spawned ${r.instance} on ${id} (${r.work}${r.branch ? `, branch ${r.branch}` : ""})${r.launched ? ` — tmux window "${r.tmux?.window}" on ${r.target.sshHost}` : " — not launched"}`);
3280
+ console.log(` remote home: ${r.home}`);
3281
+ console.log(` route snapshot: ${shortPath(r.snapshot)}`);
3282
+ for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
3283
+ console.log(` attach: ssh -t ${r.target.sshHost} tmux attach -t ${r.tmux?.session || "oats"}`);
3284
+ } else if (cmd === "retire") {
3285
+ // Everything the local retireCmd tells the operator, for a remote home
3286
+ // they cannot see: forced-incomplete state now theirs to remove by hand
3287
+ // there, work preserved there and where, and incomplete cleanup.
3288
+ if (r.forcedIncomplete) {
3289
+ console.error(`Removed ${r.retired} on ${id} under --force with cleanup INCOMPLETE — this external state was NOT cleaned up and is now yours to remove by hand on ${target.sshHost}:`);
3290
+ for (const f of r.forcedIncomplete) console.error(` ${f}`);
3291
+ }
3292
+ console.log(`Retired ${r.retired} on ${id}${r.deferred ? " (deferred completion scheduled there)" : ""}${r.rollbackIncomplete ? " — cleanup INCOMPLETE on the server, home retained there" : ""}`);
3293
+ for (const recovery of r.workRecoveries || (r.workRecovery ? [r.workRecovery] : [])) {
3294
+ console.log(`Work that was not committed has been preserved on ${target.sshHost}: ${(recovery.classes || []).join(", ")}`);
3295
+ console.log(` ${recovery.path}`);
3296
+ }
3297
+ if (r.rollbackIncomplete) { for (const f of r.rollbackIncomplete) console.error(` ${f}`); console.error(`Fix the cause there and re-run \`oats retire ${r.retired} --server ${id}\`.`); process.exit(1); }
3298
+ } else {
3299
+ console.log(`oats status — server ${id} (ssh ${r.target.sshHost}, workspace ${r.target.workspace})\n`);
3300
+ for (const a of r.agents || []) {
3301
+ console.log(` ${a.name} [work: ${a.work || "checkout"}, repo: ${a.repo || "?"}]`);
3302
+ for (const i of a.instances || []) console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"}`);
3303
+ }
3304
+ const snaps = r.snapshots || [];
3305
+ if (snaps.length) console.log(`\n spawned from this machine: ${snaps.map((s) => s.instance).join(", ")}`);
3306
+ }
3307
+ }
3308
+
3104
3309
  // ---------- main ----------
3105
3310
  // Typed config-shape failures are DEPLOYMENT state the operator can fix, not
3106
3311
  // kernel bugs: an unsafe mapping key anywhere in the visible config chain is
@@ -3116,7 +3321,9 @@ async function experimentalCmd() {
3116
3321
  // blame` pointing at the commit that last changed each command.
3117
3322
  const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
3118
3323
  try {
3119
- if (cmd === "doctor") {
3324
+ if (flag("server") !== undefined && ["spawn", "retire", "status", "session"].includes(cmd)) serverRouteCmd();
3325
+ else if (cmd === "server") serverCmd();
3326
+ else if (cmd === "doctor") {
3120
3327
  const doctorDir = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
3121
3328
  args.includes("--json") ? doctorJson(doctorDir) : doctor(doctorDir);
3122
3329
  }
@@ -3137,6 +3344,7 @@ else if (cmd === "pane") await paneCmd();
3137
3344
  else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
3138
3345
  // Same rule as the inner catch: a typed CLI failure surfaces with its own code
3139
3346
  // through the shared boundary, never re-badged as a spawn-mechanism failure.
3347
+ else if (cmd === "session") await sessionCmd();
3140
3348
  else if (cmd === "spawn") { try { spawnCmd(); } catch (e) { if (TYPED_CLI_FAILURES.has(e?.code)) throw e; if (JSON_MODE) jsonFail("E_SPAWN_FAILED", e.message || e); throw e; } }
3141
3349
  else if (cmd === "retire") retireCmd();
3142
3350
  else if (cmd === "create") createCmd();
@@ -3158,24 +3366,36 @@ Usage:
3158
3366
  Desktop CLI API v1 probe payload
3159
3367
  oats status [--json] agents, souls, running instances
3160
3368
  oats status --team [--json] whole-team roster across the team scope's repos
3369
+ oats server add <id> --ssh <alias> register another machine's OATS (OpenSSH alias,
3370
+ --workspace </abs/path> [--oats <p>] remote workspace, remote oats path; no keys stored;
3371
+ [--path <dir:dir>] --path = dirs prepended to the remote PATH, e.g. ~/.local/bin)
3372
+ oats server list|remove <id>|check <id> registry; check = reachability + version, no mutation
3373
+ oats spawn|retire|status ... --server <id> run that command on the server's installed oats
3374
+ (same flags, same envelope; the saved route per
3375
+ remote instance lives under ~/.oats/remote/)
3376
+ oats session inspect|attach --server <id> inspect (envelope) or attach a viewer (ssh PTY) for a
3377
+ --instance <name> | --home <abs> remote instance over its saved route (--print shows
3378
+ attach); the server needs oats 0.22.2 or later
3161
3379
  oats create <name> [--local] create an agent soul; --local = full
3162
3380
  [--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
3163
- [--work <mode>] [--runtime pi|claude] gitignored; same memory + lifecycle)
3164
- [--model <m>] [--instructions-file <f>]
3165
- oats spawn <agent> [--task <text>] spawn an instance (tmux; --no-launch
3381
+ [--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
3382
+ [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
3383
+ oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
3384
+ oats spawn <agent> [--task <text>] spawn an instance (tmux/Herdr; --no-launch
3166
3385
  [--purpose <slug>] [--repo <r>] = scaffold only); --instructions-file/
3167
3386
  [--parent <instance>] --def-file creates a local agent;
3168
3387
  [--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
3169
3388
  [--relative-to <instance>] new instance to an existing one; --parent X
3170
3389
  [--relative-root <agents-root>] disambiguates same-named team anchors
3171
3390
  [--work worktree|checkout|attached|workspace] = sugar for --relative-to X --relation
3172
- [--work-dir <owner-work>] [--runtime pi|claude] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
3391
+ [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
3173
3392
  [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]
3174
3393
  with team: declared, unknown local souls
3175
3394
  resolve across the team scope's repos
3176
3395
  oats retire <instance> [--force] retire an instance (window, hooks,
3177
3396
  [--self] [--delete-branch] worktree, home); --self = retire the
3178
- [--keep-dir] [--json] CALLING instance (delayed window kill)
3397
+ [--keep-dir] [--json] CALLING instance: the window dies, then
3398
+ a detached external retirement runs
3179
3399
  oats doctor [dir] [--soul <name>] [--json] resolved targets, trust, requirements;
3180
3400
  --soul shows final composed AGENTS.md
3181
3401
  oats update [--check] [--yes] check npm for a newer kernel+pi bridge and
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OATS Framework
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ {
2
+ "package": "oats.authoring",
3
+ "version": "1.0.0",
4
+ "description": "Official additive OATS guidance for capability, skill, and soul authoring.",
5
+ "compatibility": {
6
+ "oats": ">=0.19.0"
7
+ },
8
+ "capabilities": [
9
+ "."
10
+ ]
11
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "capability": "oats.authoring",
3
3
  "version": "1.0.0",
4
- "compatibility": { "oats": ">=0.6.2" },
4
+ "compatibility": { "oats": ">=0.19.0" },
5
5
  "description": "Additive framework-authoring guidance for capability packages, agent skills, and souls.",
6
6
  "requires": [],
7
7
  "skills": [
8
- "../../skills/integration-authoring",
9
- "../../skills/skill-craft",
10
- "../../skills/soul-craft"
8
+ "skills/integration-authoring",
9
+ "skills/skill-craft",
10
+ "skills/soul-craft"
11
11
  ]
12
12
  }