@awebai/oats 0.37.0 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,21 +25,21 @@
25
25
  "type": "object",
26
26
  "propertyNames": { "$ref": "#/$defs/label" },
27
27
  "additionalProperties": { "$ref": "#/$defs/team" },
28
- "description": "SHARED teams: <label>: { description?, team? }. `team` is the messaging provider's team id, the same for everyone; without it the team is declared but not yet created (readiness team-unmapped). Local teams, the default and which teams each soul belongs to live in the deployment's oats-local.yaml (`oats teams`, `oats soul teams`). A label never gates, restricts or partitions anything."
28
+ "description": "SHARED teams: <label>: { description?, team? }. `team` is the messaging provider's team id, the same for everyone; without it the team is declared but not yet created (readiness team-unmapped). The default team and which teams each soul may join are defaultTeam and souls: below; a deployment's local teams live in its oats-local.yaml where localTeams allows them. A label never gates, restricts or partitions anything."
29
29
  },
30
30
  "defaultTeam": {
31
31
  "$ref": "#/$defs/label",
32
- "description": "Team model 3 (0.38.0): the workspace's fallback default team, a label of teams: in this file. 0.36.x and 0.37.x accept and validate it without applying it."
32
+ "description": "The workspace's fallback default team, a label of teams: in this file: a soul's souls: default wins over it, and so does a deployment's local defaultTeam where localTeams: true."
33
33
  },
34
34
  "localTeams": {
35
35
  "type": "boolean",
36
- "description": "Team model 3 (0.38.0): whether deployments may declare their own teams and defaultTeam in oats-local.yaml. Absent: false. 0.36.x and 0.37.x accept it without applying it."
36
+ "description": "Whether deployments may declare their own teams and defaultTeam in oats-local.yaml (every soul may join a local team). Absent: false; then local teams and a local defaultTeam are refused (E_WORKSPACE_SCHEMA reason local-teams-closed)."
37
37
  },
38
38
  "souls": {
39
39
  "type": "object",
40
40
  "propertyNames": { "type": "string", "pattern": "^(?:\\*|[a-z0-9][a-z0-9._-]*/(?:\\*|[a-z0-9]+(?:-[a-z0-9]+)*))$" },
41
41
  "additionalProperties": { "$ref": "#/$defs/soulTeams" },
42
- "description": "Team model 3 (0.38.0): per soul pattern (\"*\", <member|package>/* or <member|package>/<soul>; the most specific key wins outright), the soul's default team and the other teams it may join. Every label is a label of teams: in this file. 0.36.x and 0.37.x accept and validate it without applying it."
42
+ "description": "Per soul pattern (\"*\", <member|package>/* or <member|package>/<soul>), the soul's default team and the other teams it may join. The most specific key gives the soul's teams outright (lists never merge); the most specific key that sets a default gives its default. A soul no key matches has its default only. Every label is a label of teams: in this file."
43
43
  },
44
44
  "defaults": { "$ref": "#/$defs/defaults" },
