@awebai/oats 0.29.3 → 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 (232) hide show
  1. package/README.md +12 -6
  2. package/bin/oats.mjs +203 -54
  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 +16 -9
  31. package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
  32. package/capabilities/oats-okf/lib/config.mjs +2 -1
  33. package/capabilities/oats-okf/lib/consult.mjs +26 -4
  34. package/capabilities/oats-okf/lib/harvest-switch.mjs +16 -3
  35. package/capabilities/oats-okf/lib/inspection.mjs +26 -7
  36. package/capabilities/oats-okf/lib/io.mjs +9 -1
  37. package/capabilities/oats-okf/lib/sources.mjs +34 -3
  38. package/capabilities/oats-okf/lib/stores.mjs +8 -6
  39. package/capabilities/oats-okf/lib/worker.mjs +7 -17
  40. package/capabilities/oats-okf/oats.json +6 -3
  41. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
  42. package/capabilities/oats-okf-harvest/oats.json +3 -3
  43. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
  44. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +37 -16
  45. package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
  46. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +6 -1
  47. package/capabilities/oats-okf-maintenance/oats.json +2 -2
  48. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +17 -2
  49. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
  50. package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
  51. package/capabilities/oats-workspace-experts/oats.json +9 -0
  52. package/docs/capabilities.md +160 -171
  53. package/docs/capability-manifest.schema.json +7 -10
  54. package/docs/configuration.md +213 -64
  55. package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
  56. package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
  57. package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
  58. package/docs/design/2026-09-27-team-model-v2.md +116 -0
  59. package/docs/design/2026-09-28-automations-trust.md +38 -0
  60. package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
  61. package/docs/design/HISTORY.md +65 -0
  62. package/docs/design/README.md +23 -54
  63. package/docs/desktop-cli-api.md +1787 -1777
  64. package/docs/desktop.md +30 -91
  65. package/docs/execution-targets.md +146 -292
  66. package/docs/first-team.md +31 -17
  67. package/docs/implementation.md +76 -288
  68. package/docs/integrations.md +118 -320
  69. package/docs/knowledge-capability-authoring.md +25 -52
  70. package/docs/knowledge-reference/acceptance.md +3 -3
  71. package/docs/knowledge-reference/adoption.md +1 -1
  72. package/docs/knowledge-reference/harvester.md +2 -2
  73. package/docs/knowledge-reference/package-craft.md +3 -3
  74. package/docs/knowledge-reference/provider-mapping.md +3 -6
  75. package/docs/knowledge-reference/reader-capture.md +3 -3
  76. package/docs/knowledge-theory.md +62 -166
  77. package/docs/knowledge.md +225 -404
  78. package/docs/layers.md +42 -97
  79. package/docs/oats-local.schema.json +58 -5
  80. package/docs/oats-membership.schema.json +1 -8
  81. package/docs/oats-package.schema.json +5 -5
  82. package/docs/oats-workspace.schema.json +8 -22
  83. package/docs/official-catalog.md +25 -28
  84. package/docs/packages.md +45 -63
  85. package/docs/plans/0.30-close-out.md +61 -0
  86. package/docs/release-lane.md +77 -0
  87. package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
  88. package/docs/release-notes/v0.19.0.md +48 -147
  89. package/docs/release-notes/v0.19.1.md +2 -3
  90. package/docs/release-notes/v0.19.3.md +2 -15
  91. package/docs/release-notes/v0.20.0.md +0 -15
  92. package/docs/release-notes/v0.22.0.md +71 -138
  93. package/docs/release-notes/v0.22.1.md +42 -90
  94. package/docs/release-notes/v0.22.10.md +1 -1
  95. package/docs/release-notes/v0.22.11.md +1 -47
  96. package/docs/release-notes/v0.22.12.md +4 -13
  97. package/docs/release-notes/v0.22.13.md +1 -42
  98. package/docs/release-notes/v0.22.14.md +3 -11
  99. package/docs/release-notes/v0.22.15.md +1 -46
  100. package/docs/release-notes/v0.22.16.md +6 -8
  101. package/docs/release-notes/v0.22.18.md +1 -99
  102. package/docs/release-notes/v0.22.19.md +3 -14
  103. package/docs/release-notes/v0.22.2.md +6 -15
  104. package/docs/release-notes/v0.22.3.md +0 -1
  105. package/docs/release-notes/v0.22.4.md +1 -14
  106. package/docs/release-notes/v0.22.5.md +2 -12
  107. package/docs/release-notes/v0.22.6.md +0 -3
  108. package/docs/release-notes/v0.23.0.md +9 -25
  109. package/docs/release-notes/v0.23.1.md +9 -25
  110. package/docs/release-notes/v0.23.2.md +2 -4
  111. package/docs/release-notes/v0.24.0.md +56 -97
  112. package/docs/release-notes/v0.24.1.md +7 -11
  113. package/docs/release-notes/v0.24.10.md +34 -45
  114. package/docs/release-notes/v0.24.11.md +12 -20
  115. package/docs/release-notes/v0.24.12.md +35 -48
  116. package/docs/release-notes/v0.24.13.md +34 -41
  117. package/docs/release-notes/v0.24.2.md +9 -13
  118. package/docs/release-notes/v0.24.3.md +7 -11
  119. package/docs/release-notes/v0.24.4.md +6 -6
  120. package/docs/release-notes/v0.24.5.md +6 -10
  121. package/docs/release-notes/v0.24.6.md +2 -5
  122. package/docs/release-notes/v0.24.7.md +46 -75
  123. package/docs/release-notes/v0.24.8.md +58 -96
  124. package/docs/release-notes/v0.24.9.md +38 -54
  125. package/docs/release-notes/v0.25.0.md +59 -76
  126. package/docs/release-notes/v0.25.1.md +57 -81
  127. package/docs/release-notes/v0.25.2.md +51 -70
  128. package/docs/release-notes/v0.25.3.md +11 -13
  129. package/docs/release-notes/v0.25.4.md +9 -13
  130. package/docs/release-notes/v0.25.5.md +3 -5
  131. package/docs/release-notes/v0.25.6.md +20 -29
  132. package/docs/release-notes/v0.25.7.md +5 -7
  133. package/docs/release-notes/v0.25.8.md +26 -39
  134. package/docs/release-notes/v0.26.0.md +175 -646
  135. package/docs/release-notes/v0.27.0.md +4 -5
  136. package/docs/release-notes/v0.27.1.md +4 -6
  137. package/docs/release-notes/v0.27.2.md +1 -1
  138. package/docs/release-notes/v0.28.0.md +57 -124
  139. package/docs/release-notes/v0.29.0.md +89 -208
  140. package/docs/release-notes/v0.29.1.md +1 -1
  141. package/docs/release-notes/v0.29.2.md +3 -4
  142. package/docs/release-notes/v0.29.4.md +90 -0
  143. package/docs/release-notes/v0.30.0.md +205 -0
  144. package/docs/schedules.md +280 -349
  145. package/docs/servers.md +99 -117
  146. package/docs/soul.schema.json +2 -9
  147. package/docs/souls-and-instances.md +145 -158
  148. package/docs/workspaces.md +132 -215
  149. package/lib/automations.mjs +28 -6
  150. package/lib/core.mjs +226 -74
  151. package/lib/instance-events.mjs +1 -1
  152. package/lib/instance-inspect.mjs +109 -34
  153. package/lib/instance-lifecycle.mjs +14 -1
  154. package/lib/instance-resolution.mjs +26 -27
  155. package/lib/launch-preference.mjs +87 -0
  156. package/lib/materialize.mjs +3 -3
  157. package/lib/packages.mjs +2 -5
  158. package/lib/resolve.mjs +29 -87
  159. package/lib/schedule.mjs +32 -18
  160. package/lib/teams-verbs.mjs +195 -0
  161. package/lib/teams.mjs +190 -0
  162. package/lib/triggers.mjs +53 -17
  163. package/lib/workspace.mjs +54 -147
  164. package/package-catalog.json +9 -15
  165. package/package.json +1 -1
  166. package/skills/oats-getting-started/SKILL.md +25 -13
  167. package/capabilities/oats-review/injects/review.md +0 -69
  168. package/capabilities/oats-review/oats.json +0 -10
  169. package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
  170. package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
  171. package/docs/conventions.md +0 -90
  172. package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
  173. package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
  174. package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
  175. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
  176. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
  177. package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
  178. package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
  179. package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
  180. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
  181. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
  182. package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
  183. package/docs/design/2026-09-15-captured-dispatch.md +0 -127
  184. package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
  185. package/docs/design/2026-09-15-package-preparation.md +0 -100
  186. package/docs/design/2026-09-15-portable-data-contract.md +0 -121
  187. package/docs/design/2026-09-15-portable-declarations.md +0 -189
  188. package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
  189. package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
  190. package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
  191. package/docs/design/2026-09-15-source-observation.md +0 -119
  192. package/docs/design/2026-09-16-captured-admission.md +0 -77
  193. package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
  194. package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
  195. package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
  196. package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
  197. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
  198. package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
  199. package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
  200. package/docs/design/2026-09-16-portable-onboarding.md +0 -179
  201. package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
  202. package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
  203. package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
  204. package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
  205. package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
  206. package/docs/design/2026-09-17-captured-native-start.md +0 -58
  207. package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
  208. package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
  209. package/docs/design/2026-09-17-public-captured-start.md +0 -108
  210. package/docs/design/2026-09-17-public-prepare-request.md +0 -90
  211. package/docs/design/2026-09-18-captured-pi-host.md +0 -205
  212. package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
  213. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
  214. package/docs/design/2026-09-20-redesign-program-board.md +0 -142
  215. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
  216. package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
  217. package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
  218. package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
  219. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
  220. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
  221. package/docs/design/2026-09-24-phase-d-plan.md +0 -305
  222. package/docs/design/2026-09-25-teams-contract.md +0 -258
  223. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
  224. package/docs/design/desktop-ux-plan.md +0 -362
  225. package/docs/design/launch-configurations.md +0 -168
  226. package/docs/design/okf-mirror-provenance.md +0 -105
  227. package/docs/design/operations-contract.md +0 -141
  228. package/docs/oats-member.schema.json +0 -38
  229. package/skills/integration-authoring/SKILL.md +0 -84
  230. package/skills/oats-support/SKILL.md +0 -79
  231. package/skills/skill-craft/SKILL.md +0 -109
  232. package/skills/soul-craft/SKILL.md +0 -116
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";
@@ -55,7 +57,8 @@ const rawArgs = process.argv.slice(2);
55
57
  const KERNEL_SWITCHES = new Set(["allow-child-spawns", "apply", "check", "clear", "delete-branch", "discard-worktree", "dry-run", "ephemeral", "force", "help", "host", "json", "keep-dir", "keep-env", "no-child-spawns", "no-launch", "no-recursive", "no-yolo", "plan", "policy", "preview", "print", "replace", "self", "verbose", "yes", "yolo"]);
