@awebai/oats 0.29.4 → 0.30.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.
Files changed (224) hide show
  1. package/README.md +12 -6
  2. package/bin/oats.mjs +194 -50
  3. package/capabilities/oats-aweb/bin/oats-aweb.mjs +538 -204
  4. package/capabilities/oats-aweb/injects/aweb.md +1 -1
  5. package/capabilities/oats-aweb/lib/binding-wire.mjs +31 -22
  6. package/capabilities/oats-aweb/oats.json +5 -12
  7. package/capabilities/oats-aweb/skills/VENDORED.md +4 -4
  8. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +1 -1
  9. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +83 -13
  10. package/capabilities/oats-code-review/injects/reviewer.md +26 -0
  11. package/capabilities/oats-code-review/oats.json +16 -0
  12. package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +66 -0
  13. package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +30 -0
  14. package/capabilities/oats-code-review/skills/security-review/SKILL.md +56 -0
  15. package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +34 -0
  16. package/capabilities/oats-developer/injects/developer.md +38 -0
  17. package/capabilities/oats-developer/oats.json +17 -0
  18. package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +43 -0
  19. package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +47 -0
  20. package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +65 -0
  21. package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +37 -0
  22. package/capabilities/oats-developer/skills/worktrees/SKILL.md +36 -0
  23. package/capabilities/oats-engineering-expert/injects/expert.md +37 -0
  24. package/capabilities/oats-engineering-expert/oats.json +17 -0
  25. package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +37 -0
  26. package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +52 -0
  27. package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +50 -0
  28. package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +53 -0
  29. package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +49 -0
  30. package/capabilities/oats-okf/bin/oats-okf.mjs +8 -4
  31. package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
  32. package/capabilities/oats-okf/lib/inspection.mjs +26 -7
  33. package/capabilities/oats-okf/lib/sources.mjs +16 -2
  34. package/capabilities/oats-okf/lib/worker.mjs +5 -16
  35. package/capabilities/oats-okf/oats.json +6 -3
  36. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
  37. package/capabilities/oats-okf-harvest/oats.json +3 -3
  38. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
  39. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +2 -2
  40. package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
  41. package/capabilities/oats-okf-maintenance/oats.json +2 -2
  42. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +1 -1
  43. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
  44. package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
  45. package/capabilities/oats-workspace-experts/oats.json +9 -0
  46. package/docs/capabilities.md +160 -171
  47. package/docs/capability-manifest.schema.json +6 -11
  48. package/docs/configuration.md +213 -64
  49. package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
  50. package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
  51. package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
  52. package/docs/design/2026-09-27-team-model-v2.md +97 -117
  53. package/docs/design/2026-09-28-automations-trust.md +38 -0
  54. package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
  55. package/docs/design/HISTORY.md +65 -0
  56. package/docs/design/README.md +23 -54
  57. package/docs/desktop-cli-api.md +1787 -1777
  58. package/docs/desktop.md +30 -91
  59. package/docs/execution-targets.md +146 -292
  60. package/docs/first-team.md +31 -17
  61. package/docs/implementation.md +76 -288
  62. package/docs/integrations.md +118 -320
  63. package/docs/knowledge-capability-authoring.md +25 -52
  64. package/docs/knowledge-reference/acceptance.md +3 -3
  65. package/docs/knowledge-reference/adoption.md +1 -1
  66. package/docs/knowledge-reference/harvester.md +2 -2
  67. package/docs/knowledge-reference/package-craft.md +3 -3
  68. package/docs/knowledge-reference/provider-mapping.md +3 -6
  69. package/docs/knowledge-reference/reader-capture.md +3 -3
  70. package/docs/knowledge-theory.md +62 -166
  71. package/docs/knowledge.md +225 -404
  72. package/docs/layers.md +42 -97
  73. package/docs/oats-local.schema.json +58 -5
  74. package/docs/oats-membership.schema.json +1 -8
  75. package/docs/oats-package.schema.json +5 -5
  76. package/docs/oats-workspace.schema.json +8 -22
  77. package/docs/official-catalog.md +25 -28
  78. package/docs/packages.md +45 -63
  79. package/docs/plans/0.30-close-out.md +61 -0
  80. package/docs/release-lane.md +77 -0
  81. package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
  82. package/docs/release-notes/v0.19.0.md +48 -147
  83. package/docs/release-notes/v0.19.1.md +2 -3
  84. package/docs/release-notes/v0.19.3.md +2 -15
  85. package/docs/release-notes/v0.20.0.md +0 -15
  86. package/docs/release-notes/v0.22.0.md +71 -138
  87. package/docs/release-notes/v0.22.1.md +42 -90
  88. package/docs/release-notes/v0.22.10.md +1 -1
  89. package/docs/release-notes/v0.22.11.md +1 -47
  90. package/docs/release-notes/v0.22.12.md +4 -13
  91. package/docs/release-notes/v0.22.13.md +1 -42
  92. package/docs/release-notes/v0.22.14.md +3 -11
  93. package/docs/release-notes/v0.22.15.md +1 -46
  94. package/docs/release-notes/v0.22.16.md +6 -8
  95. package/docs/release-notes/v0.22.18.md +1 -99
  96. package/docs/release-notes/v0.22.19.md +3 -14
  97. package/docs/release-notes/v0.22.2.md +6 -15
  98. package/docs/release-notes/v0.22.3.md +0 -1
  99. package/docs/release-notes/v0.22.4.md +1 -14
  100. package/docs/release-notes/v0.22.5.md +2 -12
  101. package/docs/release-notes/v0.22.6.md +0 -3
  102. package/docs/release-notes/v0.23.0.md +9 -25
  103. package/docs/release-notes/v0.23.1.md +9 -25
  104. package/docs/release-notes/v0.23.2.md +2 -4
  105. package/docs/release-notes/v0.24.0.md +56 -97
  106. package/docs/release-notes/v0.24.1.md +7 -11
  107. package/docs/release-notes/v0.24.10.md +34 -45
  108. package/docs/release-notes/v0.24.11.md +12 -20
  109. package/docs/release-notes/v0.24.12.md +35 -48
  110. package/docs/release-notes/v0.24.13.md +34 -41
  111. package/docs/release-notes/v0.24.2.md +9 -13
  112. package/docs/release-notes/v0.24.3.md +7 -11
  113. package/docs/release-notes/v0.24.4.md +6 -6
  114. package/docs/release-notes/v0.24.5.md +6 -10
  115. package/docs/release-notes/v0.24.6.md +2 -5
  116. package/docs/release-notes/v0.24.7.md +46 -75
  117. package/docs/release-notes/v0.24.8.md +58 -96
  118. package/docs/release-notes/v0.24.9.md +38 -54
  119. package/docs/release-notes/v0.25.0.md +59 -76
  120. package/docs/release-notes/v0.25.1.md +57 -81
  121. package/docs/release-notes/v0.25.2.md +51 -70
  122. package/docs/release-notes/v0.25.3.md +11 -13
  123. package/docs/release-notes/v0.25.4.md +9 -13
  124. package/docs/release-notes/v0.25.5.md +3 -5
  125. package/docs/release-notes/v0.25.6.md +20 -29
  126. package/docs/release-notes/v0.25.7.md +5 -7
  127. package/docs/release-notes/v0.25.8.md +26 -39
  128. package/docs/release-notes/v0.26.0.md +175 -646
  129. package/docs/release-notes/v0.27.0.md +4 -5
  130. package/docs/release-notes/v0.27.1.md +4 -6
  131. package/docs/release-notes/v0.27.2.md +1 -1
  132. package/docs/release-notes/v0.28.0.md +57 -124
  133. package/docs/release-notes/v0.29.0.md +89 -208
  134. package/docs/release-notes/v0.29.1.md +1 -1
  135. package/docs/release-notes/v0.29.2.md +3 -4
  136. package/docs/release-notes/v0.30.0.md +205 -0
  137. package/docs/schedules.md +280 -363
  138. package/docs/servers.md +99 -117
  139. package/docs/soul.schema.json +2 -9
  140. package/docs/souls-and-instances.md +145 -158
  141. package/docs/workspaces.md +132 -215
  142. package/lib/automations.mjs +21 -6
  143. package/lib/core.mjs +226 -74
  144. package/lib/instance-events.mjs +1 -1
  145. package/lib/instance-inspect.mjs +109 -34
  146. package/lib/instance-lifecycle.mjs +14 -1
  147. package/lib/instance-resolution.mjs +26 -27
  148. package/lib/launch-preference.mjs +87 -0
  149. package/lib/materialize.mjs +3 -3
  150. package/lib/resolve.mjs +29 -87
  151. package/lib/schedule.mjs +1 -1
  152. package/lib/teams-verbs.mjs +195 -0
  153. package/lib/teams.mjs +190 -0
  154. package/lib/triggers.mjs +2 -2
  155. package/lib/workspace.mjs +54 -147
  156. package/package-catalog.json +9 -15
  157. package/package.json +1 -1
  158. package/skills/oats-getting-started/SKILL.md +25 -13
  159. package/capabilities/oats-review/injects/review.md +0 -69
  160. package/capabilities/oats-review/oats.json +0 -10
  161. package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
  162. package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
  163. package/docs/conventions.md +0 -90
  164. package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
  165. package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
  166. package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
  167. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
  168. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
  169. package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
  170. package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
  171. package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
  172. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
  173. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
  174. package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
  175. package/docs/design/2026-09-15-captured-dispatch.md +0 -127
  176. package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
  177. package/docs/design/2026-09-15-package-preparation.md +0 -100
  178. package/docs/design/2026-09-15-portable-data-contract.md +0 -121
  179. package/docs/design/2026-09-15-portable-declarations.md +0 -189
  180. package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
  181. package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
  182. package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
  183. package/docs/design/2026-09-15-source-observation.md +0 -119
  184. package/docs/design/2026-09-16-captured-admission.md +0 -77
  185. package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
  186. package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
  187. package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
  188. package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
  189. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
  190. package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
  191. package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
  192. package/docs/design/2026-09-16-portable-onboarding.md +0 -179
  193. package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
  194. package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
  195. package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
  196. package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
  197. package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
  198. package/docs/design/2026-09-17-captured-native-start.md +0 -58
  199. package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
  200. package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
  201. package/docs/design/2026-09-17-public-captured-start.md +0 -108
  202. package/docs/design/2026-09-17-public-prepare-request.md +0 -90
  203. package/docs/design/2026-09-18-captured-pi-host.md +0 -205
  204. package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
  205. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
  206. package/docs/design/2026-09-20-redesign-program-board.md +0 -142
  207. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
  208. package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
  209. package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
  210. package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
  211. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
  212. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
  213. package/docs/design/2026-09-24-phase-d-plan.md +0 -305
  214. package/docs/design/2026-09-25-teams-contract.md +0 -258
  215. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
  216. package/docs/design/desktop-ux-plan.md +0 -362
  217. package/docs/design/launch-configurations.md +0 -168
  218. package/docs/design/okf-mirror-provenance.md +0 -105
  219. package/docs/design/operations-contract.md +0 -141
  220. package/docs/oats-member.schema.json +0 -38
  221. package/skills/integration-authoring/SKILL.md +0 -84
  222. package/skills/oats-support/SKILL.md +0 -79
  223. package/skills/skill-craft/SKILL.md +0 -109
  224. package/skills/soul-craft/SKILL.md +0 -116
@@ -1,1944 +1,1954 @@
1
- # Desktop CLI API v1
1
+ # Desktop CLI API
2
2
 
3
- The contract between the OATS Desktop app and the `oats` CLI. Desktop never
4
- imports kernel code; it shells out (via `execFile`, argv, absolute binary — no
5
- shell) to a discovered `oats` and speaks this JSON protocol. **API version, not
6
- source adjacency, is authoritative.**
3
+ The JSON contract between the `oats` CLI and the OATS Desktop.
7
4
 
8
- ## Probe
5
+ ## Scope
9
6
 
10
- ```
11
- oats version --json
12
- ```
7
+ The Desktop never imports kernel code. Its server runs a discovered `oats`
8
+ binary with `execFile` (absolute path, argv, no shell) and decodes the JSON it
9
+ prints, strictly. This page describes one current shape per command, as the
10
+ kernel emits it.
11
+
12
+ - **Versioning is by capability, never by version string.** The probe lists
13
+ `features` and per-API integers. Gate every read and mutation on them; an
14
+ absent feature means the kernel cannot do it.
15
+ - **A document carries its own integer** where one exists (`operationsApi`,
16
+ `readinessApi`, `eventsApi`, …). Dispatch on the payload's integer.
17
+ - **The envelope is authoritative over this prose.** If a captured payload
18
+ and this page disagree, this page is the bug.
19
+ - **Shapes are closed.** The Desktop decodes many documents with exact key
20
+ sets, so a new key is a contract change and is announced here first.
21
+
22
+ Conventions: paths are absolute; a `commit` is a full 40-hex id; `integrity`
23
+ and `digest` are `sha256-<hex>`; times are ISO-8601 UTC; repository keys are
24
+ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
25
+ `/w` as the deployment and shorten ids and digests with `…`.
13
26
 
14
- prints exactly one JSON object on stdout:
27
+ ## The probe
28
+
29
+ `oats version --json` prints one object (not an envelope):
15
30
 
16
31
  ```json
17
- {"schemaVersion":1,"name":"@awebai/oats","version":"<installed version>","desktopApi":1}
32
+ {"schemaVersion":1,"name":"@awebai/oats","version":"0.30.0","desktopApi":1,
33
+ "harnesses":["pi","claude","codex"],"sessionBackends":["tmux","herdr"],"launchOptions":["yolo"],
34
+ "remote":["spawn","retire","status","session","session-start","session-restart","launch-config","roster","harvest","schedule","session-upload","operations"],
35
+ "features":["retire-home","session-start","session-restart","launch-config","schedule","session-upload","operations","instance-git",
36
+ "instance-git-remote","souls-declarations","lifecycle-plans","retire-retention","readiness","spawn-preview","instance-events",
37
+ "instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
38
+ "workspace-v2","instance-modules","spawn-provider-payload","served-identity","packages-no-approval","spawn-name","settings-origins",
39
+ "team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference"],
40
+ "automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
41
+ "readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2}
18
42
  ```
19
43
 
