@awebai/oats 0.29.4 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +12 -6
  2. package/bin/oats.mjs +194 -50
  3. package/docs/capabilities.md +160 -171
  4. package/docs/capability-manifest.schema.json +6 -11
  5. package/docs/configuration.md +213 -64
  6. package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
  7. package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
  8. package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
  9. package/docs/design/2026-09-27-team-model-v2.md +97 -117
  10. package/docs/design/2026-09-28-automations-trust.md +38 -0
  11. package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
  12. package/docs/design/HISTORY.md +65 -0
  13. package/docs/design/README.md +23 -54
  14. package/docs/desktop-cli-api.md +1787 -1777
  15. package/docs/desktop.md +30 -91
  16. package/docs/execution-targets.md +146 -292
  17. package/docs/first-team.md +31 -17
  18. package/docs/implementation.md +77 -288
  19. package/docs/integrations.md +118 -320
  20. package/docs/knowledge-capability-authoring.md +25 -52
  21. package/docs/knowledge-reference/acceptance.md +3 -3
  22. package/docs/knowledge-reference/adoption.md +1 -1
  23. package/docs/knowledge-reference/harvester.md +2 -2
  24. package/docs/knowledge-reference/package-craft.md +3 -3
  25. package/docs/knowledge-reference/provider-mapping.md +3 -6
  26. package/docs/knowledge-reference/reader-capture.md +3 -3
  27. package/docs/knowledge-theory.md +62 -166
  28. package/docs/knowledge.md +225 -404
  29. package/docs/layers.md +42 -97
  30. package/docs/oats-local.schema.json +58 -5
  31. package/docs/oats-membership.schema.json +1 -8
  32. package/docs/oats-package.schema.json +5 -5
  33. package/docs/oats-workspace.schema.json +8 -22
  34. package/docs/official-catalog.md +25 -28
  35. package/docs/packages.md +45 -63
  36. package/docs/plans/0.30-close-out.md +83 -0
  37. package/docs/release-lane.md +82 -0
  38. package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
  39. package/docs/release-notes/v0.19.0.md +48 -147
  40. package/docs/release-notes/v0.19.1.md +2 -3
  41. package/docs/release-notes/v0.19.3.md +2 -15
  42. package/docs/release-notes/v0.20.0.md +0 -15
  43. package/docs/release-notes/v0.22.0.md +71 -138
  44. package/docs/release-notes/v0.22.1.md +42 -90
  45. package/docs/release-notes/v0.22.10.md +1 -1
  46. package/docs/release-notes/v0.22.11.md +1 -47
  47. package/docs/release-notes/v0.22.12.md +4 -13
  48. package/docs/release-notes/v0.22.13.md +1 -42
  49. package/docs/release-notes/v0.22.14.md +3 -11
  50. package/docs/release-notes/v0.22.15.md +1 -46
  51. package/docs/release-notes/v0.22.16.md +6 -8
  52. package/docs/release-notes/v0.22.18.md +1 -99
  53. package/docs/release-notes/v0.22.19.md +3 -14
  54. package/docs/release-notes/v0.22.2.md +6 -15
  55. package/docs/release-notes/v0.22.3.md +0 -1
  56. package/docs/release-notes/v0.22.4.md +1 -14
  57. package/docs/release-notes/v0.22.5.md +2 -12
  58. package/docs/release-notes/v0.22.6.md +0 -3
  59. package/docs/release-notes/v0.23.0.md +9 -25
  60. package/docs/release-notes/v0.23.1.md +9 -25
  61. package/docs/release-notes/v0.23.2.md +2 -4
  62. package/docs/release-notes/v0.24.0.md +56 -97
  63. package/docs/release-notes/v0.24.1.md +7 -11
  64. package/docs/release-notes/v0.24.10.md +34 -45
  65. package/docs/release-notes/v0.24.11.md +12 -20
  66. package/docs/release-notes/v0.24.12.md +35 -48
  67. package/docs/release-notes/v0.24.13.md +34 -41
  68. package/docs/release-notes/v0.24.2.md +9 -13
  69. package/docs/release-notes/v0.24.3.md +7 -11
  70. package/docs/release-notes/v0.24.4.md +6 -6
  71. package/docs/release-notes/v0.24.5.md +6 -10
  72. package/docs/release-notes/v0.24.6.md +2 -5
  73. package/docs/release-notes/v0.24.7.md +46 -75
  74. package/docs/release-notes/v0.24.8.md +58 -96
  75. package/docs/release-notes/v0.24.9.md +38 -54
  76. package/docs/release-notes/v0.25.0.md +59 -76
  77. package/docs/release-notes/v0.25.1.md +57 -81
  78. package/docs/release-notes/v0.25.2.md +51 -70
  79. package/docs/release-notes/v0.25.3.md +11 -13
  80. package/docs/release-notes/v0.25.4.md +9 -13
  81. package/docs/release-notes/v0.25.5.md +3 -5
  82. package/docs/release-notes/v0.25.6.md +20 -29
  83. package/docs/release-notes/v0.25.7.md +5 -7
  84. package/docs/release-notes/v0.25.8.md +26 -39
  85. package/docs/release-notes/v0.26.0.md +175 -646
  86. package/docs/release-notes/v0.27.0.md +4 -5
  87. package/docs/release-notes/v0.27.1.md +4 -6
  88. package/docs/release-notes/v0.27.2.md +1 -1
  89. package/docs/release-notes/v0.28.0.md +57 -124
  90. package/docs/release-notes/v0.29.0.md +89 -208
  91. package/docs/release-notes/v0.29.1.md +1 -1
  92. package/docs/release-notes/v0.29.2.md +3 -4
  93. package/docs/release-notes/v0.30.0.md +205 -0
  94. package/docs/release-notes/v0.30.1.md +123 -0
  95. package/docs/schedules.md +280 -363
  96. package/docs/servers.md +99 -117
  97. package/docs/soul.schema.json +2 -9
  98. package/docs/souls-and-instances.md +145 -158
  99. package/docs/workspaces.md +137 -215
  100. package/lib/automations.mjs +21 -6
  101. package/lib/core.mjs +226 -74
  102. package/lib/instance-events.mjs +1 -1
  103. package/lib/instance-inspect.mjs +109 -34
  104. package/lib/instance-lifecycle.mjs +14 -1
  105. package/lib/instance-resolution.mjs +26 -27
  106. package/lib/launch-preference.mjs +87 -0
  107. package/lib/materialize.mjs +3 -3
  108. package/lib/packages.mjs +1 -1
  109. package/lib/resolve.mjs +30 -88
  110. package/lib/schedule.mjs +1 -1
  111. package/lib/teams-verbs.mjs +195 -0
  112. package/lib/teams.mjs +190 -0
  113. package/lib/triggers.mjs +2 -2
  114. package/lib/workspace.mjs +54 -147
  115. package/package-catalog.json +10 -16
  116. package/package.json +1 -3
  117. package/skills/oats-getting-started/SKILL.md +25 -13
  118. package/capabilities/oats-authoring/LICENSE +0 -21
  119. package/capabilities/oats-authoring/oats-package.json +0 -11
  120. package/capabilities/oats-authoring/oats.json +0 -12
  121. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
  122. package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
  123. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
  124. package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
  125. package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1338
  126. package/capabilities/oats-aweb/injects/aweb.md +0 -47
  127. package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -356
  128. package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
  129. package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
  130. package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
  131. package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
  132. package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
  133. package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
  134. package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
  135. package/capabilities/oats-aweb/oats.json +0 -208
  136. package/capabilities/oats-aweb/skills/LICENSE +0 -21
  137. package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
  138. package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
  139. package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
  140. package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
  141. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
  142. package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
  143. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -216
  144. package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
  145. package/capabilities/oats-jira/injects/jira.md +0 -10
  146. package/capabilities/oats-jira/oats.json +0 -22
  147. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
  148. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
  149. package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
  150. package/capabilities/oats-linear/injects/linear.md +0 -8
  151. package/capabilities/oats-linear/oats.json +0 -24
  152. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
  153. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
  154. package/capabilities/oats-okf/bin/oats-okf.mjs +0 -209
  155. package/capabilities/oats-okf/injects/okf.md +0 -42
  156. package/capabilities/oats-okf/lib/binding-wire.mjs +0 -348
  157. package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
  158. package/capabilities/oats-okf/lib/config.mjs +0 -124
  159. package/capabilities/oats-okf/lib/consult.mjs +0 -518
  160. package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
  161. package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
  162. package/capabilities/oats-okf/lib/inspection.mjs +0 -119
  163. package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
  164. package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
  165. package/capabilities/oats-okf/lib/io.mjs +0 -118
  166. package/capabilities/oats-okf/lib/migration.mjs +0 -137
  167. package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
  168. package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
  169. package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
  170. package/capabilities/oats-okf/lib/sources.mjs +0 -424
  171. package/capabilities/oats-okf/lib/stores.mjs +0 -473
  172. package/capabilities/oats-okf/lib/worker.mjs +0 -497
  173. package/capabilities/oats-okf/oats.json +0 -148
  174. package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
  175. package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
  176. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
  177. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
  178. package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
  179. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
  180. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
  181. package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
  182. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
  183. package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
  184. package/capabilities/oats-okf-harvest/oats.json +0 -26
  185. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
  186. package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
  187. package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
  188. package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  189. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
  190. package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
  191. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
  192. package/capabilities/oats-okf-maintenance/oats.json +0 -21
  193. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
  194. package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
  195. package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
  196. package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  197. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -146
  198. package/capabilities/oats-review/injects/review.md +0 -69
  199. package/capabilities/oats-review/oats.json +0 -10
  200. package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
  201. package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
  202. package/docs/conventions.md +0 -90
  203. package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
  204. package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
  205. package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
  206. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
  207. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
  208. package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
  209. package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
  210. package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
  211. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
  212. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
  213. package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
  214. package/docs/design/2026-09-15-captured-dispatch.md +0 -127
  215. package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
  216. package/docs/design/2026-09-15-package-preparation.md +0 -100
  217. package/docs/design/2026-09-15-portable-data-contract.md +0 -121
  218. package/docs/design/2026-09-15-portable-declarations.md +0 -189
  219. package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
  220. package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
  221. package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
  222. package/docs/design/2026-09-15-source-observation.md +0 -119
  223. package/docs/design/2026-09-16-captured-admission.md +0 -77
  224. package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
  225. package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
  226. package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
  227. package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
  228. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
  229. package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
  230. package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
  231. package/docs/design/2026-09-16-portable-onboarding.md +0 -179
  232. package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
  233. package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
  234. package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
  235. package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
  236. package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
  237. package/docs/design/2026-09-17-captured-native-start.md +0 -58
  238. package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
  239. package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
  240. package/docs/design/2026-09-17-public-captured-start.md +0 -108
  241. package/docs/design/2026-09-17-public-prepare-request.md +0 -90
  242. package/docs/design/2026-09-18-captured-pi-host.md +0 -205
  243. package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
  244. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
  245. package/docs/design/2026-09-20-redesign-program-board.md +0 -142
  246. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
  247. package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
  248. package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
  249. package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
  250. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
  251. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
  252. package/docs/design/2026-09-24-phase-d-plan.md +0 -305
  253. package/docs/design/2026-09-25-teams-contract.md +0 -258
  254. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
  255. package/docs/design/desktop-ux-plan.md +0 -362
  256. package/docs/design/launch-configurations.md +0 -168
  257. package/docs/design/okf-mirror-provenance.md +0 -105
  258. package/docs/design/operations-contract.md +0 -141
  259. package/docs/oats-member.schema.json +0 -38
  260. package/skills/integration-authoring/SKILL.md +0 -84
  261. package/skills/oats-support/SKILL.md +0 -79
  262. package/skills/skill-craft/SKILL.md +0 -109
  263. package/skills/soul-craft/SKILL.md +0 -116