56
58
  /** `--flag=value` is `--flag value`: every kernel reader (flag(), valueFlag(), the onboard and
57
59
  * routed-command loops) then applies the spaced form's validation to it. `problem` is an empty
58
- * `--flag=` or a switch given a value. */
60
+ * `--flag=`, a switch given a value, or a value that is itself an option (`--model=--yolo`):
61
+ * expanded, it would be a flag token every reader sees, which the spaced form can never carry. */
59
62
  function expandInlineValues(argv) {
60
63
  const out = [];
61
64
  let problem;
@@ -63,7 +66,7 @@ function expandInlineValues(argv) {
63
66
  const eq = a.indexOf("=");
64
67
  if (!a.startsWith("--") || eq <= 2) { out.push(a); continue; }
65
68
  const name = a.slice(2, eq), value = a.slice(eq + 1);
66
- problem ??= KERNEL_SWITCHES.has(name) ? `--${name} takes no value (got ${a})` : value === "" ? `--${name}= needs a value` : undefined;
69
+ problem ??= KERNEL_SWITCHES.has(name) ? `--${name} takes no value (got ${a})` : value === "" ? `--${name}= needs a value` : value.startsWith("--") ? `--${name}= takes a value, not an option (got ${a})` : undefined;
67
70
  out.push(`--${name}`, value);
68
71
  }
69
72
  return { argv: out, problem };
@@ -71,7 +74,7 @@ function expandInlineValues(argv) {
71
74
  const { argv: args, problem: argvProblem } = expandInlineValues(rawArgs);
72
75
  let cmd = args[0];
73
76
  const HELP_WORDS = new Set(["help", "--help", "-h"]);
74
- 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"]);
75
78
  /** Commands whose argv another parser reads (packages/record and packages/experimental parse process.argv). */
76
79
  const OWN_ARGV_COMMANDS = new Set(["capture", "recall", "setup", "experimental"]);
77
80
  /** Commands `--server <id>` runs on a registered server. */
@@ -410,18 +413,17 @@ function printWorkspaceInspect(doc) {
410
413
  for (const p of doc.problems) console.log(` ! ${p.code}: ${p.message}`);
411
414
  }
412
415
 
413
- /** `{ teams, teamsSource }` for a session start of a workspace home: its eligible teams read
414
- * live (two repository reads), which the launch hook re-checks joined memberships against
415
- * (teams contract decision 6) — or the spawn record, marked `recorded`, when the read cannot
416
- * 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
417
419
  * itself refuses it. */
418
420
  async function homeLiveTeams(home) {
419
421
  let meta;
420
422
  try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); } catch { return {}; }
421
423
  if (!isWorkspaceHome(meta)) return {};
422
424
  const { liveTeams } = await import("../lib/instance-resolution.mjs");
423
- const { teams, source } = await liveTeams(home, meta, { remoteOptions: remoteOptionsFromEnv() });
424
- 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 } : {};
425
427
  }
