@awebai/oats 0.36.0 → 0.36.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.
package/bin/oats.mjs CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  writeFileAtomic, LOCK_FILE, readLock, readLockIfPresent, writeLock, resolvePackages, memoizedRemote,
37
37
  classifyPackageValue, parsePackageRequest } from "../lib/packages.mjs";
38
38
  import { loadLocal, validateWorkspace, validateLocal, discoverPackageSouls, workspaceWarnings, memberRowByKey } from "../lib/workspace.mjs";
39
- import { recordedTeams, reportRows, soulKeyOf, soulTeams, teamModel } from "../lib/teams.mjs";
39
+ import { migrationProblems, recordedTeams, reportRows, soulKeyOf, soulTeams, teamModel } from "../lib/teams.mjs";
40
40
  import { launchLayers } from "../lib/launch-preference.mjs";
41
41
  import { parseConfigData } from "../lib/config-data.mjs";
42
42
  import * as remoteModule from "../lib/remote.mjs";
@@ -221,6 +221,19 @@ async function doctorComposition(ctx, soulName, ws, bail) {
221
221
  } finally { for (const c of cleanups) { try { c(); } catch { /* best effort: temporary copies only */ } } }
222
222
  }
223
223
 
224
+ /** team-model-3-migration in doctor (0.36.x), OFFLINE like the rest of doctor: oats-local.yaml, and for its
225
+ * local teams the workspace file this machine's parsed cache holds (cachedWorkspace: no git process, no
226
+ * network). Without that file, whether local teams need `localTeams: true` is said to be unchecked
227
+ * (information), never guessed. The standalone view has no workspace rules. → { problems, information } */
228
+ function doctorTeamMigration(local) {
229
+ const standalone = typeof local.standalone === "string" && local.standalone !== "";
230
+ const file = standalone ? null : cachedWorkspace(local.workspace)?.file ?? null;
231
+ const model = teamModel(file, local);
232
+ const unchecked = !standalone && file === null && model.migration.teamKeys.length > 0;
233
+ return { problems: migrationProblems(model),
234
+ information: unchecked ? ["team-model-3-migration: whether oats-local.yaml teams/defaultTeam need localTeams: true couldn't be checked: this deployment hasn't observed its workspace yet; run oats sync"] : [] };
235
+ }
236
+
224
237
  /** Workspace-model v2 doctor data, OFFLINE: the deployment declaration found
225
238
  * walking up from ctx (oats-local.yaml) and the lock v3 beside it. Doctor never
226
239
  * goes to the network for this view (only `--soul`, which resolves the soul like a
@@ -230,7 +243,7 @@ function doctorLockData(ctx) {
230
243
  let lockDir = ctx;
231
244
  try {
232
245
  const found = loadLocal(ctx);
233
- out.local = { path: found.path, workspace: found.local.workspace };
246
+ out.local = { path: found.path, workspace: found.local.workspace, value: found.local };
234
247
  lockDir = dirname(found.path);
235
248
  } catch (e) {
236
249
  // An unreadable oats-local.yaml, or a 0.25 oats-config.yaml inside the deployment
@@ -584,12 +597,13 @@ function legacyLayoutProblems(root) {
584
597
  async function doctorWorkspaceJson(ctx, soulName, ws) {
585
598
  const composition = await doctorComposition(ctx, soulName, ws, (code, msg, details) => jsonFail(code, msg, details));
586
599
  const agentsRoot = join(dirname(ws.local.path), "agents");
587
- const problems = [...legacyLayoutProblems(agentsRoot), readableInstanceHomes(agentsRoot)].filter(Boolean);
600
+ const migration = doctorTeamMigration(ws.local.value);
601
+ const problems = [...legacyLayoutProblems(agentsRoot), readableInstanceHomes(agentsRoot), ...migration.problems].filter(Boolean);
588
602
  return {
589
603
  schemaVersion: 1, workspaceApi: 2, context: ctx,
590
604
  workspace: { file: ws.local.path, ref: ws.local.workspace },
591
605
  workspaceError: ws.localError, lockFile: ws.lockFile, packages: ws.packages, lockError: ws.lockError,
592
- information: operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : [],
606
+ information: [...(operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : []), ...migration.information],
593
607
  composedInstructions: composition?.text, instructionBlocks: composition?.blocks,
594
608
  ...(problems.length ? { problems } : {}),
595
609
  };
@@ -624,7 +638,10 @@ async function doctor(dir) {
624
638
  const composition = await doctorComposition(ctx, soulName, ws, (code, msg) => die(`${msg} [${code}]`));
625
639
  printDoctorWorkspace(ws);
626
640
  const agentsRoot = join(dirname(ws.local.path), "agents");
641
+ const migration = doctorTeamMigration(ws.local.value);
627
642
  for (const p of [...legacyLayoutProblems(agentsRoot), readableInstanceHomes(agentsRoot)].filter(Boolean)) console.log(`\n! ${p.code}: ${p.message}`);
643
+ for (const p of migration.problems) console.log(`\n! ${p.code}: ${p.message} — ${p.fix}`);
644
+ for (const line of migration.information) console.log(`\nINFO: ${line}`);
628
645
  if (soulName) {
629
646
  const information = operationalKnowledgeNote(composition, soulName);
630
647
  if (information) console.log(`\nINFO: ${information}`);
@@ -81,6 +81,12 @@ refused (`E_WORKSPACE_SCHEMA`).
81
81
  How teams are resolved, and what a messaging provider does with them, is in
82
82
  [workspaces.md](workspaces.md#teams).
83
83
 
84
+ OATS 0.37.0 (team model 3) removes `souls.teams` and `souls.default` (they move
85
+ to `souls:` in `oats-workspace.yaml`) and allows `teams` and `defaultTeam` here
86
+ only when the workspace file says `localTeams: true`. 0.36.x still applies all
87
+ four keys and warns about them (`team-model-3-migration`): see
88
+ [Preparing for team model 3](workspaces.md#preparing-for-team-model-3-036x).
89
+
84
90
  ## Launch configurations
85
91
 
86
92
  An entry has `harness` (`pi` \| `claude` \| `codex`, required), `executable`
@@ -725,7 +725,7 @@ Read-only (it writes no lock):
725
725
  "souls":["rm"],"capabilities":["nw-house-style"],"publishes":null,"url":"https://github.com/nw/agents/tree/66566512…",
726
726
  "membershipFile":{"path":"oats-membership.yaml","url":"https://github.com/nw/agents/blob/66566512…/oats-membership.yaml"}}],
727
727
  "packages":[{"id":"oats.okf","version":"3.0.0","source":"catalog:oats.okf","commit":"ab897841…","integrity":"sha256-bada35…",
728
- "capabilities":["oats.okf"],"souls":[],"latest":{"version":"4.1.0","ref":"v4.1.0"}}],
728
+ "capabilities":["oats.okf"],"souls":[],"latest":{"version":"4.1.1","ref":"v4.1.1"}}],
729
729
  "declaredPackages":["oats.framework","oats.okf"],"unsynced":["oats.framework"],"stale":[],
730
730
  "external":[{"source":"git:github.com/oss/experts@3c606e09…","soul":"security-reviewer"}],
731
731
  "problems":[],"warnings":[],
@@ -784,8 +784,8 @@ packages' capabilities and souls, sorted by name, then origin. Both carry
784
784
  "defaultTeam":{"label":"mine","team":"mine:ana.aweb.ai","from":"deployment"},
785
785
  "private":false,"path":"souls/writer","work":"directory","description":"Drafts campaigns.","harness":"pi","model":null,"harnessFrom":"kernel-default",
786
786
  "file":{"path":"souls/writer/soul.yaml","url":null},"spawnable":true,"problem":null},
787
- {"name":"knowledge-maintainer","qualifiedName":"oats.okf/knowledge-maintainer","origin":"package oats.okf v4.1.0","kind":"package","package":"oats.okf",
788
- "version":"4.1.0","repoKey":"github.com/awebai/oats-okf","commit":"e331a996…","teams":null,"defaultTeam":null,"private":false,
787
+ {"name":"knowledge-maintainer","qualifiedName":"oats.okf/knowledge-maintainer","origin":"package oats.okf v4.1.1","kind":"package","package":"oats.okf",
788
+ "version":"4.1.1","repoKey":"github.com/awebai/oats-okf","commit":"e1d604f7…","teams":null,"defaultTeam":null,"private":false,
789
789
  "path":"oats-package/souls/knowledge-maintainer","work":"directory","description":"Reviews harvested knowledge.","harness":"pi","model":null,
790
790
  "harnessFrom":"kernel-default","file":{"path":"oats-package/souls/knowledge-maintainer/soul.yaml","url":null},
791
791
  "spawnable":false,"problem":{"code":"E_TEAM_UNKNOWN","message":"team \"reviewers\" is not declared (oats-local.yaml#/souls/teams/…)"}}],
@@ -874,8 +874,8 @@ nothing reads a working clone.
874
874
  **The show:**
875
875
 
876
876
  ```json
877
- {"capabilityShowApi":1,"name":"oats.okf","kind":"package","repoKey":"github.com/awebai/oats-okf","package":"oats.okf","version":"4.1.0",
878
- "commit":"e331a996…","path":"oats-package/capabilities/oats-okf",
877
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","repoKey":"github.com/awebai/oats-okf","package":"oats.okf","version":"4.1.1",
878
+ "commit":"e1d604f7…","path":"oats-package/capabilities/oats-okf",
879
879
  "inject":{"path":"injects/okf.md","bytes":2422,"text":"## Knowledge: OKF\n\nYou have two kinds of knowledge. …","binary":false,"truncated":false},
880
880
  "skills":[{"name":"okf-consultation","path":"skills/okf-consultation","description":"Consulting your soul's knowledge with the `oats okf` CLI: …",
881
881
  "files":[{"path":"skills/okf-consultation/SKILL.md","bytes":6947},{"path":"skills/okf-consultation/references/consult.md","bytes":4465}],
@@ -901,7 +901,7 @@ nothing reads a working clone.
901
901
  **The `--file` answer:**
902
902
 
903
903
  ```json
904
- {"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"e331a996…",
904
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"e1d604f7…",
905
905
  "file":{"path":"skills/okf-instance-knowledge/SKILL.md","bytes":4787,"text":"---\nname: okf-instance-knowledge\n…","binary":false,"truncated":false}}
906
906
  ```
907
907
 
@@ -1229,9 +1229,38 @@ for a failure and `false` for a warning, plus the problem's own keys.
1229
1229
  | `default-team-changed` | warning | `recorded`, `current` | `--home` with live teams: the default changed since the spawn |
1230
1230
  | `E_TEAM_UNKNOWN` | failure | `label`, `at` | a reference to an undeclared label |
1231
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) |
1232
1233
 
1233
- The last two are also spawn, preview and inspect refusals, with the same
1234
- details.
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.
1249
+
1250
+ ```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",
1253
+ "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
+ ```
1255
+
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"`.
1235
1264
 
1236
1265
  <a id="soul-launch-preferences-feature-launch-preference-oats-0300"></a>
1237
1266
  ## Launch preferences
@@ -40,7 +40,7 @@ arrives from.
40
40
  ```yaml
41
41
  # oats-workspace.yaml: one default per slot, for every soul
42
42
  packages:
43
- oats.okf: v4.1.0
43
+ oats.okf: v4.1.1
44
44
  oats.aweb: v1.17.7
45
45
  oats.linear: v1.0.1
46
46
  oats.jira: v1.0.1
package/docs/knowledge.md CHANGED
@@ -26,7 +26,7 @@ The workspace pins the package and fills the slot for every soul by default:
26
26
  ```yaml
27
27
  # oats-workspace.yaml (excerpt)
28
28
  packages:
29
- oats.okf: v4.1.0
29
+ oats.okf: v4.1.1
30
30
  defaults:
31
31
  knowledge: { oats.okf: { from: package } }
32
32
  stores:
@@ -27,6 +27,20 @@
27
27
  "additionalProperties": { "$ref": "#/$defs/team" },
28
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."
29
29
  },
30
+ "defaultTeam": {
31
+ "$ref": "#/$defs/label",
32
+ "description": "Team model 3 (0.37.0): the workspace's fallback default team, a label of teams: in this file. 0.36.x accepts and validates it without applying it."
33
+ },
34
+ "localTeams": {
35
+ "type": "boolean",
36
+ "description": "Team model 3 (0.37.0): whether deployments may declare their own teams and defaultTeam in oats-local.yaml. Absent: false. 0.36.x accepts it without applying it."
37
+ },
38
+ "souls": {
39
+ "type": "object",
40
+ "propertyNames": { "type": "string", "pattern": "^(?:\\*|[a-z0-9][a-z0-9._-]*/(?:\\*|[a-z0-9]+(?:-[a-z0-9]+)*))$" },
41
+ "additionalProperties": { "$ref": "#/$defs/soulTeams" },
42
+ "description": "Team model 3 (0.37.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 accepts and validates it without applying it."
43
+ },
30
44
  "defaults": { "$ref": "#/$defs/defaults" },