package/README.md CHANGED
@@ -13,8 +13,6 @@ OATS—**Open Agent Team Specification**—is a framework for building, running
13
13
 
14
14
  The [official catalog](docs/official-catalog.md) is the reviewed list of official packages; a workspace trusts a package by declaring it, and listing does not declare it for you.
15
15
 
16
- **oats.aweb 1.13.1** attaches resident session-grant homes to the resident custody service during minting, verifies the written `grant.yaml` custody locator, and fails closed if an installed `aw` cannot write or prove that attachment. If a grant-sent message ever arrives as unverified, report the missing custody attachment instead of retrying.
17
-
18
16
  For example, a kernel expert, a UX expert and a customer-support expert can share capabilities for learning and coordination, while each has specific capabilities for its own area of expertise.
19
17
 
20
18
  Through your capabilities, you remain in control of:
@@ -179,7 +177,7 @@ For example, you might want:
179
177
  - **Long-running instances to retain working understanding.** A customer-support instance carries its investigations, observations and unresolved questions across many tasks.
180
178
  - **Selected learning to become shared knowledge.** Useful findings are reviewed and made available to other instances, while task-specific context stays with the instance that needs it.
181
179
 
182
- Your knowledge capability determines what context an instance receives, how it builds on experience, and which learning is retained or shared. It can organise knowledge per soul, per topic or per project, centrally or alongside soul definitions, provided that it actually supports the chosen arrangement. Mutable knowledge must not be written into immutable captured source artifacts.
180
+ Your knowledge capability determines what context an instance receives, how it builds on experience, and which learning is retained or shared. It can organise knowledge per soul, per topic or per project, centrally or alongside soul definitions, provided that it actually supports the chosen arrangement.
183
181
 
184
182
  The same principle applies to messaging and tasks: **OATS provides the contracts; you choose how your team works through them.**
185
183
 
@@ -210,7 +208,15 @@ Begin with a small team and a real piece of work:
210
208
  4. Create instances for their assignments.
211
209
  5. Verify that work, communication, learning and handoff behave as intended.
212
210
 
213
- Start with the [first-team guide](docs/first-team.md) and the [release notes](docs/release-notes/) for the supported scope of your chosen versions.
211
+ On a machine with Node.js 22+, Git and tmux, and a workspace repository to point at:
212
+
213
+ ```bash
214
+ npm install -g @awebai/oats
215
+ oats onboard ~/acme --workspace git:github.com/acme/agents # this machine's deployment
216
+ oats spawn <soul> --task "First concrete task"
217
+ ```
218
+
219
+ The [first-team guide](docs/first-team.md) walks through writing the workspace, its members and a first soul; the [release notes](docs/release-notes/) state the supported scope of each version.
214
220
 
215
221
  Further documentation:
216
222
 
@@ -220,10 +226,10 @@ Further documentation:
220
226
  - [Knowledge operations](docs/knowledge.md)
221
227
  - [Knowledge, instances and evolving expertise](docs/knowledge-theory.md)
222
228
  - [Packages](docs/packages.md)
223
- - [Execution targets](docs/execution-targets.md)
229
+ - [Sessions](docs/execution-targets.md)
224
230
  - [OATS Desktop](docs/desktop.md)
225
231
 
226
- This README explains the framework and its direction. Advanced mechanisms such as automatic speciation and context cloning require their own implementation and verification; the architectural model is not a claim that every feature or capability combination already works. Alternative knowledge layouts require a compatible capability, not a change to an undocumented kernel switch.
232
+ This README explains the framework and its direction. Advanced mechanisms such as automatic speciation and context cloning require their own implementation and verification; the model is not a claim that every feature or capability combination already works.
227
233
 
228
234
  ## Contributing and releases
229
235
 
package/bin/oats.mjs CHANGED
@@ -27,12 +27,14 @@ import {
27
27
  LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
28
28
  capabilityManifests, capabilityTrust, capabilityExecutablePath,
29
29
  officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
30
- findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
30
+ findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
31
31
  } from "../lib/core.mjs";
32
32
  import {
33
33
  writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages, memoizedRemote,
34
34
  classifyPackageValue, parsePackageRequest } from "../lib/packages.mjs";
35
35
  import { loadLocal, validateWorkspace, validateLocal, discoverPackageSouls, workspaceWarnings } from "../lib/workspace.mjs";
36
+ import { recordedTeams, reportRows, soulKeyOf, soulTeams, teamModel } from "../lib/teams.mjs";
37
+ import { launchLayers } from "../lib/launch-preference.mjs";
36
38
  import { parseConfigData } from "../lib/config-data.mjs";
