@awebai/oats 0.29.4 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +12 -6
  2. package/bin/oats.mjs +194 -50
  3. package/capabilities/oats-aweb/bin/oats-aweb.mjs +538 -204
  4. package/capabilities/oats-aweb/injects/aweb.md +1 -1
  5. package/capabilities/oats-aweb/lib/binding-wire.mjs +31 -22
  6. package/capabilities/oats-aweb/oats.json +5 -12
  7. package/capabilities/oats-aweb/skills/VENDORED.md +4 -4
  8. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +1 -1
  9. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +83 -13
  10. package/capabilities/oats-code-review/injects/reviewer.md +26 -0
  11. package/capabilities/oats-code-review/oats.json +16 -0
  12. package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +66 -0
  13. package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +30 -0
  14. package/capabilities/oats-code-review/skills/security-review/SKILL.md +56 -0
  15. package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +34 -0
  16. package/capabilities/oats-developer/injects/developer.md +38 -0
  17. package/capabilities/oats-developer/oats.json +17 -0
  18. package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +43 -0
  19. package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +47 -0
  20. package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +65 -0
  21. package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +37 -0
  22. package/capabilities/oats-developer/skills/worktrees/SKILL.md +36 -0
  23. package/capabilities/oats-engineering-expert/injects/expert.md +37 -0
  24. package/capabilities/oats-engineering-expert/oats.json +17 -0
  25. package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +37 -0
  26. package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +52 -0
  27. package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +50 -0
  28. package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +53 -0
  29. package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +49 -0
  30. package/capabilities/oats-okf/bin/oats-okf.mjs +8 -4
  31. package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
  32. package/capabilities/oats-okf/lib/inspection.mjs +26 -7
  33. package/capabilities/oats-okf/lib/sources.mjs +16 -2
  34. package/capabilities/oats-okf/lib/worker.mjs +5 -16
  35. package/capabilities/oats-okf/oats.json +6 -3
  36. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
  37. package/capabilities/oats-okf-harvest/oats.json +3 -3
  38. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
  39. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +2 -2
  40. package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
  41. package/capabilities/oats-okf-maintenance/oats.json +2 -2
  42. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +1 -1
  43. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
  44. package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
  45. package/capabilities/oats-workspace-experts/oats.json +9 -0
  46. package/docs/capabilities.md +160 -171
  47. package/docs/capability-manifest.schema.json +6 -11
  48. package/docs/configuration.md +213 -64
  49. package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
  50. package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
  51. package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
  52. package/docs/design/2026-09-27-team-model-v2.md +97 -117
  53. package/docs/design/2026-09-28-automations-trust.md +38 -0
  54. package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
  55. package/docs/design/HISTORY.md +65 -0
  56. package/docs/design/README.md +23 -54
  57. package/docs/desktop-cli-api.md +1787 -1777
  58. package/docs/desktop.md +30 -91
  59. package/docs/execution-targets.md +146 -292
  60. package/docs/first-team.md +31 -17
  61. package/docs/implementation.md +76 -288
  62. package/docs/integrations.md +118 -320
  63. package/docs/knowledge-capability-authoring.md +25 -52
  64. package/docs/knowledge-reference/acceptance.md +3 -3
  65. package/docs/knowledge-reference/adoption.md +1 -1
  66. package/docs/knowledge-reference/harvester.md +2 -2
  67. package/docs/knowledge-reference/package-craft.md +3 -3
  68. package/docs/knowledge-reference/provider-mapping.md +3 -6
  69. package/docs/knowledge-reference/reader-capture.md +3 -3
  70. package/docs/knowledge-theory.md +62 -166
  71. package/docs/knowledge.md +225 -404
  72. package/docs/layers.md +42 -97
  73. package/docs/oats-local.schema.json +58 -5
  74. package/docs/oats-membership.schema.json +1 -8
  75. package/docs/oats-package.schema.json +5 -5
  76. package/docs/oats-workspace.schema.json +8 -22
  77. package/docs/official-catalog.md +25 -28
  78. package/docs/packages.md +45 -63
  79. package/docs/plans/0.30-close-out.md +61 -0
  80. package/docs/release-lane.md +77 -0
  81. package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
  82. package/docs/release-notes/v0.19.0.md +48 -147
  83. package/docs/release-notes/v0.19.1.md +2 -3
  84. package/docs/release-notes/v0.19.3.md +2 -15
  85. package/docs/release-notes/v0.20.0.md +0 -15
  86. package/docs/release-notes/v0.22.0.md +71 -138
  87. package/docs/release-notes/v0.22.1.md +42 -90
  88. package/docs/release-notes/v0.22.10.md +1 -1
  89. package/docs/release-notes/v0.22.11.md +1 -47
  90. package/docs/release-notes/v0.22.12.md +4 -13
  91. package/docs/release-notes/v0.22.13.md +1 -42
  92. package/docs/release-notes/v0.22.14.md +3 -11
  93. package/docs/release-notes/v0.22.15.md +1 -46
  94. package/docs/release-notes/v0.22.16.md +6 -8
  95. package/docs/release-notes/v0.22.18.md +1 -99
  96. package/docs/release-notes/v0.22.19.md +3 -14
  97. package/docs/release-notes/v0.22.2.md +6 -15
  98. package/docs/release-notes/v0.22.3.md +0 -1
  99. package/docs/release-notes/v0.22.4.md +1 -14
  100. package/docs/release-notes/v0.22.5.md +2 -12
  101. package/docs/release-notes/v0.22.6.md +0 -3
  102. package/docs/release-notes/v0.23.0.md +9 -25
  103. package/docs/release-notes/v0.23.1.md +9 -25
  104. package/docs/release-notes/v0.23.2.md +2 -4
  105. package/docs/release-notes/v0.24.0.md +56 -97
  106. package/docs/release-notes/v0.24.1.md +7 -11
  107. package/docs/release-notes/v0.24.10.md +34 -45
  108. package/docs/release-notes/v0.24.11.md +12 -20
  109. package/docs/release-notes/v0.24.12.md +35 -48
  110. package/docs/release-notes/v0.24.13.md +34 -41
  111. package/docs/release-notes/v0.24.2.md +9 -13
  112. package/docs/release-notes/v0.24.3.md +7 -11
  113. package/docs/release-notes/v0.24.4.md +6 -6
  114. package/docs/release-notes/v0.24.5.md +6 -10
  115. package/docs/release-notes/v0.24.6.md +2 -5
  116. package/docs/release-notes/v0.24.7.md +46 -75
  117. package/docs/release-notes/v0.24.8.md +58 -96
  118. package/docs/release-notes/v0.24.9.md +38 -54
  119. package/docs/release-notes/v0.25.0.md +59 -76
  120. package/docs/release-notes/v0.25.1.md +57 -81
  121. package/docs/release-notes/v0.25.2.md +51 -70
  122. package/docs/release-notes/v0.25.3.md +11 -13
  123. package/docs/release-notes/v0.25.4.md +9 -13
  124. package/docs/release-notes/v0.25.5.md +3 -5
  125. package/docs/release-notes/v0.25.6.md +20 -29
  126. package/docs/release-notes/v0.25.7.md +5 -7
  127. package/docs/release-notes/v0.25.8.md +26 -39
  128. package/docs/release-notes/v0.26.0.md +175 -646
  129. package/docs/release-notes/v0.27.0.md +4 -5
  130. package/docs/release-notes/v0.27.1.md +4 -6
  131. package/docs/release-notes/v0.27.2.md +1 -1
  132. package/docs/release-notes/v0.28.0.md +57 -124
  133. package/docs/release-notes/v0.29.0.md +89 -208
  134. package/docs/release-notes/v0.29.1.md +1 -1
  135. package/docs/release-notes/v0.29.2.md +3 -4
  136. package/docs/release-notes/v0.30.0.md +205 -0
  137. package/docs/schedules.md +280 -363
  138. package/docs/servers.md +99 -117
  139. package/docs/soul.schema.json +2 -9
  140. package/docs/souls-and-instances.md +145 -158
  141. package/docs/workspaces.md +132 -215
  142. package/lib/automations.mjs +21 -6
  143. package/lib/core.mjs +226 -74
  144. package/lib/instance-events.mjs +1 -1
  145. package/lib/instance-inspect.mjs +109 -34
  146. package/lib/instance-lifecycle.mjs +14 -1
  147. package/lib/instance-resolution.mjs +26 -27
  148. package/lib/launch-preference.mjs +87 -0
  149. package/lib/materialize.mjs +3 -3
  150. package/lib/resolve.mjs +29 -87
  151. package/lib/schedule.mjs +1 -1
  152. package/lib/teams-verbs.mjs +195 -0
  153. package/lib/teams.mjs +190 -0
  154. package/lib/triggers.mjs +2 -2
  155. package/lib/workspace.mjs +54 -147
  156. package/package-catalog.json +9 -15
  157. package/package.json +1 -1
  158. package/skills/oats-getting-started/SKILL.md +25 -13
  159. package/capabilities/oats-review/injects/review.md +0 -69
  160. package/capabilities/oats-review/oats.json +0 -10
  161. package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
  162. package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
  163. package/docs/conventions.md +0 -90
  164. package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
  165. package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
  166. package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
  167. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
  168. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
  169. package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
  170. package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
  171. package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
  172. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
  173. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
  174. package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
  175. package/docs/design/2026-09-15-captured-dispatch.md +0 -127
  176. package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
  177. package/docs/design/2026-09-15-package-preparation.md +0 -100
  178. package/docs/design/2026-09-15-portable-data-contract.md +0 -121
  179. package/docs/design/2026-09-15-portable-declarations.md +0 -189
  180. package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
  181. package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
  182. package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
  183. package/docs/design/2026-09-15-source-observation.md +0 -119
  184. package/docs/design/2026-09-16-captured-admission.md +0 -77
  185. package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
  186. package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
  187. package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
  188. package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
  189. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
  190. package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
  191. package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
  192. package/docs/design/2026-09-16-portable-onboarding.md +0 -179
  193. package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
  194. package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
  195. package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
  196. package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
  197. package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
  198. package/docs/design/2026-09-17-captured-native-start.md +0 -58
  199. package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
  200. package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
  201. package/docs/design/2026-09-17-public-captured-start.md +0 -108
  202. package/docs/design/2026-09-17-public-prepare-request.md +0 -90
  203. package/docs/design/2026-09-18-captured-pi-host.md +0 -205
  204. package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
  205. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
  206. package/docs/design/2026-09-20-redesign-program-board.md +0 -142
  207. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
  208. package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
  209. package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
  210. package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
  211. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
  212. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
  213. package/docs/design/2026-09-24-phase-d-plan.md +0 -305
  214. package/docs/design/2026-09-25-teams-contract.md +0 -258
  215. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
  216. package/docs/design/desktop-ux-plan.md +0 -362
  217. package/docs/design/launch-configurations.md +0 -168
  218. package/docs/design/okf-mirror-provenance.md +0 -105
  219. package/docs/design/operations-contract.md +0 -141
  220. package/docs/oats-member.schema.json +0 -38
  221. package/skills/integration-authoring/SKILL.md +0 -84
  222. package/skills/oats-support/SKILL.md +0 -79
  223. package/skills/skill-craft/SKILL.md +0 -109
  224. package/skills/soul-craft/SKILL.md +0 -116