31
45
  "stores": {
32
46
  "type": "object",
@@ -111,6 +125,20 @@
111
125
  "team": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@/+-]{0,255}$", "description": "The messaging provider's team id (for oats.aweb, <team>:<namespace>). Absent: declared, not yet created. The kernel's safety rule: never '-'-led, no whitespace or control characters, at most 256 characters; the provider validates its own shape." }
112
126
  }
113
127
  },
128
+ "soulTeams": {
129
+ "type": "object",
130
+ "additionalProperties": false,
131
+ "properties": {
132
+ "default": { "$ref": "#/$defs/label", "description": "The soul's default team: a label of teams: in this file." },
133
+ "teams": {
134
+ "anyOf": [
135
+ { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/label" } },
136
+ { "const": "any" }
137
+ ],
138
+ "description": "The other teams the soul may join: labels of teams: in this file, or \"any\" for every one. [] (or no teams) is default only."
139
+ }
140
+ }
141
+ },
114
142
  "defaults": {
115
143
  "type": "object",
116
144
  "additionalProperties": false,
@@ -9,8 +9,8 @@ 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.1` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
13
- | `oats.okf` | `v4.1.0` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
12
+ | `oats.framework` | `oats-framework/v1.4.2` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
13
+ | `oats.okf` | `v4.1.1` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
14
14
  | `oats.aweb` | `v1.17.7` | `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` | |
@@ -27,7 +27,7 @@ no lock and adds nothing to an existing workspace.
27
27
  ## Find and use packages
28
28
 
29
29
  - A workspace pins an official package by **bare version** in its
30
- `packages:` map (`oats.okf: v4.1.0`); `oats sync` resolves it through the
30
+ `packages:` map (`oats.okf: v4.1.1`); `oats sync` resolves it through the
31
31
  catalog to an exact commit, fetches it, verifies its integrity and locks it.
32
32
  A package outside the catalog is written `git:<repo>@<ref>`. Pinning does
33
33
  not join a team or adopt the publisher's workspace. See
package/docs/packages.md CHANGED
@@ -44,14 +44,14 @@ whole organisation:
44
44
 
45
45
  ```yaml
46
46
  packages:
47
- oats.okf: v4.1.0 # bare version → the official catalog
47
+ oats.okf: v4.1.1 # bare version → the official catalog
48
48
  acme.tools: git:github.com/acme/tools@v0.4.0 # direct ref: git:<repo>@<tag or full OID>
49
49
  ```
50
50
 
51
- - **Bare version** (`v4.1.0`, `4.1.0`, `1.0.0-rc.1`): the id is looked up in
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.0` or `oats-framework/v1.4.1`) and the payload path. An id
54
+ convention (`v4.1.1` or `oats-framework/v1.4.2`) 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,8 +74,8 @@ members:
74
74
  - git:github.com/acme/agents
75
75
  - git:github.com/acme/platform
76
76
  packages:
77
- oats.framework: v1.4.1
78
- oats.okf: v4.1.0
77
+ oats.framework: v1.4.2
78
+ oats.okf: v4.1.1
79
79
  oats.aweb: v1.17.7
80
80
  teams:
81
81
  platform: { team: "platform:acme.aweb.ai", description: Platform engineering }
@@ -105,7 +105,7 @@ decision recorded in the lock.
105
105
  $ oats sync
106
106
  workspace acme (github.com/acme/agents @ 3f2a9c1e)
107
107
  members agents ✓↔ (@ 3f2a9c1e) platform ✓↔ (@ 77c0a1b2) billing ✗ (no-backlink)
108
- packages acme.tools 0.4.0 ✓ (@ 47f4b816) oats.okf 4.1.0 ✓ (@ e331a996)
108
+ packages acme.tools 0.4.0 ✓ (@ 47f4b816) oats.okf 4.1.1 ✓ (@ e1d604f7)
109
109
  changed acme.tools — → 0.4.0 (@ 47f4b816)
110
110
  souls 9 discovered (6 members, 1 external, 2 package, 0 disabled here) · 0 private capabilities
111
111
  teams platform (shared) · this deployment's: oats teams
@@ -160,8 +160,8 @@ same workspace commit hold identical locks.
160
160
  "source": "catalog:oats.okf",
161
161
  "url": "https://github.com/awebai/oats-okf.git",
162
162
  "path": "oats-package",
163
- "version": "4.1.0",
164
- "commit": "e331a9969d10aabddaa5824991f1846c7dedb388",
163
+ "version": "4.1.1",
164
+ "commit": "e1d604f70c5e4cdc39602095f139383e61f69323",
165
165
  "integrity": "sha256-…",
166
166
  "capabilities": ["oats.okf", "oats.okf-harvest", "oats.okf-maintenance"]
167
167
  },