37
39
  import * as remoteModule from "../lib/remote.mjs";
38
40
  import YAML from "yaml";
@@ -72,7 +74,7 @@ function expandInlineValues(argv) {
72
74
  const { argv: args, problem: argvProblem } = expandInlineValues(rawArgs);
73
75
  let cmd = args[0];
74
76
  const HELP_WORDS = new Set(["help", "--help", "-h"]);
75
- const KERNEL_COMMANDS = new Set(["automations", "trigger", "capture", "capabilities", "doctor", "inspect", "instance", "operation", "package", "readiness", "souls", "launch-config", "experimental", "onboard", "pane", "recall", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "sync", "update", "version", "workspace"]);
77
+ const KERNEL_COMMANDS = new Set(["automations", "trigger", "capture", "capabilities", "doctor", "inspect", "instance", "operation", "package", "readiness", "souls", "soul", "teams", "launch-config", "experimental", "onboard", "pane", "recall", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "sync", "update", "version", "workspace"]);
76
78
  /** Commands whose argv another parser reads (packages/record and packages/experimental parse process.argv). */
77
79
  const OWN_ARGV_COMMANDS = new Set(["capture", "recall", "setup", "experimental"]);
78
80
  /** Commands `--server <id>` runs on a registered server. */
@@ -411,18 +413,17 @@ function printWorkspaceInspect(doc) {
411
413
  for (const p of doc.problems) console.log(` ! ${p.code}: ${p.message}`);
412
414
  }
413
415
 
414
- /** `{ teams, teamsSource }` for a session start of a workspace home: its eligible teams read
415
- * live (two repository reads), which the launch hook re-checks joined memberships against
416
- * (teams contract decision 6) — or the spawn record, marked `recorded`, when the read cannot
417
- * answer. Anything that is not a readable workspace home gets nothing here — the start
416
+ /** `{ teams, defaultTeam, teamsSource }` for a session start of a workspace home: its teams read
417
+ * live (the workspace host + oats-local.yaml), which the launch hook re-checks joined memberships
418
+ * against — or the spawn record, marked `recorded`, when the read cannot answer. Anything that is not a readable workspace home gets nothing here — the start
418
419
  * itself refuses it. */
419
420
  async function homeLiveTeams(home) {
420
421
  let meta;
421
422
  try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); } catch { return {}; }
422
423
  if (!isWorkspaceHome(meta)) return {};
423
424
  const { liveTeams } = await import("../lib/instance-resolution.mjs");
424
- const { teams, source } = await liveTeams(home, meta, { remoteOptions: remoteOptionsFromEnv() });
425
- return Array.isArray(teams) ? { teams, teamsSource: source } : {};
425
+ const { teams, defaultTeam, source } = await liveTeams(home, meta, { remoteOptions: remoteOptionsFromEnv() });
426
+ return Array.isArray(teams) ? { teams, defaultTeam, teamsSource: source } : {};
426
427
  }
427
428
 
428
429
  // ---------- operation run: generic invoke through the capability engine ----------
@@ -1072,33 +1073,46 @@ const short = (oid) => (typeof oid === "string" ? oid.slice(0, 8) : "?");
1072
1073
  /** Display name of a discovery: the workspace's name, or the standalone label (decision 10). */
1073
1074
  const workspaceName = (discovery) => discovery.workspace?.name ?? `standalone:${memberLabel(discovery.key)}`;
1074
1075
  const memberLabel = (key) => String(key).split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || String(key);
1075
- const teamLabel = (team) => team ?? "unassigned";
1076
1076
  const originOf = (item) => (item.package ? `package ${item.package} v${item.version}` : `member ${item.repoKey} @ ${short(item.commit)}`);
1077
1077
 
1078
- /** A soul's spawn default harness and model (feature desktop-facts): its definition's, else the kernel's (pi, the
1079
- * harness's native model) — what `oats spawn <soul>` takes with no --harness/--model/--launch-config. */
1080
- const soulDefaults = (def) => ({ harness: def?.harness ?? "pi", model: def?.model ?? null, harnessFrom: def?.harness ? "soul" : "kernel-default" });
1078
+ /** A soul's launch here (feature launch-preference): the `Launch` report a spawn with no flags would decide
1079
+ * (souls.launch, the soul's own launch, the host default), and the desktop-facts `harness`, `model`,
1080
+ * `harnessFrom` equal to its effective launch (`harnessFrom` "kernel-default" for the host default). */
1081
+ function soulLaunchFacts(entry, local, { launchConfigs, contextDir }) {
1082
+ const launch = launchReportFor({ layers: launchLayers({ definition: entry.definition, entry, key: soulKeyOf(entry), local }), launchConfigs, contextDir });
1083
+ return { harness: launch.effective.harness, model: launch.effective.model, harnessFrom: launch.from === "host" ? "kernel-default" : launch.from, launch };
1084
+ }
1081
1085
 
