@awebai/oats 0.35.5 → 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}`);
@@ -1850,7 +1867,7 @@ async function statusDrift(data) {
1850
1867
  catch { /* not a workspace soul (classic, or an unreadable stamp) */ }
1851
1868
  }
1852
1869
  const anything = data.some((a) => (a.instances || []).some((i) => hasModules(i) || hasSoul(i)));
1853
- if (!anything) return { drift: new Map(), soul: new Map(), souls, unreachable: null };
1870
+ if (!anything) return { drift: new Map(), soul: new Map(), souls, unreachable: null, local: ctx.local, discovery: null };
1854
1871
  const deploymentDir = dirname(ctx.path);
1855
1872
  let lock = null;
1856
1873
  try { lock = readLockIfPresent(deploymentDir); } catch { lock = null; }
@@ -1860,7 +1877,7 @@ async function statusDrift(data) {
1860
1877
  try { const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs"); discovery = await discoverOrStandalone(ctx.local, { lock, remoteOptions: remoteOptionsFromEnv() }); }
1861
1878
  catch (e) {
1862
1879
  const reason = e?.details?.reason ? `${e.code}: ${e.details.reason}` : (e?.code || e?.message || "unknown");
1863
- return { drift: new Map(), soul: new Map(), souls, unreachable: { code: e?.code ?? null, reason, message: e?.message ?? String(e) } };
1880
+ return { drift: new Map(), soul: new Map(), souls, unreachable: { code: e?.code ?? null, reason, message: e?.message ?? String(e) }, local: ctx.local, discovery: null };
1864
1881
  }
1865
1882
  const { driftOf, soulDriftOf } = await import("../lib/materialize.mjs");
1866
1883
  const drift = new Map();
@@ -1879,7 +1896,67 @@ async function statusDrift(data) {
1879
1896
  const member = memberRowByKey(discovery.members, stamp.repoKey);
1880
1897
  if (member && typeof member.commit === "string" && (member.confirmed || (discovery.standalone === true && member.key === discovery.key))) stamp.current = member.commit;
1881
1898
  }
1882
- return { drift, soul, souls, unreachable: null };
1899
+ return { drift, soul, souls, unreachable: null, local: ctx.local, discovery };
1900
+ }
1901
+ /** The deployment's workspace identity in `oats status --json` (feature workspace-identity), read
1902
+ * OFFLINE. `key` is the workspace HOST's canonical repo key (parseRepoRef(...).key) and `ref` the
1903
+ * reference as oats-local.yaml writes it. The host is the one this run observed, else the one this
1904
+ * machine's parsed cache knows: `ref` itself when the cache holds its workspace file, or the host its
1905
+ * cached oats-membership.yaml names when `ref` is a member ("workspace"). A member whose host is not
1906
+ * known keys as itself ("member"); a ref nothing is known of is taken as the host, as the schema
1907
+ * defines `workspace:` ("workspace"). The team model resolves as teamModel resolves it (the default
1908
+ * label is local; a committed team wins a collision) over the shared teams of, in order, the workspace
1909
+ * file this run observed (`teamsFrom: "observed"`), the cached file ("cache": no git process), or none
1910
+ * ("local"). `standalone` is the CONFIGURED standalone view only (oats-local.yaml `standalone:`): it
1911
+ * reads no workspace file, so its local teams are the whole team model. A run that fell back to the
1912
+ * standalone view (the host unreadable) is not: its team model is the workspace's, and its
1913
+ * discovery is the member's, never the host's key. */
1914
+ function workspaceIdentity(local, discovery) {
1915
+ const ref = local.workspace;
1916
+ const standalone = typeof local.standalone === "string" && local.standalone !== "";
1917
+ const observed = discovery && discovery.standalone !== true && discovery.workspace ? discovery : null;
1918
+ const cached = observed ? null : cachedWorkspace(ref);
1919
+ const keyOf = (r) => { try { return remoteModule.parseRepoRef(r).key; } catch { return null; } };
1920
+ let key, keyFrom;
1921
+ if (observed) [key, keyFrom] = [observed.key, "workspace"];
1922
+ else if (cached && cached.host === null) [key, keyFrom] = [keyOf(ref), "member"];
1923
+ else [key, keyFrom] = [keyOf(cached?.host ?? ref), "workspace"];
1924
+ if (key === null) keyFrom = null; // a reference parseRepoRef refuses: unusable, never matched
1925
+ let shared = null, teamsFrom = "local";
1926
+ if (!standalone) {
1927
+ if (observed) [shared, teamsFrom] = [observed.workspace, "observed"];
1928
+ else if (cached?.file) [shared, teamsFrom] = [cached.file, "cache"];
1929
+ }
1930
+ const model = teamModel(shared, local);
1931
+ const labels = [...model.labels.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1932
+ return {
1933
+ key, ref, keyFrom, standalone,
1934
+ defaultTeam: model.defaultTeam === null ? null : { label: model.defaultTeam, team: model.labels.get(model.defaultTeam)?.team ?? null },
1935
+ teams: Object.fromEntries(labels.map((l) => [l, model.labels.get(l).team])),
1936
+ teamsFrom,
1937
+ };
1938
+ }
1939
+ /** What this machine's parsed cache knows of the workspace `ref` names (the values observeWorkspace and
1940
+ * confirmMembership stored, each at its repo's last observed commit): { host, file } — `host` the ref
1941
+ * of the workspace host, `file` its workspace file or null — when `ref` is the host (its workspace file
1942
+ * is cached) or a member whose cached oats-membership.yaml names it (discoverOrStandalone follows the
1943
+ * same backlink). A member's backlink is cached whether its own workspace slot was observed (missing)
1944
+ * or never was (the host's discovery confirmed it). { host: null, file: null } for a ref cached as
1945
+ * having no workspace file and no backlink; null when nothing is known. */
1946
+ function cachedWorkspace(ref) {
1947
+ const options = remoteOptionsFromEnv();
1948
+ const cached = (r, item) => {
1949
+ const commit = remoteModule.lastObservedCommit(r, options);
1950
+ const value = commit ? remoteModule.peekAtCommit(r, commit, item, options) : undefined;
1951
+ return value && typeof value === "object" ? value : null;
1952
+ };
1953
+ const fileOf = (read) => (read && !read.missing && !read.problems && read.value && typeof read.value === "object" ? read.value : null);
1954
+ const read = cached(ref, "workspace");
1955
+ if (read && !read.missing) return { host: ref, file: fileOf(read) };
1956
+ const membership = cached(ref, "membership");
1957
+ const host = membership?.kind === "ok" && typeof membership.value?.workspace === "string" ? membership.value.workspace : null;
1958
+ if (host !== null) return { host, file: fileOf(cached(host, "workspace")) };
1959
+ return read ? { host: null, file: null } : null;
1883
1960
  }
1884
1961
  /** One `modules:` line per module. */
1885
1962
  function driftLine(row) {
@@ -1948,7 +2025,7 @@ async function status() {
1948
2025
  }
1949
2026
  }
1950
2027
  const observation = maxAgeGiven === null ? {} : { observation: observationBlock() };
1951
- console.log(JSON.stringify({ root, agents: data, ...observation, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}), ...envelopeWarnings() }, null, 2)); return;
2028
+ console.log(JSON.stringify({ root, agents: data, ...observation, ...(ws ? { workspace: { ...(ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true }), ...workspaceIdentity(ws.local, ws.discovery) } } : {}), ...(problems.length ? { problems } : {}), ...envelopeWarnings() }, null, 2)); return;
1952
2029
  }
1953
2030
  console.log(`oats status — agents root ${shortPath(root)}\n`);
1954
2031
  if (ws?.unreachable) console.log(` workspace: unreachable (${ws.unreachable.reason}) — drift unknown\n`);
@@ -3059,7 +3136,7 @@ function versionCmd() {
3059
3136
  // Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
3060
3137
  // runs on resolve/materialize (contract §6); a feature the binary does not implement is
3061
3138
  // never listed.
3062
- console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "spawn-preview-max-age", "launch-config-default", "capability-show", "capture-file"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2, capabilityShowApi: 1 }));
3139
+ console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "spawn-preview-max-age", "launch-config-default", "capability-show", "capture-file", "workspace-identity"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2, capabilityShowApi: 1 }));
3063
3140
  return;
3064
3141
  }
3065
3142
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -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`
@@ -38,7 +38,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
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
40
  "team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