426
428
 
427
429
  // ---------- operation run: generic invoke through the capability engine ----------
@@ -1071,33 +1073,46 @@ const short = (oid) => (typeof oid === "string" ? oid.slice(0, 8) : "?");
1071
1073
  /** Display name of a discovery: the workspace's name, or the standalone label (decision 10). */
1072
1074
  const workspaceName = (discovery) => discovery.workspace?.name ?? `standalone:${memberLabel(discovery.key)}`;
1073
1075
  const memberLabel = (key) => String(key).split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || String(key);
1074
- const teamLabel = (team) => team ?? "unassigned";
1075
1076
  const originOf = (item) => (item.package ? `package ${item.package} v${item.version}` : `member ${item.repoKey} @ ${short(item.commit)}`);
1076
1077
 
1077
- /** A soul's spawn default harness and model (feature desktop-facts): its definition's, else the kernel's (pi, the
1078
- * harness's native model) — what `oats spawn <soul>` takes with no --harness/--model/--launch-config. */
1079
- 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
+ }
1080
1085
 
1081
1086
  /** Rows of every soul and capability of confirmed members (+ external souls) + locked package
1082
1087
  * capabilities. Souls have no private mode (0.26.0); a private member capability is listed with
1083
- * `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE). */
1084
- 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) {
1085
1092
  const souls = [];
1093
+ let launchConfigs = {};
1094
+ try { launchConfigs = deploymentDir ? launchConfigsAt(deploymentDir) : {}; } catch { launchConfigs = {}; }
1095
+ const soulDefaults = (entry) => soulLaunchFacts(entry, local, { launchConfigs, contextDir: deploymentDir });
1086
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
+ };
1087
1102
  for (const m of discovery.members) {
1088
1103
  if (!m.confirmed && !(discovery.standalone === true && m.key === discovery.key)) continue;
1089
- 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) });
1090
- 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 });
1091
1106
  }
