@awebai/oats 0.36.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/oats.mjs +71 -65
- package/docs/capabilities.md +64 -9
- package/docs/capability-manifest.schema.json +4 -0
- package/docs/configuration.md +16 -21
- package/docs/design/2026-09-27-team-model-v2.md +1 -1
- package/docs/design/2026-10-02-team-model-3.md +146 -0
- package/docs/design/README.md +5 -1
- package/docs/desktop-cli-api.md +159 -121
- package/docs/desktop.md +33 -0
- package/docs/first-team.md +14 -7
- package/docs/integrations.md +1 -1
- package/docs/oats-local.schema.json +2 -14
- package/docs/oats-workspace.schema.json +4 -4
- package/docs/official-catalog.md +2 -2
- package/docs/packages.md +10 -9
- package/docs/release-notes/v0.37.0.md +111 -0
- package/docs/release-notes/v0.38.0.md +128 -0
- package/docs/souls-and-instances.md +4 -3
- package/docs/workspaces.md +122 -77
- package/lib/core.mjs +123 -32
- package/lib/instance-inspect.mjs +18 -15
- package/lib/instance-resolution.mjs +9 -6
- package/lib/resolve.mjs +5 -4
- package/lib/schedule.mjs +1 -1
- package/lib/teams-verbs.mjs +44 -70
- package/lib/teams.mjs +151 -112
- package/lib/workspace.mjs +15 -5
- package/package-catalog.json +2 -2
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +20 -9
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 {
|
|
39
|
+
import { discoveredTeamKeys, isTeamRefusal, localTeamsClosedProblem, recordedTeams, reportRows, soulKeyOf, soulTeams, teamKeyOf, 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,17 +221,18 @@ 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
|
-
/**
|
|
225
|
-
* local
|
|
226
|
-
* network). Without that file, whether local teams
|
|
227
|
-
* (information), never guessed. The standalone view has no workspace
|
|
228
|
-
|
|
224
|
+
/** Local teams the workspace does not allow (team model 3: E_WORKSPACE_SCHEMA local-teams-closed), in
|
|
225
|
+
* doctor, OFFLINE like the rest of doctor: oats-local.yaml, and the workspace file this machine's parsed
|
|
226
|
+
* cache holds (cachedWorkspace: no git process, no network). Without that file, whether the local teams are
|
|
227
|
+
* allowed is said to be unchecked (information), never guessed. The standalone view has no workspace
|
|
228
|
+
* rules. → { problems, information } */
|
|
229
|
+
function doctorLocalTeams(local) {
|
|
229
230
|
const standalone = typeof local.standalone === "string" && local.standalone !== "";
|
|
230
231
|
const file = standalone ? null : cachedWorkspace(local.workspace)?.file ?? null;
|
|
231
232
|
const model = teamModel(file, local);
|
|
232
|
-
const
|
|
233
|
-
return { problems:
|
|
234
|
-
information:
|
|
233
|
+
const declared = ["teams", "defaultTeam"].some((k) => Object.hasOwn(local, k));
|
|
234
|
+
return { problems: model.closedKeys.length ? [localTeamsClosedProblem(model.closedKeys)] : [],
|
|
235
|
+
information: !standalone && file === null && declared ? ["local-teams-closed: whether oats-workspace.yaml allows oats-local.yaml teams/defaultTeam (localTeams: true) couldn't be checked: this deployment hasn't observed its workspace yet; run oats sync"] : [] };
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
/** Workspace-model v2 doctor data, OFFLINE: the deployment declaration found
|
|
@@ -411,7 +412,9 @@ async function workspaceTarget(bail, { command, liveTeams = true }) {
|
|
|
411
412
|
if (!existsSync(join(deployment, "oats-local.yaml"))) return bail("E_HOME_MISMATCH", `${homeFlag} is not at <deployment>/agents/<soul>/instances/<name>: ${deployment} has no oats-local.yaml`, { home: homeFlag, expected: join(deployment, "oats-local.yaml") });
|
|
412
413
|
if (flag("dir") !== undefined) { let given = null; try { given = dirname(loadLocal(dirFlag()).path); } catch { given = dirFlag(); } if (realOrResolved(given) !== realOrResolved(deployment)) return bail("E_HOME_MISMATCH", `--dir ${dirFlag()} is not the deployment of ${homeFlag} (${deployment}); omit --dir for a home`); }
|
|
413
414
|
if (rootFlag && realOrResolved(rootFlag) !== realOrResolved(join(deployment, "agents"))) return bail("E_HOME_MISMATCH", `--agents-root ${rootFlag} is not the agents root of ${homeFlag}`);
|
|
414
|
-
|
|
415
|
+
// A typed refusal (the deployment's removed team keys, team model 3) answers as one, as for --soul.
|
|
416
|
+
try { return await homeTarget(homeFlag, meta, { remoteOptions, discover: command === "readiness", live: liveTeams }); }
|
|
417
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
415
418
|
}
|
|
416
419
|
try { if (!isWorkspaceContext(dirFlag())) return bail("E_LOCAL_MISSING", `${command} reads a workspace deployment, and none is in reach of ${dirFlag()} (no oats-local.yaml walking up; \`oats onboard\` creates one) — or pass --home <abs> of a workspace instance`); }
|
|
417
420
|
catch (e) { return bail(e?.code || "E_WORKSPACE_SCHEMA", e?.message || String(e), e?.details); }
|
|
@@ -597,13 +600,13 @@ function legacyLayoutProblems(root) {
|
|
|
597
600
|
async function doctorWorkspaceJson(ctx, soulName, ws) {
|
|
598
601
|
const composition = await doctorComposition(ctx, soulName, ws, (code, msg, details) => jsonFail(code, msg, details));
|
|
599
602
|
const agentsRoot = join(dirname(ws.local.path), "agents");
|
|
600
|
-
const
|
|
601
|
-
const problems = [...legacyLayoutProblems(agentsRoot), readableInstanceHomes(agentsRoot), ...
|
|
603
|
+
const localTeams = doctorLocalTeams(ws.local.value);
|
|
604
|
+
const problems = [...legacyLayoutProblems(agentsRoot), readableInstanceHomes(agentsRoot), ...localTeams.problems].filter(Boolean);
|
|
602
605
|
return {
|
|
603
606
|
schemaVersion: 1, workspaceApi: 2, context: ctx,
|
|
604
607
|
workspace: { file: ws.local.path, ref: ws.local.workspace },
|
|
605
608
|
workspaceError: ws.localError, lockFile: ws.lockFile, packages: ws.packages, lockError: ws.lockError,
|
|
606
|
-
information: [...(operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : []), ...
|
|
609
|
+
information: [...(operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : []), ...localTeams.information],
|
|
607
610
|
composedInstructions: composition?.text, instructionBlocks: composition?.blocks,
|
|
608
611
|
...(problems.length ? { problems } : {}),
|
|
609
612
|
};
|
|
@@ -638,10 +641,10 @@ async function doctor(dir) {
|
|
|
638
641
|
const composition = await doctorComposition(ctx, soulName, ws, (code, msg) => die(`${msg} [${code}]`));
|
|
639
642
|
printDoctorWorkspace(ws);
|
|
640
643
|
const agentsRoot = join(dirname(ws.local.path), "agents");
|
|
641
|
-
const
|
|
644
|
+
const localTeams = doctorLocalTeams(ws.local.value);
|
|
642
645
|
for (const p of [...legacyLayoutProblems(agentsRoot), readableInstanceHomes(agentsRoot)].filter(Boolean)) console.log(`\n! ${p.code}: ${p.message}`);
|
|
643
|
-
for (const p of
|
|
644
|
-
for (const line of
|
|
646
|
+
for (const p of localTeams.problems) console.log(`\n! ${p.code} (${p.condition}): ${p.message}`);
|
|
647
|
+
for (const line of localTeams.information) console.log(`\nINFO: ${line}`);
|
|
645
648
|
if (soulName) {
|
|
646
649
|
const information = operationalKnowledgeNote(composition, soulName);
|
|
647
650
|
if (information) console.log(`\nINFO: ${information}`);
|
|
@@ -1083,11 +1086,8 @@ function maxAgeRefusal(command, head) {
|
|
|
1083
1086
|
case "spawn": return head.includes("--preview") ? null : refuse("spawn");
|
|
1084
1087
|
case "workspace": return word(1) === "status" ? null : refuse(["workspace", word(1)].filter(Boolean).join(" "));
|
|
1085
1088
|
case "teams": return word(1) === undefined ? null : refuse(`teams ${word(1)}`);
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const edit = ["--add", "--remove", "--default", "--clear-default"].find((f) => head.includes(f));
|
|
1089
|
-
return edit ? refuse(`soul teams ${edit}`) : null;
|
|
1090
|
-
}
|
|
1089
|
+
// `soul teams` only reads (its edit flags were removed in 0.38.0, and refuse as such).
|
|
1090
|
+
case "soul": return word(1) === "teams" ? null : refuse(["soul", word(1)].filter(Boolean).join(" "));
|
|
1091
1091
|
default: {
|
|
1092
1092
|
const sub = ["package", "schedule", "session", "trigger", "automations", "launch-config", "server", "instance", "operation", "pane"].includes(command) ? word(1) : undefined;
|
|
1093
1093
|
return refuse([command, sub].filter(Boolean).join(" "));
|
|
@@ -1175,8 +1175,8 @@ function soulLaunchFacts(entry, local, { launchConfigs, contextDir }) {
|
|
|
1175
1175
|
/** Rows of every soul and capability of confirmed members (+ external souls) + locked package
|
|
1176
1176
|
* capabilities. Souls have no private mode (0.26.0); a private member capability is listed with
|
|
1177
1177
|
* `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE).
|
|
1178
|
-
* Each soul row carries its teams HERE (team model
|
|
1179
|
-
*
|
|
1178
|
+
* Each soul row carries its teams HERE (team model 3: `teams`, `defaultTeam`), from the committed
|
|
1179
|
+
* workspace and the local teams it allows; both null when its teams do not resolve (isTeamRefusal). */
|
|
1180
1180
|
function workspaceItems(discovery, lock, local, deploymentDir) {
|
|
1181
1181
|
const souls = [];
|
|
1182
1182
|
let launchConfigs = {};
|
|
@@ -1185,8 +1185,8 @@ function workspaceItems(discovery, lock, local, deploymentDir) {
|
|
|
1185
1185
|
const capabilities = [];
|
|
1186
1186
|
const model = teamModel(discovery.standalone === true ? null : discovery.workspace, local, { workspaceKey: discovery.key ?? null });
|
|
1187
1187
|
const teamsHere = (entry) => {
|
|
1188
|
-
try { const t = soulTeams(model,
|
|
1189
|
-
catch (e) { if (
|
|
1188
|
+
try { const t = soulTeams(model, teamKeyOf(entry)); return { teams: reportRows(t.teams), defaultTeam: t.defaultTeam }; }
|
|
1189
|
+
catch (e) { if (isTeamRefusal(e)) return { teams: null, defaultTeam: null }; throw e; }
|
|
1190
1190
|
};
|
|
1191
1191
|
for (const m of discovery.members) {
|
|
1192
1192
|
if (!m.confirmed && !(discovery.standalone === true && m.key === discovery.key)) continue;
|
|
@@ -1589,22 +1589,23 @@ async function workspaceStatusFacts(result, discovery, lock, ctx) {
|
|
|
1589
1589
|
}
|
|
1590
1590
|
}
|
|
1591
1591
|
|
|
1592
|
-
/** The deployment's team facts for the team verbs: oats-local.yaml
|
|
1593
|
-
* (the workspace
|
|
1592
|
+
/** The deployment's team facts for the team verbs: oats-local.yaml, the committed workspace file and the
|
|
1593
|
+
* souls it offers (the workspace discovered now; none in a standalone view). */
|
|
1594
1594
|
async function teamsContext(bail) {
|
|
1595
1595
|
const ctx = workspaceContext(bail);
|
|
1596
|
-
let
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1596
|
+
let discovery;
|
|
1597
|
+
try {
|
|
1598
|
+
const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
1599
|
+
discovery = await discoverOrStandalone(ctx.local, { lock: readLockIfPresent(ctx.deploymentDir), deployment: ctx.deploymentDir, remoteOptions: ctx.remoteOptions });
|
|
1600
|
+
} catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance); throw e; }
|
|
1601
|
+
const standalone = discovery.standalone === true;
|
|
1602
|
+
return { deployment: ctx.deploymentDir, localPath: ctx.localPath, local: ctx.local, workspace: standalone ? null : discovery.workspace, workspaceKey: standalone ? null : discovery.key ?? null,
|
|
1603
|
+
soulKeys: discoveredTeamKeys(discovery), remoteOptions: ctx.remoteOptions };
|
|
1602
1604
|
}
|
|
1603
|
-
const labelsFlag = (name) => { const v = valueFlag(name); return v === undefined ? [] : String(v).split(",").map((l) => l.trim()).filter(Boolean); };
|
|
1604
1605
|
const positional = (i) => (args[i] !== undefined && !args[i].startsWith("--") ? args[i] : undefined);
|
|
1605
1606
|
|
|
1606
1607
|
/** `oats teams [--json] | add <label> --team <id> [--description <d>] | remove <label> | default <label>` —
|
|
1607
|
-
* this deployment's teams (team model
|
|
1608
|
+
* this deployment's teams (team model 3). Config only: never a provider call. */
|
|
1608
1609
|
async function teamsCmd() {
|
|
1609
1610
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1610
1611
|
const usage = "usage: oats teams [--json] | oats teams add <label> --team <id> [--description <d>] | oats teams remove <label> | oats teams default <label> [--dir <deployment>] [--json]";
|
|
@@ -1623,25 +1624,29 @@ async function teamsCmd() {
|
|
|
1623
1624
|
const doc = V.teamsDocument(result ? { ...ctx, local: result.local } : ctx);
|
|
1624
1625
|
if (JSON_MODE) { jsonOk(result ? { ...doc, changed: result.changed } : withObservation(doc)); return; }
|
|
1625
1626
|
if (result) console.log(result.changed ? `${sub === "add" ? `Declared team ${label}` : sub === "remove" ? `Removed team ${label}` : `The default team is now ${label}`} in ${shortPath(ctx.localPath)}` : "Nothing to change");
|
|
1626
|
-
console.log(`default ${doc.defaultTeam
|
|
1627
|
+
console.log(`default ${doc.defaultTeam ? `${doc.defaultTeam.label} (${doc.defaultTeam.from})` : "(none)"}`);
|
|
1628
|
+
console.log(`local ${doc.localTeams === null ? "allowed (standalone)" : doc.localTeams ? "allowed (localTeams: true)" : "not allowed"}`);
|
|
1627
1629
|
if (!doc.teams.length) console.log("teams (none: `oats aweb setup` creates them, or `oats teams add <label> --team <id>`)");
|
|
1628
1630
|
else printTable(["team", "id", "from", ""], doc.teams.map((t) => [t.label, t.team ?? "(no id yet)", t.from, t.default ? "default" : ""]));
|
|
1629
|
-
const souls = Object.entries(doc.souls
|
|
1631
|
+
const souls = Object.entries(doc.souls).map(([k, e]) => `${k}: ${[e.default ? `default ${e.default}` : null, e.teams === "any" ? "any" : Array.isArray(e.teams) ? `[${e.teams.join(",")}]` : null].filter(Boolean).join(" ") || "default only"}`);
|
|
1630
1632
|
if (souls.length) console.log(`souls ${souls.join(" · ")}`);
|
|
1631
|
-
const defaults = Object.entries(doc.souls.default).map(([k, l]) => `${k}: ${l}`);
|
|
1632
|
-
if (defaults.length) console.log(`defaults ${defaults.join(" · ")}`);
|
|
1633
1633
|
for (const p of doc.problems) console.log(`${p.severity === "failure" ? "problem" : "warning"} ${p.code} ${p.message} — ${p.fix}`);
|
|
1634
1634
|
}
|
|
1635
1635
|
|
|
1636
|
-
/** `oats soul teams
|
|
1637
|
-
|
|
1636
|
+
/** The `oats soul teams` edit flags team model 3 removed (0.38.0): a soul's teams are the workspace's `souls:`. */
|
|
1637
|
+
const SOUL_TEAMS_REMOVED_FLAGS = ["--add", "--remove", "--default", "--clear-default"];
|
|
1638
|
+
const SOUL_TEAMS_REPLACEMENT = "souls: in oats-workspace.yaml (a PR to the workspace file)";
|
|
1639
|
+
|
|
1640
|
+
/** `oats soul teams <soul>|'*' [--json]` — which teams a soul may join here, its default, and why (team
|
|
1641
|
+
* model 3: the workspace's `souls:`, and the local teams it allows). Config only, read only. */
|
|
1638
1642
|
async function soulCmd() {
|
|
1639
1643
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1640
|
-
const usage = "usage: oats soul teams <soul>|'*'
|
|
1644
|
+
const usage = "usage: oats soul teams <soul>|'*' [--dir <deployment>] [--max-age <s>] [--json]";
|
|
1645
|
+
const removed = SOUL_TEAMS_REMOVED_FLAGS.find((f) => args.some((a) => a === f || a.startsWith(`${f}=`)));
|
|
1646
|
+
if (removed) bail("E_BAD_ARGS", `oats soul teams ${removed} was removed in 0.38.0 (team model 3): which teams a soul may join, and its default, are ${SOUL_TEAMS_REPLACEMENT}`, { flag: removed, replacement: SOUL_TEAMS_REPLACEMENT });
|
|
1641
1647
|
if (positional(1) !== "teams") bail("E_USAGE", usage);
|
|
1642
1648
|
const name = positional(2);
|
|
1643
|
-
if (name === undefined) bail("E_BAD_ARGS", `oats soul teams needs a soul (or '*'
|
|
1644
|
-
const edit = { add: labelsFlag("add"), remove: labelsFlag("remove"), setDefault: valueFlag("default") ?? null, clearDefault: args.includes("--clear-default") };
|
|
1649
|
+
if (name === undefined) bail("E_BAD_ARGS", `oats soul teams needs a soul (or '*') — ${usage}`);
|
|
1645
1650
|
const ctx = workspaceContext(bail);
|
|
1646
1651
|
let teamsCtx, soul = "*", key = "*";
|
|
1647
1652
|
if (name === "*") teamsCtx = await teamsContext(bail);
|
|
@@ -1655,19 +1660,16 @@ async function soulCmd() {
|
|
|
1655
1660
|
discovery = await discoverOrStandalone(ctx.local, { lock, deployment: ctx.deploymentDir, remoteOptions: ctx.remoteOptions });
|
|
1656
1661
|
entry = findSoulEntry(discovery, name);
|
|
1657
1662
|
} catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance); throw e; }
|
|
1658
|
-
soul = entry.name; key =
|
|
1663
|
+
soul = entry.name; key = teamKeyOf(entry);
|
|
1659
1664
|
teamsCtx = { deployment: ctx.deploymentDir, localPath: ctx.localPath, local: ctx.local, workspace: discovery.standalone === true ? null : discovery.workspace, workspaceKey: discovery.key ?? null };
|
|
1660
1665
|
}
|
|
1661
1666
|
const V = await import("../lib/teams-verbs.mjs");
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
if (JSON_MODE) { jsonOk(result ? { ...doc, changed: result.changed } : withObservation(doc)); return; }
|
|
1669
|
-
if (result) console.log(result.changed ? `Updated the teams of ${key === "*" ? "every soul" : key} in ${shortPath(teamsCtx.localPath)}` : "Nothing to change");
|
|
1670
|
-
console.log(`${key === "*" ? "every soul" : key} on this computer: default ${doc.defaultTeam ? `${doc.defaultTeam.label} (${doc.defaultTeam.from})` : "(none)"}`);
|
|
1667
|
+
let doc;
|
|
1668
|
+
try { doc = V.soulTeamsDocument(teamsCtx, { soul, key }); }
|
|
1669
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
1670
|
+
if (JSON_MODE) { jsonOk(withObservation(doc)); return; }
|
|
1671
|
+
console.log(`${key === "*" ? "unlisted souls (\"*\")" : key} here: default ${doc.defaultTeam ? `${doc.defaultTeam.label} (${doc.defaultTeam.from})` : "(none)"}`);
|
|
1672
|
+
console.log(`souls: ${doc.match === null ? "no entry matches" : `teams from ${JSON.stringify(doc.match)}`}${doc.defaultMatch !== null ? `, default from ${JSON.stringify(doc.defaultMatch)}` : ""}`);
|
|
1671
1673
|
if (doc.teams.length) printTable(["team", "id", "from", "why"], doc.teams.map((t) => [t.default ? `${t.label} (default)` : t.label, t.team ?? "(no id yet)", t.from, t.via.join(",")]));
|
|
1672
1674
|
}
|
|
1673
1675
|
|
|
@@ -1904,8 +1906,8 @@ async function statusDrift(data) {
|
|
|
1904
1906
|
* machine's parsed cache knows: `ref` itself when the cache holds its workspace file, or the host its
|
|
1905
1907
|
* cached oats-membership.yaml names when `ref` is a member ("workspace"). A member whose host is not
|
|
1906
1908
|
* 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
|
-
*
|
|
1909
|
+
* defines `workspace:` ("workspace"). The team model resolves as teamModel resolves it (the default is
|
|
1910
|
+
* the local one the workspace allows, else the workspace's; a committed team wins a collision) over the shared teams of, in order, the workspace
|
|
1909
1911
|
* file this run observed (`teamsFrom: "observed"`), the cached file ("cache": no git process), or none
|
|
1910
1912
|
* ("local"). `standalone` is the CONFIGURED standalone view only (oats-local.yaml `standalone:`): it
|
|
1911
1913
|
* reads no workspace file, so its local teams are the whole team model. A run that fell back to the
|
|
@@ -1928,10 +1930,12 @@ function workspaceIdentity(local, discovery) {
|
|
|
1928
1930
|
else if (cached?.file) [shared, teamsFrom] = [cached.file, "cache"];
|
|
1929
1931
|
}
|
|
1930
1932
|
const model = teamModel(shared, local);
|
|
1933
|
+
// A soul with no souls: default of its own lives here (team model 3: the local default the workspace allows, else the workspace's).
|
|
1934
|
+
const deploymentDefault = model.localDefault ?? model.workspaceDefault;
|
|
1931
1935
|
const labels = [...model.labels.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1932
1936
|
return {
|
|
1933
1937
|
key, ref, keyFrom, standalone,
|
|
1934
|
-
defaultTeam:
|
|
1938
|
+
defaultTeam: deploymentDefault === null ? null : { label: deploymentDefault, team: model.labels.get(deploymentDefault)?.team ?? null },
|
|
1935
1939
|
teams: Object.fromEntries(labels.map((l) => [l, model.labels.get(l).team])),
|
|
1936
1940
|
teamsFrom,
|
|
1937
1941
|
};
|
|
@@ -2010,8 +2014,8 @@ async function status() {
|
|
|
2010
2014
|
const verbose = args.includes("--verbose");
|
|
2011
2015
|
const problems = legacyLayoutProblems(root);
|
|
2012
2016
|
if (args.includes("--json")) {
|
|
2013
|
-
// The soul key (feature launch-preference): what souls.
|
|
2014
|
-
//
|
|
2017
|
+
// The soul key (feature launch-preference): what souls.launch and `oats soul teams <key>` use —
|
|
2018
|
+
// from the instances' records, so it needs no remote.
|
|
2015
2019
|
for (const a of data) a.key = agentSoulKey(a);
|
|
2016
2020
|
if (ws) for (const a of data) {
|
|
2017
2021
|
const stamp = ws.souls.get(a.name);
|
|
@@ -3002,7 +3006,9 @@ async function capabilityCommand() {
|
|
|
3002
3006
|
if (!trust.trusted) bail("E_CAPABILITY_BLOCKED", `${m.capability} executable command is blocked: ${trust.reason}`);
|
|
3003
3007
|
if (homeMeta && m.capability === homeMeta.messaging) {
|
|
3004
3008
|
const { liveTeams } = await import("../lib/instance-resolution.mjs");
|
|
3005
|
-
|
|
3009
|
+
let live;
|
|
3010
|
+
try { live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() }); }
|
|
3011
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
3006
3012
|
teamCtx = homeTeamCtx(live);
|
|
3007
3013
|
}
|
|
3008
3014
|
return runManifestCommand(m, { settings: capSettings[m.capability] || {}, origins: capOrigins[m.capability] }, teamCtx, () => m._dir, () => ({ dir: soulDir, cleanup: () => {} }));
|
|
@@ -3136,7 +3142,7 @@ function versionCmd() {
|
|
|
3136
3142
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
3137
3143
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
3138
3144
|
// never listed.
|
|
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-
|
|
3145
|
+
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-3", "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 }));
|
|
3140
3146
|
return;
|
|
3141
3147
|
}
|
|
3142
3148
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3810,10 +3816,10 @@ Usage:
|
|
|
3810
3816
|
teams on this deployment
|
|
3811
3817
|
oats teams [--json] | add <label> --team <id> [--description <d>] | remove <label>
|
|
3812
3818
|
| default <label> [--dir <d>] this deployment's teams (shared + local), the
|
|
3813
|
-
[--max-age <s>] (the read form only) default
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
[--max-age <s>] (
|
|
3819
|
+
[--max-age <s>] (the read form only) default, the workspace's souls:; add/remove/default
|
|
3820
|
+
edit oats-local.yaml (only with localTeams: true)
|
|
3821
|
+
oats soul teams <soul>|'*' [--dir <d>] which teams a soul may join here, its default, and
|
|
3822
|
+
[--max-age <s>] [--json] why (the workspace's souls: and local teams)
|
|
3817
3823
|
oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
|
|
3818
3824
|
read-only Git observation of the instance's work
|
|
3819
3825
|
tree: branch, status (renames kept), ahead/behind
|
package/docs/capabilities.md
CHANGED
|
@@ -193,23 +193,27 @@ per-deployment activation or exclusion maps.
|
|
|
193
193
|
|
|
194
194
|
### Teams in the provider environment
|
|
195
195
|
|
|
196
|
-
A soul's teams here (the
|
|
197
|
-
`
|
|
196
|
+
A soul's teams here (the workspace's shared teams, `defaultTeam` and `souls:`, and
|
|
197
|
+
the deployment's `oats-local.yaml` `teams` and `defaultTeam` where the workspace
|
|
198
|
+
allows them — see [workspaces.md](workspaces.md#teams)) travel **beside** a
|
|
198
199
|
provider's settings, never inside them, in the environment of every hook, home
|
|
199
200
|
command and provider check:
|
|
200
201
|
|
|
201
202
|
- `OATS_DEFAULT_TEAM` — the soul's default label; `OATS_DEFAULT_TEAM_ID` — its
|
|
202
|
-
provider id; `OATS_DEFAULT_TEAM_FROM` — `
|
|
203
|
-
(`
|
|
203
|
+
provider id; `OATS_DEFAULT_TEAM_FROM` — `soul` (the soul's `souls:` default in
|
|
204
|
+
the workspace file), `deployment` (the local `defaultTeam`) or `workspace` (the
|
|
205
|
+
workspace's `defaultTeam`).
|
|
204
206
|
- No default configured: none of the three is set. An **unmapped** default (a
|
|
205
207
|
shared team declared without an id): `OATS_DEFAULT_TEAM` and
|
|
206
208
|
`OATS_DEFAULT_TEAM_FROM` are set and `OATS_DEFAULT_TEAM_ID` is not. What a
|
|
207
209
|
provider does then is its own contract; a messaging provider typically
|
|
208
210
|
refuses the spawn, naming the unmapped label, or saying no team is
|
|
209
211
|
configured.
|
|
210
|
-
- `OATS_TEAMS` — JSON `[{label, team, default, from: "shared"|"local"}]`:
|
|
211
|
-
mapped team the soul may be in here, the default included
|
|
212
|
-
default first, then by label
|
|
212
|
+
- `OATS_TEAMS` — JSON `[{label, team, default, from: "shared"|"local", via}]`:
|
|
213
|
+
every mapped team the soul may be in here, the default included
|
|
214
|
+
(`default: true`), default first, then by label; `via` says why
|
|
215
|
+
(`default`, `workspace`, `local`). Eligible to join = the rows with
|
|
216
|
+
`default: false`, and no other team: a provider refuses a join outside them.
|
|
213
217
|
Unset when a home's teams are unknown (none recorded, and unreadable now).
|
|
214
218
|
- `OATS_TEAMS_SOURCE` — `live` (the workspace and `oats-local.yaml` read now, or
|
|
215
219
|
a fresh resolution) or `recorded` (the spawn-time record). **A provider leaves
|
|
@@ -341,7 +345,9 @@ Hooks receive:
|
|
|
341
345
|
A spawn hook also gets `OATS_TASK`, `OATS_REPO`, `OATS_BRANCH`, `OATS_WORK`,
|
|
342
346
|
`OATS_HARNESS`, `OATS_KIND` and, for a spawn a trigger started,
|
|
343
347
|
`OATS_TRIGGER_EVENT_FILE`. A launch hook also gets `OATS_HARNESS` and
|
|
344
|
-
`OATS_PREVIOUS_HARNESS
|
|
348
|
+
`OATS_PREVIOUS_HARNESS`, and `OATS_LAUNCH_PREVIEW=1` when it runs for a
|
|
349
|
+
preview (only a preview-aware hook does, below); on a real run
|
|
350
|
+
`OATS_LAUNCH_PREVIEW` is not set.
|
|
345
351
|
|
|
346
352
|
`OATS_SETTINGS_ORIGINS` says where each leaf of
|
|
347
353
|
`OATS_SETTINGS` came from: a JSON object from a JSON pointer to `{ kind, at }`,
|
|
@@ -350,7 +356,8 @@ A spawn hook also gets `OATS_TASK`, `OATS_REPO`, `OATS_BRANCH`, `OATS_WORK`,
|
|
|
350
356
|
`{"/harvest":{"kind":"soul","at":"soul.yaml#/knowledge"}}`. A provider tells a
|
|
351
357
|
soul-set value from a host-set one there, and never reads `soul.yaml` for it;
|
|
352
358
|
a home with none recorded gives `{}`. A final JSON line may return `meta`,
|
|
353
|
-
`brief`, `warning`, or harness-specific `launch` arguments
|
|
359
|
+
`brief`, `warning`, or harness-specific `launch` arguments; a preview-aware
|
|
360
|
+
launch hook's preview answer may add `volatileEnv` (below). A **spawn or
|
|
354
361
|
launch hook** may also return an `env` object for the launched process;
|
|
355
362
|
returning `env` from a retire hook is an explicit contract error.
|
|
356
363
|
|
|
@@ -364,6 +371,54 @@ start (a renewed session grant, for example) leaves the CURRENT one on record. A
|
|
|
364
371
|
launch hook that answers without `meta` keeps its previous entry; a start whose
|
|
365
372
|
preparation fails changes nothing.
|
|
366
373
|
|
|
374
|
+
A launch hook may do idempotent provider registration on a real start (an
|
|
375
|
+
aweb home registering with the host wake broker, for example). How the
|
|
376
|
+
kernel runs it depends on whether its capability declares **preview
|
|
377
|
+
awareness**: `"launchPreview": true` at the top level of its manifest. The
|
|
378
|
+
kernel reads the declaration from the home's own module copy, so a home keeps
|
|
379
|
+
the behaviour of the module it was spawned with.
|
|
380
|
+
|
|
381
|
+
**A preview-aware hook** must change nothing under `OATS_LAUNCH_PREVIEW=1`,
|
|
382
|
+
and must return the same contribution (`launch` arguments and `env`) as for
|
|
383
|
+
a real start. It runs twice per start:
|
|
384
|
+
|
|
385
|
+
1. **As a preview, under `OATS_LAUNCH_PREVIEW=1`.** The start's preflight uses
|
|
386
|
+
this contribution: trust, environment ownership, the harness-package
|
|
387
|
+
probe and the rendered command. `oats launch-config preview` (which
|
|
388
|
+
Desktop's start dialog uses) runs only this pass.
|
|
389
|
+
2. **For real, without the flag.** This pass runs only once preflight has
|
|
390
|
+
passed, including the check that the home is not already running
|
|
391
|
+
(`E_SESSION_RUNNING`), and before a restart stops the running harness.
|
|
392
|
+
|
|
393
|
+
The real run's `meta` and warnings are what the start records. If its
|
|
394
|
+
contribution differs from its preview contribution, the start is refused
|
|
395
|
+
with `E_LAUNCH_PREPARATION` and nothing is stopped or started.
|
|
396
|
+
|
|
397
|
+
Some values only a real run can know, such as a credential minted at start.
|
|
398
|
+
A preview answer may list those names in `volatileEnv` (for example
|
|
399
|
+
`"volatileEnv": ["AWEB_IDENTITY_HOME"]`, beside `env`). For those names:
|
|
400
|
+
|
|
401
|
+
- The start takes the values from the real run, records them, and renders
|
|
402
|
+
the launch command again with them.
|
|
403
|
+
- The comparison leaves those values out. Everything else must still be
|
|
404
|
+
identical.
|
|
405
|
+
|
|
406
|
+
Each name must be one the same hook returned in `env`. Preflight sees only
|
|
407
|
+
the preview's value, so a volatile name must not affect how the harness
|
|
408
|
+
resolves its packages. The kernel refuses (`E_LAUNCH_PREPARATION`) a volatile
|
|
409
|
+
`CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `PI_CODING_AGENT_DIR`. It reads
|
|
410
|
+
`volatileEnv` only from a preview answer and never records it.
|
|
411
|
+
|
|
412
|
+
**A hook that does not declare preview awareness** runs once per start, for
|
|
413
|
+
real, during preflight, before the checks that use its contribution. A
|
|
414
|
+
refused start may therefore already have run it. `oats launch-config preview`
|
|
415
|
+
never runs it. The preview shows that capability's recorded contribution, and
|
|
416
|
+
its `capabilities` check says the hook was not run.
|
|
417
|
+
|
|
418
|
+
`launchPreview` is a top-level key so that a kernel older than 0.37 ignores
|
|
419
|
+
it and runs the hook once, as it always did. A key inside the hook's
|
|
420
|
+
declaration would make such a kernel refuse the whole package.
|
|
421
|
+
|
|
367
422
|
Hook environment values are strings, at most 8192 UTF-8 bytes, with no NUL or
|
|
368
423
|
newlines. Names use the portable environment grammar and must belong to an
|
|
369
424
|
unambiguous vendor namespace. Only a dotted capability ID participates: its
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
"type": "boolean",
|
|
62
62
|
"description": "Workspace discovery: true makes this member capability repo-owned — listed (private: true) but usable only by souls of its own repository (E_CAPABILITY_PRIVATE elsewhere)."
|
|
63
63
|
},
|
|
64
|
+
"launchPreview": {
|
|
65
|
+
"type": "boolean",
|
|
66
|
+
"description": "true declares the launch hook preview-aware: it changes nothing under OATS_LAUNCH_PREVIEW=1 and returns the same contribution (apart from its preview answer's volatileEnv values), so a start runs it as a preview during preflight and for real after it, and a launch preview runs it. Without it the hook runs once per start, for real, during preflight, and never for a launch preview. Top-level so that older kernels ignore it."
|
|
67
|
+
},
|
|
64
68
|
"layer": {
|
|
65
69
|
"enum": [
|
|
66
70
|
"knowledge",
|
package/docs/configuration.md
CHANGED
|
@@ -21,15 +21,10 @@ settings: # host-owned values per capability
|
|
|
21
21
|
oats.okf:
|
|
22
22
|
bindings-file: /Users/ana/.oats/okf-bindings.json
|
|
23
23
|
|
|
24
|
-
teams: # LOCAL teams
|
|
24
|
+
teams: # LOCAL teams (only where the workspace says localTeams: true)
|
|
25
25
|
ana-research: { team: "ana-research:acme.aweb.ai", description: Ana's research }
|
|
26
|
-
defaultTeam: ana-research #
|
|
26
|
+
defaultTeam: ana-research # this deployment's default team (same condition)
|
|
27
27
|
souls:
|
|
28
|
-
teams:
|
|
29
|
-
"*": [ana-research] # every soul is in these teams here
|
|
30
|
-
data-analyst: [platform] # and this one also joins a shared team
|
|
31
|
-
default:
|
|
32
|
-
data-analyst: platform # per-soul override of defaultTeam
|
|
33
28
|
disabled: [legacy-bot] # souls not run on this machine
|
|
34
29
|
|
|
35
30
|
host:
|
|
@@ -66,10 +61,8 @@ refused (`E_WORKSPACE_SCHEMA`).
|
|
|
66
61
|
| `standalone` | A repo ref to realize on its own: its souls and `from: here` capabilities plus `oats.core`, with no workspace lookup. For a repository whose workspace this machine cannot read ([workspaces.md](workspaces.md#the-standalone-case)). |
|
|
67
62
|
| `clones` | `<repo key>: <absolute path>` for a member clone that is not at `<deployment>/<member name>/`. Only a soul whose work target needs a clone (`work: worktree \| checkout`) uses it. Lookup order: `spawn --repo`, then this map, then `<deployment>/<member name>` (a member named `agents` → `<deployment>/agents-repo`). None → `E_CLONE_MISSING`; a directory whose `origin` is another repository → `E_CLONE_MISMATCH`. |
|
|
68
63
|
| `settings.<cap>.<key>` | Host-owned values a capability's manifest asks for: absolute paths, state roots, delivery modes. The workspace file refuses absolute paths; they go here. Merged into the capability's provider payload after the soul's own and before any `--provider` flag ([three homes](workspaces.md#provider-payloads-have-three-homes)). |
|
|
69
|
-
| `teams.<label>` | A **local** team: `{ team: <provider team id>, description? }
|
|
70
|
-
| `defaultTeam` |
|
|
71
|
-
| `souls.teams` | Which teams each soul joins here: `"*"` applies to every soul; a soul's own entry (its name, or `<package>/<soul>`) adds to it. Every soul is also in its default team. Written by `oats soul teams <soul>\|'*' --add … --remove …`. |
|
|
72
|
-
| `souls.default` | A per-soul override of `defaultTeam`; it must be one of that soul's teams here (`E_TEAM_NOT_ELIGIBLE`). Written by `oats soul teams <soul> --default <label>`. |
|
|
64
|
+
| `teams.<label>` | A **local** team: `{ team: <provider team id>, description? }`, a team only this deployment uses; every soul may join it. Allowed only where `oats-workspace.yaml` says `localTeams: true` (or in the standalone view); otherwise refused (`E_WORKSPACE_SCHEMA`, reason `local-teams-closed`). Shared teams are committed in `oats-workspace.yaml`; a label in both is `team-label-collision` (the shared one wins). Written by `oats teams add <label> --team <id>` and `oats teams remove <label>`. |
|
|
65
|
+
| `defaultTeam` | This deployment's default team: a label of a local or shared team, under the same condition as `teams`. A soul's own default in the workspace's `souls:` wins over it; it wins over the workspace's `defaultTeam`. The first `oats teams add` sets it; `oats teams default <label>` changes it. |
|
|
73
66
|
| `souls.disabled` | Souls not run on this machine; a spawn is refused with `E_SOUL_DISABLED`. A bare name disables every soul of that name; `<package>/<soul>` or `<member>/<soul>` disables one. |
|
|
74
67
|
| `session.tmuxSession` | The tmux session new tmux instances open their windows in (0.31). Absent: `OATS_TMUX_SESSION`, else `PI_AGENTS_TMUX_SESSION` (the pre-0.31 variable), else `oats-agents`. `session: { tmuxSession: pi-agents }` keeps the pre-0.31 layout. `oats inspect --json` reports it as `session`. |
|
|
75
68
|
| `host.name` | This machine's name. A workspace trigger or schedule runs only on the host named by its `runsOn` ([schedules.md](schedules.md)). |
|
|
@@ -78,14 +71,11 @@ refused (`E_WORKSPACE_SCHEMA`).
|
|
|
78
71
|
| `launch-configs.<name>` | A named way to start a harness on this host, chosen at spawn or session start, never by the soul. `default: true` makes it this host's baseline for its harness (0.32). See [Launch configurations](#launch-configurations). |
|
|
79
72
|
| `souls.launch` | This machine's launch preference per soul (0.30): `"*"` for every soul, a soul's own entry (its name, or `<package>/<soul>`) over it. A value is a `launch-configs` name or an inline `{ harness, model? }`. It overrides the soul's own `launch:`; explicit spawn flags win over both. See [Launch preferences](#launch-preferences). |
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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).
|
|
74
|
+
Which teams a soul may join, and its default, are committed in the workspace's
|
|
75
|
+
`souls:`, never here: `souls.teams` and `souls.default` were removed in 0.38.0
|
|
76
|
+
(`E_WORKSPACE_SCHEMA`, reason `removed-key`; the refusal prints the `souls:` to
|
|
77
|
+
commit instead). How teams are resolved, and what a messaging provider does
|
|
78
|
+
with them, is in [workspaces.md](workspaces.md#teams).
|
|
89
79
|
|
|
90
80
|
## Launch configurations
|
|
91
81
|
|
|
@@ -242,7 +232,12 @@ recipe is resolved again against the home's recorded context and every check
|
|
|
242
232
|
runs first. With `--reselect-launch`, the launch preferences decide again
|
|
243
233
|
(the home's recorded soul and this deployment's `souls.launch`). A capability that contributed harness-specific arguments must
|
|
244
234
|
declare a `launch` hook to follow a harness change; otherwise the start is
|
|
245
|
-
refused (`E_LAUNCH_PREPARATION`).
|
|
235
|
+
refused (`E_LAUNCH_PREPARATION`). The checks use the preview run
|
|
236
|
+
(`OATS_LAUNCH_PREVIEW=1`) of the launch hooks whose capabilities declare
|
|
237
|
+
`launchPreview`. Those hooks run for real only after every check has passed,
|
|
238
|
+
so a start refused by preflight has run none of them for real. Other launch
|
|
239
|
+
hooks run once, for real, during the checks (see
|
|
240
|
+
[capabilities.md](capabilities.md)). A launch hook's warnings do not stop
|
|
246
241
|
the start: `session start|restart` print them (and answer them as
|
|
247
242
|
`warnings` under `--json`), as spawn does, and each is kept as a
|
|
248
243
|
`launch-warning` instance event (`oats instance events`).
|
|
@@ -327,7 +322,7 @@ is safe to delete.
|
|
|
327
322
|
oats workspace status # members, locked packages, external souls
|
|
328
323
|
oats sync # confirm, resolve, lock, report the diff
|
|
329
324
|
oats teams # shared and local teams, and the default
|
|
330
|
-
oats soul teams <soul> # the teams one soul
|
|
325
|
+
oats soul teams <soul> # the teams one soul may join here, and why
|
|
331
326
|
oats spawn <soul> --preview # the exact modules, teams and provider payloads
|
|
332
327
|
oats doctor # this deployment's files and the lock
|
|
333
328
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Team model v2: shared and local teams, the default team, and membership per deployment
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
3
|
+
**Status:** superseded in part by [team model 3](2026-10-02-team-model-3.md) (kernel 0.38.0): which teams a soul may join and its default team are committed in the workspace's `souls:` and `defaultTeam`, and local teams need `localTeams: true`; `souls.teams`, `souls.default` and the `oats soul teams` edits below are removed. The rest stands. Decided and implemented (kernel 0.30.0, feature `team-model-2`). This record decides where teams, the default team and team membership live, how the kernel resolves them, and what the kernel hands a messaging provider. The reference pages ([workspaces.md](../workspaces.md#teams), [capabilities.md](../capabilities.md#teams-in-the-provider-environment), [desktop-cli-api.md](../desktop-cli-api.md#team-model-v2-feature-team-model-2-oats-0300-replaces-feature-teams)) win on operator-visible behaviour.
|
|
4
4
|
|
|
5
5
|
## Why
|
|
6
6
|
|