41
- "preview-composed-from","observe-max-age","spawn-preview-max-age","capability-show","capture-file"],
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,
44
44
  "capabilityShowApi":1}
@@ -100,6 +100,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
100
100
  | `spawn-preview-max-age` | `--max-age <s>` on `spawn --preview` and its `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311), [The preview](#the-preview)) | |
101
101
  | `capability-show` | `oats capabilities show <name>` and its `--file` form, OATS 0.34.0 ([`oats capabilities show`](#oats-capabilities-show)) | `capabilityShowApi: 1` |
102
102
  | `capture-file` | `oats capture --file <path> --format cc\|pi\|codex --home <instance home> [--json]`: one session file captured as `--home` capture would, with a receipt bound to its bytes, OATS 0.35.0 (the capture USAGE and packages/record/README.md) | |
103
+ | `workspace-identity` | the deployment's workspace identity on `oats status --json` `workspace` (`key`, `ref`, `keyFrom`, `standalone`, `defaultTeam`, `teams`, `teamsFrom`) and each `oats server roster --json` group's relayed `workspace`, OATS 0.36.0 ([Workspace identity](#workspace-identity-feature-workspace-identity-oats-0360)) | |
103
104
 
104
105
  Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
105
106
  `workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
@@ -344,7 +345,7 @@ An instance subject, abridged:
344
345
  | Key | Meaning |