20
- `version` is the installed package's exact semver (e.g. `0.20.0`).
21
- The Desktop accepts `desktopApi === 1` and gates on the kernel feature
22
- `packages-no-approval` (semver range `>=0.25.8 <0.30.0`, spelled once in
23
- `packages/desktop/cli-locator.mjs` `ACCEPT_RANGE`: the floor admits the
24
- main-branch kernel before 0.26.0 was tagged; the feature fences are the real gate,
25
- and 0.29's reads are gated on `automations` and `desktop-facts`).
26
- Earlier bands were `>=0.25.8 <0.29.0` (Desktop 0.28), `<0.28.0` (Desktop 0.27),
27
- `<0.27.0` (Desktop 0.26), `>=0.22.0 <0.26.0` (Desktop 0.25) and `>=0.22.0 <0.24.0`
28
- (Desktop 0.23). It does not establish complete
29
- UI, backend, plugin, retirement or recovery parity; capability checks and explicit
30
- refusals below remain authoritative.
31
-
32
- Optional features are negotiated from the probe's `features` array. Starting
33
- an existing home requires `session-start`; named launch configurations and
34
- harness/permission overrides require `launch-config`; restarting a running
35
- home also requires `session-restart`. Desktop checks the corresponding
36
- `remote` entries before offering these operations for a server. The router
37
- then probes the execution host before sending a mutation. An absent feature
38
- means an update is needed; it is not inferred from the version number.
39
-
40
- The band is widened one kernel minor at a time, after confirming this v1
41
- surface is unchanged, and always admits the kernel published by the same
42
- release — Desktop and the CLI are built from one tag, so a band excluding its
43
- own kernel would degrade the shipped app to observation-only. Prereleases are
44
- never accepted.
45
-
46
- ## Envelope
47
-
48
- Every other `--json` command emits **exactly one JSON object on stdout** and
49
- no progress prose (progress goes to stderr):
50
-
51
- - success (exit 0): `{"schemaVersion":1,"ok":true,"result":{...}}`
52
- - failure (nonzero exit): `{"schemaVersion":1,"ok":false,"error":{"code":"...","message":"..."}}`
53
-
54
- Either may also carry `"warnings":[…]` (0.27.0+), present only when there is
55
- something to say. The one warning so far is `deprecated-runtime-name` (see
56
- [the harness rename](#the-harness-rename-feature-harness-oats-0270)). `oats status
57
- --json`, whose document is not an envelope, carries the same `warnings` beside
58
- its `problems`. In text mode the warning is one `oats: warning: …` line on stderr.
59
-
60
- ## Flags
61
-
62
- A kernel command reads `--flag=value` exactly as `--flag value`, with the
63
- same validation (0.27.3+; before, the inline form was silently ignored, so
64
- `--harness=claude` spawned the default harness). The value is everything after
65
- the first `=`. An empty `--flag=` is `E_BAD_ARGS` ("`--flag=` needs a value"),
66
- and so is a value on a switch: `--yolo=false` is refused and never turns
67
- yolo on. A capability command's own flags belong to its provider. They are
68
- forwarded exactly as typed; the kernel reads only its dispatch flag (`--soul`)
69
- in either form. There is no feature string: a caller that must work with
70
- older kernels uses the spaced form.
71
-
72
- ## The harness rename (feature `harness`, OATS 0.27.0)
73
-
74
- What starts an instance (pi, claude or codex) is its **harness**. 0.27.0 renames
75
- the kernel's `runtime` to `harness` everywhere it means that:
76
-
77
- - Outputs speak only the new names.
78
- - Every input written before 0.27.0 still works. The rule is read either,
79
- write new: the old spelling is read as the new one, and the next write
80
- records the new one.
81
- - A command that read an old spelling answers **one** warning:
82
- `{"code":"deprecated-runtime-name","key":"runtime","replacement":"harness","sources":[…],"message":"…"}`.
83
- `sources` names each place it read the old spelling (a flag, a file and its
84
- key, a home).
85
- - A pair that disagrees is refused rather than guessed, for example
86
- `--harness pi --runtime claude`, or both keys with different values.
87
- - A later release drops the old spellings.
88
-
89
- Gate on the feature `harness`. A kernel without it speaks the old names: the
90
- routed commands (`--server`) already translate for such a host, sending
91
- `--runtime` and `runtime` keys to it and reading its `runtimes` list.
92
-
93
- | Surface | Before 0.27.0 | 0.27.0 | Old spelling still accepted? |
94
- |---|---|---|---|
95
- | `oats version --json` | `runtimes: [pi, claude, codex]` | `harnesses: [...]`, feature `harness` | **Dropped**: no `runtimes` alias; gate on the feature |
96
- | Flag on `spawn` (and `--preview`), `session start`/`restart`, `launch-config preview`, and their `--server` forms | `--runtime <h>` | `--harness <h>` | Yes, with the warning (the okf 2.1.5 harvest worker passes `--runtime` to spawn). Both flags disagreeing → `E_BAD_ARGS` |
97
- | `oats status --json`: `agents[]` rows (soul default) and `agents[].instances[]` rows | `runtime` | `harness` | Output only |
98
- | `oats status --json`: instance rows' `composition.materialized` | `runtimePackages`, `runtimePosture` | `harnessPackages`, `harnessPosture` | Output only |
99
- | `oats inspect --json` `souls[]` rows, remote roster rows | `runtime` | `harness` | Output only; a pre-0.27 host's `runtime` rows are read as `harness` |
100
- | `oats inspect --home --json` `instance` | `runtime` | `harness` | Output only |
101
- | The launch plan's package check (`launch-config preview` `problems[]`) | `runtime-packages` | `harness-packages` | Output only |
102
- | Soul `soul.yaml` (member and package souls) | `runtime:` | `harness:` | Yes, **without** a warning: released capabilities (oats.aweb 1.13.1) ship `runtime:`, and the operator cannot fix a provider's file. Both, disagreeing → `E_BAD_MANIFEST` |
103
- | `oats-local.yaml` `launch-configs.<name>` | `runtime:` | `harness:` | Yes, with the warning. Both, disagreeing → `E_WORKSPACE_SCHEMA`. `launch-config set` writes `harness` (a `runtime` in its `--file` definition too) |
104
- | `launch-config list/preview --json` rows and `selection` | `runtime` | `harness` | Output only |
105
- | Home `instance.json` | `runtime`; launch recipe `launch` version 1 `{runtime}` | `harness`; recipe version **2** `{harness}` | Yes, with the warning naming the home: a 0.26.0 home inspects, starts, restarts and retires; its next start or restart records the new names |
106
- | `oats spawn … --preview` decision | `effective.runtime` | `effective.harness` | Output only. The revision digests the key names, so a decision previewed by a 0.26 kernel is `E_DECISION_STALE` at apply (with the fresh decision) |
107
- | `spawn`/`session` results, `spawned` event `data` | `runtime` | `harness` | Output only |
108
- | Schedule definitions (`schedule add/update --spec-json`, stored jobs, `schedule list`) | `runtime` | `harness` | Yes, with the warning. A stored job is read in the new name and saved in it next time. Both, disagreeing → `E_SCHEDULE_INVALID` (a stored job: invalid on its own) |
109
- | Schedule run records (`lastRun`, `recentRuns`) | `startedRuntime` | `startedHarness` | Output; a 0.26.0 record's `startedRuntime` is still read |
110
- | Error codes | `E_UNSUPPORTED_RUNTIME`, `E_RUNTIME_PACKAGE`, `E_RUNTIME_RESOURCE_MISSING` | `E_UNSUPPORTED_HARNESS`, `E_HARNESS_PACKAGE`, `E_HARNESS_RESOURCE_MISSING` | Output only |
111
- | Hook environment (spawn and launch hooks) | `OATS_RUNTIME`, `OATS_PREVIOUS_RUNTIME` | `OATS_HARNESS`, `OATS_PREVIOUS_HARNESS` | Both are set, with the same values, for released hooks |
112
- | Capability manifest `requires[]` harness package | `runtime` | `harness` | Yes, **without** a warning (a provider's file); a row naming both is refused |
113
- | Package verification `loadedBy` | `runtime-discovery` | `harness-discovery` | Output only |
114
-
115
- Unchanged, because they do not name the harness: the session endpoint
116
- vocabulary (`runtimeAuthority`, `runtimeState`/`runtimeError` in liveness,
117
- `E_RUNTIME_ENDPOINT_UNKNOWN`, `E_RUNTIME_AUTHORITY_MISMATCH`,
118
- `E_RUNTIME_QUIESCE_FAILED`); `capabilityRuntime`; the retirement baseline's
119
- `runtime`; oats.okf's `harvest-runtime` setting; and the kernel's
120
- "runtime-neutral" design. Hook stdin carries no `launch.runtime` (no 0.26 hook
121
- emitter wrote it).
122
-
123
- ## Inspect, readiness and operation run on the workspace model (`operationsApi: 2`, `soulsApi: 2`, `readinessApi: 2`, OATS 0.26.0)
124
-
125
- On a workspace deployment (an `oats-local.yaml` in reach of `--dir`), and for
126
- any home whose `instance.json` records `modules`, these three commands read the
127
- workspace model's own records and **never the classic config chain**. The probe
128
- integers are the gate; there is no feature string. The probe's integer says
129
- this kernel CAN answer the v2 shape. **Dispatch on the payload's own integer**.
130
- There is no v1 shape any more (0.26.0 removed the classic chain's answers):
131
- with no `oats-local.yaml` in reach and no `--home`, the three commands answer
132
- `E_LOCAL_MISSING`; a `--home` whose `instance.json` records no `modules`
133
- (spawned by an earlier kernel) answers `E_UNSUPPORTED_MODE` (re-spawn it from
134
- the deployment); an unreadable `--home` answers `E_SESSION_UNKNOWN`.
135
-
136
- **The captured/portable path was removed in 0.26.** A *captured home* (its `instance.json`
137
- records `executionBinding`, `incarnationId` or `captured`: spawned through
138
- 0.24–0.25's `prepare` / `--deployment --resolution`) answers `E_UNSUPPORTED_MODE`
139
- (`details: {home, captured: true}`) to these three commands, to `session
140
- start|restart` and to its in-home commands; `oats retire` still works on it, and
141
- its result's `warnings[]` names each capability whose retire hook did NOT run
142
- (what it created is not revoked). `oats status --json` / `oats doctor --json`
143
- name captured homes once, in `problems[]`, as `legacy-captured-home` `{instances,
144
- homes, message}`. The captured selectors `--deployment`, `--resolution` and
145
- `--artifact-set`, and an inherited `OATS_DEPLOYMENT`/`OATS_RESOLUTION`, are refused
146
- by every command except `version` (`E_UNSUPPORTED_MODE`, `details.selector` or
147
- `details.inherited`); `oats prepare` and `oats inspect --request` are removed
148
- verbs (`E_UNKNOWN_COMMAND`, `details: {removed, replacement}`). The version
149
- document no longer carries `capturedDispatchApi` / `capturedDispatchActions`.
150
-
151
- | Command | Integer (probe and payload) | 0.25.x value |
44
+ - The Desktop accepts `desktopApi === 1` and a released `version` inside
45
+ `ACCEPT_RANGE` (`packages/desktop/cli-locator.mjs`); a prerelease is never
46
+ accepted. The real gate is the feature list; its minimum is
47
+ `packages-no-approval`.
48
+ - `harnesses` is what `--harness` accepts; `sessionBackends` what `--backend`
49
+ accepts. A host without the `harness` feature lists `runtimes` instead.
50
+ - `remote` is the routed surface: the commands `--server <id>` sends to a
51
+ registered server, plus `roster`. The Desktop checks the execution host's
52
+ probe before a routed mutation.
53
+ - In text mode the command prints `@awebai/oats <version> (desktop API v1)`.
54
+
55
+ ### Features
56
+
57
+ | Feature | What it enables | API number |
152
58
  |---|---|---|
153
- | `oats inspect --json` | `operationsApi: 2` (top level); each `souls[]` row `soulsApi: 2` | 1 / 1 |
154
- | `oats readiness --json` | `readinessApi: 2` | 1 |
155
- | `oats operation run --json` | `operationsApi: 2` on the result | absent |
156
-
157
- The probe's `soulsApi` follows the inspect soul rows. The `oats souls --json`
158
- document keeps its own `soulsApi: 1`, because its shape did not change (see
159
- [`oats souls`](#oats-capabilities---dir---json-capabilitiesapi-1-oats-souls---dir---json-soulsapi-1)).
160
-
161
- **The subject is an instance or a soul, never a scope.** Pass `--home <abs>`
162
- or `--soul <name>`. A workspace deployment with neither is `E_BAD_ARGS`. An
163
- `oats-local.yaml` that exists but cannot be read is reported with its own
164
- error code, never answered from the classic chain. For
165
- inspect, the message points to `oats souls` and `oats capabilities`, the
166
- scope-wide lists.
167
- - `--home` selects the instance, from its `instance.json` and the module copies
168
- under `<home>/.oats/modules/`. Everything is as spawned.
169
- - `--soul` selects the soul, resolved exactly as a spawn of it would be:
170
- discovery, then soul `capabilities:` plus workspace defaults, then the lock.
171
- - A v2 home lives at `<deployment>/agents/<soul>/instances/<name>`, and its
172
- deployment is derived from that path. `<deployment>/oats-local.yaml` must
173
- exist exactly there (never found by walking up), otherwise
174
- `E_HOME_MISMATCH`. A v2 spawn ignores an ambient `PI_AGENTS_ROOT` /
175
- `OATS_ROOT`, so its homes always have this layout.
176
- - `--dir`, if given with `--home`, must be that home's deployment
177
- (`E_HOME_MISMATCH`). `--agents-root`, if given, must be
178
- `<deployment>/agents` (`E_HOME_MISMATCH` with `--home`, `E_SOUL_UNKNOWN`
179
- with `--soul`).
180
-
181
- **Gone from every payload:** `scope` (`context`, `chain`, `team`,
182
- `agentsRoots`), config `levels`, `activation {declaredAt, target, level,
183
- source}`, `currentConfig`, `snapshot.drift`, `health {trusted, approved,
184
- locked, installedIntegrity}`, soul `provenance`/`readiness`, and the scope's
185
- portable `sources`. A capability's origin is its module's `from` (member
186
- commit, or package version + commit + integrity). Its settings are the merged
187
- payload the spawn recorded for a home, or the resolution computes for a soul.
188
-
189
- ### `oats inspect (--home <abs> | --soul <name> [--dir <d>]) --json` → `operationsApi: 2`
59
+ | `retire-home` | `oats retire <instance> --home <abs>` | |
60
+ | `session-start`, `session-restart` | `oats session start/restart --home` | |
61
+ | `launch-config` | `oats launch-config …`; the selection flags on start and restart | |
62
+ | `schedule` | `oats schedule …` | `scheduleApi: 2` |
63
+ | `session-upload` | `oats session upload` (and the host's `session receive`) | |
64
+ | `operations` | `oats operation run`; `operations[]` in inspect | `operationsApi: 2` |
65
+ | `instance-git`, `instance-git-remote` | `oats instance git/diff`; the observation's `remote` | `instanceGitApi: 1` |
66
+ | `souls-declarations` | `souls[].declarations` in inspect | `soulsApi: 2` |
67
+ | `lifecycle-plans`, `retire-retention` | stop and retire plans and guarded applies; retire keeps a worktree | `lifecycleApi: 1` |
68
+ | `readiness` | `oats readiness` | `readinessApi: 2` |
69
+ | `spawn-preview`, `spawn-preview-2` | the no-write preview with a bound `decision` (gate on `-2`) | `spawnPreviewApi: 2` |
70
+ | `spawn-apply-2` | `--expect-decision` apply | `spawnApplyApi: 1` |
71
+ | `spawn-idempotency`, `spawn-idempotency-2` | `--idempotency-key`; recovery before placement (gate on `-2`) | `spawnApplyApi: 1` |
72
+ | `instance-events`, `instance-events-2` | the bounded events read (gate on `-2`) | `eventsApi: 2` |
73
+ | `schedule-history`, `schedule-read-2` | run history with identity and provenance (gate on `schedule-read-2`) | `scheduleHistoryApi: 3` |
74
+ | `workspace-v2` | `onboard`, `sync`, `package`, `workspace status`, `capabilities`, `souls` | `workspaceApi: 2` |
75
+ | `instance-modules` | `instance.json` `modules`/`providers`/`workspace`; module drift in status; preview `modules[]` | |
76
+ | `spawn-provider-payload` | `oats spawn … --provider <cap> <key>=<value>` | |
77
+ | `served-identity` | `decision.effective.providers`; the served `identity` in inspect and status | |
78
+ | `packages-no-approval` | no package approval anywhere | |
79
+ | `spawn-name` | `oats spawn --name <slug>` | |
80
+ | `settings-origins` | preview `settingsOrigins` | |
81
+ | `settings-declared` | `declares` on inspect capabilities and preview modules | |
82
+ | `capabilities-private` | `private` on `oats capabilities` rows | |
83
+ | `layers-from` | `layers.<slot>.from` in inspect | |
84
+ | `team-model-2` | every team field; `oats teams`, `oats soul teams` | `teamsApi: 1`, `soulTeamsApi: 1` (payload only) |
85
+ | `harness` | the harness names ([Harness input spellings](#the-harness-rename-feature-harness-oats-0270)) | |
86
+ | `package-souls` | package soul rows, `qualifiedName`, `packages[].souls` | |
87
+ | `triggers` | `oats trigger …` | `triggerApi: 1` (payload only) |
88
+ | `automations` | workspace triggers and schedules; `oats automations refresh` | `automationsApi: 1` |
89
+ | `desktop-facts` | the facts under [Desktop facts](#desktop-facts-feature-desktop-facts-oats-0290) | |
90
+ | `launch-preference` | soul and local launch preferences; `launch`, `launchCurrent`, `launchFrom`; `--reselect-launch`; `key` on soul and agent rows ([Launch preferences](#soul-launch-preferences-feature-launch-preference-oats-0300)) | |
91
+
92
+ Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
93
+ `workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
94
+ `soulsApi: 1`, `teamsApi: 1`, `soulTeamsApi: 1`, `triggerApi: 1`.
95
+
96
+ **Gate on the probe, never by trying.** An older kernel can ignore an unknown
97
+ flag and act: without `lifecycle-plans`, `retire --plan` retires.
98
+
99
+ ## The envelope and dispatch errors
100
+
101
+ Every `--json` command except those below prints exactly one JSON object on
102
+ stdout (progress goes to stderr):
103
+
104
+ ```json
105
+ {"schemaVersion":1,"ok":true,"result":{}}
106
+ ```
107
+
108
+ ```json
109
+ {"schemaVersion":1,"ok":false,"error":{"code":"E_BAD_ARGS","message":"--home needs an absolute instance home"}}
110
+ ```
111
+
112
+ - Success exits 0, failure nonzero. `error.details` is present only when the
113
+ command has details.
114
+ - Either envelope may carry `warnings: [ … ]`, only when there is something
115
+ to say. The one warning is `deprecated-runtime-name`
116
+ ([Harness input spellings](#the-harness-rename-feature-harness-oats-0270));
117
+ in text mode it is an `oats: warning: …` line on stderr.
118
+ - `<kernel command> --help --json` answers `{command, usage}` and runs
119
+ nothing.
120
+
121
+ | Not an envelope | stdout |
122
+ |---|---|
123
+ | `oats version --json` | the probe |
124
+ | `oats status --json` | the [roster document](#the-roster-oats-status---json) |
125
+ | a first `oats retire --json`, and a deferred self-retire | the raw [retire receipt](#retire) |
126
+
127
+ <a id="flags"></a>
128
+ ### Flag syntax
129
+
130
+ A kernel command reads `--flag=value` exactly as `--flag value` (the value is
131
+ everything after the first `=`). `E_BAD_ARGS` for an empty `--flag=`, a value
132
+ on a switch (`--yolo=false` never turns yolo on) and a value that is itself an
133
+ option (`--model=--yolo`). A capability command's own flags are forwarded as
134
+ typed; the kernel reads only its dispatch flag (`--soul`). There is no feature
135
+ string for this: to support older kernels, use the spaced form.
136
+
137
+ ### Dispatch errors
138
+
139
+ | Code | When |
140
+ |---|---|
141
+ | `E_UNKNOWN_COMMAND` | No kernel command or capability namespace matches; an unknown capability subcommand; a [removed verb](#removed-verbs-and-flags) (`details: {removed, replacement}`) |
142
+ | `E_CAPABILITY_INACTIVE` | In a home: the namespace's module is not one of the home's recorded capabilities |
143
+ | `E_CAPABILITY_BLOCKED` | In a home: the manifest claiming the namespace is not a workspace module copy of that home. There is no package trust gate |
144
+ | `E_CAPABILITY_BROKEN` | The manifest command is not a non-empty string, its script is missing or outside the module, or the dispatcher failed |
145
+ | `E_DUPLICATE_NAMESPACE` | Two modules claim the namespace |
146
+ | `E_CONFIG_BROKEN` | The home's `instance.json` or the deployment's configuration cannot be read |
147
+ | `E_LOCAL_MISSING` | No `oats-local.yaml` in reach of `--dir` or the working directory |
148
+ | `E_UNSUPPORTED_MODE` | A home or selector the kernel no longer runs (below) |
149
+
150
+ Capability dispatch inside a home uses the home's module copies; from a
151
+ deployment it resolves the module as `oats spawn --soul <x>` would and runs it
152
+ with the soul's merged payload. `oats <namespace> --help --json` answers
153
+ `{capability, namespace, command, commands, description, help}`.
154
+
155
+ `E_UNSUPPORTED_MODE` covers a home with no recorded `modules` (re-spawn it), a
156
+ captured home (`details: {home, captured: true}`), the selectors
157
+ `--deployment`, `--resolution` and `--artifact-set` (`details.selector`), and
158
+ an inherited `OATS_DEPLOYMENT` or `OATS_RESOLUTION` (`details.inherited`).
159
+ `oats version` and `oats retire` still work. `oats status --json` names such
160
+ homes in `problems[]`: `legacy-captured-home {code, instances, homes,
161
+ message}` and `legacy-local-agents {code, dirs, instances, message}`.
162
+
163
+ <a id="inspect-readiness-and-operation-run-on-the-workspace-model-operationsapi-2-soulsapi-2-readinessapi-2-oats-0260"></a>
164
+ ## Inspect, readiness and operation run
165
+
166
+ These answer about one **subject**: an instance home (`--home <abs>`) or a
167
+ soul of a deployment (`--soul <name> [--dir <d>]`).
168
+
169
+ | Command | Integer |
170
+ |---|---|
171
+ | `oats inspect --json` | `operationsApi: 2`; each `souls[]` row `soulsApi: 2` |
172
+ | `oats readiness --json` | `readinessApi: 2` |
173
+ | `oats operation run --json` | `operationsApi: 2` |
174
+
175
+ **Addressing.**
176
+ - `--home` reads the home's `instance.json` and its module copies under
177
+ `<home>/.oats/modules/`. The home must be
178
+ `<deployment>/agents/<soul>/instances/<name>` with
179
+ `<deployment>/oats-local.yaml` exactly there, else `E_HOME_MISMATCH
180
+ {home, expected}`. A `--dir`, `--agents-root` or `--soul` that disagrees
181
+ with it is `E_HOME_MISMATCH`. An unreadable home is `E_SESSION_UNKNOWN`; a
182
+ home without `modules`, or a captured one, is `E_UNSUPPORTED_MODE`.
183
+ - `--soul` resolves the soul exactly as a spawn would (discovery, the soul's
184
+ `capabilities:` plus workspace defaults, the lock). No `oats-local.yaml` is
185
+ `E_LOCAL_MISSING`; an `--agents-root` other than `<deployment>/agents` is
186
+ `E_SOUL_UNKNOWN`.
187
+ - Neither is `E_BAD_ARGS`. `PI_AGENTS_ROOT` is ignored.
188
+
189
+ A module's origin (`from`) is `{kind: "member", repoKey, commit}` or `{kind:
190
+ "package", package, version, commit, integrity, repoKey}`.
191
+
192
+ <a id="oats-inspect---home-----soul----dir----json--operationsapi-2"></a>
193
+ ### `oats inspect`
194
+
195
+ ```text
196
+ oats inspect (--home <abs> | --soul <name> [--dir <d>]) --json
197
+ ```
198
+
199
+ An instance subject, abridged:
190
200
 
191
201
  ```json
192
- {"operationsApi":2,"kernel":"0.26.0",
193
- "subject":{"kind":"instance","instance":"release-manager-x","home":"/w/agents/release-manager/instances/release-manager-x","soul":"release-manager"},
194
- "workspace":{"key":"github.com/northwind/agents","name":null,"deployment":"/w","commit":"461b9c24…","standalone":false},
195
- "souls":[{"soulsApi":2,"name":"release-manager","repoKey":"github.com/northwind/agents","commit":"461b9c24…","team":"engineering",
196
- "kind":null,"path":null,"description":"Cuts, verifies and announces platform releases.","work":"worktree","harness":null,"model":null,
197
- "declarations":{"requires":null,"defaults":null,"knowledge":{"owns":"release-manager","reads":["platform-engineer"]},"teams":null,"resources":null,"children":null,
198
- "capabilities":{"nw-release-tooling":{"from":"here"},"nw-deploy":{"from":"package"}}},
199
- "declarationProblems":[],
200
- "instructions":{"file":"/w/agents/release-manager/souls/461b9c24929c/AGENTS.md","text":"# release-manager\n…","truncated":false}}],
202
+ {"operationsApi":2,"kernel":"0.30.0",
203
+ "subject":{"kind":"instance","instance":"rm-1","home":"/w/agents/rm/instances/rm-1","soul":"rm"},
204
+ "workspace":{"key":"github.com/nw/agents","name":"northwind","deployment":"/w","commit":"66566512…","standalone":false},
205
+ "souls":[{"soulsApi":2,"name":"rm","repoKey":"github.com/nw/agents","commit":"66566512…","kind":null,"path":null,
206
+ "description":"Cuts releases.","work":"worktree","harness":null,"model":null,
207
+ "declarations":{"requires":null,"defaults":null,"knowledge":{"owns":"rm","reads":[]},"resources":null,"children":null,"capabilities":{"oats.okf":{"from":"package"}}},
208
+ "declarationProblems":[],"instructions":{"file":"/w/agents/rm/souls/66566512168e/AGENTS.md","text":"# rm\n","truncated":false}}],
201
209
  "layers":{"knowledge":{"id":"oats.okf","from":"workspace"},"messaging":{"id":null,"from":null},"tasks":{"id":null,"from":null}},
202
- "capabilities":[
203
- {"id":"nw-house-style","version":"0.0.0-workspace","layer":null,"command":null,
204
- "from":{"kind":"member","repoKey":"github.com/northwind/agents","commit":"461b9c24…"},
205
- "dir":"/w/agents/release-manager/instances/release-manager-x/.oats/modules/nw-house-style","settings":{},"declares":[],"compatibility":{"ok":true,"range":">=0.25.0","kernel":"0.26.0"},"missingRequires":[],"operations":[]},
206
- {"id":"oats.okf","version":"2.1.3","layer":"knowledge","command":"okf",
207
- "from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"71f53649…","integrity":"sha256-5019…","repoKey":"github.com/awebai/oats-okf"},
208
- "dir":"/w/agents/release-manager/instances/release-manager-x/.oats/modules/oats.okf",
209
- "settings":{"owns":"release-manager","reads":["platform-engineer"],"state-dir":"/srv/okf"},"declares":["bindings-file","git-timeout","harvest-model","harvest-runtime","state-dir"],"compatibility":{"ok":true,"range":">=0.24.4","kernel":"0.26.0"},"missingRequires":[],
210
- "operations":[{"name":"inspect","kind":"view","command":"inspect","context":"home","description":"…","args":[],"argv":["okf","inspect"],"available":true,"reason":null}]}],
211
- "knowledge":{"provider":"oats.okf","version":"2.1.3","operations":[{"name":"inspect","kind":"view","available":true,"reason":null}]},
212
- "instance":{"home":"/w/agents/release-manager/instances/release-manager-x","instance":"release-manager-x","agent":"release-manager",
213
- "harness":"pi","model":null,"yolo":null,"launched":false,"createdAt":"<iso>","resolution":"7217670b…",
214
- "soulDir":"/w/agents/release-manager/souls/461b9c24929c",
215
- "instructions":{"file":"/w/agents/release-manager/instances/release-manager-x/AGENTS.md","text":"…","truncated":false,
216
- "sources":[{"source":"kernel:instance-boundary","file":"…"},{"source":"capability:oats.okf","file":"…/.oats/modules/oats.okf/injects/okf.md"}]}},
217
- "identity":null,"problems":[]}
210
+ "capabilities":[{"id":"oats.okf","version":"2.1.3","layer":"knowledge","command":"okf",
211
+ "from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"},
212
+ "composedFrom":null,"dir":"/w/agents/rm/instances/rm-1/.oats/modules/oats.okf","settings":{"owns":"rm","reads":[]},
213
+ "declares":["state-dir"],"compatibility":{"ok":true,"range":">=0.24.0","kernel":"0.30.0"},"missingRequires":[],
214
+ "operations":[{"name":"status","kind":"view","command":"status","context":"home","description":"Knowledge status","args":[],
215
+ "argv":["okf","status"],"available":true,"reason":null}]}],
216
+ "capabilitiesOff":[],
217
+ "teams":[{"label":"eng","team":null,"default":true,"from":"shared"},{"label":"mine","team":"mine:ana.aweb.ai","default":false,"from":"local"}],
218
+ "defaultTeam":{"label":"eng","team":null,"from":"soul"},"teamsSource":"live",
219
+ "recordedDefaultTeam":{"label":"eng","team":null,"from":"soul"},
220
+ "knowledge":{"provider":"oats.okf","version":"2.1.3","operations":[{"name":"status","kind":"view","available":true,"reason":null}]},
221
+ "instance":{"home":"/w/agents/rm/instances/rm-1","instance":"rm-1","agent":"rm","harness":"pi","model":null,"yolo":null,"launched":false,
222
+ "createdAt":"2026-09-28T10:08:01.281Z","resolution":"abacbdb5a7975098d77007c8","soulDir":"/w/agents/rm/souls/66566512168e",
223
+ "instructions":{"file":"/w/agents/rm/instances/rm-1/AGENTS.md","text":"…","truncated":false,
224
+ "sources":[{"source":"capability:oats.okf","file":"/w/agents/rm/instances/rm-1/.oats/modules/oats.okf/injects/okf.md"}]}},
225
+ "identity":null,
226
+ "problems":[]}
218
227
  ```
219
228
 
220
- - `subject` is `{kind:"instance", instance, home, soul}` for `--home` (the
221
- `home` as you passed it) or `{kind:"soul", soul, repoKey, commit, team}` for
222
- `--soul`.
223
- - `workspace.deployment` is canonical (realpath). `workspace.name` is
224
- observed, so it is `null` on `inspect --home`: that command never contacts
225
- the remotes, and `instance.json` records the workspace `key`, not its name.
226
- Identify the workspace by `key`.
227
- - `souls` holds exactly the subject's soul. For a home, it is read from the
228
- recorded `soulDir` (the per-commit copy the instance incarnates), with
229
- `path: null`. For a soul, it is the member's current definition, with
230
- `path` inside the member repository. `kind` is `member` or `external`.
231
- It is observed from discovery, so it is `null` on `inspect --home`
232
- (readiness `--home` observes it).
233
- `declarations` gains `capabilities` (the soul's own `capabilities:`).
234
- - `layers.<layer>` is `{ id, from }`: the capability filling the slot
235
- (`null` when empty) and where it came from (feature `layers-from`):
236
- `"soul"` (the soul's own `capabilities:`), `"workspace"`
237
- (`defaults.<slot>` or `defaults.capabilities`) or `"team:<label>"`
238
- (`defaults.byTeam.<label>.capabilities`). `from` is `null` for an empty
239
- slot. A soul answers from its resolution now. A home answers what its spawn
240
- recorded, even after the workspace changes. A home spawned before
241
- `layers-from` recorded nothing, so its `from` is `null`.
242
- - `capabilities[]` lists the subject's resolved modules, sorted by id:
243
- - `dir` is the home's module copy, or `null` for a soul (nothing is
244
- materialized to answer inspect).
245
- - `settings` is the merged provider payload.
246
- - `compatibility` is `{ ok, range, kernel }`: the manifest's
247
- `compatibility.oats` (`null` when none) against the running kernel. A
248
- soul's resolution refuses an incompatible module (`E_CAPABILITY_INCOMPATIBLE`),
249
- so `ok: false` appears only for a home, together with a
250
- `capability-incompatible` entry in `problems`.
251
- - `declares` lists the setting keys the manifest declares (`settings.<key>`),
252
- sorted; names only, never descriptions or defaults; `[]` when it declares
253
- none. Gate on feature `settings-declared` (e.g. offer a Teams choice only
254
- when the messaging module declares `join`).
255
- - `missingRequires` lists the manifest `requires` commands absent from PATH.
256
- - `operations[].available` is `false` with a `reason` when it cannot run
257
- here: a `context: "home"` operation for a soul subject says `needs a
258
- running home (--home)`.
259
- - `capabilities[].composedFrom` and `capabilitiesOff[]` (feature
260
- `desktop-facts`): see [Desktop facts](#desktop-facts-feature-desktop-facts-oats-0290).
261
- - `instance` is `null` for a soul. For a home, `instructions.sources` names
262
- each composed inject in order.
263
- - A soul whose resolution is refused (for example, a package the lock does
264
- not provide) is an error for inspect (`E_PACKAGE_MISSING`,
265
- `E_PACKAGE_INTEGRITY`, `E_CAPABILITY_MISSING`, `E_LOCK_SCHEMA`). Readiness
266
- reports the same condition as a failing item.
267
-
268
- ### `oats readiness (--home <abs> | --soul <name> [--dir <d>]) [--policy] --json` → `readinessApi: 2`
229
+ | Key | Meaning |
230
+ |---|---|
231
+ | `subject` | `{kind: "instance", instance, home, soul}` or `{kind: "soul", soul, repoKey, commit}` |
232
+ | `workspace` | `{key, name, deployment, commit, standalone}`; for a home, `name` is the recorded name (`null` if the spawn predates it) |
233
+ | `souls` | exactly the subject's soul |
234
+ | `layers` | `{knowledge, messaging, tasks}`, each `{id, from}` |
235
+ | `capabilities`, `capabilitiesOff` | the resolved modules (by id) and the ones the soul turned off |
236
+ | `teams`, `defaultTeam`, `teamsSource`, `recordedDefaultTeam` | [Where teams appear](#where-teams-appear); `recordedDefaultTeam` is home only |
237
+ | `knowledge` | `{provider, version, operations: [{name, kind, available, reason}]}` for the knowledge slot (`null`s and `[]` when empty) |
238
+ | `instance` | `null` for a soul; the home's facts above. `instructions.sources` lists each composed inject in order |
239
+ | `identity` | home only (absent for a soul): the served identity a messaging provider recorded, `{…, provider}`, or `null` |
240
+ | `problems` | below |
241
+
242
+ **Soul row** (`soulsApi: 2`). For a home it is read from the recorded
243
+ `soulDir` (`path` and `kind` are `null`); for a soul it is the member's current
244
+ definition (`kind` is `member` or `external`, `path` inside the member).
245
+ `declarations` is `{requires, defaults, knowledge, resources, children,
246
+ capabilities}`, each the soul.yaml section as written or `null`.
247
+ `instructions` is `{file, text, truncated}` of the soul's `AGENTS.md`, or
248
+ `null` before a spawn has copied the soul. `declarationProblems` holds
249
+ `soul-declarations-unreadable` when soul.yaml does not parse.
250
+
251
+ **Layers.** `id` is the capability filling the slot or `null`. `from`
252
+ (feature `layers-from`) is `"soul"` or `"workspace"`, `null` for an empty slot
253
+ or a home spawned before it was recorded.
254
+
255
+ **Capability rows.**
256
+ - `dir` is the home's module copy, `null` for a soul.
257
+ - `composedFrom` (feature `desktop-facts`): `"workspace"` or `"soul"` for a
258
+ soul subject; `null` for a home.
259
+ - `settings` is the merged provider payload; `declares` (feature
260
+ `settings-declared`) the manifest's setting keys, sorted.
261
+ - `compatibility` is `{ok, range, kernel}` (`range` is the manifest's
262
+ `compatibility.oats` or `null`). A soul's resolution refuses an incompatible
263
+ module, so `ok: false` appears only for a home.
264
+ - `missingRequires`: `{command, why, install}` for each manifest `requires`
265
+ command absent from `PATH`.
266
+ - `operations[]`: the declared operation (`name`, `kind`, `command`,
267
+ `context`, `description`, `args`) plus `argv`, `available` and `reason`
268
+ (for example a missing command, or a `context: "home"` operation on a soul).
269
+
270
+ **`capabilitiesOff[]`** (feature `desktop-facts`): `{id, off: true, from:
271
+ "soul", reason, slot?, overrides}`, sorted by id. `reason` is `"off"` (the
272
+ soul wrote `<id>: off`) or `"slot-none"` (the soul wrote `<slot>: none`,
273
+ emptying the slot the workspace filled with `<id>`). `overrides` is the layer
274
+ whose default was turned off (`"workspace"`). `[]` for a home.
275
+
276
+ **Problems:** `soul-declarations-unreadable`, `module-manifest-invalid`,
277
+ `module-missing {capability}`, `capability-incompatible {capability, range,
278
+ kernel}`, and for a home whose discovery failed, that error's `{code,
279
+ message}`.
280
+
281
+ A soul whose resolution is refused is an inspect error with the resolver's
282
+ code and details (`E_PACKAGE_MISSING`, `E_PACKAGE_INTEGRITY`,
283
+ `E_CAPABILITY_MISSING`, `E_LOCK_SCHEMA`, `E_REQUIREMENT_INACTIVE`,
284
+ `E_TEAM_UNKNOWN`, `E_TEAM_NOT_ELIGIBLE`); readiness reports the same condition
285
+ as an item. Soul lookup errors are those of [spawn](#spawn-errors).
286
+
287
+ <a id="oats-readiness---home-----soul----dir----policy---json--readinessapi-2"></a>
288
+ ### `oats readiness`
289
+
290
+ ```text
291
+ oats readiness (--home <abs> | --soul <name> [--dir <d>]) [--policy] --json
292
+ ```
269
293
 
270
294
  ```json
271
295
  {"readinessApi":2,
272
- "subject":{"kind":"soul","soul":"release-manager","repoKey":"github.com/northwind/agents","commit":"461b9c24…","team":"engineering"},
273
- "selector":{"kind":"soul","soul":"release-manager","agentsRoot":null,"dir":"/w"},"at":"<iso>",
296
+ "subject":{"kind":"soul","soul":"rm","repoKey":"github.com/nw/agents","commit":"66566512…"},
297
+ "selector":{"kind":"soul","soul":"rm","agentsRoot":null,"dir":"/w"},
298
+ "at":"2026-09-28T10:07:57.549Z",
274
299
  "checks":{
275
- "installed":{"status":"pass","items":[
276
- {"subject":"oats.okf","status":"pass","required":true,"reason":null,"producer":"workspace resolution",
277
- "evidence":{"from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"71f53649…","integrity":"sha256-5019…"}},"remedy":null,"capability":{"id":"oats.okf"}}]},
300
+ "installed":{"status":"pass","items":[{"subject":"oats.okf","status":"pass","required":true,"reason":null,"producer":"workspace resolution",
301
+ "evidence":{"from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"}},
302
+ "remedy":null,"capability":{"id":"oats.okf"}}]},
278
303
  "configured":{"status":"not-applicable","items":[]},
279
- "member":{"status":"pass","items":[
280
- {"subject":"member github.com/northwind/agents","status":"pass","required":true,"reason":null,"producer":"workspace discovery",
281
- "evidence":{"repoKey":"github.com/northwind/agents","workspace":"github.com/northwind/agents","commit":"461b9c24…"},"remedy":null}]},
282
- "providers":{"status":"fail","items":[
283
- {"subject":"oats.okf","status":"fail","required":true,"reason":"setting state-dir is required (absolute host path)","producer":"provider binding check",
284
- "evidence":null,"remedy":null,"capability":{"id":"oats.okf"},
285
- "result":{"status":"needs-configuration","problems":[{"code":"needs-configuration","message":"setting state-dir is required (absolute host path)"}],"warnings":[]}}]}},
304
+ "member":{"status":"pass","items":[{"subject":"member github.com/nw/agents","status":"pass","required":true,"reason":null,
305
+ "producer":"workspace discovery","evidence":{"repoKey":"github.com/nw/agents","workspace":"github.com/nw/agents","commit":"66566512…"},"remedy":null}]},
306
+ "providers":{"status":"fail","items":[{"subject":"oats.okf","status":"fail","required":true,"reason":"setting state-dir is required",
307
+ "producer":"provider binding check","evidence":null,"remedy":null,
308
+ "result":{"status":"needs-configuration","problems":[{"code":"needs-configuration","message":"setting state-dir is required"}],"warnings":[]},
309
+ "capability":{"id":"oats.okf"}}]}},
286
310
  "summary":{"ready":false,"required":3,"pass":2,"fail":1,"unknown":0,
287
311
  "byCapability":[{"capability":{"id":"oats.okf"},"checks":{"installed":"pass","configured":"not-applicable","member":"not-applicable","providers":"fail"},"ownReady":false,"ready":false}],
288
312
  "subjectBlockers":[]},
289
- "notes":["…"]}
313
+ "notes":["ready means every REQUIRED check passes; it is never inferred from an empty set"]}
290
314
  ```
291
315
 
292
- For `--home`, `subject` is `{kind:"instance", instance, home, soul}`, and
293
- `selector` is `{kind:"home", home, soul, agentsRoot}`. The selector echoes
294
- your arguments byte-exact, as before. It is now a top-level field, not
295
- `subject.selector`.
296
-
297
- The four checks are `installed | configured | member | providers`, each
298
- `{status, items}` with the item fields as before (`subject, status, required,
299
- reason, producer, evidence, remedy`, plus `capability {id}` on
300
- per-capability items). Item and check statuses are `pass | fail | unknown |
301
- not-applicable`. **`summary.ready`** means every required item passes or is
302
- not-applicable, and at least one required item exists. `byCapability` and
303
- `subjectBlockers` keep their 0.24.9 meaning over the four new checks.
304
-
305
- - **`installed`**:
306
- - For `--home` (producer `instance modules`): each recorded module, `pass`
307
- when its copy under `<home>/.oats/modules/<id>/` holds its `oats.json`.
308
- A missing copy fails, with remedy "spawn a new instance".
309
- - For `--soul` (producer `workspace resolution`): each resolved module, with
310
- its `from` as evidence.
311
- - A resolution refusal is one failing item carrying `code` (`E_PACKAGE_MISSING`,
312
- `E_PACKAGE_INTEGRITY`, `E_CAPABILITY_MISSING`, `E_LOCK_SCHEMA` or
313
- `E_REQUIREMENT_INACTIVE`), the kernel's message as `reason`, its details as
314
- `evidence`, and `remedy: "oats sync (…)"`. That covers a soul whose
315
- packages are not locked, or whose lock no longer matches. The item's
316
- `subject` is the soul; it lands in `summary.subjectBlockers`.
317
- - **`configured`** (producer `capability manifest`): each module's manifest
318
- `requires` command (`evidence.command`), `pass` on PATH and `fail`
319
- otherwise, with the manifest's `install` hint as the remedy. It is `not-applicable` when nothing declares a
320
- requirement. Settings problems are the provider's to say, in `providers`.
321
- - **`member`** (producer `workspace discovery`): the soul's member
322
- repository is confirmed in the workspace. It is the host's `members:` with
323
- the member's `oats-membership.yaml` backlink, as `oats workspace status`
324
- reports it. The kernel never reads `oats.yaml` here.
325
- - `fail` when the backlink is not confirmed; the remedy names
326
- `oats-membership.yaml`.
327
- - `unknown` when discovery could not be read.
328
- - `not-applicable` (`required: false`) for an external soul (it has no
329
- member backlink) and on a standalone view (decision 10, an allowed mode:
330
- membership is declared there, never confirmed). The reason starts with
331
- `standalone view (explicit | unreadable-host)`, and
332
- `evidence.standaloneReason` carries the reason code. A standalone soul can
333
- therefore read Ready.
334
- - Never login, never team registration.
335
- - **`providers`** (producer `provider binding check`): for each module whose
336
- manifest declares `binding`, the kernel runs the provider's own check
337
- (`binding.check`). It relays **the provider's answer verbatim** as
338
- `item.result: {status, problems: [{code, message}], warnings: [{code,
339
- message}]}`. The status maps to the item:
340
-
341
- | `result.status` | item `status` |
342
- |---|---|
343
- | `ready` | `pass` |
344
- | `needs-configuration` | `fail` |
345
- | `authorization-required` | `fail` |
346
- | `unavailable` | `unknown` |
347
-
348
- The reason is the first problem's message (`null` on pass).
349
- `authorization-required` and `unavailable` were added in 0.26.0 (additive):
350
- treat an unrecognized status as `unknown` and show `result` as sent.
351
- - `warnings` is always present (`[]` when the provider sends none). It
352
- **never changes the status** and is not counted in `summary`. A ready
353
- binding can still say, for example, that end-to-end encryption is off:
354
- show it next to the pass. A `warnings` that is not an array of `{code,
355
- message}` strings makes the whole answer `unknown`, the same as a
356
- malformed `problems`.
357
- - A provider that cannot answer is `unknown`, with `item.problems` carrying
358
- its error `{code, message}` and `result: null`. That covers:
359
- - a refusal (`ok:false`, whose code is relayed);
360
- - an invalid answer (`provider-unavailable`, see the wire below);
361
- - a timeout;
362
- - a module tree that cannot be made available.
363
- - **One time budget per readiness read**: 60 s for all provider checks
364
- together, and at most 30 s for each. Checks the budget does not reach are
365
- not run; they are `unknown` with code `time-budget-exhausted`.
366
- - For `--home` the check runs from the home's module copy, as the home's
367
- hooks do. For `--soul` it runs from the module in the deployment's module
368
- store (the tree `oats <ns> …` dispatch uses). A store tree is used only
369
- while its content digest matches the digest verified when it was fetched
370
- at the locked commit. A drifted tree is fetched again, and a fetch that
371
- does not verify is `E_PACKAGE_INTEGRITY` (the item is `unknown` with that
372
- code).
373
- - A module without `binding` has no item; the check is `not-applicable`
374
- when there are none.
375
- - This check reads the provider; it does not bind. A spawn's fail-closed
376
- hooks are unchanged.
377
- - **Removed:** `trusted` and its `signature` block (declaring a package in
378
- `packages:` is the trust decision). `--verify-signatures` answers
379
- `E_BAD_ARGS`. `enrolled` is now `member`.
380
-
381
- `--policy` is **kept**: it means the same without the chain. With `--home` it
382
- is the instance's recorded, enforced policy (`instance.json` `policy`, plus
383
- the recorded work mode). With `--soul` it is the soul's declaration
384
- (`children.spawn`, `work`), `enforced: false`. The shape is unchanged:
316
+ - `selector` echoes the arguments byte-exact: `{kind: "soul", soul,
317
+ agentsRoot, dir}` or `{kind: "home", home, soul, agentsRoot}`.
318
+ - Four checks, each `{status, items}`. An item is `{subject, status,
319
+ required, reason, producer, evidence, remedy}`, plus `capability: {id}` on a
320
+ per-capability item and the keys named below. Statuses are `pass | fail |
321
+ unknown | not-applicable`.
322
+ - A check's status rolls up its required items (any `fail` → `fail`, else any
323
+ `unknown` → `unknown`, else `pass`; `not-applicable` with none required).
324
+ - `summary.ready` is true when every required item passes or is
325
+ not-applicable and at least one required item exists; the counts are over
326
+ required items. `byCapability[]` is `{capability, checks, ownReady,
327
+ ready}` (`ready` also needs no subject blocker). `subjectBlockers[]` is
328
+ `{check, subject, status}` for each failing required item not about a
329
+ capability. `notes` is prose.
330
+
331
+ **`installed`**, one item per module with `evidence: {from}`. `--home`
332
+ (producer `instance modules`): passes when the home's copy holds `oats.json`
333
+ (remedy on failure: spawn a new instance). `--soul` (producer `workspace
334
+ resolution`): each resolved module. A resolution refusal is one failing item about the soul
335
+ with `code`, the message as `reason`, the details as `evidence` and a remedy
336
+ naming `oats sync`; team refusals go under `configured` instead.
337
+
338
+ **`configured`.** Producer `capability manifest`: each manifest `requires`
339
+ command, `evidence: {command}`, the manifest's `install` hint as remedy.
340
+ Producer `team model`: the soul's [team readiness items](#team-readiness-items).
341
+
342
+ **`member`** (producer `workspace discovery`): the soul's repository is a
343
+ confirmed member (`evidence: {repoKey, workspace, commit}`); `fail` when not
344
+ (the remedy names `oats-membership.yaml`); `unknown` when the workspace could
345
+ not be read. External souls and standalone views are `not-applicable`,
346
+ `required: false` (a standalone item carries `evidence.standaloneReason`).
347
+
348
+ **`providers`** (producer `provider binding check`): for each module whose
349
+ manifest declares `binding`, the kernel runs its `binding.check` and relays
350
+ the answer as `item.result: {status, problems, warnings}`. The request,
351
+ environment and validation are in
352
+ [capabilities.md](capabilities.md#readiness-check-bindingcheck).
353
+
354
+ | `result.status` | item `status` |
355
+ |---|---|
356
+ | `ready` | `pass` |
357
+ | `needs-configuration`, `authorization-required` | `fail` |
358
+ | `unavailable` | `unknown` |
359
+
360
+ - `reason` is the first problem's message. `warnings` is always present and
361
+ never changes the status.
362
+ - A provider that cannot answer is `unknown` with `result: null` and
363
+ `item.problems: [{code, message}]`: its refusal code,
364
+ `provider-unavailable`, `resource-not-found` (the check executable is not a
365
+ regular file in the module, or a member module checked from `--soul`),
366
+ `provider-not-qualified`, or a module-store error.
367
+ - One budget per read: 60 s total, 30 s per check; an unreached check is
368
+ `unknown` with `time-budget-exhausted`.
369
+
370
+ **`--policy`** adds `policy` and a note:
385
371
 
386
372
  ```json
387
- "policy":{"childSpawns":{"allowed":true,"enforced":true,"origin":{"kind":"default","detail":"no declaration: children allowed"}},
388
- "worktrees":{"allowed":false,"mode":"directory","enforced":true,"origin":{"kind":"work-mode","detail":"work: directory"}}}
373
+ {"childSpawns":{"allowed":true,"enforced":true,"origin":{"kind":"default","detail":"no recorded policy: children allowed (pre-0.24.8 instance)"}},
374
+ "worktrees":{"allowed":false,"mode":"directory","enforced":true,"origin":{"kind":"work-mode","detail":"work: directory"}}}
389
375
  ```
390
376
 
391
- **The provider check wire (the request `binding.check` receives).** The
392
- request is one JSON line on stdin, and the provider answers one envelope line
393
- on stdout:
377
+ With `--home` it is the recorded, enforced policy; with `--soul`, the soul's
378
+ declaration (`children.spawn`, `work`) with `enforced: false`. The spawn route
379
+ enforces `childSpawns`: a child spawn under a parent whose policy is off is
380
+ `E_CHILD_SPAWNS_DISABLED {parent, policy}`, before anything is created.
394
381
 
395
- ```json
396
- {"schemaVersion":1,"phase":"check","slot":"knowledge","capability":"oats.okf","settings":{"…":"the merged payload"},
397
- "input":{"context":{"kind":"workspace","workspace":"<workspace key>","deployment":"/w","soul":"release-manager","team":"engineering",
398
- "instance":"release-manager-x","home":"/w/agents/…/release-manager-x"},
399
- "action":{"kind":"readiness"}}}
382
+ ### `oats operation run`
383
+
384
+ ```text
385
+ oats operation run <layer>:<name> (--home <abs> | --soul <name> [--dir <d>]) [--arg k=v …] --json
400
386
  ```
401
387
 
402
388
  ```json
403
- {"schemaVersion":1,"phase":"check","slot":"knowledge","capability":"oats.okf","ok":true,
404
- "result":{"status":"ready","problems":[],"warnings":[]}}
389
+ {"operationsApi":2,"operation":"knowledge:status","capability":"oats.okf","version":"2.1.3","argv":["okf","status"],
390
+ "cwd":"/w/agents/rm/instances/rm-1","target":{"home":"/w/agents/rm/instances/rm-1","instance":"rm-1"},
391
+ "result":{"documents":[{"label":"Working state","kind":"markdown","text":"…"}]}}
405
392
  ```
406
393
 
407
- The environment is the provider's module environment:
408
- - `OATS_CAPABILITY`, `OATS_SETTINGS`, `OATS_SETTINGS_ORIGINS` (0.29.0: JSON
409
- pointer → `{ kind, at }`), `OATS_CLI_BIN` and `OATS_WORKSPACE`;
410
- - the team variables (`OATS_TEAM_*`, `OATS_WORKSPACE_NAME`/`_KEY`);
411
- - `OATS_AGENT` (the soul), and `OATS_SOUL` when the soul directory is known;
412
- - for a home, `OATS_INSTANCE` and `OATS_INSTANCE_HOME`.
413
-
414
- For a home, `OATS_WORKSPACE_NAME` is `""` until spawn records the workspace
415
- name (planned).
416
-
417
- Ambient `OATS_*`/`PI_*` is removed. For a soul, `instance` and `home` are
418
- `null`. The answer is decoded by the binding wire's response rules:
419
- - the process exits 0;
420
- - stdout is exactly one JSON document within the wire limits;
421
- - the envelope has exactly `schemaVersion`, `phase`, `slot`, `capability`,
422
- `ok` and `result` (or `error`), echoing the request's first four;
423
- - `result` has `status`, `problems` and optionally `warnings`, and nothing else;
424
- - `ready` carries no problems;
425
- - problems and warnings are `{code, message}` strings. Their codes are the
426
- provider's own and are not checked against `binding.reasons`.
427
-
428
- Anything else is `unknown` (`provider-unavailable`). The check executable must
429
- resolve (realpath) inside its module directory and be a regular file; otherwise
430
- the item is `unknown` (`resource-not-found`). The request carries no
431
- `binding`. A provider whose check
432
- still requires one answers `invalid-binding`, and readiness reports it as
433
- `unknown`.
434
-
435
- ### `oats operation run <layer>:<name> (--home <abs> | --soul <name> [--dir <d>]) [--arg k=v …] --json` → `operationsApi: 2`
394
+ - `<layer>` is `knowledge`, `messaging` or `tasks`; `<name>` matches
395
+ `[a-z][a-z0-9-]*` (`E_BAD_ARGS` otherwise).
396
+ - The provider is the module filling the slot. `cwd` is the home for a
397
+ `context: "home"` operation, else the deployment; `target` is `{home,
398
+ instance}` or `null`.
399
+ - A launched `instance` or `home` named by the provider's result is repeated
400
+ at the top level; `stderr` appears when the provider wrote any.
401
+ - A `view` operation must answer `{documents: [{label, kind?, path?, text?}]}`
402
+ (`kind` `markdown` or `text`), else `E_OPERATION_RESULT`.
403
+ - Errors: `E_OPERATION_UNKNOWN`, `E_OPERATION_UNAVAILABLE` (empty slot, or a
404
+ home operation without `--home`), `E_CAPABILITY_REQUIRES`, `E_BAD_ARGS`
405
+ (undeclared or missing `--arg`), `E_CAPABILITY_BROKEN`. A provider's `ok:
406
+ false` is relayed with its code and `details: {exit, envelope,
407
+ unconfirmed?}`. `E_OPERATION_TIMEOUT` (240 s) and `E_OPERATION_RESULT` are
408
+ unconfirmed outcomes: `details: {exit, signal, unconfirmed: true,
409
+ envelope?, stderr?, cleanup?}`.
410
+
411
+ `--server <id>` routes inspect and operation run when the destination
412
+ advertises `operations`.
413
+
414
+ **Knowledge operations.** Discover a knowledge provider's operations from
415
+ inspect; the provider's version owns their result shapes. For oats.okf, see
416
+ [knowledge.md](knowledge.md#knowledge-operations).
417
+
418
+ <a id="workspace-model-workspaceapi-2"></a>
419
+ ## Workspace
420
+
421
+ Feature `workspace-v2`, `workspaceApi: 2`. Model: [workspaces.md](workspaces.md).
422
+
423
+ - Each command needs an `oats-local.yaml` found walking up from `--dir`, else
424
+ `E_LOCAL_MISSING {dir, searched}`.
425
+ - The workspace is read over Git remotes with the operator's credentials,
426
+ never prompting: `E_REMOTE_UNREADABLE {url, reason: "auth" | "not-found" |
427
+ "network" | "timeout"}`.
428
+ - There is no package approval: declaring a package is the trust decision.
429
+ No payload carries `approvalNeeded`, `approval` or `approved`.
430
+ - A **standalone view** is a member repository whose workspace is not read
431
+ (`standalone:` in `oats-local.yaml`, or an unreadable host): its own souls
432
+ plus `oats.core`. Documents then carry `standalone: true`; otherwise the key
433
+ is absent.
434
+
435
+ ### Removed verbs and flags
436
+
437
+ A removed verb answers, before any namespace can claim it:
436
438
 
437
439
  ```json
438
- {"operationsApi":2,"operation":"knowledge:inspect","capability":"oats.okf","version":"2.1.3","argv":["okf","inspect"],
439
- "cwd":"/w/agents/release-manager/instances/release-manager-x",
440
- "target":{"home":"/w/agents/release-manager/instances/release-manager-x","instance":"release-manager-x"},
441
- "result":{"documents":[{"label":"Working state (STATE.md)","kind":"markdown","text":"…"}]}}
440
+ {"schemaVersion":1,"ok":false,"error":{"code":"E_UNKNOWN_COMMAND","message":"unknown command \"install\" — removed by the workspace model v2; use oats sync","details":{"removed":"install","replacement":"oats sync"}}}
442
441
  ```
443
442
 
444
- The provider is the module that fills `<layer>`:
445
- - for `--home`, the home's module copy, with its recorded settings;
446
- - for `--soul`, the resolved module, materialized into the deployment's module
447
- store if needed.
443
+ | Removed | Code | Replacement |
444
+ |---|---|---|
445
+ | `install`, `restore` | `E_UNKNOWN_COMMAND` | `oats sync` |
446
+ | `init` | `E_UNKNOWN_COMMAND` | `oats-local.yaml` + `oats sync` |
447
+ | `use` | `E_UNKNOWN_COMMAND` | soul.yaml `capabilities:` + workspace defaults |
448
+ | `trust` | `E_UNKNOWN_COMMAND` | declaring the package in `packages:` |
449
+ | `list` | `E_UNKNOWN_COMMAND` | `oats workspace status` / `oats capabilities` |
450
+ | `catalog` | `E_UNKNOWN_COMMAND` | `oats package add <id> <version>` |
451
+ | `remove` | `E_UNKNOWN_COMMAND` | `oats package remove <id>` |
452
+ | `migrate` | `E_UNKNOWN_COMMAND` | a rebuild |
453
+ | `config` | `E_UNKNOWN_COMMAND` | `oats-local.yaml` and `oats-workspace.yaml` |
454
+ | `create`, `type` | `E_UNKNOWN_COMMAND` | the soul's `soul.yaml` in its member repository |
455
+ | `inject` | `E_UNKNOWN_COMMAND` | the capability's inject in its member repository |
456
+ | `prepare` | `E_UNKNOWN_COMMAND` | `oats onboard` / `oats sync`; `oats spawn <soul> --preview` |
457
+ | `inspect --request` | `E_UNKNOWN_COMMAND` | `oats onboard / oats sync; oats spawn --preview` |
458
+ | `session recompose` | `E_UNKNOWN_COMMAND` (no details) | a re-spawn |
459
+ | `readiness --verify-signatures` | `E_BAD_ARGS` | none |
460
+ | `sync --approve`, `onboard --approve` | `E_BAD_ARGS` (`details: {flag}`) | none |
461
+ | `status --team` | `E_BAD_ARGS` | `oats status` in the deployment |
462
+ | `spawn --instance` | `E_BAD_ARGS` | `--purpose` or `--name` |
463
+ | `spawn --ephemeral`, `--instructions-file`, `--def-file` | `E_BAD_ARGS` | a soul in a member repository |
464
+ | `session … --native-record` | `E_BAD_ARGS` | none |
465
+
466
+ `details.replacement` is the kernel's prose (shortened above): show it, do not
467
+ parse it.
468
+
469
+ <a id="oats-onboard-onboardapi-2"></a>
470
+ ### `oats onboard`
448
471
 
449
- The rest of the contract is unchanged ([operations contract](design/operations-contract.md)):
450
- - errors: `E_OPERATION_UNKNOWN`, `E_OPERATION_UNAVAILABLE` (also a
451
- `context: "home"` operation without `--home`), `E_CAPABILITY_REQUIRES`;
452
- - the receipt rules and the `E_OPERATION_TIMEOUT` / `E_OPERATION_RESULT`
453
- unconfirmed outcomes.
472
+ ```text
473
+ oats onboard [<dir>] --workspace <repo ref> [--json]
474
+ ```
454
475
 
455
- There is no `E_CAPABILITY_BLOCKED` (no trust gate). `cwd` is the home for a
456
- `context: "home"` operation, and the deployment otherwise.
476
+ Writes `<dir>/oats-local.yaml` (`{schemaVersion: 2, workspace: <ref>}`),
477
+ creates `<dir>/agents/`, then runs the `oats sync` body. It creates no soul
478
+ and spawns nothing. `<dir>` defaults to the working directory (`--dir` is the
479
+ same argument, given once). The ref is checked before anything is written.
457
480
 
458
- **Remote.** `--server` routes as before. The destination must advertise
459
- `operations` with `operationsApi` 1 or 2; a 0.26 CLI routes to either.
460
- Payload shapes are the destination kernel's.
481
+ ```json
482
+ {"onboardApi":2,"local":"/w/oats-local.yaml","dir":"/w","agents":"/w/agents","lock":"/w/oats-lock.json",
483
+ "sync":{"syncApi":1,"workspace":{"name":"acme","key":"github.com/acme/agents"},"members":[],"packages":[],"changes":[],"problems":[],"warnings":[]},
484
+ "hosting":{"host":"github.com/acme/agents","hostIsMember":true,"rule":"If any member is private, host oats-workspace.yaml in a private repo …"},
485
+ "next":{"clone":[{"key":"github.com/acme/agents","name":"agents","url":"https://github.com/acme/agents.git","dir":"/w/agents-repo","present":false,"host":true},
486
+ {"key":"github.com/acme/platform","name":"platform","url":"https://github.com/acme/platform.git","dir":"/w/platform","present":true,"host":false}],
487
+ "spawn":"oats spawn oats-operator-expert --dir /w",
488
+ "souls":["oats-operator-expert","platform-engineer","release-manager"]}}
489
+ ```
461
490
 
462
- ## Souls and sources (`oats inspect --json`, `soulsApi: 1`) — removed in 0.26.0
491
+ - `sync` is the full [`oats sync`](#oats-sync) report (abridged above).
492
+ `standalone: true` is present on a standalone view.
493
+ - `hosting`: whether the host is itself a member, and the hosting rule (the
494
+ kernel cannot see forge visibility).
495
+ - `next.clone[]`: one row per confirmed member (or, standalone, the
496
+ repository itself): `{key, name, url, dir, present, host}`. `dir` is the
497
+ existing clone (the `clones:` entry, else `<dir>/<name>`), else where to
498
+ clone it: `<dir>/<name>`, or `<dir>/agents-repo` for a member named
499
+ `agents`. `url` is `null` when unknown.
500
+ - `next.spawn` is `oats spawn oats-operator-expert --dir <dir>` when the
501
+ workspace has a soul by that name, else `null`. `next.souls` is the first
502
+ three soul names, sorted.
503
+ - Errors: `E_BAD_ARGS` (usage, no `--workspace`, a repeated directory, an
504
+ unknown flag), `E_REPO_REF`, `E_ALREADY_ONBOARDED {local, dir}` (this
505
+ directory already has `oats-local.yaml`), `E_ONBOARD_FAILED {dir}`. A
506
+ failure before the workspace was read removes what onboarding created and
507
+ carries `details: {…, dir, rolledBack: true}`; a later failure keeps the
508
+ files and carries `details: {…, dir, local}`.
509
+
510
+ ### `oats sync`
463
511
 
464
- The classic scope document (`souls[].provenance`, `souls[].readiness`, the
465
- scope's portable `sources`) was removed with the classic config chain.
466
- `oats inspect` answers only [`soulsApi: 2`](#oats-inspect---home---soul---dir---json-operationsapi-2)
467
- rows; the soul's declarations are in `oats souls --json`.
512
+ ```text
513
+ oats sync [--dir <d>] --json
514
+ ```
468
515
 
469
- ## Instance Git state (`oats instance git|diff`, `instanceGitApi: 1`, OATS 0.24.7+)
516
+ Discovers the workspace, confirms membership, resolves `packages:`, writes
517
+ `oats-lock.json` (lockfileVersion 3), takes the automations snapshot and
518
+ reports. It creates `agents/` if missing.
470
519
 
471
- Read-only observation of one instance's **work tree**. Truth comes from the
472
- tree — the branch the tree is on, not the branch recorded at spawn (that is
473
- reported under `recorded` with a `drift` flag). Fixed-argv `git`, no shell.
520
+ ```json
521
+ {"syncApi":1,"automations":{"triggers":3,"schedules":2,"problems":1,"takenAt":"2026-09-26T19:58:09.281Z"},
522
+ "workspace":{"name":"acme","key":"github.com/acme/agents","url":"https://github.com/acme/agents.git","commit":"45b86f64…",
523
+ "observedAt":"2026-09-26T19:58:07.810Z","local":"/w/oats-local.yaml","lock":"/w/oats-lock.json"},
524
+ "members":[{"key":"github.com/acme/tools","name":"tools","commit":"19839f9e…","confirmed":true,"status":"confirmed","detail":null,
525
+ "souls":["tools-expert"],"capabilities":["acme-tools-dev"],"publishes":{"package":"acme.tools","version":"0.4.0"}},
526
+ {"key":"github.com/acme/billing","name":"billing","commit":"8f2c0d1e…","confirmed":false,"status":"no-backlink",
527
+ "detail":"github.com/acme/billing has no oats-membership.yaml","souls":[],"capabilities":[],"publishes":null}],
528
+ "packages":[{"id":"oats.okf","version":"2.1.3","source":"catalog:oats.okf","commit":"ab897841…","integrity":"sha256-bada35…",
529
+ "capabilities":["oats.okf"],"souls":["knowledge-maintainer"]}],
530
+ "changes":[{"id":"oats.okf","from":null,"to":"2.1.3","commit":"ab897841…"}],
531
+ "problems":[],"warnings":[]}
532
+ ```
474
533
 
475
- Address the instance qualified: `oats instance git <instance> --dir <scope>`
476
- resolves the name under the scope's agents roots (team roots included) and
477
- **refuses when several homes match** (`E_AMBIGUOUS_INSTANCE`, `details.candidates`);
478
- pass `--home <abs>` to pick one. Unknown → `E_SESSION_UNKNOWN`; retired or
479
- un-materialized tree → `E_NO_WORKTREE`.
534
+ - `automations`: counts from the snapshot this sync took.
535
+ - `workspace.name` is `standalone:<repo>` on a standalone view.
536
+ - `members[]`: `status` is `confirmed | not-listed | no-backlink |
537
+ backlink-elsewhere | cannot-read`, `detail` explains an unconfirmed row.
538
+ `publishes` reports a member's `oats-package/` (informational).
539
+ - `packages[]` are the lock rows; `souls` (feature `package-souls`) the
540
+ package souls it records. `changes[]`: `{id, from, to, commit}`, `to: null`
541
+ when a package left.
542
+ - `problems[]`: `{code, path, message, repoKey?, …}`, for example
543
+ `E_WORKSPACE_SCHEMA`, `E_REMOTE_*`, `E_SOUL_AMBIGUOUS` (colliding package
544
+ souls), `E_PACKAGE_MISSING` (a standalone catalog gap: `{code, id, reason:
545
+ "no-catalog", catalog, path, message}`), `E_AUTOMATION_SCHEMA` and
546
+ `E_AUTOMATION_DUPLICATE` (with `kind`). A problem never aborts the sync.
547
+ - `warnings[]`: `soul-private-ignored {code, soul, repoKey, path, message}`
548
+ for a soul.yaml still carrying `private`.
549
+ - Errors: `E_LOCAL_MISSING`, `E_WORKSPACE_SCHEMA {path, problems}`,
550
+ `E_REMOTE_UNREADABLE`, `E_LOCK_SCHEMA`, `E_PACKAGE_MISSING`,
551
+ `E_PACKAGE_INTEGRITY`, `E_PACKAGE_MANIFEST`, `E_REPO_REF`, `E_BAD_ARGS`.
552
+
553
+ ### `oats package add` and `remove`
480
554
 
481
- ```json
482
- {"instanceGitApi":1,"instance":"dev-1","agent":"dev","home":"/abs/home","workMode":"worktree",
483
- "observation":{"revision":"<HEAD oid|unborn>","indexRevision":"<index tree oid>","at":"<iso>","worktree":"/abs/work","branch":"feat/y","detached":false,"unborn":false},
484
- "recorded":{"branch":"feat/x","repo":"/abs/repo","drift":true},
485
- "upstream":{"ref":"origin/feat/y","ahead":1,"behind":0},
486
- "base":{"ref":"origin/main","source":"origin/HEAD","mergeBase":"<oid>","ahead":2,"behind":0},
487
- "remote":{"name":"origin","url":"git@github.com:acme/one.git","host":"github.com","path":"acme/one","source":"branch-upstream|origin"},
488
- "summary":{"changed":1,"renamed":1,"copied":0,"unmerged":0,"untracked":1},
489
- "files":[{"id":"<24 hex>","kind":"renamed","xy":"R.","submodule":false,"score":"R100","path":"src/new.txt","origPath":"src/old.txt","additions":84,"deletions":3,"binary":false}],
490
- "notes":[]}
555
+ ```text
556
+ oats package add <id> <version | git:<repo>@<ref>> [--dir <d>] --json
557
+ oats package remove <id> [--dir <d>] --json
491
558
  ```
492
559
 
493
- - `upstream` and `base` are **two separate comparisons**. No upstream →
494
- `upstream: {ref:null, ahead:null, behind:null}` — unknown, **not 0/0**. `base`
495
- is against the merge-base with the default branch (`origin/HEAD`, else a
496
- well-known name; `source` says which); none found → all `null` plus a note.
497
- - Status is porcelain v2, NUL-delimited: renames/copies carry `origPath`;
498
- paths with spaces/newlines are intact. `kind` ∈ changed | renamed | copied |
499
- unmerged | untracked. Ignored files are not listed.
500
- - `files[].id` is **opaque**, minted under (`revision`, `indexRevision`). It is
501
- the only way to ask for a diff.
502
- - `files[].additions`, `deletions` and `binary` (0.29.1, additive;
503
- `instanceGitApi` stays 1) are each file's line counts.
504
- - They come from one `git diff <revision> --numstat -z -M` per observation:
505
- the working tree against the observed commit, **staged and unstaged
506
- combined**. That is the baseline of the status letters and of
507
- `oats instance diff`.
508
- - An unborn tree counts against the empty tree. A rename counts on its new
509
- `path`.
510
- - A binary file is `additions: null, deletions: null, binary: true`. An
511
- untracked file (no baseline; its contents are not read) and a submodule
512
- are all `null`.
513
- - If the count itself fails, every entry is `null` and `notes` says line
514
- counts are unavailable. `null` means unknown, never zero.
515
- - `remote` (0.24.8+): the branch's configured remote (`source: branch-upstream`),
516
- else `origin`, else `null` — never invented. `host`/`path` are **parsed** from
517
- the URL (ssh/https forms; `.git` stripped) so an ADE can choose a forge backend
518
- and a `owner/repo` **without running Git**; a local path has `host: null`. No
519
- network, no forge knowledge in the kernel.
520
-
521
- `oats instance diff <instance> --file <id> --revision <rev> [--index-revision <idx>] --json`
522
- returns a bounded unified diff:
560
+ Edits `packages:` only when `oats-workspace.yaml` is tracked by the checkout
561
+ found from `--dir`; otherwise it reports the change to make.
523
562
 
524
563
  ```json
525
- {"instanceGitApi":1,"observation":{…},"file":{"id":"…","kind":"changed","xy":".M","path":"README.md","origPath":null},
526
- "against":"<captured revision oid>","binary":false,"bytes":2683,"truncated":false,"limit":262144,"patch":"diff --git …",
527
- "readOnly":{"helpers":"disabled","optionalLocks":"off","objectsWritten":0}}
564
+ {"action":"add","id":"oats.aweb","value":"v1.17.1","previous":null,"edited":true,"file":"/w/agents-repo/oats-workspace.yaml"}
528
565
  ```
529
566
 
530
- - `against` is the **captured revision oid** for tracked changes (working tree
531
- vs that exact commit, index included — never the moving `HEAD`) and `empty`
532
- for untracked files. Binary → `binary: true`, empty patch. Over 256 KiB →
533
- `truncated: true` at the byte limit.
534
- - **Read-only, helper-free, consistent across the read** (`readOnly` echoes
535
- it): the observed tree may carry a hostile repo config, so external diff,
536
- textconv, fsmonitor and hooks are disabled and the caller's Git environment
537
- and global config are not inherited; `--no-optional-locks` means no index
538
- refresh and no object is written (`ls-files --stage` hash, not `write-tree`).
539
- After producing the patch the CLI re-checks HEAD, index and the file's own
540
- content against the observation and refuses `E_STALE_OBSERVATION` if any
541
- moved mid-read — the result is never internally inconsistent.
542
- - If HEAD or the index moved since the id was minted, or the id is not in the
543
- current observation, the CLI **refuses** with `E_STALE_OBSERVATION` and
544
- attaches the current `observation` in `error.details` — re-observe, never
545
- render a diff against a tree that is not the one on screen. A path in
546
- `--file` is `E_BAD_ARGS`.
547
-
548
- No forge (PR/checks/reviews) data here: forge connections are an ADE/workstation integration (P1 decision), read by the Desktop server through the forge's own CLI; the kernel only reports the instance's `remote` so the ADE can pick a backend.
549
-
550
- ## Instance events (`oats instance events`, `eventsApi: 1` → **2**, OATS 0.24.8+) — K7
551
-
552
- Typed lifecycle events per instance, **written by the kernel action that made
553
- them true**, with the receipt it produced. Nothing is inferred from
554
- transcripts, TASK/STATE files or prose. Append-only, two logs: `<home>/.oats-events.jsonl`
555
- and `<workspace>/.agents/events/<agent>--<instance>.jsonl` (survives the
556
- home's removal, so a retired instance's `retired` event is still readable).
557
-
558
- `oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--dir <d>] --json`
559
-
560
567
  ```json
561
- {"eventsApi":1,"instance":"dev-1","home":"/abs/home","count":7,"returned":7,"truncated":false,
562
- "events":[{"eventsApi":1,"at":"<iso>","instance":"dev-1","home":"/abs/home","producer":"kernel","kind":"spawned","data":{"agent":"dev","work":"worktree","branch":"agents/dev-1","harness":"claude","model":null,"parentInstance":null,"relation":null,"launched":true}},
563
- {"…":"launched | restarted | stopped | stop-refused | retire-planned | worktree-retained | worktree-removed | branch-deleted | retired | child-spawn-refused"}],
564
- "lastEvent":{"kind":"stopped","at":"<iso>","producer":"kernel"},
565
- "waitingOnYou":null,
566
- "notes":["…"]}
568
+ {"action":"add","id":"oats.aweb","value":"v1.17.1","edited":false,"file":null,"line":"packages:\n oats.aweb: v1.17.1","hint":"oats-workspace.yaml is not in this checkout; commit the change in the workspace repo, then `oats sync`"}
567
569
  ```
568
570
 
569
- - `kind` is a closed set (unknown kinds are refused at write). `producer` is
570
- `kernel` for lifecycle facts; a capability may append its own events with
571
- its id as producer (the write API is `appendEvent`, not the renderer).
572
- - **`waitingOnYou` is `null` unless a producer reported it** (`data.waitingOnYou:
573
- true` with a `reason`). `null` means *unknown*, not "not waiting". Today no
574
- kernel path claims it; the Active overview keeps rendering unknown until a
575
- producer (a messaging or review capability) does.
576
- - Window is bounded (`--limit`, default 200; `truncated` says so). A torn line
577
- appears as `kind: "unreadable"` rather than vanishing.
578
-
579
- ### Events API 2 (`eventsApi: 2`, feature `instance-events-2`, OATS 0.24.12+) — K7b
580
-
581
- Gate a Desktop read on **both** `eventsApi === 2` and `"instance-events-2"` in
582
- `features[]`. API 1 is not a sufficient fence for a bounded read: its reader
583
- opened and read a source whole, followed symlinks, kept foreign rows and lost
584
- torn lines and cleared claims silently. API 2:
585
-
586
- - **Bounded, descriptor-safe read.** Each source (`home` =
587
- `<home>/.oats-events.jsonl`, `workspace` = `<ws>/.agents/events/<agent>--<instance>.jsonl`)
588
- is `lstat`ed first; anything but a regular file is **refused unopened**.
589
- The open itself is `O_RDONLY|O_NOFOLLOW|O_NONBLOCK` and the descriptor is
590
- `fstat`ed: it must be a regular file with the same device+inode lstat saw
591
- (closes the lstat→open swap). At most the last 4 MiB is read by descriptor.
592
- **Canonical source shape**: `{path: "home"|"workspace", status: "ok"|"absent"|"refused"|"tail", bytes}`
593
- — `"tail"` means only the last 4 MiB was read (partial first line dropped);
594
- there is no separate `tail` boolean.
595
- - **Row fields.** `incarnation` is the writing home's `instance.json.createdAt`
596
- (ISO) or `null` for rows written before the tag; the result's top-level
597
- `incarnation` is the current home's `createdAt` or `null` if unreadable. A
598
- row with `incarnation: null` never matches the current incarnation, so it
599
- cannot contribute a current waiting claim. **An unknown current incarnation
600
- (top-level `incarnation: null`) admits NO claim**: `waitingOnYou: null`,
601
- `waitingClaims: []`, rows still returned as history. A consumer must refuse
602
- a null-incarnation response that nevertheless carries claims. Dedup identity is
603
- `producer|at|kind|incarnation|data`.
604
- - **`waitingClaims[]` row shape**: `{producer: string, waiting: boolean, since: ISO, reason: string|null}`
605
- — one row per producer with a claim in the current incarnation, INCLUDING
606
- cleared ones (`waiting: false`, `reason: null`, `since` = the clearing row's
607
- `at`). `waitingOnYou` = the newest `waiting: true` row or `null`.
608
- - **Address history.** `--home <abs>` must be a home of exactly `<instance>` under
609
- the scope (`E_HOME_MISMATCH` otherwise, like K1). Rows whose `instance`/`home`
610
- are not the admitted address are dropped and counted (`integrity.foreignRows`).
611
- Every row carries `incarnation` (the writing home's `instance.json.createdAt`);
612
- the result echoes the current home's `incarnation`. Rows tagged with an earlier
613
- incarnation ARE returned — they are this address's history — so a consumer can
614
- label them "earlier instance at this address". No current home → no read
615
- (archived access is a separate contract).
616
- - **`waitingOnYou` is a producer STATE for the current incarnation**, decided per
617
- producer by that producer's latest row that carries the field: an explicit
618
- `false` clears, a row without the field does not; earlier incarnations never
619
- contribute; computed over the FULL admitted read (a `--limit` window cannot
620
- hide a clear). `waitingClaims[]` lists every producer's current claim
621
- (`{producer, waiting, since, reason}`); `waitingOnYou` is the newest positive.
622
- Still `null` today — no producer emits it.
623
- - **Provenance and corruption never disappear.** Dedup is by
624
- `producer|at|kind|incarnation|data` (the same facts from two producers are two
625
- rows). `integrity.unreadableRows` counts torn/invalid lines regardless of
626
- `--since` or the window. `count` = admitted rows after `--since`, `returned` =
627
- the window, `truncated` = window cut OR any source read as a tail.
571
+ `remove` answers `value: null` (and `line: null` when not tracked). An id that
572
+ is not declared is `E_PACKAGE_MISSING {id, …}`; an untracked `remove`
573
+ discovers the workspace over the network to check. Other errors: `E_USAGE`,
574
+ `E_WORKSPACE_SCHEMA` (a bad id or value), `E_REPO_REF`.
628
575
 
629
- ```
630
- {"eventsApi":2,"instance":"dev-1","home":"/abs/home","incarnation":"<iso>","count":7,"returned":7,"truncated":false,
631
- "integrity":{"unreadableRows":0,"foreignRows":0,"sources":[{"path":"home","status":"ok","bytes":1234},{"path":"workspace","status":"ok","bytes":1234}]},
632
- "events":[{"eventsApi":2,"at":"<iso>","instance":"dev-1","home":"/abs/home","incarnation":"<iso>","producer":"kernel","kind":"spawned","data":{...}}, ...],
633
- "lastEvent":{"kind":"launched","at":"<iso>","producer":"kernel","incarnation":"<iso>"},
634
- "waitingOnYou":null,"waitingClaims":[],"notes":[...]}
576
+ ### `oats workspace status`
577
+
578
+ ```text
579
+ oats workspace status [--dir <d>] --json
635
580
  ```
636
581
 
637
- Desktop passes `--limit` (50|100|200) only; `--since` remains a human flag.
638
-
639
- ## Schedule run history (`scheduleApi: 2`, `scheduleHistoryApi: 2` → **3**, OATS 0.24.8+) — K8
640
-
641
- `oats schedule show|list --json` entries gain **`recentRuns`**: the last 50
642
- settled runs (newest first) — every `lastRun` the scheduler recorded once its
643
- outcome settled (`ended | stopped | blocked | invalid | delivered | skipped |
644
- unknown …`, never `active`/`starting`), exactly as the producer wrote it,
645
- deduplicated per run. Where the run launched or targeted an instance, a
646
- `transcript: {instance, home, kind: "session"}` pointer says which home's
647
- session to open (the existing `oats session` surface); the kernel does not
648
- copy transcripts. `nextRun`/`lastRun`/`executionStatus` are unchanged. The
649
- Schedules view (frame 08) renders `recentRuns` as the recent-runs list and the
650
- transcript pointer as the handoff (definition fields are untouched by this
651
- addition). A stored captured definition (removed in 0.26) lists as `invalid`.
652
-
653
- ### History API 3 (`scheduleHistoryApi: 3`, feature `schedule-read-2`, OATS 0.24.13+) — K8b
654
-
655
- Gate a Desktop history read on **both** `scheduleHistoryApi === 3` and
656
- `"schedule-read-2"` in `features[]` (`scheduleApi` stays 2 — mutation verbs are
657
- unchanged). API 2's reader keyed runs by outcome, read state files whole and
658
- unchecked, echoed a stored `definition.id` without checking it, and named a
659
- `transcript` that no reader backs. API 3:
660
-
661
- - **Run identity is time, not outcome.** `runId = sha256(scheduledFor|startedAt|attemptId)[0:24]`.
662
- A run's later facts update its one row; `transitions[]` keeps the outcome
663
- sequence (`["started","unknown","ended"]`); `settled: boolean`
664
- (`pending: true` is never settled); `recordedAt`. Pre-API-3 rows are returned
665
- with `runId: null, legacy: true, settled: null, transitions: null` and are
666
- never merged. `lastRun` carries the same `runId` as its history row.
667
- - **Bounded, descriptor-safe state.** `oats-schedules.json` and
668
- `.agents/schedules/state.json` are `lstat`ed (regular file only), opened
669
- `O_NOFOLLOW|O_NONBLOCK`, `fstat`-verified (dev+ino), and read whole **only
670
- within a 1 MiB budget** — over budget is a typed `E_SCHEDULE_STATE_OVERSIZE`
671
- refusal with `details.source`, never truncated JSON. `list`/`show` carry
672
- `integrity: {sources: [{path: "definitions"|"state", status: "ok"|"absent"|"refused"|"oversize"|"corrupt", bytes}]}`.
673
- History is capped at 50 rows **at read** (`history: {status, stored, truncated}`);
674
- one job's corrupt history (`history.status: "corrupt"`, `recentRuns: []`) or
675
- bad identity (`unreadable: {code, message}`) never fails the other jobs in `list`.
676
- - **Subject truth.** `list` and `show` echo `scope` (the resolved schedule-owning
677
- workspace) and canonical `id`. A definition whose own `id` differs from its
678
- key → `E_SCHEDULE_IDENTITY` (`details.key`, `details.declared`). IDs must match
679
- `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$` (`E_BAD_ARGS` otherwise, before any read).
680
- - **Session provenance, never a transcript.** The `transcript` key is gone.
681
- Each run (and `lastRun`) carries
682
- `session: {instance: string|null, home: string|null, incarnation: string|null, server: string|null, delivery: "launched"|"delivered-active"|"none"}`
683
- — facts the recorder had at write time (an active-session wake names the
684
- instance only when the input result did; `incarnation` = the home's
685
- `instance.json.createdAt` at record time; `server` = the answering peer for a
686
- remote command). **There is no reader behind this block**: a consumer renders
687
- provenance and a precise unavailable reason. A read-only transcript verb is a
688
- separate seam (K12), not implied by this API.
582
+ Read-only (it writes no lock):
689
583
 
584
+ ```json
585
+ {"workspaceStatusApi":1,
586
+ "workspace":{"name":"northwind","key":"github.com/nw/agents","url":"https://github.com/nw/agents.git","commit":"66566512…",
587
+ "observedAt":"2026-09-28T10:07:51.783Z","local":"/w/oats-local.yaml",
588
+ "teams":[{"label":"eng","team":null,"description":"Platform engineering"},{"label":"oats","team":"oats:oats.aweb.ai","description":"The OATS project"}],
589
+ "file":{"path":"oats-workspace.yaml","url":"https://github.com/nw/agents/blob/66566512…/oats-workspace.yaml"}},
590
+ "members":[{"key":"github.com/nw/agents","name":"agents","commit":"66566512…","confirmed":true,"status":"confirmed","detail":null,
591
+ "souls":["rm"],"capabilities":["nw-house-style"],"publishes":null,"url":"https://github.com/nw/agents/tree/66566512…",
592
+ "membershipFile":{"path":"oats-membership.yaml","url":"https://github.com/nw/agents/blob/66566512…/oats-membership.yaml"}}],
593
+ "packages":[{"id":"oats.okf","version":"3.0.0","source":"catalog:oats.okf","commit":"ab897841…","integrity":"sha256-bada35…",
594
+ "capabilities":["oats.okf"],"souls":[],"latest":{"version":"4.0.4","ref":"v4.0.4"}}],
595
+ "declaredPackages":["oats.framework","oats.okf"],"unsynced":["oats.framework"],"stale":[],
596
+ "external":[{"source":"git:github.com/oss/experts@3c606e09…","soul":"security-reviewer"}],
597
+ "problems":[],"warnings":[],
598
+ "automations":{"host":"ana-laptop","snapshot":{"takenAt":"2026-09-28T10:00:00.000Z","problems":0},
599
+ "rows":[{"kind":"schedule","id":"agents/nightly","runsOn":"ana-laptop","owner":"github.com/ana","runsHere":true,"reason":null,"enabledHere":true,
600
+ "origin":{"kind":"workspace","repoKey":"github.com/nw/agents","path":"oats-schedules/nightly.yaml","commit":"66566512…","url":null,"localPath":null}}]},
601
+ "defaults":{"slots":{"knowledge":{"name":"oats.okf","from":"package"},"messaging":"none","tasks":null},
602
+ "capabilities":[{"name":"oats.core","from":"package","off":false}]},
603
+ "clones":[{"key":"github.com/nw/agents","name":"agents","path":"/w/agents-repo","rule":"convention"}],
604
+ "disabledSouls":[],"lock":{"path":"/w/oats-lock.json","lockfileVersion":3}}
690
605
  ```
691
- {"scope":"/abs/ws","scheduleApi":2,"scheduleHistoryApi":3,
692
- "integrity":{"sources":[{"path":"definitions","status":"ok","bytes":812},{"path":"state","status":"ok","bytes":4410}]},
693
- "schedules":[{"id":"nightly","scope":"/abs/ws","scheduleApi":2,"scheduleHistoryApi":3,…,
694
- "history":{"status":"ok","stored":7,"truncated":false},
695
- "recentRuns":[{"runId":"3f…","scheduledFor":"<iso>","startedAt":"<iso>","outcome":"ended","settled":true,"transitions":["started","ended"],"recordedAt":"<iso>",
696
- "session":{"instance":"dev-1","home":"/abs/home","incarnation":"<iso>","server":null,"delivery":"launched"}}]}],
697
- "scheduler":{…}}
606
+
607
+ - `members[]` and `packages[]` are the sync rows (packages from the lock).
608
+ - `declaredPackages`: the ids in `packages:` (standalone: the kernel's
609
+ default). `unsynced`: declared, not locked. `stale`: locked, no longer
610
+ declared. `external[]`: `{source, soul}`.
611
+ - `workspace.teams` (feature `team-model-2`): the shared teams `{label, team,
612
+ description}` by label; `[]` standalone.
613
+ - `problems`, `warnings`: as in sync.
614
+ - `automations` (feature `automations`): `{host, snapshot: {takenAt,
615
+ problems (a count)} | null, rows: [{kind, id, runsOn, owner, runsHere,
616
+ reason, enabledHere, origin, invalid?}]}`.
617
+ - Automation trust (0.30) adds to `warnings`:
618
+ - `{code: "automation-untrusted", kind, id, message, remedy}` for each row
619
+ whose reason is `untrusted`. `remedy` is the `oats-local.yaml` line.
620
+ - `{code: "automation-trust-stale", entry, message}` for a trust entry that
621
+ names no workspace trigger or schedule (a member may not have synced yet).
622
+ - The rest are [Desktop facts](#desktop-facts-feature-desktop-facts-oats-0290).
623
+
624
+ <a id="oats-capabilities---dir---json--capabilitiesapi-1--oats-souls---dir---json--soulsapi-1"></a>
625
+ ### `oats capabilities` and `oats souls`
626
+
627
+ ```text
628
+ oats capabilities [--dir <d>] --json
629
+ oats souls [--dir <d>] --json
698
630
  ```
699
631
 
700
- **Exact shapes (API 3):**
701
- - `schedule list --json` → `result = {scope, scheduleApi: 2, scheduleHistoryApi: 3, integrity, schedules: Entry[], triggers: {count, command: "oats trigger list"}, scheduler}` (`triggers`, 0.28.0: the trigger definitions this listing leaves out).
702
- - `schedule show <id> --json` → `result = {schedule: Entry}` (one level of nesting; `integrity` is NOT on `show` — it is a scope fact reported by `list`).
703
- - `Entry` (readable) = definition fields (`id, kind, home, message|operation, cron, enabled, …`) + `{scope, scheduleApi: 2, scheduleHistoryApi: 3, executionStatus, nextRun: ISO|null, lastRun: Run|null, history, recentRuns: Run[], running: boolean, attempt?, pendingWake?}`.
704
- - `Entry` (unreadable, `list` only) = `{id, scope, scheduleApi: 2, scheduleHistoryApi: 3, unreadable: {code, message}, history: {status: "corrupt", stored: null, truncated: false}, recentRuns: []}` — no definition fields.
705
- - `history` = `{status: "ok", stored: integer, truncated: boolean}` | `{status: "corrupt", stored: null, truncated: false}`.
706
- - `Run` (API 3 row) = producer-written fields (`scheduledFor, startedAt, kind, outcome, …`) + `{runId: string, legacy: false, settled: boolean, recordedAt: ISO, transitions: string[], session}`; `transitions[]` elements are outcome strings in write order, first element = the first recorded outcome.
707
- - `Run` (legacy row) = producer-written fields + `{runId: null, legacy: true, settled: null, transitions: null, session}` — no `recordedAt`, no `key`.
708
- - `Run` (corrupt element) = `{runId: null, legacy: true, corrupt: true}` only.
709
- - `session` = `{instance: string|null, home: string|null, incarnation: ISO|null, server: string|null, delivery: "launched"|"delivered-active"|"none"}`; always present on rows and on `lastRun`.
710
- - `runId` is opaque to consumers: never recompute, never dedup client-side.
711
- - **Refusals** (`ok:false`): `E_SCHEDULE_STATE_OVERSIZE` / `E_SCHEDULE_INVALID` carry `error.details.source = {path, status, bytes}` (+ `field`); `E_SCHEDULE_IDENTITY` carries `error.details.key` and `error.details.declared`; `E_BAD_ARGS` (id shape) carries no details. A refusal has no `integrity` block — `list` refuses as a whole only when a scope file itself is unreadable.
712
- - **Open path** (both files): `lstat` → regular file → `open(O_RDONLY|O_NOFOLLOW|O_NONBLOCK)` → `fstat` regular + same dev/ino + `size ≤ 1 MiB` → read exactly `fstat.size` bytes by descriptor (a file that grows past the budget between lstat and fstat is refused, never partially read).
713
-
714
- ## Spawn preview (`oats spawn … --preview`, `spawnPreviewApi: 1`, OATS 0.24.8+)
715
-
716
- The Spawn modal's fields are backed by the kernel's own decision, taken **before
717
- any side effect**: `oats spawn <agent> [same flags as a real spawn] --preview --json`
718
- runs every preflight a spawn runs (placement, composition, resources,
719
- executable, harness packages, child-spawn policy) and returns what the spawn
720
- *would* do — then returns without creating a home, branch or worktree.
632
+ Every item of every confirmed member, the external souls, and the locked
633
+ packages' capabilities and souls, sorted by name, then origin. Both carry
634
+ `workspace: {name, key, commit}`, `problems` and, on a standalone view,
635
+ `standalone: true`.
721
636
 
722
637
  ```json
723
- {"spawnPreviewApi":1,"preview":true,"agent":"dev","kind":"persistent","instance":"dev-fix-login","home":"/abs/agents/dev/instances/dev-fix-login",
724
- "repo":"/abs/repo","work":"worktree","harness":"claude","model":"opus","modelSource":"explicit","launchConfig":null,"yolo":false,"backend":"tmux",
725
- "branch":"agents/dev-fix-login","base":{"ref":"HEAD","oid":"<oid>"},"worktree":"/abs/agents/dev/instances/dev-fix-login/work",
726
- "relation":null,"parentInstance":null,"policy":{"childSpawns":{"allowed":true,"origin":{"kind":"default","detail":"…"}}},
727
- "executable":"/abs/bin/claude","capabilities":["oats.core"],"skills":["oats-operate","oats-souls"],"task":"…"}
638
+ {"soulsApi":1,"workspace":{"name":"northwind","key":"github.com/nw/agents","commit":"66566512…"},
639
+ "souls":[
640
+ {"name":"writer","origin":"member github.com/nw/mkt @ 46b3494a","kind":"member","repoKey":"github.com/nw/mkt","commit":"46b3494a…",
641
+ "teams":[{"label":"mine","team":"mine:ana.aweb.ai","default":true,"from":"local"},{"label":"global","team":null,"default":false,"from":"shared"}],
642
+ "defaultTeam":{"label":"mine","team":"mine:ana.aweb.ai","from":"deployment"},
643
+ "private":false,"path":"souls/writer","work":"directory","description":"Drafts campaigns.","harness":"pi","model":null,"harnessFrom":"kernel-default",
644
+ "file":{"path":"souls/writer/soul.yaml","url":null},"spawnable":true,"problem":null},
645
+ {"name":"knowledge-maintainer","qualifiedName":"oats.okf/knowledge-maintainer","origin":"package oats.okf v4.0.4","kind":"package","package":"oats.okf",
646
+ "version":"4.0.4","repoKey":"github.com/awebai/oats-okf","commit":"a4ccca02…","teams":null,"defaultTeam":null,"private":false,
647
+ "path":"oats-package/souls/knowledge-maintainer","work":"directory","description":"Reviews harvested knowledge.","harness":"pi","model":null,
648
+ "harnessFrom":"kernel-default","file":{"path":"oats-package/souls/knowledge-maintainer/soul.yaml","url":null},
649
+ "spawnable":false,"problem":{"code":"E_TEAM_UNKNOWN","message":"team \"reviewers\" is not declared (oats-local.yaml#/souls/teams/…)"}}],
650
+ "problems":[]}
728
651
  ```
729
652
 
730
- - **Name / work area**: `instance` is the name: by default the derived shape
731
- `<agent>-<purpose>` (de-duplicated with `-2`, `-3`…), or exactly the
732
- `--name <slug>` the caller gave (see *Instance names* below); `home` and
733
- `worktree` are the canonical paths. The renderer never derives paths.
734
- - **Branch / base** (worktree mode): `branch` defaults to `agents/<instance>`
735
- (`--branch <name>` overrides; validated); `base` is `--base <ref>` resolved
736
- to its commit oid (default `HEAD`). `E_BRANCH_EXISTS` and `E_BASE_UNKNOWN`
737
- are refused in preview and in apply, before anything exists. Apply creates
738
- the worktree **from that exact oid**.
739
- - **Model**: `model`/`modelSource` are the resolved selection. Omitting
740
- `--model` **inherits** the launch configuration's or soul's preference;
741
- `--model @native-default` is the explicit "use the harness's own default"
742
- (`modelSource: "native default (explicit)"`). These are different requests
743
- and the UI must not relabel one as the other.
744
- - **Policy**: `policy.childSpawns` is what this instance will record (soul
745
- declaration / spawn option / default), enforced later by the spawn route for
746
- its children (see readiness).
747
- - **Apply** = the same command without `--preview`; the same inputs yield the
748
- same decisions (instance, branch, base oid). If the world moved between
749
- preview and apply (name taken, branch created, base gone) the apply refuses
750
- with the same typed codes — the preview is a statement, not a reservation.
751
- - Not in preview (later K6 follow-ups): attach-knowledge refs from the
752
- knowledge provider (05 excluded, attach stays), auto-PR intent (ADE-owned,
753
- P1).
754
-
755
- ### Preview API 2 (0.24.9+, feature `spawn-preview-2`) — the safe-mode fence
756
-
757
- **API 1 previews wrote before they returned** (a refused child spawn appended an
758
- event to the parent; a Herdr backend could be started; an unknown soul could be
759
- imported from an importable def). A consumer must therefore gate on
760
- **`spawnPreviewApi === 2` AND `features.includes("spawn-preview-2")`** — API 1 is
761
- the pre-fix marker and is never accepted for dispatch.
762
-
763
- - **No writes, success or refusal.** A preview appends no event, starts no
764
- daemon (`backendStatus {name, installed, started:false}` reports what it
765
- observed), and never creates/updates a soul (`E_SOUL_UNKNOWN` instead of an
766
- import; `--instructions-file`/`--def-file` refused with `E_BAD_ARGS`). Test:
767
- the deployment tree is byte-identical after a success, a refusal and an
768
- unknown-soul preview.
769
- **Workspace deployments (0.26.0+)**: this holds for the FIRST preview of a
770
- soul or commit too. A preview reads the soul from the deployment's per-commit
771
- cache (`agents/<soul>/souls/<commit>/`) when a spawn already filled it, else
772
- fetches it to a temporary copy outside the deployment and removes it
773
- (`soulFetched: true` in the result). Only a spawn fills the cache or moves the
774
- `agents/<soul>/soul` pointer. (0.25.x previews populated the cache: that stated
775
- exception is gone.)
776
- - **Exact root**: `spawn <soul> --agents-root <abs>` binds the soul to that root
777
- (as inspect/readiness take it) — no team-soul / importable-
778
- def fallback; mismatch → `E_SOUL_UNKNOWN`. The preview echoes
779
- `subject {soul, agentsRoot|null, dir|null}` **as given, byte-exact**.
780
- - **Decision binding**: `decision {instance, home, branch, base{ref,oid},
781
- effective{…, providers}, resolution, revision}` (24-hex). From 0.25.6
782
- (`features: served-identity`) `effective.providers` is the merged
783
- per-module payload each provider will receive (`{ "<cap>": {…} }`, exactly
784
- `settings.<cap>` of the preview) — so a confirmed apply binds every
785
- provider fact (an identity choice, a delivery mode) **by value**; a Desktop
786
- that changes a provider field re-previews. From 0.26.0 the merged payload
787
- includes the manifest's declared setting defaults (`settings.<key>.default`,
788
- the lowest layer), and the preview's **`settingsOrigins.<cap>`** maps each
789
- leaf of `settings.<cap>` (a JSON pointer, e.g. `/identity/mode`) to
790
- `{ kind, at }`: `kind` is `manifest-default` | `workspace` | `soul` |
791
- `host` | `spawn` — the last layer that set it. (`workspace-team` no longer
792
- appears since teams amendment K: a label's `byTeam` entry is not merged into
793
- the settings; it is in `teams[].payload`.) —
794
- and `at` names where (`oats.json#/settings/identity/default`,
795
- `soul.yaml#/messaging`, `oats-local.yaml#/settings/<cap>`,
796
- `--provider <cap>`, …). A Desktop labels `manifest-default` values
797
- "Default" from this, instead of hardcoding them (feature
798
- **`settings-origins`**). `instances[].identity` (status)
799
- and `selected.identity` (inspect) carry the served principal a messaging
800
- provider reported: `{ mode: "local"|"global", alias, team, address|null,
801
- resident|null, grant?: { id, expiresAt, scopes }, provider }`; absent when
802
- no provider emitted one. A Desktop offers the identity choice as
803
- `--provider <cap> identity.mode=… identity.resident=…` — there is no
804
- kernel flag for it. Apply with `spawn … --expect-decision <revision>`: the
805
- kernel recomputes name/home/branch/base under the same placement path and
806
- refuses **`E_DECISION_STALE`** with `details.decision` (the fresh one) on ANY
807
- drift — no auto-suffix, no silent re-base, nothing created. A GUI re-previews
808
- and re-confirms; it never second-guesses names or paths. Without the flag the
809
- CLI keeps its legacy auto-suffix for humans.
810
- - **Bounded preflight**: every native probe a preview runs (`pi --list-models`,
811
- `pi list`, `claude plugin list`) shares ONE budget (20 s default), runs in its
812
- own process group and is group-killed on timeout; `preflight {status:
813
- complete|timeout, budgetMs, elapsedMs}` says which. A hanging harness CLI cannot
814
- hang a preview.
815
- - **Confirmed apply contract** (0.24.10+, feature `spawn-apply-2`,
816
- `spawnApplyApi: 1`) — what a GUI may promise at "Confirm spawn":
817
- - `decision` gains **`effective {repo, work, harness, model, launchConfig,
818
- yolo, backend, childSpawns, relation{kind, anchor{instance, agentsRoot}}}`**
819
- and `revision` hashes placement + effective. An inherited default that would
820
- change what launches (the soul's model edited between preview and apply,
821
- say) → `E_DECISION_STALE`. A GUI does not re-resolve anything itself.
822
- - **No effect before the fence**: backend presence and `ensureHerdr` run only
823
- AFTER a successful `--expect-decision` binding and after the placement
824
- reservation. A stale apply with `--backend herdr` starts nothing. (The
825
- parent-policy refusal still appends `child-spawn-refused` to the PARENT's
826
- log on a non-preview apply — that is an audit of a real refusal, not an
827
- effect on the target.)
828
- - **Exclusive placement**: the home is reserved with a non-recursive `mkdir`
829
- immediately after the decision check; a concurrent spawn that lost refuses
830
- **`E_PLACEMENT_TAKEN`** having touched nothing. Two concurrent applies of
831
- one decision yield exactly one home. There is no wider lock; this
832
- reservation is the guarantee. Instance names are deployment-wide
833
- (0.26.0), so right after its reservation a spawn re-checks the whole agents
834
- root: when another soul's concurrent spawn reserved the same name, it
835
- removes its own empty reservation and refuses (`E_INSTANCE_NAME_TAKEN` for a
836
- `--name`, `E_PLACEMENT_TAKEN` for a derived name). At most one wins, and
837
- possibly neither.
838
- - Gate confirmation AND the exec owner on `spawn-preview-2` +
839
- `spawn-apply-2` + `spawn-idempotency`; a legacy local request on such a CLI
840
- is refused by the Desktop (`E_PLAN_REQUIRED`), not routed around the fence.
841
- - **Replay custody** (0.24.10+, feature **`spawn-idempotency-2`** — gate on
842
- this, not on `spawn-idempotency`, whose replay could be blocked by
843
- `E_BRANCH_EXISTS`): key recovery runs **first**, right after the name is
844
- decided and before any placement/branch/base/preflight/backend work — so a
845
- retry of a spawn that created its explicit branch still reaches its receipt.
846
- The key-bearing home records `spawnCompleted:false` at its first write and
847
- `true` only after launch + lineage + final events; a same-key retry of an
848
- unfinished spawn refuses **`E_SPAWN_INCOMPLETE`** (`details.{instance, home,
849
- launched}`; remedy is the session surface, never another spawn). The key
850
- lives in the home by design: durable across the GUI's restart, gone with a
851
- retired home — after a retire, "check result" is a roster question. The wake
852
- outcome is recorded (`wake {requested, saved, error}`) and returned on
853
- replay; `saved:null` means *not recorded* (crash in the interval) — render
854
- "Agent created; wake outcome unavailable — check Schedules", never
855
- saved/not-saved without the record.
856
- - **Retention stays clean**: the completion marker and the wake record are
857
- kernel writes to `instance.json` made after the spawn's retirement baseline;
858
- the kernel re-stamps the baseline's home fingerprint after each, so a fresh
859
- keyed home retires with **no** `changed instance-home bytes` — only the
860
- agent's own changes ever read as work to recover.
861
- - **Idempotent apply** (0.24.10+, feature `spawn-idempotency`): `spawn …
862
- --expect-decision <rev> --idempotency-key <key>` records the key and the
863
- decision in the new home's `instance.json`; a **retry with the same key**
864
- replays the recorded receipt (`replayed: true`, same instance/home, no second
865
- spawn, no wake re-saved) — found by key across the soul's instances, never by
866
- name (the planned name may have been auto-suffixed past it, which is exactly
867
- the retry case). The same key with a *different* decision refuses
868
- **`E_IDEMPOTENCY_CONFLICT`** (`details.instance/home` of the prior spawn); a
869
- different key with a fresh decision is a genuinely new confirmation. Mint the
870
- key server-side on the first confirmation and keep it for that intent's
871
- retries (as 2c does); a lost response is a replay, never a guess by name.
872
- - Still absent (named follow-ups, not parity-done): attach-knowledge node refs
873
- (provider contract), auto-PR (P1/ADE write approval), branch enumeration
874
- (producer seam).
875
-
876
- ## Readiness quartet (`readinessApi: 1`) — removed in 0.26.0
877
-
878
- The quartet (`installed | trusted | configured | enrolled`), its signature
879
- verification (`--verify-signatures`, feature `readiness-verify`) and the
880
- scope subject were removed with the classic config chain. `oats readiness`
881
- answers only [`readinessApi: 2`](#oats-readiness---home---soul---dir---policy---json-readinessapi-2);
882
- `--verify-signatures` is `E_BAD_ARGS`.
883
-
884
- ### Enforced child-spawn policy (`--policy`)
885
-
886
- `childSpawns` is **enforced by the spawn route**: `soul.yaml` may declare
887
- `children: {spawn: false}`; `oats spawn --allow-child-spawns | --no-child-spawns`
888
- overrides per spawn; the result is recorded in `instance.json`
889
- `policy.childSpawns {allowed, origin}`. A spawn with `--parent <p>` (or
890
- `--relation child --relative-to <p>`) under a parent whose recorded policy is
891
- off refuses **`E_CHILD_SPAWNS_DISABLED`** (`details.parent`, `details.policy`)
892
- before anything is created. Absent policy (pre-0.24.8 instances) = allowed,
893
- reported as `origin.kind: "default"`. With `--home <abs>` the policy is the
894
- instance's recorded (enforced) one; with only `--soul` it is the declaration
895
- (`enforced: false`). It is a lifecycle-authority claim, not an OS sandbox —
896
- the UI says so.
897
-
898
- ## Lifecycle plans — Stop and Remove (`lifecycleApi: 1`, OATS 0.24.8+)
899
-
900
- The Desktop's Stop and Remove confirmations render **plans**: a read-only
901
- statement of what the action would touch, with the facts a human needs, and a
902
- `planRevision` hashed from the facts that make the action safe. Apply carries
903
- the revision back; if reality moved, apply **refuses with the fresh plan**
904
- (`E_PLAN_STALE`, `details.plan`) instead of acting on a world the human did
905
- not see. An `idempotencyKey` makes a retried apply return the first receipt.
906
-
907
- ### `oats instance stop <instance> --plan [--no-recursive] [--home <abs>] [--dir <d>] --json`
653
+ **Capability rows:** `name`, `origin` (display text), `kind`, then for a
654
+ member `repoKey, commit, private, path, layer, version`, for a package
655
+ `package, version, commit, private: false, layer`; plus the Desktop facts
656
+ `description, skills, commands, hooks, file, tree`. `private: true` (feature
657
+ `capabilities-private`) marks a capability usable only by its own repository's
658
+ souls (`E_CAPABILITY_PRIVATE` otherwise). An unsynced package's capabilities
659
+ are absent until `sync`.
660
+
661
+ **Soul rows:** `name, origin, kind (member | external | package), repoKey,
662
+ commit, teams, defaultTeam, private (always false), path, work, description`,
663
+ plus the Desktop facts `harness, model, harnessFrom, file, spawnable,
664
+ problem`, and (feature `launch-preference`) `key` and `launch`.
665
+ - `key` is the soul key that `souls.teams`, `souls.default` and `souls.launch`
666
+ use, and that `oats soul teams <key>` takes: `qualifiedName` for a package
667
+ soul, the bare `name` for a member or external soul. Two member souls that
668
+ share a bare name share one entry (spawning that name is
669
+ `E_SOUL_AMBIGUOUS`).
670
+ - `launch` is a [Launch](#the-launch-report-launch).
671
+ - `teams` and `defaultTeam` (feature `team-model-2`) are a
672
+ [TeamRow](#the-team-row-teamrow) list and a
673
+ [DefaultTeam](#the-default-defaultteam); both `null` when the soul's teams
674
+ do not resolve (`problem` names the `E_TEAM_*` code).
675
+ - Package souls (feature `package-souls`) add `qualifiedName`
676
+ (`<package>/<soul>`), `package` and `version`. Spawn one by
677
+ `qualifiedName` (the bare name when unique). Its instances live under
678
+ `agents/<package>--<soul>/` with `.` written `-` (for example
679
+ `oats-okf--knowledge-maintainer`), which is its agent `name` in the roster.
680
+
681
+ This document keeps `soulsApi: 1`; the probe's `soulsApi: 2` is the inspect
682
+ soul row's.
683
+
684
+ <a id="desktop-facts-feature-desktop-facts-oats-0290"></a>
685
+ ### Desktop facts
686
+
687
+ Feature `desktop-facts`: facts the kernel reports so the Desktop never derives
688
+ them. Gate each field below on it.
689
+
690
+ | Document | Fields |
691
+ |---|---|
692
+ | `oats inspect --soul` | `capabilities[].composedFrom`, `capabilitiesOff[]` |
693
+ | `oats souls` rows | `harness`, `model`, `harnessFrom`, `spawnable`, `problem`, `file` |
694
+ | `oats capabilities` rows | `layer`, `description`, `skills`, `commands`, `hooks`, `file`, `tree` |
695
+ | `oats workspace status` | `workspace.file`, `members[].url`, `members[].membershipFile`, `packages[].latest`, `defaults`, `clones`, `disabledSouls`, `lock` |
696
+ | `oats status` instance rows | `startedAt`, `modelFrom`, `identityAddress`; `modules[].current.version` on member rows |
697
+
698
+ - **Souls.** `harness`, `model`, `harnessFrom`: what a spawn starts with when
699
+ no selection flag is given, else `harness: "pi"`, `model: null`,
700
+ `harnessFrom: "kernel-default"` (`"soul"` when the definition names one; a
701
+ v2 soul.yaml cannot). `spawnable`/`problem`: whether a spawn here would
702
+ refuse, resolved without spawning, writing or reaching past the sync cache;
703
+ `problem` is `{code, message}` or `null` (`E_SOUL_DISABLED`,
704
+ `E_TEAM_UNKNOWN`, `E_TEAM_NOT_ELIGIBLE`, `E_CAPABILITY_*`, `E_PACKAGE_*`,
705
+ `E_LOCK_SCHEMA`, `E_REMOTE_*`, …). `file`: `{path, url}` of soul.yaml.
706
+ - **Capabilities.** `layer` on every row, `null` outside the slots.
707
+ `description` or `null`. `skills`, `commands`, `hooks`: names, sorted
708
+ (`skills` is `null` when they cannot be listed). `file`: `{path, url}` of
709
+ `oats.json`, or `null` when unreadable. `tree`: a member capability's Git
710
+ tree id at the member commit; `null` for a package (its fingerprint is the
711
+ lock's `integrity`). Package facts are read from the sync cache; unreadable
712
+ manifests leave them `null`.
713
+ - **`defaults`**: the workspace file's defaults as declared. `slots.<slot>` is
714
+ `{name, from}`, `"none"` or `null`; `capabilities` are `{name, from, off}`
715
+ by name (`from` is `"package"`, `"here"` or a member key; `null` when off).
716
+ Standalone: every slot `null`, `capabilities: []`.
717
+ - **`clones[]`**: `{key, name, path, rule}` with `rule` `"clones"`,
718
+ `"convention"` or `null`. A path that is not the member's clone gives
719
+ `path: null, rule: null, problem: {code: "E_CLONE_MISMATCH", message}`.
720
+ - **`disabledSouls`**: `souls.disabled` as written. **`lock`**: `{path,
721
+ lockfileVersion}`.
722
+ - **`packages[].latest`**: `{version, ref}` when the kernel's bundled catalog
723
+ has a newer version of a catalog package; `null` otherwise and for `git:`
724
+ packages. No network (`OATS_PACKAGE_CATALOG` overrides the catalog).
725
+ - **`workspace.file`**, **`members[].url`**, **`members[].membershipFile`**:
726
+ `{path, url}` at the named commit (`workspace.file` is `null` standalone).
727
+ - **URLs** exist only for `github.com` (`…/blob/<commit>/<path>` or
728
+ `…/tree/<commit>`); any other host gives `url: null` with `path` set.
729
+ `path` is repository-relative.
730
+
731
+ <a id="team-model-v2-feature-team-model-2-oats-0300-replaces-feature-teams"></a>
732
+ ## Teams
733
+
734
+ Feature `team-model-2`: gate every team field and verb on it. Design:
735
+ [team model v2](design/2026-09-27-team-model-v2.md); operator guide:
736
+ [workspaces.md](workspaces.md).
737
+
738
+ - **Shared teams** live in the committed `oats-workspace.yaml`:
739
+ `teams.<label> = {description?, team?}`. A shared team without `team` (the
740
+ provider id) is **unmapped**.
741
+ - **Local** configuration lives in `oats-local.yaml`: `teams.<label> = {team,
742
+ description?}`, `defaultTeam: <label>`, `souls.teams: {"*": [labels],
743
+ "<soul>": [labels]}` and `souls.default: {"<soul>": <label>}`. A soul key is
744
+ the spawn name (`<package>/<soul>` for a package soul). A label matches
745
+ `[a-z0-9][a-z0-9._-]*`.
746
+ - **Team ids.** A `team` value (in either file, and `oats teams add --team`)
747
+ matches `^[A-Za-z0-9][A-Za-z0-9._:@/+-]{0,255}$`: the kernel's safety rule
748
+ (never `-`-led, no whitespace or control characters, bounded). Otherwise
749
+ `E_WORKSPACE_SCHEMA` (a file) or `E_BAD_ARGS` (the verb). The messaging
750
+ provider validates its own id shape (oats.aweb: `<name>:<namespace>`).
751
+ - **Resolution.** The soul's default is `souls.default[soul] ??
752
+ defaultTeam`; its teams are that default plus `souls.teams["*"]` plus
753
+ `souls.teams[soul]`. A label in both files is a `team-label-collision`
754
+ warning, and the shared definition wins. An undeclared label is
755
+ `E_TEAM_UNKNOWN`; a `souls.default` outside the soul's teams is
756
+ `E_TEAM_NOT_ELIGIBLE`.
757
+ - **Removed keys** are `E_WORKSPACE_SCHEMA` with `reason: "removed-key"`:
758
+ `messaging.byTeam`, `defaults.byTeam`, a soul.yaml `team`, an
759
+ oats-membership.yaml `team`, and `byTeam` in any provider payload layer.
760
+ Other payload keys are opaque (a `team` setting passes through).
761
+
762
+ ### The team row (`TeamRow`)
763
+
764
+ Exactly `{label, team, default, from}`:
908
765
 
909
766
  ```json
910
- {"lifecycleApi":1,"action":"stop","instance":"dev-1","home":"/abs/home","recursive":true,"at":"<iso>",
911
- "targets":[{"instance":"dev-1-child","agent":"dev","home":"/abs/child","depth":1,"workMode":"worktree","launched":true,
912
- "session":{"state":"unknown","present":true,"backend":"tmux","established":true},
913
- "work":{"observed":true,"revision":"<oid>","branch":"feat/x","detached":false,"drift":false,"changed":2,"untracked":1,"upstream":{"ref":null,"ahead":null,"behind":null},"base":{"ref":"origin/main","ahead":1,"behind":0},"remote":{"host":"github.com","path":"acme/one"}},
914
- "retiring":false,"stopPending":false,"midTask":true}],
915
- "skipped":[],"planRevision":"<24 hex>","notes":[]}
767
+ [{"label":"antares","team":"antares:ana.aweb.ai","default":true,"from":"local"},
768
+ {"label":"oats","team":"oats:oats.aweb.ai","default":false,"from":"shared"},
769
+ {"label":"reviewers","team":null,"default":false,"from":"shared"}]
916
770
  ```
917
771
 
918
- - `targets` are the instance's **recorded descendants deepest-first, then the
919
- instance** (recorded parentage — `parentInstance` — is the only relation the
920
- kernel knows). `--no-recursive` lists them under `skipped` instead.
921
- - `session.state` is the backend's word: `shell`/`stopped`/`not-launched` are
922
- idle; `unknown` means a non-shell process is running whose identity tmux
923
- cannot name (the ordinary state of a launched harness). If the state **could
924
- not be established**, `established:false`, `present:null`,
925
- `state:"unestablished"`, with a `reason` — render that as unknown, never as
926
- idle.
927
- - `work` is K1's observation (`observed:false` with a `reason` when there is no
928
- work tree or it cannot be read — not "clean").
929
- - `midTask` is **reported** activity: `true` (running session or dirty work),
930
- `false` (established idle and observed clean), or `"unknown"`.
931
-
932
- ### `oats instance stop <instance> --apply --plan-revision <rev> --idempotency-key <key> [--no-recursive] [--grace-ms <n>] --json`
933
-
934
- Quiesces each target (SIGTERM to the harness processes, bounded wait, **never
935
- escalated**), children first, under a per-home stop marker; retains home, work
936
- tree, transcript and launch configuration so `oats session restart` brings the
937
- instance back. Refuses `E_PLAN_STALE` (fresh plan attached),
938
- `E_INSTANCE_RETIRING`, `E_LIFECYCLE_BUSY`.
772
+ `team` is `null` for an unmapped team. `default` is true on exactly the
773
+ soul's default row; the others are teams an instance may join (offered, never
774
+ joined automatically). `from` is `"shared"` or `"local"`. The default row
775
+ comes first, then the rest by label (codepoint order). Reports include
776
+ unmapped rows; `OATS_TEAMS` and `instance.json.teams` carry mapped rows only.
777
+
778
+ ### The default (`DefaultTeam`)
939
779
 
940
780
  ```json
941
- {"lifecycleApi":1,"action":"stop","instance":"dev-1","home":"/abs/home","idempotencyKey":"k","planRevision":"<rev>","at":"<iso>",
942
- "ok":false,"results":[{"instance":"dev-1-child","home":"/abs/child","ok":false,"code":"E_SESSION_STOP_FAILED","message":"…still running after 1500 ms; nothing was escalated","stillRunning":[4242]},
943
- {"instance":"dev-1","home":"/abs/home","ok":true,"stopped":true,"alreadyIdle":false,"state":"shell"}],
944
- "retained":["home","work","transcript","launch"],"replayed":false}
781
+ {"label":"antares","team":"antares:ana.aweb.ai","from":"deployment"}
945
782
  ```
946
783
 
947
- `ok:false` means at least one target is still running; the receipt says which
948
- pid. Nothing was killed harder. A replay (`replayed:true`) is the recorded
949
- receipt for that key, not a second action.
784
+ `from` is `"deployment"` (`defaultTeam`) or `"soul"` (`souls.default`). It is
785
+ `null` only when no default is configured (with messaging active, that is
786
+ `E_TEAM_UNCONFIGURED`). An unmapped default is `{label, team: null, from}`,
787
+ the blocking problem `team-unmapped`.
788
+
789
+ <a id="where-teams-appear"></a>
790
+ ### Where teams appear
791
+
792
+ | Document | Team fields |
793
+ |---|---|
794
+ | `oats spawn … --preview` | `teams` (unmapped included), `defaultTeam` |
795
+ | `oats inspect --soul` | `teams`, `defaultTeam`, `teamsSource: "live"` |
796
+ | `oats inspect --home` | `teams`, `defaultTeam`, `teamsSource` (`"live"`, or `"recorded"` when the workspace cannot be read; `teams: null` when none was recorded), `recordedDefaultTeam` |
797
+ | `oats readiness` | items under `checks.configured` |
798
+ | `oats souls` rows | `teams`, `defaultTeam` (`null` when they do not resolve) |
799
+ | `oats workspace status` | `workspace.teams` |
800
+ | `instance.json` | `teams` (mapped rows), `defaultTeam` |
801
+
802
+ A home's live teams are the files as they are now; a running instance keeps
803
+ its spawn-time default until respawned (readiness says so with
804
+ `default-team-changed`). No document carries a soul-level `team`, `labels`,
805
+ `primary`, `mapped` or `byTeam`, and no capability origin is `team:<label>`.
806
+
807
+ **Provider environment.** Hooks, commands, operations and provider checks get
808
+ the teams in `OATS_DEFAULT_TEAM`, `OATS_DEFAULT_TEAM_ID`,
809
+ `OATS_DEFAULT_TEAM_FROM`, `OATS_TEAMS` and `OATS_TEAMS_SOURCE`:
810
+ [capabilities.md](capabilities.md#teams-in-the-provider-environment).
811
+ `OATS_WORKSPACE_NAME` is the recorded workspace name (the discovered one for a
812
+ soul subject), `""` when unknown.
813
+
814
+ ### `oats teams`
950
815
 
951
- ### `oats retire <instance> --plan [--home <abs>] [--dir <d>] --json`
952
-
953
- What Remove would touch, with the design's defaults. Read-only.
816
+ ```text
817
+ oats teams [--dir <d>] --json
818
+ oats teams add <label> --team <id> [--description <d>] --json
819
+ oats teams remove <label> --json
820
+ oats teams default <label> --json
821
+ ```
954
822
 
955
823
  ```json
956
- {"lifecycleApi":1,"action":"retire","instance":"dev-1","home":"/abs/home","at":"<iso>",
957
- "facts":{"session":{…},"work":{…K1 summary…},"workMode":"worktree","repo":"/abs/repo","recordedBranch":"agents/dev-1",
958
- "children":[{"instance":"dev-1-child","agent":"dev","home":"/abs/child","session":{…}}],"pullRequest":"unknown"},
959
- "defaults":{"retainWorktree":true,"deleteBranch":false,"stopChildren":true,"retainChildren":true},
960
- "planRevision":"<24 hex>","notes":["the worktree is on feat/x, not the recorded agents/dev-1; branch actions use the worktree's branch", "…"]}
824
+ {"teamsApi":1,"deployment":"/w","defaultTeam":"antares",
825
+ "teams":[{"label":"antares","team":"antares:ana.aweb.ai","description":null,"from":"local","default":true,"at":"oats-local.yaml#/teams/antares"},
826
+ {"label":"reviewers","team":null,"description":null,"from":"shared","default":false,"at":"github.com/awebai/oats:oats-workspace.yaml#/teams/reviewers"}],
827
+ "souls":{"teams":{"*":["oats"],"oats-expert":["reviewers"]},"default":{"oats-expert":"oats"}},
828
+ "problems":[{"code":"team-unmapped","label":"reviewers","default":false,"severity":"warning","at":"github.com/awebai/oats:oats-workspace.yaml#/teams/reviewers",
829
+ "message":"shared team reviewers has no provider id yet","fix":"its owner runs `oats aweb setup`, then commits the id"}]}
961
830
  ```
962
831
 
963
- `pullRequest` is **always `"unknown"` from the kernel**: forge facts belong to
964
- the ADE's connection (P1). Branch actions use the **worktree's** branch
965
- (`facts.work.branch`), never `recordedBranch`.
832
+ - `defaultTeam` is the deployment's label (or `null`), not a `DefaultTeam`.
833
+ - `teams[]`: every declared team by label, `{label, team, description, from,
834
+ default, at}`; `at` is a pointer into `oats-local.yaml` or
835
+ `<workspace key>:oats-workspace.yaml#/teams/<label>`. A collision shows the
836
+ shared definition.
837
+ - `souls`: `souls.teams` and `souls.default` as written. `problems`: the
838
+ deployment's [team readiness items](#team-readiness-items).
839
+ - The verbs never call a provider. They validate, rewrite `oats-local.yaml` in
840
+ place, and answer the document plus `changed: bool`.
841
+ - **`add`**: the first team added also becomes `defaultTeam`. A label already
842
+ declared is `E_TEAM_EXISTS {label, from}`; a bad label or no `--team` is
843
+ `E_BAD_ARGS`.
844
+ - **`remove`**: a referenced label is `E_TEAM_IN_USE {label, usedBy}` (each
845
+ `"defaultTeam"`, `"souls.teams:<key>"` or `"souls.default:<key>"`); a shared
846
+ label is `E_TEAM_SHARED {label, at}` (a label in both files can be removed
847
+ locally); unknown is `E_TEAM_UNKNOWN {label}`.
848
+ - **`default`**: any declared label, else `E_TEAM_UNKNOWN`.
849
+ - A write that would introduce an unknown or ineligible reference is refused
850
+ with that code; an invalid result is `E_WORKSPACE_SCHEMA`.
851
+
852
+ ### `oats soul teams`
966
853
 
967
- ### `oats retire <instance> [--discard-worktree] [--delete-branch] --json` — retention is the default (K3b)
968
-
969
- Plain `retire` now **retains** a worktree-mode instance's work: the worktree
970
- cannot stay under the removed home, so it is **re-homed** with
971
- `git worktree move` to `<workspace>/.agents/worktrees/<repo>/<branch>` (a
972
- `-2`, `-3` suffix if taken; detached → `detached-<oid12>`), with staged,
973
- unstaged and untracked state intact, and the repository knows the new
974
- location. The receipt says so:
854
+ ```text
855
+ oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <label> | --clear-default] [--dir <d>] --json
856
+ ```
975
857
 
976
858
  ```json
977
- {"retired":"dev-1","retention":{"worktree":"retained","movedTo":"/ws/.agents/worktrees/repo/feat-x","branch":"feat/x","detachedAt":null,"recordedBranch":"agents/dev-1"},
978
- "worktreeRemoved":false,"branchDeleted":false, "workRecovery":{…}}
859
+ {"soulTeamsApi":1,"soul":"oats-expert","key":"oats-expert","defaultTeam":{"label":"oats","team":"oats:oats.aweb.ai","from":"soul"},
860
+ "teams":[{"label":"oats","team":"oats:oats.aweb.ai","default":true,"from":"shared","via":["default","*"]},
861
+ {"label":"reviewers","team":null,"default":false,"from":"shared","via":["soul"]}],
862
+ "local":{"teams":["reviewers"],"default":"oats"},"all":["oats"]}
979
863
  ```
980
864
 
981
- - `--discard-worktree` restores removal (`retention.worktree: "removed"`).
982
- - `--delete-branch` deletes the **worktree's verified branch**
983
- (`retention.branchDeleted`), never the recorded spawn name, and implies
984
- discarding the worktree (a checked-out branch cannot be deleted).
985
- - A failed move keeps the home and refuses `E_WORK_PRESERVATION_FAILED` —
986
- nothing is lost; retry or pass `--discard-worktree`.
987
- - When a recovery's Git status disagrees with the source's (0.27.2),
988
- `E_WORK_PRESERVATION_FAILED` carries `details: {home, statusDisagreement:
989
- {repo, rows: [{path, source, recovery}], total}}`. `repo` is `.` or a nested
990
- repository's path. `source`/`recovery` are the porcelain `XY` codes, or
991
- `null` where that side has no row. `rows` holds the first 10 paths, sorted,
992
- and `total` counts all of them. The message names the same rows.
993
- - Non-worktree modes report `retention: null`. Quarantine/rollback paths keep
994
- their removal semantics.
995
- - A recovery (`workRecovery`, `workRecoveries[]`) is `{path, classes, bytes,
996
- outputs?, repoCopy?}` (0.26.0: `bytes`, `outputs`): `bytes` is the recovery's
997
- own size; `outputs: {paths: [{path, bytes}], bytes}` names what it copied
998
- beyond tracked state — a worktree's untracked and ignored paths, or a
999
- directory's work entries — grouped by top-level entry, largest first. Absent
1000
- when only home bytes were copied.
1001
- - The Remove dialog's "also delete worktree / branch" checkboxes map to these
1002
- two flags; the kernel never touches a PR.
1003
- - **Guarded apply** (what a GUI sends): `oats retire <i> --plan-revision <rev>
1004
- --idempotency-key <key> [--discard-worktree] [--delete-branch] --json`. The
1005
- revision is revalidated against a fresh plan first — facts moved →
1006
- `E_PLAN_STALE` with `details.plan` (re-render, re-confirm; nothing retired);
1007
- a repeated key **replays** the recorded receipt (`replayed: true`, JSON-v1
1008
- envelope) instead of retiring twice. A first retire prints its raw receipt
1009
- (pre-existing shape) with `planRevision`/`idempotencyKey`/`replayed:false`
1010
- added. Mint the key server-side per confirmation intent and keep it for that
1011
- intent's retries.
1012
- - **Children first, kernel-owned.** The plan's `facts.children` are stopped
1013
- by the kernel before retirement (bounded SIGTERM, never escalated) and
1014
- retained; the receipt lists `childrenStopped[]`. A child still running
1015
- after the grace **refuses the whole retirement** — `E_CHILDREN_RUNNING`
1016
- with `details.childrenStopped` (pids) and `details.plan`; nothing retired.
1017
- - **Branch deletion is bound to the confirmed branch.** The kernel re-verifies
1018
- the worktree's branch at the moment of deletion, after hooks (which may
1019
- mutate the tree); a mismatch deletes nothing and reports
1020
- `retention.branchDeletionSkipped {expected, actual, reason}`.
1021
- - **Ambiguous parentage is reported, never acted on.** Recorded parentage is
1022
- a bare name; if a child's parent name resolves to several homes under the
1023
- root, that child appears under `ambiguous[]` — `plan.ambiguous` on a stop
1024
- plan, `plan.facts.ambiguous` on a retire plan — with the reason, and is
1025
- excluded from `targets`/`children`.
1026
- - **Stop replay horizon**: stop receipts are stored **per idempotency key**
1027
- (`<home>/.oats-stop-receipt.<key>.json`); any earlier key replays its own
1028
- receipt for as long as the home exists. Retire receipts live beside the
1029
- instances directory and replay after the home is gone.
1030
-
1031
- ### Feature advertisement — gate every new command on the probe
1032
-
1033
- `oats version --json` `features` now lists: `instance-git`,
1034
- `instance-git-remote`, `souls-declarations`, `lifecycle-plans`,
1035
- `retire-retention`, `readiness`, `spawn-preview`, `instance-events`,
1036
- `schedule-history`, and carries the API integers (`instanceGitApi`, `soulsApi`,
1037
- `lifecycleApi`, `readinessApi`, `spawnPreviewApi`, `eventsApi`,
1038
- `scheduleHistoryApi`, `operationsApi`). In 0.26.0, `soulsApi`, `readinessApi`
1039
- and `operationsApi` are **2** ([the workspace-model inspect](#inspect-readiness-and-operation-run-on-the-workspace-model-operationsapi-2-soulsapi-2-readinessapi-2-oats-0260)). **Gate on these, never on a version string and never by
1040
- optimistic invocation**: an older CLI ignores an unknown `--plan` on `retire`
1041
- and *retires*. Absent feature → the view is unavailable. (`catalog` was the
1042
- 0.24 `oats catalog` verb's flag; the verb is removed under the workspace model
1043
- and the flag is no longer advertised — the official catalog is reached through
1044
- `packages:` + `oats sync`, not a command.)
1045
-
1046
- ## Workspace model (`workspaceApi: 2`)
1047
-
1048
- Features: **`workspace-v2`** (the declaration files, `sync`, `package`,
1049
- `workspace status`, `capabilities`, `souls`; `init`/`use`/`install`/`restore`
1050
- removed), **`instance-modules`** (`instance.json.modules` / `providers` /
1051
- `workspace`; `status --json` module drift; preview `modules[]`),
1052
- **`spawn-provider-payload`** (`oats spawn … --provider <cap> k=v`). The probe
1053
- carries `workspaceApi: 2`. Model: [workspaces.md](workspaces.md).
1054
-
1055
- Every command below needs a deployment with `oats-local.yaml` (walked up from
1056
- `--dir`/cwd) — else `E_LOCAL_MISSING { dir, searched[] }` — and reads the
1057
- workspace over Git remotes with the operator's credentials, never prompting
1058
- (`E_REMOTE_UNREADABLE { url, reason: "auth"|"not-found"|"network"|"timeout" }`).
1059
- Every `commit` is a full 40-hex OID; every digest is `sha256-<hex>`; every
1060
- `at`/`observedAt` is ISO-8601 UTC. Repo keys are canonical
1061
- (`github.com/org/repo`; `local/<abs-path>` for file remotes).
1062
-
1063
- ### Removed verbs answer `E_UNKNOWN_COMMAND` with a replacement
1064
-
1065
- `install`, `restore`, `init`, `use`, `trust`, `list`, `catalog`, `remove`,
1066
- `migrate`, `config` — checked before capability dispatch, both modes:
865
+ - Rows are `TeamRow` plus `via`, a non-empty ordered subset of `"default"`,
866
+ `"*"` and `"soul"`. `key` is the soul's `souls.*` key; `local` its own
867
+ entries; `all` is `souls.teams["*"]`.
868
+ - For `'*'`: `soul` and `key` are `"*"`, `teams` the deployment default plus
869
+ `souls.teams["*"]`, `local.default: null`.
870
+ - Mutations answer the document plus `changed`. Unknown label:
871
+ `E_TEAM_UNKNOWN {label}`. A `--default` outside the soul's teams:
872
+ `E_TEAM_NOT_ELIGIBLE {soul, label, at}` (`at`: `oats-local.yaml#/souls/default/<key>`,
873
+ `/` written `~1`). `--default`/`--clear-default` with
874
+ `'*'`, or both together: `E_BAD_ARGS`. Soul lookup: `E_SOUL_UNKNOWN`,
875
+ `E_SOUL_AMBIGUOUS`.
876
+ - **Writes** (`oats teams add|remove|default`, `oats soul teams`) edit
877
+ `oats-local.yaml` in place and touch only the entries that change: comments
878
+ and styles elsewhere, including inline comments on sibling entries and flow
879
+ lists, are kept. Each verb re-reads the file and judges its refusals on it
880
+ as it is now, and writes only if the file did not change meanwhile (else it
881
+ redoes the edit on the new content). A file that keeps changing is
882
+ `E_LOCAL_CHANGED {path}`; nothing was written.
883
+
884
+ ### The messaging provider's teams document
885
+
886
+ The messaging provider's `teams` operation (`messaging:teams`) is produced by
887
+ the provider (oats.aweb 1.17 or later), not the kernel:
1067
888
 
1068
889
  ```json
1069
- {"schemaVersion":1,"ok":false,"error":{"code":"E_UNKNOWN_COMMAND","message":"unknown command \"install\" — removed by the workspace model v2; use oats sync","details":{"removed":"install","replacement":"oats sync"}}}
890
+ {"defaultTeam":{"label":"antares","team":"antares:ana.aweb.ai","from":"deployment"},
891
+ "eligible":[{"label":"oats","team":"oats:oats.aweb.ai","joined":true}],
892
+ "joined":[{"label":"oats","team":"oats:oats.aweb.ai","identityHome":"/w/agents/oe/instances/oe-1/.aw-teams/oats","receive":"live","since":"2026-09-28T09:00:00.000Z"}],
893
+ "left":[{"label":"reviewers","team":"reviewers:acme.aweb.ai","at":"2026-09-28T09:30:00.000Z","reason":"no-longer-eligible"}],
894
+ "at":"2026-09-28T10:00:00.000Z"}
1070
895
  ```
1071
896
 
1072
- ### oats onboard (onboardApi 2)
897
+ `defaultTeam` is the kernel's `DefaultTeam` from the environment. `eligible`
898
+ are the non-default `OATS_TEAMS` rows; `joined[].receive` is `live` or
899
+ `poll`; `left` holds the last 20 leaves. A join or leave answer adds
900
+ `actions: [{action: "join" | "leave", label, released?, receipt?}]`. There is
901
+ no `primary` and no `unmapped`: unmapped teams are kernel readiness items.
1073
902
 
1074
- `oats onboard [<dir>] --workspace <repo ref> [--json]`
903
+ <a id="team-readiness-items"></a>
904
+ ### Team readiness items
1075
905
 
1076
- The **bootstrap** of a deployment (decision 9): realizes a workspace on this
1077
- machine in the directory the operator chooses (any existing folder). It writes
1078
- `<dir>/oats-local.yaml` (`{ schemaVersion: 2, workspace: <ref> }`), creates
1079
- `<dir>/agents/` (the instance homes), then runs exactly the `oats sync` body
1080
- over the directory just written — discover over the remotes, confirm
1081
- membership, resolve `packages:`, write `oats-lock.json`. It installs nothing, creates no soul, spawns nothing
1082
- and writes no `oats-config.yaml`; the member clones and the setup-expert spawn
1083
- are printed as next steps. `<dir>` defaults to cwd; `--dir <d>` is the same
1084
- argument (give it once). `--workspace` is required and must be a ref
1085
- `lib/remote.mjs` parses (`E_REPO_REF`) — checked **before** anything is
1086
- written. Captured selectors are refused (`E_UNSUPPORTED_MODE`: the captured/portable path was removed in 0.26).
906
+ `oats teams` lists them under `problems[]` as `{code, severity: "failure" |
907
+ "warning", message, fix, …}`. `oats readiness` lists the soul's under
908
+ `checks.configured` with `subject` `"team <label>"` (or `"teams"`), `producer:
909
+ "team model"`, `code`, `reason`, `remedy`, `status: "fail"`, `required: true`
910
+ for a failure and `false` for a warning, plus the problem's own keys.
911
+
912
+ | Code | Severity | Keys | When |
913
+ |---|---|---|---|
914
+ | `E_TEAM_UNCONFIGURED` | failure | | messaging is active and the soul has no default |
915
+ | `team-unmapped` | failure if `default`, else warning | `label`, `default`, `at` | a shared team without `team` |
916
+ | `team-label-collision` | warning | `label`, `shared`, `local` (each `{team, description, at}`) | a label in both files |
917
+ | `default-team-changed` | warning | `recorded`, `current` | `--home` with live teams: the default changed since the spawn |
918
+ | `E_TEAM_UNKNOWN` | failure | `label`, `at` | a reference to an undeclared label |
919
+ | `E_TEAM_NOT_ELIGIBLE` | failure | `soul`, `label`, `at` | `souls.default` outside the soul's teams |
920
+
921
+ The last two are also spawn, preview and inspect refusals, with the same
922
+ details.
923
+
924
+ <a id="soul-launch-preferences-feature-launch-preference-oats-0300"></a>
925
+ ## Launch preferences
926
+
927
+ Feature `launch-preference` (OATS 0.30.0): gate every field and flag below on
928
+ it. Design: [soul launch preferences](design/2026-09-28-soul-launch-preference.md).
929
+ Every object shape here is closed.
930
+
931
+ - **The soul** may declare `launch: {harness, model?}` in soul.yaml.
932
+ - `harness` is `pi`, `claude` or `codex`. `model` is a non-empty model id for
933
+ that harness.
934
+ - Nothing else is allowed (no args, env, yolo or executable; those stay host
935
+ facts, in launch configurations). Another key is `E_WORKSPACE_SCHEMA`.
936
+ - A package soul may declare one too.
937
+ - **The machine** may override it in `oats-local.yaml` `souls.launch`. A key is
938
+ a soul key (as for `souls.teams`: `"*"`, a bare soul name, or
939
+ `<package>/<soul>`). A value is a `launch-configs` name in the same file, or
940
+ an inline `{harness, model?}` with the soul's rules.
941
+ ```yaml
942
+ souls:
943
+ launch:
944
+ "*": opus # every soul on this machine
945
+ oats-expert: opus # a launch configuration
946
+ oats.engineering/code-reviewer: {harness: codex} # an inline preference
947
+ ```
948
+ - **Migration.** 0.29.x refuses an unknown soul.yaml key, and members are read
949
+ at their latest commit. A committed soul gains `launch:` only on the flag day
950
+ (every deployment of the workspace runs 0.30). Until then, use `souls.launch`
951
+ or `--launch-config`.
952
+
953
+ **Precedence** for a new selection. The first layer with a value decides:
954
+ 1. The flags: `--launch-config` or `--harness` (`from: "flag"`). `--model`
955
+ alone keeps the next deciding layer's harness and replaces only its model.
956
+ 2. `souls.launch.<key>` (`"local"`).
957
+ 3. `souls.launch."*"` (`"local-default"`).
958
+ 4. The soul's `launch` (`"soul"`).
959
+ 5. The host default: `pi`, its own model, no configuration (`"host"`).
960
+
961
+ - A **launch configuration** name runs that configuration's full recipe.
962
+ - An **inline or soul preference** runs its harness with this host's baseline
963
+ for it (the executable the host resolves; no args, no env) and its `model`.
964
+ - A preference is a unit. Without `model`, the harness's own model runs; a
965
+ lower layer's model is never borrowed. A model never crosses harnesses
966
+ (`E_MODEL_UNKNOWN`, as before).
967
+
968
+ **Existing homes.** A home's recorded launch is frozen. A plain `session
969
+ start`/`restart` runs it unchanged. The precedence decides only a new
970
+ selection: a spawn, a start/restart with `--launch-config` or `--harness`, or
971
+ a start/restart with `--reselect-launch`, which applies the current layers
972
+ without naming anything. A start/restart with `--model` alone is not a new
973
+ selection: it keeps the recorded harness (and configuration) and replaces only
974
+ the model (`modelFrom: "start"`; `launchFrom` is unchanged). **A changed preference does not affect
975
+ a running or existing home until `--reselect-launch` or a respawn.** Readiness
976
+ shows the drift as `launch-changed`.
977
+
978
+ **Refusals** (spawn, preview, and a reselecting start):
979
+ - `E_HARNESS_UNAVAILABLE {harness, from, at, fix}`: the chosen harness has no
980
+ executable on this machine. There is no fallback to another harness. `fix`
981
+ is "install <harness>, or override it on this machine in oats-local.yaml
982
+ souls.launch" (from the soul or the host), "install <harness>, or change
983
+ oats-local.yaml souls.launch" (from local layers), or "install <harness>, or
984
+ choose another --harness / --launch-config" (from a flag).
985
+ - `E_LAUNCH_CONFIG_UNKNOWN {name, from, at}`: `souls.launch` names a launch
986
+ configuration this `oats-local.yaml` does not declare.
987
+ - `E_WORKSPACE_SCHEMA`: a malformed `launch` or `souls.launch`, path named.
988
+
989
+ ### The launch report (`Launch`)
1087
990
 
1088
991
  ```json
1089
- {"onboardApi":2,
1090
- "local":"/abs/acme-workspace/oats-local.yaml","dir":"/abs/acme-workspace","agents":"/abs/acme-workspace/agents",
1091
- "lock":"/abs/acme-workspace/oats-lock.json",
1092
- "sync":{"syncApi":1,"…":"the full sync report (next section)"},
1093
- "hosting":{"host":"github.com/acme/agents","hostIsMember":true,
1094
- "rule":"If any member is private, host oats-workspace.yaml in a private repo that is not a public member (a dedicated <org>/workspace repo); public contributors then use the standalone case (from: here capabilities + oats.core)."},
1095
- "next":{"clone":[{"key":"github.com/acme/agents","name":"agents","url":"https://github.com/acme/agents.git","dir":"/abs/acme-workspace/agents-repo"},
1096
- {"key":"github.com/acme/platform","name":"platform","url":"https://github.com/acme/platform.git","dir":"/abs/acme-workspace/platform"}],
1097
- "spawn":"oats spawn oats-setup-expert --dir /abs/acme-workspace"}}
992
+ {"declared":{"harness":"claude","model":"claude-opus-5-5"},
993
+ "effective":{"harness":"codex","model":null,"launchConfig":null},
994
+ "from":"local","at":"oats-local.yaml#/souls/launch/oats.engineering~1code-reviewer",
995
+ "problem":null}
1098
996
  ```
1099
997
 
1100
- - `sync` is the `syncApi: 1` report of the first sync (members, packages,
1101
- changes, `problems`); `lock` is the lock it wrote. Exit `0` on success —
1102
- there is no approval-pending outcome (0.26.0, feature
1103
- `packages-no-approval`; earlier kernels exited `2` with `approvalNeeded`).
1104
- - `hosting` states decision 26 (the kernel cannot see forge visibility, so it
1105
- reports `hostIsMember` and the rule rather than judging).
1106
- - `next.clone[]` is one row per **confirmed** member (`url` = what the
1107
- workspace's `members:` ref resolves to; `dir` = `<dir>/<name>`, or
1108
- `<dir>/agents-repo` for a member called `agents`, since `agents/` is the
1109
- instance homes). `next.spawn` is the setup-expert spawn command string.
1110
- - Errors (all `E_*`): `E_BAD_ARGS` (usage; missing `--workspace`; dir given
1111
- twice), `E_REPO_REF`, `E_ALREADY_ONBOARDED { local, dir }` — **this**
1112
- directory already has `oats-local.yaml` (an enclosing deployment's file does
1113
- not count; run `oats sync` there instead), `E_ONBOARD_FAILED { dir }`
1114
- (cannot inspect/write the directory; `<dir>` exists and is not a directory).
1115
- Failures while **discovering** — `E_REMOTE_UNREADABLE`, `E_WORKSPACE_SCHEMA`
1116
- (not a workspace host), `E_LOCAL_MISSING`, plus `E_PACKAGE_MISSING` /
1117
- `E_PACKAGE_INTEGRITY` / `E_LOCK_SCHEMA` and the other `sync` errors raised
1118
- before the workspace was read — roll back the files onboarding created and
1119
- carry `details.rolledBack: true` and `details.dir`; a typo'd ref never
1120
- leaves a half-onboarded directory that looks finished. Once the workspace
1121
- has been read, a later failure keeps the files (a lock may exist) and
1122
- carries `details.dir` + `details.local` instead.
1123
-
1124
- ### `oats sync [--dir <d>] --json` → `syncApi: 1`
1125
-
1126
- Discovers, confirms membership, resolves `packages:` to commits + integrity,
1127
- writes `oats-lock.json` (lockfileVersion 3), reports; exit `0` on success.
1128
- **No package approval** (0.26.0, human decision 2026-09-24; feature
1129
- `packages-no-approval`): declaring a package in `packages:` is the trust
1130
- decision. The report has no `approvalNeeded`, package rows no `approved`,
1131
- `changes[]` rows no `approvalNeeded`; there is no prompt and no exit `2`, and
1132
- `--approve` is `E_BAD_ARGS`. A lock written by an earlier kernel keeps working
1133
- (its `approved` records are ignored and dropped on the next write). The fields
1134
- went away without an API-number bump — `syncApi`, `workspaceStatusApi` and
1135
- `capabilitiesApi` stay `1`; the removal is signalled by the feature string
1136
- alone — so a consumer reading `approvalNeeded`, `approval` or `approved` must
1137
- gate that on the absence of `packages-no-approval`.
998
+ - `declared`: the soul's own `launch` as `{harness, model}` (`model` may be
999
+ `null`), or `null` when the soul declares none.
1000
+ - `effective`: `{harness, model, launchConfig}`. `model` is the id passed to the
1001
+ harness, or `null` (the harness's own). `launchConfig` is a configuration
1002
+ name or `null`.
1003
+ - `from`: `"flag"`, `"local"`, `"local-default"`, `"soul"` or `"host"`; on a
1004
+ home, also `"recorded"` (a home from before 0.30).
1005
+ - `at`: where the deciding value lives, or `null` (a flag, the host):
1006
+ `"oats-local.yaml#/souls/launch/<key>"` (JSON-pointer escaped: `/` is `~1`),
1007
+ `"oats-local.yaml#/souls/launch/*"`, `"<repoKey>:<path>/soul.yaml#/launch"`,
1008
+ or `"package:<id>:<path>/soul.yaml#/launch"`.
1009
+ - `problem`: `null`, or `{code, message, fix}` when a spawn would refuse
1010
+ (`E_HARNESS_UNAVAILABLE`, `E_LAUNCH_CONFIG_UNKNOWN`, or `E_LAUNCH_EXECUTABLE`
1011
+ for a configuration whose own executable is missing). Only reports carry it;
1012
+ spawn and preview refuse instead. With `E_LAUNCH_CONFIG_UNKNOWN`,
1013
+ `effective` is the host default (`pi`, `null`, `null`): the named
1014
+ configuration launches nothing here.
1015
+ - In listings (`oats souls`, `inspect --soul`, `launchCurrent`) `model` is the
1016
+ configured id: no model catalogue is probed. The preview's `model` is the
1017
+ resolved one.
1018
+
1019
+ ### Where the launch appears
1020
+
1021
+ - **`oats spawn … --preview --json`**: `launch`, flags applied. The existing
1022
+ `harness`, `model`, `launchConfig` equal `launch.effective`, and
1023
+ `decision.effective` binds them (a preference edited between preview and
1024
+ apply is `E_DECISION_STALE`).
1025
+ - **`oats inspect --soul <name>`** and **`oats souls` rows**: `launch` as a
1026
+ spawn with no flags would decide it here (`from` is never `"flag"`). A soul
1027
+ whose harness is missing still lists, with `problem` set. The souls rows'
1028
+ Desktop facts `harness`, `model`, `harnessFrom` equal `launch.effective`;
1029
+ `harnessFrom` is `"soul"`, `"local"`, `"local-default"` or `"kernel-default"`
1030
+ (the host default).
1031
+ - **`oats inspect --home <abs>`**: `launch` is the record (`from` the recorded
1032
+ layer; `declared` the soul's preference then), and `launchCurrent: Launch |
1033
+ null` is what `--reselect-launch` would choose now: the home's recorded soul
1034
+ copy's `launch` and this deployment's `souls.launch` as they are now (`null`
1035
+ when `oats-local.yaml` cannot be read).
1036
+ - **`instance.json`** (a spawn, and a start that makes a new selection):
1037
+ `launchFrom` (a `from` value), `launchAt` (its `at`) and `launchDeclared`
1038
+ (the soul's own preference then, `{harness, model}` or `null`). All three
1039
+ are absent on a home from before 0.30. A start with `--launch-config` or
1040
+ `--harness` records `launchFrom: "flag"`; a plain or `--model`-only start
1041
+ keeps them. `harness`, `model`, `launchConfig` and the recipe
1042
+ record the effective launch as before.
1043
+ - **`modelFrom`** (instance.json and roster rows) gains `"local"` and
1044
+ `"local-default"` (an inline override's model). `"soul"` is the soul's
1045
+ `launch.model`.
1046
+ - **Readiness** (`--home` only), in `checks.configured`: `launch-changed`, a
1047
+ warning (`required: false`), `subject: "launch"`, `producer: "launch
1048
+ preference"`, with `recorded` and `current` (each `{harness, model,
1049
+ launchConfig}`), `from` and `at` (the current layer's). `remedy`:
1050
+ "`oats session restart --reselect-launch`, or respawn". Only a home whose
1051
+ recorded launch a layer chose (`launchFrom` `local`, `local-default`,
1052
+ `soul` or `host`) is compared: one launched with explicit flags, or from
1053
+ before 0.30, never warns.
1054
+ - **`--reselect-launch`** with `--launch-config` is `E_BAD_ARGS` (choose one);
1055
+ with `--harness` the flag decides, as at spawn.
1056
+
1057
+ No verb writes `souls.launch`; it is plain YAML. A GUI that edits it checks
1058
+ the result with `oats inspect --soul <name> --json`.
1059
+
1060
+ ## Spawn
1061
+
1062
+ ### The preview
1138
1063
 
1139
- ```json
1140
- {"syncApi":1,
1141
- "workspace":{"name":"acme","key":"github.com/acme/agents","url":"https://github.com/acme/agents.git","commit":"<oid>","observedAt":"<iso>",
1142
- "local":"/abs/acme-workspace/oats-local.yaml","lock":"/abs/acme-workspace/oats-lock.json"},
1143
- "members":[{"key":"github.com/acme/agents","name":"agents","commit":"<oid>","confirmed":true,"status":"confirmed","detail":null,"team":"global",
1144
- "souls":["release-manager"],"capabilities":["acme-house-style"],"publishes":null},
1145
- {"key":"github.com/acme/tools","name":"tools","commit":"<oid>","confirmed":true,"status":"confirmed","detail":null,"team":"engineering",
1146
- "souls":["tools-expert"],"capabilities":["acme-tools-dev"],"publishes":{"package":"acme.tools","version":"0.4.0"}},
1147
- {"key":"github.com/acme/billing","name":"billing","commit":"<oid>","confirmed":false,"status":"no-backlink","detail":"github.com/acme/billing@… has no oats-membership.yaml","team":null,
1148
- "souls":[],"capabilities":[],"publishes":null}],
1149
- "packages":[{"id":"oats.okf","version":"2.1.3","source":"catalog:oats.okf","commit":"<oid>","integrity":"sha256-…","capabilities":["oats.okf"],"souls":[]},
1150
- {"id":"acme.tools","version":"0.4.0","source":"git:github.com/acme/tools@v0.4.0","commit":"<oid>","integrity":"sha256-…","capabilities":["acme-deploy","acme-lint"],"souls":["release-reviewer"]}],
1151
- "changes":[{"id":"acme.tools","from":null,"to":"0.4.0","commit":"<oid>"}],
1152
- "problems":[]}
1064
+ ```text
1065
+ oats spawn <soul> [the flags of a real spawn] --preview --json
1153
1066
  ```
1154
1067
 
1155
- - `members[].status`: `confirmed` | `not-listed` | `no-backlink` |
1156
- `backlink-elsewhere` | `cannot-read`; `detail` explains an unconfirmed row.
1157
- `publishes` reports a member's `oats-package/` (informational — its
1158
- capabilities are **not** in `capabilities[]`; the non-collapse rule).
1159
- - `packages[].souls` (feature `package-souls`, 0.28.0): the names of the
1160
- package souls the lock records for that package (`[]` when none).
1161
- - `changes[]`: `from` = previously locked version or `null`; `to` = `null` when
1162
- the package was dropped from `packages:` and from the lock.
1163
- - `problems[]`: `{ code, path, message, repoKey? }` — per-item discovery
1164
- problems (`E_WORKSPACE_SCHEMA`, `E_TEAM_UNKNOWN`, `E_REMOTE_*`, …). Never an
1165
- abort: an unreadable member directory is a problem of that member.
1166
- - Errors: `E_LOCAL_MISSING`, `E_WORKSPACE_SCHEMA { path, problems[] }`,
1167
- `E_REMOTE_UNREADABLE`, `E_LOCK_SCHEMA`, `E_PACKAGE_MISSING` (catalog has no
1168
- such id), `E_PACKAGE_INTEGRITY { why: "branch" | locked/observed }`,
1169
- `E_PACKAGE_MANIFEST`, `E_REPO_REF`, `E_BAD_ARGS` (`--approve`: package
1170
- approval was removed).
1171
-
1172
- ### `oats package add <id> <version|git:<repo>@<ref>> | remove <id> [--dir] --json`
1173
-
1174
- Edits `packages:` **only** when `oats-workspace.yaml` is tracked by the Git
1175
- checkout walked up from `--dir`; otherwise reports the line to add.
1068
+ Feature `spawn-preview-2`, `spawnPreviewApi: 2`. The preview runs every
1069
+ preflight a spawn runs and writes nothing, on success or refusal. It reads the
1070
+ soul from the per-commit cache (`agents/<soul>/souls/<commit12>/`) or fetches
1071
+ it to a temporary copy (`soulFetched: true`).
1176
1072
 
1177
1073
  ```json
1178
- {"action":"add","id":"oats.aweb","value":"v1.11.2","previous":null,"edited":true,"file":"/abs/agents/oats-workspace.yaml"}
1179
- {"action":"add","id":"oats.aweb","value":"v1.11.2","edited":false,"file":null,"line":"packages:\n oats.aweb: v1.11.2","hint":"oats-workspace.yaml is not in this checkout; commit the change in the workspace repo, then `oats sync`"}
1074
+ {"modules":[
1075
+ {"name":"nw-tools","from":{"kind":"member","repoKey":"github.com/nw/agents","commit":"66566512…"},"layer":null,"private":false,"declares":[],
1076
+ "changedSince":{"instance":"rm-2","was":"45b86f64…"}},
1077
+ {"name":"oats.okf","from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"},
1078
+ "layer":"knowledge","private":false,"declares":["state-dir"],"changedSince":false}],
1079
+ "teams":[{"label":"eng","team":"eng:nw.aweb.ai","default":true,"from":"shared"}],
1080
+ "defaultTeam":{"label":"eng","team":"eng:nw.aweb.ai","from":"soul"},
1081
+ "resolution":"abacbdb5a7975098d77007c8","declRevision":"068a0d3f1311a9e84e9aff2e","payloadRevision":"81a368006c610194aa35dbe0",
1082
+ "workspace":"github.com/nw/agents","standalone":false,"providers":{},
1083
+ "settings":{"nw-tools":{},"oats.okf":{"owns":"rm"}},
1084
+ "settingsOrigins":{"nw-tools":{},"oats.okf":{"/owns":{"kind":"soul","at":"soul.yaml#/knowledge"}}},
1085
+ "spawnPreviewApi":2,"preview":true,"agent":"rm","kind":"persistent","instance":"rm-api","home":"/w/agents/rm/instances/rm-api",
1086
+ "repo":"/w/agents-repo","work":"worktree","subject":{"soul":"rm","agentsRoot":null,"dir":"/w"},
1087
+ "decision":{"instance":"rm-api","home":"/w/agents/rm/instances/rm-api","branch":"agents/rm-api","base":{"ref":"HEAD","oid":"66566512…"},
1088
+ "effective":{"repo":"/w/agents-repo","work":"worktree","harness":"pi","model":null,"launchConfig":null,"yolo":null,"backend":"tmux",
1089
+ "childSpawns":true,"relation":null,"providers":{"nw-tools":{},"oats.okf":{"owns":"rm"}}},
1090
+ "resolution":"abacbdb5a7975098d77007c8","revision":"c557d8ec9a272ba1c1739dc3"},
1091
+ "preflight":{"status":"complete","budgetMs":20000,"elapsedMs":53},"backendStatus":{"name":"tmux","installed":true,"started":false},
1092
+ "harness":"pi","model":null,"modelSource":"native default","launchConfig":null,"backend":"tmux",
1093
+ "branch":"agents/rm-api","base":{"ref":"HEAD","oid":"66566512…"},"worktree":"/w/agents/rm/instances/rm-api/work",
1094
+ "relation":null,"parentInstance":null,"policy":{"childSpawns":{"allowed":true,"origin":{"kind":"default","detail":"no spawn option: children allowed"}}},
1095
+ "executable":"/usr/local/bin/pi",
1096
+ "capabilities":[{"name":"nw-tools","origin":"member:github.com/nw/agents@66566512…"},{"name":"oats.okf","origin":"package:oats.okf@2.1.3"}],
1097
+ "skills":["release-checklist",{"name":"okf","source":"module:oats.okf"}],
1098
+ "task":null,"soulFetched":true}
1180
1099
  ```
1181
1100
 
1182
- `remove` → `{ action: "remove", id, value: null, previous: "<old value>", edited: true, file }`
1183
- on the tracked branch. On the untracked branch the shape is
1184
- `{ action: "remove", id, value: null, edited: false, file: null, line: null, hint }`
1185
- — **no `previous`** (nothing was read), `line: null` (there is no line to add
1186
- for a removal). **Both branches** answer `E_PACKAGE_MISSING { id, path? }` when
1187
- `<id>` is not declared in `packages:` — the untracked branch reads the file it
1188
- found to check the declaration even though it does not edit it. `E_USAGE`,
1189
- `E_WORKSPACE_SCHEMA` (bad id/value, or the edit would make the file invalid),
1190
- `E_REPO_REF`. No network.
1191
-
1192
- ### `oats workspace status [--dir] --json` → `workspaceStatusApi: 1`
1101
+ **Placement.**
1102
+ - `instance`: `<agent>-<purpose>` with `--purpose`, else `<agent>-<n>`,
1103
+ de-duplicated across the deployment; or exactly `--name`
1104
+ ([Instance names](#instance-names)). `home` and `worktree` (worktree mode,
1105
+ else `null`) are canonical: never derive paths.
1106
+ - `repo`: `--repo`, else the `clones:` entry, else `<deployment>/<member>`.
1107
+ - `branch` defaults to `agents/<instance>` (`--branch` overrides); `base` is
1108
+ `--base` (default `HEAD`) resolved to `oid`. `E_BRANCH_EXISTS` and
1109
+ `E_BASE_UNKNOWN` refuse preview and apply alike.
1110
+ - `subject` echoes `{soul, agentsRoot, dir}` byte-exact.
1111
+
1112
+ **Launch.**
1113
+ - `harness`, `model`, `modelSource`, `launchConfig`, `backend`, `yolo`
1114
+ (absent when nothing sets it) are the resolved selection.
1115
+ `backendStatus` is `{name, installed, started: false}`, `null` with
1116
+ `--no-launch`. `executable` is the resolved harness binary.
1117
+ - `modelSource` is `"explicit"`, `"soul default"`, `"launch-config <name>"`,
1118
+ `"native default"` or `"native default (explicit)"` (`--model
1119
+ @native-default`). Omitting `--model` and asking for the native default are
1120
+ different requests.
1121
+ - `preflight`: `{status: "complete" | "timeout", budgetMs, elapsedMs}`; all
1122
+ native probes share one 20 s budget.
1123
+ - `policy.childSpawns` is what the instance will record.
1124
+
1125
+ **Composition.**
1126
+ - `modules[]` (feature `instance-modules`): `{name, from, layer, private,
1127
+ declares, changedSince}`; `from` is what `instance.json` will record.
1128
+ `changedSince` is `null` (no previous instance), `false` (unchanged since the
1129
+ newest one) or `{instance, was}`.
1130
+ - `capabilities[]` (`{name, origin}`, `origin` `package:<id>@<v>` or
1131
+ `member:<repoKey>@<commit>`) and `skills[]` (the soul's own skills as
1132
+ strings, module skills as `{name, source: "module:<cap>"}`) are display
1133
+ only: bind to `modules[]` and `decision`.
1134
+ - `resolution` (24 hex) hashes `declRevision` (the declarations) and
1135
+ `payloadRevision` (the merged payloads). `workspace` is the host key;
1136
+ `standalone` marks a standalone view. `task` is the task text or `null`.
1137
+
1138
+ **Provider settings.**
1139
+ - `providers` is the `--provider` map as typed.
1140
+ - `settings.<cap>`: the merged payload (manifest defaults, then workspace,
1141
+ soul, `oats-local.yaml` `settings.<cap>`, `--provider`).
1142
+ - `settingsOrigins.<cap>` (feature `settings-origins`) maps each leaf pointer
1143
+ (`/identity/mode`) to `{kind, at}`: `kind` is `manifest-default | workspace |
1144
+ soul | host | spawn`, `at` names the place.
1145
+ - `--provider <cap> <key>=<value>` (feature `spawn-provider-payload`,
1146
+ repeatable, `a.b=c` nests): a malformed pair is `E_BAD_ARGS`; a capability
1147
+ the soul does not resolve is `E_CAPABILITY_MISSING {capability, soul,
1148
+ modules}`.
1149
+
1150
+ ### The decision
1151
+
1152
+ `decision` is `{instance, home, branch, base, effective, resolution,
1153
+ revision}`. `effective` (feature `spawn-apply-2`) is `{repo, work, harness,
1154
+ model, launchConfig, yolo, backend, childSpawns, relation, providers}`;
1155
+ `relation` is `null` or `{kind, anchor: {instance, agentsRoot}}`; `providers`
1156
+ (feature `served-identity`) equals `settings`. `revision` (24 hex) hashes the
1157
+ decision.
1158
+
1159
+ Apply with `oats spawn <soul> … --expect-decision <revision> --json`. Any drift
1160
+ refuses `E_DECISION_STALE` with the fresh `details.decision`; nothing is
1161
+ created. Without `--expect-decision` the CLI keeps its interactive
1162
+ auto-suffix.
1163
+
1164
+ ### Apply
1165
+
1166
+ Feature `spawn-apply-2`, `spawnApplyApi: 1`. The Desktop gates on
1167
+ `spawn-preview-2`, `spawn-apply-2` and `spawn-idempotency-2`.
1168
+
1169
+ - Backend startup runs only after the decision check and the placement
1170
+ reservation; a missing backend binary is refused before placement.
1171
+ - The home is reserved with a non-recursive `mkdir`. A concurrent loser
1172
+ refuses `E_PLACEMENT_TAKEN {instance, home}` having touched nothing. Names
1173
+ are deployment-wide: a same-name race with another soul ends in
1174
+ `E_INSTANCE_NAME_TAKEN` (for `--name`) or `E_PLACEMENT_TAKEN`.
1175
+ - A refused child spawn appends `child-spawn-refused` to the parent's log (on
1176
+ apply only).
1177
+
1178
+ **Idempotency** (feature `spawn-idempotency-2`): `--idempotency-key <key>`
1179
+ with `--expect-decision` records the key and decision in `instance.json`.
1180
+ - Recovery runs right after naming, before placement or preflight. A retry
1181
+ with the same key replays the receipt (`replayed: true`, no second spawn,
1182
+ no second wake). The same key with another decision is
1183
+ `E_IDEMPOTENCY_CONFLICT {instance, home}`.
1184
+ - `spawnCompleted` is `false` until launch, lineage and events are done; a
1185
+ retry of an unfinished spawn is `E_SPAWN_INCOMPLETE {instance, home,
1186
+ launched}` (recover through the session surface).
1187
+ - The key lives in the home. Mint it on the first confirmation and keep it
1188
+ for that intent's retries.
1189
+ - `wake: {requested, saved, error}` is recorded and replayed; `saved: null`
1190
+ means the outcome was not recorded.
1191
+
1192
+ **Result** (`oats spawn <soul> … --json`):
1193
1193
 
1194
1194
  ```json
1195
- {"workspaceStatusApi":1,
1196
- "workspace":{"name":"acme","key":"github.com/acme/agents","url":"…","commit":"<oid>","observedAt":"<iso>","local":"/abs/…/oats-local.yaml","teams":["global","engineering"]},
1197
- "members":[ … same rows as sync … ],
1198
- "packages":[ … same rows as sync (from the lock) … ],
1199
- "declaredPackages":["acme.tools","oats.okf"],
1200
- "unsynced":[],
1201
- "stale":[],
1202
- "external":[{"source":"git:github.com/oss-collective/experts@<oid>","soul":"security-reviewer","team":"unassigned"}],
1203
- "problems":[],
1204
- "warnings":[]}
1195
+ {"instance":"rm-api","agent":"rm","home":"/w/agents/rm/instances/rm-api","work":"worktree","branch":"agents/rm-api","launched":true,"warnings":[],
1196
+ "tmux":{"session":"pi-agents","window":"rm-api"},"repo":"/w/agents-repo","harness":"pi","model":null,"parent":null,"sibling":null,"relation":null,
1197
+ "spawnOrigin":"operator","attach":"tmux attach -t pi-agents","decision":{"instance":"rm-api","revision":"c557d8ec9a272ba1c1739dc3"},"replayed":false,
1198
+ "wake":{"requested":false,"saved":null,"error":null},"launchConfig":null,
1199
+ "launch":{"version":2,"harness":"pi","launchConfig":null,"launchConfigSource":null,"executable":"/usr/local/bin/pi","executableDeclared":null,
1200
+ "executableResolvedFrom":"PATH","args":[],"env":{},"model":null,"hooks":{"launch":{},"env":{},"contributions":[]},"prompt":{"kind":"task-file","file":"TASK.md"}}}
1205
1201
  ```
1206
1202
 
1207
- `warnings[]` (feature `teams`, also in the `sync` report): `{ code, label,
1208
- souls, paths, message }` — one `unmapped-team-label` per label that is in
1209
- `teams:` but not in `messaging.byTeam`, naming its souls (sorted) and each
1210
- soul's `<repoKey>:<path>#/team`; sorted by label. Never a problem.
1203
+ (`decision` is abridged: it is the full bound decision.)
1204
+
1205
+ - Always present: `instance, agent, home, work, branch, launched, warnings
1206
+ (array), tmux ({session, window} | null), repo, harness, model, parent,
1207
+ sibling, relation, spawnOrigin (operator | instance), attach, launchConfig,
1208
+ launch` (the redacted recipe).
1209
+ - When they apply: `sessionTarget` (Herdr), `yolo`, `decision` and
1210
+ `replayed` (bound apply), `wake` (keyed apply), `wakeSchedule` and
1211
+ `wakeScheduleError` (a requested wake).
1212
+
1213
+ <a id="instance-names"></a>
1214
+ ### Instance names
1215
+
1216
+ Feature `spawn-name`. `--name <slug>` is the exact name, with no prefix.
1217
+ - `--name` with `--purpose`, or without a value: `E_BAD_ARGS`.
1218
+ - A name that is not a slug (lowercase letters and digits, single dashes),
1219
+ equals a soul name, or exceeds 64 characters (derived names included, with
1220
+ their suffix) is `E_INSTANCE_NAME_INVALID`.
1221
+ - A name any soul's `instances/` holds, or a live tmux window carries, is
1222
+ `E_INSTANCE_NAME_TAKEN {instance, home, session?}`; a typed name never gets
1223
+ a silent `-2`.
1224
+ - The name is part of the decision.
1225
+
1226
+ <a id="spawn-errors"></a>
1227
+ ### Spawn errors
1228
+
1229
+ | Code | Details | When |
1230
+ |---|---|---|
1231
+ | `E_USAGE`, `E_BAD_ARGS` | | no soul; bad, contradictory or removed flags |
1232
+ | `E_LOCAL_MISSING`, `E_NO_DEPLOYMENT` | | no `oats-local.yaml`; no `agents/` root |
1233
+ | `E_SOUL_UNKNOWN` | `{name, members, packages}` | no such soul, or not at `--agents-root` |
1234
+ | `E_SOUL_AMBIGUOUS` | `{name, repos, qualified}` | several souls answer the bare name; use one of `qualified` |
1235
+ | `E_SOUL_DISABLED` | `{name, qualifiedName, entry}` | listed in `souls.disabled` |
1236
+ | `E_UNKNOWN_AGENT` | | the resolved soul is not under the deployment's agents root |
1237
+ | `E_TEAM_UNKNOWN`, `E_TEAM_NOT_ELIGIBLE` | `{label, at}`, `{soul, label, at}` | the soul's teams do not resolve |
1238
+ | `E_NOT_A_MEMBER`, `E_MEMBERSHIP_UNCONFIRMED` | | the soul's repository is not a confirmed member |
1239
+ | `E_CAPABILITY_MISSING`, `E_CAPABILITY_PRIVATE`, `E_CAPABILITY_INCOMPATIBLE`, `E_COMPATIBILITY` | | a capability cannot be resolved |
1240
+ | `E_PACKAGE_MISSING`, `E_PACKAGE_INTEGRITY`, `E_LOCK_SCHEMA` | | the lock does not provide it (standalone: `{…, standalone: true, reason: "no-catalog", catalog}`) |
1241
+ | `E_SLOT_CONFLICT`, `E_SKILL_DUPLICATE` | | the composition conflicts |
1242
+ | `E_WORKSPACE_SCHEMA` | `{path, key, reason}` | a removed key or invalid payload |
1243
+ | `E_CLONE_MISSING`, `E_CLONE_MISMATCH` | | the member's clone is missing or wrong |
1244
+ | `E_REQUIREMENT_INACTIVE` | `{soul, capabilities, context, remedy}` | a declared requirement is not active |
1245
+ | `E_CHILD_SPAWNS_DISABLED` | `{parent, policy}` | the parent's policy is off |
1246
+ | `E_PARENT_NOT_FOUND`, `E_RELATIVE_NOT_FOUND` | | the anchor name matches no instance |
1247
+ | `E_RELATIVE_AMBIGUOUS` | | the anchor matches several instances (`--relative-root` picks one) or a same-named instance would shadow the edge |
1248
+ | `E_BRANCH_EXISTS`, `E_BASE_UNKNOWN` | | |
1249
+ | `E_INSTANCE_NAME_INVALID`, `E_INSTANCE_NAME_TAKEN` | see above | |
1250
+ | `E_DECISION_STALE` | `{decision}` | |
1251
+ | `E_PLACEMENT_TAKEN`, `E_IDEMPOTENCY_CONFLICT` | `{instance, home}` | |
1252
+ | `E_SPAWN_INCOMPLETE` | `{instance, home, launched}` | |
1253
+ | `E_LAUNCH_*`, `E_MODEL_UNKNOWN`, `E_UNSUPPORTED_HARNESS` | | the launch selection is refused |
1254
+ | `E_SCHEDULE_INVALID` | | a bad wake (`--wake-json`, `--wake-file`, `--wake-*`) |
1255
+ | `E_SPAWN_FAILED` | | anything else |
1256
+
1257
+ ## `instance.json` and the roster
1258
+
1259
+ <a id="instancejson"></a>
1260
+ ### `instance.json`
1261
+
1262
+ Written by the spawn; read by the roster and every `--home` command. The
1263
+ workspace-model fields (feature `instance-modules`):
1211
1264
 
1212
- `defaults`, `clones`, `disabledSouls`, `lock`, file locations and
1213
- `packages[].latest` (feature `desktop-facts`): see [Desktop facts](#desktop-facts-feature-desktop-facts-oats-0290).
1265
+ ```json
1266
+ {"agent":"rm","kind":"persistent","instance":"rm-api","home":"/w/agents/rm/instances/rm-api","soulDir":"/w/agents/rm/souls/66566512168e",
1267
+ "repo":"/w/agents-repo","work":"worktree","branch":"agents/rm-api","harness":"pi","modelFrom":"harness-default","spawnOrigin":"operator",
1268
+ "policy":{"childSpawns":{"allowed":true,"origin":{"kind":"default","detail":"no spawn option: children allowed"}}},
1269
+ "modules":{"oats.okf":{"from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"},
1270
+ "commit":"ab897841…","digest":"sha256-9a0e…","materializedAt":"2026-09-28T10:08:01.100Z"}},
1271
+ "providers":{"oats.okf":{"owns":"rm","state-dir":"/Users/ana/.oats/okf"}},
1272
+ "workspace":{"key":"github.com/nw/agents","name":"northwind","deployment":"/w","commit":"66566512…","resolution":"abacbdb5a7975098d77007c8","standalone":false,
1273
+ "soul":{"id":"github.com/nw/agents#rm","repoKey":"github.com/nw/agents","commit":"66566512…"},
1274
+ "layers":{"knowledge":{"capability":"oats.okf","from":"workspace"},"messaging":null,"tasks":null}},
1275
+ "teams":[{"label":"mine","team":"mine:ana.aweb.ai","default":false,"from":"local"}],
1276
+ "defaultTeam":{"label":"eng","team":null,"from":"soul"},
1277
+ "capabilities":[{"id":"oats.okf","layer":"knowledge","command":"okf","origin":"package:oats.okf@2.1.3","level":"/w/agents/rm/instances/rm-api",
1278
+ "settings":{"owns":"rm","state-dir":"/Users/ana/.oats/okf"},"settingsOrigins":{},"provenance":["package oats.okf v2.1.3"],
1279
+ "skills":["/w/agents/rm/instances/rm-api/.agents/skills/oats.okf/okf"],"hooks":["retire","spawn"],"trusted":true}],
1280
+ "skills":[{"name":"release-checklist","source":"soul"},{"name":"okf","source":"module:oats.okf"}],
1281
+ "createdAt":"2026-09-28T10:08:01.281Z"}
1282
+ ```
1214
1283
 
1215
- `unsynced` = declared in `packages:` but not in the lock (run `sync`);
1216
- `stale` = locked but no longer declared. Read-only: does not write the lock.
1217
- (0.26.0: the `approval` object is gone with package approval.)
1284
+ Abridged: the record also carries the launch recipe and command,
1285
+ composition evidence, the capability runtime, the tmux or Herdr target,
1286
+ lineage (`parentInstance`, `siblingInstance`, `relation`, `relativeTo`), and
1287
+ the keyed-spawn fields `decision`, `spawnIdempotencyKey`, `spawnCompleted` and
1288
+ `wake`; later starts add `restarts` and `restartCount`.
1289
+
1290
+ - `modules.<cap>`: `{from, commit, digest, materializedAt}`; `digest` hashes
1291
+ the copy at `<home>/.oats/modules/<cap>/`. Module skills are copied to
1292
+ `<home>/.agents/skills/<cap>/<skill>/`.
1293
+ - `providers.<cap>`: the merged payload (`{}` when none).
1294
+ - `workspace`: `{key, name, deployment, commit, resolution, standalone, soul,
1295
+ layers}`. `name` is recorded, and every hook, command and operation of the
1296
+ home receives it as `OATS_WORKSPACE_NAME`. `soul.id` is `<repoKey>#<soul>`,
1297
+ or `package:<id>#<soul>` for a package soul, which also records `name`,
1298
+ `qualifiedName` and `package: {id, version, commit, digest, path}`.
1299
+ `layers.<slot>` is `{capability, from}` or `null`. A standalone spawn
1300
+ records `standalone: true` and the member's key.
1301
+ - `teams` (mapped rows, as the providers received them) and `defaultTeam`
1302
+ are never rewritten.
1303
+ - `soulDir` is the soul the instance incarnates; hooks receive it as
1304
+ `OATS_SOUL`. Homes carry no `soul` link.
1305
+ - `modelFrom`: see the roster. `trigger` (a triggered instance): `{id, key,
1306
+ source, repo, number, url, event, headSha, observedAt, eventFile}`.
1307
+ `capabilityMeta.<cap>.identity`: the served identity a provider recorded.
1308
+
1309
+ <a id="the-roster-oats-status---json"></a>
1310
+ ### The roster (`oats status --json`)
1218
1311
 
1219
- ### `oats capabilities [--dir] --json` → `capabilitiesApi: 1` · `oats souls [--dir] --json` → `soulsApi: 1`
1312
+ ```text
1313
+ oats status [--dir <d>] --json
1314
+ ```
1220
1315
 
1221
- Every item of every confirmed member, external souls, and locked package
1222
- capabilities, sorted by name then origin. Souls have no private mode (their
1223
- `private` is always `false`); a repo-owned capability is listed with
1224
- `private: true` — usable only by its own repo's souls. The Desktop shows its
1225
- "Repo owned" section when `version --json` lists the `capabilities-private`
1226
- feature. `origin` is the
1227
- human string (`member <key> @ <8-char commit>` | `package <id> v<version>` |
1228
- `external <key> @ <commit>`); `kind` is the machine field. `team` is the label
1229
- or `"unassigned"`.
1316
+ Not an envelope: `{root, agents, workspace?, problems?, warnings?}`.
1230
1317
 
1231
1318
  ```json
1232
- {"capabilitiesApi":1,"workspace":{"name":"acme","key":"github.com/acme/agents","commit":"<oid>"},
1233
- "capabilities":[
1234
- {"name":"acme-house-style","origin":"member github.com/acme/agents @ 3f2a9c1e","kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>",
1235
- "team":"global","private":false,"path":"capabilities/acme-house-style","layer":null,"version":"0.0.0-workspace"},
1236
- {"name":"oats.okf","origin":"package oats.okf v2.1.3","kind":"package","package":"oats.okf","version":"2.1.3","commit":"<oid>",
1237
- "team":"unassigned","private":false}],
1238
- "problems":[]}
1319
+ {"root":"/w/agents",
1320
+ "agents":[{"name":"rm","description":"Cuts releases.","work":"worktree","kind":"persistent","dir":"/w/agents/rm",
1321
+ "soulSource":{"repoKey":"github.com/nw/agents","commit":"66566512…","path":"souls/rm","current":"66566512…","status":"current"},
1322
+ "instances":[{"agent":"rm","instance":"rm-api","home":"/w/agents/rm/instances/rm-api","harness":"pi","launched":true,
1323
+ "createdAt":"2026-09-28T10:08:01.281Z","modelFrom":"harness-default","startedAt":"2026-09-28T10:08:01.281Z","identityAddress":null,"running":true,
1324
+ "modules":[{"name":"oats.okf","from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"},
1325
+ "commit":"ab897841…","current":{"commit":"ab897841…","version":"2.1.3"},"status":"current"}],
1326
+ "soul":{"repoKey":"github.com/nw/agents","commit":"66566512…","current":"66566512…","status":"current"}}]}],
1327
+ "workspace":{"reachable":true}}
1239
1328
  ```
1240
1329
 
1241
- ```json
1242
- {"soulsApi":1,"workspace":{…},
1243
- "souls":[
1244
- {"name":"release-manager","origin":"member github.com/acme/agents @ 3f2a9c1e","kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>",
1245
- "team":"engineering","private":false,"path":"souls/release-manager","work":"worktree","description":"Cuts, verifies and announces releases."},
1246
- {"name":"security-reviewer","origin":"external github.com/oss-collective/experts @ 9c4e1f2a","kind":"external",…},
1247
- {"name":"release-reviewer","qualifiedName":"acme.tools/release-reviewer","origin":"package acme.tools v0.4.0","kind":"package","package":"acme.tools","version":"0.4.0",
1248
- "repoKey":"github.com/acme/tools","commit":"<oid>","team":"engineering","labels":["engineering"],"private":false,"path":"oats-package/souls/release-reviewer","work":"directory","description":"…"}],
1249
- "problems":[]}
1250
- ```
1330
+ - **Agent rows**: the soul's recorded definition plus `dir` and `instances`,
1331
+ and (feature `launch-preference`) `key`: the soul key, as on
1332
+ [soul rows](#oats-capabilities-and-oats-souls), or `null` when no instance
1333
+ records a workspace soul;
1334
+ `soulSource` (`{repoKey, commit, path, current?, status?}`, `current` or
1335
+ `moved`) for a workspace soul; `retireFailures[]` (`{instance, completedAt,
1336
+ error, incomplete, retry, resultPath}`) when a deferred self-retire failed.
1337
+ A capability agent's row is `{name, kind: "capability", capability,
1338
+ description, dir, instances}`.
1339
+ - **Instance rows**: the home's `instance.json` (launch recipe and command
1340
+ redacted) plus `home` and `instance` (from the directory; a disagreeing
1341
+ claim is kept as `recordedHome`/`recordedInstance`), `running` (`null` when
1342
+ a Herdr session is unreachable, with `runtimeState`/`runtimeError`),
1343
+ `identity` when a provider recorded one, `rollbackIncomplete` and
1344
+ `retirePending` when present, and the Desktop facts below.
1345
+ - **`modules`** becomes drift rows `{name, from, commit, current, status,
1346
+ reason?}` when the workspace was read. `status` is `current`, `moved` or
1347
+ `missing` (`reason`: `capability-absent`, `package-absent`, or the member's
1348
+ unconfirmed reason; `current: null`). `current` is `{commit, version}`
1349
+ (`version` on member rows is a Desktop fact). A package module without a
1350
+ lock reads `current`.
1351
+ - **`soul`** is the soul source's drift `{repoKey, commit, current, status,
1352
+ reason?}`; a package soul adds `package`, `version`, `currentVersion`
1353
+ (`missing` reasons: `package-absent`, `soul-absent`).
1354
+ - **`workspace`**: `{reachable: true}`, or `{reachable: false, code, reason,
1355
+ message}` (modules then stay the recorded map). Absent without
1356
+ `oats-local.yaml`.
1357
+ - `problems`: the legacy-home rows ([dispatch errors](#dispatch-errors)).
1358
+ `warnings`: envelope warnings. `--team` is `E_BAD_ARGS` (an envelope).
1359
+
1360
+ **Desktop facts** (feature `desktop-facts`): `startedAt` is the last start or
1361
+ restart, else `createdAt` for a launched home, else `null`. `modelFrom` is
1362
+ `"soul"`, `"spawn"` or `"start"` (an explicit `--model`), `"launch-config"`,
1363
+ `"harness-default"`, or `null` for an older home. `identityAddress` is the
1364
+ messaging identity's `address` (else `alias`), or `null`.
1365
+
1366
+ <a id="instance-git-state-oats-instance-gitdiff-instancegitapi-1-oats-0247"></a>
1367
+ ## Git and diff
1368
+
1369
+ Feature `instance-git` (`instance-git-remote` for `remote`),
1370
+ `instanceGitApi: 1`. A read-only observation of one instance's work tree: the
1371
+ branch the tree is on, not the recorded one (reported under `recorded`).
1251
1372
 
1252
- **Package souls** (feature `package-souls`, 0.28.0): a row with `kind:
1253
- "package"` is a soul a locked package ships (listed only while the workspace
1254
- declares the package). It carries `package`, `version` and `qualifiedName`
1255
- (`<package>/<soul>`); spawn it by `qualifiedName` (the bare `name` works when
1256
- it is unique). The Souls page shows it as "from package <id> <version>".
1257
- Its instances home under `agents/<package>--<soul>/` (`.` in the package id
1258
- becomes `-`), and that directory is the agent `name` in `oats status --json`
1259
- (`agents[].name`, e.g. `oats-okf--knowledge-maintainer`). A
1260
- problem about a package soul carries `package` (and `repoKey: null`); its
1261
- `path` is `package:<id>:<path in the repo>`.
1373
+ ```text
1374
+ oats instance git <instance> [--home <abs>] [--dir <d>] --json
1375
+ oats instance diff <instance> --file <id> --revision <rev> [--index-revision <idx>] [--home <abs>] [--dir <d>] --json
1376
+ ```
1262
1377
 
1263
- Souls rows' defaults, spawnability and `file`, and capability rows' `layer`,
1264
- `description`, provides, `file` and `tree` (feature `desktop-facts`): see
1265
- [Desktop facts](#desktop-facts-feature-desktop-facts-oats-0290).
1378
+ `<instance>` is resolved under the deployment's agents root. Several homes of
1379
+ that name: `E_AMBIGUOUS_INSTANCE {candidates: [{root, agent, home}]}` (pass
1380
+ `--home`; a wrong one is `E_HOME_MISMATCH`). Unknown: `E_SESSION_UNKNOWN`. No
1381
+ tree: `E_NO_WORKTREE`.
1266
1382
 
1267
- Package capabilities of declared-but-unsynced packages are absent until `sync`.
1268
- (`oats souls --json` keeps `soulsApi: 1` in 0.26.0: its shape is unchanged.
1269
- The probe's `soulsApi` is **2** because it tracks the `oats inspect --json`
1270
- soul rows. The two payloads are distinguished by their command.)
1383
+ ```json
1384
+ {"instanceGitApi":1,"instance":"dev-1","agent":"dev","home":"/w/agents/dev/instances/dev-1","workMode":"worktree",
1385
+ "observation":{"revision":"46c20668…","indexRevision":"3147fef2…","at":"2026-09-26T18:15:26.487Z","worktree":"/w/agents/dev/instances/dev-1/work",
1386
+ "branch":"feat/y","detached":false,"unborn":false},
1387
+ "recorded":{"branch":"agents/dev-1","repo":"/w/one","drift":true},
1388
+ "upstream":{"ref":"origin/feat/y","ahead":1,"behind":0},
1389
+ "base":{"ref":"origin/main","source":"origin/HEAD","mergeBase":"46c20668…","ahead":2,"behind":0},
1390
+ "remote":{"name":"origin","url":"git@github.com:acme/one.git","host":"github.com","path":"acme/one","source":"branch-upstream"},
1391
+ "summary":{"changed":1,"renamed":1,"copied":0,"unmerged":0,"untracked":1},
1392
+ "files":[{"id":"0d0cd6557e40c03eba2abd46","kind":"renamed","xy":"R.","submodule":false,"score":"R100","path":"src/new.txt","origPath":"src/old.txt",
1393
+ "additions":84,"deletions":3,"binary":false}],
1394
+ "notes":[]}
1395
+ ```
1271
1396
 
1272
- ### `oats spawn <soul> … --preview --json` — additions (Preview API 2 unchanged)
1397
+ - `observation.revision` is the HEAD oid (or `unborn`); `branch` is `null`
1398
+ when detached.
1399
+ - `upstream` without one is all `null` (unknown, not zero). `base` compares
1400
+ with the default branch's merge-base; `source` is `origin/HEAD` or
1401
+ `well-known`; none found is all `null` plus a note.
1402
+ - `remote` (feature `instance-git-remote`): the branch's remote (`source:
1403
+ "branch-upstream"`), else `origin` (`"origin"`), else `null`; `host` and
1404
+ `path` are parsed from the URL (`host: null` for a local path). The kernel
1405
+ has no forge data.
1406
+ - `files[]` come from porcelain v2: `kind` is `changed | renamed | copied |
1407
+ unmerged | untracked`; renames and copies carry `origPath` and `score`;
1408
+ ignored files are omitted. `summary` counts rows per kind.
1409
+ - `files[].id` is opaque, minted under (`revision`, `indexRevision`); it is
1410
+ the only way to ask for a diff.
1411
+ - `additions`, `deletions`, `binary`: line counts of the working tree against
1412
+ the observed commit. A binary file is `{null, null, true}`; an untracked
1413
+ file or submodule is all `null`; if counting fails every entry is `null`
1414
+ with a note. `null` means unknown.
1415
+
1416
+ The diff answers `{instanceGitApi: 1, observation, file: {id, kind, xy,
1417
+ path, origPath}, against, binary, bytes, truncated, limit: 262144, patch,
1418
+ readOnly: {helpers: "disabled", optionalLocks: "off", objectsWritten: 0}}`.
1419
+
1420
+ - `against` is the observed revision (the working tree against that commit,
1421
+ index included) or `"empty"` for an untracked file. A binary file has an
1422
+ empty patch; over 256 KiB, `truncated: true`.
1423
+ - The read runs without external diff, textconv, fsmonitor, hooks, the
1424
+ caller's Git environment or global config, and writes nothing (`readOnly`).
1425
+ - If HEAD or the index moved, the id is not in the current observation, or
1426
+ anything moved during the read: `E_STALE_OBSERVATION` with
1427
+ `details.observation`. Re-observe; never render a diff of another tree.
1428
+ - A `--file` that is not 24 hex, or no `--revision`: `E_BAD_ARGS`. Git
1429
+ failure: `E_GIT_FAILED`.
1430
+
1431
+ ## Events
1432
+
1433
+ Feature `instance-events-2`, `eventsApi: 2`: typed lifecycle events, written
1434
+ by the kernel action that made them true.
1273
1435
 
1274
- On a workspace deployment the preview carries four extra top-level fields, and
1275
- `decision.resolution` binds the resolution revision (so a member that moved
1276
- between preview and apply is `E_DECISION_STALE`):
1436
+ ```text
1437
+ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--dir <d>] --json
1438
+ ```
1277
1439
 
1278
1440
  ```json
1279
- {"modules":[
1280
- {"name":"acme-release-tooling","from":{"kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>"},"layer":null,"private":false,"declares":[],
1281
- "changedSince":{"instance":"release-manager-v2","was":"<old oid>"}},
1282
- {"name":"oats.okf","from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"<oid>","integrity":"sha256-…","repoKey":"github.com/awebai/oats-okf"},
1283
- "layer":"knowledge","private":false,"declares":["bindings-file","git-timeout","harvest-model","harvest-runtime","state-dir"],"changedSince":false}],
1284
- "team":"engineering",
1285
- "resolution":"6e3050c0d005879441ab017d",
1286
- "workspace":"github.com/acme/agents",
1287
- "spawnPreviewApi":2,"preview":true,"agent":"release-manager","instance":"release-manager-cut","home":"/abs/…",
1288
- "decision":{"instance":"…","home":"…","branch":"…","base":{…},"effective":{…},"resolution":"6e3050c0d005879441ab017d","revision":"<24 hex>"},
1289
- "…":"every Preview API 2 field as before"}
1441
+ {"eventsApi":2,"instance":"dev-1","home":"/w/agents/dev/instances/dev-1","incarnation":"2026-09-28T10:08:01.281Z",
1442
+ "count":1,"returned":1,"truncated":false,
1443
+ "integrity":{"unreadableRows":0,"foreignRows":0,"sources":[{"path":"home","status":"ok","bytes":612},{"path":"workspace","status":"ok","bytes":612}]},
1444
+ "events":[{"eventsApi":2,"at":"2026-09-28T10:08:02.000Z","instance":"dev-1","home":"/w/agents/dev/instances/dev-1","incarnation":"2026-09-28T10:08:01.281Z",
1445
+ "producer":"kernel","kind":"spawned",
1446
+ "data":{"agent":"dev","work":"worktree","branch":"agents/dev-1","harness":"claude","model":null,"parentInstance":null,"relation":null,"launched":true}}],
1447
+ "lastEvent":{"kind":"spawned","at":"2026-09-28T10:08:02.000Z","producer":"kernel","incarnation":"2026-09-28T10:08:01.281Z"},
1448
+ "waitingOnYou":null,"waitingClaims":[],"notes":["…"]}
1290
1449
  ```
1291
1450
 
1292
- - `modules[].from` is exactly the `from` recorded in `instance.json` on apply.
1293
- - `modules[].declares`: the manifest's declared setting keys, as in `inspect`
1294
- (feature `settings-declared`).
1295
- - `changedSince`: `null` (no previous instance of this soul), `false`
1296
- (unchanged since the newest previous instance), or
1297
- `{ instance, was }` (`was` = the previous commit, or `null` when the previous
1298
- instance had no such module).
1299
- - `capabilities[]` / `skills[]` on a **workspace** spawn are **objects**, not
1300
- Preview-1's strings: `capabilities[]` is `{ name, origin }` (`origin` =
1301
- `package:<id>@<version>` or `member:<repoKey>@<commit>`), and `skills[]` is
1302
- `{ name, source }`. Neither is a binding surface, because the authoritative
1303
- module set is `modules[]` and what apply binds is `decision.effective` /
1304
- `decision.resolution`. Consumers should read those fields, not project
1305
- `capabilities[]` / `skills[]`. (Corrected 2026-09-24: this said "keep their
1306
- Preview-1 meaning", which read as strings; found by the Desktop engineer in
1307
- F3.)
1308
- - `workspace` is the workspace host's canonical key, `team` the soul's label
1309
- (or `null`), `resolution` the 24-hex revision `decision.resolution` binds.
1310
- - `--provider <cap> <key>=<value>` (repeatable; `a.b=c` nests) is accepted by
1311
- preview and apply. `E_BAD_ARGS` for a malformed pair or when the deployment
1312
- has no `oats-local.yaml`; `E_CAPABILITY_MISSING { capability, soul, modules[] }`
1313
- when the soul does not resolve that capability. `byTeam` is a **reserved
1314
- key**: legal only at the top level of the workspace file's `messaging:`;
1315
- anywhere else in any payload layer (soul, `oats-local.yaml` `settings`,
1316
- `--provider`, at any depth) it is `E_WORKSPACE_SCHEMA { reason: "reserved-key", path, key }`.
1317
- - Soul lookup: `E_SOUL_UNKNOWN { name, members[], packages[] }` (not among
1318
- confirmed members, externals or package souls), `E_SOUL_AMBIGUOUS { name,
1319
- repos[], qualified[] }` (name one of `qualified`: `<member>/<soul>` or
1320
- `<package>/<soul>`), `E_SOUL_DISABLED { name, qualifiedName, entry }` (the
1321
- soul is in `oats-local.yaml` `souls.disabled`). A package soul whose fetched
1322
- content does not match the lock is `E_PACKAGE_INTEGRITY { why:
1323
- "soul-digest", package, soul, locked, observed }`. Resolution errors keep their codes (`E_NOT_A_MEMBER`,
1324
- `E_MEMBERSHIP_UNCONFIRMED { repoKey, reason }`, `E_CAPABILITY_MISSING { hint? }`,
1325
- `E_CAPABILITY_PRIVATE`, `E_PACKAGE_MISSING`, `E_PACKAGE_INTEGRITY { why: "capabilities", listed, locked }`,
1326
- `E_SLOT_CONFLICT { slot, modules[], reason? }`, `E_SKILL_DUPLICATE { name, modules[] }`,
1327
- `E_COMPATIBILITY { capability, package, version, range, why? }`).
1328
-
1329
- The apply result (`oats spawn … --json`) is unchanged in shape; the new facts
1330
- live in the home's `instance.json`.
1331
-
1332
- ### `instance.json` — `modules`, `providers`, `workspace` (feature `instance-modules`)
1333
-
1334
- Written by materialization inside the spawn transaction; read back by
1335
- `oats status --json` and the roster.
1451
+ - **Sources.** `home` is `<home>/.oats-events.jsonl`; `workspace` is
1452
+ `<deployment>/.agents/events/<agent>--<instance>.jsonl` (it survives the
1453
+ home). Each is `{path, status: "ok" | "absent" | "refused" | "tail",
1454
+ bytes}`. Only a regular file is opened (no symlinks, same device and inode
1455
+ after open), and at most its last 4 MiB is read (`"tail"`).
1456
+ - **Kinds:** `spawned`, `launched`, `restarted`, `stopped`, `stop-refused`,
1457
+ `retire-planned`, `retired`, `worktree-retained`, `worktree-removed`,
1458
+ `branch-deleted`, `child-spawn-refused`, `launch-warning` (0.30: a
1459
+ `launch` hook's warning at session start/restart, `data: {message}`),
1460
+ `recomposed` (from earlier kernels). `producer` is `kernel` or a capability id. Older rows may carry
1461
+ `eventsApi: 1`.
1462
+ - **Incarnation.** Each row carries the writing home's `createdAt` (or
1463
+ `null` for old rows); the top-level `incarnation` is the current home's (or
1464
+ `null`). Earlier incarnations are returned as this address's history.
1465
+ - **Address.** `--home` must be a home of `<instance>` (`E_HOME_MISMATCH`).
1466
+ Rows for another address are dropped and counted in
1467
+ `integrity.foreignRows`; torn or invalid lines are counted in
1468
+ `integrity.unreadableRows`. Duplicates are removed.
1469
+ - **Window.** `count` is the rows after `--since`; `returned` the window
1470
+ (`--limit`, default 200, 1–2000); `truncated` means rows were cut or a
1471
+ source was a tail. `lastEvent` is `{kind, at, producer, incarnation}` of
1472
+ the last returned row, or `null`.
1473
+ - **Waiting.** `waitingClaims[]` is `{producer, waiting, since, reason}` per
1474
+ producer with a claim in the current incarnation (cleared ones included).
1475
+ A producer's latest row with `data.waitingOnYou` decides. `waitingOnYou` is
1476
+ `{since, producer, reason}` of the newest positive claim, or `null`
1477
+ (unknown, not "not waiting"). No kernel path claims waiting today.
1478
+ - Errors: `E_SESSION_UNKNOWN`, `E_AMBIGUOUS_INSTANCE`, `E_HOME_MISMATCH`,
1479
+ `E_BAD_ARGS`, `E_EVENTS_FAILED`.
1480
+
1481
+ ## Lifecycle: stop and retire
1482
+
1483
+ Feature `lifecycle-plans` (and `retire-retention`), `lifecycleApi: 1`. A
1484
+ **plan** lists what an action would touch, with a `planRevision` (24 hex)
1485
+ hashed from the facts that make it safe. Apply carries the revision back; if
1486
+ reality moved it refuses `E_PLAN_STALE` with the fresh `details.plan`. An
1487
+ idempotency key (`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`) makes a retried apply
1488
+ return the first receipt. Recorded parentage (`parentInstance`) is the only
1489
+ relation followed; a child whose parent name matches several homes is listed
1490
+ under `ambiguous` and never acted on.
1491
+
1492
+ ### Stop
1336
1493
 
1337
- ```json
1338
- {"modules":{
1339
- "acme-release-tooling":{"from":{"kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>"},
1340
- "commit":"<oid>","digest":"sha256-…","materializedAt":"<iso>"},
1341
- "oats.okf":{"from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"<oid>","integrity":"sha256-…","repoKey":"github.com/awebai/oats-okf"},
1342
- "commit":"<oid>","digest":"sha256-…","materializedAt":"<iso>"}},
1343
- "providers":{"acme-release-tooling":{},"oats.okf":{"owns":"release-manager","reads":["platform-engineer"],"state-dir":"/Users/ana/.oats/okf"}},
1344
- "workspace":{"key":"github.com/acme/agents","name":"acme","deployment":"/Users/ana/acme-workspace","commit":"<oid>","resolution":"<24 hex>","standalone":false,
1345
- "soul":{"repoKey":"github.com/acme/agents","commit":"<oid>","team":"engineering"}},
1346
- "…":"a package soul's workspace.soul also records package: {id, version, commit, digest, path}, and its id is package:<id>#<soul>",
1347
- "capabilities":[{"id":"oats.okf","capability":"oats.okf","origin":"package:oats.okf@2.1.3","…":"one row per module"}]}
1494
+ ```text
1495
+ oats instance stop <instance> --plan [--no-recursive] [--home <abs>] [--dir <d>] --json
1348
1496
  ```
1349
1497
 
1350
- `workspace.name` (the workspace file's `name`) and `workspace.deployment` (the
1351
- directory holding the deployment's `oats-local.yaml`) are recorded from 0.26.0,
1352
- so a home answers them without discovery: `oats inspect --home` reports the
1353
- recorded name, and `oats operation run --home` hands it to the provider as
1354
- `OATS_WORKSPACE_NAME`. Homes spawned
1355
- before 0.26.0 lack both; the name is then discovered, or `null`.
1356
-
1357
- `workspace.standalone` is `true` when the instance was spawned from the
1358
- **standalone view** (decisions 10/25: a *member* whose workspace could not be
1359
- read — `workspace.key` is then the member repo's key, and `modules` holds the
1360
- soul's `from: here` capabilities plus `oats.core`); `false` for a workspace
1361
- spawn. The same view is marked `standalone: true` in `oats sync --json` (with
1362
- `workspace.name` = `standalone:<repo>`) and in the roster. `capabilities[]` is
1363
- the per-module row set (`toCapabilityRows`) that `oats inspect`/`status` read.
1364
-
1365
- `soulDir` (0.26.0) is the absolute soul directory the instance incarnates — a
1366
- workspace soul's per-commit copy `<deployment>/agents/<soul>/souls/<commit12>`, or
1367
- the read-only soul inside a capability package — and is what every classic
1368
- lifecycle hook and dispatched command receives as `OATS_SOUL`. Instance homes carry no `soul` link.
1369
-
1370
- `digest` is the sha256 of the copied module tree (`<home>/.oats/modules/<cap>/`);
1371
- `providers.<cap>` is the merged payload (manifest defaults ⊕ soul ⊕
1372
- `oats-local.yaml` `settings.<cap>` ⊕ `--provider`), `{}` for a capability with none. Copies live at
1373
- `<home>/.oats/modules/<cap>/` and `<home>/.agents/skills/<cap>/<skill>/`.
1374
-
1375
- ### `oats status [--dir] --json` — module drift
1376
-
1377
- On a workspace deployment the roster does one discovery and rewrites each
1378
- instance's `modules` from the recorded map into **drift rows**, and adds a
1379
- top-level `workspace` reachability field:
1380
-
1381
1498
  ```json
1382
- {"root":"/abs/acme-workspace/agents",
1383
- "agents":[{"name":"release-manager",…,"instances":[{"instance":"release-manager-cut",…,
1384
- "modules":[
1385
- {"name":"acme-release-tooling","from":{"kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>"},"commit":"<oid>",
1386
- "current":{"commit":"<new oid>"},"status":"moved"},
1387
- {"name":"acme-house-style","from":{…},"commit":"<oid>","current":{"commit":"<oid>"},"status":"missing","reason":"capability-absent"},
1388
- {"name":"oats.okf","from":{"kind":"package",…},"commit":"<oid>","current":{"commit":"<oid>","version":"2.1.3"},"status":"current"}]}]}],
1389
- "workspace":{"reachable":true}}
1499
+ {"lifecycleApi":1,"action":"stop","instance":"dev-1","home":"/w/agents/dev/instances/dev-1","recursive":true,"at":"2026-09-28T11:00:00.000Z",
1500
+ "targets":[{"instance":"dev-1","agent":"dev","home":"/w/agents/dev/instances/dev-1","depth":0,"workMode":"worktree","launched":true,
1501
+ "session":{"state":"unknown","present":true,"backend":"tmux","established":true},
1502
+ "work":{"observed":true,"revision":"46c20668…","branch":"feat/x","detached":false,"drift":false,"changed":2,"untracked":1,
1503
+ "upstream":{"ref":null,"ahead":null,"behind":null},"base":{"ref":"origin/main","ahead":1,"behind":0},"remote":{"host":"github.com","path":"acme/one"}},
1504
+ "retiring":false,"stopPending":false,"midTask":true}],
1505
+ "skipped":[],"ambiguous":[],"planRevision":"9c1f2e3d4b5a69788796a5b4","notes":[]}
1390
1506
  ```
1391
1507
 
1392
- - `status`: `current` | `moved` (member or locked package now at another
1393
- commit) | `missing` with `reason`: `capability-absent` (gone from the member /
1394
- package), `package-absent` (no longer locked), or the member's unconfirmed
1395
- reason (`no-backlink`, `cannot-read`, `unconfirmed`, …; `current: null`).
1396
- - Package modules without a lock are reported `current`.
1397
- - Offline: `"workspace":{"reachable":false,"code":"E_REMOTE_UNREADABLE","reason":"E_REMOTE_UNREADABLE: network","message":"…"}`
1398
- and `modules` stays the recorded map (no drift rows). A deployment without
1399
- `oats-local.yaml` has no `workspace` field and `modules` as recorded.
1400
- - Text mode prints `modules: <cap> from <member|package …> @ <7-char>` lines
1401
- for non-current modules (`--verbose` for all).
1402
- - `instances[].soul` is the soul source's drift: `{ repoKey, commit, current,
1403
- status, reason? }`. For a **package soul** (feature `package-souls`) it also
1404
- carries `package`, `version` and `currentVersion`; `moved` means the package
1405
- pin moved (another version/commit is locked now), `missing` has `reason`
1406
- `package-absent` (no longer locked or declared) or `soul-absent` (the locked
1407
- package no longer ships it). Text: `soul: <name> from package <id> v<version>
1408
- @ <7-char> [package moved since (now v<version> @ <7-char>)]`.
1409
-
1410
- ### Triggers (feature `triggers`, OATS 0.28.0) — `oats trigger … --json` → `triggerApi: 1`
1411
-
1412
- Event-driven spawns of a deployment ([schedules.md#triggers](schedules.md#triggers)).
1413
- Definitions live in `oats-schedules.json` (`kind: "trigger"`); `oats schedule list`
1414
- does not show them. From 0.29.0 a row's `id` is qualified (`local/<id>` here; a
1415
- workspace trigger's is `<member>/<id>`) and the row carries the shared fields of
1416
- [workspace triggers and schedules](#workspace-triggers-and-schedules-feature-automations-oats-0290-automationsapi-1).
1508
+ - `targets`: the recorded descendants, deepest first, then the instance
1509
+ (`depth: 0`). With `--no-recursive` descendants go to `skipped` (`{instance,
1510
+ agent, home, reason: "recursive=false"}`). `ambiguous[]`: `{instance, agent,
1511
+ home, reason}`.
1512
+ - `session.state` is the backend's word: `shell`, `stopped` and
1513
+ `not-launched` are idle; `unknown` is a running process tmux cannot name
1514
+ (the normal state of a harness). Not established: `{state:
1515
+ "unestablished", present: null, backend: null, established: false,
1516
+ reason}`; render it as unknown, never idle.
1517
+ - `work` is the Git observation summarized (`changed` counts changed,
1518
+ renamed, copied and unmerged rows), or `{observed: false, reason}`.
1519
+ - `midTask`: `true`, `false` or `"unknown"`.
1417
1520
 
1418
- ```json
1419
- {"triggerApi":1,"scope":"/abs/deployment","triggers":[
1420
- {"id":"okf-harvest-review","enabled":true,"kind":"trigger",
1421
- "on":{"source":"github.pull_request","repo":"github.com/acme/knowledge","events":["opened","reopened","ready_for_review"],"labels":["okf-harvest"],"base":"main","poll":"2m"},
1422
- "spawn":{"soul":"oats.okf/knowledge-maintainer","purpose":"review-pr-{number}","task":"…","teams":["okf"],"launchConfig":"reviewers","harness":"claude","model":"opus"},
1423
- "concurrency":{"max":2,"perKey":1},"template":{"package":"oats.okf","version":"4.0.0","commit":"<oid>","template":"harvest-review"},
1424
- "triggerApi":1,"scope":"/abs/deployment","createdAt":"<iso>","updatedAt":"<iso>"}]}
1521
+ ```text
1522
+ oats instance stop <instance> --apply --plan-revision <rev> --idempotency-key <key> [--no-recursive] [--grace-ms <n>] [--home <abs>] --json
1425
1523
  ```
1426
1524
 
1427
- - `list` → the document above; `show <id>`, `add`, `enable`, `disable` →
1428
- `{ trigger }` (one row); a stored definition that no longer validates carries
1429
- `invalid: { code, message }`. `remove <id>` → `{ removed, live: [instance] }`.
1430
- - `status [<id>]` → `{ triggerApi, scope, triggers: [Status] }`. It writes
1431
- nothing. Each `Status`:
1432
- - `id`, `name`, `enabled`, `runsHere`, `reason`, `enabledHere`, `repo`, `soul` (the soul name);
1433
- - `concurrency: { max, perKey }` and `liveCount`: live instances against `max`;
1434
- `live: [{ instance, home, repo, number, event }]`;
1435
- - `lastPoll: { at, ok: true, prs, matching } | { at, ok: false, error } | null`;
1436
- `nextPollAt`; `nextDue` (the next poll when it runs here, else `null`);
1437
- - `pending: [{ key, event, number, url, observedAt }]`: observed, not yet spawned
1438
- (held, or its spawn failed);
1439
- - `fired: [{ key, at, instance, home, event, number }]` (newest 50) and `firedTotal`;
1440
- - `lastError: { at, code, message, key? } | null`.
1441
- - `test <id>` → `{ triggerApi, id, ok, gh: { ok, account, credentialSource:
1442
- "keyring" | "config" | "env:<VAR>" | "unknown" | null, reachesHostTimer:
1443
- boolean | null, note, detail }, repo: { key,
1444
- readable, fullName, permissions: { push, maintain, admin }, canMerge } |
1445
- { key, readable: false, error }, soul: { resolves, name, agent, messaging } |
1446
- { resolves: false, name, error }, teams: { requested, undeclared | null,
1447
- messaging }, wouldFire: [{ key, repo, number, event, url, held? }], pollError?, problems:
1448
- [string], warnings: [string], spawned: false }`. It writes nothing. `ok`
1449
- counts `problems` only; a credential the host timer cannot reach
1450
- (`reachesHostTimer: false`) is a warning.
1451
- - `oats schedule list --json` gains `triggers: { count, command: "oats trigger
1452
- list" }`: the triggers it does not list.
1453
- - The tick's `considered[]` gains trigger rows `{ workspace, trigger, action,
1454
- … }` with `action` `not-due`, `poll-failed`, `polled` (`prs`, `matching`;
1455
- nothing to fire), `held`, `fired` (`key`,
1456
- `instance`, `home`), `spawn-failed` (`key`, `code`, `error`), `would-fire`
1457
- (`--dry-run`) or `invalid`.
1458
- - A triggered instance's `instance.json.trigger` is `{ id, key, source, repo,
1459
- number, url, event, headSha, observedAt, eventFile }`; the event file is
1460
- `OATS_TRIGGER_EVENT_FILE`.
1461
- - Errors: `E_TRIGGER_INVALID { field }`, `E_TRIGGER_EXISTS`,
1462
- `E_TRIGGER_UNKNOWN`, `E_BAD_ARGS` (`missing` / `parameters` for a template),
1463
- `E_PACKAGE_MISSING`, `E_PACKAGE_MANIFEST`, `E_LOCAL_MISSING`.
1464
-
1465
- ### Workspace triggers and schedules (feature `automations`, OATS 0.29.0; `automationsApi: 1`)
1466
-
1467
- See [schedules.md#workspace-triggers-and-schedules](schedules.md#workspace-triggers-and-schedules).
1468
- `oats trigger list --json` and `oats schedule list --json` answer this machine's
1469
- local items and every workspace item defined in a member the user can read. The
1470
- Desktop renders these rows and never re-derives them. The two lists stay separate:
1471
- a trigger never appears in `schedule list`, and a schedule never appears in
1472
- `trigger list`.
1473
-
1474
- - Both lists add:
1475
- - `host: { name | null, ghUser: { <gh host>: <login> | null } }`: this machine's
1476
- `oats-local.yaml` `host.name`, and who its `gh` is logged in as on every GitHub
1477
- host the rows name (the owners'; a trigger's repository's) — `null` when `gh`
1478
- is not authenticated there. The Desktop compares it with a row's `owner`.
1479
- - `snapshot: { takenAt, problems } | null` (`null` until `oats sync` has found some).
1480
- - `scheduler: { installed, active, registered, lastTick, maxConcurrent, … }`: the
1481
- host tick (the same object as `oats schedule host status`). Nothing runs unless
1482
- it is installed, active and this deployment is registered.
1483
- - **Identity:**
1484
- - `id`:
1485
- - a trigger row's is always qualified (`local/<id>`, `<member>/<id>`);
1486
- - a local schedule row keeps its bare id (the 0.28 contract);
1487
- - a workspace schedule row's is `<member>/<id>`.
1488
- - `qualifiedId` is always the qualified form, and `name` is the bare id.
1489
- - Every verb accepts `local/<id>` or a bare local id.
1490
- - **Shared fields in every row:**
1491
- - `origin`: where the item is defined, and where to open it:
1492
- - `{ kind: "local", path: "oats-schedules.json", url: null, localPath }`;
1493
- - `{ kind: "workspace", repoKey, path, commit, url, localPath }`: `url` is the
1494
- file's web URL at `commit` (`https://github.com/<owner>/<repo>/blob/<commit>/<path>`
1495
- for a `github.com` member, else `null`); `localPath` is the file in this
1496
- machine's clone of the member (`null` when the member is not cloned here).
1497
- - `description`, `owner`, `runsOn`;
1498
- - `runsHere`; `reason` (`null` | `host-unnamed` | `assigned-elsewhere` | `owner-mismatch`) with `reasonDetail`;
1499
- - `enabledHere`;
1500
- - `soul`: `{ name, origin } | null` (`null` for a command, wake or operation schedule). `origin` is where the name resolves, per the snapshot:
1501
- - `{ kind: "member", repoKey, member }`: a soul in a workspace member;
1502
- - `{ kind: "package", package, version }`: a soul of a locked package;
1503
- - `{ kind: "external", repoKey, source }`: an external soul (`source` is the workspace's `external[].source` ref);
1504
- - `{ kind: "ambiguous", candidates }`: a bare name several souls answer to (`candidates` is how many); a spawn needs the qualified name;
1505
- - `null`: not found (or no snapshot yet).
1506
- - `task`: the template, verbatim;
1507
- - `teams`, `launchConfig`, `harness`, `model`, `concurrency`;
1508
- - `lastRun`, `nextDue`;
1509
- - `invalid?: { code, message, field? }`.
1510
- - **A trigger row** also carries `kind: "trigger"`, `on`, `spawn`, and `template?`:
1511
- - `on: { source: "github.pull_request", repo: "<host>/<owner>/<repo>", events: [opened | reopened | ready_for_review | labeled | synchronize], labels: [string], base?: string, poll: "<n>s|m|h" }` (`base` absent: any base branch);
1512
- - `spawn: { soul, purpose, task, teams: [label], launchConfig?, harness?, model?, yolo?, backend? }`
1513
- (`purpose` and `task` are templates over `{repo}`, `{number}`, `{url}`, `{event}`, `{trigger}`, `{headSha}`;
1514
- `launchConfig` names a launch configuration in the running host's `oats-local.yaml`);
1515
- - `template?: { package, version, commit, template }`: the package template it was added from.
1516
- - `lastRun` is the last fired event: `{ at, instance, home, event, number, key }`.
1517
- - `nextDue` is the next poll, only when it runs here; `null` before its first poll (it polls at the next tick).
1518
- - **A schedule row** keeps every 0.28 field (`scheduleApi: 2`). Its `kind` is the run (`spawn` | `command` | `wake` | `operation`); it also carries `cron` and `tz`.
1519
- - `nextDue` is the next minute, only when it runs here.
1520
- - `teams` is `[]` and `concurrency` is `null`.
1521
- - A workspace schedule another host runs carries its definition and placement only: `lastRun` and `nextDue` are `null`.
1522
- - A spawn schedule's `launchConfig` names a launch configuration in the running host's `oats-local.yaml`.
1523
- - **Naming:** `nextDue` is the one name for "when it next runs" in every trigger and
1524
- schedule row. A schedule row still carries the 0.24 `nextRun` for older readers;
1525
- they agree whenever it runs here.
1526
- - **Actions:**
1527
- - `enable` and `disable` on a workspace id edit `oats-local.yaml` `triggers.disabled` or `schedules.disabled`.
1528
- - `update` and `remove` refuse it with `E_AUTOMATION_WORKSPACE { id, origin }`.
1529
- - `schedule run` and `schedule reconcile` work when it runs here, else `E_AUTOMATION_NOT_HERE { id, reason, runsOn, owner }`.
1530
- - **`oats trigger test <id>`** adds `placement: { runsOn, owner, host, runsHere, reason, detail?, enabledHere }`. Any reason, or disabled here, is a problem (`ok: false`).
1531
- - **`oats schedule test <id> --json`** (local or workspace) → `{ test: { id, qualifiedId, kind,
1532
- placement: { runsHere, reason, reasonDetail?, enabledHere, runsOn, owner, host },
1533
- soul: { name, origin, resolves, error: { code, message } | null } | null, nextDue,
1534
- spawned: false, problems: [string], ok } }`. `soul` is checked the way the run
1535
- would start it (`oats spawn <soul> --preview`, which writes nothing); it is `null`
1536
- for a command, wake or operation. `nextDue` is the next cron match whether or not
1537
- this host runs it (`placement` says that). Not running here, disabled, invalid or a
1538
- soul that does not resolve is a problem (`ok: false`). It spawns nothing and records
1539
- nothing. Errors: `E_SCHEDULE_UNKNOWN`, `E_BAD_ARGS`.
1540
- - **`oats trigger|schedule add … --workspace <member> --runs-on <host> --owner <host>/<login> --json`** answers `{ id, written, file: { member, repoKey, path, content, written? } }`.
1541
- - Errors: `E_AUTOMATION_MEMBER` (not a confirmed member), `E_TRIGGER_EXISTS` or `E_SCHEDULE_EXISTS` (the file exists), and the kind's validation codes.
1542
- - **`oats automations refresh --json`** answers `{ automationsApi, snapshot, triggers, schedules, problems: [...], takenAt }`.
1543
- - **`oats sync --json`** gains `automations: { triggers, schedules, problems, takenAt }`. Discovery problems join `problems` (`E_AUTOMATION_SCHEMA`, `E_AUTOMATION_DUPLICATE`, each with `kind`, `repoKey` and `path`).
1544
- - **`oats workspace status --json`** gains `automations: { host, snapshot, rows: [{ kind, id, runsOn, owner, runsHere, reason, enabledHere, origin, invalid? }] }`.
1545
- - **The tick's `considered[]`** gains the trigger action `not-here` (`reason: owner-mismatch`, `detail`): a trigger naming this host that this host cannot run. A workspace schedule's row `id` is its state key, `<member>~<id>`. A failed snapshot refresh is `{ action: "error", error: "automations refresh: …" }`.
1546
-
1547
- ### Desktop facts (feature `desktop-facts`, OATS 0.29.0)
1548
-
1549
- These are facts the Workspace view shows. The kernel reports them so the
1550
- Desktop never works them out itself. Gate reading every field below on
1551
- `desktop-facts` in `features[]`. No API integer changes, and every field is an
1552
- addition to an existing row.
1553
-
1554
- **`oats inspect --soul <name> --json`: why each capability is there**
1555
-
1556
- - `capabilities[].composedFrom` says which layer put the module in the soul:
1557
- `"workspace"` (`defaults.<slot>` or `defaults.capabilities`),
1558
- `"team:<label>"` (`defaults.byTeam.<label>.capabilities`) or `"soul"` (the
1559
- soul's own `capabilities:`). This is the same vocabulary as
1560
- `layers.<slot>.from`. It is `null` on `inspect --home`, because a spawn does
1561
- not record it. `from` stays the module's origin object (`{kind, repoKey,
1562
- commit}` or the package object), so it is a separate key.
1563
- - `capabilitiesOff[]` lists the capabilities the soul turned off, which a
1564
- lower layer would otherwise have given it. They are not rows of
1565
- `capabilities[]`, because those are resolved modules with operations. Each
1566
- entry is `{ id, off: true, from: "soul", reason, slot?, overrides }`:
1567
- - `reason: "off"`: the soul wrote `<id>: off` over a workspace or team
1568
- default.
1569
- - `reason: "slot-none"`: the soul wrote `<slot>: none` (`slot` names it),
1570
- which emptied the slot the workspace filled with `<id>`.
1571
- - `overrides`: the layer whose default was turned off (`"workspace"` or
1572
- `"team:<label>"`).
1573
- - Sorted by id. `[]` on `inspect --home`.
1574
-
1575
- **`oats souls --json` rows**
1576
-
1577
- - `harness`, `model`, `harnessFrom`: what a spawn of the soul starts with
1578
- when no `--harness`/`--model` is given. A v2 `soul.yaml` cannot declare a
1579
- harness or a model, so today this is always `harness: "pi"`, `model: null`
1580
- (the harness's native model) and `harnessFrom: "kernel-default"`.
1581
- `harnessFrom: "soul"` is reserved for a schema that lets a soul declare
1582
- one.
1583
- - `spawnable`, `problem`: whether a spawn here would refuse.
1584
- - `problem` is `{ code, message }` when a spawn would refuse, else `null`.
1585
- - The kernel resolves the soul exactly as a spawn does, but spawns nothing,
1586
- writes nothing and reads only the sync cache.
1587
- - Codes: `E_SOUL_DISABLED` (this machine's `souls.disabled`),
1588
- `E_TEAM_CONFLICT`, `E_CAPABILITY_MISSING`, `E_CAPABILITY_PRIVATE`,
1589
- `E_CAPABILITY_INCOMPATIBLE`, `E_PACKAGE_MISSING`, `E_PACKAGE_INTEGRITY`,
1590
- `E_LOCK_SCHEMA`, `E_REMOTE_*`, and any other resolution refusal.
1591
- - An `E_TEAM_UNKNOWN` problem in `problems[]` is informational. It does not
1592
- make a soul unspawnable.
1593
- - `file`: `{ path, url }`, the soul's `soul.yaml` in its repository (see
1594
- **URLs** at the end of this section).
1595
-
1596
- **`oats capabilities --json` rows**
1597
-
1598
- - `layer` on every row. Package rows now carry it too, from the package
1599
- manifest; `null` for a capability outside the slots.
1600
- - `description`: the manifest's `description`, or `null`.
1601
- - `skills`, `commands`, `hooks`: what the capability provides, by name,
1602
- sorted.
1603
- - `skills` is enumerated as a spawn would. It is `null` when the declared
1604
- skills cannot be listed, which a spawn of it would refuse.
1605
- - `commands` and `hooks` are the keys of the manifest's `commands` and
1606
- `hooks`.
1607
- - `file`: `{ path, url }`, the capability's `oats.json`, or `null` when the
1608
- manifest cannot be read.
1609
- - `tree`: a member capability's fingerprint, the Git tree id of its
1610
- directory at the member commit. The same bytes give the same id. It is
1611
- `null` on package rows, whose fingerprint is `integrity` in the lock (see
1612
- `oats workspace status`).
1613
- - A package whose manifests cannot be read at its locked commit leaves these
1614
- facts `null` on its rows.
1615
- - Package manifests are read at the locked commit from the sync cache. There
1616
- is no network beyond what `sync` already fetched.
1617
-
1618
- **`oats workspace status --json`**
1525
+ Children first: SIGTERM to the harness processes and a bounded wait
1526
+ (`--grace-ms`, 1–300000, default 20000), never escalated. Home, work,
1527
+ transcript and launch configuration are kept; `oats session restart` brings
1528
+ the instance back.
1529
+
1530
+ - The receipt is `{lifecycleApi: 1, action: "stop", instance, home,
1531
+ idempotencyKey, planRevision, at, ok, results, retained: ["home", "work",
1532
+ "transcript", "launch"], replayed: false}`. A result is `{instance, home,
1533
+ ok: true, stopped, alreadyIdle, state}` or `{instance, home, ok: false,
1534
+ code, message, stillRunning: [pid]}`.
1535
+ - `ok: false`: at least one target still runs (text mode exits 1).
1536
+ - A replay is the stored receipt (`<home>/.oats-stop-receipt.<key>.json`)
1537
+ with `replayed: true`.
1538
+ - Refusals: `E_BAD_ARGS` (no `--plan-revision`, a bad key, not exactly one of
1539
+ `--plan`/`--apply`), `E_PLAN_STALE`, `E_INSTANCE_RETIRING` and
1540
+ `E_LIFECYCLE_BUSY` (each with `details.plan`), `E_SESSION_UNKNOWN`,
1541
+ `E_AMBIGUOUS_INSTANCE`, `E_HOME_MISMATCH`, `E_LIFECYCLE_FAILED`.
1542
+
1543
+ <a id="retire"></a>
1544
+ ### Retire
1619
1545
 
1620
- ```json
1621
- {"workspace":{"…":"…","file":{"path":"oats-workspace.yaml","url":"https://github.com/acme/agents/blob/<oid>/oats-workspace.yaml"}},
1622
- "members":[{"…":"…","url":"https://github.com/acme/tools/tree/<oid>","membershipFile":{"path":"oats-membership.yaml","url":"https://github.com/acme/tools/blob/<oid>/oats-membership.yaml"}}],
1623
- "packages":[{"id":"oats.okf","version":"3.0.0","source":"catalog:oats.okf","commit":"<oid>","…":"…","latest":{"version":"4.0.0","ref":"v4.0.0"}}],
1624
- "defaults":{"slots":{"knowledge":{"name":"oats.okf","from":"package"},"messaging":"none","tasks":null},
1625
- "capabilities":[{"name":"acme-house-style","from":"github.com/acme/agents","off":false}],
1626
- "byTeam":{"engineering":{"capabilities":[{"name":"acme-house-style","from":null,"off":true},{"name":"acme-deploy","from":"package","off":false}]}}},
1627
- "clones":[{"key":"github.com/acme/agents","name":"agents","path":"/abs/acme-workspace/agents","rule":"convention"},
1628
- {"key":"github.com/acme/tools","name":"tools","path":null,"rule":null}],
1629
- "disabledSouls":["release-reviewer"],
1630
- "lock":{"path":"/abs/acme-workspace/oats-lock.json","lockfileVersion":3}}
1546
+ ```text
1547
+ oats retire <instance> --plan [--home <abs>] [--dir <d>] --json
1631
1548
  ```
1632
1549
 
1633
- - `defaults`: the workspace file's defaults, as declared rather than
1634
- resolved for a soul.
1635
- - `slots.<slot>` is `{ name, from }` when the workspace fills it, `"none"`
1636
- when it empties it, and `null` when it says nothing.
1637
- - `capabilities` and `byTeam.<label>.capabilities` are rows `{ name, from,
1638
- off }`, sorted by name. `from` is the declared location (`"package"`,
1639
- `"here"` or a member repo key); an `off` row has `from: null`.
1640
- - Standalone: slots `null`, `capabilities: []` and `byTeam: {}`.
1641
- - `clones`: this computer's clone of each member.
1642
- - `path` is the absolute clone, or `null` when this machine has none.
1643
- - `rule` names what found it: `"clones"` (the `oats-local.yaml` `clones:`
1644
- entry) or `"convention"` (`<deployment>/<member name>`). It is `null`
1645
- with no clone.
1646
- - A path that is not the member's clone gives `path: null, rule: null,
1647
- problem: { code: "E_CLONE_MISMATCH", message }`, the refusal a spawn
1648
- would meet.
1649
- - (`--repo` is a spawn option, so it plays no part here.)
1650
- - `disabledSouls`: `oats-local.yaml` `souls.disabled`, as written.
1651
- - `lock`: `{ path, lockfileVersion }`. The per-package commit is each
1652
- `packages[]` row's `commit`, and its fingerprint is `integrity`.
1653
- - `packages[].latest`: `{ version, ref }` when the official catalog shipped
1654
- with this kernel has a newer version of a catalog-sourced package than the
1655
- lock holds. It is `null` when the pin is current and for `git:` packages.
1656
- It never reaches the network: the catalog is the kernel's own
1657
- (`OATS_PACKAGE_CATALOG` overrides it, as for `sync`).
1658
- - `workspace.file` is `{ path, url }` for the workspace file in the
1659
- workspace repository (at `workspace.key` @ `workspace.commit`). It is
1660
- `null` for a standalone deployment.
1661
- - `members[].url` is the member repository at its commit.
1662
- `members[].membershipFile` is `{ path, url }`.
1663
-
1664
- **`oats status --json` instance rows**
1665
-
1666
- - A member module's `modules[].current` gains `version` (the capability's
1667
- manifest version at the current commit, `null` when it has none) beside
1668
- `commit`, on `current` and `moved` rows. Package rows already carried it. On a `moved` row, the recorded `commit`/`from` and `current`
1669
- together say what moved and to what.
1670
- - `startedAt`: the last session start or restart (the session receipt). A
1671
- home spawned with a launch and never restarted uses `createdAt`. A home
1672
- never launched is `null`. `createdAt` stays the spawn time.
1673
- - `modelFrom`: where the model the home runs came from.
1674
- - `"soul"`: the soul's model preference.
1675
- - `"spawn"` or `"start"`: an explicit `--model` on that command.
1676
- - `"launch-config"`: a launch configuration's model.
1677
- - `"harness-default"`: the harness's own model.
1678
- - A start that reuses the recorded model keeps the recorded answer.
1679
- - `null` for a home spawned before 0.29.0. `instance.json` records it as
1680
- `modelFrom`.
1681
- - `identityAddress`: the messaging identity's `address` (else `alias`) that
1682
- the messaging capability recorded (`capabilityMeta.<messaging>.identity`),
1683
- passed through unchanged. `null` otherwise.
1684
-
1685
- **URLs.** Every `url` is a browsable page of
1686
- the file (or of the repository, for a member) at the commit the row names.
1687
- Only repositories on `github.com` have one (`https://github.com/<org>/<repo>/blob/<commit>/<path>`,
1688
- or `/tree/<commit>`). Every other host and local repository gives `url:
1689
- null`, with `path` still set. `path` is relative to that repository's root.
1690
-
1691
- **Help.** `oats help` lists `spawn … [--provider <capability> <key>=<value>]`.
1692
-
1693
- ### Eligible teams (feature `teams`, OATS 0.26.0)
1694
-
1695
- A soul's `team` may be a list of labels; the first is the primary
1696
- ([teams contract](design/2026-09-25-teams-contract.md)). Every label is an
1697
- **eligible** team — which the messaging provider may join on an explicit
1698
- request (a spawn provider setting, or its own join/leave verbs); the kernel
1699
- joins nothing. One entry per label, in soul order:
1700
-
1701
1550
  ```json
1702
- {"label":"engineering","team":"aweb:acme.eng","mapped":true,"payload":{"private":"per-human","team":"aweb:acme.eng"}}
1703
- {"label":"reviewers","team":null,"mapped":false,"payload":{"private":"per-human"}}
1551
+ {"lifecycleApi":1,"action":"retire","instance":"dev-1","home":"/w/agents/dev/instances/dev-1","at":"2026-09-28T11:10:00.000Z",
1552
+ "facts":{"session":{"state":"shell","present":true,"backend":"tmux","established":true},
1553
+ "work":{"observed":true,"revision":"46c20668…","branch":"feat/x","detached":false,"drift":true,"changed":0,"untracked":1,
1554
+ "upstream":{"ref":null,"ahead":null,"behind":null},"base":{"ref":null,"ahead":null,"behind":null},"remote":null},
1555
+ "workMode":"worktree","repo":"/w/one","recordedBranch":"agents/dev-1",
1556
+ "children":[{"instance":"dev-1-child","agent":"dev","home":"/w/agents/dev/instances/dev-1-child","session":{"state":"shell","present":true,"backend":"tmux","established":true}}],
1557
+ "ambiguous":[],"pullRequest":"unknown"},
1558
+ "defaults":{"retainWorktree":true,"deleteBranch":false,"stopChildren":true,"retainChildren":true},
1559
+ "planRevision":"4e5f6a7b8c9d0e1f2a3b4c5d","notes":["the worktree is on feat/x, not the recorded agents/dev-1; …"]}
1704
1560
  ```
1705
1561
 
1706
- `payload` = `workspace.messaging` ⊕ `byTeam[label]` (base alone when unmapped);
1707
- `team` = the mapped payload's team id, else `null`. No label → `[]`.
1708
-
1709
- Where it appears:
1710
- - `oats spawn … --preview --json`: top-level `teams` (next to `team`, which
1711
- stays the primary label). `settings.<messaging>` stays the primary's merged
1712
- payload and never carries `teams`.
1713
- - `oats inspect --soul|--home --json`: top-level `teams` and `teamsSource`.
1714
- For `--home` the teams are **live** (the soul's labels and the workspace's
1715
- `messaging` as the deployment resolves them now, in two repository reads;
1716
- the home's modules are unchanged): `teamsSource: "live"`, or `"recorded"`
1717
- with the spawn-time list when the workspace cannot be read now. Providers
1718
- get the same marker as `OATS_TEAMS_SOURCE`.
1719
- - `instance.json`: `teams` (the spawn-time list, kept as evidence; never
1720
- rewritten) and `workspace.soul.labels`.
1721
- - `oats souls --json`: each row carries `labels` (`team` stays the primary).
1722
- - A spawn, preview or `inspect --soul` whose labels give one capability
1723
- different `defaults.byTeam` entries answers `E_TEAM_CONFLICT { capability,
1724
- labels: [a, b], entries, paths }`.
1725
-
1726
- ### Probe
1562
+ - The plan changes nothing except appending a `retire-planned` event to the
1563
+ workspace log. `pullRequest` is always `"unknown"`. Branch actions use the
1564
+ worktree's branch, never `recordedBranch`.
1565
+ - A home spawned before 0.25.9 has no session receipt (0.30). Its
1566
+ `facts.session` is `{state: "absent", present: false, backend, established:
1567
+ true, note}` when the session is observably gone: instance.json records no
1568
+ launch, or the recorded tmux server is not running, or the recorded window
1569
+ is gone and no pane on that server works in the home, and in every case no
1570
+ live process on the host has its working directory in the home (`lsof`; a
1571
+ scan that cannot run counts as not absent). Retire then proceeds
1572
+ without quiescing (hooks run, work is preserved). Otherwise it stays
1573
+ `unestablished`, with a `note` saying why, and retire refuses with
1574
+ `E_RUNTIME_ENDPOINT_UNKNOWN`, `--force` included. `notes` repeats either
1575
+ case. Read an unknown `state` as not idle.
1576
+
1577
+ Plain `retire` keeps a worktree-mode instance's work: the worktree is moved
1578
+ (`git worktree move`) to `<deployment>/.agents/worktrees/<repo>/<branch>` (a
1579
+ `-2` suffix if taken; `detached-<oid12>` when detached), state intact.
1727
1580
 
1728
- ```json
1729
- {"…":"…","features":["…","workspace-v2","instance-modules","spawn-provider-payload","packages-no-approval","spawn-name","settings-origins","teams"],"workspaceApi":2}
1581
+ ```text
1582
+ oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--delete-branch] [--home <abs>] --json
1730
1583
  ```
1731
1584
 
1732
- A feature is listed only once the binary implements it. Gate `sync`/`package`/
1733
- `workspace status`/`capabilities`/`souls` on `workspace-v2`; gate reading
1734
- `instance.json.modules` and preview `modules[]` on `instance-modules`; gate
1735
- `--provider` on `spawn-provider-payload`; gate reading `teams`, `teamsSource`,
1736
- `labels` and `warnings[]` on `teams`; gate reading `declares` on
1737
- `settings-declared`.
1738
-
1739
- ## Instruction refresh (`oats session recompose`) — removed in 0.26.0
1585
+ A first retire prints the **raw receipt**, not an envelope:
1740
1586
 
1741
- `oats session recompose` answers `E_UNKNOWN_COMMAND`, and the
1742
- `session-recompose` feature is no longer advertised. An instance never changes
1743
- under itself: the refresh path is a re-spawn (preview → apply of the same
1744
- soul/purpose, then retire the old instance), which fetches the soul at the
1745
- member's current commit.
1587
+ ```json
1588
+ {"retired":"dev-1","agent":"dev",
1589
+ "retention":{"worktree":"retained","movedTo":"/w/.agents/worktrees/one/feat-x","branch":"feat/x","detachedAt":null,"recordedBranch":"agents/dev-1"},
1590
+ "worktreeRemoved":false,"branchDeleted":false,"removedDir":true,
1591
+ "workRecovery":{"path":"/w/.agents/recovered/dev-1-20260928T111000Z","classes":["untracked"],"bytes":2048,
1592
+ "outputs":{"paths":[{"path":"notes.md","bytes":2048}],"bytes":2048}},
1593
+ "childrenStopped":[{"instance":"dev-1-child","home":"/w/agents/dev/instances/dev-1-child","ok":true,"stopped":false,"alreadyIdle":true}],
1594
+ "planRevision":"4e5f6a7b8c9d0e1f2a3b4c5d","idempotencyKey":"r1","replayed":false}
1595
+ ```
1746
1596
 
1747
- ## Mutations exposed to Desktop v1
1597
+ - `retention`: `{worktree: "retained" | "removed" | "absent", movedTo?,
1598
+ branch, detachedAt?, recordedBranch, branchDeleted?,
1599
+ branchDeletionSkipped?: {expected, actual, reason}}`, or `null` for a
1600
+ non-worktree mode.
1601
+ - `--discard-worktree` removes the worktree. `--delete-branch` deletes the
1602
+ worktree's verified branch (re-verified at deletion time) and implies
1603
+ discarding; a mismatch deletes nothing and reports
1604
+ `branchDeletionSkipped`.
1605
+ - `workRecovery` (or `workRecoveries[]`): `{path, classes, bytes, outputs?,
1606
+ repoCopy?}`; `outputs: {paths: [{path, bytes}], bytes}` names what was
1607
+ copied beyond tracked state, largest first.
1608
+ - When they apply: `rollbackIncomplete` and `retainedHome` (cleanup
1609
+ incomplete, home kept, exit 1), `forcedIncomplete`, `relinked`,
1610
+ `capabilityMeta`, `warnings`, `wakeSchedulesRemoved`.
1611
+ - A deferred self-retire (`--self`) prints `{retired, agent, deferred: true,
1612
+ pendingMarker, resultPath, logPath, completesInSec, completionPid}`, or
1613
+ `{…, alreadyScheduled: true, requestedAt}`.
1614
+
1615
+ **Guarded apply** (what the Desktop sends): `--plan-revision` and
1616
+ `--idempotency-key` together.
1617
+ - A used key replays its receipt as an **envelope** with `replayed: true`
1618
+ (receipts live beside the instances directory and outlive the home).
1619
+ - The revision is checked against a fresh plan: `E_PLAN_STALE {plan}`.
1620
+ - Children are stopped first (never escalated) and kept; `childrenStopped[]`
1621
+ lists them. One still running refuses everything: `E_CHILDREN_RUNNING
1622
+ {childrenStopped, plan}`.
1623
+ - A first guarded retire prints the raw receipt with `planRevision`,
1624
+ `idempotencyKey` and `replayed: false`.
1625
+
1626
+ Refusals (envelopes): `E_PLAN_STALE`, `E_CHILDREN_RUNNING`,
1627
+ `E_WORK_PRESERVATION_FAILED` (the home is kept; retry or
1628
+ `--discard-worktree`), `E_SESSION_UNKNOWN`, `E_AMBIGUOUS_INSTANCE`,
1629
+ `E_NO_ROOT`, `E_LIFECYCLE_FAILED`. A recovery whose Git status disagrees with
1630
+ the source's carries `details: {home, statusDisagreement: {repo, rows: [{path,
1631
+ source, recovery}], total}}` (the first 10 paths). Usage errors are text on
1632
+ stderr, not envelopes.
1633
+
1634
+ ## Sessions and launch configurations
1635
+
1636
+ ### Start and restart
1748
1637
 
1749
- The commands below use the same envelope. Additional capability operations
1750
- are described in [the operations contract](design/operations-contract.md).
1638
+ ```text
1639
+ oats session start --home <abs> [--launch-config <name>|none] [--harness pi|claude|codex] [--model <id>] [--yolo|--no-yolo] [--server <id>] --json
1640
+ oats session restart --home <abs> [the same options] [--stop-grace <1-300 s>] --json
1641
+ ```
1751
1642
 
1752
- ### Existing-home launch and restart
1643
+ Features `session-start`, `session-restart`, and `launch-config` for the
1644
+ selection flags. See [the start workflow](desktop-instance-start.md).
1645
+
1646
+ - The result is `{instance, agent, home, harness, backend, model,
1647
+ launchConfig, yolo, target, startedAt, restartCount, reused, warnings}`,
1648
+ plus `nativeRecordId` and `stop` (a restart's stop receipt) when they apply.
1649
+ - `warnings` (0.30) is always present: an array of strings, the warnings the
1650
+ capabilities' `launch` hooks returned for this start (as spawn's
1651
+ `warnings`), `[]` when there are none. Each is also appended to the
1652
+ instance's events as a `launch-warning` row, `data: {message}`. They are
1653
+ advisory: the start went ahead. Earlier kernels omit the field; read a
1654
+ missing `warnings` as `[]`.
1655
+ - Restart is one command: the kernel validates the new selection before
1656
+ stopping, and owns the stop, lock, launch recovery and metadata. Never
1657
+ restart by retiring and spawning.
1658
+ - A lost response does not mean the launch failed: check status before a
1659
+ retry. A remote home's saved route names its execution host.
1660
+ - Errors: `E_BAD_ARGS`, `E_SESSION_UNKNOWN`, `E_UNSUPPORTED_MODE`,
1661
+ `E_SESSION_START_BUSY`, `E_INSTANCE_RETIRING`, `E_LAUNCH_*`,
1662
+ `E_MODEL_UNKNOWN`, `E_UNSUPPORTED_HARNESS`, `E_SESSION_FAILED`.
1663
+
1664
+ ### Upload
1753
1665
 
1754
1666
  ```text
1755
- oats session start --home /absolute/home [--server id] \
1756
- [--launch-config name] [--harness pi|claude|codex] \
1757
- [--model id] [--yolo|--no-yolo] --json
1758
- oats session restart --home /absolute/home [the same options] --json
1667
+ oats session upload (--home <abs> | --server <id> --instance <name> | --server <id> --home <abs>) --file <path> --json
1759
1668
  ```
1760
1669
 
1761
- Desktop addresses the exact existing home from the selected workspace's
1762
- roster. Restart is one kernel command. The kernel owns configuration
1763
- validation, stop observation, the lifecycle lock, launch recovery and session
1764
- metadata. Desktop does not implement restart by retiring and spawning.
1765
- Failure or timeout requires a fresh status check before retrying: a lost
1766
- response does not establish that launch failed.
1670
+ Feature `session-upload`: copies a file (at most 64 MiB) into the instance's
1671
+ attachments. Remotely the bytes go on ssh stdin to the host's `session
1672
+ receive`, and the sha256 is verified.
1767
1673
 
1768
- For a remote home, its saved route supplies the execution host even if its
1769
- registration has subsequently changed. The remote kernel validates the new
1770
- configuration before stopping the current harness. A missing feature fails
1771
- before any stop/start command is sent.
1674
+ The result is `{path, bytes, sha256, name, home, source}` (`path` is the
1675
+ stored file); a remote upload adds `server`, `instance` and `stderr?`. Errors:
1676
+ `E_BAD_ARGS`, `E_UPLOAD_TOO_LARGE`, `E_UPLOAD_FAILED` (including a remote
1677
+ sha256 mismatch; the remote file is left), `E_SESSION_UNKNOWN`,
1678
+ `E_REMOTE_INCOMPATIBLE`.
1772
1679
 
1773
1680
  ### Launch configurations
1774
1681
 
1775
1682
  ```text
1776
- oats launch-config list [--dir /scope | --home /home | --soul name --agents-root /scope/agents] --json
1777
- oats launch-config set name --file /private/definition.json [--keep-env] --dir /scope --json
1778
- oats launch-config remove name --dir /scope --json
1779
- oats launch-config preview (--home /home | --soul name --agents-root /scope/agents --dir /scope) \
1780
- [--launch-config name] [--harness harness] [--model id] [--yolo|--no-yolo] --json
1683
+ oats launch-config list [--dir <d> | --home <abs> | --soul <name> [--dir <d>] [--agents-root <abs>]] --json
1684
+ oats launch-config set <name> --file <definition.json | -> [--keep-env] [--dir <d>] --json
1685
+ oats launch-config remove <name> [--dir <d>] --json
1686
+ oats launch-config preview (--home <abs> | --soul <name> [--dir <d>]) [--launch-config <name>|none] [--harness <h>] [--model <id>] [--yolo|--no-yolo] --json
1781
1687
  ```
1782
1688
 
1783
- All accept `--server id`. Scope edits follow the registration; inspection and
1784
- preview of an existing home follow its saved route. A local definition file
1785
- is serialized to SSH stdin and read on the host with `--file -`; the local
1786
- filename is never passed to the server as though it existed there.
1787
-
1788
- The list result supplies `context`, `selected` and `configurations`. Each
1789
- configuration has a name, harness, executable, literal argument array,
1790
- environment, model, permission choice and declaring `source`. Environment
1791
- literals appear as `{ "redacted": true }`; references appear as
1792
- `{ "fromEnv": "VARIABLE_NAME" }`. Optional executable/model/yolo fields can be
1793
- null. An editor must not write redaction markers back. `--keep-env`, with
1794
- `env` omitted from the replacement definition, copies the effective named
1795
- configuration's environment once into the complete replacement.
1796
-
1797
- Preview is read-only and returns a redacted invocation plus `preflight`
1798
- checks. A successful inspection envelope can contain `result.ok: false`:
1799
- the selected launch is not ready. Desktop displays the failed checks rather
1800
- than treating successful inspection as permission to launch. Environment
1801
- references resolve on the execution host at launch, including subsequent
1802
- starts of the saved recipe. Editing a named definition does not change a
1803
- running instance or silently update its frozen launch recipe. Select the
1804
- configuration explicitly on a later start/restart to apply the new definition.
1805
-
1806
- See [launch configuration syntax](configuration.md) and
1807
- [the Desktop start/restart workflow](desktop-instance-start.md).
1808
-
1809
- ### `oats spawn <agent> … --json`
1810
-
1811
- `result` fields (always present):
1812
-
1813
- | field | type | meaning |
1814
- | ---------- | --------------- | ------------------------------------------ |
1815
- | `instance` | string | new instance name |
1816
- | `agent` | string | soul/agent name |
1817
- | `home` | string | absolute instance home path |
1818
- | `work` | string | work mode (worktree/checkout/attached/workspace/directory) |
1819
- | `branch` | string \| null | work branch when applicable |
1820
- | `launched` | boolean | whether a tmux window was started |
1821
- | `warnings` | string[] | non-fatal warnings (always an array) |
1822
- | `tmux` | {session,window} \| null | tmux target |
1823
-
1824
- Additional informative fields: `repo`, `harness`, `model`, `parent`,
1825
- `sibling` (explicit sibling cluster link when a root-level sibling relation
1826
- was declared, else null), `relation` (`child`/`sibling`/`parent` when a
1827
- relation was declared at spawn, else null), `spawnOrigin`, `attach`.
1828
-
1829
- Stable error codes: `E_USAGE`, `E_LOCAL_MISSING` (no `oats-local.yaml` in reach:
1830
- a spawn needs a workspace deployment), `E_NO_DEPLOYMENT` (the deployment's
1831
- `agents/` root is missing), `E_SOUL_UNKNOWN`, `E_UNKNOWN_AGENT`,
1832
- `E_AMBIGUOUS_SOUL`, `E_PARENT_NOT_FOUND`, `E_RELATIVE_NOT_FOUND`,
1833
- `E_RELATIVE_AMBIGUOUS` (a `--relative-to`/`--parent` anchor name matches
1834
- multiple team instances — disambiguate with `--relative-root <agents-root>`
1835
- — or the chosen anchor is shadowed by a same-named instance so the lineage
1836
- edge would resolve wrongly), `E_BAD_ARGS`,
1837
- `E_INSTANCE_NAME_INVALID`, `E_INSTANCE_NAME_TAKEN`, `E_SPAWN_FAILED`.
1838
-
1839
- **Instance names** (0.26.0, feature `spawn-name`). By default the name is
1840
- derived: `<agent>-<purpose>` with `--purpose <slug>`, else `<agent>-<n>`.
1841
- `--name <slug>` (human decision 2026-09-24) is the explicit opt-in: the
1842
- instance name is **exactly** `<slug>`, with no `<agent>-` prefix.
1843
-
1844
- - `--name` and `--purpose` are mutually exclusive (`E_BAD_ARGS`), and
1845
- `--name` needs a value (`E_BAD_ARGS`).
1846
- - The name is never rewritten. Input that is not already a slug (lowercase
1847
- letters and digits, single dashes between them) is
1848
- `E_INSTANCE_NAME_INVALID`, and so is a name equal to any soul name of the
1849
- deployment (souls on the agents root, and every soul the workspace
1850
- declares, fetched or not). Soul and instance references stay unambiguous.
1851
- - **Instance names are at most 64 characters** (0.26.0; the tightest
1852
- consumer is the messaging alias, which allows 1–64). This covers every
1853
- name, explicit and derived. A longer name is `E_INSTANCE_NAME_INVALID`
1854
- ("instance names are at most 64 characters"), in preview and apply alike,
1855
- and is never truncated. For a derived name the refusal names the purpose
1856
- to shorten, and the de-duplication suffix counts: when `<agent>-<purpose>`
1857
- is taken and `<agent>-<purpose>-2` would exceed 64, the spawn is refused.
1858
- - **Names are unique across the deployment.** An explicit name that any
1859
- `<agents-root>/<soul>/instances/` already holds (including homes whose soul
1860
- was since removed), or that a live window in the target tmux session carries
1861
- (tmux backend, launched or `--no-launch`), is `E_INSTANCE_NAME_TAKEN`
1862
- (`details.instance`, `details.home` or `details.session`). There is never a
1863
- silent `-2` for a name the operator typed. These checks run after
1864
- idempotency-key recovery (a keyed retry replays its receipt), and a
1865
- concurrent spawn of another soul under the same name is caught after
1866
- placement (see *Exclusive placement*). The invariant covers spawns through
1867
- the CLI. Homes from earlier kernels may already share a name.
1868
- - Derived names de-duplicate deployment-wide too (`-2`, `-3`, …), against
1869
- every soul's instances and every soul name. Two souls never derive the same
1870
- name (soul `a` with `--purpose b-c` against soul `a-b` with `--purpose c`).
1871
- - `--preview` reports the final name (`instance`, `decision.instance`) and
1872
- refuses with the same codes. The name is part of the decision revision, so
1873
- `--expect-decision` binds it: another name under a confirmed decision is
1874
- `E_DECISION_STALE`.
1875
-
1876
- Dispatch-level failures (any `--json` command): `E_UNKNOWN_COMMAND` (no
1877
- kernel subcommand or capability namespace matches, or unknown capability
1878
- subcommand), `E_CAPABILITY_INACTIVE`, `E_CAPABILITY_BLOCKED` (untrusted),
1879
- `E_CAPABILITY_BROKEN`, `E_DUPLICATE_NAMESPACE`, `E_CONFIG_BROKEN` — all still
1880
- exactly one stdout envelope with a nonzero exit.
1881
-
1882
- ### Knowledge operations and OKF v2
1883
-
1884
- Discover provider-declared operations rather than assuming a particular memory
1885
- format. The knowledge capability's version owns its result shape; CLI API v1
1886
- does not freeze the old OKF v1 `harvest: spawned|skipped` body for every provider.
1887
- See [knowledge](knowledge.md) for the prepared OKF 2.0.0 version scope.
1888
-
1889
- ```bash
1890
- oats operation run knowledge:inspect --home /absolute/source-home --json
1891
- oats operation run knowledge:harvest --home /absolute/source-home --json
1892
- ```
1689
+ Feature `launch-config`. Configurations are a host choice in
1690
+ `oats-local.yaml` `launch-configs:` ([syntax](configuration.md)). All four
1691
+ accept `--server <id>`.
1893
1692
 
1894
- The operation runner preserves the provider view/action through the ordinary
1895
- operations contract. Direct `oats okf inspect` returns the standard JSON-v1
1896
- success/error envelope. Its result includes:
1897
-
1898
- - `summary`, durable `source`, frozen `owns`, `reads`, `bases`;
1899
- - `acceptedView` (the registered snapshot, not a fresh read), `status` with
1900
- capture/processing/delivery/acceptance receipts, and `scheduler` diagnostics;
1901
- - `liveMemory: {available, reason, observedAt}` and labeled `documents`.
1902
-
1903
- Live Markdown documents are `Working state (STATE.md)`, `Log (log.md)` and
1904
- sorted `Pending note: <relative-name>`, including nested notes. Missing files
1905
- are omitted; durable receipts follow as a text document. Only a live source
1906
- whose pointer/metadata still matches may supply live memory. Retired, missing,
1907
- reused or unverified homes return durable documents and explicit unavailability.
1908
- Unsafe live documents fail instead of returning a partial success. Inspection is
1909
- read-only and does not capture, refresh, schedule or launch a model.
1910
-
1911
- The explicit preview limit is **256 KiB per document**, with `truncated: true`
1912
- and original `bytes` for larger files. Smaller files are byte-exact. The complete
1913
- JSON envelope drains stdout; consumers must not clip it at a small output-buffer
1914
- limit. Provider `read` returns full Markdown, not this inspection preview.
1915
-
1916
- After the home disappears, operate from durable deployment context:
1917
-
1918
- ```bash
1919
- oats okf inspect --source /absolute/state/sources/UUID/source.json --soul domain-expert --json
1920
- oats okf refresh --source /absolute/state/sources/UUID/source.json --soul domain-expert --json
1693
+ ```json
1694
+ {"context":"/w","level":"/w","file":"/w/oats-local.yaml","selected":null,
1695
+ "configurations":[{"name":"reviewers","harness":"claude","executable":null,"args":["--permission-mode","plan"],
1696
+ "env":{"ANTHROPIC_API_KEY":{"fromEnv":"REVIEW_KEY"},"REVIEW_MODE":{"redacted":true}},
1697
+ "model":"opus","yolo":null,"source":"/w/oats-local.yaml","shadows":[]}]}
1921
1698
  ```
1922
1699
 
1923
- Every descriptor-selected read/refresh creates its new view under that source's
1924
- state directory, not the invoking repository or a replacement home.
1700
+ - **list**: `selected` is `null`, `{home, instance}` or `{soul, agentsRoot}`;
1701
+ `level` and `file` are `null` without an `oats-local.yaml` (the set is then
1702
+ empty). An environment literal is `{redacted: true}`, a reference
1703
+ `{fromEnv}`; values never leave the file.
1704
+ - **set**/**remove**: `{name, action, level, file, before, after,
1705
+ effective}`. `set --file` takes `{harness, executable?, args?, env?, model?,
1706
+ yolo?}` (`-` reads stdin). `--keep-env` keeps the declared environment when
1707
+ `env` is omitted. Errors: `E_LOCAL_MISSING`, `E_BAD_ARGS` (including
1708
+ `--home`/`--soul`), `E_LAUNCH_CONFIG_UNKNOWN`, `E_LAUNCH_CONFIG_INVALID`,
1709
+ `E_CONFIG_BROKEN`, `E_HOME_UNKNOWN`.
1710
+ - **preview** (read-only) answers `{context, selected, selection: {source,
1711
+ launchConfig, harness, model, yolo}, harness, model, modelSource, yolo,
1712
+ launchConfig, launchConfigSource, executable: {path, declared,
1713
+ resolvedFrom}, argv, environment: [{name, fromEnv} | {name, redacted:
1714
+ true} | {name, reference: true}], command (redacted), prompt, hooks,
1715
+ preflight: [{check, ok, detail}], ok}`.
1716
+
1717
+ A successful envelope can carry `ok: false`: show the failed `preflight`
1718
+ checks. The prompt is named, never the task body. A home predating launch
1719
+ recipes answers its frozen command with `selection.source: "frozen-command"`
1720
+ and `hooks: null`; a selection on it is `E_LAUNCH_LEGACY`. Editing a
1721
+ definition never changes a running instance's recipe.
1722
+
1723
+ <a id="schedules-and-triggers"></a>
1724
+ ## Schedules and triggers
1725
+
1726
+ Schedules (feature `schedule`, `scheduleApi: 2`; history feature
1727
+ `schedule-read-2`, `scheduleHistoryApi: 3`), triggers (feature `triggers`,
1728
+ `triggerApi: 1`) and workspace automations (feature `automations`,
1729
+ `automationsApi: 1`). Model: [schedules.md](schedules.md#triggers).
1730
+
1731
+ The lists stay separate: a trigger never appears in `schedule list`, nor a
1732
+ schedule in `trigger list`. Each answers this machine's local items and every
1733
+ workspace item of a readable member. Render the rows; never re-derive them.
1734
+
1735
+ ### `oats schedule`
1925
1736
 
1926
- Direct `oats okf harvest --json` captures notes and record and requests an
1927
- independent directory worker. Representative result shapes (not exhaustive):
1737
+ ```text
1738
+ oats schedule list [--dir <d>] --json
1739
+ oats schedule show <id> --json
1740
+ ```
1928
1741
 
1929
1742
  ```json
1930
- {"status":"running","run":"<run-id>","instance":"<worker-instance>","home":"/absolute/worker-home"}
1743
+ {"scope":"/w","scheduleApi":2,"scheduleHistoryApi":3,
1744
+ "integrity":{"sources":[{"path":"definitions","status":"ok","bytes":1206},{"path":"state","status":"ok","bytes":4410}]},
1745
+ "host":{"name":"ana-laptop","ghUser":{"github.com":"ana"}},"triggers":{"count":4,"command":"oats trigger list"},
1746
+ "snapshot":{"takenAt":"2026-09-26T19:58:09.281Z","problems":1},
1747
+ "schedules":[{"id":"nightly","kind":"spawn","cron":"0 7 * * *","tz":"Europe/Madrid","agent":"rm","task":"Check the release branch.","purpose":"nightly",
1748
+ "enabled":true,"createdAt":"2026-09-26T19:58:09.380Z","updatedAt":"2026-09-26T19:58:09.380Z","scope":"/w","scheduleApi":2,"scheduleHistoryApi":3,
1749
+ "executionStatus":{"kind":"legacy","capture":"unknown","migrationRequired":true},
1750
+ "nextRun":"2026-09-29T05:00:00.000Z","nextDue":"2026-09-29T05:00:00.000Z","lastRun":null,
1751
+ "history":{"status":"ok","stored":1,"truncated":false},
1752
+ "recentRuns":[{"scheduledFor":"2026-09-28T05:00:00.000Z","startedAt":"2026-09-28T05:00:01.000Z","kind":"spawn","outcome":"ended",
1753
+ "runId":"3f9a0b1c2d3e4f5a6b7c8d9e","legacy":false,"settled":true,"recordedAt":"2026-09-28T05:40:00.000Z","transitions":["started","ended"],
1754
+ "session":{"instance":"rm-nightly","home":"/w/agents/rm/instances/rm-nightly","incarnation":"2026-09-28T05:00:01.000Z","server":null,"delivery":"launched"}}],
1755
+ "running":false,"name":"nightly","qualifiedId":"local/nightly",
1756
+ "origin":{"kind":"local","path":"oats-schedules.json","url":null,"localPath":"/w/oats-schedules.json"},
1757
+ "description":null,"owner":null,"runsOn":null,"runsHere":true,"reason":null,"enabledHere":true,
1758
+ "soul":{"name":"rm","origin":{"kind":"member","repoKey":"github.com/nw/agents","member":"agents"}},
1759
+ "teams":[],"launchConfig":null,"harness":null,"model":null,"concurrency":null}],
1760
+ "scheduler":{"installed":true,"active":true,"registered":true,"lastTick":"2026-09-28T11:59:00.000Z","maxConcurrent":2}}
1931
1761
  ```
1932
1762
 
1933
- ```json
1934
- {"status":"empty","processed":true}
1763
+ - `list`: `{scope, scheduleApi, scheduleHistoryApi, integrity, host,
1764
+ triggers, snapshot, schedules, scheduler}`; `triggers` counts the trigger
1765
+ definitions left out. `show <id>`: `{schedule}`, without `integrity`.
1766
+ - **A readable row**: the definition (`id, kind, cron, tz, enabled, …`, and
1767
+ `agent/task/purpose/harness` for a spawn, `argv/cwd` for a command, the
1768
+ message for a wake, the operation for an operation) plus `scope,
1769
+ scheduleApi, scheduleHistoryApi, executionStatus, nextRun, lastRun, history,
1770
+ recentRuns, running, attempt?, pendingWake?` and the
1771
+ [shared row fields](#automations-shared-rows).
1772
+ `executionStatus` is `{kind: "legacy" | "invalid", capture: "unknown",
1773
+ migrationRequired: true, reason?, intent?}`; only `legacy` runs.
1774
+ - **An unreadable row** (`list` only): `{id, scope, scheduleApi,
1775
+ scheduleHistoryApi, unreadable: {code, message}, history: {status:
1776
+ "corrupt", stored: null, truncated: false}, recentRuns: []}`. One bad job
1777
+ never fails the others.
1778
+ - `history`: `{status: "ok", stored, truncated}` or the corrupt form; capped
1779
+ at 50 rows.
1780
+ - **A run** (`recentRuns[]`, `lastRun`): the producer's fields
1781
+ (`scheduledFor, startedAt, kind, outcome, …`) plus `runId, legacy: false,
1782
+ settled, recordedAt, transitions, session`.
1783
+ - `runId` = `sha256(scheduledFor|startedAt|attemptId)[0:24]`, opaque: never
1784
+ recompute or dedupe by it. `lastRun` shares its history row's `runId`.
1785
+ - `transitions` is the outcome sequence; `settled` a boolean. `outcome` is
1786
+ the producer's word (`ended`, `stopped`, `blocked`, `invalid`,
1787
+ `delivered`, `skipped`, `unknown`, …).
1788
+ - A pre-API-3 row: `runId: null, legacy: true, settled: null, transitions:
1789
+ null`, `session`, no `recordedAt`. A corrupt element: `{runId: null,
1790
+ legacy: true, corrupt: true}`.
1791
+ - `session` is `{instance, home, incarnation, server, delivery: "launched"
1792
+ | "delivered-active" | "none"}`: recorded provenance, not a transcript
1793
+ reader.
1794
+ - `nextDue` is "when it next runs" in every row (`null` when another host
1795
+ runs it); a schedule row also keeps `nextRun`.
1796
+ - **Integrity.** `oats-schedules.json` and `.agents/schedules/state.json` are
1797
+ opened as regular files only, at most 1 MiB. `integrity.sources[]` is
1798
+ `{path: "definitions" | "state", status: "ok" | "absent" | "refused" |
1799
+ "oversize" | "corrupt", bytes}`.
1800
+ - Refusals: `E_SCHEDULE_STATE_OVERSIZE` and `E_SCHEDULE_INVALID` (`details:
1801
+ {source, field?}`), `E_SCHEDULE_IDENTITY` (`details: {key, declared}`), and
1802
+ `E_BAD_ARGS` for an id not matching `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`.
1803
+ `list` refuses as a whole only when a scope file is unreadable.
1804
+
1805
+ **Other verbs** (envelopes):
1806
+ - `add <id> (--file <spec.json> | --spec-json <json>)`, `update`, `enable`,
1807
+ `disable` → `{schedule}`. `run [--force]`, `remove [--force]`, `reconcile
1808
+ [--clear]` → the action's receipt. `tick [--dry-run] [--host]` →
1809
+ `{tickedAt, considered, scheduler}`; `host install | uninstall | status` →
1810
+ `{scheduler}`.
1811
+ - `test <id>` → `{test: {id, qualifiedId, kind, placement: {runsHere, reason,
1812
+ reasonDetail?, enabledHere, runsOn, owner, host}, soul: {name, origin,
1813
+ resolves, error} | null, nextDue, spawned: false, problems, ok}}`. The soul
1814
+ is checked as the run would start it; not running here, disabled, invalid
1815
+ or unresolved is a problem. It runs nothing.
1816
+ - Errors: `E_SCHEDULE_UNKNOWN`, `E_SCHEDULE_EXISTS`, `E_SCHEDULE_INVALID`,
1817
+ `E_SCHEDULE_RUNNING`, `E_SCHEDULE_DISABLED`, `E_SCHEDULE_UNRESOLVED`,
1818
+ `E_SCHEDULE_FAILED`, `E_LOCAL_MISSING`, `E_BAD_ARGS`.
1819
+
1820
+ ### `oats trigger`
1821
+
1822
+ ```text
1823
+ oats trigger list | show <id> | status [<id>] | test <id> | add (--file <json> | --from <package>:<template> [--set k=v]) | enable <id> | disable <id> | remove <id> --json
1935
1824
  ```
1936
1825
 
1937
- An explicit `--no-launch` request can return `status: "ready"` without starting
1938
- a model; existing runs report their current status without starting duplicates.
1939
- Nonzero errors use the ordinary JSON-v1 error envelope. `running`/`ready` are not
1940
- successful knowledge delivery. Inspect and reconcile provider receipts; never
1941
- infer acceptance from a launch or from a worker disappearing.
1826
+ Event-driven spawns. Local definitions live in `oats-schedules.json` (`kind:
1827
+ "trigger"`).
1828
+
1829
+ - `list`: `{triggerApi, scope, host, snapshot, triggers, scheduler}`.
1830
+ `show`, `add`, `enable`, `disable` → `{trigger}`; `remove` → `{removed,
1831
+ live: [instance]}`. A stored definition that no longer validates carries
1832
+ `invalid: {code, message, field?}`.
1833
+ - **A trigger row**: the [shared row fields](#automations-shared-rows) plus
1834
+ `kind: "trigger", on, spawn, template?, enabled, triggerApi, scope,
1835
+ createdAt, updatedAt`.
1836
+ - `id` is always qualified (`local/<id>` or `<member>/<id>`); verbs accept
1837
+ `local/<id>` or a bare local id.
1838
+ - `on`: `{source: "github.pull_request", repo, events: [opened | reopened |
1839
+ ready_for_review | labeled | synchronize], labels, base?, poll}`.
1840
+ - `spawn`: `{soul, purpose, task, teams, launchConfig?, harness?, model?,
1841
+ yolo?, backend?}`. `purpose` and `task` template over `{repo}`,
1842
+ `{number}`, `{url}`, `{event}`, `{trigger}`, `{headSha}`. `teams` are
1843
+ distinct labels passed as `--provider <messaging cap> join=<labels>`; a
1844
+ soul without messaging refuses `E_TRIGGER_TEAMS {soul, teams}`.
1845
+ - `template?`: `{package, version, commit, template}`.
1846
+ - `lastRun`: `{at, instance, home, event, number, key}`; `nextDue`: the
1847
+ next poll here, `null` before the first.
1848
+ - `status [<id>]` → `{triggerApi, scope, triggers: [{id, name, enabled,
1849
+ runsHere, reason, enabledHere, repo, soul, concurrency: {max, perKey},
1850
+ liveCount, live: [{instance, home, repo, number, event}], lastPoll: {at, ok:
1851
+ true, prs, matching} | {at, ok: false, error} | null, nextPollAt, nextDue,
1852
+ pending: [{key, event, number, url, observedAt}], fired: [{key, at,
1853
+ instance, home, event, number}] (newest 50), firedTotal, lastError: {at,
1854
+ code, message, key?} | null}]}`. It writes nothing.
1855
+ - `test <id>` → `{triggerApi, id, ok, placement: {runsOn, owner, host,
1856
+ runsHere, reason, detail?, enabledHere}, gh: {ok, account,
1857
+ credentialSource, reachesHostTimer, note, detail}, repo: {key, readable,
1858
+ fullName, permissions: {push, maintain, admin}, canMerge} | {key, readable:
1859
+ false, error}, soul: {resolves, name, agent, messaging} | {resolves: false,
1860
+ name, error}, teams: {requested, undeclared | null, messaging}, wouldFire:
1861
+ [{key, repo, number, event, url, held?}], pollError?, problems, warnings,
1862
+ spawned: false}`. `ok` counts `problems` only; a credential the host timer
1863
+ cannot reach is a warning.
1864
+ - A triggered instance records `instance.json.trigger`; its event file is
1865
+ `OATS_TRIGGER_EVENT_FILE`.
1866
+ - Errors: `E_TRIGGER_INVALID {field}`, `E_TRIGGER_EXISTS`,
1867
+ `E_TRIGGER_UNKNOWN`, `E_TRIGGER_TEAMS`, `E_TRIGGER_POLL`,
1868
+ `E_TRIGGER_FAILED`, `E_BAD_ARGS`, `E_PACKAGE_MISSING`,
1869
+ `E_PACKAGE_MANIFEST`, `E_LOCAL_MISSING`.
1870
+
1871
+ <a id="automations-shared-rows"></a>
1872
+ ### Shared row fields and workspace automations
1873
+
1874
+ Details: [schedules.md](schedules.md#workspace-triggers-and-schedules).
1875
+
1876
+ **Both lists** carry `host: {name | null, ghUser: {<gh host>: <login> |
1877
+ null}}` (this machine's `host.name` and its `gh` logins, compared with a
1878
+ row's `owner`), `snapshot: {takenAt, problems (a count)} | null` (the last
1879
+ `oats sync` snapshot), and `scheduler: {installed, active, registered,
1880
+ lastTick, maxConcurrent, …}` (nothing runs unless installed, active and
1881
+ registered).
1882
+
1883
+ **Every row** carries:
1884
+ - `id` (a local schedule keeps its bare id; a workspace item is
1885
+ `<member>/<id>`), `qualifiedId` (always qualified), `name` (the bare id).
1886
+ - `origin`: `{kind: "local", path: "oats-schedules.json", url: null,
1887
+ localPath}` or `{kind: "workspace", repoKey, path, commit, url,
1888
+ localPath}` (`url` for `github.com` only; `localPath` `null` when the
1889
+ member is not cloned here).
1890
+ - `description`, `owner`, `runsOn`, `runsHere`, `reason` (`null` |
1891
+ `host-unnamed` | `assigned-elsewhere` | `owner-mismatch` | `untrusted`),
1892
+ `reasonDetail`, `enabledHere`.
1893
+ - `untrusted` (0.30, [automations.trust](configuration.md#who-runs-workspace-automations)):
1894
+ placed on this host (`runsOn` and `owner` match) but `oats-local.yaml`
1895
+ `automations.trust` does not admit it, so it never runs here. Its
1896
+ `reasonDetail` names the line to add. Group it as needing attention, like
1897
+ `owner-mismatch`. A kernel before 0.30 never sends it; treat an unknown
1898
+ reason as "does not run here".
1899
+ - `soul: {name, origin} | null` (`null` for command, wake and operation
1900
+ schedules). `origin` is `{kind: "member", repoKey, member}`, `{kind:
1901
+ "package", package, version}`, `{kind: "external", repoKey, source}`,
1902
+ `{kind: "ambiguous", candidates (a count)}`, or `null`.
1903
+ - `task`, `teams`, `launchConfig`, `harness`, `model`, `concurrency`,
1904
+ `lastRun`, `nextDue`, `invalid?`. A schedule row's `kind` is its run
1905
+ (`spawn | command | wake | operation`), its `teams` is `[]` and
1906
+ `concurrency` `null`. A workspace item another host runs carries its
1907
+ definition and placement only.
1908
+
1909
+ **Workspace items:**
1910
+ - `enable`/`disable` edit `oats-local.yaml` `triggers.disabled` or
1911
+ `schedules.disabled`. `update` and `remove` refuse `E_AUTOMATION_WORKSPACE
1912
+ {id, origin}`. `schedule run`/`reconcile` elsewhere refuse
1913
+ `E_AUTOMATION_NOT_HERE {id, reason, runsOn, owner}`.
1914
+ - `oats trigger|schedule add … --workspace <member> --runs-on <host> --owner
1915
+ <host>/<login> --json` writes the file in the member clone and answers
1916
+ `{id, written, file: {member, repoKey, path, content, written?}}`. Errors:
1917
+ `E_AUTOMATION_MEMBER`, `E_TRIGGER_EXISTS`/`E_SCHEDULE_EXISTS`, and the
1918
+ kind's validation codes.
1919
+ - `oats automations refresh --json` → `{automationsApi, snapshot (the file's
1920
+ path), triggers, schedules (counts), problems, takenAt}`.
1921
+ - The tick's `considered[]` holds schedule rows and trigger rows `{workspace,
1922
+ trigger, action, …}` with actions `not-due`, `poll-failed`, `polled`
1923
+ (`prs`, `matching`), `held`, `fired` (`key`, `instance`, `home`),
1924
+ `spawn-failed` (`key`, `code`, `error`), `would-fire`, `invalid` and
1925
+ `not-here`. A workspace schedule's state key is `<member>~<id>`. A failed
1926
+ snapshot refresh is `{workspace, action: "error", error}`.
1927
+
1928
+ <a id="the-harness-rename-feature-harness-oats-0270"></a>
1929
+ ## Harness input spellings
1930
+
1931
+ What starts an instance (pi, claude or codex) is its **harness** (feature
1932
+ `harness`), and every output uses that name. These inputs still accept the
1933
+ older `runtime` spelling: it is read as `harness`, and the next write records
1934
+ `harness`. A pair that disagrees is refused.
1935
+
1936
+ | Input | Old spelling | Warning | Both, disagreeing |
1937
+ |---|---|---|---|
1938
+ | `spawn` (and `--preview`), `session start`/`restart`, `launch-config preview`, and their `--server` forms | `--runtime <h>` | yes | `E_BAD_ARGS` |
1939
+ | `oats-local.yaml` `launch-configs.<name>` | `runtime:` | yes | `E_WORKSPACE_SCHEMA` (`reason: "harness-conflict"`) |
1940
+ | `launch-config set --file` | `runtime` | yes; written as `harness` | `E_LAUNCH_CONFIG_INVALID` |
1941
+ | a home's `instance.json` and launch recipe `version: 1` | `runtime` | yes, naming the home; the next start records `harness` | |
1942
+ | schedule definitions (`add`/`update`, stored jobs) | `runtime` | yes | `E_SCHEDULE_INVALID` |
1943
+ | schedule run records | `startedRuntime` | no | |
1944
+ | a flat soul.yaml (a capability-defined agent) | `runtime:` | no | `E_BAD_MANIFEST` |
1945
+ | a manifest `requires[]` harness-package row | `runtime` | no | a spawn problem |
1946
+
1947
+ One warning per command, however many old spellings it read:
1948
+
1949
+ ```json
1950
+ {"code":"deprecated-runtime-name","key":"runtime","replacement":"harness","sources":["the --runtime flag (use --harness)"],"message":"`runtime` was renamed to `harness` in 0.27.0; the old name is still read here (the --runtime flag (use --harness)) and a later release drops it"}
1951
+ ```
1942
1952
 
1943
- Kernel envelope/dispatch tests live in `test/cli-json-contract.test.mjs`;
1944
- provider-specific behavior is qualified against the exported OKF runtime.
1953
+ The `--server` routes translate for a host without the `harness` feature:
1954
+ they send `--runtime` and `runtime` keys and read its `runtimes` list.