@@ -331,12 +331,12 @@ A soul that names one of the package's capabilities with
331
331
  {
332
332
  "policy": "docs/official-catalog.md",
333
333
  "packages": {
334
- "oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v4.1.0", "path": "oats-package" },
335
- "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.4.1", "path": "oats-package" }
334
+ "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.2", "path": "oats-package" }
336
336
  }
337
337
  }
338
338
  ```
339
339
 
340
- `ref` carries the tag convention: a workspace's `oats.framework: v1.4.1`
341
- resolves to tag `oats-framework/v1.4.1`. Resolving through the catalog never
340
+ `ref` carries the tag convention: a workspace's `oats.framework: v1.4.2`
341
+ resolves to tag `oats-framework/v1.4.2`. Resolving through the catalog never
342
342
  advances a lock by itself: `oats sync` does, and says so.
@@ -0,0 +1,103 @@
1
+ # OATS 0.36.1
2
+
3
+ ## Changed
4
+
5
+ - **oats.okf 4.1.1** (catalog and workspace pin, and the bundled mirrors):
6
+ three fixes to harvest completion and `harvest --once`.
7
+ - `oats okf complete` on an amended PR that is still open
8
+ (awebai/oats-okf#36). After the knowledge-maintainer pushed an amendment
9
+ on top of the delivered commit, `complete --run <id>` failed with `E_PR:
10
+ publication branch has unexpected commit; never force push`. The run is
11
+ now reported `delivered` when the known PR is open at the branch's tip
12
+ and Git shows that tip descends from the delivered commit; the answer
13
+ names the amended head. A tip that does not contain the delivered commit
14
+ is still refused with `E_PR`, and nothing is ever force-pushed. Ancestry
15
+ is read from the commit objects alone, so a checkout's grafts or
16
+ commit-graph file cannot fake it.
17
+ - `harvest --once`: one-shots of a seat no longer race
18
+ (awebai/oats-okf#39). The overlap checks and the install run under one
19
+ seat lock, so of two overlapping one-shots started together one installs
20
+ and the other gets `E_ONCE_OVERLAP`.
21
+ - `harvest --once`: a note edited between runs no longer strands a
22
+ draining one-shot (awebai/oats-okf#40). A rerun with the same manifest
23
+ continues from custody without reading the listed notes. Another
24
+ manifest listing a note a one-shot already holds is refused with
25
+ `E_ONCE_OVERLAP`, naming that one-shot, instead of a `sha256 mismatch`.
26
+ - **Preparing for team model 3** (awebai/oats#485). OATS 0.37.0 commits the
27
+ teams an organisation's souls may join, and their default team, in the
28
+ workspace file, closed by default (awebai/oats#484), so the organisation's
29
+ intended team set is visible and reviewable in its git and a deployment's
30
+ `oats-local.yaml` no longer adds a team by accident. 0.36.1 lets every workspace and
31
+ deployment migrate before 0.37.0 refuses the old shape:
32
+ - `oats-workspace.yaml` accepts `defaultTeam`, `localTeams` and `souls:`,
33
+ and validates them: every label they name must be a shared team in
34
+ `teams:` of the same file (`E_WORKSPACE_SCHEMA` otherwise, when the file
35
+ is read). 0.36.1 does **not** apply them: a soul's teams and default are
36
+ still resolved from `oats-local.yaml`. Earlier releases refuse these keys,
37
+ so add them only once everyone who reads the workspace runs 0.36.1 or
38
+ later.
39
+ - A new readiness warning, `team-model-3-migration` (never blocking), names
40
+ what 0.37.0 will refuse. `oats teams`, `oats readiness` (and so the
41
+ Desktop's readiness view) and `oats doctor` show it. Its `condition` is
42
+ `local-soul-teams` when `oats-local.yaml` has `souls.teams` or
43
+ `souls.default`, and `local-teams-closed` when `oats-local.yaml` declares
44
+ `teams` or `defaultTeam` and the workspace file does not say
45
+ `localTeams: true` (never in the standalone view). `oats doctor` stays
46
+ offline: it checks `local-teams-closed` against the workspace file this
47
+ machine last observed, and says so when there is none yet (`oats sync`
48
+ fixes that). The shapes are in docs/desktop-cli-api.md, under Team
49
+ readiness items.
50
+ - **oats.framework 1.4.2** (oats.setup 2.2.1; catalog and workspace pin
51
+ `oats-framework/v1.4.2`) ships the oats.setup skill changes made since
52
+ 1.4.1, which deployments had not received:
53
+ - `oats-onboarding`: declare how this machine starts a harness once, as
54
+ that harness's default launch configuration (0.32.0, awebai/oats#368);
55
+ `oats spawn --preview` names the launch configuration that applies.
56
+ - `oats-workspace-config`: a soul's launch preference versus a host launch
57
+ configuration, `E_LAUNCH_CONFIG_INVALID` and `E_CLAUDE_CONFIG_REMOVED`
58
+ (awebai/oats#368), and the team model 3 workspace keys `defaultTeam`,
59
+ `localTeams` and `souls:` (awebai/oats#495).
60
+ - `oats-teams`: preparing for team model 3, and what each
61
+ `team-model-3-migration` condition asks for (awebai/oats#495).
62
+ - `oats-package-pins`: the example pins `oats.okf: v4.1.1`
63
+ (awebai/oats#494).
64
+
65
+ ## What 0.37.0 will change
66
+
67
+ - **`souls:` in `oats-workspace.yaml` decides which teams a soul may join**
68
+ besides its default: the most specific key wins (`<member|package>/<soul>`,
69
+ then `<member|package>/*`, then `"*"`); `teams` is a list of shared labels
70
+ or `any`. A soul no key matches joins its default only. This applies to
71
+ package souls too: a workspace opens a package explicitly.
72
+ - **A soul's default team**, in order: its `souls:` `default`; else the
73
+ deployment's `defaultTeam`, only when the workspace says
74
+ `localTeams: true`; else the workspace's `defaultTeam`.
75
+ - **`oats-local.yaml` `souls.teams` and `souls.default` are refused**
76
+ (`E_WORKSPACE_SCHEMA`, reason `removed-key`), and so are `oats soul teams
77
+ --add/--remove/--default/--clear-default`.
78
+ - **Local `teams` and `defaultTeam` are refused unless the workspace says
79
+ `localTeams: true`**, and so are `oats teams add/remove/default`. The
80
+ standalone view still allows them.
81
+
82
+ ## Upgrade
83
+
84
+ Nothing is required for 0.36.1 itself. To be ready for 0.37.0
85
+ (`oats teams` lists each deployment's `team-model-3-migration` warnings):
86
+
87
+ 1. **Now**, once everyone who reads the workspace runs 0.36.1 or later,
88
+ commit in `oats-workspace.yaml` the choices each deployment makes locally:
89
+ - a `souls:` entry for what `souls.teams` says (`"*": { teams: [...] }`
90
+ for every soul, `<member|package>/<soul>: { teams: [...] }` for one;
91
+ local keys are bare soul names, workspace keys are qualified by the
92
+ member or package, as in `souls.disabled`), with a `default:` for what
93
+ `souls.default` says;
94
+ - for local teams and a local `defaultTeam`, choose one: (a) keep them
95
+ personal, and add `localTeams: true`, which clears `local-teams-closed`
96
+ at once; or (b) commit the teams in `teams:` and the default as
97
+ `defaultTeam:`.
98
+ 2. **When the deployment moves to 0.37.0**, remove from its `oats-local.yaml`
99
+ `souls.teams`, `souls.default` and, under (b), the local `teams` and
100
+ `defaultTeam`. Not before: 0.36.1 does not apply the workspace keys, so
101
+ those local keys are still what decides a soul's teams and default until
102
+ then, and their warnings stay until they go. 0.37.0 refuses them, naming
103
+ the replacement.
@@ -152,8 +152,8 @@ composed skills and instructions, a spawn records:
152
152
  "commit": "3f2a9c1e…", "digest": "sha256-…", "materializedAt": "2026-09-24T10:12:44.118Z"
153
153
  },
154
154
  "oats.okf": {
155
- "from": { "kind": "package", "package": "oats.okf", "version": "4.1.0", "commit": "e331a996…", "integrity": "sha256-…", "repoKey": "github.com/awebai/oats-okf" },
156
- "commit": "e331a996…", "digest": "sha256-…", "materializedAt": "2026-09-24T10:12:44.201Z"
155
+ "from": { "kind": "package", "package": "oats.okf", "version": "4.1.1", "commit": "e1d604f7…", "integrity": "sha256-…", "repoKey": "github.com/awebai/oats-okf" },
156
+ "commit": "e1d604f7…", "digest": "sha256-…", "materializedAt": "2026-09-24T10:12:44.201Z"
157
157
  }
158
158
  },
159
159
  "providers": {
@@ -54,8 +54,8 @@ 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.1 # bare version → resolves through the official catalog
58
- oats.okf: v4.1.0
57
+ oats.framework: v1.4.2 # bare version → resolves through the official catalog
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
@@ -359,6 +359,39 @@ there is no default; `team-unmapped`, blocking when it is the default;
359
359
  its environment — see [capabilities.md](capabilities.md#teams-in-the-provider-environment).
360
360
  Exact shapes: [desktop-cli-api.md](desktop-cli-api.md#team-model-v2-feature-team-model-2-oats-0300-replaces-feature-teams).
361
361
 
362
+ ### Preparing for team model 3 (0.36.x)
363
+
364
+ OATS 0.37.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 prepares 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.37.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).
394
+
362
395
  ## Provider payloads have three homes
363
396
 
364
397
  | What it is | Where | Example |
package/lib/teams.mjs CHANGED
@@ -17,6 +17,10 @@
17
17
  * Rows (docs/desktop-cli-api.md, Team model v2): TeamRow { label, team, default, from: shared|local },
18
18
  * the default first, then by label. Reports carry unmapped rows (team null); OATS_TEAMS and
19
19
  * instance.json carry mapped rows only. DefaultTeam { label, team, from: deployment|soul } | null.
20
+ *
21
+ * Team model 3 (0.37.0, awebai/oats#484) moves these choices into the committed workspace file. 0.36.x
22
+ * validates its keys (lib/workspace.mjs) without applying them, and warns about what 0.37.0 will refuse
23
+ * (migrationProblems: team-model-3-migration).
20
24
  */