345
346
  |---|---|
346
347
  | `subject` | `{kind: "instance", instance, home, soul}` or `{kind: "soul", soul, repoKey, commit}` |
347
- | `workspace` | `{key, name, deployment, commit, standalone}`; for a home, `name` is the recorded name (`null` if the spawn predates it) |
348
+ | `workspace` | `{key, name, deployment, commit, standalone}`; for a home, `name` is the recorded name (`null` if the spawn predates it). `standalone` is the view the subject resolves in (a fallback for an unreadable host included), unlike `oats status`'s configured-only [`standalone`](#workspace-identity-feature-workspace-identity-oats-0360) |
348
349
  | `souls` | exactly the subject's soul |
349
350
  | `layers` | `{knowledge, messaging, tasks}`, each `{id, from}` |
350
351
  | `capabilities`, `capabilitiesOff` | the resolved modules (by id) and the ones the soul turned off |
@@ -724,7 +725,7 @@ Read-only (it writes no lock):
724
725
  "souls":["rm"],"capabilities":["nw-house-style"],"publishes":null,"url":"https://github.com/nw/agents/tree/66566512…",
725
726
  "membershipFile":{"path":"oats-membership.yaml","url":"https://github.com/nw/agents/blob/66566512…/oats-membership.yaml"}}],
726
727
  "packages":[{"id":"oats.okf","version":"3.0.0","source":"catalog:oats.okf","commit":"ab897841…","integrity":"sha256-bada35…",
727
- "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"}}],
728
729
  "declaredPackages":["oats.framework","oats.okf"],"unsynced":["oats.framework"],"stale":[],
729
730
  "external":[{"source":"git:github.com/oss/experts@3c606e09…","soul":"security-reviewer"}],
730
731
  "problems":[],"warnings":[],
@@ -783,8 +784,8 @@ packages' capabilities and souls, sorted by name, then origin. Both carry
783
784
  "defaultTeam":{"label":"mine","team":"mine:ana.aweb.ai","from":"deployment"},
784
785
  "private":false,"path":"souls/writer","work":"directory","description":"Drafts campaigns.","harness":"pi","model":null,"harnessFrom":"kernel-default",
785
786
  "file":{"path":"souls/writer/soul.yaml","url":null},"spawnable":true,"problem":null},
786
- {"name":"knowledge-maintainer","qualifiedName":"oats.okf/knowledge-maintainer","origin":"package oats.okf v4.1.0","kind":"package","package":"oats.okf",
787
- "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,
788
789
  "path":"oats-package/souls/knowledge-maintainer","work":"directory","description":"Reviews harvested knowledge.","harness":"pi","model":null,
789
790
  "harnessFrom":"kernel-default","file":{"path":"oats-package/souls/knowledge-maintainer/soul.yaml","url":null},
790
791
  "spawnable":false,"problem":{"code":"E_TEAM_UNKNOWN","message":"team \"reviewers\" is not declared (oats-local.yaml#/souls/teams/…)"}}],
@@ -873,8 +874,8 @@ nothing reads a working clone.
873
874
  **The show:**
874
875
 
875
876
  ```json
876
- {"capabilityShowApi":1,"name":"oats.okf","kind":"package","repoKey":"github.com/awebai/oats-okf","package":"oats.okf","version":"4.1.0",
877
- "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",
878
879
  "inject":{"path":"injects/okf.md","bytes":2422,"text":"## Knowledge: OKF\n\nYou have two kinds of knowledge. …","binary":false,"truncated":false},
879
880
  "skills":[{"name":"okf-consultation","path":"skills/okf-consultation","description":"Consulting your soul's knowledge with the `oats okf` CLI: …",
880
881
  "files":[{"path":"skills/okf-consultation/SKILL.md","bytes":6947},{"path":"skills/okf-consultation/references/consult.md","bytes":4465}],
@@ -900,7 +901,7 @@ nothing reads a working clone.
900
901
  **The `--file` answer:**
901
902
 
902
903
  ```json
903
- {"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"e331a996…",
904
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"e1d604f7…",
904
905
  "file":{"path":"skills/okf-instance-knowledge/SKILL.md","bytes":4787,"text":"---\nname: okf-instance-knowledge\n…","binary":false,"truncated":false}}
905
906
  ```
906
907
 
@@ -1228,9 +1229,38 @@ for a failure and `false` for a warning, plus the problem's own keys.
1228
1229
  | `default-team-changed` | warning | `recorded`, `current` | `--home` with live teams: the default changed since the spawn |
1229
1230
  | `E_TEAM_UNKNOWN` | failure | `label`, `at` | a reference to an undeclared label |
1230
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) |
1231
1233
 
1232
- The last two are also spawn, preview and inspect refusals, with the same
1233
- 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"`.
1234
1264
 