1092
1107
  for (const ext of discovery.external || []) {
1093
1108
  const s = ext.soul;
1094
- 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) });
1095
1110
  }
1096
1111
  for (const s of discovery.packageSouls || []) {
1097
- 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) });
1098
1113
  }
1099
1114
  for (const [id, entry] of Object.entries(lock?.packages || {})) {
1100
- 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 });
1101
1116
  }
1102
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);
1103
1118
  souls.sort(byName);
@@ -1108,7 +1123,7 @@ function workspaceItems(discovery, lock) {
1108
1123
  /** Membership rows for `sync` / `workspace status`. */
1109
1124
  function memberRows(discovery) {
1110
1125
  return discovery.members.map((m) => ({
1111
- 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,
1112
1127
  souls: m.souls.map((s) => s.name), capabilities: m.capabilities.map((c) => c.name), publishes: m.publishes ?? null,
1113
1128
  }));
1114
1129
  }
@@ -1179,7 +1194,7 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
1179
1194
  const members = memberRows(discovery);
1180
1195
  const packages = packageRows(lock);
1181
1196
  const changes = resolved.changes;
1182
- const items = workspaceItems(discovery, lock);
1197
+ const items = workspaceItems(discovery, lock, ctx.local, ctx.deploymentDir);
1183
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 ?? [] };
1184
1199
  return { report, lock, discovery, items, lockFile, problems };
