@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 +86 -9
- package/docs/configuration.md +6 -0
- package/docs/desktop-cli-api.md +112 -13
- package/docs/desktop.md +47 -0
- package/docs/integrations.md +1 -1
- package/docs/knowledge.md +1 -1
- package/docs/oats-workspace.schema.json +28 -0
- package/docs/official-catalog.md +3 -3
- package/docs/packages.md +12 -12
- package/docs/release-notes/v0.36.0.md +118 -0
- package/docs/release-notes/v0.36.1.md +103 -0
- package/docs/servers.md +3 -1
- package/docs/souls-and-instances.md +2 -2
- package/docs/workspaces.md +35 -2
- package/lib/servers.mjs +4 -1
- package/lib/teams.mjs +36 -3
- package/lib/workspace.mjs +14 -0
- package/package-catalog.json +2 -2
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +2 -2
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
|
|
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)`);
|
package/docs/configuration.md
CHANGED
|
@@ -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`
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -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.
|
|
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.
|
|
787
|
-
"version":"4.1.
|
|
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.
|
|
877
|
-
"commit":"
|
|
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":"
|
|
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
|
|
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)
|
|
1716
|
-
|
|
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,
|
package/docs/integrations.md
CHANGED
package/docs/knowledge.md
CHANGED
|
@@ -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,
|
package/docs/official-catalog.md
CHANGED
|
@@ -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.
|
|
13
|
-
| `oats.okf` | `v4.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
78
|
-
oats.okf: v4.1.
|
|
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.
|
|
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.
|
|
164
|
-
"commit": "
|
|
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.
|
|
335
|
-
"oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.4.
|
|
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.
|
|
341
|
-
resolves to tag `oats-framework/v1.4.
|
|
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
|
|
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.
|
|
156
|
-
"commit": "
|
|
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": {
|
package/docs/workspaces.md
CHANGED
|
@@ -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.
|
|
58
|
-
oats.okf: v4.1.
|
|
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
|
package/package-catalog.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
-
oats.okf: v4.1.
|
|
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 } }
|