1235
1265
  <a id="soul-launch-preferences-feature-launch-preference-oats-0300"></a>
1236
1266
  ## Launch preferences
@@ -1679,7 +1709,8 @@ Not an envelope: `{root, agents, observation?, workspace?, problems?, warnings?}
1679
1709
  "modules":[{"name":"oats.okf","from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"},
1680
1710
  "commit":"ab897841…","current":{"commit":"ab897841…","version":"2.1.3"},"status":"current"}],
1681
1711
  "soul":{"repoKey":"github.com/nw/agents","commit":"66566512…","current":"66566512…","status":"current"}}]}],
1682
- "workspace":{"reachable":true}}
1712
+ "workspace":{"reachable":true,"key":"github.com/nw/agents","ref":"git:github.com/nw/agents","keyFrom":"workspace","standalone":false,"defaultTeam":{"label":"eng","team":"eng:nw.aweb.ai"},
1713
+ "teams":{"eng":"eng:nw.aweb.ai","mine":"mine:ana.aweb.ai","ops":null},"teamsFrom":"observed"}}
1683
1714
  ```
1684
1715
 
1685
1716
  - **Agent rows**: the soul's recorded definition plus `dir` and `instances`,
@@ -1712,11 +1743,68 @@ Not an envelope: `{root, agents, observation?, workspace?, problems?, warnings?}
1712
1743
  reason?}`; a package soul adds `package`, `version`, `currentVersion`
1713
1744
  (`missing` reasons: `package-absent`, `soul-absent`).
1714
1745
  - **`workspace`**: `{reachable: true}`, or `{reachable: false, code, reason,
1715
- message}` (modules then stay the recorded map). Absent without
1716
- `oats-local.yaml`.
1746
+ message}` (modules then stay the recorded map), plus the deployment's
1747
+ [workspace identity](#workspace-identity-feature-workspace-identity-oats-0360)
1748
+ either way. Absent without `oats-local.yaml`.
1717
1749
  - `problems`: the legacy-home rows ([dispatch errors](#dispatch-errors)).
1718
1750
  `warnings`: envelope warnings. `--team` is `E_BAD_ARGS` (an envelope).
1719
1751
 
1752
+ <a id="workspace-identity-feature-workspace-identity-oats-0360"></a>
1753
+ **Workspace identity** (feature `workspace-identity`, OATS 0.36.0). The
1754
+ `workspace` object says which workspace and teams this deployment is, read
1755
+ offline with no network, so it is there whether `reachable` is `true` or
1756
+ `false`:
1757
+
1758
+ | Key | Meaning |
1759
+ |---|---|
1760
+ | `key` | the canonical repo key of the workspace HOST (`parseRepoRef(…).key`: every spelling of one repository gives one key, e.g. `git:github.com/nw/agents`, `https://github.com/nw/agents.git` and `git@github.com:nw/agents.git` all give `github.com/nw/agents`). When `oats-local.yaml` names a member in place of its host, it is the host's key once the member's backlink is known. `null` when `parseRepoRef` refuses the reference |
1761
+ | `ref` | the reference exactly as `oats-local.yaml` writes it (`workspace:`), for display only |
1762
+ | `keyFrom` | `"workspace"`: `key` is the host's, because this run observed it, or the cache holds the ref's workspace file, or a member's cached backlink names the host. With nothing observed or cached, the ref is taken as the host, as the schema defines `workspace:`. `"member"`: the ref names a member whose host is not known yet, so `key` is the member's own. `null` with a `null` key |
1763
+ | `standalone` | `true` only when `oats-local.yaml` sets `standalone:` (the configured standalone view, whose local teams are its whole team model). A run that fell back to the standalone view because the host is unreadable is `false`: its teams are the workspace's, read through the sources below. Unlike `workspace.standalone` on [inspect](#oats-inspect), which is the view a home runs in |
1764
+ | `defaultTeam` | `{label, team}`: the label is `oats-local.yaml`'s `defaultTeam`, `team` its provider id from `teams` (`null` when that map gives none). `null` when `oats-local.yaml` names no default team |
1765
+ | `teams` | `{<label>: <provider team id> \| null}`: every team label the deployment maps, local and shared, by label; a label in both is the committed (shared) one, as [`oats teams`](#oats-teams) resolves it |
1766
+ | `teamsFrom` | where the shared teams came from: `"observed"`, the workspace file this run read; `"cache"`, this machine's cached copy at the host commit it last observed (no git process, no network; when `key` names a member, the host its cached `oats-membership.yaml` names); `"local"`, none: `teams` holds the local teams only |
1767
+
1768
+ A configured standalone deployment reads no workspace file, so it is always
1769
+ `teamsFrom: "local"`, with its local teams only (as spawn resolves them there).
1770
+ `oats status` only reads the workspace when an instance records modules or a
1771
+ workspace soul, so an empty deployment answers from the cache, or from local
1772
+ teams on a host that has not observed its workspace (`oats sync` and
1773
+ `oats teams` observe it). The cache is the running kernel's own: after an
1774
+ OATS upgrade it is empty until the host next observes its workspace.
1775
+
1776
+ **Matching workspaces across machines.** Two deployments are the same
1777
+ workspace when their `key`s are equal; `ref` is never compared. The identity
1778
+ is resolved the same offline way for every deployment, standalone included,
1779
+ whatever its team view:
1780
+
1781
+ - `keyFrom: "member"` is unresolved: never match it, and show it as
1782
+ unresolved (the host is learned when the deployment observes its
1783
+ workspace, e.g. `oats sync`).
1784
+ - `keyFrom: "workspace"` with nothing observed (`teamsFrom: "local"` on a
1785
+ deployment that is not standalone) means the ref was taken as the host, as
1786
+ the schema defines. If it is really a member, the worst case is a split
1787
+ (one workspace shown as two until `oats sync` there), never a wrong merge.
1788
+ - A `null` key is an unusable reference and never matches. Show `ref` with
1789
+ "this deployment's workspace reference isn't valid; fix oats-local.yaml".
1790
+ - Known limit: `parseRepoRef` lowercases the host but keeps the path's case,
1791
+ so references that differ in owner or repository case give different keys.
1792
+
1793
+ **Matching teams across machines.** This is the rule for comparing two
1794
+ deployments' teams (as the Desktop does to attach a remote machine to a
1795
+ workspace):
1796
+
1797
+ - `teamsFrom` `"observed"` or `"cache"`: a `null` team is **unmapped**, and
1798
+ unmapped matches only unmapped.
1799
+ - `standalone: true` with `teamsFrom: "local"`: the local config IS the
1800
+ complete team model, so a `null` team is **unmapped** (matches only
1801
+ unmapped). Reason to show: "teams are local only on this host
1802
+ (standalone)", with no sync advice.
1803
+ - `standalone: false` with `teamsFrom: "local"`: a `null` default team is
1804
+ **unknown** and never matches. Reason to show: "this host hasn't observed
1805
+ its workspace yet; run oats sync there". A non-null default team (a locally
1806
+ mapped team) matches normally.
1807
+
1720
1808
  **Desktop facts** (feature `desktop-facts`): `startedAt` is the last start or