1185
1200
  }
@@ -1215,10 +1230,8 @@ function printSyncReport(ctx, synced) {
1215
1230
  const packageSouls = items.souls.filter((s) => s.kind === "package");
1216
1231
  const disabledHere = items.souls.filter(isDisabled);
1217
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("; ")})` : ""}`);
1218
- const teams = new Map();
1219
- for (const s of items.souls) { const t = teams.get(s.team) || { souls: 0, capabilities: 0 }; t.souls++; teams.set(s.team, t); }
1220
- 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); }
1221
- 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`);
1222
1235
  const ac = report.automations;
1223
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)`);
1224
1237
  for (const p of synced.problems ?? discovery.problems) console.log(`problem ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`);
@@ -1416,32 +1429,46 @@ async function workspaceCmd() {
1416
1429
  const locked = new Set(packages.map((p) => p.id));
1417
1430
  const unsynced = declared.filter((id) => !locked.has(id));
1418
1431
  const stale = packages.filter((p) => !declared.includes(p.id)).map((p) => p.id);
1419
- 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 ?? [])] };
1420
1433
  // Workspace automations (0.29.0), from the snapshot `oats sync` took, placed on this host.
1421
1434
  const actx = scopeAutomations(ctx.deploymentDir, {});
1422
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
+ }
1423
1444
  await workspaceStatusFacts(result, discovery, lock, ctx);
1424
1445
  if (JSON_MODE) { jsonOk(result); return; }
1425
1446
  console.log(`workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)}) local ${shortPath(ctx.localPath)}\n`);
1426
1447
  if (standalone) console.log(` (${standaloneNote(discovery)})\n`);
1427
1448
  console.log("Members:");
1428
- 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 ?? "?"}` : "—"]));
1429
1450
  for (const m of members.filter((m) => !m.confirmed)) console.log(` ${m.name}: ${m.detail}`);
1430
1451
  console.log("\nPackages:");
1431
1452
  if (!packages.length) console.log(unsynced.length ? ` (none locked yet — \`oats sync\` resolves ${unsynced.join(", ")})` : " (none)");
1432
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(",") || "—"]));
1433
1454
  if (packages.length && unsynced.length) console.log(` declared but not locked (run \`oats sync\`): ${unsynced.join(", ")}`);
1434
1455
  if (stale.length) console.log(` locked but no longer declared (run \`oats sync\`): ${stale.join(", ")}`);
1435
- 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(" ")}`);
1436
1457
  if (result.automations.rows.length) {
1437
1458
  console.log(`\nAutomations (this host: ${result.automations.host ?? "unnamed — set host.name in oats-local.yaml"}):`);
1438
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"]));
1439
1460
  }
1440
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}`); }
1441
- 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 }));
1442
1469
  }
1443
1470
 
1444
- /** `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
1445
1472
  * computer (member clones and the rule that found each, disabled souls, the lock); file locations with
1446
1473
  * browsable urls; each catalog package's newer version, when the official catalog has one. */
