@awebai/oats 0.36.0 → 0.37.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 +21 -4
- package/docs/capabilities.md +53 -2
- package/docs/capability-manifest.schema.json +4 -0
- package/docs/configuration.md +12 -1
- package/docs/desktop-cli-api.md +37 -8
- package/docs/desktop.md +33 -0
- package/docs/integrations.md +2 -2
- package/docs/knowledge.md +1 -1
- package/docs/oats-workspace.schema.json +28 -0
- package/docs/official-catalog.md +4 -4
- package/docs/packages.md +14 -14
- package/docs/release-notes/v0.36.1.md +103 -0
- package/docs/release-notes/v0.37.0.md +111 -0
- package/docs/souls-and-instances.md +2 -2
- package/docs/workspaces.md +35 -2
- package/lib/core.mjs +123 -32
- package/lib/schedule.mjs +1 -1
- package/lib/teams.mjs +36 -3
- package/lib/workspace.mjs +14 -0
- package/package-catalog.json +3 -3
- 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}`);
|
package/docs/capabilities.md
CHANGED
|
@@ -341,7 +341,9 @@ Hooks receive:
|
|
|
341
341
|
A spawn hook also gets `OATS_TASK`, `OATS_REPO`, `OATS_BRANCH`, `OATS_WORK`,
|
|
342
342
|
`OATS_HARNESS`, `OATS_KIND` and, for a spawn a trigger started,
|
|
343
343
|
`OATS_TRIGGER_EVENT_FILE`. A launch hook also gets `OATS_HARNESS` and
|
|
344
|
-
`OATS_PREVIOUS_HARNESS
|
|
344
|
+
`OATS_PREVIOUS_HARNESS`, and `OATS_LAUNCH_PREVIEW=1` when it runs for a
|
|
345
|
+
preview (only a preview-aware hook does, below); on a real run
|
|
346
|
+
`OATS_LAUNCH_PREVIEW` is not set.
|
|
345
347
|
|
|
346
348
|
`OATS_SETTINGS_ORIGINS` says where each leaf of
|
|
347
349
|
`OATS_SETTINGS` came from: a JSON object from a JSON pointer to `{ kind, at }`,
|
|
@@ -350,7 +352,8 @@ A spawn hook also gets `OATS_TASK`, `OATS_REPO`, `OATS_BRANCH`, `OATS_WORK`,
|
|
|
350
352
|
`{"/harvest":{"kind":"soul","at":"soul.yaml#/knowledge"}}`. A provider tells a
|
|
351
353
|
soul-set value from a host-set one there, and never reads `soul.yaml` for it;
|
|
352
354
|
a home with none recorded gives `{}`. A final JSON line may return `meta`,
|
|
353
|
-
`brief`, `warning`, or harness-specific `launch` arguments
|
|
355
|
+
`brief`, `warning`, or harness-specific `launch` arguments; a preview-aware
|
|
356
|
+
launch hook's preview answer may add `volatileEnv` (below). A **spawn or
|
|
354
357
|
launch hook** may also return an `env` object for the launched process;
|
|
355
358
|
returning `env` from a retire hook is an explicit contract error.
|
|
356
359
|
|
|
@@ -364,6 +367,54 @@ start (a renewed session grant, for example) leaves the CURRENT one on record. A
|
|
|
364
367
|
launch hook that answers without `meta` keeps its previous entry; a start whose
|
|
365
368
|
preparation fails changes nothing.
|
|
366
369
|
|
|
370
|
+
A launch hook may do idempotent provider registration on a real start (an
|
|
371
|
+
aweb home registering with the host wake broker, for example). How the
|
|
372
|
+
kernel runs it depends on whether its capability declares **preview
|
|
373
|
+
awareness**: `"launchPreview": true` at the top level of its manifest. The
|
|
374
|
+
kernel reads the declaration from the home's own module copy, so a home keeps
|
|
375
|
+
the behaviour of the module it was spawned with.
|
|
376
|
+
|
|
377
|
+
**A preview-aware hook** must change nothing under `OATS_LAUNCH_PREVIEW=1`,
|
|
378
|
+
and must return the same contribution (`launch` arguments and `env`) as for
|
|
379
|
+
a real start. It runs twice per start:
|
|
380
|
+
|
|
381
|
+
1. **As a preview, under `OATS_LAUNCH_PREVIEW=1`.** The start's preflight uses
|
|
382
|
+
this contribution: trust, environment ownership, the harness-package
|
|
383
|
+
probe and the rendered command. `oats launch-config preview` (which
|
|
384
|
+
Desktop's start dialog uses) runs only this pass.
|
|
385
|
+
2. **For real, without the flag.** This pass runs only once preflight has
|
|
386
|
+
passed, including the check that the home is not already running
|
|
387
|
+
(`E_SESSION_RUNNING`), and before a restart stops the running harness.
|
|
388
|
+
|
|
389
|
+
The real run's `meta` and warnings are what the start records. If its
|
|
390
|
+
contribution differs from its preview contribution, the start is refused
|
|
391
|
+
with `E_LAUNCH_PREPARATION` and nothing is stopped or started.
|
|
392
|
+
|
|
393
|
+
Some values only a real run can know, such as a credential minted at start.
|
|
394
|
+
A preview answer may list those names in `volatileEnv` (for example
|
|
395
|
+
`"volatileEnv": ["AWEB_IDENTITY_HOME"]`, beside `env`). For those names:
|
|
396
|
+
|
|
397
|
+
- The start takes the values from the real run, records them, and renders
|
|
398
|
+
the launch command again with them.
|
|
399
|
+
- The comparison leaves those values out. Everything else must still be
|
|
400
|
+
identical.
|
|
401
|
+
|
|
402
|
+
Each name must be one the same hook returned in `env`. Preflight sees only
|
|
403
|
+
the preview's value, so a volatile name must not affect how the harness
|
|
404
|
+
resolves its packages. The kernel refuses (`E_LAUNCH_PREPARATION`) a volatile
|
|
405
|
+
`CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `PI_CODING_AGENT_DIR`. It reads
|
|
406
|
+
`volatileEnv` only from a preview answer and never records it.
|
|
407
|
+
|
|
408
|
+
**A hook that does not declare preview awareness** runs once per start, for
|
|
409
|
+
real, during preflight, before the checks that use its contribution. A
|
|
410
|
+
refused start may therefore already have run it. `oats launch-config preview`
|
|
411
|
+
never runs it. The preview shows that capability's recorded contribution, and
|
|
412
|
+
its `capabilities` check says the hook was not run.
|
|
413
|
+
|
|
414
|
+
`launchPreview` is a top-level key so that a kernel older than 0.37 ignores
|
|
415
|
+
it and runs the hook once, as it always did. A key inside the hook's
|
|
416
|
+
declaration would make such a kernel refuse the whole package.
|
|
417
|
+
|
|
367
418
|
Hook environment values are strings, at most 8192 UTF-8 bytes, with no NUL or
|
|
368
419
|
newlines. Names use the portable environment grammar and must belong to an
|
|
369
420
|
unambiguous vendor namespace. Only a dotted capability ID participates: its
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
"type": "boolean",
|
|
62
62
|
"description": "Workspace discovery: true makes this member capability repo-owned — listed (private: true) but usable only by souls of its own repository (E_CAPABILITY_PRIVATE elsewhere)."
|
|
63
63
|
},
|
|
64
|
+
"launchPreview": {
|
|
65
|
+
"type": "boolean",
|
|
66
|
+
"description": "true declares the launch hook preview-aware: it changes nothing under OATS_LAUNCH_PREVIEW=1 and returns the same contribution (apart from its preview answer's volatileEnv values), so a start runs it as a preview during preflight and for real after it, and a launch preview runs it. Without it the hook runs once per start, for real, during preflight, and never for a launch preview. Top-level so that older kernels ignore it."
|
|
67
|
+
},
|
|
64
68
|
"layer": {
|
|
65
69
|
"enum": [
|
|
66
70
|
"knowledge",
|
package/docs/configuration.md
CHANGED
|
@@ -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.38.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 and 0.37.x still apply all
|
|
87
|
+
four keys and warn 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`
|
|
@@ -236,7 +242,12 @@ recipe is resolved again against the home's recorded context and every check
|
|
|
236
242
|
runs first. With `--reselect-launch`, the launch preferences decide again
|
|
237
243
|
(the home's recorded soul and this deployment's `souls.launch`). A capability that contributed harness-specific arguments must
|
|
238
244
|
declare a `launch` hook to follow a harness change; otherwise the start is
|
|
239
|
-
refused (`E_LAUNCH_PREPARATION`).
|
|
245
|
+
refused (`E_LAUNCH_PREPARATION`). The checks use the preview run
|
|
246
|
+
(`OATS_LAUNCH_PREVIEW=1`) of the launch hooks whose capabilities declare
|
|
247
|
+
`launchPreview`. Those hooks run for real only after every check has passed,
|
|
248
|
+
so a start refused by preflight has run none of them for real. Other launch
|
|
249
|
+
hooks run once, for real, during the checks (see
|
|
250
|
+
[capabilities.md](capabilities.md)). A launch hook's warnings do not stop
|
|
240
251
|
the start: `session start|restart` print them (and answer them as
|
|
241
252
|
`warnings` under `--json`), as spawn does, and each is kept as a
|
|
242
253
|
`launch-warning` instance event (`oats instance events`).
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -725,7 +725,7 @@ Read-only (it writes no lock):
|
|
|
725
725
|
"souls":["rm"],"capabilities":["nw-house-style"],"publishes":null,"url":"https://github.com/nw/agents/tree/66566512…",
|
|
726
726
|
"membershipFile":{"path":"oats-membership.yaml","url":"https://github.com/nw/agents/blob/66566512…/oats-membership.yaml"}}],
|
|
727
727
|
"packages":[{"id":"oats.okf","version":"3.0.0","source":"catalog:oats.okf","commit":"ab897841…","integrity":"sha256-bada35…",
|
|
728
|
-
"capabilities":["oats.okf"],"souls":[],"latest":{"version":"4.1.
|
|
728
|
+
"capabilities":["oats.okf"],"souls":[],"latest":{"version":"4.1.1","ref":"v4.1.1"}}],
|
|
729
729
|
"declaredPackages":["oats.framework","oats.okf"],"unsynced":["oats.framework"],"stale":[],
|
|
730
730
|
"external":[{"source":"git:github.com/oss/experts@3c606e09…","soul":"security-reviewer"}],
|
|
731
731
|
"problems":[],"warnings":[],
|
|
@@ -784,8 +784,8 @@ packages' capabilities and souls, sorted by name, then origin. Both carry
|
|
|
784
784
|
"defaultTeam":{"label":"mine","team":"mine:ana.aweb.ai","from":"deployment"},
|
|
785
785
|
"private":false,"path":"souls/writer","work":"directory","description":"Drafts campaigns.","harness":"pi","model":null,"harnessFrom":"kernel-default",
|
|
786
786
|
"file":{"path":"souls/writer/soul.yaml","url":null},"spawnable":true,"problem":null},
|
|
787
|
-
{"name":"knowledge-maintainer","qualifiedName":"oats.okf/knowledge-maintainer","origin":"package oats.okf v4.1.
|
|
788
|
-
"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,
|
|
789
789
|
"path":"oats-package/souls/knowledge-maintainer","work":"directory","description":"Reviews harvested knowledge.","harness":"pi","model":null,
|
|
790
790
|
"harnessFrom":"kernel-default","file":{"path":"oats-package/souls/knowledge-maintainer/soul.yaml","url":null},
|
|
791
791
|
"spawnable":false,"problem":{"code":"E_TEAM_UNKNOWN","message":"team \"reviewers\" is not declared (oats-local.yaml#/souls/teams/…)"}}],
|
|
@@ -874,8 +874,8 @@ nothing reads a working clone.
|
|
|
874
874
|
**The show:**
|
|
875
875
|
|
|
876
876
|
```json
|
|
877
|
-
{"capabilityShowApi":1,"name":"oats.okf","kind":"package","repoKey":"github.com/awebai/oats-okf","package":"oats.okf","version":"4.1.
|
|
878
|
-
"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",
|
|
879
879
|
"inject":{"path":"injects/okf.md","bytes":2422,"text":"## Knowledge: OKF\n\nYou have two kinds of knowledge. …","binary":false,"truncated":false},
|
|
880
880
|
"skills":[{"name":"okf-consultation","path":"skills/okf-consultation","description":"Consulting your soul's knowledge with the `oats okf` CLI: …",
|
|
881
881
|
"files":[{"path":"skills/okf-consultation/SKILL.md","bytes":6947},{"path":"skills/okf-consultation/references/consult.md","bytes":4465}],
|
|
@@ -901,7 +901,7 @@ nothing reads a working clone.
|
|
|
901
901
|
**The `--file` answer:**
|
|
902
902
|
|
|
903
903
|
```json
|
|
904
|
-
{"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"
|
|
904
|
+
{"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"e1d604f7…",
|
|
905
905
|
"file":{"path":"skills/okf-instance-knowledge/SKILL.md","bytes":4787,"text":"---\nname: okf-instance-knowledge\n…","binary":false,"truncated":false}}
|
|
906
906
|
```
|
|
907
907
|
|
|
@@ -1229,9 +1229,38 @@ for a failure and `false` for a warning, plus the problem's own keys.
|
|
|
1229
1229
|
| `default-team-changed` | warning | `recorded`, `current` | `--home` with live teams: the default changed since the spawn |
|
|
1230
1230
|
| `E_TEAM_UNKNOWN` | failure | `label`, `at` | a reference to an undeclared label |
|
|
1231
1231
|
| `E_TEAM_NOT_ELIGIBLE` | failure | `soul`, `label`, `at` | `souls.default` outside the soul's teams |
|
|
1232
|
+
| `team-model-3-migration` | warning | `condition`, `keys` | 0.36.x and 0.37.x: what OATS 0.38.0 (team model 3) refuses, one item per condition (below) |
|
|
1232
1233
|
|
|
1233
|
-
The
|
|
1234
|
-
details.
|
|
1234
|
+
The `E_TEAM_UNKNOWN` and `E_TEAM_NOT_ELIGIBLE` codes are also spawn, preview and
|
|
1235
|
+
inspect refusals, with the same details.
|
|
1236
|
+
|
|
1237
|
+
`team-model-3-migration` is a deployment fact, so every soul's readiness
|
|
1238
|
+
carries it, and `oats teams` lists it once. Its `condition`:
|
|
1239
|
+
|
|
1240
|
+
- `local-soul-teams`: `oats-local.yaml` has `souls.teams` and/or
|
|
1241
|
+
`souls.default` (`keys`: `["souls.teams", "souls.default"]` as found). They
|
|
1242
|
+
move to `souls:` in `oats-workspace.yaml`.
|
|
1243
|
+
- `local-teams-closed`: `oats-local.yaml` declares `teams` and/or
|
|
1244
|
+
`defaultTeam` (`keys`: `["teams", "defaultTeam"]` as found) and the
|
|
1245
|
+
workspace file does not say `localTeams: true`. The `fix` names both
|
|
1246
|
+
remedies: add `localTeams: true` to the workspace file, or commit the teams
|
|
1247
|
+
and `defaultTeam` there and remove them locally. Never raised in the
|
|
1248
|
+
standalone view, which has no workspace rules.
|
|
1249
|
+
|
|
1250
|
+
```json
|
|
1251
|
+
{"code":"team-model-3-migration","severity":"warning","condition":"local-teams-closed","keys":["teams","defaultTeam"],
|
|
1252
|
+
"message":"oats-local.yaml declares teams, defaultTeam, but oats-workspace.yaml does not say localTeams: true: OATS 0.38.0 refuses local teams and a local defaultTeam unless the workspace allows them",
|
|
1253
|
+
"fix":"either (a) add `localTeams: true` to oats-workspace.yaml, or (b) commit the teams and defaultTeam in oats-workspace.yaml, then remove them from oats-local.yaml"}
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
As a readiness item it is `{subject: "teams", status: "fail", required: false,
|
|
1257
|
+
producer: "team model", code, reason: <message>, remedy: <fix>, condition,
|
|
1258
|
+
keys}`. `oats doctor --json` lists the same problems under `problems[]`. Doctor
|
|
1259
|
+
stays offline: for `local-teams-closed` it reads only the workspace file this
|
|
1260
|
+
machine's parsed cache holds; when there is none, it adds no problem and says
|
|
1261
|
+
so in `information[]`: `"team-model-3-migration: whether oats-local.yaml
|
|
1262
|
+
teams/defaultTeam need localTeams: true couldn't be checked: this deployment
|
|
1263
|
+
hasn't observed its workspace yet; run oats sync"`.
|
|
1235
1264
|
|
|
1236
1265
|
<a id="soul-launch-preferences-feature-launch-preference-oats-0300"></a>
|
|
1237
1266
|
## Launch preferences
|
package/docs/desktop.md
CHANGED
|
@@ -104,6 +104,39 @@ 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 window per workspace
|
|
108
|
+
|
|
109
|
+
Each workspace has at most one window, titled with the workspace's name (a
|
|
110
|
+
workspace on a server: `name — server`).
|
|
111
|
+
|
|
112
|
+
- **Switching.** Choosing a workspace in the switcher shows it in the current
|
|
113
|
+
window. If that workspace already has a window, that window comes to the
|
|
114
|
+
front instead and the current one doesn't change.
|
|
115
|
+
- **Open in new window.** Each workspace in the switcher has an **Open in new
|
|
116
|
+
window** button beside it. From the keyboard: Right Arrow on the workspace,
|
|
117
|
+
then Enter, or ⌘Enter on macOS / Ctrl+Enter on Linux and Windows. A
|
|
118
|
+
workspace that already has a window is brought to the front.
|
|
119
|
+
- **New Window.** On macOS, **File → New Window** (⌘⇧N); everywhere,
|
|
120
|
+
**Window: new window** in the command palette. The new window has no
|
|
121
|
+
workspace yet: it opens the switcher, and shows "Choose a workspace" until
|
|
122
|
+
you pick one. It reads nothing until then.
|
|
123
|
+
- **Moving between windows.** On macOS, ⌘\` cycles the app's windows, and the
|
|
124
|
+
**Window** menu lists them.
|
|
125
|
+
- **Restore.** Quitting and relaunching brings every window back with its
|
|
126
|
+
size and place (moved onto a visible display if its own is gone). A window
|
|
127
|
+
whose workspace isn't served at launch doesn't come back, but it is
|
|
128
|
+
remembered until you close it while its workspace is served. With nothing
|
|
129
|
+
to restore, one window opens on the last workspace you used.
|
|
130
|
+
- **Launching again.** Running the app again (`open -a "OATS Desktop" --args
|
|
131
|
+
--dir <deployment>`, or from inside a deployment) adds that deployment if
|
|
132
|
+
needed and brings its window to the front, opening one if it has none. Any
|
|
133
|
+
other launch brings the most recently used window to the front.
|
|
134
|
+
- **Closing.** Closing a window leaves the other windows, and their
|
|
135
|
+
terminals, running. Closing the last window quits the app.
|
|
136
|
+
- Tabs belong to their window: a workspace's terminals and tabs stay in the
|
|
137
|
+
window they were opened in, and switching that window back to the
|
|
138
|
+
workspace brings them back.
|
|
139
|
+
|
|
107
140
|
## One workspace, several machines
|
|
108
141
|
|
|
109
142
|
The switcher lists each workspace once, however many deployments it has: the
|
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.38.0): the workspace's fallback default team, a label of teams: in this file. 0.36.x and 0.37.x accept and validate it without applying it."
|
|
33
|
+
},
|
|
34
|
+
"localTeams": {
|
|
35
|
+
"type": "boolean",
|
|
36
|
+
"description": "Team model 3 (0.38.0): whether deployments may declare their own teams and defaultTeam in oats-local.yaml. Absent: false. 0.36.x and 0.37.x accept 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.38.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 and 0.37.x accept and validate 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,9 +9,9 @@ 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.
|
|
14
|
-
| `oats.aweb` | `v1.
|
|
12
|
+
| `oats.framework` | `oats-framework/v1.4.3` (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
|
+
| `oats.aweb` | `v1.18.1` | `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` | |
|
|
17
17
|
| `oats.jira` | `v1.0.1` | `oats.jira` (tasks) | |
|
|
@@ -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.3`) 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,9 +74,9 @@ 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.
|
|
79
|
-
oats.aweb: v1.
|
|
77
|
+
oats.framework: v1.4.3
|
|
78
|
+
oats.okf: v4.1.1
|
|
79
|
+
oats.aweb: v1.18.1
|
|
80
80
|
teams:
|
|
81
81
|
platform: { team: "platform:acme.aweb.ai", description: Platform engineering }
|
|
82
82
|
defaults:
|
|
@@ -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
|
|
@@ -134,7 +134,7 @@ Declaring a package in the workspace's `packages:` is the trust decision
|
|
|
134
134
|
## `oats package add | remove`
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
-
oats package add oats.aweb v1.
|
|
137
|
+
oats package add oats.aweb v1.18.1 # a catalog version
|
|
138
138
|
oats package add acme.tools git:github.com/acme/tools@v0.4.0
|
|
139
139
|
oats package remove acme.tools
|
|
140
140
|
```
|
|
@@ -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.3", "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.3`
|
|
341
|
+
resolves to tag `oats-framework/v1.4.3`. Resolving through the catalog never
|
|
342
342
|
advances a lock by itself: `oats sync` does, and says so.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# OATS 0.36.1
|
|
2
|
+
|
|
3
|
+
## Changed
|
|
4
|
+
|
|
5
|
+
- **oats.okf 4.1.1** (catalog and workspace pin, and the bundled mirrors):
|
|
6
|
+
three fixes to harvest completion and `harvest --once`.
|
|
7
|
+
- `oats okf complete` on an amended PR that is still open
|
|
8
|
+
(awebai/oats-okf#36). After the knowledge-maintainer pushed an amendment
|
|
9
|
+
on top of the delivered commit, `complete --run <id>` failed with `E_PR:
|
|
10
|
+
publication branch has unexpected commit; never force push`. The run is
|
|
11
|
+
now reported `delivered` when the known PR is open at the branch's tip
|
|
12
|
+
and Git shows that tip descends from the delivered commit; the answer
|
|
13
|
+
names the amended head. A tip that does not contain the delivered commit
|
|
14
|
+
is still refused with `E_PR`, and nothing is ever force-pushed. Ancestry
|
|
15
|
+
is read from the commit objects alone, so a checkout's grafts or
|
|
16
|
+
commit-graph file cannot fake it.
|
|
17
|
+
- `harvest --once`: one-shots of a seat no longer race
|
|
18
|
+
(awebai/oats-okf#39). The overlap checks and the install run under one
|
|
19
|
+
seat lock, so of two overlapping one-shots started together one installs
|
|
20
|
+
and the other gets `E_ONCE_OVERLAP`.
|
|
21
|
+
- `harvest --once`: a note edited between runs no longer strands a
|
|
22
|
+
draining one-shot (awebai/oats-okf#40). A rerun with the same manifest
|
|
23
|
+
continues from custody without reading the listed notes. Another
|
|
24
|
+
manifest listing a note a one-shot already holds is refused with
|
|
25
|
+
`E_ONCE_OVERLAP`, naming that one-shot, instead of a `sha256 mismatch`.
|
|
26
|
+
- **Preparing for team model 3** (awebai/oats#485). OATS 0.37.0 commits the
|
|
27
|
+
teams an organisation's souls may join, and their default team, in the
|
|
28
|
+
workspace file, closed by default (awebai/oats#484), so the organisation's
|
|
29
|
+
intended team set is visible and reviewable in its git and a deployment's
|
|
30
|
+
`oats-local.yaml` no longer adds a team by accident. 0.36.1 lets every workspace and
|
|
31
|
+
deployment migrate before 0.37.0 refuses the old shape:
|
|
32
|
+
- `oats-workspace.yaml` accepts `defaultTeam`, `localTeams` and `souls:`,
|
|
33
|
+
and validates them: every label they name must be a shared team in
|
|
34
|
+
`teams:` of the same file (`E_WORKSPACE_SCHEMA` otherwise, when the file
|
|
35
|
+
is read). 0.36.1 does **not** apply them: a soul's teams and default are
|
|
36
|
+
still resolved from `oats-local.yaml`. Earlier releases refuse these keys,
|
|
37
|
+
so add them only once everyone who reads the workspace runs 0.36.1 or
|
|
38
|
+
later.
|
|
39
|
+
- A new readiness warning, `team-model-3-migration` (never blocking), names
|
|
40
|
+
what 0.37.0 will refuse. `oats teams`, `oats readiness` (and so the
|
|
41
|
+
Desktop's readiness view) and `oats doctor` show it. Its `condition` is
|
|
42
|
+
`local-soul-teams` when `oats-local.yaml` has `souls.teams` or
|
|
43
|
+
`souls.default`, and `local-teams-closed` when `oats-local.yaml` declares
|
|
44
|
+
`teams` or `defaultTeam` and the workspace file does not say
|
|
45
|
+
`localTeams: true` (never in the standalone view). `oats doctor` stays
|
|
46
|
+
offline: it checks `local-teams-closed` against the workspace file this
|
|
47
|
+
machine last observed, and says so when there is none yet (`oats sync`
|
|
48
|
+
fixes that). The shapes are in docs/desktop-cli-api.md, under Team
|
|
49
|
+
readiness items.
|
|
50
|
+
- **oats.framework 1.4.2** (oats.setup 2.2.1; catalog and workspace pin
|
|
51
|
+
`oats-framework/v1.4.2`) ships the oats.setup skill changes made since
|
|
52
|
+
1.4.1, which deployments had not received:
|
|
53
|
+
- `oats-onboarding`: declare how this machine starts a harness once, as
|
|
54
|
+
that harness's default launch configuration (0.32.0, awebai/oats#368);
|
|
55
|
+
`oats spawn --preview` names the launch configuration that applies.
|
|
56
|
+
- `oats-workspace-config`: a soul's launch preference versus a host launch
|
|
57
|
+
configuration, `E_LAUNCH_CONFIG_INVALID` and `E_CLAUDE_CONFIG_REMOVED`
|
|
58
|
+
(awebai/oats#368), and the team model 3 workspace keys `defaultTeam`,
|
|
59
|
+
`localTeams` and `souls:` (awebai/oats#495).
|
|
60
|
+
- `oats-teams`: preparing for team model 3, and what each
|
|
61
|
+
`team-model-3-migration` condition asks for (awebai/oats#495).
|
|
62
|
+
- `oats-package-pins`: the example pins `oats.okf: v4.1.1`
|
|
63
|
+
(awebai/oats#494).
|
|
64
|
+
|
|
65
|
+
## What 0.37.0 will change
|
|
66
|
+
|
|
67
|
+
- **`souls:` in `oats-workspace.yaml` decides which teams a soul may join**
|
|
68
|
+
besides its default: the most specific key wins (`<member|package>/<soul>`,
|
|
69
|
+
then `<member|package>/*`, then `"*"`); `teams` is a list of shared labels
|
|
70
|
+
or `any`. A soul no key matches joins its default only. This applies to
|
|
71
|
+
package souls too: a workspace opens a package explicitly.
|
|
72
|
+
- **A soul's default team**, in order: its `souls:` `default`; else the
|
|
73
|
+
deployment's `defaultTeam`, only when the workspace says
|
|
74
|
+
`localTeams: true`; else the workspace's `defaultTeam`.
|
|
75
|
+
- **`oats-local.yaml` `souls.teams` and `souls.default` are refused**
|
|
76
|
+
(`E_WORKSPACE_SCHEMA`, reason `removed-key`), and so are `oats soul teams
|
|
77
|
+
--add/--remove/--default/--clear-default`.
|
|
78
|
+
- **Local `teams` and `defaultTeam` are refused unless the workspace says
|
|
79
|
+
`localTeams: true`**, and so are `oats teams add/remove/default`. The
|
|
80
|
+
standalone view still allows them.
|
|
81
|
+
|
|
82
|
+
## Upgrade
|
|
83
|
+
|
|
84
|
+
Nothing is required for 0.36.1 itself. To be ready for 0.37.0
|
|
85
|
+
(`oats teams` lists each deployment's `team-model-3-migration` warnings):
|
|
86
|
+
|
|
87
|
+
1. **Now**, once everyone who reads the workspace runs 0.36.1 or later,
|
|
88
|
+
commit in `oats-workspace.yaml` the choices each deployment makes locally:
|
|
89
|
+
- a `souls:` entry for what `souls.teams` says (`"*": { teams: [...] }`
|
|
90
|
+
for every soul, `<member|package>/<soul>: { teams: [...] }` for one;
|
|
91
|
+
local keys are bare soul names, workspace keys are qualified by the
|
|
92
|
+
member or package, as in `souls.disabled`), with a `default:` for what
|
|
93
|
+
`souls.default` says;
|
|
94
|
+
- for local teams and a local `defaultTeam`, choose one: (a) keep them
|
|
95
|
+
personal, and add `localTeams: true`, which clears `local-teams-closed`
|
|
96
|
+
at once; or (b) commit the teams in `teams:` and the default as
|
|
97
|
+
`defaultTeam:`.
|
|
98
|
+
2. **When the deployment moves to 0.37.0**, remove from its `oats-local.yaml`
|
|
99
|
+
`souls.teams`, `souls.default` and, under (b), the local `teams` and
|
|
100
|
+
`defaultTeam`. Not before: 0.36.1 does not apply the workspace keys, so
|
|
101
|
+
those local keys are still what decides a soul's teams and default until
|
|
102
|
+
then, and their warnings stay until they go. 0.37.0 refuses them, naming
|
|
103
|
+
the replacement.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# OATS 0.37.0
|
|
2
|
+
|
|
3
|
+
## Added
|
|
4
|
+
|
|
5
|
+
- **The Desktop opens one window per workspace** (awebai/oats#481). Each
|
|
6
|
+
workspace has at most one window, titled with its name (a workspace on a
|
|
7
|
+
server: `name — server`). Choosing a workspace in the switcher shows it in the
|
|
8
|
+
current window, or brings its own window to the front when it has one.
|
|
9
|
+
Each workspace in the switcher has an **Open in new window** button
|
|
10
|
+
(keyboard: Right Arrow then Enter, or ⌘Enter / Ctrl+Enter). On macOS,
|
|
11
|
+
**File → New Window** (⌘⇧N) opens a window with no workspace that asks you
|
|
12
|
+
to choose one, and ⌘\` cycles the windows; on Linux and Windows the command
|
|
13
|
+
palette has **Window: new window**, with no default chord. Quitting and
|
|
14
|
+
relaunching brings every window back with its size and place; a window
|
|
15
|
+
whose workspace isn't served at launch stays remembered until you close it
|
|
16
|
+
while it is served. Launching the app again with `--dir <deployment>`
|
|
17
|
+
brings that deployment's window to the front, adding the deployment if
|
|
18
|
+
needed. An unfocused window re-reads its instance list at the server's
|
|
19
|
+
blurred cadence (30 s). A window's requests always carry its own
|
|
20
|
+
workspace: one the server doesn't serve is refused with
|
|
21
|
+
`E_WORKSPACE_NOT_SERVED`, never answered with another workspace's data.
|
|
22
|
+
Window positions are kept in `windows.json` in the app's data folder.
|
|
23
|
+
|
|
24
|
+
## Changed
|
|
25
|
+
|
|
26
|
+
- **The Desktop accepts OATS CLIs `>=0.25.8 <0.38.0`**, so it runs against
|
|
27
|
+
this release's kernel. Install the CLI and the Desktop 0.37.0 together: the
|
|
28
|
+
Desktop 0.36.x refuses a 0.37 CLI.
|
|
29
|
+
|
|
30
|
+
- **Team model 3 is OATS 0.38.0, not 0.37.0.** 0.37.0 keeps 0.36.1's
|
|
31
|
+
behaviour: the new `oats-workspace.yaml` keys are validated but not applied,
|
|
32
|
+
and the `team-model-3-migration` warning, which now names 0.38.0, says what
|
|
33
|
+
0.38.0 will refuse. The migration steps in the 0.36.1 notes apply unchanged.
|
|
34
|
+
|
|
35
|
+
- **oats.framework 1.4.3** (oats.setup 2.2.2, catalog and workspace pin
|
|
36
|
+
`oats-framework/v1.4.3`): the `oats-teams` and
|
|
37
|
+
`oats-workspace-config` skills say team model 3 is 0.38.0, and that 0.36.x
|
|
38
|
+
and 0.37.x keep applying the local team keys. Remove them only when the
|
|
39
|
+
deployment moves to 0.38.0.
|
|
40
|
+
|
|
41
|
+
- **oats.aweb 1.18.1** (catalog and workspace pin, and the bundled mirror):
|
|
42
|
+
how mail reaches a session now depends on the runtime. Under the default
|
|
43
|
+
`delivery: channel`, Claude Code takes mail and chat through its
|
|
44
|
+
`aweb-channel` plugin and pi through the `@awebai/pi` extension, and
|
|
45
|
+
neither goes through the host wake broker. Codex keeps the wake broker. A
|
|
46
|
+
Codex home under `channel` is now registered with it at every start, where
|
|
47
|
+
before it got no wake at all. Every start of a home re-decides the path for
|
|
48
|
+
that start's runtime and leaves the home on exactly one path. A start that
|
|
49
|
+
cannot register or deregister the home is refused. Existing instances keep
|
|
50
|
+
the delivery they were spawned with until they are respawned.
|
|
51
|
+
`delivery: session` still sends every runtime through the broker.
|
|
52
|
+
|
|
53
|
+
oats.aweb 1.18.1 declares `launchPreview` (see Fixed below). A launch
|
|
54
|
+
preview, including Desktop's start dialog, and a start refused by
|
|
55
|
+
preflight change no broker registration: the hook calls `aw` only in the
|
|
56
|
+
real run, after preflight has passed.
|
|
57
|
+
|
|
58
|
+
## Fixed
|
|
59
|
+
|
|
60
|
+
- **Launch hooks no longer have side effects under a preview, or before a
|
|
61
|
+
start's preflight has passed** (awebai/oats#500). A launch hook may register
|
|
62
|
+
a home with its provider; oats.aweb's registers it with the host wake
|
|
63
|
+
broker. But `oats launch-config preview`, which Desktop's start dialog
|
|
64
|
+
calls for running homes, ran every hook for real. A start refused by
|
|
65
|
+
preflight, including a start of a home that was already running, had also
|
|
66
|
+
run them. Previewing a running Claude home with another harness could
|
|
67
|
+
therefore change how that live session is woken.
|
|
68
|
+
|
|
69
|
+
A capability now declares that its launch hook is preview-aware, with
|
|
70
|
+
`"launchPreview": true` at the top level of its manifest. The kernel reads
|
|
71
|
+
the declaration from the home's own module copy. A preview-aware hook
|
|
72
|
+
runs twice per start:
|
|
73
|
+
- **As a preview,** with `OATS_LAUNCH_PREVIEW=1` in the hook environment
|
|
74
|
+
(a new, additive hook environment variable). This run's contribution
|
|
75
|
+
drives the preflight and the rendered command.
|
|
76
|
+
- **For real, without the flag,** only once every check has passed, and
|
|
77
|
+
before a restart stops the running harness.
|
|
78
|
+
|
|
79
|
+
A preview runs only the first pass. A preview-aware hook must change
|
|
80
|
+
nothing under the flag. It must also return the same contribution either
|
|
81
|
+
way, apart from the values of the env names its preview answer lists in
|
|
82
|
+
`volatileEnv` (below). If the real run's contribution differs otherwise,
|
|
83
|
+
the start is refused with `E_LAUNCH_PREPARATION` and nothing is stopped or
|
|
84
|
+
started.
|
|
85
|
+
|
|
86
|
+
A hook that doesn't declare `launchPreview` runs as it did in 0.36: once
|
|
87
|
+
per start, for real, during preflight. `oats launch-config preview` never
|
|
88
|
+
runs it; the preview shows its recorded contribution and says so. The
|
|
89
|
+
kernel never passes on an `OATS_LAUNCH_PREVIEW` inherited from the
|
|
90
|
+
environment.
|
|
91
|
+
|
|
92
|
+
**Provider authors:** declare `launchPreview` once your launch hook
|
|
93
|
+
honours `OATS_LAUNCH_PREVIEW`, and expect it to run twice per start
|
|
94
|
+
(docs/capabilities.md). oats.aweb 1.18.1 does. A kernel before 0.37
|
|
95
|
+
ignores the key and runs the hook once, as before.
|
|
96
|
+
- **Existing homes whose launch hook renews a credential at every start
|
|
97
|
+
start again** (awebai/oats#504). The first version of the two-pass rule
|
|
98
|
+
above ran every launch hook twice. A hook from before the preview flag
|
|
99
|
+
then really ran under the preview too, and a hook that answers new values
|
|
100
|
+
at every call failed the comparison. That covers oats.aweb homes with
|
|
101
|
+
`identity.mode: global` and `renew: launch`, which mint a grant at each
|
|
102
|
+
start: their starts were refused with `E_LAUNCH_PREPARATION`. Two passes
|
|
103
|
+
are now opt-in, through `launchPreview`.
|
|
104
|
+
|
|
105
|
+
A preview-aware hook can also name, in its preview answer, env whose
|
|
106
|
+
values only the real run can know, such as `"volatileEnv":
|
|
107
|
+
["AWEB_IDENTITY_HOME"]`. The start takes those values from the real run
|
|
108
|
+
and leaves them out of the comparison. Each must be a name the same hook
|
|
109
|
+
returned. A harness configuration selector (`CLAUDE_CONFIG_DIR`,
|
|
110
|
+
`CODEX_HOME`, `PI_CODING_AGENT_DIR`) is refused as volatile, because the
|
|
111
|
+
package probe reads it before the real run.
|
|
@@ -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.3 # 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.38.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 and 0.37.x prepare 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.38.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/core.mjs
CHANGED
|
@@ -1574,8 +1574,12 @@ export function teamEnv(resolved) {
|
|
|
1574
1574
|
OATS_WORKSPACE_NAME: typeof ws.name === "string" ? ws.name : "", OATS_WORKSPACE_KEY: typeof ws.key === "string" ? ws.key : "",
|
|
1575
1575
|
};
|
|
1576
1576
|
}
|
|
1577
|
+
function withoutAmbientLaunchPreview(env) {
|
|
1578
|
+
const { OATS_LAUNCH_PREVIEW: _ambient, ...rest } = env;
|
|
1579
|
+
return rest;
|
|
1580
|
+
}
|
|
1577
1581
|
export function runLifecycleHooks(event, { home, instance, agentName, soulDir, soulId, contextDir, workspaceDir, rootDir, resolved, priorMeta = {}, extraEnv = {}, assertRoots }) {
|
|
1578
|
-
const results = { meta: {}, briefs: [], warnings: [], order: [], launch: {}, env: {}, failures: [], contributions: [] };
|
|
1582
|
+
const results = { meta: {}, briefs: [], warnings: [], order: [], launch: {}, env: {}, failures: [], contributions: [], volatileEnv: {} };
|
|
1579
1583
|
// OATS_SOUL is set for EVERY hook: a caller that does not name the soul (launch,
|
|
1580
1584
|
// retire) gets the directory the home's spawn recorded.
|
|
1581
1585
|
if (!soulDir && home) soulDir = instanceSoulDir(home);
|
|
@@ -1596,7 +1600,9 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, s
|
|
|
1596
1600
|
const stdout = execSync(cmd, {
|
|
1597
1601
|
cwd: home, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 120000,
|
|
1598
1602
|
env: {
|
|
1599
|
-
|
|
1603
|
+
// OATS_LAUNCH_PREVIEW is the kernel's to give (prepareLaunchHooks), never
|
|
1604
|
+
// inherited: a real start's hook must not read an ambient one as a preview.
|
|
1605
|
+
...withoutAmbientLaunchPreview(process.env),
|
|
1600
1606
|
// OATS_INSTANCE_HOME is the runtime-neutral contract name for the
|
|
1601
1607
|
// instance home (absolute). OATS_HOME predates it and stays as a
|
|
1602
1608
|
// compatibility alias: shipped capability hooks read it
|
|
@@ -1624,6 +1630,8 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, s
|
|
|
1624
1630
|
let o = {};
|
|
1625
1631
|
try { o = JSON.parse(lastLine); } catch { /* non-JSON hook output is fine */ }
|
|
1626
1632
|
if (o.meta) results.meta[cap.id] = o.meta;
|
|
1633
|
+
// A launch hook's answer as given; prepareLaunchHooks validates it.
|
|
1634
|
+
if (event === "launch" && o.volatileEnv !== undefined) results.volatileEnv[cap.id] = o.volatileEnv;
|
|
1627
1635
|
if (o.brief) results.briefs.push(`- ${o.brief}`);
|
|
1628
1636
|
if (o.warning) results.warnings.push(o.warning);
|
|
1629
1637
|
if (o.launch && typeof o.launch === "object") for (const [rt, args] of Object.entries(o.launch)) results.launch[rt] = `${results.launch[rt] ? `${results.launch[rt]} ` : ""}${args}`;
|
|
@@ -2468,7 +2476,10 @@ function requirementsWithArgsMessage(harness, providers, config) {
|
|
|
2468
2476
|
/** ONE planner for what a start would run, used by preview and by starts of
|
|
2469
2477
|
* existing homes alike: the recorded recipe (or, under a selection, the
|
|
2470
2478
|
* current scoped configuration) resolved, preflighted, rendered. `preview`
|
|
2471
|
-
* collects every failed check into `preflight` instead of throwing.
|
|
2479
|
+
* collects every failed check into `preflight` instead of throwing. Launch
|
|
2480
|
+
* hooks of capabilities declaring `launchPreview` run here only as a preview
|
|
2481
|
+
* (OATS_LAUNCH_PREVIEW=1); the others run here for real when a start plans,
|
|
2482
|
+
* and not at all for a preview. A home
|
|
2472
2483
|
* that records no launch recipe (an earlier kernel spawned it) is not planned:
|
|
2473
2484
|
* E_LAUNCH_LEGACY, re-spawn it from the deployment. */
|
|
2474
2485
|
export function planLaunch({ home, instance, meta, contextDir, agentLike, selection = {}, launchConfigs, resolvedCfg, env = process.env, preview = false, assertRoots, reselect = null }) {
|
|
@@ -2514,14 +2525,17 @@ export function planLaunch({ home, instance, meta, contextDir, agentLike, select
|
|
|
2514
2525
|
// Recorded contributions, refreshed by capabilities that declare a
|
|
2515
2526
|
// launch hook; a harness change needs the new harness's arguments from
|
|
2516
2527
|
// every capability that gave harness-specific ones; recorded arguments
|
|
2517
|
-
// of a capability the scope no longer trusts are not reused.
|
|
2528
|
+
// of a capability the scope no longer trusts are not reused. Hooks that
|
|
2529
|
+
// declare launchPreview run here under OATS_LAUNCH_PREVIEW=1, for a start
|
|
2530
|
+
// too (it runs them for real once its preflight passed); the others run
|
|
2531
|
+
// here for real on a start, and not at all for a preview.
|
|
2518
2532
|
try {
|
|
2519
|
-
hooks = prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, contextDir, assertRoots });
|
|
2533
|
+
hooks = prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, contextDir, assertRoots, pass: preview ? "preview" : "plan" });
|
|
2520
2534
|
const current = new Map((resolvedCfg?.capabilities || []).map((c) => [c.id, c]));
|
|
2521
2535
|
const untrusted = hooks.contributions.filter((c) => c.capability && current.has(c.capability) && !current.get(c.capability).trust?.trusted).map((c) => c.capability);
|
|
2522
2536
|
const inactive = hooks.contributions.filter((c) => c.capability && !current.has(c.capability)).map((c) => c.capability);
|
|
2523
2537
|
if (untrusted.length) fail("capabilities", "E_LAUNCH_PREPARATION", `${untrusted.join(", ")} contributed to this launch at spawn but is no longer trusted in the scope; respawn the instance; nothing was stopped`);
|
|
2524
|
-
else problems.push({ check: "capabilities", ok: true, detail: `${harness !== frozen.harness ? "prepared for the new harness" : "recorded contributions reused"}${hooks.refreshed?.length ? `; refreshed by launch hooks: ${hooks.refreshed.join(", ")}` : ""}${inactive.length ? `; no longer active in the scope, recorded contribution kept: ${inactive.join(", ")}` : ""}` });
|
|
2538
|
+
else problems.push({ check: "capabilities", ok: true, detail: `${harness !== frozen.harness ? "prepared for the new harness" : "recorded contributions reused"}${hooks.refreshed?.length ? `; refreshed by launch hooks: ${hooks.refreshed.join(", ")}` : ""}${hooks.notRun?.length ? `; recorded contribution shown, launch hook not run for a preview (not preview-aware): ${hooks.notRun.join(", ")}` : ""}${inactive.length ? `; no longer active in the scope, recorded contribution kept: ${inactive.join(", ")}` : ""}` });
|
|
2525
2539
|
} catch (e) {
|
|
2526
2540
|
if (e.code !== "E_LAUNCH_PREPARATION") throw e;
|
|
2527
2541
|
fail("capabilities", e.code, e.message);
|
|
@@ -2574,7 +2588,7 @@ export function planLaunch({ home, instance, meta, contextDir, agentLike, select
|
|
|
2574
2588
|
const { trustHome } = launchFolderTrust({ harness, home, meta, yolo: recipe.yolo, env });
|
|
2575
2589
|
const command = renderLaunchRecipe(recipe, { home, instance: inst, trustHome });
|
|
2576
2590
|
const selectionSource = frozen ? (config?.frozen || (!config && !selection.launchConfig && !selection.harness) ? "frozen" : "config") : "config";
|
|
2577
|
-
return { recipe, command, trustHome, harness, model: model || undefined, modelSource, yolo, config, executable, preflight: problems, ok: problems.every((c) => c.ok), selectionSource, frozen, launchChoice, warnings: hooks.warnings || [], ...(hooks.meta ? { hookMeta: hooks.meta } : {}) };
|
|
2591
|
+
return { recipe, command, trustHome, harness, model: model || undefined, modelSource, yolo, config, executable, preflight: problems, ok: problems.every((c) => c.ok), selectionSource, frozen, launchChoice, warnings: hooks.warnings || [], previewedHooks: hooks.previewed || [], volatileEnv: hooks.volatileEnv || [], ...(hooks.meta ? { hookMeta: hooks.meta } : {}) };
|
|
2578
2592
|
}
|
|
2579
2593
|
/** The environment a planned launch runs under: the host's base, the
|
|
2580
2594
|
* capabilities' validated env, the configuration's literals and its
|
|
@@ -4863,10 +4877,24 @@ export function capturedProviders(meta, frozen) {
|
|
|
4863
4877
|
}
|
|
4864
4878
|
/** Capability contributions for a start of an existing home: the recorded
|
|
4865
4879
|
* ones, refreshed by any capability that declares a `launch` hook (asked
|
|
4866
|
-
* for the target harness;
|
|
4867
|
-
*
|
|
4868
|
-
*
|
|
4869
|
-
|
|
4880
|
+
* for the target harness; spawn hooks are never re-run). A harness change
|
|
4881
|
+
* needs the new harness's launch arguments from every capability that
|
|
4882
|
+
* contributed harness-specific ones.
|
|
4883
|
+
* The hook contract: a launch hook may do idempotent provider registration
|
|
4884
|
+
* on a real start. A capability whose manifest (the home's module copy)
|
|
4885
|
+
* declares `launchPreview: true` promises that its hook changes nothing
|
|
4886
|
+
* under OATS_LAUNCH_PREVIEW=1 and returns the same contribution (launch
|
|
4887
|
+
* arguments and env) either way, except for the env names its preview
|
|
4888
|
+
* answer lists in `volatileEnv`, whose values only the real run knows.
|
|
4889
|
+
* `pass` says which hooks run, and how:
|
|
4890
|
+
* - "preview" (a launch preview): declaring hooks under the flag; the others
|
|
4891
|
+
* do not run, and their recorded contributions stand (`notRun`);
|
|
4892
|
+
* - "plan" (a start's preflight): declaring hooks under the flag, the others
|
|
4893
|
+
* once, for real;
|
|
4894
|
+
* - "real" (a start after its preflight passed): declaring hooks for real,
|
|
4895
|
+
* over `frozen.hooks` = the planned contributions; startInstanceSession
|
|
4896
|
+
* refuses a contribution that differs from the preview's. */
|
|
4897
|
+
export function prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, contextDir, extraEnv = {}, assertRoots, pass = "plan" }) {
|
|
4870
4898
|
const contributions = (frozen.hooks?.contributions || []).map((c) => ({ ...c }));
|
|
4871
4899
|
const env = { ...(frozen.hooks?.env || {}) };
|
|
4872
4900
|
const refreshed = [];
|
|
@@ -4889,20 +4917,44 @@ export function prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, c
|
|
|
4889
4917
|
const hooks = manifestHookCommands(manifest);
|
|
4890
4918
|
if (hooks.launch) withLaunchHook.push({ id: p.id, capability: p.id, manifest, layer: p.contribution?.layer ?? p.binding?.layer ?? manifest.layer ?? null, level: p.contribution?.level ?? p.binding?.level ?? null, settings: p.settings, settingsOrigins: p.settingsOrigins, hooks, trust, environment: [...(manifest.environment || [])], environmentNamespaces: [...(manifest.environmentNamespaces || [])], missingRequires: [] });
|
|
4891
4919
|
}
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4920
|
+
const aware = withLaunchHook.filter((c) => c.manifest.launchPreview === true);
|
|
4921
|
+
const unaware = withLaunchHook.filter((c) => c.manifest.launchPreview !== true);
|
|
4922
|
+
const runs = (pass === "real" ? [[aware, false]] : pass === "preview" ? [[aware, true]] : [[unaware, false], [aware, true]]).filter(([caps]) => caps.length);
|
|
4923
|
+
const notRun = pass === "preview" ? unaware.map((c) => c.id) : [];
|
|
4924
|
+
const previewed = runs.filter(([, asPreview]) => asPreview).flatMap(([caps]) => caps.map((c) => c.id));
|
|
4925
|
+
const volatileEnv = [];
|
|
4926
|
+
if (runs.length) {
|
|
4927
|
+
const results = runs.map(([caps, asPreview]) => ({ asPreview, res: runLifecycleHooks("launch", { assertRoots, home, instance: meta.instance, agentName: meta.agent, soulDir: instanceSoulDir(home, meta), contextDir: ctx, rootDir: dirname(dirname(dirname(home))), resolved: { ...(resolvedCfg || {}), capabilities: caps }, priorMeta: meta.capabilityMeta || {}, extraEnv: { OATS_HARNESS: harness, OATS_PREVIOUS_HARNESS: frozen.harness || "", OATS_RUNTIME: harness, OATS_PREVIOUS_RUNTIME: frozen.harness || "", ...(asPreview ? { OATS_LAUNCH_PREVIEW: "1" } : {}), ...extraEnv } }) }));
|
|
4928
|
+
const failed = results.flatMap(({ res }) => res.failures || []).map((f) => `${f.capability}: ${f.message}`);
|
|
4895
4929
|
if (failed.length) throw oatsError("E_LAUNCH_PREPARATION", `a capability could not prepare the ${harness} launch:\n ${failed.join("\n ")}`);
|
|
4930
|
+
const fresh = results.flatMap(({ res }) => res.contributions || []);
|
|
4896
4931
|
// Ownership holds across retained AND refreshed contributions, as the
|
|
4897
4932
|
// spawn runner holds it across providers: a refreshed provider may
|
|
4898
|
-
// replace its own previous keys, never a key another provider retains
|
|
4899
|
-
|
|
4933
|
+
// replace its own previous keys, never a key another provider retains,
|
|
4934
|
+
// nor one another provider's hook set in this pass.
|
|
4935
|
+
const refreshedIds = new Set(fresh.map((c) => c.capability));
|
|
4900
4936
|
const retainedOwner = new Map();
|
|
4901
4937
|
for (const c of contributions) if (c.capability && !refreshedIds.has(c.capability)) for (const name of c.env || []) retainedOwner.set(name, c.capability);
|
|
4902
|
-
|
|
4938
|
+
const freshOwner = new Map();
|
|
4939
|
+
for (const c of fresh) for (const name of c.env || []) {
|
|
4903
4940
|
if (retainedOwner.has(name)) throw oatsError("E_LAUNCH_PREPARATION", `${c.capability}'s launch hook set ${name}, which ${retainedOwner.get(name)} contributed at spawn and retains; one provider owns an environment name; nothing was stopped`);
|
|
4941
|
+
if (freshOwner.has(name) && freshOwner.get(name) !== c.capability) throw oatsError("E_LAUNCH_PREPARATION", `${c.capability}'s launch hook set ${name}, which ${freshOwner.get(name)}'s launch hook also set; one provider owns an environment name; nothing was stopped`);
|
|
4942
|
+
freshOwner.set(name, c.capability);
|
|
4943
|
+
}
|
|
4944
|
+
// A preview answer may name env whose values only the real run knows
|
|
4945
|
+
// (a credential minted at start). Each is a name the same hook returned,
|
|
4946
|
+
// and never a harness's configuration selector: the package probe reads
|
|
4947
|
+
// that under the preview's value before the real run.
|
|
4948
|
+
for (const { asPreview, res } of results) if (asPreview) for (const [id, names] of Object.entries(res.volatileEnv || {})) {
|
|
4949
|
+
if (!Array.isArray(names) || names.some((n) => typeof n !== "string")) throw oatsError("E_LAUNCH_PREPARATION", `${id}'s launch hook answered volatileEnv that is not an array of environment names; nothing was stopped`);
|
|
4950
|
+
const returned = (res.contributions || []).find((c) => c.capability === id)?.env || [];
|
|
4951
|
+
for (const name of names) {
|
|
4952
|
+
if (!returned.includes(name)) throw oatsError("E_LAUNCH_PREPARATION", `${id}'s launch hook declared ${name} volatile but did not return it in env; nothing was stopped`);
|
|
4953
|
+
if (HARNESS_CONFIG_SELECTORS.has(name)) throw oatsError("E_LAUNCH_PREPARATION", `${id}'s launch hook declared ${name} volatile, but ${name} selects a harness's configuration, which the start's package probe reads before the real run; a volatile value must not affect harness package resolution; nothing was stopped`);
|
|
4954
|
+
volatileEnv.push(name);
|
|
4955
|
+
}
|
|
4904
4956
|
}
|
|
4905
|
-
for (const c of
|
|
4957
|
+
for (const c of fresh) {
|
|
4906
4958
|
const idx = contributions.findIndex((x) => x.capability === c.capability);
|
|
4907
4959
|
// The provider's previous contribution goes whole, its env names
|
|
4908
4960
|
// included, before its new (validated) one is merged: an empty answer
|
|
@@ -4912,23 +4964,32 @@ export function prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, c
|
|
|
4912
4964
|
if (idx >= 0) contributions[idx] = row; else contributions.push(row);
|
|
4913
4965
|
refreshed.push(c.capability);
|
|
4914
4966
|
}
|
|
4915
|
-
Object.assign(env, res.env || {});
|
|
4967
|
+
for (const { res } of results) Object.assign(env, res.env || {});
|
|
4968
|
+
// What the caller records comes from real runs only; a launch preview
|
|
4969
|
+
// answers its own runs' warnings.
|
|
4970
|
+
const recorded = results.filter(({ asPreview }) => asPreview === (pass === "preview")).map(({ res }) => res);
|
|
4916
4971
|
// A launch hook's `meta` is part of its documented return (the same shape
|
|
4917
4972
|
// spawn persists as capabilityMeta). It was collected and then dropped
|
|
4918
4973
|
// here, so a provider that re-issues a credential at every start — a
|
|
4919
4974
|
// renewed session grant, for instance — left the ORIGINAL id on record and
|
|
4920
4975
|
// retire undid the wrong one. Carry it to the caller; the start records it.
|
|
4921
|
-
|
|
4976
|
+
const metaOf = Object.assign({}, ...recorded.map((res) => res.meta || {}));
|
|
4977
|
+
hookMeta = Object.keys(metaOf).length ? metaOf : undefined;
|
|
4922
4978
|
// The hooks' advisory warnings go to the start's answer and events, as
|
|
4923
4979
|
// spawn's do; they were dropped here before 0.30.
|
|
4924
|
-
warnings =
|
|
4980
|
+
warnings = recorded.flatMap((res) => res.warnings || []);
|
|
4925
4981
|
}
|
|
4926
|
-
|
|
4982
|
+
// A hook not run for a preview keeps its recorded contribution: whether it
|
|
4983
|
+
// has arguments for the target harness is its own run's answer, at start.
|
|
4984
|
+
const unprepared = contributions.filter((c) => !refreshed.includes(c.capability) && !notRun.includes(c.capability) && c.launch && c.launch[frozen.harness] !== undefined && c.launch[harness] === undefined).map((c) => c.capability);
|
|
4927
4985
|
if (unprepared.length) throw oatsError("E_LAUNCH_PREPARATION", `${unprepared.join(", ")} contributed ${frozen.harness} launch arguments at spawn and none for ${harness}; change that capability's setting (for example its delivery mode), or the provider must declare a launch hook; nothing was stopped`);
|
|
4928
4986
|
const launch = {};
|
|
4929
4987
|
for (const c of contributions) { for (const [rt, args] of Object.entries(c.launch || {})) if (args) launch[rt] = `${launch[rt] ? `${launch[rt]} ` : ""}${args}`; }
|
|
4930
|
-
return { launch, env, contributions, refreshed, warnings, ...(hookMeta ? { meta: hookMeta } : {}) };
|
|
4988
|
+
return { launch, env, contributions, refreshed, notRun, previewed, volatileEnv, warnings, ...(hookMeta ? { meta: hookMeta } : {}) };
|
|
4931
4989
|
}
|
|
4990
|
+
/** Environment that selects which configuration (account, packages) a
|
|
4991
|
+
* harness reads, under which the package probe inspects its packages. */
|
|
4992
|
+
const HARNESS_CONFIG_SELECTORS = new Set(["CLAUDE_CONFIG_DIR", "CODEX_HOME", "PI_CODING_AGENT_DIR"]);
|
|
4932
4993
|
|
|
4933
4994
|
|
|
4934
4995
|
export function restartInstanceSession(home, o = {}) { return startInstanceSession(home, { ...o, restart: true }); }
|
|
@@ -5093,7 +5154,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
5093
5154
|
const reselect = o.reselectLaunch === true ? homeLaunchLayers(realHome, meta) : null;
|
|
5094
5155
|
const selected = reselect !== null || o.launchConfig !== undefined || o.harness !== undefined || o.yolo !== undefined;
|
|
5095
5156
|
const hasRecipe = meta.launch && typeof meta.launch === "object";
|
|
5096
|
-
let launchPlan = null, explicitModelFrom = null, warnings = [];
|
|
5157
|
+
let launchPlan = null, launchHooksPass = null, hookMeta, explicitModelFrom = null, warnings = [];
|
|
5097
5158
|
if (selected || hasRecipe) {
|
|
5098
5159
|
// Every start of a home with a recipe (ordinary, model-only, or under a
|
|
5099
5160
|
// selection) goes through the one planner: recipe shape, the recorded
|
|
@@ -5106,11 +5167,15 @@ export function startInstanceSession(home, o = {}) {
|
|
|
5106
5167
|
const resolvedCfg = resolvedFromHome(realHome, meta, { teams: o.teams, defaultTeam: o.defaultTeam, teamsSource: o.teamsSource });
|
|
5107
5168
|
let agent; try { agent = findAgent(dirname(dirname(dirname(realHome))), meta.agent); } catch { agent = undefined; }
|
|
5108
5169
|
const plan = planLaunch({ home: realHome, instance: meta.instance, meta, contextDir: context, agentLike: agent || { harness: meta.harness, model: meta.model, yolo: meta.yolo }, selection: { launchConfig: o.launchConfig, harness: o.harness, model: o.model, yolo: o.yolo }, reselect, resolvedCfg, env: o.env || process.env, assertRoots: checkRoots });
|
|
5109
|
-
launchPlan = { recipe: plan.recipe, command: plan.command, harness: plan.harness, model: plan.model, yolo: plan.yolo, modelFrom: modelFromOf(plan.modelSource, { at: "start", prior: meta.modelFrom ?? null }),
|
|
5170
|
+
launchPlan = { recipe: plan.recipe, command: plan.command, harness: plan.harness, model: plan.model, yolo: plan.yolo, modelFrom: modelFromOf(plan.modelSource, { at: "start", prior: meta.modelFrom ?? null }),
|
|
5110
5171
|
...(plan.launchChoice ? { launchFrom: plan.launchChoice.from, launchAt: plan.launchChoice.at, launchDeclared: plan.launchChoice.declared } : {}) };
|
|
5172
|
+
// The plan ran preview-aware launch hooks as a preview: their real run, over the planned
|
|
5173
|
+
// contributions, waits for the rest of preflight.
|
|
5174
|
+
if (plan.previewedHooks.length) launchHooksPass = { frozen: { ...plan.frozen, hooks: plan.recipe.hooks }, harness: plan.harness, resolvedCfg, contextDir: context, previewed: plan.recipe.hooks, volatileEnv: plan.volatileEnv, trustHome: plan.trustHome };
|
|
5111
5175
|
command = launchPlan.command; model = launchPlan.model;
|
|
5112
|
-
// The launch hooks
|
|
5113
|
-
// the start does next, and the answer carries them.
|
|
5176
|
+
// The other launch hooks ran for real in the plan: their warnings are
|
|
5177
|
+
// events now, whatever the start does next, and the answer carries them.
|
|
5178
|
+
hookMeta = plan.hookMeta;
|
|
5114
5179
|
warnings = plan.warnings;
|
|
5115
5180
|
for (const message of warnings) appendEvent(realHome, { kind: "launch-warning", data: { message } });
|
|
5116
5181
|
} else if (o.model !== undefined && o.model !== null && String(o.model).trim() !== "") {
|
|
@@ -5127,10 +5192,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
5127
5192
|
if (recipeForEnv) { const missing = missingLaunchEnvRefs(recipeForEnv.env, o.env || process.env); if (missing.length) throw oatsError("E_LAUNCH_ENV_MISSING", `this home's launch references ${missing.join(", ")}, not set on this host; nothing was started`); }
|
|
5128
5193
|
const paneEnv = recipeForEnv ? launchEnvRefs(recipeForEnv, o.env || process.env) : [];
|
|
5129
5194
|
const paneEnvFlags = paneEnv.flatMap((r) => ["-e", `${r.name}=${r.value}`]);
|
|
5130
|
-
checkRoots(); //
|
|
5131
|
-
const planExtra = launchPlan ? { launch: launchPlan.recipe, harness: launchPlan.harness, yolo: launchPlan.yolo, ...(launchPlan.modelFrom ? { modelFrom: launchPlan.modelFrom } : {}),
|
|
5132
|
-
...(launchPlan.launchFrom ? { launchFrom: launchPlan.launchFrom, launchAt: launchPlan.launchAt, launchDeclared: launchPlan.launchDeclared } : {}),
|
|
5133
|
-
...(launchPlan.hookMeta ? { hookMeta: launchPlan.hookMeta } : {}) } : (explicitModelFrom ? { modelFrom: explicitModelFrom } : {});
|
|
5195
|
+
checkRoots(); // preparation has run; no backend has been observed
|
|
5134
5196
|
let target = receipt.target;
|
|
5135
5197
|
let state = { present: false, state: "not-launched" };
|
|
5136
5198
|
let serverGone = false;
|
|
@@ -5141,9 +5203,38 @@ export function startInstanceSession(home, o = {}) {
|
|
|
5141
5203
|
else throw oatsError("E_SESSION_UNKNOWN", `cannot establish whether ${meta.instance} is running, so nothing was started: ${String(e.stderr ?? e.message ?? "").trim() || e.message}`);
|
|
5142
5204
|
}
|
|
5143
5205
|
}
|
|
5206
|
+
if (state.present && state.state !== "shell" && !o.restart) throw oatsError("E_SESSION_RUNNING", `${meta.instance} is running (${state.state}); nothing was started`);
|
|
5207
|
+
// Every preflight has passed: the preview-aware launch hooks run for real
|
|
5208
|
+
// (they may register the home with their provider), before a restart's
|
|
5209
|
+
// stop, and must contribute exactly what they contributed as a preview,
|
|
5210
|
+
// which is what the command was rendered and preflighted from, except the
|
|
5211
|
+
// values of the env their preview declared volatile: those come from this
|
|
5212
|
+
// run, and the command is rendered again with them.
|
|
5213
|
+
if (launchHooksPass) {
|
|
5214
|
+
const { previewed, volatileEnv, trustHome, ...pass } = launchHooksPass;
|
|
5215
|
+
const real = prepareLaunchHooks({ ...pass, home: realHome, meta, assertRoots: checkRoots, pass: "real" });
|
|
5216
|
+
const canon = (v) => canonicalJson(JSON.parse(JSON.stringify(v ?? null)));
|
|
5217
|
+
const stable = (h) => Object.fromEntries(Object.entries(h.env).filter(([n]) => !volatileEnv.includes(n)));
|
|
5218
|
+
if (canon({ launch: real.launch, env: stable(real), contributions: real.contributions }) !== canon({ launch: previewed.launch, env: stable(previewed), contributions: previewed.contributions })) {
|
|
5219
|
+
// A provider's row names its env; the values are in the merged env.
|
|
5220
|
+
const rowOf = (h, id) => { const row = h.contributions.find((c) => c.capability === id); return canon({ row, values: (row?.env || []).map((n) => volatileEnv.includes(n) ? null : h.env[n]) }); };
|
|
5221
|
+
const differing = [...new Set([...real.contributions, ...previewed.contributions].map((c) => c.capability))].filter((id) => rowOf(real, id) !== rowOf(previewed, id));
|
|
5222
|
+
throw oatsError("E_LAUNCH_PREPARATION", `the launch hook of ${differing.join(", ") || "a capability"} returned a contribution that differs from its preview contribution (a launch hook returns the same contribution under OATS_LAUNCH_PREVIEW, apart from the values of its volatileEnv); nothing was stopped or started`);
|
|
5223
|
+
}
|
|
5224
|
+
launchPlan.recipe = { ...launchPlan.recipe, hooks: { launch: real.launch, env: real.env, contributions: real.contributions } };
|
|
5225
|
+
command = launchPlan.command = renderLaunchRecipe(launchPlan.recipe, { home: realHome, instance: meta.instance, trustHome });
|
|
5226
|
+
if (real.meta) hookMeta = { ...(hookMeta || {}), ...real.meta };
|
|
5227
|
+
// The real run's warnings are events now, whatever the start does next,
|
|
5228
|
+
// and the answer carries them.
|
|
5229
|
+
warnings = [...warnings, ...real.warnings];
|
|
5230
|
+
for (const message of real.warnings) appendEvent(realHome, { kind: "launch-warning", data: { message } });
|
|
5231
|
+
checkRoots();
|
|
5232
|
+
}
|
|
5233
|
+
const planExtra = launchPlan ? { launch: launchPlan.recipe, harness: launchPlan.harness, yolo: launchPlan.yolo, ...(launchPlan.modelFrom ? { modelFrom: launchPlan.modelFrom } : {}),
|
|
5234
|
+
...(launchPlan.launchFrom ? { launchFrom: launchPlan.launchFrom, launchAt: launchPlan.launchAt, launchDeclared: launchPlan.launchDeclared } : {}),
|
|
5235
|
+
...(hookMeta ? { hookMeta } : {}) } : (explicitModelFrom ? { modelFrom: explicitModelFrom } : {});
|
|
5144
5236
|
let stopReceipt = null;
|
|
5145
5237
|
if (state.present && state.state !== "shell") {
|
|
5146
|
-
if (!o.restart) throw oatsError("E_SESSION_RUNNING", `${meta.instance} is running (${state.state}); nothing was started`);
|
|
5147
5238
|
// Restart: every preflight above passed, so ask the running harness to
|
|
5148
5239
|
// end and wait, bounded. A harness still there afterwards is reported
|
|
5149
5240
|
// as running; nothing is escalated and nothing is launched.
|
package/lib/schedule.mjs
CHANGED
|
@@ -516,7 +516,7 @@ export function childEnv() {
|
|
|
516
516
|
for (const key of [...RESERVED_LAUNCH_ENV,
|
|
517
517
|
"OATS_DEPLOYMENT", "OATS_RESOLUTION", "OATS_CAPABILITY", "OATS_LAYER", "OATS_LEVEL", "OATS_META", "OATS_OPERATION",
|
|
518
518
|
"OATS_REPO", "OATS_BRANCH", "OATS_WORK", "OATS_KIND", "OATS_TASK",
|
|
519
|
-
"OATS_HARNESS", "OATS_PREVIOUS_HARNESS", "OATS_RUNTIME", "OATS_PREVIOUS_RUNTIME", "OATS_RETIRE_INTENT",
|
|
519
|
+
"OATS_HARNESS", "OATS_PREVIOUS_HARNESS", "OATS_RUNTIME", "OATS_PREVIOUS_RUNTIME", "OATS_LAUNCH_PREVIEW", "OATS_RETIRE_INTENT",
|
|
520
520
|
"OATS_TEAM_NAME", "OATS_TEAM_SCOPE", "OATS_TEAM_ID", "OATS_TEAM_LABEL", "OATS_TEAM_LABELS", "OATS_DEFAULT_TEAM", "OATS_DEFAULT_TEAM_ID", "OATS_DEFAULT_TEAM_FROM", "OATS_TEAMS", "OATS_TEAMS_SOURCE", "OATS_TRIGGER_EVENT_FILE",
|
|
521
521
|
]) delete env[key];
|
|
522
522
|
return env;
|
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.38.0, awebai/oats#484) moves these choices into the committed workspace file. 0.36.x and
|
|
22
|
+
* 0.37.x validate its keys (lib/workspace.mjs) without applying them, and warn about what 0.38.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.38.0) moves these keys; 0.36.x and 0.37.x only warn (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 and 0.37.x; 0.38.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.38.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.38.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,12 +3,12 @@
|
|
|
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": {
|
|
10
10
|
"url": "https://github.com/awebai/oats-aweb.git",
|
|
11
|
-
"ref": "v1.
|
|
11
|
+
"ref": "v1.18.1",
|
|
12
12
|
"path": "oats-package"
|
|
13
13
|
},
|
|
14
14
|
"oats.jira": {
|
|
@@ -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.3",
|
|
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.37.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",
|
|
@@ -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.3 # 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 } }
|