@awebai/oats 0.35.5 → 0.36.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 +65 -5
- package/docs/desktop-cli-api.md +75 -5
- package/docs/desktop.md +47 -0
- package/docs/release-notes/v0.36.0.md +118 -0
- package/docs/servers.md +3 -1
- package/lib/servers.mjs +4 -1
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -1850,7 +1850,7 @@ async function statusDrift(data) {
|
|
|
1850
1850
|
catch { /* not a workspace soul (classic, or an unreadable stamp) */ }
|
|
1851
1851
|
}
|
|
1852
1852
|
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 };
|
|
1853
|
+
if (!anything) return { drift: new Map(), soul: new Map(), souls, unreachable: null, local: ctx.local, discovery: null };
|
|
1854
1854
|
const deploymentDir = dirname(ctx.path);
|
|
1855
1855
|
let lock = null;
|
|
1856
1856
|
try { lock = readLockIfPresent(deploymentDir); } catch { lock = null; }
|
|
@@ -1860,7 +1860,7 @@ async function statusDrift(data) {
|
|
|
1860
1860
|
try { const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs"); discovery = await discoverOrStandalone(ctx.local, { lock, remoteOptions: remoteOptionsFromEnv() }); }
|
|
1861
1861
|
catch (e) {
|
|
1862
1862
|
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) } };
|
|
1863
|
+
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
1864
|
}
|
|
1865
1865
|
const { driftOf, soulDriftOf } = await import("../lib/materialize.mjs");
|
|
1866
1866
|
const drift = new Map();
|
|
@@ -1879,7 +1879,67 @@ async function statusDrift(data) {
|
|
|
1879
1879
|
const member = memberRowByKey(discovery.members, stamp.repoKey);
|
|
1880
1880
|
if (member && typeof member.commit === "string" && (member.confirmed || (discovery.standalone === true && member.key === discovery.key))) stamp.current = member.commit;
|
|
1881
1881
|
}
|
|
1882
|
-
return { drift, soul, souls, unreachable: null };
|
|
1882
|
+
return { drift, soul, souls, unreachable: null, local: ctx.local, discovery };
|
|
1883
|
+
}
|
|
1884
|
+
/** The deployment's workspace identity in `oats status --json` (feature workspace-identity), read
|
|
1885
|
+
* OFFLINE. `key` is the workspace HOST's canonical repo key (parseRepoRef(...).key) and `ref` the
|
|
1886
|
+
* reference as oats-local.yaml writes it. The host is the one this run observed, else the one this
|
|
1887
|
+
* machine's parsed cache knows: `ref` itself when the cache holds its workspace file, or the host its
|
|
1888
|
+
* cached oats-membership.yaml names when `ref` is a member ("workspace"). A member whose host is not
|
|
1889
|
+
* known keys as itself ("member"); a ref nothing is known of is taken as the host, as the schema
|
|
1890
|
+
* defines `workspace:` ("workspace"). The team model resolves as teamModel resolves it (the default
|
|
1891
|
+
* label is local; a committed team wins a collision) over the shared teams of, in order, the workspace
|
|
1892
|
+
* file this run observed (`teamsFrom: "observed"`), the cached file ("cache": no git process), or none
|
|
1893
|
+
* ("local"). `standalone` is the CONFIGURED standalone view only (oats-local.yaml `standalone:`): it
|
|
1894
|
+
* reads no workspace file, so its local teams are the whole team model. A run that fell back to the
|
|
1895
|
+
* standalone view (the host unreadable) is not: its team model is the workspace's, and its
|
|
1896
|
+
* discovery is the member's, never the host's key. */
|
|
1897
|
+
function workspaceIdentity(local, discovery) {
|
|
1898
|
+
const ref = local.workspace;
|
|
1899
|
+
const standalone = typeof local.standalone === "string" && local.standalone !== "";
|
|
1900
|
+
const observed = discovery && discovery.standalone !== true && discovery.workspace ? discovery : null;
|
|
1901
|
+
const cached = observed ? null : cachedWorkspace(ref);
|
|
1902
|
+
const keyOf = (r) => { try { return remoteModule.parseRepoRef(r).key; } catch { return null; } };
|
|
1903
|
+
let key, keyFrom;
|
|
1904
|
+
if (observed) [key, keyFrom] = [observed.key, "workspace"];
|
|
1905
|
+
else if (cached && cached.host === null) [key, keyFrom] = [keyOf(ref), "member"];
|
|
1906
|
+
else [key, keyFrom] = [keyOf(cached?.host ?? ref), "workspace"];
|
|
1907
|
+
if (key === null) keyFrom = null; // a reference parseRepoRef refuses: unusable, never matched
|
|
1908
|
+
let shared = null, teamsFrom = "local";
|
|
1909
|
+
if (!standalone) {
|
|
1910
|
+
if (observed) [shared, teamsFrom] = [observed.workspace, "observed"];
|
|
1911
|
+
else if (cached?.file) [shared, teamsFrom] = [cached.file, "cache"];
|
|
1912
|
+
}
|
|
1913
|
+
const model = teamModel(shared, local);
|
|
1914
|
+
const labels = [...model.labels.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1915
|
+
return {
|
|
1916
|
+
key, ref, keyFrom, standalone,
|
|
1917
|
+
defaultTeam: model.defaultTeam === null ? null : { label: model.defaultTeam, team: model.labels.get(model.defaultTeam)?.team ?? null },
|
|
1918
|
+
teams: Object.fromEntries(labels.map((l) => [l, model.labels.get(l).team])),
|
|
1919
|
+
teamsFrom,
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
/** What this machine's parsed cache knows of the workspace `ref` names (the values observeWorkspace and
|
|
1923
|
+
* confirmMembership stored, each at its repo's last observed commit): { host, file } — `host` the ref
|
|
1924
|
+
* of the workspace host, `file` its workspace file or null — when `ref` is the host (its workspace file
|
|
1925
|
+
* is cached) or a member whose cached oats-membership.yaml names it (discoverOrStandalone follows the
|
|
1926
|
+
* same backlink). A member's backlink is cached whether its own workspace slot was observed (missing)
|
|
1927
|
+
* or never was (the host's discovery confirmed it). { host: null, file: null } for a ref cached as
|
|
1928
|
+
* having no workspace file and no backlink; null when nothing is known. */
|
|
1929
|
+
function cachedWorkspace(ref) {
|
|
1930
|
+
const options = remoteOptionsFromEnv();
|
|
1931
|
+
const cached = (r, item) => {
|
|
1932
|
+
const commit = remoteModule.lastObservedCommit(r, options);
|
|
1933
|
+
const value = commit ? remoteModule.peekAtCommit(r, commit, item, options) : undefined;
|
|
1934
|
+
return value && typeof value === "object" ? value : null;
|
|
1935
|
+
};
|
|
1936
|
+
const fileOf = (read) => (read && !read.missing && !read.problems && read.value && typeof read.value === "object" ? read.value : null);
|
|
1937
|
+
const read = cached(ref, "workspace");
|
|
1938
|
+
if (read && !read.missing) return { host: ref, file: fileOf(read) };
|
|
1939
|
+
const membership = cached(ref, "membership");
|
|
1940
|
+
const host = membership?.kind === "ok" && typeof membership.value?.workspace === "string" ? membership.value.workspace : null;
|
|
1941
|
+
if (host !== null) return { host, file: fileOf(cached(host, "workspace")) };
|
|
1942
|
+
return read ? { host: null, file: null } : null;
|
|
1883
1943
|
}
|
|
1884
1944
|
/** One `modules:` line per module. */
|
|
1885
1945
|
function driftLine(row) {
|
|
@@ -1948,7 +2008,7 @@ async function status() {
|
|
|
1948
2008
|
}
|
|
1949
2009
|
}
|
|
1950
2010
|
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;
|
|
2011
|
+
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
2012
|
}
|
|
1953
2013
|
console.log(`oats status — agents root ${shortPath(root)}\n`);
|
|
1954
2014
|
if (ws?.unreachable) console.log(` workspace: unreachable (${ws.unreachable.reason}) — drift unknown\n`);
|
|
@@ -3059,7 +3119,7 @@ function versionCmd() {
|
|
|
3059
3119
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
3060
3120
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
3061
3121
|
// 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 }));
|
|
3122
|
+
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
3123
|
return;
|
|
3064
3124
|
}
|
|
3065
3125
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
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 |
|
|
@@ -1679,7 +1680,8 @@ Not an envelope: `{root, agents, observation?, workspace?, problems?, warnings?}
|
|
|
1679
1680
|
"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
1681
|
"commit":"ab897841…","current":{"commit":"ab897841…","version":"2.1.3"},"status":"current"}],
|
|
1681
1682
|
"soul":{"repoKey":"github.com/nw/agents","commit":"66566512…","current":"66566512…","status":"current"}}]}],
|
|
1682
|
-
"workspace":{"reachable":true}
|
|
1683
|
+
"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"},
|
|
1684
|
+
"teams":{"eng":"eng:nw.aweb.ai","mine":"mine:ana.aweb.ai","ops":null},"teamsFrom":"observed"}}
|
|
1683
1685
|
```
|
|
1684
1686
|
|
|
1685
1687
|
- **Agent rows**: the soul's recorded definition plus `dir` and `instances`,
|
|
@@ -1712,11 +1714,68 @@ Not an envelope: `{root, agents, observation?, workspace?, problems?, warnings?}
|
|
|
1712
1714
|
reason?}`; a package soul adds `package`, `version`, `currentVersion`
|
|
1713
1715
|
(`missing` reasons: `package-absent`, `soul-absent`).
|
|
1714
1716
|
- **`workspace`**: `{reachable: true}`, or `{reachable: false, code, reason,
|
|
1715
|
-
message}` (modules then stay the recorded map)
|
|
1716
|
-
|
|
1717
|
+
message}` (modules then stay the recorded map), plus the deployment's
|
|
1718
|
+
[workspace identity](#workspace-identity-feature-workspace-identity-oats-0360)
|
|
1719
|
+
either way. Absent without `oats-local.yaml`.
|
|
1717
1720
|
- `problems`: the legacy-home rows ([dispatch errors](#dispatch-errors)).
|
|
1718
1721
|
`warnings`: envelope warnings. `--team` is `E_BAD_ARGS` (an envelope).
|
|
1719
1722
|
|
|
1723
|
+
<a id="workspace-identity-feature-workspace-identity-oats-0360"></a>
|
|
1724
|
+
**Workspace identity** (feature `workspace-identity`, OATS 0.36.0). The
|
|
1725
|
+
`workspace` object says which workspace and teams this deployment is, read
|
|
1726
|
+
offline with no network, so it is there whether `reachable` is `true` or
|
|
1727
|
+
`false`:
|
|
1728
|
+
|
|
1729
|
+
| Key | Meaning |
|
|
1730
|
+
|---|---|
|
|
1731
|
+
| `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 |
|
|
1732
|
+
| `ref` | the reference exactly as `oats-local.yaml` writes it (`workspace:`), for display only |
|
|
1733
|
+
| `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 |
|
|
1734
|
+
| `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 |
|
|
1735
|
+
| `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 |
|
|
1736
|
+
| `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 |
|
|
1737
|
+
| `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 |
|
|
1738
|
+
|
|
1739
|
+
A configured standalone deployment reads no workspace file, so it is always
|
|
1740
|
+
`teamsFrom: "local"`, with its local teams only (as spawn resolves them there).
|
|
1741
|
+
`oats status` only reads the workspace when an instance records modules or a
|
|
1742
|
+
workspace soul, so an empty deployment answers from the cache, or from local
|
|
1743
|
+
teams on a host that has not observed its workspace (`oats sync` and
|
|
1744
|
+
`oats teams` observe it). The cache is the running kernel's own: after an
|
|
1745
|
+
OATS upgrade it is empty until the host next observes its workspace.
|
|
1746
|
+
|
|
1747
|
+
**Matching workspaces across machines.** Two deployments are the same
|
|
1748
|
+
workspace when their `key`s are equal; `ref` is never compared. The identity
|
|
1749
|
+
is resolved the same offline way for every deployment, standalone included,
|
|
1750
|
+
whatever its team view:
|
|
1751
|
+
|
|
1752
|
+
- `keyFrom: "member"` is unresolved: never match it, and show it as
|
|
1753
|
+
unresolved (the host is learned when the deployment observes its
|
|
1754
|
+
workspace, e.g. `oats sync`).
|
|
1755
|
+
- `keyFrom: "workspace"` with nothing observed (`teamsFrom: "local"` on a
|
|
1756
|
+
deployment that is not standalone) means the ref was taken as the host, as
|
|
1757
|
+
the schema defines. If it is really a member, the worst case is a split
|
|
1758
|
+
(one workspace shown as two until `oats sync` there), never a wrong merge.
|
|
1759
|
+
- A `null` key is an unusable reference and never matches. Show `ref` with
|
|
1760
|
+
"this deployment's workspace reference isn't valid; fix oats-local.yaml".
|
|
1761
|
+
- Known limit: `parseRepoRef` lowercases the host but keeps the path's case,
|
|
1762
|
+
so references that differ in owner or repository case give different keys.
|
|
1763
|
+
|
|
1764
|
+
**Matching teams across machines.** This is the rule for comparing two
|
|
1765
|
+
deployments' teams (as the Desktop does to attach a remote machine to a
|
|
1766
|
+
workspace):
|
|
1767
|
+
|
|
1768
|
+
- `teamsFrom` `"observed"` or `"cache"`: a `null` team is **unmapped**, and
|
|
1769
|
+
unmapped matches only unmapped.
|
|
1770
|
+
- `standalone: true` with `teamsFrom: "local"`: the local config IS the
|
|
1771
|
+
complete team model, so a `null` team is **unmapped** (matches only
|
|
1772
|
+
unmapped). Reason to show: "teams are local only on this host
|
|
1773
|
+
(standalone)", with no sync advice.
|
|
1774
|
+
- `standalone: false` with `teamsFrom: "local"`: a `null` default team is
|
|
1775
|
+
**unknown** and never matches. Reason to show: "this host hasn't observed
|
|
1776
|
+
its workspace yet; run oats sync there". A non-null default team (a locally
|
|
1777
|
+
mapped team) matches normally.
|
|
1778
|
+
|
|
1720
1779
|
**Desktop facts** (feature `desktop-facts`): `startedAt` is the last start or
|
|
1721
1780
|
restart, else `createdAt` for a launched home, else `null`. `modelFrom` is
|
|
1722
1781
|
`"soul"`, `"spawn"` or `"start"` (an explicit `--model`), `"launch-config"`,
|
|
@@ -1738,6 +1797,8 @@ route target:
|
|
|
1738
1797
|
{"id":"build:3f2a…","server":"build","label":"Build box","registrationPresent":true,
|
|
1739
1798
|
"target":{"sshHost":"build-host","workspace":"/srv/team","oatsPath":"oats"},
|
|
1740
1799
|
"probe":{"ok":true},"agentsRoot":"/srv/team/agents",
|
|
1800
|
+
"workspace":{"reachable":true,"key":"github.com/acme/team","ref":"git:github.com/acme/team","keyFrom":"workspace","standalone":false,"defaultTeam":{"label":"default","team":"acme:team"},
|
|
1801
|
+
"teams":{"default":"acme:team"},"teamsFrom":"observed"},
|
|
1741
1802
|
"souls":[{"name":"dev","harness":"claude","work":"worktree","agentsRoot":"/srv/team/agents"}],
|
|
1742
1803
|
"instances":[{"server":"build","instance":"dev-a","agent":"dev","home":"/srv/team/agents/dev/instances/dev-a",
|
|
1743
1804
|
"agentsRoot":"/srv/team/agents","harness":"claude","backend":"tmux","tmux":{"session":"oats-agents","window":"dev-a"},
|
|
@@ -1749,6 +1810,15 @@ route target:
|
|
|
1749
1810
|
"retireFailures":[]}
|
|
1750
1811
|
```
|
|
1751
1812
|
|
|
1813
|
+
- **`workspace`** (feature `workspace-identity`, OATS 0.36.0): the host's
|
|
1814
|
+
own `status --json` [`workspace` object](#workspace-identity-feature-workspace-identity-oats-0360),
|
|
1815
|
+
relayed verbatim, or `null` when the host reports none (a deployment
|
|
1816
|
+
without `oats-local.yaml`, or a failed or skipped probe). A host before
|
|
1817
|
+
0.36.0 answers the reachability-only object (`{reachable, code?, reason?,
|
|
1818
|
+
message?}`) with no identity fields, so the identity is there only when
|
|
1819
|
+
the object has a `key` field (which a 0.36.0 host always sends, `null` for
|
|
1820
|
+
an unusable reference). It is never derived on this side. It is on the group, not the
|
|
1821
|
+
rows, so an empty remote deployment still reports it.
|
|
1752
1822
|
- **Instance rows** relay the host's own `status --json` row: `identity`,
|
|
1753
1823
|
`identityAddress`, `teams`, `startedAt`, `createdAt`, `model`,
|
|
1754
1824
|
`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,
|
|
@@ -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.
|
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
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
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",
|