1447
1474
  async function workspaceStatusFacts(result, discovery, lock, ctx) {
@@ -1453,7 +1480,6 @@ async function workspaceStatusFacts(result, discovery, lock, ctx) {
1453
1480
  result.defaults = {
1454
1481
  slots: Object.fromEntries(LAYERS.map((l) => [l, slot(defaults[l])])),
1455
1482
  capabilities: rows(defaults.capabilities),
1456
- byTeam: Object.fromEntries(Object.entries(defaults.byTeam && typeof defaults.byTeam === "object" ? defaults.byTeam : {}).map(([label, t]) => [label, { capabilities: rows(t?.capabilities) }])),
1457
1483
  };
1458
1484
  result.workspace.file = discovery.standalone === true ? null : { path: "oats-workspace.yaml", url: browseUrl(discovery.key, discovery.commit, "oats-workspace.yaml") };
1459
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") } });
@@ -1474,6 +1500,88 @@ async function workspaceStatusFacts(result, discovery, lock, ctx) {
1474
1500
  }
1475
1501
  }
1476
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
+
1477
1585
  /** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
1478
1586
  async function itemsCmd(kind) {
1479
1587
  const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
@@ -1481,7 +1589,7 @@ async function itemsCmd(kind) {
1481
1589
  const discovery = await discoverForCli(ctx, bail);
1482
1590
  let lock;
1483
1591
  try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
1484
- const items = workspaceItems(discovery, lock)[kind];
1592
+ const items = workspaceItems(discovery, lock, ctx.local, ctx.deploymentDir)[kind];
1485
1593
  // One command's reads at a commit are shared: many souls resolve over the same manifests and listings.
1486
1594
  const remote = memoizedRemote(remoteModule);
1487
1595
  if (kind === "capabilities") await capabilityFacts(items, discovery, lock, ctx, remote);
@@ -1490,8 +1598,8 @@ async function itemsCmd(kind) {
1490
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; }
1491
1599
  console.log(`${kind} of workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)})${standalone ? ` — ${standaloneNote(discovery)}` : ""}\n`);
1492
1600
  if (!items.length) console.log(" (none)");
1493
- 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 ?? "—"]));
1494
- 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 ?? "—"]));
1495
1603
  const unsynced = Object.keys(discovery.workspace?.packages || {}).filter((id) => !lock.packages[id]);
1496
1604
  if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
1497
1605
  }
@@ -1635,6 +1743,16 @@ function soulRepoLabel(a, ws) {
1635
1743
  const moved = stamp.current && stamp.commit && stamp.current !== stamp.commit ? ` (member now @ ${short7(stamp.current)})` : "";
1636
1744
  return `${memberLabel(stamp.repoKey)} @ ${short7(stamp.commit)}${moved}`;
1637
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
+ }
1638
1756
  const short7 = (oid) => (typeof oid === "string" ? oid.slice(0, 7) : "?");
1639
1757
 
1640
1758
  async function status() {
@@ -1647,6 +1765,9 @@ async function status() {
1647
1765
  const verbose = args.includes("--verbose");
1648
1766
  const problems = legacyLayoutProblems(root);
1649
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);
1650
1771
  if (ws) for (const a of data) {
1651
1772
  const stamp = ws.souls.get(a.name);
1652
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" } : {}) };
@@ -1763,7 +1884,7 @@ async function spawnCmd() {
1763
1884
  if (!agent || soulFetched || agent._dir !== dirname(soulDir)) agent = findAgent(root, soulName);
1764
1885
  }
1765
1886
  if (!agent) bail("E_SOUL_UNKNOWN", `soul "${name}" was fetched to ${shortPath(soulDir)} but is not readable as a soul there`);
1766
- 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" : ""})`);
1767
1888
  } catch (e) {
1768
1889
  // Standalone (decisions 10/25): the ONLY package request is the kernel's own
1769
1890
  // default; when the catalog cannot name it, say so instead of "add it to packages:"
@@ -1921,7 +2042,7 @@ async function spawnCmd() {
1921
2042
  // A launch refusal (configuration, executable, environment reference,
1922
2043
  // model, harness) is a fact about the selection, not a spawn-mechanism
1923
2044
  // failure: it keeps its own code so a GUI can act on it.
1924
- 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; }
1925
2046
  // An unmet declared requirement is a fact about the soul's configuration
1926
2047
  // (with a remedy), not a spawn-mechanism failure: keep its code and details.
1927
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; }
@@ -2144,7 +2265,7 @@ async function scheduleCmd() {
2144
2265
  if (args.includes("--host")) return out(tickHost({ io, dryRun }));
2145
2266
  const reg = readRegistry();
2146
2267
  const tctx = scopeAutomations(ws(), { io, refresh: !dryRun });
2147
- const considered = withHostLock(() => [...(tctx.refresh && !tctx.refresh.ok ? [{ workspace: ws(), action: "error", error: `automations refresh: ${tctx.refresh.error}` }] : []), ...tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun, ctx: tctx }), ...tickTriggers(ws(), { io, dryRun, ctx: tctx })]);
2268
+ const considered = withHostLock(() => [...(tctx.refresh && !tctx.refresh.ok ? [{ workspace: ws(), action: "error", error: `automations refresh: ${tctx.refresh.error}` }] : []), ...tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun, ctx: tctx }), ...tickTriggers(ws(), { io, dryRun, ctx: tctx, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()] })]);
2148
2269
  return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws(), io) });
2149
2270
  }
2150
2271
  case "host": {
@@ -2200,7 +2321,9 @@ async function triggerCmd() {
2200
2321
  try {
2201
2322
  const { observeWorkspace } = await import("../lib/workspace.mjs");
2202
2323
  const { local } = loadLocal(ws());
2203
- 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()];
2204
2327
  } catch { /* reported as unknown (null) */ }