1082
1086
  /** Rows of every soul and capability of confirmed members (+ external souls) + locked package
1083
1087
  * capabilities. Souls have no private mode (0.26.0); a private member capability is listed with
1084
- * `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE). */
1085
- function workspaceItems(discovery, lock) {
1088
+ * `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE).
1089
+ * Each soul row carries its teams HERE (team model v2: `teams`, `defaultTeam`), from the committed
1090
+ * shared teams and `local` (oats-local.yaml); both null when its teams do not resolve (E_TEAM_*). */
1091
+ function workspaceItems(discovery, lock, local, deploymentDir) {
1086
1092
  const souls = [];
1093
+ let launchConfigs = {};
1094
+ try { launchConfigs = deploymentDir ? launchConfigsAt(deploymentDir) : {}; } catch { launchConfigs = {}; }
1095
+ const soulDefaults = (entry) => soulLaunchFacts(entry, local, { launchConfigs, contextDir: deploymentDir });
1087
1096
  const capabilities = [];
1097
+ const model = teamModel(discovery.standalone === true ? null : discovery.workspace, local, { workspaceKey: discovery.key ?? null });
1098
+ const teamsHere = (entry) => {
1099
+ try { const t = soulTeams(model, soulKeyOf(entry)); return { teams: reportRows(t.teams), defaultTeam: t.defaultTeam }; }
1100
+ catch (e) { if (String(e?.code).startsWith("E_TEAM_")) return { teams: null, defaultTeam: null }; throw e; }
1101
+ };
1088
1102
  for (const m of discovery.members) {
1089
1103
  if (!m.confirmed && !(discovery.standalone === true && m.key === discovery.key)) continue;
1090
- for (const s of m.souls) souls.push({ name: s.name, origin: originOf(s), kind: "member", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s.definition) });
1091
- for (const c of m.capabilities) capabilities.push({ name: c.name, origin: originOf(c), kind: "member", repoKey: c.repoKey, commit: c.commit, team: teamLabel(c.team), private: c.private, path: c.path, layer: c.manifest.layer ?? null, version: c.manifest.version ?? null });
1104
+ for (const s of m.souls) souls.push({ name: s.name, key: soulKeyOf(s), origin: originOf(s), kind: "member", repoKey: s.repoKey, commit: s.commit, ...teamsHere(s), private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s) });
1105
+ for (const c of m.capabilities) capabilities.push({ name: c.name, origin: originOf(c), kind: "member", repoKey: c.repoKey, commit: c.commit, private: c.private, path: c.path, layer: c.manifest.layer ?? null, version: c.manifest.version ?? null });
1092
1106
  }
1093
1107
  for (const ext of discovery.external || []) {
1094
1108
  const s = ext.soul;
1095
- souls.push({ name: s.name, origin: `external ${s.repoKey} @ ${short(s.commit)}`, kind: "external", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s.definition) });
1109
+ souls.push({ name: s.name, key: soulKeyOf(s), origin: `external ${s.repoKey} @ ${short(s.commit)}`, kind: "external", repoKey: s.repoKey, commit: s.commit, ...teamsHere(s), private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s) });
1096
1110
  }
1097
1111
  for (const s of discovery.packageSouls || []) {
1098
- souls.push({ name: s.name, qualifiedName: s.qualifiedName, origin: originOf(s), kind: "package", package: s.package, version: s.version, repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? [])], private: false, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s.definition) });
1112
+ souls.push({ name: s.name, key: soulKeyOf(s), qualifiedName: s.qualifiedName, origin: originOf(s), kind: "package", package: s.package, version: s.version, repoKey: s.repoKey, commit: s.commit, ...teamsHere(s), private: false, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s) });
1099
1113
  }
1100
1114
  for (const [id, entry] of Object.entries(lock?.packages || {})) {
1101
- for (const name of entry.capabilities) capabilities.push({ name, origin: originOf({ package: id, version: entry.version }), kind: "package", package: id, version: entry.version, commit: entry.commit, team: teamLabel(null), private: false });
1115
+ for (const name of entry.capabilities) capabilities.push({ name, origin: originOf({ package: id, version: entry.version }), kind: "package", package: id, version: entry.version, commit: entry.commit, private: false });
1102
1116
  }
1103
1117
  const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0);
1104
1118
  souls.sort(byName);
@@ -1109,7 +1123,7 @@ function workspaceItems(discovery, lock) {
1109
1123
  /** Membership rows for `sync` / `workspace status`. */
1110
1124
  function memberRows(discovery) {
1111
1125
  return discovery.members.map((m) => ({
1112
- key: m.key, name: memberLabel(m.key), commit: m.commit ?? null, confirmed: m.confirmed, status: m.confirmed ? "confirmed" : m.reason, detail: m.detail ?? null, team: m.team ?? null,
1126
+ key: m.key, name: memberLabel(m.key), commit: m.commit ?? null, confirmed: m.confirmed, status: m.confirmed ? "confirmed" : m.reason, detail: m.detail ?? null,
1113
1127
  souls: m.souls.map((s) => s.name), capabilities: m.capabilities.map((c) => c.name), publishes: m.publishes ?? null,
1114
1128
  }));
1115
1129
  }
@@ -1180,7 +1194,7 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
1180
1194
  const members = memberRows(discovery);
1181
1195
  const packages = packageRows(lock);
1182
1196
  const changes = resolved.changes;
1183
- const items = workspaceItems(discovery, lock);
1197
+ const items = workspaceItems(discovery, lock, ctx.local, ctx.deploymentDir);
1184
1198
  const report = { syncApi: 1, automations: automationCounts(automations), standalone: discovery.standalone === true || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, url: discovery.url, commit: discovery.commit, observedAt: discovery.observedAt, local: ctx.localPath, lock: lockFile }, members, packages, changes, problems, warnings: discovery.warnings ?? [] };
1185
1199
  return { report, lock, discovery, items, lockFile, problems };
1186
1200
  }
@@ -1216,10 +1230,8 @@ function printSyncReport(ctx, synced) {
1216
1230
  const packageSouls = items.souls.filter((s) => s.kind === "package");
1217
1231
  const disabledHere = items.souls.filter(isDisabled);
1218
1232
  console.log(`souls ${items.souls.length} discovered (${memberSouls.length} members, ${externalSouls.length} external, ${packageSouls.length ? `${packageSouls.length} package, ` : ""}${disabledHere.length} disabled here) · ${repoOwned.length} private capabilit${repoOwned.length === 1 ? "y" : "ies"}${repoOwned.length ? ` (${repoOwned.map((c) => `${c.name}, ${memberLabel(c.repoKey)} only`).join("; ")})` : ""}`);
1219
- const teams = new Map();
1220
- for (const s of items.souls) { const t = teams.get(s.team) || { souls: 0, capabilities: 0 }; t.souls++; teams.set(s.team, t); }
1221
- for (const c of items.capabilities.filter((c) => c.kind === "member")) { const t = teams.get(c.team) || { souls: 0, capabilities: 0 }; t.capabilities++; teams.set(c.team, t); }
1222
- console.log(`teams ${[...teams.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([team, n]) => `${team} ${n.souls} soul${n.souls === 1 ? "" : "s"}${n.capabilities ? `, ${n.capabilities} capabilit${n.capabilities === 1 ? "y" : "ies"}` : ""}`).join(" · ") || "(none)"}`);
1233
+ const sharedTeams = Object.keys(discovery.workspace?.teams || {}).sort();
1234
+ console.log(`teams ${sharedTeams.length ? `${sharedTeams.join(", ")} (shared)` : "(no shared teams)"} · this deployment's: oats teams`);
1223
1235
  const ac = report.automations;
1224
1236
  if (ac.triggers || ac.schedules) console.log(`automations ${ac.triggers} trigger${ac.triggers === 1 ? "" : "s"}, ${ac.schedules} schedule${ac.schedules === 1 ? "" : "s"} in the members (oats trigger list · oats schedule list)`);
1225
1237
  for (const p of synced.problems ?? discovery.problems) console.log(`problem ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`);
@@ -1417,32 +1429,46 @@ async function workspaceCmd() {
1417
1429
  const locked = new Set(packages.map((p) => p.id));
1418
1430
  const unsynced = declared.filter((id) => !locked.has(id));
1419
1431
  const stale = packages.filter((p) => !declared.includes(p.id)).map((p) => p.id);
1420
- const result = { workspaceStatusApi: 1, standalone: standalone || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, url: discovery.url, commit: discovery.commit, observedAt: discovery.observedAt, local: ctx.localPath, teams: Object.keys(discovery.workspace?.teams || {}) }, members, packages, declaredPackages: declared, unsynced, stale, external: (discovery.external || []).map((e) => ({ source: e.source, soul: e.soul.name, team: teamLabel(e.soul.team) })), problems: discovery.problems, warnings: discovery.warnings ?? [] };
1432
+ const result = { workspaceStatusApi: 1, standalone: standalone || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, url: discovery.url, commit: discovery.commit, observedAt: discovery.observedAt, local: ctx.localPath, teams: sharedTeamRows(discovery) }, members, packages, declaredPackages: declared, unsynced, stale, external: (discovery.external || []).map((e) => ({ source: e.source, soul: e.soul.name })), problems: discovery.problems, warnings: [...(discovery.warnings ?? [])] };
1421
1433
  // Workspace automations (0.29.0), from the snapshot `oats sync` took, placed on this host.
1422
1434
  const actx = scopeAutomations(ctx.deploymentDir, {});
1423
1435
  result.automations = { host: actx.host.name, snapshot: actx.snapshot ? { takenAt: actx.snapshot.takenAt, problems: actx.snapshot.problems.length } : null, rows: [...actx.triggers, ...actx.schedules].map((a) => ({ kind: a.kind, id: a.id, runsOn: a.runsOn, owner: a.owner, runsHere: a.placement.runsHere, reason: a.placement.reason, enabledHere: a.placement.enabledHere, origin: a.origin, ...(a.invalid ? { invalid: a.invalid } : {}) })) };
1436
+ // Automation trust (0.30): a workspace automation placed here but not trusted, and a trust entry
1437
+ // that names none, are warnings the operator acts on in oats-local.yaml.
1438
+ for (const a of [...actx.triggers, ...actx.schedules].filter((x) => x.placement.reason === "untrusted")) {
1439
+ result.warnings.push({ code: "automation-untrusted", kind: a.kind, id: a.id, message: `${a.kind} ${a.id} is declared for this host (${a.runsOn}, as ${a.owner}) but not trusted here, so it does not run`, remedy: A.trustRemedy(a.id) });
1440
+ }
1441
+ for (const entry of A.staleTrust(actx.host, [...actx.triggers, ...actx.schedules])) {
1442
+ result.warnings.push({ code: "automation-trust-stale", entry, message: `oats-local.yaml automations.trust names ${entry}, which is no workspace trigger or schedule${actx.snapshot ? "" : " (no automations snapshot yet: run oats sync)"}; its member may not have synced yet` });
1443
+ }
1424
1444
  await workspaceStatusFacts(result, discovery, lock, ctx);
1425
1445
  if (JSON_MODE) { jsonOk(result); return; }
1426
1446
  console.log(`workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)}) local ${shortPath(ctx.localPath)}\n`);
1427
1447
  if (standalone) console.log(` (${standaloneNote(discovery)})\n`);
1428
1448
  console.log("Members:");