package/lib/workspace.mjs CHANGED
@@ -17,6 +17,7 @@ import { oatsError } from "./errors.mjs";
17
17
  import * as defaultRemote from "./remote.mjs";
18
18
  import { bindRemote, classifyPackageValue, lockedPackageRef } from "./packages.mjs";
19
19
  import { manifestContractProblems } from "./capability-contract.mjs";
20
+ import { BY_TEAM_REMOVED, TEAM_MEMBERSHIP_MOVED } from "./teams.mjs";
20
21
 
21
22
  /* ───────────────────────────── errors ─────────────────────────────────── */
22
23
 
@@ -163,10 +164,10 @@ const ABSOLUTE_PATH = /^(?:\/|\\\\|[A-Za-z]:[\\/])/;
163
164
  /** The workspace file's REF/PATH fields — the only values the absolute-path refusal applies to (contract §2,
164
165
  * Phase B: "absolute paths" means bare filesystem paths as VALUES that stand for a repo or a path inside one;
165
166
  * host state belongs in oats-local.yaml). Free text (`teams.*.description`) and the opaque provider payload
166
- * (`messaging`, incl. `byTeam.*`) are never scanned: a description may mention `/srv`, a provider may
167
+ * (`messaging`) are never scanned: a description may mention `/srv`, a provider may
167
168
  * legitimately carry a socket path or a URL, and neither is a place the kernel resolves.
168
- * → [[path, value]] for members[], packages.*, stores.*, external[].source|soul, defaults.<slot|capabilities>.*.from,
169
- * defaults.byTeam.*.capabilities.*.from */
169
+ * → [[path, value]] for members[], packages.*, stores.*, external[].source|soul, defaults.<slot|capabilities>.*.from
170
+ */
170
171
  function* refFields(value) {
171
172
  if (Array.isArray(value.members)) for (let i = 0; i < value.members.length; i++) if (typeof value.members[i] === "string") yield [`/members/${i}`, value.members[i]];
172
173
  if (isObject(value.packages)) for (const [id, v] of Object.entries(value.packages)) if (typeof v === "string") yield [`/packages/${pointerKey(id)}`, v];
@@ -180,7 +181,6 @@ function* refFields(value) {
180
181
  const d = value.defaults;
181
182
  const froms = function* (caps, path) { if (isObject(caps)) for (const [name, choice] of Object.entries(caps)) if (isObject(choice) && typeof choice.from === "string") yield [`${path}/${pointerKey(name)}/from`, choice.from]; };
182
183
  for (const slot of ["knowledge", "messaging", "tasks", "capabilities"]) yield* froms(d[slot], `/defaults/${slot}`);
183
- if (isObject(d.byTeam)) for (const [label, team] of Object.entries(d.byTeam)) yield* froms(team?.capabilities, `/defaults/byTeam/${pointerKey(label)}/capabilities`);
184
184
  }
185
185
  }