2205
2328
  return out(T.testTrigger(ws(), needId(), { workspaceTeams, ctx: ctx() }), (r) => [
2206
2329
  `trigger ${r.id}: ${r.ok ? "ready" : "NOT ready"} (nothing was spawned)`,
@@ -2278,7 +2401,11 @@ async function sessionCmd() {
2278
2401
  if (launchConfig === true) bad("--launch-config needs a configuration name, or none");
2279
2402
  const harness = harnessFlag();
2280
2403
  if (harness === true || (harness !== undefined && !LAUNCH_HARNESSES.includes(harness))) bad(`--harness must be one of ${LAUNCH_HARNESSES.join(", ")}`);
2281
- 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)) };
2282
2409
  if (args[1] === "restart") {
2283
2410
  const grace = flag("stop-grace");
2284
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; }
@@ -2301,8 +2428,13 @@ async function sessionCmd() {
2301
2428
  const file = flag("file");
2302
2429
  if (!file || file === true) throw Object.assign(new Error("session upload needs --file <local path>"), { code: "E_BAD_ARGS" });
2303
2430
  result = uploadAttachment({ file, home: home === true ? undefined : home });
2304
- } 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" });
2305
- 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
+ }
2306
2438
  } catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message, e.details); }
2307
2439
  }
2308
2440
 