1429
- printTable(["member", "status", "commit", "team", "souls", "capabilities", "publishes"], members.map((m) => [m.name, m.status, short(m.commit), m.team ?? "—", m.souls.join(",") || "—", m.capabilities.join(",") || "—", m.publishes ? `${m.publishes.package} v${m.publishes.version ?? "?"}` : "—"]));
1449
+ printTable(["member", "status", "commit", "souls", "capabilities", "publishes"], members.map((m) => [m.name, m.status, short(m.commit), m.souls.join(",") || "—", m.capabilities.join(",") || "—", m.publishes ? `${m.publishes.package} v${m.publishes.version ?? "?"}` : "—"]));
1430
1450
  for (const m of members.filter((m) => !m.confirmed)) console.log(` ${m.name}: ${m.detail}`);
1431
1451
  console.log("\nPackages:");
1432
1452
  if (!packages.length) console.log(unsynced.length ? ` (none locked yet — \`oats sync\` resolves ${unsynced.join(", ")})` : " (none)");
1433
1453
  else printTable(["package", "version", "source", "commit", "capabilities", "souls"], packages.map((p) => [p.id, p.version, p.source, short(p.commit), p.capabilities.join(","), p.souls.join(",") || "—"]));
1434
1454
  if (packages.length && unsynced.length) console.log(` declared but not locked (run \`oats sync\`): ${unsynced.join(", ")}`);
1435
1455
  if (stale.length) console.log(` locked but no longer declared (run \`oats sync\`): ${stale.join(", ")}`);
1436
- if (result.external.length) console.log(`\nExternal: ${result.external.map((e) => `${e.soul} (${e.source.replace(/@([0-9a-f]{40})$/, (_, o) => `@${short(o)}`)}, ${e.team})`).join(" ")}`);
1456
+ if (result.external.length) console.log(`\nExternal: ${result.external.map((e) => `${e.soul} (${e.source.replace(/@([0-9a-f]{40})$/, (_, o) => `@${short(o)}`)})`).join(" ")}`);
1437
1457
  if (result.automations.rows.length) {
1438
1458
  console.log(`\nAutomations (this host: ${result.automations.host ?? "unnamed — set host.name in oats-local.yaml"}):`);
1439
1459
  printTable(["kind", "id", "runs on", "owner", "here"], result.automations.rows.map((r) => [r.kind, r.id, r.runsOn, r.owner, r.runsHere ? "runs here" : r.invalid ? "invalid" : r.reason ?? "disabled here"]));
1440
1460
  }
1441
1461
  if (discovery.problems.length) { console.log("\nProblems:"); for (const p of discovery.problems) console.log(` ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`); }
1442
- if (discovery.warnings?.length) { console.log("\nWarnings:"); for (const w of discovery.warnings) console.log(` ${w.code} ${w.message}`); }
1462
+ if (result.warnings.length) { console.log("\nWarnings:"); for (const w of result.warnings) console.log(` ${w.code} ${w.message}${w.remedy ? `\n ${w.remedy}` : ""}`); }
1463
+ }
1464
+
1465
+ /** The committed (shared) teams as `workspace status` reports them: rows in label order (team model v2). */
1466
+ function sharedTeamRows(discovery) {
1467
+ const teams = discovery.standalone === true ? {} : discovery.workspace?.teams || {};
1468
+ return Object.keys(teams).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).map((label) => ({ label, team: typeof teams[label]?.team === "string" ? teams[label].team : null, description: typeof teams[label]?.description === "string" ? teams[label].description : null }));
1443
1469
  }
1444
1470
 
1445
- /** `oats workspace status` facts (feature desktop-facts): the workspace and team defaults as rows; this
1471
+ /** `oats workspace status` facts (feature desktop-facts): the workspace defaults as rows; this
1446
1472
  * computer (member clones and the rule that found each, disabled souls, the lock); file locations with
1447
1473
  * browsable urls; each catalog package's newer version, when the official catalog has one. */