21
25
  import { oatsError } from "./errors.mjs";
22
26
 
@@ -53,11 +57,19 @@ export function teamModel(workspace, local, { workspaceKey = null } = {}) {
53
57
  localTeams.set(label, { label, team: str(def?.team), description: str(def?.description), from: "local", at: `${LOCAL_FILE}#/teams/${pointerKey(label)}` });
54
58
  }
55
59
  const souls = isObject(local?.souls) ? local.souls : {};
60
+ const has = (v, k) => isObject(v) && Object.hasOwn(v, k);
56
61
  return {
57
62
  shared, local: localTeams,
58
63
  labels: new Map([...localTeams, ...shared]), // the committed definition wins a collision
59
64
  defaultTeam: str(local?.defaultTeam),
60
65
  souls: { teams: isObject(souls.teams) ? souls.teams : {}, default: isObject(souls.default) ? souls.default : {} },
66
+ // Team model 3 (0.37.0) moves these keys; 0.36.x only warns (migrationProblems). `localTeams` is
67
+ // the workspace's answer, null without a workspace file (the standalone view has no workspace rules).
68
+ migration: {
69
+ soulKeys: ["teams", "default"].filter((k) => has(local?.souls, k)).map((k) => `souls.${k}`),
70
+ teamKeys: ["teams", "defaultTeam"].filter((k) => has(local, k)),
71
+ localTeams: isObject(workspace) ? workspace.localTeams === true : null,
72
+ },
61
73
  };