@@ -2312,8 +2444,8 @@ async function paneCmd() {
2312
2444
 
2313
2445
  /** `oats onboard [<dir>] --workspace <repo ref> [--json]` — workspace model v2 (decision 9).
2314
2446
  *
2315
- * Realizes a workspace on this machine in the directory the operator chooses (decision 9)
2316
- * (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
2317
2449
  * `<dir>/oats-local.yaml` naming the workspace, creates `<dir>/agents/` (the
2318
2450
  * instance homes), then runs exactly the `oats sync` path — discover over the
2319
2451
  * remotes, confirm membership, resolve `packages:`, write `oats-lock.json`. Nothing is installed, no soul
@@ -2503,7 +2635,11 @@ async function capabilityCommand() {
2503
2635
  const teamCtx = teamEnv(resolvedFromPrepared(hit.prepared, hit.deployment));
2504
2636
  // No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
2505
2637
  // already fetched exactly this commit; otherwise the command gets none (never ambient).
2506
- const cachedSoul = hit.soul?.commit ? join(hit.deployment, "agents", hit.soul.name, "souls", String(hit.soul.commit).slice(0, 12)) : null;
2638
+ // The agent directory is the soul entry's own (a package soul's is `<package>--<soul>`), never
2639
+ // the bare name, which a same-named member soul's copy may occupy (as instance-inspect does).
2640
+ const { agentDirOf } = await import("../lib/instance-resolution.mjs");
2641
+ const entry = hit.prepared?.soulEntry;
2642
+ const cachedSoul = entry?.commit ? join(hit.deployment, "agents", agentDirOf(entry), "souls", String(entry.commit).slice(0, 12)) : null;
2507
2643
  return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, { settings: hit.settings, origins: hit.resolution?.payloadOrigins?.[hit.module.name] }, teamCtx, hit.ensureTree, cachedSoul && existsSync(join(cachedSoul, "soul.yaml")) ? realpathSync(cachedSoul) : undefined);
2508
2644
  }
2509
2645
 
@@ -2538,8 +2674,8 @@ async function capabilityCommand() {
2538
2674
  const ws = meta.workspace && typeof meta.workspace === "object" ? meta.workspace : {};
2539
2675
  const messaging = (meta.capabilities || []).find((c) => c.layer === "messaging")?.id;
2540
2676
  homeMeta = { meta, messaging };
2541
- 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 });
2542
- 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" });
2543
2679
  } else {
2544
2680
  // Not inside a home: a v2 deployment (oats-local.yaml in reach) resolves
2545
2681
  // through the workspace, exactly as a spawn of --soul would (below).
@@ -2562,7 +2698,7 @@ async function capabilityCommand() {
2562
2698
  if (homeMeta && m.capability === homeMeta.messaging) {
2563
2699
  const { liveTeams } = await import("../lib/instance-resolution.mjs");
2564
2700
  const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
2565
- teamCtx = homeTeamCtx(live.teams, live.source);
2701
+ teamCtx = homeTeamCtx(live);
2566
2702
  }
2567
2703
  return runManifestCommand(m, { settings: capSettings[m.capability] || {}, origins: capOrigins[m.capability] }, teamCtx, () => m._dir, soulDir);
2568
2704
  }
@@ -2687,7 +2823,7 @@ function versionCmd() {
2687
2823
  // Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
2688
2824
  // runs on resolve/materialize (contract §6); a feature the binary does not implement is
2689
2825
  // never listed.
2690
- 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 }));
2691
2827
  return;
2692
2828
  }
2693
2829
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -3039,7 +3175,7 @@ async function serverRouteCmd() {
3039
3175
  // blame` pointing at the commit that last changed each command.
3040
3176
  const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
3041
3177
  /** Removed 0.24 verbs → their v2 replacement (workspace model v2, decision 5). Checked before capability dispatch. */
3042
- 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" };
3043
3179
  try {
3044
3180
  // The captured/portable path was removed in 0.26 (lead decisions on (e), D2/D3):
3045
3181
  // its selectors and an inherited captured context are refused, never quietly
@@ -3095,6 +3231,8 @@ else if (cmd === "sync") await syncCmd();
3095
3231
  else if (cmd === "package") await packageCmd();
3096
3232
  else if (cmd === "workspace") await workspaceCmd();
3097
3233
  else if (cmd === "capabilities" || cmd === "souls") await itemsCmd(cmd);
3234
+ else if (cmd === "teams") await teamsCmd();
3235
+ else if (cmd === "soul") await soulCmd();
3098
3236
  else if (cmd === "status") await status();
3099
3237
  else if (cmd === "pane") await paneCmd();
3100
3238
  else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
@@ -3218,6 +3356,9 @@ Usage:
3218
3356
  oats session start --home <absolute-home> start a STOPPED instance again in its existing home
3219
3357
  [--model m] [--launch-config n|none] (recorded recipe as is; a selection re-resolves it
3220
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
3221
3362
  oats session restart --home <abs-home> stop the running harness (SIGTERM, bounded wait,
3222
3363
  [same flags] [--stop-grace <s>] never escalated) and start it again in place under
3223
3364
  the same lock; a stop that is not observed is
@@ -3225,7 +3366,7 @@ Usage:
3225
3366
  [--model <m>] [--json] (same identity, worktree, notes and launch env; no
3226
3367
  spawn hooks); --model replaces the recorded model
3227
3368
  for this and later starts; a live harness is refused
3228
- 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
3229
3370
  [--purpose <slug>] [--repo <r>] = scaffold only); the agent is a workspace
3230
3371
  [--parent <instance>] soul or a capability-defined agent
3231
3372
  [--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
@@ -3233,7 +3374,9 @@ Usage:
3233
3374
  [--relative-root <agents-root>] disambiguates same-named team anchors
3234
3375
  [--work worktree|checkout|attached|workspace|directory] = sugar for --relative-to X --relation
3235
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)
3236
- [--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
3237
3380
  with team: declared, unknown souls
3238
3381
  resolve across the team scope's repos
3239
3382
  directory: owned home/work, config context may
@@ -3297,7 +3440,13 @@ Usage:
3297
3440
  its own repo's souls) + the locked packages
3298
3441
  oats souls [--dir <d>] [--json] every soul of every confirmed member + external souls
3299
3442
  (souls have no private mode), with origin
3300
- (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
3301
3450
  oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
3302
3451
  read-only Git observation of the instance's work
3303
3452
  tree: branch, status (renames kept), ahead/behind