1448
1474
  async function workspaceStatusFacts(result, discovery, lock, ctx) {
@@ -1454,7 +1480,6 @@ async function workspaceStatusFacts(result, discovery, lock, ctx) {
1454
1480
  result.defaults = {
1455
1481
  slots: Object.fromEntries(LAYERS.map((l) => [l, slot(defaults[l])])),
1456
1482
  capabilities: rows(defaults.capabilities),
1457
- byTeam: Object.fromEntries(Object.entries(defaults.byTeam && typeof defaults.byTeam === "object" ? defaults.byTeam : {}).map(([label, t]) => [label, { capabilities: rows(t?.capabilities) }])),
1458
1483
  };
1459
1484
  result.workspace.file = discovery.standalone === true ? null : { path: "oats-workspace.yaml", url: browseUrl(discovery.key, discovery.commit, "oats-workspace.yaml") };
1460
1485
  for (const m of result.members) Object.assign(m, { url: browseUrl(m.key, m.commit), membershipFile: { path: "oats-membership.yaml", url: browseUrl(m.key, m.commit, "oats-membership.yaml") } });
@@ -1475,6 +1500,88 @@ async function workspaceStatusFacts(result, discovery, lock, ctx) {
1475
1500
  }
1476
1501
  }
1477
1502
 
1503
+ /** The deployment's team facts for the team verbs: oats-local.yaml and the committed shared teams
1504
+ * (the workspace host read now; none in a standalone view). */
1505
+ async function teamsContext(bail) {
1506
+ const ctx = workspaceContext(bail);
1507
+ let workspace = null, workspaceKey = null;
1508
+ if (!(typeof ctx.local.standalone === "string" && ctx.local.standalone)) {
1509
+ try { const { observeWorkspace } = await import("../lib/workspace.mjs"); const obs = await observeWorkspace(ctx.local.workspace, { remoteOptions: ctx.remoteOptions }); workspace = obs.workspace; workspaceKey = obs.key; }
1510
+ catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance); throw e; }
1511
+ }
1512
+ return { deployment: ctx.deploymentDir, localPath: ctx.localPath, local: ctx.local, workspace, workspaceKey, remoteOptions: ctx.remoteOptions };
1513
+ }
1514
+ const labelsFlag = (name) => { const v = valueFlag(name); return v === undefined ? [] : String(v).split(",").map((l) => l.trim()).filter(Boolean); };
1515
+ const positional = (i) => (args[i] !== undefined && !args[i].startsWith("--") ? args[i] : undefined);
1516
+
1517
+ /** `oats teams [--json] | add <label> --team <id> [--description <d>] | remove <label> | default <label>` —
1518
+ * this deployment's teams (team model v2). Config only: never a provider call. */
1519
+ async function teamsCmd() {
1520
+ const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
1521
+ const usage = "usage: oats teams [--json] | oats teams add <label> --team <id> [--description <d>] | oats teams remove <label> | oats teams default <label> [--dir <deployment>] [--json]";
1522
+ const sub = positional(1);
1523
+ if (sub !== undefined && !["add", "remove", "default"].includes(sub)) bail("E_USAGE", usage);
1524
+ const label = sub ? positional(2) : undefined;
1525
+ if (sub && label === undefined) bail("E_BAD_ARGS", `oats teams ${sub} needs a team label — ${usage}`);
1526
+ const ctx = await teamsContext(bail);
1527
+ const V = await import("../lib/teams-verbs.mjs");
1528
+ let result = null;
1529
+ try {
1530
+ if (sub === "add") result = V.teamsAdd(ctx, label, { team: valueFlag("team"), description: valueFlag("description") });
1531
+ else if (sub === "remove") result = V.teamsRemove(ctx, label);
1532
+ else if (sub === "default") result = V.teamsDefault(ctx, label);
1533
+ } catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
1534
+ const doc = V.teamsDocument(result ? { ...ctx, local: result.local } : ctx);
1535
+ if (JSON_MODE) { jsonOk(result ? { ...doc, changed: result.changed } : doc); return; }
1536
+ if (result) console.log(result.changed ? `${sub === "add" ? `Declared team ${label}` : sub === "remove" ? `Removed team ${label}` : `The default team is now ${label}`} in ${shortPath(ctx.localPath)}` : "Nothing to change");
1537
+ console.log(`default ${doc.defaultTeam ?? "(none)"}`);
1538
+ if (!doc.teams.length) console.log("teams (none: `oats aweb setup` creates them, or `oats teams add <label> --team <id>`)");
1539
+ else printTable(["team", "id", "from", ""], doc.teams.map((t) => [t.label, t.team ?? "(no id yet)", t.from, t.default ? "default" : ""]));
1540
+ const souls = Object.entries(doc.souls.teams).map(([k, l]) => `${k}: ${l.join(",")}`);
1541
+ if (souls.length) console.log(`souls ${souls.join(" · ")}`);
1542
+ const defaults = Object.entries(doc.souls.default).map(([k, l]) => `${k}: ${l}`);
1543
+ if (defaults.length) console.log(`defaults ${defaults.join(" · ")}`);
1544
+ for (const p of doc.problems) console.log(`${p.severity === "failure" ? "problem" : "warning"} ${p.code} ${p.message} — ${p.fix}`);
1545
+ }
1546
+
1547
+ /** `oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <label> | --clear-default] [--json]` —
1548
+ * which teams a soul belongs to here, and why (team model v2). Config only. */
1549
+ async function soulCmd() {
1550
+ const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
1551
+ const usage = "usage: oats soul teams <soul>|'*' [--add <label>[,…]] [--remove <label>[,…]] [--default <label> | --clear-default] [--dir <deployment>] [--json]";
1552
+ if (positional(1) !== "teams") bail("E_USAGE", usage);
1553
+ const name = positional(2);
1554
+ if (name === undefined) bail("E_BAD_ARGS", `oats soul teams needs a soul (or '*' for every soul) — ${usage}`);
1555
+ const edit = { add: labelsFlag("add"), remove: labelsFlag("remove"), setDefault: valueFlag("default") ?? null, clearDefault: args.includes("--clear-default") };
1556
+ const ctx = workspaceContext(bail);
1557
+ let teamsCtx, soul = "*", key = "*";
1558
+ if (name === "*") teamsCtx = await teamsContext(bail);
1559
+ else {
1560
+ // The soul is named as for spawn: E_SOUL_UNKNOWN / E_SOUL_AMBIGUOUS; package souls come from the lock.
1561
+ let lock = null;
1562
+ try { lock = existsSync(join(ctx.deploymentDir, LOCK_FILE)) ? readLock(ctx.deploymentDir) : null; } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
1563
+ let discovery, entry;
1564
+ try {
1565
+ const { discoverOrStandalone, findSoulEntry } = await import("../lib/instance-resolution.mjs");
1566
+ discovery = await discoverOrStandalone(ctx.local, { lock, deployment: ctx.deploymentDir, remoteOptions: ctx.remoteOptions });
1567
+ entry = findSoulEntry(discovery, name);
1568
+ } catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance); throw e; }
1569
+ soul = entry.name; key = soulKeyOf(entry);
1570
+ teamsCtx = { deployment: ctx.deploymentDir, localPath: ctx.localPath, local: ctx.local, workspace: discovery.standalone === true ? null : discovery.workspace, workspaceKey: discovery.key ?? null };
1571
+ }
1572
+ const V = await import("../lib/teams-verbs.mjs");
1573
+ const mutating = edit.add.length || edit.remove.length || edit.setDefault !== null || edit.clearDefault;
1574
+ let result = null, doc;
1575
+ try {
1576
+ if (mutating) result = V.soulTeamsEdit(teamsCtx, key, edit);
1577
+ doc = V.soulTeamsDocument(result ? { ...teamsCtx, local: result.local } : teamsCtx, { soul, key });
1578
+ } catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
1579
+ if (JSON_MODE) { jsonOk(result ? { ...doc, changed: result.changed } : doc); return; }
1580
+ if (result) console.log(result.changed ? `Updated the teams of ${key === "*" ? "every soul" : key} in ${shortPath(teamsCtx.localPath)}` : "Nothing to change");
1581
+ console.log(`${key === "*" ? "every soul" : key} on this computer: default ${doc.defaultTeam ? `${doc.defaultTeam.label} (${doc.defaultTeam.from})` : "(none)"}`);
1582
+ if (doc.teams.length) printTable(["team", "id", "from", "why"], doc.teams.map((t) => [t.default ? `${t.label} (default)` : t.label, t.team ?? "(no id yet)", t.from, t.via.join(",")]));
1583
+ }
1584
+
1478
1585
  /** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
1479
1586
  async function itemsCmd(kind) {
1480
1587
  const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
@@ -1482,7 +1589,7 @@ async function itemsCmd(kind) {
1482
1589
  const discovery = await discoverForCli(ctx, bail);
1483
1590
  let lock;
1484
1591
  try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
1485
- const items = workspaceItems(discovery, lock)[kind];
1592
+ const items = workspaceItems(discovery, lock, ctx.local, ctx.deploymentDir)[kind];
1486
1593
  // One command's reads at a commit are shared: many souls resolve over the same manifests and listings.
1487
1594
  const remote = memoizedRemote(remoteModule);
1488
1595
  if (kind === "capabilities") await capabilityFacts(items, discovery, lock, ctx, remote);
@@ -1491,8 +1598,8 @@ async function itemsCmd(kind) {
1491
1598
  if (JSON_MODE) { jsonOk({ [`${kind}Api`]: 1, standalone: standalone || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, commit: discovery.commit }, [kind]: items, problems: discovery.problems }); return; }
1492
1599
  console.log(`${kind} of workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)})${standalone ? ` — ${standaloneNote(discovery)}` : ""}\n`);
1493
1600
  if (!items.length) console.log(" (none)");
1494
- else if (kind === "souls") printTable(["name", "origin", "team", "work"], items.map((s) => [s.name, s.origin, s.labels?.length > 1 ? s.labels.join(",") : s.team, s.work ?? "—"]));
1495
- else printTable(["name", "origin", "team", "layer"], items.map((c) => [c.private ? `${c.name} (repo-owned)` : c.name, c.origin, c.team, c.layer ?? "—"]));
1601
+ else if (kind === "souls") printTable(["name", "origin", "teams here", "work"], items.map((s) => [s.name, s.origin, s.teams === null ? "(invalid: oats teams)" : s.teams.map((t) => (t.default ? `${t.label}*` : t.label)).join(",") || "—", s.work ?? "—"]));
1602
+ else printTable(["name", "origin", "layer"], items.map((c) => [c.private ? `${c.name} (repo-owned)` : c.name, c.origin, c.layer ?? "—"]));
1496
1603
  const unsynced = Object.keys(discovery.workspace?.packages || {}).filter((id) => !lock.packages[id]);
1497
1604
  if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
1498
1605
  }
@@ -1636,6 +1743,16 @@ function soulRepoLabel(a, ws) {
1636
1743
  const moved = stamp.current && stamp.commit && stamp.current !== stamp.commit ? ` (member now @ ${short7(stamp.current)})` : "";
1637
1744
  return `${memberLabel(stamp.repoKey)} @ ${short7(stamp.commit)}${moved}`;
1638
1745
  }
1746
+ /** A roster agent's soul key: the qualified name of a package soul, else the recorded agent (soul) name;
1747
+ * null when no instance records a workspace soul. Exactly soulKeyOf for the soul it was spawned from. */
1748
+ function agentSoulKey(a) {
1749
+ for (const i of a.instances || []) {
1750
+ const soul = i?.workspace?.soul;
1751
+ if (!soul || typeof soul !== "object") continue;
1752
+ return soul.package && typeof soul.qualifiedName === "string" ? soul.qualifiedName : (typeof i.agent === "string" ? i.agent : a.name);
1753
+ }
1754
+ return null;
1755
+ }
1639
1756
  const short7 = (oid) => (typeof oid === "string" ? oid.slice(0, 7) : "?");
1640
1757
 