1721
1809
  restart, else `createdAt` for a launched home, else `null`. `modelFrom` is
1722
1810
  `"soul"`, `"spawn"` or `"start"` (an explicit `--model`), `"launch-config"`,
@@ -1738,6 +1826,8 @@ route target:
1738
1826
  {"id":"build:3f2a…","server":"build","label":"Build box","registrationPresent":true,
1739
1827
  "target":{"sshHost":"build-host","workspace":"/srv/team","oatsPath":"oats"},
1740
1828
  "probe":{"ok":true},"agentsRoot":"/srv/team/agents",
1829
+ "workspace":{"reachable":true,"key":"github.com/acme/team","ref":"git:github.com/acme/team","keyFrom":"workspace","standalone":false,"defaultTeam":{"label":"default","team":"acme:team"},
1830
+ "teams":{"default":"acme:team"},"teamsFrom":"observed"},
1741
1831
  "souls":[{"name":"dev","harness":"claude","work":"worktree","agentsRoot":"/srv/team/agents"}],
1742
1832
  "instances":[{"server":"build","instance":"dev-a","agent":"dev","home":"/srv/team/agents/dev/instances/dev-a",
1743
1833
  "agentsRoot":"/srv/team/agents","harness":"claude","backend":"tmux","tmux":{"session":"oats-agents","window":"dev-a"},
@@ -1749,6 +1839,15 @@ route target:
1749
1839
  "retireFailures":[]}
1750
1840
  ```
1751
1841
 
1842
+ - **`workspace`** (feature `workspace-identity`, OATS 0.36.0): the host's
1843
+ own `status --json` [`workspace` object](#workspace-identity-feature-workspace-identity-oats-0360),
1844
+ relayed verbatim, or `null` when the host reports none (a deployment
1845
+ without `oats-local.yaml`, or a failed or skipped probe). A host before
1846
+ 0.36.0 answers the reachability-only object (`{reachable, code?, reason?,
1847
+ message?}`) with no identity fields, so the identity is there only when
1848
+ the object has a `key` field (which a 0.36.0 host always sends, `null` for
1849
+ an unusable reference). It is never derived on this side. It is on the group, not the
1850
+ rows, so an empty remote deployment still reports it.
1752
1851
  - **Instance rows** relay the host's own `status --json` row: `identity`,
1753
1852
  `identityAddress`, `teams`, `startedAt`, `createdAt`, `model`,
1754
1853
  `runtimeState`, `parentInstance`, `siblingInstance`, `relation`,
package/docs/desktop.md CHANGED
@@ -104,6 +104,53 @@ opened, or your home directory when there is none (never ~/Downloads);
104
104
 
105
105
  Launch flags for scripted use: `--dir <workspace>` and `OATS_DESKTOP_PORT`.
106
106
 
107
+ ## One workspace, several machines
108
+
109
+ The switcher lists each workspace once, however many deployments it has: the
110
+ deployments you opened on this Mac and those your registered servers report,
111
+ matched by the workspace each one reports (its repository key and default
112
+ team, by the kernel's rules). It needs a CLI with the `workspace-identity`
113
+ feature (OATS 0.36.0); without it each deployment has its own entry.
114
+
115
+ - **Deployments.** The **Deployments** page (formerly the Active overview)
116
+ shows the workspace's overview trees. **All** stacks one section per
117
+ deployment, each headed by its machine and folder ("This Mac ·
118
+ ~/Agents/oats", "altair · ~/Agents/tsm"). Then each deployment has a tab of
119
+ its own, named by its machine ("altair"; "This Mac · oats-v2" when this Mac
120
+ holds two). A workspace with one deployment shows just that deployment's
121
+ tab. The tab you chose is remembered per workspace. With two or more
122
+ deployments, the sidebar's instance list is grouped under one heading per
123
+ machine; with one, it has no headings.
124
+ - **Not shown live.** A deployment that can't be shown live says why in a few
125
+ words on its heading ("ssh needs a prompt", "Timed out", "OATS too old to
126
+ report its workspace"), and **How to fix** under it gives the full sentence
127
+ and the steps. Causes: ssh needs a prompt or a host key, the read timed out,
128
+ the host's OATS is too old to report its workspace, its workspace reference
129
+ needs `oats sync` or fixing in `oats-local.yaml`, or this computer's OATS
130
+ can't read other machines. A server that can't be reached stays under the
131
+ workspace it last reported, marked "remembered".
132
+ - **The switcher.** Each workspace names its machines on one line ("This Mac ·
133
+ altair"), with a mark when one of them isn't live.
134
+ - **Not matched to a workspace.** A deployment that can't be matched is
135
+ listed on its own under that heading in the switcher, with its machine and
136
+ a short reason; choosing it opens its tab on the Deployments page. When this
137
+ computer's OATS is too old to report workspaces, nothing can be matched:
138
+ each deployment is listed on its own, as before, and its heading says to
139
+ update OATS.
140
+ - **Actions.** Everything you do to an instance goes to that instance's own
141
+ deployment. Workspace-wide pages (Setup, Capabilities, Sync, Automations,
142
+ Schedules, Teams configuration) act on the workspace's first deployment on
143
+ this Mac, else its first, and say "On <deployment>" when there are two or
144
+ more.
145
+ - **Spawn.** With two or more deployments, the Spawn dialog asks first which
146
+ one to spawn in (and says "Runs on" in its summary), starting on the one you last used in that workspace when it
147
+ offers the soul, else the first that does (this Mac first). With one, it
148
+ asks nothing.
149
+
150
+ Saved selections and tabs move to the workspace that holds their
151
+ deployment. Details are in
152
+ [the deployment model](../packages/desktop/docs/desktop-deployment-model.md#workspace-views-and-deployments).
153
+
107
154
  ## Scheduling agents and wake messages
108
155
 
109
156
  Open **Schedules** in the selected workspace to launch a new agent on a cron,
@@ -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,118 @@
1
+ # OATS 0.36.0
2
+
3
+ ## Added
4
+
5
+ - **A deployment reports its workspace identity** (feature
6
+ `workspace-identity`, awebai/oats#482). `oats status --json`'s `workspace`
7
+ object now also carries `key` (the workspace host's canonical repository
8
+ key, so every spelling of one repository matches, and the host's key when
9
+ `oats-local.yaml` names a member whose backlink is known), `ref` (the
10
+ reference exactly as `oats-local.yaml` writes it, for display), `keyFrom`
11
+ (`"workspace"`, or `"member"` while a member's host is not known),
12
+ `standalone`, `defaultTeam` (`{label, team}`),
13
+ `teams` (`{<label>: <provider team id> | null}`) and `teamsFrom`. They are
14
+ read offline, so they are there whether the workspace is reachable or not.
15
+ Shared teams come from the workspace file the run read
16
+ (`teamsFrom: "observed"`), else this machine's cached copy at the host
17
+ commit it last observed (`"cache"`, no git process), else nowhere
18
+ (`"local"`: local teams only). `oats server roster --json` relays each
19
+ host's `workspace` object verbatim on its group (from a host before
20
+ 0.36.0, the reachability-only object), or `null` from a host that reports
21
+ none. The rule for matching teams across machines (when a
22
+ `null` team is unmapped and when it is unknown) is in
23
+ docs/desktop-cli-api.md, under Workspace identity. A deployment without
24
+ `oats-local.yaml` still has no `workspace` object, and the human-readable
25
+ `oats status` is unchanged. After upgrading, run `oats sync` on each remote
26
+ host (and on any deployment with no instances): the parsed cache belongs to
27
+ the running kernel, so until the deployment observes its workspace again,
28
+ a status that does not read the workspace itself reports
29
+ `teamsFrom: "local"` and its shared default team shows as unknown.
30
+ - **The Desktop shows a workspace's deployments on every machine**
31
+ (awebai/oats#482). The switcher lists each workspace once: its deployments
32
+ on this Mac and on registered servers are matched by the workspace each
33
+ reports (its key and default team, by the kernel's rules in
34
+ docs/desktop-cli-api.md), and named by machine. The Active overview becomes
35
+ **Deployments**: **All** shows the overview trees split by deployment ("This
36
+ Mac · ~/Agents/oats", "altair · ~/Agents/tsm"), then each deployment has its
37
+ own tab. Every action is still addressed to the row's own deployment. A
38
+ deployment that isn't shown live says why in a few words, with **How to fix**
39
+ under it (ssh needs a prompt or a host key, timed out, the host's OATS is too
40
+ old, its workspace reference needs `oats sync` or fixing), and one that can't
41
+ be matched is listed in the switcher under "Not matched to a workspace". The
42
+ Desktop remembers the last workspace each remote reported, so an
43
+ unreachable server stays under its workspace, marked "remembered". With two
44
+ or more deployments the sidebar groups instances by machine, and the Spawn
45
+ dialog asks first which deployment to spawn in; the Teams board shows only
46
+ the workspace's own deployments, grouped by deployment.
47
+ Saved selections and tabs move to the workspace that holds their
48
+ deployment. It needs the CLI's `workspace-identity` feature; without it
49
+ each deployment keeps its own entry, as before.
50
+
51
+ ## Changed
52
+
53
+ - **The Desktop's tabs fit the window, and the selected tab stays in view.**
54
+ Tabs share the tab strip like VS Code's "shrink" mode: each takes at most its
55
+ natural width (still capped at 280px) and they shrink evenly as more open,
56
+ down to 168px; below that the strip scrolls. A shrunk tab keeps the end of
57
+ its name ("oats-…palette"), so tabs whose names share a prefix still read
58
+ differently. The branch gives way before the name, and the full
59
+ name stays in the tooltip and the accessible name. Whenever a tab becomes
60
+ active, a tab closes, or a strip resizes (the window, the sidebar or panel,
61
+ a split), the active tab is scrolled fully into view, in split groups too.
62
+ After the tabs, only Split right and Split down remain. Closing a split
63
+ (⌥⌘W / Ctrl+Shift+Alt+W) and showing or hiding the instance panel
64
+ (⌥⌘B / Ctrl+Alt+B) are on their chords and in the command palette, and the
65
+ panel keeps its own collapse control
66
+ ([desktop-keyboard.md](../../packages/desktop/docs/desktop-keyboard.md#the-command-palette-k)).
67
+ - **The Desktop's command palette lists instances like the sidebar and cycles
68
+ with ⌘K.** With an empty query it shows every instance in the sidebar's
69
+ groups, order and indentation, then the commands. A query keeps that tree,
70
+ showing each match under its dimmed ancestors. While the palette is open,
71
+ ⌘K moves down and ⇧⌘K up, wrapping. Enter opens the row and Esc closes, so
72
+ a second ⌘K no longer closes the palette. On Linux/Windows the chord,
73
+ Ctrl+Shift+P, already holds Shift, so ArrowUp moves up there.
74
+ - **The Desktop's default monospace font is Inconsolata, bundled with the
75
+ app.** The terminal and the UI's code, paths and shortcut hints use it on
76
+ every machine, ahead of the system monospace fonts. The terminal's default
77
+ size is now 15px, and the terminal's text sits closer to the pane's edges
78
+ (12px each side instead of 32px), centred in the pane. Box drawing (Claude
79
+ Code's input box, tmux borders) is drawn as a native terminal draws it, with
80
+ thin lines whatever the font. A terminal font or size you already
81
+ chose is kept, and resetting the terminal's typography (⌘0 / Ctrl+0, the
82
+ command palette, or Settings → Terminal) goes back to 15px. Settings (the sidebar's settings
83
+ button, now titled just "Settings") has a Terminal section with a font size
84
+ stepper (9–28px, or type a size) that changes every open terminal at once
85
+ and follows ⌘= / ⌘- / ⌘0. Inconsolata is under the SIL Open Font License 1.1
86
+ ([fonts/README.md](../../packages/desktop/renderer/fonts/README.md)).
87
+
88
+ - **The Desktop follows a spawn to its new instance.** After **Spawn** the
89
+ dialog closes at once and the pending row is revealed in the roster, without
90
+ taking focus. When the instance is running, the Desktop opens its terminal
91
+ tab and selects its row: arriving there is the confirmation, so a successful
92
+ spawn no longer posts a toast. It never pulls the operator away from
93
+ something they did since the press: typing (a lone modifier does not count),
94
+ pasting, moving focus, opening another tab or view, or an open dialog or
95
+ overlay. Then they stay where they are, and the row says **New** until it or
96
+ its tab is first opened. Either way, assistive technology hears "*name*
97
+ spawned". Partial, refused, failed and unknown outcomes keep their
98
+ notifications
99
+ ([desktop-spawn-preview.md](../../packages/desktop/docs/desktop-spawn-preview.md)).
100
+
101
+ - **The Desktop's spawn dialog opens on Name and jumps between its sections.**
102
+ Name is focused whenever the dialog opens for a chosen soul, Quick Open
103
+ included, with the caret after a restored name. **⌘1–⌘7** (Ctrl+1–7
104
+ elsewhere) jump to Name, Harness, Model, Relationship, Teams and the Opening
105
+ instruction; ⌘7 opens or closes Developer settings. Quiet hints show the
106
+ keys; the shortcuts editor lists them under *Spawn dialog*, where they can
107
+ be rebound, and the shell's own ⌘1–9 do not fire behind the dialog
108
+ ([desktop-keyboard.md](../../packages/desktop/docs/desktop-keyboard.md)).
109
+ In "Works in", a directory instance now reads "own folder · free to work
110
+ across repos".
111
+
112
+ - **The Active overview no longer has a Spawn button.** Spawn from the
113
+ canvas with **S**, from the sidebar's **Spawn instance** (⌘N), from Quick
114
+ Open or from a soul card.
115
+
116
+ - **The Desktop accepts OATS CLIs `>=0.25.8 <0.37.0`**, so it runs against
117
+ this release's kernel. Install the CLI and the Desktop 0.36.0 together: the
118
+ Desktop 0.35.x refuses a 0.36 CLI.
@@ -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.
package/docs/servers.md CHANGED
@@ -156,7 +156,9 @@ oats okf harvest --server build --instance dev-fix-123 # the harvest, run in t
156
156
 
157
157
  The **roster** is what the Desktop shows: one group per server id and route
158
158
  target (host and workspace), with the registration (present or not), the
159
- probe result, the remote souls, the instances joined with saved routes
159
+ probe result, the host's workspace identity (`workspace`: its own `status
160
+ --json` `workspace` object relayed verbatim, reachability only from a host
161
+ before 0.36.0, `null` when it reports none or the probe failed), the remote souls, the instances joined with saved routes
160
162
  (`savedRoute`, `running` or `null` when unknown, `retirePending`,
161
163
  `rollbackIncomplete`, `missingRemotely`, `addressable`), and `retireFailures`
162
164
  (deferred self-retirements that failed there). Each instance row also relays
@@ -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/servers.mjs CHANGED
@@ -739,7 +739,7 @@ export function rosterGroups({ server, io = {} } = {}) {
739
739
  const groups = new Map();
740
740
  const add = (serverId, target, registrationPresent, label) => {
741
741
  const key = `${serverId}:${targetKey(target)}`;
742
- if (!groups.has(key)) groups.set(key, { id: key, server: serverId, label: label || serverId, registrationPresent, target, probe: null, agentsRoot: undefined, souls: [], instances: [], retireFailures: [], _snapshots: [] });
742
+ if (!groups.has(key)) groups.set(key, { id: key, server: serverId, label: label || serverId, registrationPresent, target, probe: null, agentsRoot: undefined, workspace: null, souls: [], instances: [], retireFailures: [], _snapshots: [] });
743
743
  const g = groups.get(key);
744
744
  if (registrationPresent) g.registrationPresent = true;
745
745
  return g;
@@ -769,6 +769,9 @@ export function rosterGroups({ server, io = {} } = {}) {
769
769
  if (status?.ok) {
770
770
  g.probe = { ok: true };
771
771
  g.agentsRoot = status.result.root;
772
+ // The host's own workspace identity (feature workspace-identity), relayed as it answered it: never
773
+ // derived here, null when the host reports none.
774
+ g.workspace = status.result.workspace ?? null;
772
775
  for (const a of status.result.agents || []) {
773
776
  g.souls.push({ name: a.name, harness: a.harness ?? a.runtime, work: a.work, backend: a.backend, description: a.description, agentsRoot: status.result.root });
774
777
  // A failed deferred self-retirement needs an operator: it rides with
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.35.5",
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 } }