62
74
  }
63
75
 
@@ -130,6 +142,27 @@ const unconfiguredProblem = () => ({ code: "E_TEAM_UNCONFIGURED", severity: "fai
130
142
  const refusalProblem = (e) => ({ code: e.code, ...e.details, severity: "failure", message: e.message,
131
143
  fix: e.code === "E_TEAM_UNKNOWN" ? "declare the team (`oats teams add`), or remove the reference" : "add the label to the soul's teams (`oats soul teams … --add`), or clear its default (`--clear-default`)" });
132
144
 
145
+ /** The fields of a team-model-3-migration problem other than code/severity/message/fix, as every
146
+ * surface (oats teams, readiness, doctor) carries them. */
147
+ export const MIGRATION_CODE = "team-model-3-migration";
148
+ /**
149
+ * The team-model-3-migration warnings (0.36.x; 0.37.0 refuses what they name): `local-soul-teams` when
150
+ * oats-local.yaml has souls.teams / souls.default, `local-teams-closed` when it declares teams /
151
+ * defaultTeam and the workspace file does not say `localTeams: true` (never without a workspace file).
152
+ */
153
+ export function migrationProblems(model) {
154
+ const m = model.migration, problems = [];
155
+ if (m.soulKeys.length) problems.push({ code: MIGRATION_CODE, severity: "warning", condition: "local-soul-teams", keys: [...m.soulKeys],
156
+ message: `oats-local.yaml ${m.soulKeys.join(", ")}: OATS 0.37.0 refuses ${m.soulKeys.length > 1 ? "these keys" : "this key"}; which teams a soul may join, and its default, move to souls: in oats-workspace.yaml`,
157
+ fix: `commit the same choices as souls: entries in oats-workspace.yaml ("*" or <member|package>/<soul>: { default, teams }), then remove ${m.soulKeys.join(" and ")} from oats-local.yaml` });
158
+ if (m.teamKeys.length && m.localTeams === false) problems.push(localTeamsClosedProblem(m.teamKeys));
159
+ return problems;
160
+ }
161
+ /** `local-teams-closed` for the oats-local.yaml `keys` found (doctor builds it from its offline read). */
162
+ export const localTeamsClosedProblem = (keys) => ({ code: MIGRATION_CODE, severity: "warning", condition: "local-teams-closed", keys: [...keys],
163
+ message: `oats-local.yaml declares ${keys.join(", ")}, 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`,
164
+ 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" });
165
+
133
166
  /**
134
167
  * Readiness problems. Without `key`: the deployment's (`oats teams`): collisions, unmapped shared
135
168
  * teams (a failure when it is `defaultTeam`), every unknown reference, every ineligible
@@ -141,11 +174,11 @@ export function teamProblems(model, { key = null, messaging = false } = {}) {
141
174
  if (key !== null) {
142
175
  let t;
143
176
  try { t = soulTeams(model, key); }
144
- catch (e) { if (e.code === "E_TEAM_UNKNOWN" || e.code === "E_TEAM_NOT_ELIGIBLE") return [refusalProblem(e)]; throw e; }
177
+ catch (e) { if (e.code === "E_TEAM_UNKNOWN" || e.code === "E_TEAM_NOT_ELIGIBLE") return [refusalProblem(e), ...migrationProblems(model)]; throw e; }
145
178
  for (const r of t.teams) if (model.shared.has(r.label) && model.local.has(r.label)) problems.push(collisionProblem(r.label, model.shared.get(r.label), model.local.get(r.label)));
146
179
  for (const r of t.teams) if (r.team === null) problems.push(unmappedProblem(model.labels.get(r.label), r.default));
147
180
  if (messaging && t.defaultTeam === null) problems.push(unconfiguredProblem());
148
- return problems;
181
+ return [...problems, ...migrationProblems(model)];
149
182
  }
150
183
  const labels = [...model.labels.keys()].sort(byCodepoint);
151
184
  for (const label of labels) if (model.shared.has(label) && model.local.has(label)) problems.push(collisionProblem(label, model.shared.get(label), model.local.get(label)));
@@ -159,7 +192,7 @@ export function teamProblems(model, { key = null, messaging = false } = {}) {
159
192
  try { soulTeams(model, k); } catch (e) { if (e.code === "E_TEAM_NOT_ELIGIBLE" && e.details.soul === k) problems.push(refusalProblem(e)); else if (e.code !== "E_TEAM_UNKNOWN") throw e; }
160
193
  }
161
194
  if (messaging && model.defaultTeam === null) problems.push(unconfiguredProblem());
162
- return problems;
195
+ return [...problems, ...migrationProblems(model)];
163
196
  }
164
197
 
165
198
  /**
package/lib/workspace.mjs CHANGED
@@ -243,8 +243,22 @@ export function validateWorkspace(value, { remote = defaultRemote } = {}) {
243
243
  const d = value.defaults;
244
244
  for (const slot of ["knowledge", "messaging", "tasks", "capabilities"]) fromProblems(remote, d[slot], `/defaults/${slot}`, problems, { here: false });
245
245
  }
246
+ sharedLabelProblems(value, problems);
246
247
  return withRemovedKeys(problems, value, REMOVED_KEYS.workspace);
247
248
  }
249
+ /** Team model 3: every label `defaultTeam` and `souls:` name is a shared team of this same file, so an
250
+ * unknown label is refused when the file is read, never at a spawn on someone else's machine. */
251
+ function sharedLabelProblems(value, problems) {
252
+ const shared = new Set(isObject(value.teams) ? Object.keys(value.teams) : []);
253
+ const known = (label, path) => { if (typeof label === "string" && !shared.has(label)) problems.push({ path, message: `${show(label)} is not a shared team: declare it in teams: of this file` }); };
254
+ known(value.defaultTeam, "/defaultTeam");
255
+ if (isObject(value.souls)) for (const [key, entry] of Object.entries(value.souls)) {
256
+ if (!isObject(entry)) continue;
257
+ const at = `/souls/${pointerKey(key)}`;
258
+ known(entry.default, `${at}/default`);
259
+ if (Array.isArray(entry.teams)) entry.teams.forEach((label, i) => known(label, `${at}/teams/${i}`));
260
+ }
261
+ }
248
262
  export function validateMembership(value) { return withRemovedKeys(validateAgainst(schemaFor("membership"), value), value, REMOVED_KEYS.membership); }
249
263
 
250
264
  /** Keys 0.30 removed (team model v2): each is a schema problem naming its replacement, in place of
@@ -3,7 +3,7 @@
3
3
  "packages": {
4
4
  "oats.okf": {
5
5
  "url": "https://github.com/awebai/oats-okf.git",
6
- "ref": "v4.1.0",
6
+ "ref": "v4.1.1",
7
7
  "path": "oats-package"
8
8
  },
9
9
  "oats.aweb": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "oats.framework": {
35
35
  "url": "https://github.com/awebai/oats.git",
36
- "ref": "oats-framework/v1.4.1",
36
+ "ref": "oats-framework/v1.4.2",
37
37
  "path": "oats-package"
38
38
  }
39
39
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
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,8 +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.4.1 # bare versions resolve through the official catalog
64
- oats.okf: v4.1.0
63
+ oats.framework: v1.4.2 # bare versions resolve through the official catalog
64
+ oats.okf: v4.1.1
65
65
  defaults:
66
66
  capabilities: { oats.core: { from: package } }
67
67
  knowledge: { oats.okf: { from: package } }