1641
1758
  async function status() {
@@ -1648,6 +1765,9 @@ async function status() {
1648
1765
  const verbose = args.includes("--verbose");
1649
1766
  const problems = legacyLayoutProblems(root);
1650
1767
  if (args.includes("--json")) {
1768
+ // The soul key (feature launch-preference): what souls.teams / souls.default / souls.launch and
1769
+ // `oats soul teams <key>` use — from the instances' records, so it needs no remote.
1770
+ for (const a of data) a.key = agentSoulKey(a);
1651
1771
  if (ws) for (const a of data) {
1652
1772
  const stamp = ws.souls.get(a.name);
1653
1773
  if (stamp) a.soulSource = { repoKey: stamp.repoKey, commit: stamp.commit, path: stamp.path, ...(stamp.current ? { current: stamp.current, status: stamp.current === stamp.commit ? "current" : "moved" } : {}) };
@@ -1764,7 +1884,7 @@ async function spawnCmd() {
1764
1884
  if (!agent || soulFetched || agent._dir !== dirname(soulDir)) agent = findAgent(root, soulName);
1765
1885
  }
1766
1886
  if (!agent) bail("E_SOUL_UNKNOWN", `soul "${name}" was fetched to ${shortPath(soulDir)} but is not readable as a soul there`);
1767
- note(`(workspace soul: "${name}" from ${wsPrepared.soulEntry.repoKey} @ ${String(wsPrepared.soulEntry.commit).slice(0, 12)}${wsPrepared.soulEntry.team ? `, team ${wsPrepared.soulEntry.team}` : ""}${soulFetched ? "; soul source fetched" : ""})`);
1887
+ note(`(workspace soul: "${name}" from ${wsPrepared.soulEntry.repoKey} @ ${String(wsPrepared.soulEntry.commit).slice(0, 12)}${wsPrepared.resolution.defaultTeam ? `, team ${wsPrepared.resolution.defaultTeam.label}` : ""}${soulFetched ? "; soul source fetched" : ""})`);
1768
1888
  } catch (e) {
1769
1889
  // Standalone (decisions 10/25): the ONLY package request is the kernel's own
1770
1890
  // default; when the catalog cannot name it, say so instead of "add it to packages:"
@@ -1922,7 +2042,7 @@ async function spawnCmd() {
1922
2042
  // A launch refusal (configuration, executable, environment reference,
1923
2043
  // model, harness) is a fact about the selection, not a spawn-mechanism
1924
2044
  // failure: it keeps its own code so a GUI can act on it.
1925
- if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_HARNESS$/.test(e.code)) { bail(e.code, e.message); throw e; }
2045
+ if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_HARNESS$|^E_HARNESS_UNAVAILABLE$/.test(e.code)) { bail(e.code, e.message, e.details); throw e; }
1926
2046
  // An unmet declared requirement is a fact about the soul's configuration
1927
2047
  // (with a remedy), not a spawn-mechanism failure: keep its code and details.
1928
2048
  if (e?.code === "E_REQUIREMENT_INACTIVE") { bail(e.code, e.message, { soul: e.soul, capabilities: e.capabilities, context: e.context, remedy: e.remedy }); throw e; }
@@ -2201,7 +2321,9 @@ async function triggerCmd() {
2201
2321
  try {
2202
2322
  const { observeWorkspace } = await import("../lib/workspace.mjs");
2203
2323
  const { local } = loadLocal(ws());
2204
- if (typeof local.workspace === "string") workspaceTeams = Object.keys((await observeWorkspace(local.workspace, { remoteOptions: remoteOptionsFromEnv() })).workspace.teams || {});
2324
+ // The labels declared here: the committed shared teams and this deployment's local ones.
2325
+ const shared = typeof local.workspace === "string" && !local.standalone ? (await observeWorkspace(local.workspace, { remoteOptions: remoteOptionsFromEnv() })).workspace : null;
2326
+ workspaceTeams = [...teamModel(shared, local).labels.keys()];
2205
2327
  } catch { /* reported as unknown (null) */ }
2206
2328
  return out(T.testTrigger(ws(), needId(), { workspaceTeams, ctx: ctx() }), (r) => [
2207
2329
  `trigger ${r.id}: ${r.ok ? "ready" : "NOT ready"} (nothing was spawned)`,
@@ -2279,7 +2401,11 @@ async function sessionCmd() {
2279
2401
  if (launchConfig === true) bad("--launch-config needs a configuration name, or none");
2280
2402
  const harness = harnessFlag();
2281
2403
  if (harness === true || (harness !== undefined && !LAUNCH_HARNESSES.includes(harness))) bad(`--harness must be one of ${LAUNCH_HARNESSES.join(", ")}`);
2282
- const opts = { model: model || undefined, launchConfig, harness, yolo: yoloFlag(), env: process.env, ...(await homeLiveTeams(home)) };
2404
+ // --reselect-launch (feature launch-preference): the home's launch layers decide again; without it a
2405
+ // recorded launch stays frozen (a changed preference never reaches an existing home on its own).
2406
+ const reselectLaunch = args.includes("--reselect-launch");
2407
+ if (reselectLaunch && launchConfig !== undefined) bad("--reselect-launch applies the launch preferences; --launch-config names one explicitly — choose one");
2408
+ const opts = { model: model || undefined, launchConfig, harness, yolo: yoloFlag(), ...(reselectLaunch ? { reselectLaunch: true } : {}), env: process.env, ...(await homeLiveTeams(home)) };
2283
2409
  if (args[1] === "restart") {
2284
2410
  const grace = flag("stop-grace");
2285
2411
  if (grace !== undefined) { if (grace === true || !/^\d+$/.test(String(grace)) || Number(grace) < 1 || Number(grace) > 300) bad("--stop-grace needs a number of seconds (1..300) to wait for the harness after SIGTERM"); opts.stopGraceMs = Number(grace) * 1000; }
@@ -2302,8 +2428,13 @@ async function sessionCmd() {
2302
2428
  const file = flag("file");
2303
2429
  if (!file || file === true) throw Object.assign(new Error("session upload needs --file <local path>"), { code: "E_BAD_ARGS" });
2304
2430
  result = uploadAttachment({ file, home: home === true ? undefined : home });
2305
- } else throw Object.assign(new Error("usage: oats session inspect|input|attach|start|restart|receive|upload --home /absolute/home [--text-file path] [--model id] [--launch-config name|none] [--harness pi|claude|codex] [--yolo|--no-yolo] [--stop-grace seconds] [--name file] [--file path] [--json]"), { code: "E_BAD_ARGS" });
2306
- if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
2431
+ } else throw Object.assign(new Error("usage: oats session inspect|input|attach|start|restart|receive|upload --home /absolute/home [--text-file path] [--model id] [--launch-config name|none | --reselect-launch] [--harness pi|claude|codex] [--yolo|--no-yolo] [--stop-grace seconds] [--name file] [--file path] [--json]"), { code: "E_BAD_ARGS" });
2432
+ if (JSON_MODE) jsonOk(result);
2433
+ else {
2434
+ console.log(JSON.stringify(result, null, 2));
2435
+ // A start's launch-hook warnings, as spawn prints its own (stderr keeps stdout one JSON document).
2436
+ for (const w of result?.warnings || []) console.error(` WARNING: ${w}`);
2437
+ }
2307
2438
  } catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message, e.details); }
2308
2439
  }
2309
2440
 
@@ -2313,8 +2444,8 @@ async function paneCmd() {
2313
2444
 
2314
2445
  /** `oats onboard [<dir>] --workspace <repo ref> [--json]` — workspace model v2 (decision 9).
2315
2446
  *
2316
- * Realizes a workspace on this machine in the directory the operator chooses (decision 9)
2317
- * (docs/design/2026-09-23-simplified-workspace-model.md §4): writes
2447
+ * Realizes a workspace on this machine in the directory the operator chooses
2448
+ * (docs/workspaces.md): writes
2318
2449
  * `<dir>/oats-local.yaml` naming the workspace, creates `<dir>/agents/` (the
2319
2450
  * instance homes), then runs exactly the `oats sync` path — discover over the
2320
2451
  * remotes, confirm membership, resolve `packages:`, write `oats-lock.json`. Nothing is installed, no soul
@@ -2543,8 +2674,8 @@ async function capabilityCommand() {
2543
2674
  const ws = meta.workspace && typeof meta.workspace === "object" ? meta.workspace : {};
2544
2675
  const messaging = (meta.capabilities || []).find((c) => c.layer === "messaging")?.id;
2545
2676
  homeMeta = { meta, messaging };
2546
- homeTeamCtx = (teams, teamsSource) => teamEnv({ workspace: { key: ws.key, name: ws.name, deployment: ws.deployment, team: ws.soul?.team, slots: { messaging } }, payloads: meta.providers, teams, teamsSource });
2547
- teamCtx = homeTeamCtx(meta.teams, "recorded");
2677
+ homeTeamCtx = (t) => teamEnv({ workspace: { key: ws.key, name: ws.name, deployment: ws.deployment }, teams: t.teams, defaultTeam: t.defaultTeam, teamsSource: t.source });
2678
+ teamCtx = homeTeamCtx({ ...recordedTeams(meta), source: "recorded" });
2548
2679
  } else {
2549
2680
  // Not inside a home: a v2 deployment (oats-local.yaml in reach) resolves
2550
2681
  // through the workspace, exactly as a spawn of --soul would (below).
@@ -2567,7 +2698,7 @@ async function capabilityCommand() {
2567
2698
  if (homeMeta && m.capability === homeMeta.messaging) {
2568
2699
  const { liveTeams } = await import("../lib/instance-resolution.mjs");
2569
2700
  const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
2570
- teamCtx = homeTeamCtx(live.teams, live.source);
2701
+ teamCtx = homeTeamCtx(live);
2571
2702
  }
2572
2703
  return runManifestCommand(m, { settings: capSettings[m.capability] || {}, origins: capOrigins[m.capability] }, teamCtx, () => m._dir, soulDir);
2573
2704
  }
@@ -2692,7 +2823,7 @@ function versionCmd() {
2692
2823
  // Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
2693
2824
  // runs on resolve/materialize (contract §6); a feature the binary does not implement is
2694
2825
  // never listed.
2695
- console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "teams", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
2826
+ console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
2696
2827
  return;
2697
2828
  }
2698
2829
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -3044,7 +3175,7 @@ async function serverRouteCmd() {
3044
3175
  // blame` pointing at the commit that last changed each command.
3045
3176
  const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
3046
3177
  /** Removed 0.24 verbs → their v2 replacement (workspace model v2, decision 5). Checked before capability dispatch. */
3047
- const REMOVED_VERBS = { prepare: "`oats onboard` / `oats sync` to set up a workspace, and `oats spawn <soul> --preview` to see what a spawn would resolve (the captured/portable path was removed in 0.26)", create: "author souls/<name>/soul.yaml + AGENTS.md in a member repository, then `oats sync`", type: "the soul's own soul.yaml in its member repository (agent types were a classic config block)", soul: "the soul's soul.yaml / AGENTS.md in its member repository, then `oats sync` (per-spawn choices: spawn flags or a launch configuration)", install: "oats sync", restore: "oats sync", init: "oats-local.yaml + oats sync", use: "soul.yaml capabilities: { <cap>: { from } } + workspace defaults", trust: "declaring the package in packages: (package approval was removed; oats sync locks commit + integrity)", list: "oats workspace status | oats capabilities", catalog: "oats package add <id> <version> (bare versions resolve through package-catalog.json)", remove: "oats package remove <id>", migrate: "a rebuild (no migration: docs/design/2026-09-23-workspace-module-contracts.md)", config: "oats-local.yaml (host settings) and oats-workspace.yaml (shared)", inject: "injection overrides are not part of the workspace model yet; edit the capability inject in its member repo" };
3178
+ const REMOVED_VERBS = { prepare: "`oats onboard` / `oats sync` to set up a workspace, and `oats spawn <soul> --preview` to see what a spawn would resolve (the captured/portable path was removed in 0.26)", create: "author souls/<name>/soul.yaml + AGENTS.md in a member repository, then `oats sync`", type: "the soul's own soul.yaml in its member repository (agent types were a classic config block)", install: "oats sync", restore: "oats sync", init: "oats-local.yaml + oats sync", use: "soul.yaml capabilities: { <cap>: { from } } + workspace defaults", trust: "declaring the package in packages: (package approval was removed; oats sync locks commit + integrity)", list: "oats workspace status | oats capabilities", catalog: "oats package add <id> <version> (bare versions resolve through package-catalog.json)", remove: "oats package remove <id>", migrate: "a rebuild (no migration: docs/design/2026-09-23-workspace-module-contracts.md)", config: "oats-local.yaml (host settings) and oats-workspace.yaml (shared)", inject: "injection overrides are not part of the workspace model yet; edit the capability inject in its member repo" };
3048
3179
  try {
3049
3180
  // The captured/portable path was removed in 0.26 (lead decisions on (e), D2/D3):
3050
3181
  // its selectors and an inherited captured context are refused, never quietly
@@ -3100,6 +3231,8 @@ else if (cmd === "sync") await syncCmd();
3100
3231
  else if (cmd === "package") await packageCmd();
3101
3232
  else if (cmd === "workspace") await workspaceCmd();
3102
3233
  else if (cmd === "capabilities" || cmd === "souls") await itemsCmd(cmd);
3234
+ else if (cmd === "teams") await teamsCmd();
3235
+ else if (cmd === "soul") await soulCmd();
3103
3236
  else if (cmd === "status") await status();
3104
3237
  else if (cmd === "pane") await paneCmd();
3105
3238
  else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
@@ -3223,6 +3356,9 @@ Usage:
3223
3356
  oats session start --home <absolute-home> start a STOPPED instance again in its existing home
3224
3357
  [--model m] [--launch-config n|none] (recorded recipe as is; a selection re-resolves it
3225
3358
  [--harness r] [--yolo|--no-yolo] against the scope; a named configuration is a unit)
3359
+ [--reselect-launch] applies the launch preferences now (oats-local.yaml
3360
+ souls.launch, the soul's launch); a changed preference
3361
+ never reaches an existing home without it or a respawn
3226
3362
  oats session restart --home <abs-home> stop the running harness (SIGTERM, bounded wait,
3227
3363
  [same flags] [--stop-grace <s>] never escalated) and start it again in place under
3228
3364
  the same lock; a stop that is not observed is
@@ -3230,7 +3366,7 @@ Usage:
3230
3366
  [--model <m>] [--json] (same identity, worktree, notes and launch env; no
3231
3367
  spawn hooks); --model replaces the recorded model
3232
3368
  for this and later starts; a live harness is refused
3233
- oats spawn <agent> [--task <text>] spawn an instance (tmux/Herdr; --no-launch
3369
+ oats spawn <agent> [--task <text> | --task-file <f>] spawn an instance (tmux/Herdr; --no-launch
3234
3370
  [--purpose <slug>] [--repo <r>] = scaffold only); the agent is a workspace
3235
3371
  [--parent <instance>] soul or a capability-defined agent
3236
3372
  [--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
@@ -3238,7 +3374,9 @@ Usage:
3238
3374
  [--relative-root <agents-root>] disambiguates same-named team anchors
3239
3375
  [--work worktree|checkout|attached|workspace|directory] = sugar for --relative-to X --relation
3240
3376
  [--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
3241
- [--no-launch] [--json]
3377
+ [--no-launch] [--json] without --launch-config/--harness the launch is the
3378
+ soul's preference: oats-local.yaml souls.launch.<soul>,
3379
+ then souls.launch."*", then the soul's launch:, then pi
3242
3380
  with team: declared, unknown souls
3243
3381
  resolve across the team scope's repos
3244
3382
  directory: owned home/work, config context may
@@ -3302,7 +3440,13 @@ Usage:
3302
3440
  its own repo's souls) + the locked packages
3303
3441
  oats souls [--dir <d>] [--json] every soul of every confirmed member + external souls
3304
3442
  (souls have no private mode), with origin
3305
- (member <key> @ <commit> | package <id> v<ver>) and team
3443
+ (member <key> @ <commit> | package <id> v<ver>) and its
3444
+ teams on this deployment
3445
+ oats teams [--json] | add <label> --team <id> [--description <d>] | remove <label>
3446
+ | default <label> [--dir <d>] this deployment's teams (shared + local), the
3447
+ default; add/remove/default edit oats-local.yaml
3448
+ oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <l> | --clear-default]
3449
+ [--dir <d>] [--json] which teams a soul (or every soul) belongs to here
3306
3450
  oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
3307
3451
  read-only Git observation of the instance's work
3308
3452
  tree: branch, status (renames kept), ahead/behind