186
186
  function refKey(remote, ref) {
@@ -211,7 +211,6 @@ export function validateWorkspace(value, { remote = defaultRemote } = {}) {
211
211
  if (c.problem) { problems.push({ path: `/packages/${pointerKey(id)}`, message: `${c.problem} (a package is a bare version like v2.1.3 or git:<repo>@<ref>)` }); continue; }
212
212
  if (c.kind === "git") keyOrProblem(remote, c.repo, `/packages/${pointerKey(id)}`, problems);
213
213
  }
214
- const teams = isObject(value.teams) ? Object.keys(value.teams) : [];
215
214
  if (Array.isArray(value.members)) {
216
215
  const seen = new Map();
217
216
  value.members.forEach((ref, i) => {
@@ -223,15 +222,8 @@ export function validateWorkspace(value, { remote = defaultRemote } = {}) {
223
222
  });
224
223
  }
225
224
  if (isObject(value.stores)) for (const [name, ref] of Object.entries(value.stores)) if (typeof ref === "string") keyOrProblem(remote, ref, `/stores/${pointerKey(name)}`, problems);
226
- // Decision 23: messaging.byTeam addresses provider payload by team label — every label must be declared.
227
- // `byTeam` is legal HERE only; nested inside a team's payload it would be a second, unmerged byTeam (M9).
228
- if (isObject(value.messaging) && value.messaging.byTeam !== undefined) {
229
- if (!isObject(value.messaging.byTeam)) problems.push({ path: "/messaging/byTeam", message: "must be an object of { <team label>: <payload> }" });
230
- else for (const label of Object.keys(value.messaging.byTeam)) {
231
- if (!teams.includes(label)) problems.push({ path: `/messaging/byTeam/${pointerKey(label)}`, message: `team ${show(label)} is not declared in teams` });
232
- if (!isObject(value.messaging.byTeam[label])) problems.push({ path: `/messaging/byTeam/${pointerKey(label)}`, message: "must be an object (the payload for that team)" });
233
- else reservedKeyProblems(value.messaging.byTeam[label], `/messaging/byTeam/${pointerKey(label)}`, problems);
234
- }
225
+ if (isObject(value.messaging) && Object.hasOwn(value.messaging, "byTeam")) {
226
+ problems.push({ path: "/messaging/byTeam", reason: "removed-key", message: "messaging.byTeam was removed in 0.30: a team's provider id is teams.<label>.team (committed, shared) or oats-local.yaml teams.<label>.team (local)" });
235
227
  }
236
228
  if (Array.isArray(value.external)) value.external.forEach((entry, i) => {
237
229
  if (!isObject(entry)) return;
@@ -239,23 +231,35 @@ export function validateWorkspace(value, { remote = defaultRemote } = {}) {
239
231
  const at = entry.source.lastIndexOf("@");
240
232
  if (at > 0) keyOrProblem(remote, entry.source.slice(0, at), `/external/${i}/source`, problems);
241
233
  }
242
- if (typeof entry.team === "string" && !teams.includes(entry.team)) problems.push({ path: `/external/${i}/team`, message: `team ${show(entry.team)} is not declared in teams` });
243
234
  });
244
235
  if (isObject(value.defaults)) {
245
236
  const d = value.defaults;
246
237
  for (const slot of ["knowledge", "messaging", "tasks", "capabilities"]) fromProblems(remote, d[slot], `/defaults/${slot}`, problems, { here: false });
247
- if (isObject(d.byTeam)) for (const label of Object.keys(d.byTeam)) {
248
- if (!teams.includes(label)) problems.push({ path: `/defaults/byTeam/${pointerKey(label)}`, message: `team ${show(label)} is not declared in teams` });
249
- fromProblems(remote, d.byTeam[label]?.capabilities, `/defaults/byTeam/${pointerKey(label)}/capabilities`, problems, { here: false });
250
- }
251
238
  }
252
- return problems;
239
+ return withRemovedKeys(problems, value, REMOVED_KEYS.workspace);
240
+ }
241
+ export function validateMembership(value) { return withRemovedKeys(validateAgainst(schemaFor("membership"), value), value, REMOVED_KEYS.membership); }
242
+
243
+ /** Keys 0.30 removed (team model v2): each is a schema problem naming its replacement, in place of
244
+ * the schema's generic "unexpected property". `at(value)` → the JSON pointers where it appears. */
245
+ const REMOVED_KEYS = {
246
+ workspace: [
247
+ { at: (v) => (isObject(v?.defaults) && Object.hasOwn(v.defaults, "byTeam") ? ["/defaults/byTeam"] : []), message: "defaults.byTeam was removed in 0.30: capabilities compose from defaults.capabilities and the soul only" },
248
+ { at: (v) => (Array.isArray(v?.external) ? v.external.flatMap((e, i) => (isObject(e) && Object.hasOwn(e, "team") ? [`/external/${i}/team`] : [])) : []), message: TEAM_MEMBERSHIP_MOVED },
249
+ ],
250
+ membership: [{ at: (v) => (isObject(v) && Object.hasOwn(v, "team") ? ["/team"] : []), message: TEAM_MEMBERSHIP_MOVED }],
251
+ soul: [{ at: (v) => (isObject(v) && Object.hasOwn(v, "team") ? ["/team"] : []), message: TEAM_MEMBERSHIP_MOVED }],
252
+ };
253
+ function withRemovedKeys(problems, value, removed) {
254
+ const found = removed.flatMap((r) => r.at(value).map((path) => ({ path, reason: "removed-key", message: r.message })));
255
+ if (!found.length) return problems;
256
+ const paths = new Set(found.map((p) => p.path));
257
+ return [...problems.filter((p) => !paths.has(p.path)), ...found];
253
258
  }
254
- export function validateMembership(value) { return validateAgainst(schemaFor("membership"), value); }
255
259
  /** Schema + domain problems for a soul.yaml value: `from:` values must be canonical (see fromProblems);
256
- * slot payloads may not carry the reserved `byTeam` key (see reservedKeyProblems). */
260
+ * slot payloads may not carry the removed `byTeam` key (see reservedKeyProblems). */
257
261
  export function validateSoul(value, { remote = defaultRemote } = {}) {
258
- const problems = validateAgainst(schemaFor("soul"), value);
262
+ const problems = withRemovedKeys(validateAgainst(schemaFor("soul"), value), value, REMOVED_KEYS.soul);
259
263
  if (isObject(value) && value.schemaVersion === 2) {
260
264
  fromProblems(remote, value.capabilities, "/capabilities", problems, { here: true });
261
265
  for (const slot of ["knowledge", "messaging", "tasks"]) if (isObject(value[slot])) reservedKeyProblems(value[slot], `/${slot}`, problems);
@@ -263,15 +267,14 @@ export function validateSoul(value, { remote = defaultRemote } = {}) {
263
267
  return problems;
264
268
  }
265
269
 
266
- /** `byTeam` is RESERVED (decision 23): it addresses a per-team payload and is legal only at the top level
267
- * of workspace.messaging, where resolve merges base ⊕ byTeam[soul.team] and strips it. Anywhere else — a
268
- * soul's slot payload, oats-local.yaml settings[cap], a spawn provider, or nested inside a team's own
269
- * payload — it would reach the provider verbatim (or shadow the real one); refused as a schema problem
270
- * with `reason: "reserved-key"`. Only the payload's TOP level is checked: deeper keys are the provider's. */
271
- const RESERVED_PAYLOAD_KEY = "byTeam";
270
+ /** `byTeam` was removed in 0.30 (team model v2): a per-team payload no longer exists — a team's
271
+ * provider id is teams.<label>.team. At the top level of any payload layer (a soul's slot payload,
272
+ * oats-local.yaml settings[cap], a spawn provider) it would reach the provider verbatim as a dead key;
273
+ * refused as a schema problem with `reason: "removed-key"`. Deeper keys are the provider's. */
274
+ const REMOVED_PAYLOAD_KEY = "byTeam";
272
275
  function reservedKeyProblems(payload, path, problems) {
273
276
  if (!isObject(payload)) return;
274
- if (Object.hasOwn(payload, RESERVED_PAYLOAD_KEY)) problems.push({ path: `${path}/${RESERVED_PAYLOAD_KEY}`, reason: "reserved-key", message: `${show(RESERVED_PAYLOAD_KEY)} is reserved: it is legal only at the top level of the workspace file's messaging: payload (decision 23)` });
277
+ if (Object.hasOwn(payload, REMOVED_PAYLOAD_KEY)) problems.push({ path: `${path}/${REMOVED_PAYLOAD_KEY}`, reason: "removed-key", message: BY_TEAM_REMOVED });
275
278
  }
276
279
 
277
280
  /** `from:` must be exactly what the resolver compares against — "package", "here" (souls only) or a CANONICAL
@@ -290,7 +293,7 @@ function fromProblems(remote, capabilities, path, problems, { here }) {
290
293
  if (key !== from) problems.push({ path: at, message: `${show(from)} is not a canonical repo key (expected <host>/<path> as parseRepoRef(...).key${key ? `, e.g. ${show(key)}` : ""}); \`here\`, \`package\` or a key are the only forms` });
291
294
  }
292
295
  }
293
- /** Schema + domain problems for an oats-local.yaml value: settings[<cap>] may not carry the reserved `byTeam` key. */
296
+ /** Schema + domain problems for an oats-local.yaml value: settings[<cap>] may not carry the removed `byTeam` key. */
294
297
  export function validateLocal(value) {
295
298
  const problems = validateAgainst(schemaFor("local"), value);
296
299
  if (isObject(value) && isObject(value.settings)) for (const [cap, payload] of Object.entries(value.settings)) reservedKeyProblems(payload, `/settings/${pointerKey(cap)}`, problems);
@@ -345,7 +348,7 @@ function schemaError(kind, origin, problems, value) {
345
348
 
346
349
  /** What a 0.25 `oats-config.yaml` held, and where each piece lives now (0.26.0 removed the file). */
347
350
  export const LEGACY_CONFIG_MIGRATION = "oats-config.yaml is no longer read (removed in 0.26.0): launch-configs moved to the deployment's oats-local.yaml; "
348
- + "capability bindings, layers and team moved to oats-workspace.yaml (defaults, teams) and each soul's soul.yaml in its member repository; "
351
+ + "capability bindings and layers moved to oats-workspace.yaml (defaults) and each soul's soul.yaml in its member repository, and team membership to the deployment's oats-local.yaml (`oats teams`, `oats soul teams`); "
349
352
  + "agents-md-injection, skill-overrides, work-modes and yolo are removed (yolo is --yolo or a launch configuration's yolo). Move what you still need, then delete the file";
350
353
 
351
354
  /** Walk up from dir to find oats-local.yaml → { path, local } | E_LOCAL_MISSING.
@@ -392,52 +395,11 @@ export async function observeWorkspace(ref, { at, remote: injected, remoteOption
392
395
  return { key: obs.key, url: obs.url, ref, commit: obs.commit, workspace: read.value, observedAt: obs.observedAt };
393
396
  }
394
397
 
395
- /**
396
- * A soul's team labels as its repository declares them NOW, without a workspace discovery (teams
397
- * contract decision 6, the live teams of an existing home): the workspace host has already been read
398
- * (`wsObs`, observeWorkspace); this reads the soul's own repo once — a member at its current commit
399
- * (`souls/<name>/soul.yaml`, and `oats-membership.yaml` only when the soul declares no `team`), or an
400
- * `external[]` soul at its pinned commit (its entry's `team` wins, as in discovery).
401
- * → labels (primary first), or null when the workspace no longer lists the soul's repo.
402
- * Membership is not re-confirmed here: the home's soul was confirmed at spawn; this reads labels only.
403
- */
404
- export async function observeSoulLabels(wsObs, { name, repoKey }, { remote: injected, remoteOptions } = {}) {
405
- const remote = remoteOf({ remote: injected, remoteOptions });
406
- const workspace = wsObs.workspace;
407
- const readSoul = async (ref, commit, path) => {
408
- const file = `${path.replace(/\/+$/, "")}/soul.yaml`;
409
- const { bytes } = await remote.readRemoteFile(ref, commit, file);
410
- const read = readDeclaration("soul", bytes, { kind: "soul", repoKey, commit, path: file });
411
- if (read.problems) throw schemaError("soul", { kind: "soul", repoKey, commit, path: file }, read.problems, read.value);
412
- return read.value;
413
- };
414
- for (const entry of workspace.external || []) {
415
- const pin = entry.source.lastIndexOf("@");
416
- let key; try { key = refKey(remote, entry.source.slice(0, pin)); } catch { continue; }
417
- if (key !== repoKey) continue;
418
- if (typeof entry.team === "string") return [entry.team];
419
- const soul = await readSoul(entry.source.slice(0, pin), entry.source.slice(pin + 1), entry.soul);
420
- if (soul.name !== name) continue;
421
- return labelList(soul.team) ?? [];
422
- }
423
- const memberRef = (workspace.members || []).find((ref) => { try { return refKey(remote, ref) === repoKey; } catch { return false; } });
424
- if (!memberRef) return null;
425
- const obs = await remote.observeRemote(memberRef);
426
- const soul = await readSoul(memberRef, obs.commit, `souls/${name}`);
427
- const own = labelList(soul.team);
428
- if (own) return own;
429
- try {
430
- const { bytes } = await remote.readRemoteFile(memberRef, obs.commit, "oats-membership.yaml");
431
- const read = readDeclaration("membership", bytes, { kind: "membership", repoKey, commit: obs.commit, path: "oats-membership.yaml" });
432
- return read.problems ? [] : labelList(read.value.team) ?? [];
433
- } catch (e) { if (e?.code === "E_REMOTE_PATH_MISSING") return []; throw e; }
434
- }
435
-
436
398
  const unconfirmed = (key, reason, detail, extra = {}) => ({ key, confirmed: false, reason, detail, ...extra });
437
399
 
438
400
  /**
439
401
  * Read the member's oats-membership.yaml at its default branch in the same access context.
440
- * → { key, commit, confirmed: true, team } | { key, confirmed: false, reason, detail }
402
+ * → { key, commit, confirmed: true } | { key, confirmed: false, reason, detail }
441
403
  */
442
404
  export async function confirmMembership(workspaceObs, memberRef, { remote: injected, remoteOptions } = {}) {
443
405
  const remote = remoteOf({ remote: injected, remoteOptions });
@@ -481,35 +443,24 @@ export async function confirmMembership(workspaceObs, memberRef, { remote: injec
481
443
  const caseOnly = backlinkKey.toLowerCase() === workspaceObs.key.toLowerCase();
482
444
  return unconfirmed(key, "backlink-elsewhere", `${key}@${obs.commit.slice(0, 12)} names workspace ${backlinkKey}, not ${workspaceObs.key}${caseOnly ? " (the keys differ only by letter case: repo paths are case-sensitive identities; spell the workspace ref exactly as the workspace lists itself)" : ""}`, { commit: obs.commit, backlink: backlinkKey, ...(caseOnly ? { caseOnly: true } : {}) });
483
445
  }
484
- const labels = labelList(read.value.team) ?? [];
485
- return { key, commit: obs.commit, confirmed: true, team: labels[0] ?? null, labels };
446
+ return { key, commit: obs.commit, confirmed: true };
486
447
  }
487
448
 
488
449
  /* ───────────────────────────── enumeration ────────────────────────────── */
489
450
 
490
451
  const SOUL_FILE = /^([^/]+)\/soul\.yaml$/;
491
452
  const CAP_FILE = /^([^/]+)\/oats\.json$/;
492
- /** A declared `team` as a label list (teams contract 2026-09-25: a label or a non-empty list of distinct
493
- * labels, the first the primary); null when the item declares none. */
494
- const labelList = (team) => (typeof team === "string" ? [team] : Array.isArray(team) ? team.filter((l) => typeof l === "string") : null);
495
- /** The item's labels, else the repo default's (oats-membership.yaml). */
496
- const labelsOf = (item, fallback) => labelList(item?.team) ?? [...(fallback || [])];
497
- /** Each label with the path it is declared at: `/team` for one label, `/team/<i>` in a list. */
498
- const labelPaths = (item, labels, file) => labels.map((label, i) => [label, Array.isArray(item?.team) ? `${file}#/team/${i}` : `${file}#/team`]);
499
453
 
500
454
  /**
501
455
  * Enumerate souls/*\/soul.yaml and capabilities/*\/oats.json of one repo at one commit.
502
456
  * Validates each item; collects problems instead of aborting.
503
457
  * → { souls: [SoulEntry], capabilities: [CapEntry], problems: [{ code, path, message, repoKey }] }
504
458
  */
505
- async function enumerateRepo(remote, ref, key, commit, { defaultLabels = [], teams = null } = {}) {
459
+ async function enumerateRepo(remote, ref, key, commit) {
506
460
  const souls = [];
507
461
  const capabilities = [];
508
462
  const problems = [];
509
463
  const problem = (code, path, message) => problems.push({ code, repoKey: key, path, message });
510
- const checkTeam = (team, path) => {
511
- if (team !== null && teams && !teams.includes(team)) problem("E_TEAM_UNKNOWN", path, `team ${show(team)} is not declared in the workspace's teams (${teams.join(", ") || "none"})`);
512
- };
513
464
  // A listing failure on one directory is a problem of THIS repo, never an abort of the whole picture.
514
465
  const list = async (dir) => {
515
466
  try { return await remote.listRemoteTree(ref, commit, dir, { depth: 2 }); }
@@ -533,10 +484,8 @@ async function enumerateRepo(remote, ref, key, commit, { defaultLabels = [], tea
533
484
  if (definition.name !== m[1]) problem("E_WORKSPACE_SCHEMA", `${file}#/name`, `soul name ${show(definition.name)} does not match its directory ${show(m[1])}`);
534
485
  if (soulNames.has(definition.name)) { problem("E_WORKSPACE_SCHEMA", `${file}#/name`, `soul name ${show(definition.name)} is already declared by ${soulNames.get(definition.name)}; the second declaration is not listed`); continue; }
535
486
  soulNames.set(definition.name, file);
536
- const labels = labelsOf(definition, defaultLabels);
537
- for (const [label, at] of labelPaths(definition, labels, file)) checkTeam(label, at);
538
487
  // Souls have no private mode since 0.26.0: `private` is accepted, warned (soul-private-ignored) and never hides a soul.
539
- souls.push({ name: definition.name, path, repoKey: key, commit, team: labels[0] ?? null, labels, private: false, definition });
488
+ souls.push({ name: definition.name, path, repoKey: key, commit, private: false, definition });
540
489
  }
541
490
  const capNames = new Map();
542
491
  for (const entry of await list("capabilities")) {
@@ -559,7 +508,6 @@ async function enumerateRepo(remote, ref, key, commit, { defaultLabels = [], tea
559
508
  capability: { type: "string", pattern: "^[a-z0-9][a-z0-9._-]*$" },
560
509
  version: { type: "string", minLength: 1 },
561
510
  private: { type: "boolean" },
562
- team: { type: "string", pattern: "^[a-z0-9][a-z0-9._-]*$" },
563
511
  layer: { enum: ["knowledge", "messaging", "tasks"] },
564
512
  },
565
513
  }, manifest);
@@ -569,10 +517,7 @@ async function enumerateRepo(remote, ref, key, commit, { defaultLabels = [], tea
569
517
  if (contract.length) { for (const p of contract) problem("E_WORKSPACE_SCHEMA", `${file}#${p.pointer}`, p.message); continue; }
570
518
  if (capNames.has(manifest.capability)) { problem("E_WORKSPACE_SCHEMA", `${file}#/capability`, `capability ${show(manifest.capability)} is already declared by ${capNames.get(manifest.capability)}; the second declaration is not listed`); continue; }
571
519
  capNames.set(manifest.capability, file);
572
- // A capability is LISTED under one team (its own `team:`, else the repo default's primary); it joins none.
573
- const team = typeof manifest.team === "string" ? manifest.team : defaultLabels[0] ?? null;
574
- checkTeam(team, `${file}#/team`);
575
- capabilities.push({ name: manifest.capability, path, repoKey: key, commit, team, private: manifest.private === true, manifest });
520
+ capabilities.push({ name: manifest.capability, path, repoKey: key, commit, private: manifest.private === true, manifest });
576
521
  }
577
522
  // Non-collapse rule: a member's oats-package/ is REPORTED (publishes), never enumerated as member capabilities.
578
523
  let publishes = null;
@@ -608,7 +553,7 @@ export async function discoverRepo(ref, { at, remote: injected, remoteOptions }
608
553
  } catch (e) {
609
554
  if (e?.code !== "E_REMOTE_PATH_MISSING") throw e;
610
555
  }
611
- const items = await enumerateRepo(remote, ref, obs.key, obs.commit, { defaultLabels: labelList(membership?.team) ?? [], teams: null });
556
+ const items = await enumerateRepo(remote, ref, obs.key, obs.commit);
612
557
  return { key: obs.key, commit: obs.commit, membership, ...items, problems: [...problems, ...items.problems] };
613
558
  }
614
559
 
@@ -622,8 +567,8 @@ export function packageSoulAgentName(id, soul) { return `${String(id).replace(/[
622
567
  /**
623
568
  * Package souls (0.28.0): the souls the lock records for each package the workspace STILL declares,
624
569
  * read at the locked commit. A package soul is listed like a member soul, with its package origin
625
- * (`package`, `version`) and its qualified name `<package>/<soul>`; its labels are its soul.yaml's
626
- * own `team` (a package has no membership default) and each must be declared (E_TEAM_UNKNOWN).
570
+ * (`package`, `version`) and its qualified name `<package>/<soul>`. A package soul.yaml still carrying
571
+ * `team:` (removed in 0.30: team membership is local) is invalid and not listed.
627
572
  * Nothing here trusts the lock's digests — a spawn verifies the fetched soul against them.
628
573
  * `lock` null (or no workspace) → nothing. → { souls: [SoulEntry], problems }
629
574
  */
@@ -632,7 +577,6 @@ export async function discoverPackageSouls(workspace, lock, { remote: injected,
632
577
  if (!isObject(lock?.packages) || !isObject(workspace)) return { souls, problems };
633
578
  const remote = remoteOf({ remote: injected, remoteOptions });
634
579
  const declared = isObject(workspace.packages) ? workspace.packages : {};
635
- const teams = isObject(workspace.teams) ? Object.keys(workspace.teams) : [];
636
580
  for (const id of Object.keys(lock.packages).sort()) {
637
581
  const entry = lock.packages[id];
638
582
  if (!Array.isArray(entry?.souls) || entry.souls.length === 0 || !Object.hasOwn(declared, id)) continue;
@@ -656,12 +600,8 @@ export async function discoverPackageSouls(workspace, lock, { remote: injected,
656
600
  if (read.problems) { for (const p of read.problems) problem("E_WORKSPACE_SCHEMA", `${file}#${p.path}`, `${p.message}${schemaHint("soul", read.value)}`); continue; }
657
601
  const definition = read.value;
658
602
  if (definition.name !== locked.name) { problem("E_WORKSPACE_SCHEMA", `${file}#/name`, `soul name ${show(definition.name)} does not match its directory ${show(locked.name)}`); continue; }
659
- const labels = labelList(definition.team) ?? [];
660
- for (const [label, where] of labelPaths(definition, labels, file)) {
661
- if (!teams.includes(label)) problems.push({ code: "E_TEAM_UNKNOWN", repoKey: null, package: id, path: at(where), message: `team ${show(label)} is not declared in the workspace's teams (${teams.join(", ") || "none"}) — declare it in oats-workspace.yaml teams: to spawn ${id}/${locked.name}` });
662
- }
663
603
  souls.push({
664
- name: locked.name, path, repoKey: key, ref, commit: entry.commit, team: labels[0] ?? null, labels, private: false, definition,
604
+ name: locked.name, path, repoKey: key, ref, commit: entry.commit, private: false, definition,
665
605
  package: id, version: entry.version, qualifiedName: `${id}/${locked.name}`, agentName: packageSoulAgentName(id, locked.name), digest: locked.digest,
666
606
  });
667
607
  }
@@ -683,7 +623,7 @@ export async function discoverPackageSouls(workspace, lock, { remote: injected,
683
623
 
684
624
  /**
685
625
  * The whole picture in one access context.
686
- * → { workspace, members: [{ key, commit, confirmed, reason?, team, souls, capabilities, publishes }], external: [...],
626
+ * → { workspace, members: [{ key, commit, confirmed, reason?, souls, capabilities, publishes }], external: [...],
687
627
  * problems: [{ code, path, message, repoKey? }] }
688
628
  * `publishes` = { package, version } when the member carries oats-package/oats-package.json (informational:
689
629
  * the package's capabilities are NOT member capabilities — non-collapse rule), else null.
@@ -692,12 +632,11 @@ export async function discoverWorkspace(ref, { at, local, lock = null, remote: i
692
632
  const remote = remoteOf({ remote: injected, remoteOptions });
693
633
  const wsObs = await observeWorkspace(ref, { at, remote });
694
634
  const workspace = wsObs.workspace;
695
- const teams = isObject(workspace.teams) ? Object.keys(workspace.teams) : [];
696
635
  const problems = [];
697
636
  const members = [];
698
637
  for (const memberRef of workspace.members || []) {
699
638
  const confirmation = await confirmMembership(wsObs, memberRef, { remote });
700
- const row = { key: confirmation.key, ref: memberRef, commit: confirmation.commit ?? null, confirmed: confirmation.confirmed, team: confirmation.team ?? null, labels: confirmation.labels ?? [], souls: [], capabilities: [], publishes: null };
639
+ const row = { key: confirmation.key, ref: memberRef, commit: confirmation.commit ?? null, confirmed: confirmation.confirmed, souls: [], capabilities: [], publishes: null };
701
640
  if (!confirmation.confirmed) {
702
641
  // Contract: an unconfirmed member contributes nothing but its row.
703
642
  row.reason = confirmation.reason;
@@ -705,10 +644,7 @@ export async function discoverWorkspace(ref, { at, local, lock = null, remote: i
705
644
  members.push(row);
706
645
  continue;
707
646
  }
708
- for (const [i, label] of row.labels.entries()) {
709
- if (!teams.includes(label)) problems.push({ code: "E_TEAM_UNKNOWN", repoKey: row.key, path: row.labels.length > 1 ? `oats-membership.yaml#/team/${i}` : "oats-membership.yaml#/team", message: `team ${show(label)} is not declared in the workspace's teams (${teams.join(", ") || "none"})` });
710
- }
711
- const items = await enumerateRepo(remote, memberRef, row.key, row.commit, { defaultLabels: row.labels, teams });
647
+ const items = await enumerateRepo(remote, memberRef, row.key, row.commit);
712
648
  row.souls = items.souls;
713
649
  row.capabilities = items.capabilities;
714
650
  row.publishes = items.publishes;
@@ -735,47 +671,18 @@ export async function discoverWorkspace(ref, { at, local, lock = null, remote: i
735
671
  }
736
672
  const read = readDeclaration("soul", bytes, { kind: "soul", repoKey: key, commit, path: file });
737
673
  if (read.problems) { for (const p of read.problems) pushProblem("E_WORKSPACE_SCHEMA", `${file}#${p.path}`, `${p.message}${schemaHint("soul", read.value)}`); continue; }
738
- // The workspace's `external[].team` (one label) overrides the soul's own labels.
739
- const labels = labelList(entry.team) ?? labelList(read.value.team) ?? [];
740
- for (const label of labels) if (!teams.includes(label)) pushProblem("E_TEAM_UNKNOWN", `${file}#/team`, `team ${show(label)} is not declared in the workspace's teams`);
741
- externalRows.push({ source: entry.source, key, commit, soul: { name: read.value.name, path: entry.soul, repoKey: key, commit, team: labels[0] ?? null, labels, private: false, definition: read.value } });
674
+ externalRows.push({ source: entry.source, key, commit, soul: { name: read.value.name, path: entry.soul, repoKey: key, commit, private: false, definition: read.value } });
742
675
  }
743
676
  const pkg = await discoverPackageSouls(workspace, lock, { remote });
744
677
  problems.push(...pkg.problems);
745
- return { workspace, key: wsObs.key, url: wsObs.url, commit: wsObs.commit, observedAt: wsObs.observedAt, local: local ?? null, members, external: externalRows, packageSouls: pkg.souls, problems, warnings: workspaceWarnings(workspace, members, externalRows, pkg.souls, teams) };
746
- }
747
-
748
- /** The discovery's warnings: unmapped team labels and ignored `private:`, over every listed soul. */
749
- export function workspaceWarnings(workspace, members, externalRows, packageSouls, teams = isObject(workspace?.teams) ? Object.keys(workspace.teams) : []) {
750
- const extra = (packageSouls || []).map((soul) => ({ soul }));
751
- return [...unmappedLabelWarnings(workspace, members, [...externalRows, ...extra], teams), ...privateSoulWarnings([...members.filter((m) => m.confirmed).flatMap((m) => m.souls), ...externalRows.map((e) => e.soul), ...(packageSouls || [])])];
678
+ return { workspace, key: wsObs.key, url: wsObs.url, commit: wsObs.commit, observedAt: wsObs.observedAt, local: local ?? null, members, external: externalRows, packageSouls: pkg.souls, problems, warnings: workspaceWarnings(workspace, members, externalRows, pkg.souls) };
752
679
  }
753
680
 
754
- /** Teams contract decision 5: a soul label the workspace declares in `teams:` but does not map in
755
- * `messaging.byTeam` is a WARNING, not a problem — the label stays eligible-but-unmapped and the
756
- * messaging provider falls back to the workspace's default team for it. (An undeclared label is E_TEAM_UNKNOWN.)
757
- * ONE warning per unmapped label, naming its souls (a workspace on its default team alone would otherwise print
758
- * a line per soul): { code, label, souls, paths, message }, sorted by label. */
681
+ /** The discovery's warnings: ignored `private:`, over every listed soul. (0.30 removed the
682
+ * `unmapped-team-label` warning: an unmapped shared team is the readiness item `team-unmapped`.) */
759
683
  const byCodepointOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
760
- function unmappedLabelWarnings(workspace, members, externalRows, teams) {
761
- const byTeam = isObject(workspace?.messaging) && isObject(workspace.messaging.byTeam) ? workspace.messaging.byTeam : {};
762
- const byLabel = new Map();
763
- const souls = [...members.filter((m) => m.confirmed).flatMap((m) => m.souls), ...externalRows.map((e) => e.soul)];
764
- for (const soul of souls) {
765
- for (const label of soul.labels || []) {
766
- if (!teams.includes(label) || Object.hasOwn(byTeam, label)) continue;
767
- const row = byLabel.get(label) || { souls: [], paths: [] };
768
- row.souls.push(soul.name); row.paths.push(`${soul.repoKey}:${soul.path}/soul.yaml#/team`);
769
- byLabel.set(label, row);
770
- }
771
- }
772
- return [...byLabel.keys()].sort(byCodepointOrder).map((label) => {
773
- const { souls: names, paths } = byLabel.get(label);
774
- const order = names.map((n, i) => i).sort((x, y) => byCodepointOrder(names[x], names[y]) || byCodepointOrder(paths[x], paths[y]));
775
- const sorted = order.map((i) => names[i]);
776
- return { code: "unmapped-team-label", label, souls: sorted, paths: order.map((i) => paths[i]),
777
- message: `team ${show(label)} has no messaging.byTeam entry; its souls (${sorted.join(", ")}) fall back to the workspace's default team for it` };
778
- });
684
+ export function workspaceWarnings(workspace, members, externalRows, packageSouls) {
685
+ return privateSoulWarnings([...members.filter((m) => m.confirmed).flatMap((m) => m.souls), ...externalRows.map((e) => e.soul), ...(packageSouls || [])]);
779
686
  }
780
687
 
781
688
  /** `private:` in a soul.yaml has no effect since 0.26.0 (human decision 2026-09-25): every soul of a
@@ -837,7 +744,7 @@ export function standaloneRepo(ref, commit, discovery, { remote: injected } = {}
837
744
  });
838
745
  return {
839
746
  standalone: true, key, commit: source.commit ?? commit ?? null, workspace: null,
840
- members: [{ key, commit: source.commit ?? commit ?? null, confirmed: false, reason: "cannot-read", detail: `workspace of ${key} cannot be read; standalone view`, team: source.team ?? labelList(source.membership?.team)?.[0] ?? null, souls, capabilities, publishes: source.publishes ?? null }],
747
+ members: [{ key, commit: source.commit ?? commit ?? null, confirmed: false, reason: "cannot-read", detail: `workspace of ${key} cannot be read; standalone view`, souls, capabilities, publishes: source.publishes ?? null }],
841
748
  external: [], problems, warnings: privateSoulWarnings(souls),
842
749
  };
843
750
  }
@@ -3,12 +3,12 @@
3
3
  "packages": {
4
4
  "oats.okf": {
5
5
  "url": "https://github.com/awebai/oats-okf.git",
6
- "ref": "v4.0.1",
6
+ "ref": "v4.0.4",
7
7
  "path": "oats-package"
8
8
  },
9
9
  "oats.aweb": {
10
10
  "url": "https://github.com/awebai/oats-aweb.git",
11
- "ref": "v1.16.0",
11
+ "ref": "v1.17.1",
12
12
  "path": "oats-package"
13
13
  },
14
14
  "oats.jira": {
@@ -26,19 +26,18 @@
26
26
  "ref": "v1.0.3",
27
27
  "path": "oats-package"
28
28
  },
29
- "oats.dev": {
30
- "url": "https://github.com/awebai/oats-dev.git",
29
+ "oats.engineering": {
30
+ "url": "https://github.com/awebai/oats-engineering.git",
31
31
  "ref": "v1.1.0",
32
32
  "path": "oats-package"
33
33
  },
34
34
  "oats.framework": {
35
35
  "url": "https://github.com/awebai/oats.git",
36
- "ref": "oats-framework/v1.3.2",
36
+ "ref": "oats-framework/v1.4.0",
37
37
  "path": "oats-package"
38
38
  }
39
39
  },
40
40
  "capabilities": {
41
- "oats.review": "oats.dev",
42
41
  "oas.okf": {
43
42
  "package": "oats.okf",
44
43
  "capability": "oats.okf"
@@ -59,18 +58,13 @@
59
58
  "package": "oats.authoring",
60
59
  "capability": "oats.authoring"
61
60
  },
62
- "oas.dev": {
63
- "package": "oats.dev",
64
- "capability": "oats.dev"
65
- },
66
- "oas.review": {
67
- "package": "oats.dev",
68
- "capability": "oats.review"
69
- },
70
61
  "oats.knowledge-theory": "oats.framework",
71
62
  "oats.core": "oats.framework",
72
63
  "oats.setup": "oats.framework",
73
64
  "oats.okf-harvest": "oats.okf",
74
- "oats.okf-maintenance": "oats.okf"
65
+ "oats.okf-maintenance": "oats.okf",
66
+ "oats.engineering-expert": "oats.engineering",
67
+ "oats.developer": "oats.engineering",
68
+ "oats.code-review": "oats.engineering"
75
69
  }
76
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.29.4",
3
+ "version": "0.30.0",
4
4
  "description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
5
5
  "keywords": [
6
6
  "agents",
@@ -60,10 +60,8 @@ members:
60
60
  - git:github.com/acme/agents # the host is a member too
61
61
  - git:github.com/acme/platform
62
62
  packages:
63
- oats.framework: v1.1.3 # bare versions resolve through the official catalog
64
- oats.okf: v2.1.5
65
- teams:
66
- global: { description: Org-wide souls }
63
+ oats.framework: v1.4.0 # bare versions resolve through the official catalog
64
+ oats.okf: v4.0.4
67
65
  defaults:
68
66
  capabilities: { oats.core: { from: package } }
69
67
  knowledge: { oats.okf: { from: package } }
@@ -72,7 +70,7 @@ defaults:
72
70
  Take the current package versions from the official catalog
73
71
  (`package-catalog.json` in the OATS repository); ask which slots the user wants
74
72
  filled (knowledge, messaging, tasks) instead of copying the example. No absolute
75
- paths, accounts or team ids go in this file.
73
+ paths or accounts go in this file. Teams come with messaging (step 6).
76
74
 
77
75
  Declaring a package in `packages:` is the decision to trust it: its commands
78
76
  and hooks run on every machine that spawns a soul using it. Show the user what
@@ -84,7 +82,6 @@ In **every** member repository, including the host, `oats-membership.yaml`:
84
82
  ```yaml
85
83
  schemaVersion: 2
86
84
  workspace: git:github.com/acme/agents
87
- team: global
88
85
  ```
89
86
 
90
87
  Membership is reciprocal: the workspace lists the repository and the
@@ -135,20 +132,35 @@ each package to an exact commit and integrity, and content that no longer
135
132
  matches is refused (`E_PACKAGE_INTEGRITY`). Member capabilities come from
136
133
  membership.
137
134
 
138
- ## 6. Spawn the first soul
135
+ ## 6. Give the deployment a team (with messaging)
136
+
137
+ If a messaging capability fills the messaging slot, every instance lives in a
138
+ team, and readiness fails with `E_TEAM_UNCONFIGURED` until this deployment has
139
+ a default team. Create the team with the messaging provider (its own skills say
140
+ how), then record it:
141
+
142
+ ```bash
143
+ oats teams add research --team <provider team id> # a local team; the first one becomes the default
144
+ oats teams
145
+ ```
146
+
147
+ A team the whole organisation uses is committed in `oats-workspace.yaml` as
148
+ `teams.<label>` with its provider id; which souls join which team on this
149
+ machine is `oats soul teams` (`/oats-teams`, in `oats.setup`).
150
+
151
+ ## 7. Spawn the first soul
139
152
 
140
153
  A soul with `work: worktree | checkout` needs a clone of its repository at
141
154
  `<dir>/<repo name>` (or named in `oats-local.yaml` `clones:`).
142
155
 
143
156
  ```bash
144
- oats souls --dir <dir> # what the workspace offers, with origin and team
157
+ oats souls --dir <dir> # what the workspace offers, with origin and teams
145
158
  oats spawn backend-expert --preview # modules, commits, merged provider settings — nothing created
146
159
  oats spawn backend-expert --task "First concrete task"
147
160
  oats status
148
161
  ```
149
162
 
150
- Create and spawn only when asked. After the first spawn, load the `oats.setup`
151
- skills for the rest of the deployment (messaging, more souls, rebuilds). For
152
- custom capabilities and integrations, use `integration-authoring`; for deep
153
- architecture questions or bugs, use `oats-support`. The model in full is
154
- `docs/workspaces.md` in the OATS repository.
163
+ Create and spawn only when asked. After the first spawn, the `oats.setup`
164
+ skills carry the rest of the deployment (messaging, more souls, rebuilds); the
165
+ `oats.authoring` package teaches writing capabilities, skills and souls. The
166
+ model in full is `docs/workspaces.md` in the OATS repository.
@@ -1,69 +0,0 @@
1
- ## Review discipline: oats.review
2
-
3
- **After every substantive commit, launch the reviewer** — and, when a knowledge
4
- layer is active, run its promotion step alongside (whatever command that layer
5
- documents; omit the line entirely if you have no knowledge layer):
6
-
7
- ```bash
8
- <your knowledge layer's promotion command> # omit when no knowledge layer is active
9
- oats spawn reviewer --work attached --work-dir "$PWD/work" \
10
- --purpose "<short-sha>" \
11
- --task "Review commit <sha> on branch <branch>. Report to <your-instance> per your operating loop."
12
- ```
13
-
14
- - `reviewer` is the oats.dev package's soul (`oats.dev/reviewer`).
15
- `--purpose "<short-sha>"` gives it a unique, commit-relevant instance name
16
- (`oats-dev-reviewer-<short-sha>`); attached mode shares your work tree
17
- and automatically makes the reviewer your child (attached agents are always
18
- children of the work-tree owner — no relation flags needed or allowed).
19
- - The reviewer reviews **that commit's diff only** and reports its verdict
20
- (`APPROVE` / `APPROVE WITH NITS` / `NEEDS CHANGES`) back to you the way your
21
- deployment delivers messages: over your messaging layer when one is active,
22
- and otherwise in its own session transcript, which is where you read it.
23
- When a messaging layer IS active, do not wait actively — finish your turn and
24
- go idle; the layer wakes you when the verdict arrives. `NEEDS CHANGES` means
25
- fix, commit, and re-review before the work is ready.
26
- - Do not review your own commits in its place — the point is eyes that are
27
- not yours.
28
- - Skip only for trivial mechanical commits (typo, lockfile refresh) — when
29
- in doubt, review.
30
-
31
- ## Delivery discipline (all OATS developers)
32
-
33
- - You work in a dedicated worktree on your own branch. **Main only moves
34
- through PRs** — never push to main.
35
- - **Single-developer features**: branch from main (`agents/<instance>` or as
36
- tasked), open the PR yourself (`gh pr create`) when review-clean, then
37
- **spawn a fresh maintainer instance for it** — one per PR, always, even if
38
- another oats-expert instance is live:
39
-
40
- ```bash
41
- oats spawn oats-expert --purpose "pr<n>" --relation parent --relative-to "$OATS_INSTANCE" \
42
- --task "Maintainer review of PR #<n>: run your pr-review gates. You own this PR to its terminal outcome — on RETURN stay alive and idle for my fix notice, re-review, repeat; on merge/close record the delivery in your stewardship knowledge and retire yourself. Report verdicts to <your-instance>."
43
- ```
44
-
45
- Go idle for the verdict. **The maintainer instance stays alive across
46
- RETURN rounds** — when you push fixes, notify the SAME maintainer, continuing
47
- the existing exchange rather than starting a new one; do not spawn another for
48
- this PR. You never merge to main
49
- yourself.
50
-
51
- **Reviewers are the opposite: one per commit, then gone.** The post-commit
52
- reviewer reviews its one diff, reports its verdict, and retires — it no
53
- longer exists when you fix its findings. To re-review a fix, spawn a NEW
54
- reviewer on the fix commit (`--purpose <new-short-sha>`); never message a
55
- retired reviewer or expect it to follow up.
56
- - **Multi-developer features**: the coordinator owns the feature branch
57
- (`feature/<name>`) and the PR. Branch `<you>/<name>` **from the feature
58
- branch**, push your branch, and tell the coordinator when it is ready —
59
- the coordinator merges, validates, and reviews the integrated state. Never
60
- merge into the feature branch yourself.
61
- - **If you need another developer's unmerged code, ask the coordinator** —
62
- never fetch or merge a peer's branch yourself. The coordinator
63
- lands the dependency on the feature branch and tells you to merge the
64
- feature branch into yours.
65
- - Quality bar before handing off or opening a PR: the repo's full test/check
66
- gate green; docs updated with behavior changes.
67
- - While waiting on the reviewer, the coordinator, or a peer: **do not sleep,
68
- poll, or busy-wait** — go idle. An active messaging layer wakes you when the
69
- answer arrives; with none, read the reviewer's own session when it finishes.
@@ -1,10 +0,0 @@
1
- {
2
- "capability": "oats.review",
3
- "private": true,
4
- "version": "1.3.0",
5
- "compatibility": { "oats": ">=0.28.0" },
6
- "description": "Post-commit review discipline: a fresh reviewer (this package's soul `reviewer`) reviews each new commit's diff, reports its verdict to its spawner over the deployment's messaging layer (or in its transcript when there is none), and retires — plus the shared developer delivery discipline and the reviewer's code-review and security-review skills.",
7
- "requires": [],
8
- "skills": ["skills/code-review", "skills/security-review"],
9
- "inject": "injects/review.md"
10
- }