45
45
  "stores": {
@@ -9,9 +9,9 @@ or workspace membership alone does not make a package official.
9
9
 
10
10
  | package | release | capabilities | package souls |
11
11
  |---|---|---|---|
12
- | `oats.framework` | `oats-framework/v1.4.3` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
12
+ | `oats.framework` | `oats-framework/v1.5.0` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
13
13
  | `oats.okf` | `v4.1.1` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
14
- | `oats.aweb` | `v1.18.1` | `oats.aweb` (messaging) | |
14
+ | `oats.aweb` | `v1.20.0` | `oats.aweb` (messaging) | |
15
15
  | `oats.engineering` | `v1.5.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
16
16
  | `oats.authoring` | `v1.0.3` | `oats.authoring` | |
17
17
  | `oats.jira` | `v1.0.1` | `oats.jira` (tasks) | |
package/docs/packages.md CHANGED
@@ -51,7 +51,7 @@ packages:
51
51
  - **Bare version** (`v4.1.1`, `4.1.1`, `1.0.0-rc.1`): the id is looked up in
52
52
  the official catalog — `package-catalog.json` in the `oats` repo, or the file
53
53
  named by `OATS_PACKAGE_CATALOG` — which supplies the repo url, the tag
54
- convention (`v4.1.1` or `oats-framework/v1.4.3`) and the payload path. An id
54
+ convention (`v4.1.1` or `oats-framework/v1.5.0`) and the payload path. An id
55
55
  the catalog does not know is `E_PACKAGE_MISSING` ("use `git:<repo>@<ref>` for
56
56
  a package outside the catalog"). The catalog is the reviewed official list
57
57
  ([official-catalog.md](official-catalog.md)) and the only way a
@@ -74,9 +74,9 @@ members:
74
74
  - git:github.com/acme/agents
75
75
  - git:github.com/acme/platform
76
76
  packages:
77
- oats.framework: v1.4.3
77
+ oats.framework: v1.5.0
78
78
  oats.okf: v4.1.1
79
- oats.aweb: v1.18.1
79
+ oats.aweb: v1.20.0
80
80
  teams:
81
81
  platform: { team: "platform:acme.aweb.ai", description: Platform engineering }
82
82
  defaults:
@@ -134,7 +134,7 @@ Declaring a package in the workspace's `packages:` is the trust decision
134
134
  ## `oats package add | remove`
135
135
 
136
136
  ```bash
137
- oats package add oats.aweb v1.18.1 # a catalog version
137
+ oats package add oats.aweb v1.20.0 # a catalog version
138
138
  oats package add acme.tools git:github.com/acme/tools@v0.4.0
139
139
  oats package remove acme.tools
140
140
  ```
@@ -247,8 +247,9 @@ oats-package/
247
247
  souls; otherwise `E_SOUL_AMBIGUOUS` names each qualified form
248
248
  (`details.qualified`). A member soul's qualified form is `<member name>/<soul>`.
249
249
  - **Resolved** like any soul: the workspace defaults apply, `off` and
250
- `<slot>: none` work, its teams here are keyed `<package>/<soul>` in
251
- `oats-local.yaml` `souls.teams`, and `from: here` means **this package** at the locked commit
250
+ `<slot>: none` work, its teams are keyed `<package>/<soul>` (or `<package>/*`)
251
+ in the workspace's `souls:` (unlisted, it joins its default only), and
252
+ `from: here` means **this package** at the locked commit
252
253
  (a capability it does not provide is `E_CAPABILITY_MISSING`).
253
254
  - **Spawned** at the locked commit: the soul is fetched into the per-commit
254
255
  soul cache and its digest must equal the lock's (`E_PACKAGE_INTEGRITY
@@ -332,11 +333,11 @@ A soul that names one of the package's capabilities with
332
333
  "policy": "docs/official-catalog.md",
333
334
  "packages": {
334
335
  "oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v4.1.1", "path": "oats-package" },
335
- "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.4.3", "path": "oats-package" }
336
+ "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.5.0", "path": "oats-package" }
336
337
  }
337
338
  }
338
339
  ```
339
340
 
340
- `ref` carries the tag convention: a workspace's `oats.framework: v1.4.3`
341
- resolves to tag `oats-framework/v1.4.3`. Resolving through the catalog never
341
+ `ref` carries the tag convention: a workspace's `oats.framework: v1.5.0`
342
+ resolves to tag `oats-framework/v1.5.0`. Resolving through the catalog never
342
343
  advances a lock by itself: `oats sync` does, and says so.
@@ -0,0 +1,128 @@
1
+ # OATS 0.38.0
2
+
3
+ ## Changed (breaking)
4
+
5
+ - **The Desktop accepts OATS CLIs `>=0.25.8 <0.39.0`**, so it runs against
6
+ this release's kernel. Install the CLI and the Desktop 0.38.0 together: the
7
+ Desktop 0.37.x refuses a 0.38 CLI.
8
+
9
+ - **Team model 3: the teams a soul may join, and its default, are committed
10
+ in the workspace** (awebai/oats#484; feature `team-model-3` replaces
11
+ `team-model-2`). An instance in two teams is a bridge between them, so which
12
+ teams an organisation's instances may be in is now its decision, committed
13
+ in `oats-workspace.yaml` and closed by default. This prevents accidental
14
+ joins and makes the intended team set reviewable in git; it does not stop a
15
+ deliberate relay by someone holding credentials for two teams (see
16
+ docs/design/2026-10-02-team-model-3.md).
17
+ - `oats-workspace.yaml` `defaultTeam`, `localTeams` and `souls:` (accepted
18
+ since 0.36.1) now apply. `souls:` keys are `"*"`, `<member|package>/*` and
19
+ `<member|package>/<soul>`; the most specific key gives a soul's teams
20
+ outright (lists never merge), and the most specific key that sets one
21
+ gives its default: `a/*: {default: security}` and `a/x: {teams: [docs]}`
22
+ give `a/x` the default `security` and the teams `[docs]` only. `teams:
23
+ any` is every shared team. A soul no key matches has its default only,
24
+ package souls included.
25
+ - A soul's default team is its `souls:` default; else the deployment's
26
+ `oats-local.yaml` `defaultTeam`, only when the workspace says
27
+ `localTeams: true`; else the workspace's `defaultTeam`; else none.
28
+ `defaultTeam.from` and `OATS_DEFAULT_TEAM_FROM` are `soul`, `deployment`
29
+ or `workspace`; `soul` now means the workspace's `souls:` default (it was
30
+ the local `souls.default`).
31
+ - A soul may join its default, its `souls:` teams and, with
32
+ `localTeams: true`, every local team. Every team row carries `via`
33
+ (`default`, `workspace`, `local`), in reports, `OATS_TEAMS` and
34
+ `instance.json`.
35
+ - `oats-local.yaml` `souls.teams` and `souls.default` are refused
36
+ (`E_WORKSPACE_SCHEMA`, reason `removed-key`). The refusal names each key
37
+ and prints the `souls:` to commit instead (`details.replacement`); `oats
38
+ doctor` answers with it.
39
+ - `oats-local.yaml` `teams` and `defaultTeam` are refused unless the
40
+ workspace says `localTeams: true` (`E_WORKSPACE_SCHEMA`, reason
41
+ `local-teams-closed`, naming both fixes). Spawn, preview and inspect
42
+ refuse; `oats teams`, readiness and `oats doctor` (offline, from the cached
43
+ workspace file) report it as a failure. The standalone view allows them.
44
+ - `oats teams add` and `oats teams default` are refused where local teams
45
+ are closed; `oats teams remove` still works there. `oats soul teams` is
46
+ read only: `--add`, `--remove`, `--default` and `--clear-default` are
47
+ `E_BAD_ARGS`, naming `souls:` in `oats-workspace.yaml`.
48
+ - `oats teams --json` is `teamsApi: 2` (`localTeams`, a `DefaultTeam`, the
49
+ workspace's `souls:`) and now discovers the workspace's souls; `oats soul
50
+ teams --json` is `soulTeamsApi: 2` (`key` is the qualified soul key,
51
+ `match` and `defaultMatch` name the `souls:` keys that applied).
52
+ - New readiness items: local-teams-closed (failure) and `team-soul-unknown`
53
+ (a warning for a `souls:` key that names no soul of the workspace).
54
+ `team-model-3-migration` is retired, and the kernel no longer reports
55
+ `E_TEAM_NOT_ELIGIBLE` (a soul's default is always eligible).
56
+ - Shapes: docs/desktop-cli-api.md, under Teams.
57
+ - **The Desktop follows team model 3.** With a 0.38 CLI (feature `team-model-3`) the Workspace ›
58
+ Teams page shows the workspace's `souls:` read only, and offers Add and Make default for local teams
59
+ only where the workspace says `localTeams: true`; where it doesn't, a local team still in
60
+ `oats-local.yaml` keeps Remove under the kernel's local-teams-closed message. A soul's *Teams here* is
61
+ read only: it says where the soul's default and teams come from and that they change as `souls:` in
62
+ `oats-workspace.yaml` (a PR to the workspace file). The Desktop never sends the removed `oats soul
63
+ teams` edit flags, and a 0.37 or older CLI keeps the team model v2 views.
64
+ - **oats.aweb 1.19.0** (catalog and workspace pin, and the bundled mirror),
65
+ for team model 3. A default team that comes from the workspace's
66
+ `defaultTeam` is reported as `from: "workspace"` in `oats aweb teams --json`
67
+ and the recorded meta (1.18 reported it as `deployment`). `oats aweb setup
68
+ --create` asks the kernel whether the workspace allows local teams before it
69
+ creates anything; where it does not, it creates the team and its per-team
70
+ root, records no local team, and prints the `teams:` entry (and
71
+ `defaultTeam:`) to commit in `oats-workspace.yaml`, or the `localTeams: true`
72
+ alternative. `--username` and setup's other advice say the same where local
73
+ teams are closed, and an unmapped default's remedy names `defaultTeam:` in
74
+ the workspace file. Joining stays limited to the eligible `OATS_TEAMS` rows.
75
+
76
+ - **oats.framework 1.5.0** (oats.core 2.3.0, oats.setup 2.3.0; catalog and
77
+ workspace pin `oats-framework/v1.5.0`): the skills
78
+ describe team model 3. `oats-teams`, `oats-workspace-config`,
79
+ `oats-setup-model`, `oats-onboarding` and the setup inject say a soul's teams
80
+ and default are committed in `souls:` of `oats-workspace.yaml`, local teams
81
+ only with `localTeams: true`; `oats-operate` and `oats-souls` say where an
82
+ instance's teams come from.
83
+
84
+ ## Upgrade
85
+
86
+ Before upgrading a deployment, its workspace must say what its
87
+ `oats-local.yaml` said. 0.36.1 and 0.37.x already accept the workspace keys,
88
+ and their `team-model-3-migration` warnings list what each deployment has to
89
+ move.
90
+
91
+ 1. **In the workspace file** (a PR, once everyone who reads it runs 0.36.1 or
92
+ later):
93
+ - commit `souls:` entries for what the deployments' `souls.teams` and
94
+ `souls.default` said. A v2 soul's teams were its default, `"*"`'s teams
95
+ and its own, and patterns never merge, so a soul's own entry lists
96
+ `"*"`'s teams too. Qualify bare soul names with their member's name
97
+ (`<member>/<soul>`, as `souls.disabled` names it). Running 0.38.0 against
98
+ an unmigrated `oats-local.yaml` prints this snippet for you;
99
+ - commit `defaultTeam: <label>` when the deployments' default is a shared
100
+ team;
101
+ - for local (personal) teams and a local default, choose: (a) keep them
102
+ personal, with `localTeams: true`; or (b) commit them in `teams:` (and the
103
+ default as `defaultTeam:`).
104
+ 2. **In each `oats-local.yaml`**, when the deployment moves to 0.38.0: remove
105
+ `souls.teams` and `souls.default`, and, under (b), the local `teams` and
106
+ `defaultTeam` (`oats teams remove` works while local teams are closed).
107
+ 3. **Check:** `oats teams` lists no problems, and `oats spawn <soul>
108
+ --preview` shows each soul's `defaultTeam` and `teams` as intended. Running
109
+ instances keep their spawn-time default; readiness reports
110
+ `default-team-changed` where it moved, and a respawn follows it.
111
+
112
+ What each kind of deployment changes:
113
+
114
+ - **A workspace whose people keep personal teams or a personal default** (for
115
+ example awebai/oats): `localTeams: true`, plus `souls:` for the souls'
116
+ shared teams (e.g. `"*": { teams: [oats] }`); drop `souls.teams` locally.
117
+ - **A workspace whose default team is a committed shared team**: commit
118
+ `defaultTeam: <label>` and the `souls:` entries; drop the local
119
+ `defaultTeam` (and any `souls.*` team keys).
120
+ - **A standalone deployment**: nothing changes for local teams and the local
121
+ default; drop `souls.teams` and `souls.default` (a standalone view has no
122
+ `souls:` patterns: every soul may join every local team).
123
+
124
+ Consumers: the Desktop's team views read `teamsApi: 1` / `soulTeamsApi: 1` and
125
+ edit through `oats soul teams --add`; they follow in their own change. Another
126
+ messaging provider receives `OATS_DEFAULT_TEAM_FROM=workspace` and `via` on
127
+ the `OATS_TEAMS` rows; one that records local teams with `oats teams add` needs
128
+ the workspace to allow them (oats.aweb 1.19.0 does both, above).
@@ -0,0 +1,50 @@
1
+ # OATS 0.38.1
2
+
3
+ ## Changed
4
+
5
+ - **oats.aweb 1.20.0: `oats aweb roster` lists who is on the team and says
6
+ what each entry is** (awebai/oats-aweb#46). The roster printed only the
7
+ team's membership certificates (`aw id team members`). That listing omits
8
+ identities the registry does not list, such as a team's retained global
9
+ coordinator and dashboard humans. It also listed deployment roots and
10
+ certificates with no workspace exactly like agents. The roster is now the
11
+ union of those certificates and the team's workspace presence
12
+ (`aw workspace status`), read from the minting root. Each entry has its
13
+ sources (`certificate`, `presence` or both), its status (active, when last
14
+ seen, or `certificate only: no workspace record`) and a kind:
15
+ - `global identity`, from the identity scope, listed first;
16
+ - `human` and `hosted agent`, from the session context;
17
+ - `instance` and `deployment root`, from the workspace path;
18
+ - `unknown`.
19
+ An inferred kind says so, and nothing is called retired. aw does not mark a
20
+ team's coordinator, and the roster says so. When a source cannot be read,
21
+ or presence is past its 200-workspace cap, the roster still lists what it
22
+ has and says it is incomplete. `oats aweb roster --json` is now an oats.aweb
23
+ document (`{team, members, certificatesComplete, presenceComplete,
24
+ problems}`) instead of aw's raw listing. The oats-aweb skill, the inject
25
+ and the spawn brief say what the roster shows and what it can't tell you.
26
+
27
+ ## Fixed
28
+
29
+ - **The Desktop opened from Finder no longer shows `/` as its workspace**
30
+ (awebai/oats#518). A Finder launch starts in `/`, and when no saved
31
+ workspace was a deployment, the Desktop served `/`. It titled the window
32
+ `/`, showed `E_NO_DEPLOYMENT` in Instances, and remembered `/` in
33
+ `windows.json` for every later launch. A folder that is not a deployment is
34
+ now never served, bound to a window or saved. With no deployment to open,
35
+ the window opens on the workspace switcher, which lists the deployments
36
+ directly inside `~/Agents` under "On this computer": one click opens one in
37
+ that window. A window remembered on `/` is forgotten at launch.
38
+
39
+ - **A window with no workspace lists the workspaces served after it opened**
40
+ (awebai/oats#521). Its choices were fixed when it opened, so remote
41
+ workspaces arriving a few seconds after launch never appeared and the
42
+ switcher said "No workspace choices reported."
43
+
44
+ - **Copying an agent's output in the Desktop** (awebai/oats#520). A plain
45
+ drag over a terminal now selects the text (tmux's copy mode, so it can
46
+ reach into the scrollback) and copies it to the clipboard on release;
47
+ ⌘C, Edit › Copy and the right-click menu find it there. The Desktop takes
48
+ tmux's copies (OSC 52) write-only: nothing in a terminal can read the
49
+ clipboard. Remote terminals (`oats session attach --server`) bind the same
50
+ drag. ⌘V is unchanged; Option-drag still makes a terminal-only selection.
@@ -164,8 +164,8 @@ composed skills and instructions, a spawn records:
164
164
  "key": "github.com/acme/agents", "commit": "3f2a9c1e…", "resolution": "20ec8ec527311d71d0973086",
165
165
  "soul": { "repoKey": "github.com/acme/agents", "commit": "3f2a9c1e…" }
166
166
  },
167
- "teams": [{ "label": "ana-acme", "team": "ana-acme:ana.aweb.ai", "default": true, "from": "local" },
168
- { "label": "engineering", "team": "engineering:acme.aweb.ai", "default": false, "from": "shared" }],
167
+ "teams": [{ "label": "ana-acme", "team": "ana-acme:ana.aweb.ai", "default": true, "from": "local", "via": ["default", "local"] },
168
+ { "label": "engineering", "team": "engineering:acme.aweb.ai", "default": false, "from": "shared", "via": ["workspace"] }],
169
169
  "defaultTeam": { "label": "ana-acme", "team": "ana-acme:ana.aweb.ai", "from": "deployment" }
170
170
  }
171
171
  ```
@@ -183,7 +183,8 @@ composed skills and instructions, a spawn records:
183
183
  compares `workspace.soul` with the member's current commit too: `soul: <name>
184
184
  from <member> @ <c7> [member moved since …]` (`--json`: `instances[].soul`).
185
185
  - `teams` / `defaultTeam`: the soul's teams at spawn, exactly as the providers
186
- received them (mapped teams only) and its default: evidence, never rewritten.
186
+ received them (mapped teams only, each with `via`: why the soul may join it)
187
+ and its default: evidence, never rewritten.
187
188
  A running home's hooks and messaging commands read the teams live
188
189
  ([capabilities.md](capabilities.md#teams-in-the-provider-environment)).
189
190
 
@@ -35,7 +35,7 @@ workspace commit. Schemas: [`oats-workspace.schema.json`](oats-workspace.schema.
35
35
  [`oats-membership.schema.json`](oats-membership.schema.json),
36
36
  [`soul.schema.json`](soul.schema.json), [`oats-local.schema.json`](oats-local.schema.json);
37
37
  the lock's format is in [packages](packages.md#lock-v3). The JSON schemas encode
38
- shapes; domain rules (declared teams, duplicate members, canonical `from:` keys,
38
+ shapes; domain rules (labels that are shared teams, duplicate members, canonical `from:` keys,
39
39
  the two `packages:` value forms) live in the kernel's `validateWorkspace` /
40
40
  `validateSoul`, which are the authority.
41
41
 
@@ -54,13 +54,16 @@ members: # repo refs, NO @revision (E_WORKSPAC
54
54
  - git:github.com/acme/tools # a member that ALSO publishes a package (see below)
55
55
 
56
56
  packages: # the ONLY versioned things
57
- oats.framework: v1.4.3 # bare version → resolves through the official catalog
57
+ oats.framework: v1.5.0 # bare version → resolves through the official catalog
58
58
  oats.okf: v4.1.1
59
59
  acme.tools: git:github.com/acme/tools@v0.4.0 # outside the catalog → git:<repo>@<tag|OID>; still a package
60
60
 
61
61
  teams: # SHARED teams: the same provider team for everyone
62
62
  engineering: { description: Platform and release automation, team: "engineering:acme.aweb.ai" }
63
63
  reviewers: { description: Code review } # declared, not created yet (no `team` id): readiness team-unmapped
64
+ defaultTeam: engineering # the workspace's default team; see Teams below
65
+ souls: # which teams each soul may join besides its default
66
+ platform/*: { teams: [reviewers] }
64
67
 
65
68
  defaults:
66
69
  capabilities:
@@ -146,7 +149,8 @@ its content digest.
146
149
  ### `oats-local.yaml`: the only per-machine file
147
150
 
148
151
  Which workspace this machine realizes, where member clones live, host-owned
149
- provider settings, this deployment's local teams and team membership, host
152
+ provider settings, this deployment's local teams and default (where the
153
+ workspace allows them), host
150
154
  facts for automations, and launch configurations. The full reference is
151
155
  [configuration.md](configuration.md).
152
156
 
@@ -308,89 +312,130 @@ Harnesses start normally, with their own skill discovery intact
308
312
 
309
313
  ## Teams
310
314
 
311
- A team is a messaging-provider team (for oats.aweb, an
312
- aweb team id `<team>:<namespace>`) under a **label**. Two files declare them:
313
-
314
- - **Shared teams**: the committed `oats-workspace.yaml` `teams.<label> =
315
- { description?, team? }`: the same provider team for everyone, edited by a PR.
316
- A shared team without `team` is declared but not created yet (readiness
317
- `team-unmapped`): its owner creates it with the messaging provider, then
318
- commits the id.
319
- - **Local teams**: the deployment's `oats-local.yaml` `teams.<label> = { team,
320
- description? }`: a team only this deployment uses (a personal team). A label in
321
- both files is `team-label-collision` (a warning); the **shared** definition
322
- wins, and the fix is renaming the local label.
323
-
324
- `oats-local.yaml` also says which teams each soul belongs to **here**:
325
-
326
- - `defaultTeam: <label>`: the team every instance of this deployment lives in
327
- (its default-team identity);
328
- - `souls.teams`: `"*"` for every soul, and a soul's own entry (its bare name, or
329
- `<package>/<soul>` for a package soul) adds to it;
330
- - `souls.default`: a per-soul override of `defaultTeam`; it must be one of that
331
- soul's teams (`E_TEAM_NOT_ELIGIBLE`).
332
-
333
- A soul's default is `souls.default[soul] ?? defaultTeam`; its teams are that
334
- default ∪ `souls.teams["*"]` ∪ `souls.teams[soul]`. A label no file declares is
335
- `E_TEAM_UNKNOWN` (a spawn, preview or `inspect --soul` of that soul is
336
- refused). At spawn an instance joins its **default** only; the others are
315
+ A team is a messaging-provider team (for oats.aweb, an aweb team id
316
+ `<team>:<namespace>`) under a **label**. An instance that sits in two teams is
317
+ a bridge between them: one process reads both inboxes and could relay anything
318
+ from one to the other. So which teams an organisation's instances may be in is
319
+ the organisation's decision, committed in its workspace file and closed by
320
+ default; a deployment's `oats-local.yaml` adds a team only where the workspace
321
+ allows it. Souls stay team-free in their own repositories: an organisation
322
+ says how souls behave in its teams in its `oats-workspace.yaml`, and someone
323
+ running the same souls standalone sees none of it.
324
+
325
+ **What this protects against, and what it does not.** The workspace's team
326
+ rules prevent **accidental** joins in a cooperative installation, and they make
327
+ the organisation's intended team set visible and reviewable in its git. That is
328
+ all they claim. They do not stop an operator from bridging teams on purpose:
329
+ whoever holds credentials for two teams can read under one identity and relay
330
+ under the other, or copy content outside the messaging layer, and distinct keys
331
+ do not prove distinct processes. The messaging provider's admission (for aweb,
332
+ the team controller key signs memberships; hosted invites are mediated by the
333
+ service) controls who holds credentials for a team, not what a process does
334
+ with what it reads. A deployment's authority comes from the credentials it
335
+ holds, not from its path or its name, and a team certificate carries no soul,
336
+ workspace or home-team claim (such metadata would be self-asserted, not a
337
+ control).
338
+
339
+ ### Where teams are declared
340
+
341
+ ```yaml
342
+ # oats-workspace.yaml (committed, edited by PR)
343
+ teams: # SHARED teams: the same provider team for everyone
344
+ engineering: { team: "engineering:acme.aweb.ai" }
345
+ security: { team: "security:acme.aweb.ai" }
346
+ docs: { description: Docs rota } # declared, not created yet: team-unmapped
347
+ defaultTeam: engineering # the workspace's fallback default team (a shared label)
348
+ localTeams: false # may deployments declare their own teams? (absent: false)
349
+ souls: # per soul pattern: its default team and the other teams it may join
350
+ "*": { teams: [] } # unlisted souls: default only (also what no entry means)
351
+ security-souls/*: { default: security, teams: [engineering] }
352
+ security-souls/incident-responder: { default: security, teams: [engineering, docs] }
353
+ oats.engineering/*: { teams: any } # any: every shared team of this file
354
+ ```
355
+
356
+ - **Shared teams** (`teams.<label> = { description?, team? }`): a shared team
357
+ without `team` is declared but not created yet (readiness `team-unmapped`):
358
+ its owner creates it with the messaging provider, then commits the id.
359
+ - **`souls:` keys are patterns**: `<member>/<soul>` or `<package>/<soul>`,
360
+ `<member>/*` or `<package>/*`, and `"*"`, with the member and package names
361
+ `souls.disabled` uses (a member's name is its repository's). A bare soul name
362
+ is refused. A key that is neither a pattern nor a soul the workspace offers
363
+ is the warning `team-soul-unknown` (a typo guard).
364
+ - **The most specific key wins outright** for a soul's teams, and lists never
365
+ merge: the soul's own key, then `<member|package>/*`, then `"*"`. The
366
+ default comes from the most specific key that sets one. So
367
+ `a/*: {default: security}` and `a/x: {teams: [docs]}` give `a/x` the default
368
+ `security` and the teams `[docs]` only.
369
+ - **A soul no key matches** (and no `"*"`) has its default only. That applies
370
+ to member and package souls alike: a workspace opens a package explicitly.
371
+ - **Every label** (`defaultTeam`, each `default`, each of `teams`) must be a
372
+ shared team of the same file; anything else is `E_WORKSPACE_SCHEMA` when the
373
+ file is read, never at a spawn on someone else's machine.
374
+ - **Local teams** (`oats-local.yaml` `teams.<label> = { team, description? }`)
375
+ and a local `defaultTeam` are allowed only when the workspace says
376
+ `localTeams: true`. Otherwise every spawn, preview and inspect is refused with
377
+ `E_WORKSPACE_SCHEMA` (reason `local-teams-closed`), naming both fixes: add
378
+ `localTeams: true` to the workspace file, or commit the teams and the
379
+ default there and remove them locally. A label in both files is
380
+ `team-label-collision` (a warning): the **shared** definition wins, and the
381
+ fix is renaming the local label.
382
+ - **The standalone view** (an explicit `standalone:`, or a fallback when the
383
+ workspace cannot be read) has no workspace rules: local teams and the local
384
+ `defaultTeam` apply there.
385
+ - `soul.yaml` and `oats-membership.yaml` say nothing about teams.
386
+ `oats-local.yaml` `souls.teams` and `souls.default` were removed in 0.38.0:
387
+ they are refused, and the refusal prints the `souls:` to commit instead.
388
+
389
+ ### A soul's default team and its teams
390
+
391
+ The default, in order:
392
+
393
+ 1. the soul's `default` from `souls:` (the most specific matching key that
394
+ sets one);
395
+ 2. else the deployment's `oats-local.yaml` `defaultTeam`, only when
396
+ `localTeams: true`;
397
+ 3. else the workspace's `defaultTeam`;
398
+ 4. else none (`E_TEAM_UNCONFIGURED` when a messaging layer is active).
399
+
400
+ `defaultTeam.from` says which: `soul`, `deployment` or `workspace`.
401
+
402
+ The teams a soul may join: its default, plus the `teams` of its most specific
403
+ matching `souls:` key, plus, with `localTeams: true`, every local team the
404
+ deployment declares. Each row says why (`via`: `default`, `workspace`,
405
+ `local`). At spawn an instance joins its **default** only; the others are
337
406
  eligible: offered, and joined on request through the provider (`join=` at
338
- spawn, or its own verbs later). Nothing committed besides the shared `teams:`
339
- says anything about teams, and capabilities compose from the workspace defaults
340
- and the soul only, the same for everyone. **A label never gates, restricts,
341
- changes trust or partitions the knowledge store.**
407
+ spawn, or its own verbs later), which refuses a team that is not eligible.
408
+ A local default no file declares is `E_TEAM_UNKNOWN`. Capabilities compose
409
+ from the workspace defaults and the soul only, the same for everyone. **A
410
+ label never gates, restricts, changes trust or partitions the knowledge
411
+ store.**
412
+
413
+ ### The verbs
342
414
 
343
- The verbs edit `oats-local.yaml` in place; they never call a provider:
415
+ They never call a provider. `oats teams add` and `oats teams default` edit
416
+ `oats-local.yaml` in place, where the workspace allows local teams; `oats teams
417
+ remove` runs anywhere (removing local teams is how a deployment moves them into
418
+ the workspace). A soul's teams are edited by a PR to `oats-workspace.yaml`:
419
+ `oats soul teams` only reads them.
344
420
 
345
421
  ```
346
- oats teams [--json] # this deployment's teams, ids, the default, problems
347
- oats teams add <label> --team <id> [--description d] # declare a local team (the first one becomes the default)
348
- oats teams remove <label> # refused while referenced (E_TEAM_IN_USE) or shared (E_TEAM_SHARED)
349
- oats teams default <label>
350
- oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <label> | --clear-default] [--json]
422
+ oats teams [--json] # the teams, ids, the default, localTeams, the workspace's souls:, problems
423
+ oats teams add <label> --team <id> [--description d] # declare a local team (the first one becomes the local default)
424
+ oats teams remove <label> # refused while it is the local default (E_TEAM_IN_USE) or shared (E_TEAM_SHARED)
425
+ oats teams default <label> # the local default
426
+ oats soul teams <soul>|'*' [--json] # a soul's default and teams here, and which souls: key gave them
351
427
  ```
352
428
 
353
- The messaging provider's own setup creates provider teams and records them with
354
- `oats teams add` (see the provider's documentation). The spawn preview, `inspect` and `oats souls` report a soul's
429
+ The messaging provider's own setup creates provider teams (see the provider's
430
+ documentation). The spawn preview, `inspect` and `oats souls` report a soul's
355
431
  `teams` and `defaultTeam`; readiness reports the team problems in
356
432
  `checks.configured` (`E_TEAM_UNCONFIGURED` when a messaging layer is active and
357
433
  there is no default; `team-unmapped`, blocking when it is the default;
358
- `default-team-changed` for a running instance). The provider receives them in
359
- its environment — see [capabilities.md](capabilities.md#teams-in-the-provider-environment).
360
- Exact shapes: [desktop-cli-api.md](desktop-cli-api.md#team-model-v2-feature-team-model-2-oats-0300-replaces-feature-teams).
361
-
362
- ### Preparing for team model 3 (0.36.x)
363
-
364
- OATS 0.38.0 commits a soul's teams in the workspace (team model 3,
365
- awebai/oats#484): the teams an organisation's instances may join become its
366
- own decision, visible and reviewable in its git, so a deployment's
367
- `oats-local.yaml` no longer adds one by accident. 0.36.x and 0.37.x prepare for it, so
368
- every workspace and deployment can migrate first:
369
-
370
- - **`oats-workspace.yaml` accepts the new keys** and validates them, but
371
- **does not apply them**: a soul's teams and default are still resolved as
372
- above, from `oats-local.yaml`.
373
-
374
- ```yaml
375
- defaultTeam: engineering # the workspace's fallback default team
376
- localTeams: true # deployments may declare their own teams (absent: false)
377
- souls: # per pattern: "*", <member|package>/*, <member|package>/<soul>
378
- "*": { teams: [] } # default only ({} says the same)
379
- security-souls/*: { default: security, teams: [engineering] }
380
- oats.engineering/*: { teams: any } # every shared team
381
- ```
382
-
383
- `<member|package>` is the name `souls.disabled` uses. Every label (`defaultTeam`,
384
- a `souls:` `default`, each of its `teams`) must be a shared team in `teams:` of
385
- the same file; anything else is `E_WORKSPACE_SCHEMA` when the file is read. A
386
- key naming a member or package the workspace does not have is not an error.
387
- - **The readiness warning `team-model-3-migration`** (never blocking) names
388
- what 0.38.0 will refuse: `souls.teams` / `souls.default` in `oats-local.yaml`
389
- (they move to `souls:`), and local `teams` / `defaultTeam` while the workspace
390
- does not say `localTeams: true` (fix: add `localTeams: true`, or commit the
391
- teams and `defaultTeam` in the workspace file). `oats teams`, readiness (and
392
- so the Desktop) and `oats doctor` show it. The migration steps are in the
393
- [0.36.1 release notes](release-notes/v0.36.1.md).
434
+ local-teams-closed; `team-soul-unknown`; `default-team-changed` for a running
435
+ instance). The provider receives them in its environment — see
436
+ [capabilities.md](capabilities.md#teams-in-the-provider-environment). Exact
437
+ shapes: [desktop-cli-api.md](desktop-cli-api.md#team-model-3-feature-team-model-3-oats-0370-replaces-feature-team-model-2).
438
+ Why it is shaped this way: [team model 3](design/2026-10-02-team-model-3.md).
394
439
 
395
440
  ## Provider payloads have three homes
396
441