@awebai/oats 0.36.1 → 0.38.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.
@@ -0,0 +1,146 @@
1
+ # Team model 3: the teams a soul may join, and its default, are committed in the workspace
2
+
3
+ **Status:** decided and implemented (kernel 0.38.0, feature `team-model-3`; prepared in 0.36.1).
4
+ Specs: awebai/oats#484 and #485, agreed by both maintainers on 2026-10-02. This record supersedes the
5
+ membership and default-team parts of [team model v2](2026-09-27-team-model-v2.md); v2's shared teams,
6
+ unmapped ids, live teams and provider environment stand. The reference pages
7
+ ([workspaces.md](../workspaces.md#teams), [capabilities.md](../capabilities.md#teams-in-the-provider-environment),
8
+ [desktop-cli-api.md](../desktop-cli-api.md#team-model-3-feature-team-model-3-oats-0370-replaces-feature-team-model-2))
9
+ win on operator-visible behaviour.
10
+
11
+ ## Why
12
+
13
+ **The local bridge.** An instance that sits in two teams is a bridge between them: one process reads
14
+ both inboxes, holds both contexts in one session, and can relay anything from one to the other. Under
15
+ team model v2, one person's `oats-local.yaml` could put an organisation's instance into a team the
16
+ organisation never sees: a local extra team in `souls.teams`, or a local default team while its souls
17
+ joined the organisation's shared teams. Either way it never showed in the organisation's git, and it
18
+ differed per person and per machine. The organisation owns its teams' trust boundary, so the teams its
19
+ instances can be in are its decision, committed in its workspace, and closed by default.
20
+
21
+ **Scale.** With many souls and teams, per-deployment lists were repeated by every person on every
22
+ machine.
23
+
24
+ **Principle**, consistent with v2's "shared facts committed, personal choices local": everything
25
+ workspace-specific lives in the workspace file; souls stay team-free in their repositories, open source
26
+ included; an organisation says how souls behave in its teams in its own `oats-workspace.yaml`, and
27
+ someone running the souls standalone sees none of it.
28
+
29
+ ## What it protects against, and what it does not
30
+
31
+ The workspace's eligibility rules (`souls:`, `localTeams` closed by default) **prevent accidental joins**
32
+ in a cooperative installation, and make the organisation's intended team set visible and reviewable in
33
+ its git. That is all they claim.
34
+
35
+ They are not anti-bridging enforcement, and neither is the messaging layer. Distinct keys do not prove
36
+ distinct processes: an operator controlling two identities can read under one and relay under the
37
+ other, or copy content outside the messaging layer. aweb admission controls who holds credentials for
38
+ a team (the team controller key signs memberships; hosted invites are mediated by the service), not
39
+ what a process does with what it reads. A deployment's authority comes from the credentials it holds,
40
+ not from its path or its name. Today's team certificates carry no soul, workspace or home-team claim,
41
+ and any such metadata would be self-asserted, so it would not be prevention. Enforcement on the
42
+ messaging side (admission per root, claims in certificates, a namespace policy, bridge visibility for a
43
+ team's owner) is a separate question for the aweb protocol owners.
44
+
45
+ ## The model
46
+
47
+ ### `oats-workspace.yaml`
48
+
49
+ ```yaml
50
+ teams: # shared teams, as in v2
51
+ engineering: { team: "engineering:acme.aweb.ai" }
52
+ security: { team: "security:acme.aweb.ai" }
53
+ docs: { team: "docs:acme.aweb.ai" }
54
+ defaultTeam: engineering # the workspace's fallback default team (a shared label)
55
+ localTeams: false # may deployments declare their own teams? (absent: false)
56
+ souls: # per pattern: the default team and the other teams a soul may join
57
+ "*": { teams: [] }
58
+ security-souls/*: { default: security, teams: [engineering] }
59
+ security-souls/incident-responder: { default: security, teams: [engineering, docs] }
60
+ oats.engineering/*: { teams: any }
61
+ ```
62
+
63
+ - **Keys are patterns:** `<member>/<soul>` or `<package>/<soul>`, `<member>/*` or `<package>/*`, and
64
+ `"*"`, with the names `souls.disabled` already uses. A soul's key is its qualified name. Bare names are
65
+ refused: a bare name is ambiguous across members, and the qualified form is what the workspace sees.
66
+ - **The most specific key wins outright for teams,** with no merging across levels; the default comes
67
+ from the most specific key that sets one. `a/*: {default: security}` and `a/x: {teams: [docs]}` give
68
+ `a/x` the default `security` and the teams `[docs]` only. Merging was rejected: an entry that adds to
69
+ a broader one hides what a soul can join behind two places, and the point of the model is that a
70
+ reviewer reads one entry.
71
+ - **`any`** is every shared team of the file.
72
+ - **A soul no key matches** (and no `"*"`) has its default only, member and package souls alike; a
73
+ workspace opens a package explicitly.
74
+ - **Every label is a shared team of the same file.** An unknown label, or a default that isn't one, is
75
+ `E_WORKSPACE_SCHEMA` when the file is read, never at a spawn on someone else's machine. A key that is
76
+ neither a pattern nor a discovered soul's qualified name is the warning `team-soul-unknown` (a typo
77
+ guard; not an error, because the file is validated without discovery and packages come and go).
78
+
79
+ ### The default team, in order
80
+
81
+ 1. the soul's `default` from `souls:` (`from: "soul"`);
82
+ 2. else the deployment's `oats-local.yaml` `defaultTeam`, only when `localTeams: true`
83
+ (`from: "deployment"`);
84
+ 3. else the workspace's `defaultTeam` (`from: "workspace"`);
85
+ 4. else none (`E_TEAM_UNCONFIGURED` when a messaging layer is active).
86
+
87
+ The soul's own default comes first because it is the organisation's most specific statement; a
88
+ personal default, where allowed, comes before the workspace's fallback so a person can keep their own
89
+ default team (awebai/oats sets `localTeams: true` for this).
90
+
91
+ ### The teams a soul may join
92
+
93
+ Its default, plus the `teams` of its most specific matching key, plus, with `localTeams: true`, every
94
+ local team the deployment declares (there is no per-soul local list: a deployment that is trusted with
95
+ local teams is trusted with them for every soul). Each row says why: `via` ⊂ `default`, `workspace`,
96
+ `local`. Unchanged from v2: an instance joins only its default at spawn; the others are offered and
97
+ joined on request, and the messaging provider decides admission from the eligible rows it receives in
98
+ `OATS_TEAMS`.
99
+
100
+ ### `oats-local.yaml`
101
+
102
+ - `souls.teams` and `souls.default` are removed keys. The refusal names each key found and prints the
103
+ `souls:` that gives the same result (a v2 soul's teams were its default ∪ `"*"` ∪ its own list, so each
104
+ named soul's entry lists `"*"`'s teams too; a bare soul name is written `<member>/<soul>` for the
105
+ operator to qualify), so a deployment's migration is a copy, a qualification and a PR.
106
+ - `teams` and `defaultTeam` are allowed only when the workspace says `localTeams: true`. Otherwise every
107
+ spawn, preview and inspect refuses with `E_WORKSPACE_SCHEMA` (reason `local-teams-closed`), and `oats
108
+ teams`, readiness and doctor report it as a failure. The message names both fixes: allow local teams,
109
+ or commit them in the workspace and remove them locally. The rule can only be checked where the
110
+ workspace file has been read, so it is not part of `oats-local.yaml`'s own validation.
111
+ - The standalone view (explicit, or a fallback when the workspace can't be read) has no workspace rules:
112
+ it is an individual running souls outside any organisation, so local teams apply.
113
+ - Verbs: `oats teams add` and `oats teams default` refuse where local teams are closed; `oats teams
114
+ remove` runs there, since removing local teams is the last step of committing them in the workspace.
115
+ `oats soul teams` is read only; its edit flags were removed (a soul's teams are a PR to the workspace).
116
+
117
+ ### `soul.yaml` and `oats-membership.yaml`
118
+
119
+ Unchanged: they say nothing about teams, so a soul in a public repository carries no organisation's
120
+ labels. Per-soul lists in `oats-membership.yaml` and teams in `soul.yaml` were rejected for the same
121
+ reason v2 rejected them: labels belong to a workspace, and there should be one central place.
122
+
123
+ ## Reporting (contract)
124
+
125
+ - `defaultTeam.from` (spawn preview, `inspect`, `oats souls`, readiness, `OATS_DEFAULT_TEAM_FROM`):
126
+ `soul`, `deployment` or `workspace`. `soul` changed meaning: it was the local `souls.default`.
127
+ - Every `TeamRow` gains `via`.
128
+ - `oats teams --json` is `teamsApi: 2`: `localTeams`, a `DefaultTeam` for the deployment, and the
129
+ workspace's `souls:` in place of the local `souls` block. `oats soul teams --json` is
130
+ `soulTeamsApi: 2`: `match` and `defaultMatch` (the `souls:` keys that applied) in place of `local` and
131
+ `all`. Feature `team-model-3` replaces `team-model-2`.
132
+ - Readiness: local-teams-closed (failure) and `team-soul-unknown` (warning) join the team items;
133
+ `E_TEAM_NOT_ELIGIBLE` is no longer a kernel item (a soul's default is always one of its teams); it
134
+ remains the provider's join refusal.
135
+
136
+ ## 0.36.x → 0.38.0
137
+
138
+ 0.36.1 accepted and validated `defaultTeam`, `localTeams` and `souls:` without applying them, and warned
139
+ (`team-model-3-migration`) about what 0.38.0 refuses, so every workspace could commit its side first, as
140
+ 0.29.4 did for 0.30. 0.38.0 applies the keys, refuses the old shape, and retires the warning. The steps
141
+ for each kind of deployment are in the [0.38.0 release notes](../release-notes/v0.38.0.md).
142
+
143
+ ## Out of scope
144
+
145
+ Enforcement on the aweb side (above); nested teams; the Desktop's team-editing UI (its own spec:
146
+ anything that writes `souls.teams` or `souls.default` goes).
@@ -17,7 +17,11 @@ successor, with a link to its full text.
17
17
  - [OKF knowledge operations](2026-09-26-okf-knowledge-operations.md): package
18
18
  souls, triggers and automations for harvest and maintenance.
19
19
  - [Team model v2](2026-09-27-team-model-v2.md): shared teams in the workspace,
20
- local teams, the default team and membership in each deployment.
20
+ local teams, live teams and the provider environment (its membership and
21
+ default-team parts are superseded by team model 3).
22
+ - [Team model 3](2026-10-02-team-model-3.md): the teams a soul may join, and
23
+ its default, are committed in the workspace; local teams only where the
24
+ workspace allows them.
21
25
 
22
26
  The Desktop's design brief for designers is in
23
27
  [packages/desktop/docs](../../packages/desktop/docs/design-brief.md).
@@ -37,7 +37,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
37
37
  "instance-git-remote","souls-declarations","lifecycle-plans","retire-retention","readiness","spawn-preview","instance-events",
38
38
  "instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
39
39
  "workspace-v2","instance-modules","spawn-provider-payload","served-identity","packages-no-approval","spawn-name","settings-origins",
40
- "team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
40
+ "team-model-3","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
41
41
  "preview-composed-from","observe-max-age","spawn-preview-max-age","capability-show","capture-file","workspace-identity"],
42
42
  "automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
43
43
  "readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2,
@@ -88,7 +88,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
88
88
  | `settings-declared` | `declares` on inspect capabilities and preview modules | |
89
89
  | `capabilities-private` | `private` on `oats capabilities` rows | |
90
90
  | `layers-from` | `layers.<slot>.from` in inspect | |
91
- | `team-model-2` | every team field; `oats teams`, `oats soul teams` | `teamsApi: 1`, `soulTeamsApi: 1` (payload only) |
91
+ | `team-model-3` | every team field; `oats teams`, `oats soul teams` (0.38.0; replaces `team-model-2`) | `teamsApi: 2`, `soulTeamsApi: 2` (payload only) |
92
92
  | `harness` | the harness names ([Harness input spellings](#the-harness-rename-feature-harness-oats-0270)) | |
93
93
  | `package-souls` | package soul rows, `qualifiedName`, `packages[].souls` | |
94
94
  | `triggers` | `oats trigger …` | `triggerApi: 1` (payload only) |
@@ -253,9 +253,11 @@ oats capabilities show <name> … --max-age <seconds> --json (feature capabilit
253
253
  commit can no longer be fetched. Each is observed live, as without the flag.
254
254
  A live observation that fails is the usual error, never an older head.
255
255
  - **Refusals:** every other command, a spawn apply (with or without
256
- `--expect-decision`; the form named is `spawn`), every edit form (`teams add|remove|default`,
257
- `soul teams --add|--remove|--default|--clear-default`) and any `--server`
258
- invocation refuse the flag before reading or writing anything, with
256
+ `--expect-decision`; the form named is `spawn`), every edit form (`teams add|remove|default`)
257
+ and any `--server`
258
+ invocation refuse the flag before reading or writing anything (the removed
259
+ `soul teams --add|--remove|--default|--clear-default` answer their own
260
+ `E_BAD_ARGS` first), with
259
261
  `E_BAD_ARGS` "--max-age is not accepted by \`oats <form>\`: only the read
260
262
  verbs reuse observations (status, workspace status, souls, capabilities,
261
263
  capabilities show, inspect --soul|--home, spawn --preview, and the read
@@ -397,7 +399,8 @@ message}`.
397
399
  A soul whose resolution is refused is an inspect error with the resolver's
398
400
  code and details (`E_PACKAGE_MISSING`, `E_PACKAGE_INTEGRITY`,
399
401
  `E_CAPABILITY_MISSING`, `E_LOCK_SCHEMA`, `E_REQUIREMENT_INACTIVE`,
400
- `E_TEAM_UNKNOWN`, `E_TEAM_NOT_ELIGIBLE`); readiness reports the same condition
402
+ `E_TEAM_UNKNOWN`, and `E_WORKSPACE_SCHEMA` reason `local-teams-closed`);
403
+ readiness reports the same condition
401
404
  as an item. Soul lookup errors are those of [spawn](#spawn-errors).
402
405
 
403
406
  <a id="oats-readiness---home-----soul----dir----policy---json--readinessapi-2"></a>
@@ -595,6 +598,7 @@ A removed verb answers, before any namespace can claim it:
595
598
  | `status --team` | `E_BAD_ARGS` | `oats status` in the deployment |
596
599
  | `spawn --instance` | `E_BAD_ARGS` | `--purpose` or `--name` |
597
600
  | `spawn --ephemeral`, `--instructions-file`, `--def-file` | `E_BAD_ARGS` | a soul in a member repository |
601
+ | `soul teams --add`, `--remove`, `--default`, `--clear-default` (0.38.0) | `E_BAD_ARGS` (`details: {flag, replacement}`) | `souls:` in `oats-workspace.yaml` |
598
602
  | `session … --native-record` | `E_BAD_ARGS` | none |
599
603
 
600
604
  `details.replacement` is the kernel's prose (shortened above): show it, do not
@@ -750,7 +754,7 @@ Read-only (it writes no lock):
750
754
  - `declaredPackages`: the ids in `packages:` (standalone: the kernel's
751
755
  default). `unsynced`: declared, not locked. `stale`: locked, no longer
752
756
  declared. `external[]`: `{source, soul}`.
753
- - `workspace.teams` (feature `team-model-2`): the shared teams `{label, team,
757
+ - `workspace.teams` (feature `team-model-3`): the shared teams `{label, team,
754
758
  description}` by label; `[]` standalone.
755
759
  - `problems`, `warnings`: as in sync.
756
760
  - `automations` (feature `automations`): `{host, snapshot: {takenAt,
@@ -788,7 +792,7 @@ packages' capabilities and souls, sorted by name, then origin. Both carry
788
792
  "version":"4.1.1","repoKey":"github.com/awebai/oats-okf","commit":"e1d604f7…","teams":null,"defaultTeam":null,"private":false,
789
793
  "path":"oats-package/souls/knowledge-maintainer","work":"directory","description":"Reviews harvested knowledge.","harness":"pi","model":null,
790
794
  "harnessFrom":"kernel-default","file":{"path":"oats-package/souls/knowledge-maintainer/soul.yaml","url":null},
791
- "spawnable":false,"problem":{"code":"E_TEAM_UNKNOWN","message":"team \"reviewers\" is not declared (oats-local.yaml#/souls/teams/…)"}}],
795
+ "spawnable":false,"problem":{"code":"E_TEAM_UNKNOWN","message":"team \"reviewers\" is not declared (oats-local.yaml#/defaultTeam): …"}}],
792
796
  "problems":[]}
793
797
  ```
794
798
 
@@ -804,13 +808,13 @@ are absent until `sync`.
804
808
  commit, teams, defaultTeam, private (always false), path, work, description`,
805
809
  plus the Desktop facts `harness, model, harnessFrom, file, spawnable,
806
810
  problem`, and (feature `launch-preference`) `key` and `launch`.
807
- - `key` is the soul key that `souls.teams`, `souls.default` and `souls.launch`
808
- use, and that `oats soul teams <key>` takes: `qualifiedName` for a package
811
+ - `key` is the soul key that `souls.launch` uses, and that
812
+ `oats soul teams <key>` takes: `qualifiedName` for a package
809
813
  soul, the bare `name` for a member or external soul. Two member souls that
810
814
  share a bare name share one entry (spawning that name is
811
815
  `E_SOUL_AMBIGUOUS`).
812
816
  - `launch` is a [Launch](#the-launch-report-launch).
813
- - `teams` and `defaultTeam` (feature `team-model-2`) are a
817
+ - `teams` and `defaultTeam` (feature `team-model-3`) are a
814
818
  [TeamRow](#the-team-row-teamrow) list and a
815
819
  [DefaultTeam](#the-default-defaultteam); both `null` when the soul's teams
816
820
  do not resolve (`problem` names the `E_TEAM_*` code).
@@ -1012,7 +1016,7 @@ them. Gate each field below on it.
1012
1016
  v2 soul.yaml cannot). `spawnable`/`problem`: whether a spawn here would
1013
1017
  refuse, resolved without spawning, writing or reaching past the sync cache;
1014
1018
  `problem` is `{code, message}` or `null` (`E_SOUL_DISABLED`,
1015
- `E_TEAM_UNKNOWN`, `E_TEAM_NOT_ELIGIBLE`, `E_CAPABILITY_*`, `E_PACKAGE_*`,
1019
+ `E_TEAM_UNKNOWN`, `E_WORKSPACE_SCHEMA` (local-teams-closed), `E_CAPABILITY_*`, `E_PACKAGE_*`,
1016
1020
  `E_LOCK_SCHEMA`, `E_REMOTE_*`, …). `file`: `{path, url}` of soul.yaml.
1017
1021
  - **Capabilities.** `layer` on every row, `null` outside the slots.
1018
1022
  `description` or `null`. `skills`, `commands`, `hooks`: names, sorted
@@ -1039,61 +1043,91 @@ them. Gate each field below on it.
1039
1043
  `…/tree/<commit>`); any other host gives `url: null` with `path` set.
1040
1044
  `path` is repository-relative.
1041
1045
 
1046
+ <a id="team-model-3-feature-team-model-3-oats-0370-replaces-feature-team-model-2"></a>
1042
1047
  <a id="team-model-v2-feature-team-model-2-oats-0300-replaces-feature-teams"></a>
1043
1048
  ## Teams
1044
1049
 
1045
- Feature `team-model-2`: gate every team field and verb on it. Design:
1046
- [team model v2](design/2026-09-27-team-model-v2.md); operator guide:
1047
- [workspaces.md](workspaces.md).
1048
-
1049
- - **Shared teams** live in the committed `oats-workspace.yaml`:
1050
- `teams.<label> = {description?, team?}`. A shared team without `team` (the
1051
- provider id) is **unmapped**.
1052
- - **Local** configuration lives in `oats-local.yaml`: `teams.<label> = {team,
1053
- description?}`, `defaultTeam: <label>`, `souls.teams: {"*": [labels],
1054
- "<soul>": [labels]}` and `souls.default: {"<soul>": <label>}`. A soul key is
1055
- the spawn name (`<package>/<soul>` for a package soul). A label matches
1056
- `[a-z0-9][a-z0-9._-]*`.
1050
+ Feature `team-model-3` (OATS 0.38.0; it replaces `team-model-2`): gate every
1051
+ team field and verb on it. Design: [team model 3](design/2026-10-02-team-model-3.md);
1052
+ operator guide: [workspaces.md](workspaces.md#teams).
1053
+
1054
+ - **The workspace file** (`oats-workspace.yaml`, committed) holds the shared
1055
+ teams `teams.<label> = {description?, team?}` (a shared team without `team`,
1056
+ the provider id, is **unmapped**), `defaultTeam: <label>` (the workspace's
1057
+ fallback default), `localTeams: true|false` (absent: false) and `souls:`
1058
+ (per pattern, `{default?, teams?: [labels] | "any"}`). Every label there is
1059
+ a shared label of that file; anything else is `E_WORKSPACE_SCHEMA` when the
1060
+ file is read.
1061
+ - **`oats-local.yaml`** may declare local teams `teams.<label> = {team,
1062
+ description?}` and `defaultTeam: <label>` only where the workspace says
1063
+ `localTeams: true`, or in the standalone view (no workspace file is read).
1064
+ Elsewhere they are refused: `E_WORKSPACE_SCHEMA` with details `{reason:
1065
+ "local-teams-closed", path: "oats-local.yaml", keys}`, the message naming
1066
+ both fixes. `souls.teams` and `souls.default` are removed keys
1067
+ (`E_WORKSPACE_SCHEMA`, reason `removed-key`): the refusal names each key
1068
+ found, and `details.replacement` is the `souls:` YAML to commit instead (a
1069
+ bare soul name written `<member>/<soul>` for the operator to qualify), also
1070
+ printed at the end of the message. Every command that reads the file
1071
+ refuses, an instance home's messaging commands, operations, `inspect --home`
1072
+ and `readiness --home` included: they never fall back to the home's
1073
+ recorded teams for it. A home's other capability commands read only the
1074
+ home's own record, as always.
1075
+ - **A soul's key** is its qualified name: `<package>/<soul>`, or
1076
+ `<member>/<soul>` with the member repository's name (as `souls.disabled`
1077
+ names it). A label matches `[a-z0-9][a-z0-9._-]*`.
1057
1078
  - **Team ids.** A `team` value (in either file, and `oats teams add --team`)
1058
1079
  matches `^[A-Za-z0-9][A-Za-z0-9._:@/+-]{0,255}$`: the kernel's safety rule
1059
1080
  (never `-`-led, no whitespace or control characters, bounded). Otherwise
1060
1081
  `E_WORKSPACE_SCHEMA` (a file) or `E_BAD_ARGS` (the verb). The messaging
1061
1082
  provider validates its own id shape (oats.aweb: `<name>:<namespace>`).
1062
- - **Resolution.** The soul's default is `souls.default[soul] ??
1063
- defaultTeam`; its teams are that default plus `souls.teams["*"]` plus
1064
- `souls.teams[soul]`. A label in both files is a `team-label-collision`
1065
- warning, and the shared definition wins. An undeclared label is
1066
- `E_TEAM_UNKNOWN`; a `souls.default` outside the soul's teams is
1067
- `E_TEAM_NOT_ELIGIBLE`.
1083
+ - **Resolution.** The `souls:` patterns for a soul are its key, then
1084
+ `<member|package>/*`, then `"*"`. The most specific one that exists gives
1085
+ the soul's teams outright (lists never merge); the most specific one that
1086
+ sets `default` gives its default. So `a/*: {default: security}` and
1087
+ `a/x: {teams: [docs]}` give `a/x` the default `security` and the teams
1088
+ `[docs]` only. The default is that `default`; else the local `defaultTeam`
1089
+ where local teams are allowed; else the workspace's `defaultTeam`; else
1090
+ none. A soul's teams are its default, its pattern's `teams` (`any`: every
1091
+ shared team) and, where local teams are allowed, every local team. A soul
1092
+ no pattern matches has its default only. A label in both files is a
1093
+ `team-label-collision` warning, and the shared definition wins. A local
1094
+ default no file declares is `E_TEAM_UNKNOWN`.
1068
1095
  - **Removed keys** are `E_WORKSPACE_SCHEMA` with `reason: "removed-key"`:
1096
+ `oats-local.yaml` `souls.teams` and `souls.default` (0.38.0);
1069
1097
  `messaging.byTeam`, `defaults.byTeam`, a soul.yaml `team`, an
1070
- oats-membership.yaml `team`, and `byTeam` in any provider payload layer.
1071
- Other payload keys are opaque (a `team` setting passes through).
1098
+ oats-membership.yaml `team`, and `byTeam` in any provider payload layer
1099
+ (0.30). Other payload keys are opaque (a `team` setting passes through).
1072
1100
 
1073
1101
  ### The team row (`TeamRow`)
1074
1102
 
1075
- Exactly `{label, team, default, from}`:
1103
+ Exactly `{label, team, default, from, via}`:
1076
1104
 
1077
1105
  ```json
1078
- [{"label":"antares","team":"antares:ana.aweb.ai","default":true,"from":"local"},
1079
- {"label":"oats","team":"oats:oats.aweb.ai","default":false,"from":"shared"},
1080
- {"label":"reviewers","team":null,"default":false,"from":"shared"}]
1106
+ [{"label":"security","team":"security:acme.aweb.ai","default":true,"from":"shared","via":["default"]},
1107
+ {"label":"docs","team":null,"default":false,"from":"shared","via":["workspace"]},
1108
+ {"label":"mine","team":"mine:ana.aweb.ai","default":false,"from":"local","via":["local"]}]
1081
1109
  ```
1082
1110
 
1083
1111
  `team` is `null` for an unmapped team. `default` is true on exactly the
1084
1112
  soul's default row; the others are teams an instance may join (offered, never
1085
- joined automatically). `from` is `"shared"` or `"local"`. The default row
1086
- comes first, then the rest by label (codepoint order). Reports include
1087
- unmapped rows; `OATS_TEAMS` and `instance.json.teams` carry mapped rows only.
1113
+ joined automatically). `from` is where the label is defined: `"shared"` or
1114
+ `"local"`. `via` is why the soul may join it, an ordered non-empty subset of
1115
+ `"default"`, `"workspace"` (its `souls:` pattern) and `"local"` (a local team
1116
+ where local teams are allowed). The default row comes first, then the rest by
1117
+ label (codepoint order). Reports include unmapped rows; `OATS_TEAMS` and
1118
+ `instance.json.teams` carry mapped rows only. A home spawned before 0.38.0
1119
+ recorded rows without `via`, and they are reported as recorded.
1088
1120
 
1089
1121
  ### The default (`DefaultTeam`)
1090
1122
 
1091
1123
  ```json
1092
- {"label":"antares","team":"antares:ana.aweb.ai","from":"deployment"}
1124
+ {"label":"security","team":"security:acme.aweb.ai","from":"soul"}
1093
1125
  ```
1094
1126
 
1095
- `from` is `"deployment"` (`defaultTeam`) or `"soul"` (`souls.default`). It is
1096
- `null` only when no default is configured (with messaging active, that is
1127
+ `from` is `"soul"` (the soul's `souls:` default in the workspace file; before
1128
+ 0.38.0 it meant `oats-local.yaml` `souls.default`), `"deployment"` (the local
1129
+ `defaultTeam`) or `"workspace"` (the workspace's `defaultTeam`). It is `null`
1130
+ only when no default is configured (with messaging active, that is
1097
1131
  `E_TEAM_UNCONFIGURED`). An unmapped default is `{label, team: null, from}`,
1098
1132
  the blocking problem `team-unmapped`.
1099
1133
 
@@ -1119,9 +1153,12 @@ its spawn-time default until respawned (readiness says so with
1119
1153
  the teams in `OATS_DEFAULT_TEAM`, `OATS_DEFAULT_TEAM_ID`,
1120
1154
  `OATS_DEFAULT_TEAM_FROM`, `OATS_TEAMS` and `OATS_TEAMS_SOURCE`:
1121
1155
  [capabilities.md](capabilities.md#teams-in-the-provider-environment).
1122
- `OATS_WORKSPACE_NAME` is the recorded workspace name (the discovered one for a
1123
- soul subject), `""` when unknown.
1156
+ `OATS_TEAMS` is exactly the soul's eligible mapped rows, so a provider that
1157
+ admits joins from it refuses any other team. `OATS_WORKSPACE_NAME` is the
1158
+ recorded workspace name (the discovered one for a soul subject), `""` when
1159
+ unknown.
1124
1160
 
1161
+ <a id="oats-teams"></a>
1125
1162
  ### `oats teams`
1126
1163
 
1127
1164
  ```text
@@ -1132,66 +1169,75 @@ oats teams default <label> --json
1132
1169
  ```
1133
1170
 
1134
1171
  ```json
1135
- {"teamsApi":1,"deployment":"/w","defaultTeam":"antares",
1136
- "teams":[{"label":"antares","team":"antares:ana.aweb.ai","description":null,"from":"local","default":true,"at":"oats-local.yaml#/teams/antares"},
1137
- {"label":"reviewers","team":null,"description":null,"from":"shared","default":false,"at":"github.com/awebai/oats:oats-workspace.yaml#/teams/reviewers"}],
1138
- "souls":{"teams":{"*":["oats"],"oats-expert":["reviewers"]},"default":{"oats-expert":"oats"}},
1139
- "problems":[{"code":"team-unmapped","label":"reviewers","default":false,"severity":"warning","at":"github.com/awebai/oats:oats-workspace.yaml#/teams/reviewers",
1140
- "message":"shared team reviewers has no provider id yet","fix":"its owner runs `oats aweb setup`, then commits the id"}]}
1172
+ {"teamsApi":2,"deployment":"/w","localTeams":true,"defaultTeam":{"label":"mine","team":"mine:ana.aweb.ai","from":"deployment"},
1173
+ "teams":[{"label":"mine","team":"mine:ana.aweb.ai","description":null,"from":"local","default":true,"at":"oats-local.yaml#/teams/mine"},
1174
+ {"label":"docs","team":null,"description":null,"from":"shared","default":false,"at":"github.com/acme/agents:oats-workspace.yaml#/teams/docs"}],
1175
+ "souls":{"*":{"teams":["docs"]},"security-souls/*":{"default":"security","teams":["engineering"]}},
1176
+ "problems":[{"code":"team-unmapped","label":"docs","default":false,"severity":"warning","at":"github.com/acme/agents:oats-workspace.yaml#/teams/docs",
1177
+ "message":"shared team docs has no provider id yet","fix":"its owner runs `oats aweb setup`, then commits the id"}]}
1141
1178
  ```
1142
1179
 
1143
- - `defaultTeam` is the deployment's label (or `null`), not a `DefaultTeam`.
1180
+ - `localTeams`: the workspace's answer (`true`/`false`), `null` in the
1181
+ standalone view. `defaultTeam` is the `DefaultTeam` a soul without a
1182
+ `souls:` default gets here (`from` `"deployment"` or `"workspace"`), or
1183
+ `null`.
1144
1184
  - `teams[]`: every declared team by label, `{label, team, description, from,
1145
- default, at}`; `at` is a pointer into `oats-local.yaml` or
1146
- `<workspace key>:oats-workspace.yaml#/teams/<label>`. A collision shows the
1147
- shared definition.
1148
- - `souls`: `souls.teams` and `souls.default` as written. `problems`: the
1149
- deployment's [team readiness items](#team-readiness-items).
1185
+ default, at}`; `default` marks that `defaultTeam`; `at` is a pointer into
1186
+ `oats-local.yaml` or `<workspace key>:oats-workspace.yaml#/teams/<label>`.
1187
+ A collision shows the shared definition.
1188
+ - `souls`: the workspace's `souls:` as committed (`{}` when none).
1189
+ `problems`: the deployment's [team readiness items](#team-readiness-items).
1190
+ The command discovers the workspace (every member's souls), for the
1191
+ `team-soul-unknown` check.
1150
1192
  - The verbs never call a provider. They validate, rewrite `oats-local.yaml` in
1151
1193
  place, and answer the document plus `changed: bool`.
1152
- - **`add`**: the first team added also becomes `defaultTeam`. A label already
1153
- declared is `E_TEAM_EXISTS {label, from}`; a bad label or no `--team` is
1154
- `E_BAD_ARGS`.
1155
- - **`remove`**: a referenced label is `E_TEAM_IN_USE {label, usedBy}` (each
1156
- `"defaultTeam"`, `"souls.teams:<key>"` or `"souls.default:<key>"`); a shared
1157
- label is `E_TEAM_SHARED {label, at}` (a label in both files can be removed
1158
- locally); unknown is `E_TEAM_UNKNOWN {label}`.
1194
+ - **Where local teams are not allowed**, `add` and `default` are refused
1195
+ before anything is written: `E_WORKSPACE_SCHEMA {reason:
1196
+ "local-teams-closed", path, keys}` (`keys` what the verb writes). `remove`
1197
+ still runs there: taking local teams away is the last step of committing
1198
+ them in the workspace.
1199
+ - **`add`**: the first team added also becomes the local `defaultTeam`. A
1200
+ label already declared is `E_TEAM_EXISTS {label, from}`; a bad label or no
1201
+ `--team` is `E_BAD_ARGS`.
1202
+ - **`remove`**: a label the local `defaultTeam` names is `E_TEAM_IN_USE
1203
+ {label, usedBy: ["defaultTeam"]}`; a shared label is `E_TEAM_SHARED {label,
1204
+ at}` (a label in both files can be removed locally); unknown is
1205
+ `E_TEAM_UNKNOWN {label}`.
1159
1206
  - **`default`**: any declared label, else `E_TEAM_UNKNOWN`.
1160
- - A write that would introduce an unknown or ineligible reference is refused
1161
- with that code; an invalid result is `E_WORKSPACE_SCHEMA`.
1207
+ - A write that would introduce an unknown reference is refused with that
1208
+ code; an invalid result is `E_WORKSPACE_SCHEMA`.
1209
+ - **Writes** edit `oats-local.yaml` in place and touch only the entries that
1210
+ change: comments and styles elsewhere, including inline comments on sibling
1211
+ entries, are kept. Each verb re-reads the file and judges its refusals on it
1212
+ as it is now, and writes only if the file did not change meanwhile (else it
1213
+ redoes the edit on the new content). A file that keeps changing is
1214
+ `E_LOCAL_CHANGED {path}`; nothing was written.
1162
1215
 
1216
+ <a id="oats-soul-teams"></a>
1163
1217
  ### `oats soul teams`
1164
1218
 
1165
1219
  ```text
1166
- oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <label> | --clear-default] [--dir <d>] --json
1167
- oats soul teams <soul>|'*' [--dir <d>] [--max-age <s>] --json (the read form only)
1220
+ oats soul teams <soul>|'*' [--dir <d>] [--max-age <s>] --json
1168
1221
  ```
1169
1222
 
1170
1223
  ```json
1171
- {"soulTeamsApi":1,"soul":"oats-expert","key":"oats-expert","defaultTeam":{"label":"oats","team":"oats:oats.aweb.ai","from":"soul"},
1172
- "teams":[{"label":"oats","team":"oats:oats.aweb.ai","default":true,"from":"shared","via":["default","*"]},
1173
- {"label":"reviewers","team":null,"default":false,"from":"shared","via":["soul"]}],
1174
- "local":{"teams":["reviewers"],"default":"oats"},"all":["oats"]}
1224
+ {"soulTeamsApi":2,"soul":"incident-responder","key":"security-souls/incident-responder",
1225
+ "match":"security-souls/incident-responder","defaultMatch":"security-souls/*",
1226
+ "defaultTeam":{"label":"security","team":"security:acme.aweb.ai","from":"soul"},
1227
+ "teams":[{"label":"security","team":"security:acme.aweb.ai","default":true,"from":"shared","via":["default"]},
1228
+ {"label":"docs","team":null,"default":false,"from":"shared","via":["workspace"]},
1229
+ {"label":"engineering","team":"engineering:acme.aweb.ai","default":false,"from":"shared","via":["workspace"]}]}
1175
1230
  ```
1176
1231
 
1177
- - Rows are `TeamRow` plus `via`, a non-empty ordered subset of `"default"`,
1178
- `"*"` and `"soul"`. `key` is the soul's `souls.*` key; `local` its own
1179
- entries; `all` is `souls.teams["*"]`.
1180
- - For `'*'`: `soul` and `key` are `"*"`, `teams` the deployment default plus
1181
- `souls.teams["*"]`, `local.default: null`.
1182
- - Mutations answer the document plus `changed`. Unknown label:
1183
- `E_TEAM_UNKNOWN {label}`. A `--default` outside the soul's teams:
1184
- `E_TEAM_NOT_ELIGIBLE {soul, label, at}` (`at`: `oats-local.yaml#/souls/default/<key>`,
1185
- `/` written `~1`). `--default`/`--clear-default` with
1186
- `'*'`, or both together: `E_BAD_ARGS`. Soul lookup: `E_SOUL_UNKNOWN`,
1187
- `E_SOUL_AMBIGUOUS`.
1188
- - **Writes** (`oats teams add|remove|default`, `oats soul teams`) edit
1189
- `oats-local.yaml` in place and touch only the entries that change: comments
1190
- and styles elsewhere, including inline comments on sibling entries and flow
1191
- lists, are kept. Each verb re-reads the file and judges its refusals on it
1192
- as it is now, and writes only if the file did not change meanwhile (else it
1193
- redoes the edit on the new content). A file that keeps changing is
1194
- `E_LOCAL_CHANGED {path}`; nothing was written.
1232
+ - Read only. `key` is the soul's key; `match` the `souls:` key its teams come
1233
+ from and `defaultMatch` the one its default comes from (`null`: none
1234
+ matches, or none sets a default). For `'*'`, `soul`, `key` and the only
1235
+ pattern tried are `"*"`. Soul lookup: `E_SOUL_UNKNOWN`, `E_SOUL_AMBIGUOUS`;
1236
+ the soul's teams: `E_WORKSPACE_SCHEMA` (local-teams-closed), `E_TEAM_UNKNOWN`.
1237
+ - The edit flags `--add`, `--remove`, `--default` and `--clear-default` were
1238
+ removed in 0.38.0: `E_BAD_ARGS` with details `{flag, replacement: "souls: in
1239
+ oats-workspace.yaml (a PR to the workspace file)"}`, before anything else
1240
+ is judged.
1195
1241
 
1196
1242
  ### The messaging provider's teams document
1197
1243
 
@@ -1218,8 +1264,9 @@ no `primary` and no `unmapped`: unmapped teams are kernel readiness items.
1218
1264
  `oats teams` lists them under `problems[]` as `{code, severity: "failure" |
1219
1265
  "warning", message, fix, …}`. `oats readiness` lists the soul's under
1220
1266
  `checks.configured` with `subject` `"team <label>"` (or `"teams"`), `producer:
1221
- "team model"`, `code`, `reason`, `remedy`, `status: "fail"`, `required: true`
1222
- for a failure and `false` for a warning, plus the problem's own keys.
1267
+ "team model"`, `code`, `reason` (the message), `remedy` (the fix), `status:
1268
+ "fail"`, `required: true` for a failure and `false` for a warning, plus the
1269
+ problem's own keys.
1223
1270
 
1224
1271
  | Code | Severity | Keys | When |
1225
1272
  |---|---|---|---|
@@ -1227,40 +1274,31 @@ for a failure and `false` for a warning, plus the problem's own keys.
1227
1274
  | `team-unmapped` | failure if `default`, else warning | `label`, `default`, `at` | a shared team without `team` |
1228
1275
  | `team-label-collision` | warning | `label`, `shared`, `local` (each `{team, description, at}`) | a label in both files |
1229
1276
  | `default-team-changed` | warning | `recorded`, `current` | `--home` with live teams: the default changed since the spawn |
1230
- | `E_TEAM_UNKNOWN` | failure | `label`, `at` | a reference to an undeclared label |
1231
- | `E_TEAM_NOT_ELIGIBLE` | failure | `soul`, `label`, `at` | `souls.default` outside the soul's teams |
1232
- | `team-model-3-migration` | warning | `condition`, `keys` | 0.36.x: what OATS 0.37.0 (team model 3) refuses, one item per condition (below) |
1233
-
1234
- The `E_TEAM_UNKNOWN` and `E_TEAM_NOT_ELIGIBLE` codes are also spawn, preview and
1235
- inspect refusals, with the same details.
1236
-
1237
- `team-model-3-migration` is a deployment fact, so every soul's readiness
1238
- carries it, and `oats teams` lists it once. Its `condition`:
1239
-
1240
- - `local-soul-teams`: `oats-local.yaml` has `souls.teams` and/or
1241
- `souls.default` (`keys`: `["souls.teams", "souls.default"]` as found). They
1242
- move to `souls:` in `oats-workspace.yaml`.
1243
- - `local-teams-closed`: `oats-local.yaml` declares `teams` and/or
1244
- `defaultTeam` (`keys`: `["teams", "defaultTeam"]` as found) and the
1245
- workspace file does not say `localTeams: true`. The `fix` names both
1246
- remedies: add `localTeams: true` to the workspace file, or commit the teams
1247
- and `defaultTeam` there and remove them locally. Never raised in the
1248
- standalone view, which has no workspace rules.
1277
+ | `E_TEAM_UNKNOWN` | failure | `label`, `at` | a local default no file declares |
1278
+ | `E_WORKSPACE_SCHEMA` | failure | `condition: "local-teams-closed"`, `path`, `keys` | `oats-local.yaml` declares `teams` / `defaultTeam` and the workspace does not allow local teams |
1279
+ | `team-soul-unknown` | warning | `key`, `at` | a `souls:` key that is neither `"*"` nor `<name>/*` nor a discovered soul's key (a typo guard) |
1280
+
1281
+ `E_TEAM_UNKNOWN` and local-teams-closed are also spawn, preview and inspect
1282
+ refusals (local-teams-closed as `E_WORKSPACE_SCHEMA` with details `{reason,
1283
+ path, keys}`). While local teams are closed, that one item is the soul's only
1284
+ team item. `team-soul-unknown` is reported where the souls are discovered
1285
+ (`oats teams`, readiness).
1249
1286
 
1250
1287
  ```json
1251
- {"code":"team-model-3-migration","severity":"warning","condition":"local-teams-closed","keys":["teams","defaultTeam"],
1252
- "message":"oats-local.yaml declares teams, defaultTeam, but oats-workspace.yaml does not say localTeams: true: OATS 0.37.0 refuses local teams and a local defaultTeam unless the workspace allows them",
1288
+ {"code":"E_WORKSPACE_SCHEMA","severity":"failure","condition":"local-teams-closed","path":"oats-local.yaml","keys":["teams","defaultTeam"],
1289
+ "message":"oats-local.yaml declares teams, defaultTeam, but oats-workspace.yaml does not allow local teams (localTeams: true): either (a) add `localTeams: true` to oats-workspace.yaml, or (b) commit the teams and defaultTeam in oats-workspace.yaml, then remove them from oats-local.yaml",
1253
1290
  "fix":"either (a) add `localTeams: true` to oats-workspace.yaml, or (b) commit the teams and defaultTeam in oats-workspace.yaml, then remove them from oats-local.yaml"}
1254
1291
  ```
1255
1292
 
1256
- As a readiness item it is `{subject: "teams", status: "fail", required: false,
1257
- producer: "team model", code, reason: <message>, remedy: <fix>, condition,
1258
- keys}`. `oats doctor --json` lists the same problems under `problems[]`. Doctor
1259
- stays offline: for `local-teams-closed` it reads only the workspace file this
1260
- machine's parsed cache holds; when there is none, it adds no problem and says
1261
- so in `information[]`: `"team-model-3-migration: whether oats-local.yaml
1262
- teams/defaultTeam need localTeams: true couldn't be checked: this deployment
1263
- hasn't observed its workspace yet; run oats sync"`.
1293
+ `oats doctor --json` lists local-teams-closed under `problems[]` (text: `!
1294
+ E_WORKSPACE_SCHEMA (local-teams-closed): …`). Doctor stays offline: it reads
1295
+ only the workspace file this machine's parsed cache holds; when there is
1296
+ none, it adds no problem and says so in `information[]`:
1297
+ `"local-teams-closed: whether oats-workspace.yaml allows oats-local.yaml
1298
+ teams/defaultTeam (localTeams: true) couldn't be checked: this deployment
1299
+ hasn't observed its workspace yet; run oats sync"`. A removed key in
1300
+ `oats-local.yaml` makes doctor answer that refusal, as for any unreadable
1301
+ local file.
1264
1302
 
1265
1303
  <a id="soul-launch-preferences-feature-launch-preference-oats-0300"></a>
1266
1304
  ## Launch preferences
@@ -1613,7 +1651,7 @@ Feature `spawn-name`. `--name <slug>` is the exact name, with no prefix.
1613
1651
  | `E_SOUL_AMBIGUOUS` | `{name, repos, qualified}` | several souls answer the bare name; use one of `qualified` |
1614
1652
  | `E_SOUL_DISABLED` | `{name, qualifiedName, entry}` | listed in `souls.disabled` |
1615
1653
  | `E_UNKNOWN_AGENT` | | the resolved soul is not under the deployment's agents root |
1616
- | `E_TEAM_UNKNOWN`, `E_TEAM_NOT_ELIGIBLE` | `{label, at}`, `{soul, label, at}` | the soul's teams do not resolve |
1654
+ | `E_TEAM_UNKNOWN`, `E_WORKSPACE_SCHEMA` (local-teams-closed) | `{label, at}`, `{reason, path, keys}` | the soul's teams do not resolve |
1617
1655
  | `E_NOT_A_MEMBER`, `E_MEMBERSHIP_UNCONFIRMED` | | the soul's repository is not a confirmed member |
1618
1656
  | `E_CAPABILITY_MISSING`, `E_CAPABILITY_PRIVATE`, `E_CAPABILITY_INCOMPATIBLE`, `E_COMPATIBILITY` | | a capability cannot be resolved |
1619
1657
  | `E_PACKAGE_MISSING`, `E_PACKAGE_INTEGRITY`, `E_LOCK_SCHEMA` | | the lock does not provide it (standalone: `{…, standalone: true, reason: "no-catalog", catalog}`) |