@awebai/oats 0.30.1 → 0.30.3
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 +37 -2
- package/docs/capabilities.md +3 -3
- package/docs/configuration.md +18 -1
- package/docs/design/2026-09-23-workspace-module-contracts.md +2 -1
- package/docs/desktop-cli-api.md +28 -11
- package/docs/execution-targets.md +9 -3
- package/docs/first-team.md +1 -1
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +6 -6
- package/docs/release-notes/v0.30.2.md +85 -0
- package/docs/release-notes/v0.30.3.md +75 -0
- package/docs/souls-and-instances.md +10 -4
- package/docs/workspaces.md +1 -1
- package/lib/core.mjs +109 -35
- package/lib/herdr.mjs +45 -8
- package/lib/instance-inspect.mjs +1 -1
- package/lib/instance-resolution.mjs +15 -8
- package/lib/materialize.mjs +33 -19
- package/package-catalog.json +1 -1
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
|
|
28
28
|
capabilityManifests, capabilityTrust, capabilityExecutablePath,
|
|
29
29
|
officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
|
|
30
|
-
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
30
|
+
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, recordedKernelBin, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
31
31
|
} from "../lib/core.mjs";
|
|
32
32
|
import {
|
|
33
33
|
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages, memoizedRemote,
|
|
@@ -1792,6 +1792,9 @@ async function status() {
|
|
|
1792
1792
|
console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
|
|
1793
1793
|
const key = i.home ?? `${a.name}/${i.instance}`;
|
|
1794
1794
|
if (i.identity) console.log(` identity: ${servedIdentityLine(i.identity)}`);
|
|
1795
|
+
// The kernel the home's plain `oats` runs (its last launch's), when it is not this one.
|
|
1796
|
+
const launchedBy = i.home ? recordedKernelBin(i.home) : null;
|
|
1797
|
+
if (typeof launchedBy === "string" && (verbose || launchedBy !== CLI_BIN)) console.log(` kernel: ${launchedBy}${launchedBy !== CLI_BIN ? " (not this oats)" : ""}`);
|
|
1795
1798
|
const s = ws?.soul.get(key);
|
|
1796
1799
|
if (s && (verbose || s.status !== "current")) console.log(` ${soulDriftLine(s, a.name)}`);
|
|
1797
1800
|
const rows = ws?.drift.get(key) || [];
|
|
@@ -1803,6 +1806,31 @@ async function status() {
|
|
|
1803
1806
|
}
|
|
1804
1807
|
}
|
|
1805
1808
|
|
|
1809
|
+
/** The flags `oats spawn` reads: those taking a value, and switches. `--provider` takes two words.
|
|
1810
|
+
* `--instance` is refused by a local spawn (with its replacement) but still travels to an older
|
|
1811
|
+
* host through `--server`, whose route reads it. */
|
|
1812
|
+
const SPAWN_VALUE_FLAGS = new Set(["agents-root", "backend", "base", "branch", "dir", "expect-decision", "harness", "herdr-socket", "idempotency-key", "instance", "launch-config", "model", "name", "parent", "purpose", "relation", "relative-root", "relative-to", "repo", "runtime", "task", "task-file", "trigger-event", "wake-cron", "wake-every", "wake-file", "wake-json", "wake-message", "wake-message-file", "wake-tz", "work", "work-dir"]);
|
|
1813
|
+
const SPAWN_SWITCHES = new Set(["allow-child-spawns", "json", "no-child-spawns", "no-launch", "no-yolo", "preview", "yolo"]);
|
|
1814
|
+
/** Why `argv` (after `spawn`, the soul first) is not a spawn, or undefined: a positional after the
|
|
1815
|
+
* soul or a flag spawn does not read is never ignored. A value flag consumes its value exactly as
|
|
1816
|
+
* flag() reads it; a missing value is the flag's own check. */
|
|
1817
|
+
function spawnArgvProblem(argv) {
|
|
1818
|
+
const soul = argv[0];
|
|
1819
|
+
for (let i = 1; i < argv.length; i++) {
|
|
1820
|
+
const a = argv[i];
|
|
1821
|
+
if (a === "--provider") { i += 2; continue; }
|
|
1822
|
+
if (a.startsWith("--")) {
|
|
1823
|
+
const name = a.slice(2);
|
|
1824
|
+
if (SPAWN_SWITCHES.has(name)) continue;
|
|
1825
|
+
if (!SPAWN_VALUE_FLAGS.has(name)) return `oats spawn: unknown flag ${a}`;
|
|
1826
|
+
if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--")) i++;
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
if (/^[A-Za-z0-9_.-]+=/.test(a)) return `oats spawn: unexpected argument ${JSON.stringify(a)} after the soul ${JSON.stringify(soul)}: a capability setting is given as --provider <capability> key=value (here: --provider <capability> ${a})`;
|
|
1830
|
+
return `oats spawn: unexpected argument ${JSON.stringify(a)} after the soul ${JSON.stringify(soul)}: spawn takes one soul`;
|
|
1831
|
+
}
|
|
1832
|
+
return undefined;
|
|
1833
|
+
}
|
|
1806
1834
|
async function spawnCmd() {
|
|
1807
1835
|
// JSON mode: contract envelope, stable error codes, stderr-only progress.
|
|
1808
1836
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
@@ -1831,6 +1859,7 @@ async function spawnCmd() {
|
|
|
1831
1859
|
// The slug rule is checked here too, before any side effect (soul fetch).
|
|
1832
1860
|
if (nameFlag !== undefined) { try { explicitInstanceName(String(nameFlag)); } catch (e) { bail(e.code, e.message); throw e; } }
|
|
1833
1861
|
if (args.includes("--ephemeral")) bail("E_BAD_ARGS", "--ephemeral was removed by the runtime-boundary ruling — declare the agent in a capability manifest (agents:) for automatic ephemeral semantics");
|
|
1862
|
+
{ const problem = spawnArgvProblem(args.slice(1)); if (problem) bail("E_BAD_ARGS", problem); }
|
|
1834
1863
|
let root;
|
|
1835
1864
|
// A spawn needs a workspace deployment (lead decision c3-1): no oats-local.yaml
|
|
1836
1865
|
// in reach is E_LOCAL_MISSING, before anything else is read. The agents root is
|
|
@@ -2823,7 +2852,7 @@ function versionCmd() {
|
|
|
2823
2852
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2824
2853
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2825
2854
|
// never listed.
|
|
2826
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], 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"], 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 }));
|
|
2855
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], 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"], 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 }));
|
|
2827
2856
|
return;
|
|
2828
2857
|
}
|
|
2829
2858
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3084,6 +3113,12 @@ async function serverRouteCmd() {
|
|
|
3084
3113
|
const r = spawnSyncProc(route.argv[0], route.argv.slice(1), { stdio: "inherit" });
|
|
3085
3114
|
process.exit(r.status ?? 1);
|
|
3086
3115
|
}
|
|
3116
|
+
// A spawn's argv is checked here, before the server is contacted.
|
|
3117
|
+
if (cmd === "spawn") {
|
|
3118
|
+
const local = args.slice(1).filter((a, i, all) => a !== "--server" && all[i - 1] !== "--server");
|
|
3119
|
+
const problem = spawnArgvProblem(local);
|
|
3120
|
+
if (problem) bail("E_BAD_ARGS", problem);
|
|
3121
|
+
}
|
|
3087
3122
|
// Everything after the command word travels, minus the routing flags; a
|
|
3088
3123
|
// local --task-file is read here and travels as --task text, since the
|
|
3089
3124
|
// remote cannot read this machine's files.
|
package/docs/capabilities.md
CHANGED
|
@@ -28,7 +28,7 @@ A capability lives in one of two kinds of source:
|
|
|
28
28
|
A soul says `capabilities: { <name>: { from: here | <repo key> | package } }`
|
|
29
29
|
(or `off`); the workspace supplies defaults. At spawn every resolved
|
|
30
30
|
capability is **copied whole** into the instance (`<home>/.oats/modules/<name>/`,
|
|
31
|
-
skills into `<home>/.agents/skills/<
|
|
31
|
+
its skills flat into `<home>/.agents/skills/<skill>/`), and the instance's `AGENTS.md` is
|
|
32
32
|
generated without changing the canonical soul. Nothing is installed or
|
|
33
33
|
activated at a deployment.
|
|
34
34
|
|
|
@@ -237,8 +237,8 @@ live.
|
|
|
237
237
|
|
|
238
238
|
Every spawned instance gets a **full copy** of each capability its soul
|
|
239
239
|
resolved to, under `<home>/.oats/modules/<capability>/` (manifest, `bin/`,
|
|
240
|
-
injects, skills), and those skills under
|
|
241
|
-
|
|
240
|
+
injects, skills), and those skills flat under `<home>/.agents/skills/<skill>/`
|
|
241
|
+
beside the soul's own, one level deep where harnesses discover them. Its generated `AGENTS.md` is the
|
|
242
242
|
soul's `AGENTS.md`, the kernel and work-mode blocks, then each module's inject
|
|
243
243
|
in name order. Two composed skills with one name fail the spawn
|
|
244
244
|
(`E_SKILL_DUPLICATE`). The harness then starts normally, with its own skill
|
package/docs/configuration.md
CHANGED
|
@@ -151,10 +151,27 @@ source variable must be set on the host (`E_LAUNCH_ENV_MISSING`, before
|
|
|
151
151
|
anything is created or stopped), and only the harness's pane receives it.
|
|
152
152
|
`list` and `preview` redact every environment value, literals included.
|
|
153
153
|
|
|
154
|
+
**The instance's `oats`.** Every launch (`oats spawn`, `session start`,
|
|
155
|
+
`session restart`, locally or through `--server`) writes `<home>/.oats/bin/oats`,
|
|
156
|
+
a link to the launching kernel's `bin/oats.mjs`, and runs the harness with
|
|
157
|
+
`<home>/.oats/bin` first on `PATH` and the rest of `PATH` unchanged. Plain
|
|
158
|
+
`oats` inside an instance is therefore the kernel that launched it, even on a
|
|
159
|
+
machine whose `PATH` finds another kernel first. A launch configuration's own
|
|
160
|
+
`PATH` (literal or `fromEnv`) comes after it. A restart by a different kernel
|
|
161
|
+
re-points the link to that kernel; `spawn --no-launch` writes it too. The
|
|
162
|
+
recipe in `instance.json` records the target as `launch.kernelBin` (no JSON
|
|
163
|
+
answer carries it); `oats status` prints it
|
|
164
|
+
(`kernel:`) under `--verbose`, or when it is not the `oats` running the status.
|
|
165
|
+
A launch that cannot write the link fails with `E_LAUNCH_SHIM` naming the path
|
|
166
|
+
and the cause: a spawn is rolled back, a start starts nothing. The recorded
|
|
167
|
+
`command` does not carry the `PATH`; the kernel adds it when it runs the
|
|
168
|
+
command. Hooks still receive `OATS_CLI_BIN`, unchanged.
|
|
169
|
+
|
|
154
170
|
**The launch recipe.** A spawn records what a start is made of in
|
|
155
171
|
`instance.json` under `launch`: the harness, the configuration and where it
|
|
156
172
|
came from, the executable, args, env, model, yolo, and each capability's
|
|
157
|
-
launch contribution with its settings and trust
|
|
173
|
+
launch contribution with its settings and trust, and the kernel that launched
|
|
174
|
+
it (`kernelBin`, re-written by every start). One renderer turns it into
|
|
158
175
|
the `command`. Configuration `args` go after the harness's own options and
|
|
159
176
|
before capability arguments; every argument is single-quoted.
|
|
160
177
|
|
|
@@ -286,7 +286,8 @@ shims that throw `E_REMOVED { name, contract }`, pointing at this record.
|
|
|
286
286
|
must match the lock's commit (`E_MATERIALIZE_INTEGRITY { why: "lock" }`); an unknown repo is
|
|
287
287
|
`E_MATERIALIZE_SOURCE`.
|
|
288
288
|
2. The copy's digest must equal the fetch's and any `module.digest` (`E_MATERIALIZE_INTEGRITY`).
|
|
289
|
-
3. Copy skills whole to `<home>/.agents/skills/<
|
|
289
|
+
3. Copy skills whole to `<home>/.agents/skills/<skill>/`, flat (0.30.2; the grouped
|
|
290
|
+
`<name>/<skill>/` layout this section first specified hid every skill from the harnesses).
|
|
290
291
|
4. Compose `<home>/AGENTS.md` = soul body (`options.soulAgentsMd` or `soulDir`) + kernel blocks + module
|
|
291
292
|
injects; operating guidance comes from a module such as `oats.core`. Keep `CLAUDE.md → AGENTS.md`
|
|
292
293
|
and `.claude/skills → ../.agents/skills`.
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -36,7 +36,8 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
36
36
|
"instance-git-remote","souls-declarations","lifecycle-plans","retire-retention","readiness","spawn-preview","instance-events",
|
|
37
37
|
"instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
|
|
38
38
|
"workspace-v2","instance-modules","spawn-provider-payload","served-identity","packages-no-approval","spawn-name","settings-origins",
|
|
39
|
-
"team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference"
|
|
39
|
+
"team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
|
|
40
|
+
"preview-composed-from"],
|
|
40
41
|
"automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
|
|
41
42
|
"readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2}
|
|
42
43
|
```
|
|
@@ -88,6 +89,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
88
89
|
| `automations` | workspace triggers and schedules; `oats automations refresh` | `automationsApi: 1` |
|
|
89
90
|
| `desktop-facts` | the facts under [Desktop facts](#desktop-facts-feature-desktop-facts-oats-0290) | |
|
|
90
91
|
| `launch-preference` | soul and local launch preferences; `launch`, `launchCurrent`, `launchFrom`; `--reselect-launch`; `key` on soul and agent rows ([Launch preferences](#soul-launch-preferences-feature-launch-preference-oats-0300)) | |
|
|
92
|
+
| `preview-composed-from` | `composedFrom` on preview `modules[]` ([Composition](#the-preview)) | |
|
|
91
93
|
|
|
92
94
|
Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
|
|
93
95
|
`workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
|
|
@@ -1073,9 +1075,9 @@ it to a temporary copy (`soulFetched: true`).
|
|
|
1073
1075
|
```json
|
|
1074
1076
|
{"modules":[
|
|
1075
1077
|
{"name":"nw-tools","from":{"kind":"member","repoKey":"github.com/nw/agents","commit":"66566512…"},"layer":null,"private":false,"declares":[],
|
|
1076
|
-
"changedSince":{"instance":"rm-2","was":"45b86f64…"}},
|
|
1078
|
+
"changedSince":{"instance":"rm-2","was":"45b86f64…"},"composedFrom":"soul"},
|
|
1077
1079
|
{"name":"oats.okf","from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"},
|
|
1078
|
-
"layer":"knowledge","private":false,"declares":["state-dir"],"changedSince":false}],
|
|
1080
|
+
"layer":"knowledge","private":false,"declares":["state-dir"],"changedSince":false,"composedFrom":"workspace"}],
|
|
1079
1081
|
"teams":[{"label":"eng","team":"eng:nw.aweb.ai","default":true,"from":"shared"}],
|
|
1080
1082
|
"defaultTeam":{"label":"eng","team":"eng:nw.aweb.ai","from":"soul"},
|
|
1081
1083
|
"resolution":"abacbdb5a7975098d77007c8","declRevision":"068a0d3f1311a9e84e9aff2e","payloadRevision":"81a368006c610194aa35dbe0",
|
|
@@ -1124,9 +1126,18 @@ it to a temporary copy (`soulFetched: true`).
|
|
|
1124
1126
|
|
|
1125
1127
|
**Composition.**
|
|
1126
1128
|
- `modules[]` (feature `instance-modules`): `{name, from, layer, private,
|
|
1127
|
-
declares, changedSince}`; `from` is what `instance.json` will
|
|
1128
|
-
`changedSince` is `null` (no previous instance), `false` (unchanged
|
|
1129
|
-
newest one) or `{instance, was}`.
|
|
1129
|
+
declares, changedSince, composedFrom}`; `from` is what `instance.json` will
|
|
1130
|
+
record. `changedSince` is `null` (no previous instance), `false` (unchanged
|
|
1131
|
+
since the newest one) or `{instance, was}`.
|
|
1132
|
+
- `composedFrom` (feature `preview-composed-from`, OATS 0.30.2): why the
|
|
1133
|
+
module is there — `"soul"` (the soul declares it, including a soul entry
|
|
1134
|
+
that overrides a workspace default of the same name, a package soul's
|
|
1135
|
+
`from: here`, and every module of a standalone view) or `"workspace"` (a
|
|
1136
|
+
`defaults.<slot>` or `defaults.capabilities` entry). It is the same value
|
|
1137
|
+
`oats inspect --soul` reports as `capabilities[].composedFrom`. It is
|
|
1138
|
+
provenance only: it never enters `resolution`, `declRevision`,
|
|
1139
|
+
`payloadRevision` or `decision.revision`, and `instance.json` does not
|
|
1140
|
+
record it. `from` says where the bytes come from.
|
|
1130
1141
|
- `capabilities[]` (`{name, origin}`, `origin` `package:<id>@<v>` or
|
|
1131
1142
|
`member:<repoKey>@<commit>`) and `skills[]` (the soul's own skills as
|
|
1132
1143
|
strings, module skills as `{name, source: "module:<cap>"}`) are display
|
|
@@ -1146,6 +1157,9 @@ it to a temporary copy (`soulFetched: true`).
|
|
|
1146
1157
|
repeatable, `a.b=c` nests): a malformed pair is `E_BAD_ARGS`; a capability
|
|
1147
1158
|
the soul does not resolve is `E_CAPABILITY_MISSING {capability, soul,
|
|
1148
1159
|
modules}`.
|
|
1160
|
+
- Any other positional after the soul, or a flag spawn does not read, is
|
|
1161
|
+
`E_BAD_ARGS` naming the argument, before anything is resolved; a bare
|
|
1162
|
+
`key=value` is refused with the `--provider <capability> key=value` form.
|
|
1149
1163
|
|
|
1150
1164
|
### The decision
|
|
1151
1165
|
|
|
@@ -1228,7 +1242,7 @@ Feature `spawn-name`. `--name <slug>` is the exact name, with no prefix.
|
|
|
1228
1242
|
|
|
1229
1243
|
| Code | Details | When |
|
|
1230
1244
|
|---|---|---|
|
|
1231
|
-
| `E_USAGE`, `E_BAD_ARGS` | | no soul; bad, contradictory or
|
|
1245
|
+
| `E_USAGE`, `E_BAD_ARGS` | | no soul; bad, contradictory, removed or unknown flags; an argument after the soul |
|
|
1232
1246
|
| `E_LOCAL_MISSING`, `E_NO_DEPLOYMENT` | | no `oats-local.yaml`; no `agents/` root |
|
|
1233
1247
|
| `E_SOUL_UNKNOWN` | `{name, members, packages}` | no such soul, or not at `--agents-root` |
|
|
1234
1248
|
| `E_SOUL_AMBIGUOUS` | `{name, repos, qualified}` | several souls answer the bare name; use one of `qualified` |
|
|
@@ -1251,6 +1265,7 @@ Feature `spawn-name`. `--name <slug>` is the exact name, with no prefix.
|
|
|
1251
1265
|
| `E_PLACEMENT_TAKEN`, `E_IDEMPOTENCY_CONFLICT` | `{instance, home}` | |
|
|
1252
1266
|
| `E_SPAWN_INCOMPLETE` | `{instance, home, launched}` | |
|
|
1253
1267
|
| `E_LAUNCH_*`, `E_MODEL_UNKNOWN`, `E_UNSUPPORTED_HARNESS` | | the launch selection is refused |
|
|
1268
|
+
| `E_LAUNCH_SHIM` | | the home's `oats` (`<home>/.oats/bin/oats`) cannot be written; the spawn is rolled back |
|
|
1254
1269
|
| `E_SCHEDULE_INVALID` | | a bad wake (`--wake-json`, `--wake-file`, `--wake-*`) |
|
|
1255
1270
|
| `E_SPAWN_FAILED` | | anything else |
|
|
1256
1271
|
|
|
@@ -1288,8 +1303,9 @@ the keyed-spawn fields `decision`, `spawnIdempotencyKey`, `spawnCompleted` and
|
|
|
1288
1303
|
`wake`; later starts add `restarts` and `restartCount`.
|
|
1289
1304
|
|
|
1290
1305
|
- `modules.<cap>`: `{from, commit, digest, materializedAt}`; `digest` hashes
|
|
1291
|
-
the copy at `<home>/.oats/modules/<cap>/`. Module skills are copied to
|
|
1292
|
-
`<home>/.agents/skills/<
|
|
1306
|
+
the copy at `<home>/.oats/modules/<cap>/`. Module skills are copied flat to
|
|
1307
|
+
`<home>/.agents/skills/<skill>/` (homes spawned by 0.30.1 or earlier keep
|
|
1308
|
+
`<home>/.agents/skills/<cap>/<skill>/`).
|
|
1293
1309
|
- `providers.<cap>`: the merged payload (`{}` when none).
|
|
1294
1310
|
- `workspace`: `{key, name, deployment, commit, resolution, standalone, soul,
|
|
1295
1311
|
layers}`. `name` is recorded, and every hook, command and operation of the
|
|
@@ -1658,8 +1674,9 @@ selection flags. See [the start workflow](desktop-instance-start.md).
|
|
|
1658
1674
|
- A lost response does not mean the launch failed: check status before a
|
|
1659
1675
|
retry. A remote home's saved route names its execution host.
|
|
1660
1676
|
- Errors: `E_BAD_ARGS`, `E_SESSION_UNKNOWN`, `E_UNSUPPORTED_MODE`,
|
|
1661
|
-
`E_SESSION_START_BUSY`, `E_INSTANCE_RETIRING`, `E_LAUNCH_
|
|
1662
|
-
`
|
|
1677
|
+
`E_SESSION_START_BUSY`, `E_INSTANCE_RETIRING`, `E_LAUNCH_*` (among them
|
|
1678
|
+
`E_LAUNCH_SHIM`: the home's `oats` link cannot be written, nothing was
|
|
1679
|
+
started), `E_MODEL_UNKNOWN`, `E_UNSUPPORTED_HARNESS`, `E_SESSION_FAILED`.
|
|
1663
1680
|
|
|
1664
1681
|
### Upload
|
|
1665
1682
|
|
|
@@ -59,9 +59,15 @@ id distinguishes a replacement occupant of the same pane.
|
|
|
59
59
|
(default `~/.config/...`) and starts `herdr --session oats server` if no
|
|
60
60
|
server is running there.
|
|
61
61
|
- The adapter speaks the Herdr socket API at an explicit protocol version,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
20 or 22. A new session records the protocol its server reports (20 for
|
|
63
|
+
Herdr 0.8, 22 for 0.9) and refuses any other; before 0.30.3 it always
|
|
64
|
+
selected 20, so Herdr 0.9 could not be spawned on. A recorded target is
|
|
65
|
+
never renegotiated: each call, the kernel's and the Desktop's, checks that
|
|
66
|
+
the server's snapshot reports the recorded protocol.
|
|
67
|
+
- Herdr cannot give a pane a command at creation, so OATS types the launch
|
|
68
|
+
command into the pane's shell. It waits until the shell has drawn its prompt
|
|
69
|
+
(`herdr pane read`, at most 10 s): text typed earlier is cut at the
|
|
70
|
+
terminal's line-buffer limit.
|
|
65
71
|
|
|
66
72
|
## Lifecycle
|
|
67
73
|
|
package/docs/first-team.md
CHANGED
|
@@ -120,7 +120,7 @@ oats status
|
|
|
120
120
|
`--harness pi|claude|codex` picks the harness; complete any native folder
|
|
121
121
|
trust or authentication prompt in the printed session. The instance home is
|
|
122
122
|
`agents/<soul>/instances/<instance>/`; `work/` is its repository view;
|
|
123
|
-
`.oats/modules/<cap>/` and `.agents/skills/<
|
|
123
|
+
`.oats/modules/<cap>/` are the copied capabilities and `.agents/skills/<skill>/` their skills;
|
|
124
124
|
`instance.json` records `modules` (from, commit, digest), `providers` and
|
|
125
125
|
`workspace`. A running instance never changes under itself — a member that
|
|
126
126
|
moves affects only new spawns, and `oats status` shows the drift
|
|
@@ -29,7 +29,7 @@ an author soul:
|
|
|
29
29
|
```yaml
|
|
30
30
|
# oats-workspace.yaml
|
|
31
31
|
packages:
|
|
32
|
-
oats.framework: v1.4.
|
|
32
|
+
oats.framework: v1.4.1 # provides oats.core, oats.setup, oats.knowledge-theory
|
|
33
33
|
|
|
34
34
|
# souls/<author-soul>/soul.yaml
|
|
35
35
|
capabilities:
|
package/docs/official-catalog.md
CHANGED
|
@@ -9,7 +9,7 @@ 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.
|
|
12
|
+
| `oats.framework` | `oats-framework/v1.4.1` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
|
|
13
13
|
| `oats.okf` | `v4.0.5` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
|
|
14
14
|
| `oats.aweb` | `v1.17.1` | `oats.aweb` (messaging) | |
|
|
15
15
|
| `oats.engineering` | `v1.3.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
|
package/docs/packages.md
CHANGED
|
@@ -51,7 +51,7 @@ packages:
|
|
|
51
51
|
- **Bare version** (`v4.0.5`, `4.0.5`, `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.0.5` or `oats-framework/v1.4.
|
|
54
|
+
convention (`v4.0.5` or `oats-framework/v1.4.1`) 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,7 +74,7 @@ members:
|
|
|
74
74
|
- git:github.com/acme/agents
|
|
75
75
|
- git:github.com/acme/platform
|
|
76
76
|
packages:
|
|
77
|
-
oats.framework: v1.4.
|
|
77
|
+
oats.framework: v1.4.1
|
|
78
78
|
oats.okf: v4.0.5
|
|
79
79
|
oats.aweb: v1.17.1
|
|
80
80
|
teams:
|
|
@@ -212,7 +212,7 @@ At spawn a `from: package` module is fetched at the lock's commit from the
|
|
|
212
212
|
lock's `url`, at the manifest-listed directory (`oats-package.json#capabilities[]`
|
|
213
213
|
entry), into `<home>/.oats/modules/<cap>/`; the copy's digest is verified
|
|
214
214
|
against what the fetch reported; skills are copied to
|
|
215
|
-
`<home>/.agents/skills/<
|
|
215
|
+
`<home>/.agents/skills/<skill>/` (flat). `instance.json.modules.<cap>.from` is
|
|
216
216
|
`{ kind: "package", package, version, commit, integrity, repoKey }`. Bumping
|
|
217
217
|
`packages:` and syncing affects **only new spawns**; `oats status` shows a
|
|
218
218
|
running instance's package module as `moved` once the lock points elsewhere.
|
|
@@ -332,11 +332,11 @@ A soul that names one of the package's capabilities with
|
|
|
332
332
|
"policy": "docs/official-catalog.md",
|
|
333
333
|
"packages": {
|
|
334
334
|
"oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v4.0.5", "path": "oats-package" },
|
|
335
|
-
"oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.4.
|
|
335
|
+
"oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.4.1", "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.1`
|
|
341
|
+
resolves to tag `oats-framework/v1.4.1`. Resolving through the catalog never
|
|
342
342
|
advances a lock by itself: `oats sync` does, and says so.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# OATS 0.30.2
|
|
2
|
+
|
|
3
|
+
## Changed
|
|
4
|
+
|
|
5
|
+
- **Desktop: a faster, calmer load.** The backend reads a deployment's status
|
|
6
|
+
and workspace status in parallel, holds the souls and capabilities catalogs
|
|
7
|
+
and soul inspections server-side (re-read when the workspace state or a
|
|
8
|
+
Desktop mutation moves, or when viewed after 60 s), and slows its cycle
|
|
9
|
+
from 5 s to 30 s while no window is focused. Returning to the window no longer
|
|
10
|
+
re-reads the capabilities list or cancels the observation in flight. When
|
|
11
|
+
the installed CLI supports `--max-age`, background reads accept the kernel's
|
|
12
|
+
recent observation; `/api/panel`, `/api/agents`, `/api/workspace-sync` and
|
|
13
|
+
`/api/capabilities` inspect now report `observedAt` and `refreshing`.
|
|
14
|
+
- **Desktop: Where it works shows the work folder.** The Instance tab's Where
|
|
15
|
+
it works card adds a Folder row, `<home>/work`, between Branch and Home, with
|
|
16
|
+
a copy button; in checkout, attached and workspace mode a muted "shared" tag
|
|
17
|
+
says it links to the tree other instances share.
|
|
18
|
+
|
|
19
|
+
- **Desktop: consistent loading states.** The sidebar roster, the hierarchy,
|
|
20
|
+
the soul inspector and readiness now run on one state model: skeletons
|
|
21
|
+
shaped like the final content instead of false empty lists,
|
|
22
|
+
content and focus kept while refreshing, stale data labelled with its
|
|
23
|
+
observation's age and a Retry, and the actions that need current state held
|
|
24
|
+
with an accessible reason until a good read.
|
|
25
|
+
|
|
26
|
+
- **Desktop: the Workspace view and the instance panel join the loading-state
|
|
27
|
+
model.** The Souls grid, the Capabilities tab and its capability pages, and
|
|
28
|
+
the instance panel's Soul and Messaging sections show skeletons at the real
|
|
29
|
+
row and card size instead of empty claims, keep their content (and a held
|
|
30
|
+
table) through a refresh or a failed re-read, and name a stale observation's
|
|
31
|
+
age with a Retry; the spawn dialog's harness, model and launch hints describe
|
|
32
|
+
the preview for the choices on screen only.
|
|
33
|
+
- **The spawn preview says why each module is there.** Every `modules[]` row
|
|
34
|
+
of `oats spawn … --preview --json` carries `composedFrom`: `"soul"` when the
|
|
35
|
+
soul declares the capability (a package soul's `from: here` and every module
|
|
36
|
+
of a standalone view included), `"workspace"` when a workspace default
|
|
37
|
+
(`defaults.<slot>` or `defaults.capabilities`) gave it. It is the value
|
|
38
|
+
`oats inspect --soul` already reports as `capabilities[].composedFrom`.
|
|
39
|
+
Provenance only: `resolution`, `declRevision`, `payloadRevision` and the
|
|
40
|
+
spawn decision are unchanged, so a previewed decision still binds its apply.
|
|
41
|
+
Gate on the new feature `preview-composed-from`
|
|
42
|
+
([Desktop CLI API](../desktop-cli-api.md#the-preview)).
|
|
43
|
+
|
|
44
|
+
## Fixed
|
|
45
|
+
|
|
46
|
+
- **Instance skills are flat again, so Claude Code finds them.** Since the
|
|
47
|
+
workspace model (0.25.0) spawn copied each module's skills grouped by
|
|
48
|
+
capability, `<home>/.agents/skills/<capability>/<skill>/SKILL.md`. Claude
|
|
49
|
+
Code discovers skills one level deep (`.claude/skills/<skill>/SKILL.md`), so
|
|
50
|
+
no capability skill resolved there: `/oats-operate`, `/oats-aweb`,
|
|
51
|
+
`/okf-consultation` and the rest were missing from the session although the
|
|
52
|
+
composed `AGENTS.md` tells the agent to load them. (pi's loader recurses, so
|
|
53
|
+
pi instances were not affected.) Spawn now copies every skill, the soul's
|
|
54
|
+
own and every module's, flat to `<home>/.agents/skills/<skill>/`, the
|
|
55
|
+
layout the Agent Skills convention and every harness expect;
|
|
56
|
+
`.claude/skills → ../.agents/skills` is unchanged. `instance.json` records
|
|
57
|
+
the flat paths (`capabilities[].skills`; a module's skill tree in
|
|
58
|
+
`composition.expected[].resolved` is the skills root).
|
|
59
|
+
- **One skill name, one directory.** Every composed skill shares the flat
|
|
60
|
+
directory, so names are unique across the soul and its modules, compared
|
|
61
|
+
case-insensitively: a soul's own skill named like a module's skill, or two
|
|
62
|
+
names differing only in case, is `E_SKILL_DUPLICATE` at spawn and the home
|
|
63
|
+
is removed. A soul skill named like a *capability* (`oats.okf`) is no
|
|
64
|
+
longer a collision. No soul in this repository is affected.
|
|
65
|
+
- **oats.framework 1.4.1** (oats.core 2.2.1): the `oats-operate` skill shows
|
|
66
|
+
the flat layout.
|
|
67
|
+
|
|
68
|
+
**Existing Claude Code instances keep their grouped layout and still do not
|
|
69
|
+
see their capability skills: respawn them to pick up the fix.** Nothing
|
|
70
|
+
rewrites an existing home: a running instance never changes under itself, and
|
|
71
|
+
moving its skills would contradict what its `instance.json` recorded. Until
|
|
72
|
+
it is respawned, an agent can read a skill by path,
|
|
73
|
+
`.agents/skills/<capability>/<skill>/SKILL.md`.
|
|
74
|
+
|
|
75
|
+
## Still known in 0.30.2
|
|
76
|
+
|
|
77
|
+
The 0.30.1 notes planned the first two below for 0.30.2. They are not in this
|
|
78
|
+
release; they are planned for 0.30.3.
|
|
79
|
+
|
|
80
|
+
- With no teams configured, a spawn is refused by the provider
|
|
81
|
+
(`E_SPAWN_FAILED`, relaying "no teams configured: run `oats aweb setup`"),
|
|
82
|
+
not by the kernel's `E_TEAM_UNCONFIGURED`.
|
|
83
|
+
- A capability operator command run from a deployment needs `--soul <soul>`.
|
|
84
|
+
- On a team you control (BYOT), retire cannot revoke membership; the retire
|
|
85
|
+
prints the owner's `aw id team remove-member` command, as in 0.30.0.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# OATS 0.30.3
|
|
2
|
+
|
|
3
|
+
## Changed
|
|
4
|
+
|
|
5
|
+
- **Desktop: the whole app works from the keyboard, with a Linux-sane keymap.**
|
|
6
|
+
Changed Linux and Windows defaults: the command palette is now Ctrl+Shift+P
|
|
7
|
+
on Linux and Windows; close tab Ctrl+Shift+W; split Ctrl+Shift+E /
|
|
8
|
+
Ctrl+Shift+O (close the split Ctrl+Shift+Alt+W); Spawn instance
|
|
9
|
+
Ctrl+Shift+N; the theme cycle has no default chord (on macOS too; the palette
|
|
10
|
+
keeps it). No default chord takes a key a program in the terminal reads:
|
|
11
|
+
plain Ctrl+W, Ctrl+K, Ctrl+\, Ctrl+P and Ctrl+B, F6, Alt+digit and
|
|
12
|
+
Ctrl+PgUp/PgDn (and ⌃digits on macOS) always reach the program, and nothing
|
|
13
|
+
binds Super. New: go to tab (⌥⌘1–⌥⌘9 / Alt+1–Alt+9), Ctrl+PgDn / Ctrl+PgUp,
|
|
14
|
+
⌘3 / Ctrl+3 for Automations, and F6 / Shift+F6 to move between the sidebar,
|
|
15
|
+
the instances, the main area and the instance panel; from a terminal,
|
|
16
|
+
⇧⌘F6 / Ctrl+Shift+F6 leaves it for the next region. Your own rebinds are
|
|
17
|
+
kept; one that now clashes with another shortcut is flagged in the shortcuts
|
|
18
|
+
editor, never changed for you. Quick Open (⌘P / Ctrl+P)
|
|
19
|
+
now opens the spawn dialog for the soul you pick, and Esc takes you back to
|
|
20
|
+
where you were; ⌘↵ / Ctrl+Enter spawns from any field, and Tab walks the
|
|
21
|
+
form in the order you see it. Keyboard gaps found in an audit are closed:
|
|
22
|
+
roster rows of unknown state, Independent nodes in the Active overview, the
|
|
23
|
+
Automations row menu, the schedule form and focus lost after switching tabs
|
|
24
|
+
or views. The keymap and the audit are in
|
|
25
|
+
`packages/desktop/docs/desktop-keyboard.md`.
|
|
26
|
+
- **Desktop: the spawn dialog says why each capability is there.** In "What
|
|
27
|
+
will be created", each Capabilities row shows a muted reason tag beside its
|
|
28
|
+
source: **Soul**, **Workspace default**, or, for a workspace default that
|
|
29
|
+
fills a core slot, **Workspace default · messaging** (knowledge, messaging or
|
|
30
|
+
tasks). It is the preview's `composedFrom`, read only from a CLI that reports
|
|
31
|
+
`preview-composed-from`; with an older CLI the rows show no tag, never a
|
|
32
|
+
guess. The Core capabilities table is unchanged.
|
|
33
|
+
- **Desktop: clearer overview lines, a centred rail, pinned Workspace controls.**
|
|
34
|
+
The Active overview draws parent links as smooth curves from a parent's
|
|
35
|
+
bottom to its child's top, with dashed arcs between siblings, in a colour
|
|
36
|
+
that is visible in every theme. The collapsed instance panel's icons sit
|
|
37
|
+
on its centre line. In Workspace, the Capabilities section pills and search
|
|
38
|
+
and the Teams header stay in view while the content scrolls, and scrolling
|
|
39
|
+
to the end of a list no longer moves the whole window.
|
|
40
|
+
|
|
41
|
+
## Fixes
|
|
42
|
+
|
|
43
|
+
- **Plain `oats` inside an instance is the kernel that launched it.** On a
|
|
44
|
+
machine with two kernels side by side (a global 0.24 for classic
|
|
45
|
+
deployments and a 0.30 prefix install), an agent typing `oats` got whatever
|
|
46
|
+
`PATH` found first: `oats aweb teams` answered `E_UNKNOWN_COMMAND` in a 0.30
|
|
47
|
+
home. Every launch (spawn, `session start`, `session restart`) now writes
|
|
48
|
+
`<home>/.oats/bin/oats`, a link to the launching kernel, and runs the harness
|
|
49
|
+
with `<home>/.oats/bin` first on `PATH`. A restart by another kernel
|
|
50
|
+
re-points it. The recipe records the target as `launch.kernelBin`, and
|
|
51
|
+
`oats status` shows it (`kernel:`) when it is not the running `oats`. A
|
|
52
|
+
launch that cannot write the link fails with `E_LAUNCH_SHIM`. Existing homes
|
|
53
|
+
get the link at their next start or restart.
|
|
54
|
+
- **`oats spawn` refuses an argument it does not read.** A positional after
|
|
55
|
+
the soul, or a flag spawn does not know, is `E_BAD_ARGS` naming it, before
|
|
56
|
+
anything is resolved or created. `oats spawn <soul> join=oats` used to be
|
|
57
|
+
accepted and the bare `join=oats` ignored, so the instance joined nothing;
|
|
58
|
+
the refusal now names the form that was meant, `--provider <capability>
|
|
59
|
+
key=value`. A routed spawn (`--server`) is refused on this machine, before
|
|
60
|
+
the server is contacted.
|
|
61
|
+
- **Herdr 0.9 spawns work.** A new Herdr session was always opened at protocol
|
|
62
|
+
20, so `oats spawn --backend herdr` against Herdr 0.9, which speaks protocol
|
|
63
|
+
22, failed with "Herdr snapshot does not match selected protocol 20". A new
|
|
64
|
+
session now records the protocol its server reports when it is one OATS
|
|
65
|
+
supports (20 or 22), and refuses any other by name. A recorded session is
|
|
66
|
+
still checked against its recorded protocol on every call and is never
|
|
67
|
+
renegotiated. The Desktop attaches to protocol-22 sessions too; it accepted
|
|
68
|
+
only protocol 20 before. An instance recorded at protocol 20 is unreachable
|
|
69
|
+
after an in-place Herdr 0.8 → 0.9 upgrade until it is respawned.
|
|
70
|
+
- **A Herdr launch no longer arrives cut.** OATS typed the launch command into
|
|
71
|
+
the new pane before its shell had started. The terminal's line buffer
|
|
72
|
+
(1024 bytes on macOS) dropped the rest of the longer command, so the harness
|
|
73
|
+
never started: the shell waited on an unclosed quote. On Herdr 0.9, 0 of 5
|
|
74
|
+
fresh panes ran a 2000-character command intact. A launch now waits, up to
|
|
75
|
+
10 s, until the pane has drawn its prompt, then types; 5 of 5 ran intact.
|
|
@@ -29,6 +29,7 @@ that model.
|
|
|
29
29
|
| Instance operating doc | `<home>/AGENTS.md` (generated) |
|
|
30
30
|
| Instance skills | `<home>/.agents/skills/` |
|
|
31
31
|
| Instance modules | `<home>/.oats/modules/<capability>/` (the copies this instance runs) |
|
|
32
|
+
| Instance `oats` | `<home>/.oats/bin/oats` (a link to the kernel that last launched it) |
|
|
32
33
|
| Instance record | `<home>/instance.json` (`modules`, `providers`, `workspace`, `teams`) |
|
|
33
34
|
|
|
34
35
|
## Soul anatomy
|
|
@@ -122,10 +123,11 @@ full copy** of every capability the soul resolved to:
|
|
|
122
123
|
<agents-root>/<soul>/instances/<instance>/
|
|
123
124
|
AGENTS.md # generated: soul AGENTS.md + kernel/work-mode blocks + each module's inject
|
|
124
125
|
CLAUDE.md → AGENTS.md
|
|
125
|
-
.agents/skills/ # canonical skill tree
|
|
126
|
-
<
|
|
126
|
+
.agents/skills/ # canonical skill tree, flat: the soul's skills and every module's, full copies
|
|
127
|
+
<skill>/SKILL.md # one level deep, where every harness discovers skills
|
|
127
128
|
.claude/skills → ../.agents/skills
|
|
128
129
|
.oats/modules/<capability>/ # the whole capability: oats.json, bin/, injects/, skills/ (hooks run from here)
|
|
130
|
+
.oats/bin/oats → <kernel>/bin/oats.mjs # the kernel that last launched this home: first on the harness's PATH
|
|
129
131
|
work/ # worktree, checkout symlink, attached tree, or private directory
|
|
130
132
|
TASK.md # briefing and task
|
|
131
133
|
instance.json # provenance (below); `soulDir` = the soul directory hooks get as OATS_SOUL
|
|
@@ -193,7 +195,8 @@ bump affects only new spawns.
|
|
|
193
195
|
OATS is a skill contributor, not a skill sandbox. The harness (pi, Claude Code,
|
|
194
196
|
Codex) starts with cwd = the instance home and its **own** skill discovery
|
|
195
197
|
intact: it sees, nearest first, the instance's `.agents/skills/` (soul skills
|
|
196
|
-
and the copied capability skills
|
|
198
|
+
and the copied capability skills, all flat at `.agents/skills/<skill>/SKILL.md`,
|
|
199
|
+
the one level deep Claude Code discovers through `.claude/skills`), the repo's own `.agents/skills/` once it
|
|
197
200
|
works in `work/`, and whatever the operator keeps at machine level. All three
|
|
198
201
|
are intended. Two *composed* skills with one name is a spawn error naming both
|
|
199
202
|
capabilities (`E_SKILL_DUPLICATE`); a composed skill versus an ambient one is
|
|
@@ -243,7 +246,10 @@ it would bind, `providers` (the `--provider` map exactly as given) and
|
|
|
243
246
|
the apply refuses with `E_DECISION_STALE` if a member
|
|
244
247
|
moved in between. `--provider <cap> key=value` (repeatable; dotted keys nest)
|
|
245
248
|
must name a capability the soul resolves (`E_CAPABILITY_MISSING` otherwise) and
|
|
246
|
-
needs a workspace deployment.
|
|
249
|
+
needs a workspace deployment. Spawn takes one soul and the flags it reads:
|
|
250
|
+
another positional, or a flag it does not know, is `E_BAD_ARGS` naming the
|
|
251
|
+
argument, before anything is created (a bare `key=value` is a provider
|
|
252
|
+
setting given without `--provider <capability>`). The full DTOs are in
|
|
247
253
|
[desktop-cli-api.md](desktop-cli-api.md#workspace-model-workspaceapi-2).
|
|
248
254
|
|
|
249
255
|
Examples of spawn hooks:
|
package/docs/workspaces.md
CHANGED
|
@@ -54,7 +54,7 @@ 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.
|
|
57
|
+
oats.framework: v1.4.1 # bare version → resolves through the official catalog
|
|
58
58
|
oats.okf: v4.0.5
|
|
59
59
|
acme.tools: git:github.com/acme/tools@v0.4.0 # outside the catalog → git:<repo>@<tag|OID>; still a package
|
|
60
60
|
|
package/lib/core.mjs
CHANGED
|
@@ -1537,7 +1537,7 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, s
|
|
|
1537
1537
|
// Hooks also run through direct core callers (not only bin/oats).
|
|
1538
1538
|
// Author this from the running kernel, never PATH, ambient env or
|
|
1539
1539
|
// a caller's extraEnv: those may point at a different executable.
|
|
1540
|
-
OATS_CLI_BIN:
|
|
1540
|
+
OATS_CLI_BIN: kernelBin(),
|
|
1541
1541
|
OATS_SETTINGS: JSON.stringify(cap.settings || {}),
|
|
1542
1542
|
// Where each leaf of OATS_SETTINGS came from (JSON pointer → { kind, at }; kind is
|
|
1543
1543
|
// manifest-default | workspace | soul | host | spawn | anchor), so a provider can tell a
|
|
@@ -2191,6 +2191,53 @@ export function launchEnvRefs(recipe, env = process.env) {
|
|
|
2191
2191
|
export function launchEnvTmuxFlags(recipe, env) { return launchEnvRefs(recipe, env).map((r) => ` -e ${shq(`${r.name}=${r.value}`)}`).join(""); }
|
|
2192
2192
|
export function launchEnvExports(recipe, env) { return launchEnvRefs(recipe, env).map((r) => `export ${r.name}=${shq(r.value)}; `).join(""); }
|
|
2193
2193
|
|
|
2194
|
+
/** The canonical path of this kernel's CLI: what the home's shim points at, and OATS_CLI_BIN. */
|
|
2195
|
+
export function kernelBin() { return realpathSync(join(PKG_ROOT, "bin", "oats.mjs")); }
|
|
2196
|
+
/** Where a home's `oats` lives: first on its harness's PATH, so plain `oats` inside an instance is
|
|
2197
|
+
* the kernel that launched it, whatever other `oats` the host's PATH finds first. */
|
|
2198
|
+
export const kernelShimDir = (home) => join(home, ".oats", "bin");
|
|
2199
|
+
/** Write <home>/.oats/bin/oats, a symlink to this kernel's CLI, replacing any shim a previous
|
|
2200
|
+
* launch (perhaps by another kernel) left: built aside, then renamed over. `.oats` and `.oats/bin`
|
|
2201
|
+
* must be directories of the home itself: a link there would carry the write outside the home.
|
|
2202
|
+
* The target, or E_LAUNCH_SHIM naming the path and the cause (a launch never runs with the
|
|
2203
|
+
* wrong `oats`), before anything outside the home is touched. */
|
|
2204
|
+
export function writeKernelShim(home) {
|
|
2205
|
+
const dir = kernelShimDir(home), shim = join(dir, "oats");
|
|
2206
|
+
let target, aside;
|
|
2207
|
+
try {
|
|
2208
|
+
target = kernelBin();
|
|
2209
|
+
for (const d of [join(home, ".oats"), dir]) {
|
|
2210
|
+
let st;
|
|
2211
|
+
try { st = lstatSync(d); }
|
|
2212
|
+
catch (e) { if (e.code !== "ENOENT") throw e; mkdirSync(d); st = lstatSync(d); }
|
|
2213
|
+
if (!st.isDirectory()) throw new Error(`${d} is ${st.isSymbolicLink() ? "a symbolic link" : "not a directory"}`);
|
|
2214
|
+
}
|
|
2215
|
+
if (realpathSync(dir) !== join(realpathSync(home), ".oats", "bin")) throw new Error(`${dir} resolves outside the home`);
|
|
2216
|
+
aside = join(dir, `.oats-${randomUUID()}`);
|
|
2217
|
+
symlinkSync(target, aside);
|
|
2218
|
+
renameSync(aside, shim);
|
|
2219
|
+
return target;
|
|
2220
|
+
} catch (e) {
|
|
2221
|
+
if (aside) rmSync(aside, { force: true });
|
|
2222
|
+
throw oatsError("E_LAUNCH_SHIM", `cannot write the kernel shim ${shim}${target ? ` (to ${target})` : ""}: ${e.message}; nothing was started`);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
/** A parsed launch command's environment prefix with the home's shim directory first on PATH: a
|
|
2226
|
+
* configuration's PATH (literal or reference) follows it, else the PATH the command runs under. */
|
|
2227
|
+
function shimPathPrefix(tokens, binary, home) {
|
|
2228
|
+
const dir = shq(kernelShimDir(home));
|
|
2229
|
+
const prefix = tokens.slice(0, binary);
|
|
2230
|
+
const texts = prefix.map((t) => t.name !== "PATH" ? t.text : t.kind === "env" ? `PATH=${dir}:${shq(t.value)}` : `PATH=${dir}:"$${t.source}"`);
|
|
2231
|
+
if (!prefix.some((t) => t.name === "PATH")) texts.push(`PATH=${dir}:"$PATH"`);
|
|
2232
|
+
return texts;
|
|
2233
|
+
}
|
|
2234
|
+
/** The persisted launch command as a shell runs it: the same command, with the home's kernel shim
|
|
2235
|
+
* first on PATH. The persisted bytes never carry it (they stay a shape parseLaunchCommand reads). */
|
|
2236
|
+
export function launchShellCommand(command, home) {
|
|
2237
|
+
const { tokens, binary } = parseLaunchCommand(command);
|
|
2238
|
+
return [...shimPathPrefix(tokens, binary, home), ...tokens.slice(binary).map((t) => t.text)].join(" ");
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2194
2241
|
/** The harness command line of a recipe. With no configuration the bytes
|
|
2195
2242
|
* equal what spawn rendered before recipes: env prefix, the executable, the
|
|
2196
2243
|
* harness's own arguments, capability launch args, the task prompt. A
|
|
@@ -2234,14 +2281,15 @@ export function renderLaunchRecipe(recipe, { home, instance, redact = false }) {
|
|
|
2234
2281
|
|
|
2235
2282
|
/** Execution, not preview: mark pending before dispatch, then resolve native
|
|
2236
2283
|
* storage inside the backend shell under the actual command environment.
|
|
2237
|
-
* The original executable/argv is exec'd unchanged after recording succeeds
|
|
2284
|
+
* The original executable/argv is exec'd unchanged after recording succeeds,
|
|
2285
|
+
* with the home's kernel shim first on PATH (every launch runs through here). */
|
|
2238
2286
|
function nativeRecordCommand(command, home, harness) {
|
|
2239
2287
|
const { tokens, binary } = parseLaunchCommand(command);
|
|
2240
2288
|
const args = tokens.slice(binary + 1).filter(t => t.kind !== "prompt").map(t => t.value ?? t.text);
|
|
2241
2289
|
const id = prepareNativeStart(home, harness);
|
|
2242
2290
|
const recorder = join(PKG_ROOT, "packages", "record", "bin", "record-native-start.mjs");
|
|
2243
2291
|
const inner = `${shq(process.execPath)} ${shq(recorder)} ${shq(home)} ${shq(id)} ${shq(harness)} ${shq(JSON.stringify(args))} && exec ${tokens.slice(binary).map(t => t.text).join(" ")}`;
|
|
2244
|
-
return `${tokens
|
|
2292
|
+
return `${shimPathPrefix(tokens, binary, home).join(" ")} /bin/sh -c ${shq(inner)}`;
|
|
2245
2293
|
}
|
|
2246
2294
|
|
|
2247
2295
|
|
|
@@ -2374,7 +2422,7 @@ export function planLaunch({ home, instance, meta, contextDir, agentLike, select
|
|
|
2374
2422
|
version: LAUNCH_RECIPE_VERSION, harness, launchConfig: config?.name || null, launchConfigSource: config?.source || null,
|
|
2375
2423
|
executable: executable.path || executable.declared || harness, executableDeclared: executable.declared ?? null, executableResolvedFrom: executable.resolvedFrom,
|
|
2376
2424
|
args: [...(config?.args || [])], env: { ...configEnv }, model: model || null, ...(yolo !== undefined ? { yolo } : {}),
|
|
2377
|
-
hooks: frozen ? { launch: hooks.launch, env: hooks.env, contributions: hooks.contributions } : hooks, prompt: LAUNCH_PROMPT,
|
|
2425
|
+
hooks: frozen ? { launch: hooks.launch, env: hooks.env, contributions: hooks.contributions } : hooks, prompt: LAUNCH_PROMPT, kernelBin: kernelBin(),
|
|
2378
2426
|
...(frozen?.legacy ? { legacy: { ...frozen.legacy, ...(hooks.refreshed?.length ? { replacedBy: hooks.refreshed } : {}) } } : {}),
|
|
2379
2427
|
};
|
|
2380
2428
|
const inst = instance || meta?.instance || basename(home);
|
|
@@ -2412,11 +2460,18 @@ export function redactLaunchCommand(command) {
|
|
|
2412
2460
|
try { return parseLaunchCommand(command).tokens.map((t) => t.kind === "env" && !IDENTITY_LAUNCH_ENV.has(t.name) ? `${t.name}='<redacted>'` : t.text).join(" "); }
|
|
2413
2461
|
catch { return "<unparseable launch command withheld>"; }
|
|
2414
2462
|
}
|
|
2415
|
-
/** The recipe
|
|
2463
|
+
/** The recipe as answers carry it: every environment value withheld, and without `kernelBin`
|
|
2464
|
+
* (the home's record keeps it; human `oats status` reads it from there). */
|
|
2416
2465
|
export function redactLaunchRecipe(recipe) {
|
|
2417
2466
|
const env = Object.fromEntries(Object.keys(recipe.env || {}).sort().map((n) => [n, typeof recipe.env[n] === "string" ? { redacted: true } : { fromEnv: recipe.env[n].fromEnv }]));
|
|
2418
2467
|
const hooks = recipe.hooks ? { ...recipe.hooks, env: Object.fromEntries(Object.keys(recipe.hooks.env || {}).sort().map((n) => [n, { redacted: true }])) } : undefined;
|
|
2419
|
-
|
|
2468
|
+
const { kernelBin: _recorded, ...answered } = recipe;
|
|
2469
|
+
return { ...answered, env, ...(hooks ? { hooks } : {}) };
|
|
2470
|
+
}
|
|
2471
|
+
/** The kernel a home's last launch pointed its `oats` at (instance.json launch.kernelBin), or null. */
|
|
2472
|
+
export function recordedKernelBin(home) {
|
|
2473
|
+
try { const bin = JSON.parse(readFileSync(join(home, "instance.json"), "utf8"))?.launch?.kernelBin; return typeof bin === "string" ? bin : null; }
|
|
2474
|
+
catch { return null; }
|
|
2420
2475
|
}
|
|
2421
2476
|
/** Spawn. With `o.prepared` (workspace model: a resolution prepared by
|
|
2422
2477
|
* instance-resolution.mjs) this is ASYNC — capabilities are fetched and copied
|
|
@@ -2992,7 +3047,7 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2992
3047
|
return e;
|
|
2993
3048
|
};
|
|
2994
3049
|
// Workspace model: copy every resolved capability WHOLE into the new home
|
|
2995
|
-
// (.oats/modules/<cap
|
|
3050
|
+
// (.oats/modules/<cap>/, its skills flat in .agents/skills/<skill>/) and record modules/providers
|
|
2996
3051
|
// in instance.json. Then REBUILD the capability rows against the copies that
|
|
2997
3052
|
// landed (H1): until now `resolvedCfg.capabilities` were PLANNED rows (no
|
|
2998
3053
|
// skills, no inject, no dir) — hooks, environment, requirements, retirement
|
|
@@ -3034,9 +3089,14 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
3034
3089
|
const row = rows.find((c) => c.id === r.module);
|
|
3035
3090
|
if (r.type === "injection") { r.path = row?.inject; continue; }
|
|
3036
3091
|
if (r.type === "skill-tree") {
|
|
3037
|
-
|
|
3092
|
+
// Module skills land flat in the canonical skills root; the entries are
|
|
3093
|
+
// the names materialize placed for this module (what the resolution
|
|
3094
|
+
// promised), verified against the root in the completeness check.
|
|
3095
|
+
const skillsRoot = join(home, ".agents", "skills");
|
|
3038
3096
|
r.path = existsSync(skillsRoot) ? skillsRoot : undefined;
|
|
3039
|
-
r.entries = (
|
|
3097
|
+
r.entries = Array.isArray(materializeOutcome?.skills)
|
|
3098
|
+
? materializeOutcome.skills.filter((s) => s.module === r.module).map((s) => s.name)
|
|
3099
|
+
: (row?.skills || []).map((p) => basename(p));
|
|
3040
3100
|
}
|
|
3041
3101
|
}
|
|
3042
3102
|
} catch (e) { throw rollbackEmptyOrPreparedHome(e); }
|
|
@@ -3074,30 +3134,43 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
3074
3134
|
}
|
|
3075
3135
|
if (!existsSync(join(home, "CLAUDE.md"))) symlinkSync("AGENTS.md", join(home, "CLAUDE.md"));
|
|
3076
3136
|
|
|
3077
|
-
// Skills: module skills were copied WHOLE into <home>/.agents/skills/<
|
|
3137
|
+
// Skills: module skills were copied WHOLE and FLAT into <home>/.agents/skills/<skill>/
|
|
3078
3138
|
// by materialize (decision 7/13); here only the soul's own skills join them, one
|
|
3079
|
-
// directory each
|
|
3080
|
-
//
|
|
3139
|
+
// directory each, at the same level — the one level every harness discovers. There
|
|
3140
|
+
// is no override: a soul skill named like a module's skill is a duplicate (decision 16). The harness is launched with its normal
|
|
3081
3141
|
// discovery — the machine's and the repo's skills are the harness's business.
|
|
3082
3142
|
const sources = [];
|
|
3083
3143
|
const soulSkills = join(soulDir, "skills");
|
|
3084
3144
|
if (existsSync(soulSkills)) sources.push({ id: "soul", path: soulSkills });
|
|
3085
3145
|
const chosen = new Map();
|
|
3146
|
+
// Names compare case-insensitively: one directory per skill on a case-insensitive
|
|
3147
|
+
// filesystem (APFS, NTFS) must not silently merge `Foo` into `foo`.
|
|
3148
|
+
const moduleSkillOwner = new Map((materializeOutcome?.skills || []).map((s) => [s.name.toLowerCase(), s.module]));
|
|
3149
|
+
const chosenKeys = new Map();
|
|
3086
3150
|
const offer = (name, src, source) => {
|
|
3087
|
-
|
|
3151
|
+
const key = name.toLowerCase();
|
|
3152
|
+
if (chosenKeys.has(key) || moduleSkillOwner.has(key) || existsSync(join(home, ".agents", "skills", name))) {
|
|
3153
|
+
const other = chosenKeys.get(key) ?? (moduleSkillOwner.has(key) ? `module ${moduleSkillOwner.get(key)}'s skill` : `the existing .agents/skills/${name}`);
|
|
3154
|
+
throw oatsError("E_SKILL_DUPLICATE", `skill "${name}" from ${source} collides with ${other}`);
|
|
3155
|
+
}
|
|
3088
3156
|
chosen.set(name, { src, source });
|
|
3157
|
+
chosenKeys.set(key, source);
|
|
3089
3158
|
};
|
|
3090
|
-
//
|
|
3091
|
-
//
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3159
|
+
// A refusal here comes after materialize populated the home: remove it whole,
|
|
3160
|
+
// like every other failure between materialize and the completeness check.
|
|
3161
|
+
try {
|
|
3162
|
+
// Same enumerator preflight used, so "what a tree promises" and "what gets
|
|
3163
|
+
// copied" cannot drift apart.
|
|
3164
|
+
for (const source of sources) for (const entry of skillEntriesIn(source.path)) offer(entry.name, entry.src, source.id);
|
|
3165
|
+
mkdirSync(join(home, ".agents", "skills"), { recursive: true });
|
|
3166
|
+
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
3167
|
+
for (const [name, selected] of [...chosen].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3168
|
+
// Pi's recursive skill scanner does not descend through directory symlinks.
|
|
3169
|
+
// Copy each selected tree so the exact instance-local set is real and immutable.
|
|
3170
|
+
copyTreeSafe(realpathSync(selected.src), join(home, ".agents", "skills", name));
|
|
3171
|
+
}
|
|
3172
|
+
if (!existsSync(join(home, ".claude", "skills"))) symlinkSync(join("..", ".agents", "skills"), join(home, ".claude", "skills"));
|
|
3173
|
+
} catch (e) { throw rollbackEmptyOrPreparedHome(e); }
|
|
3101
3174
|
|
|
3102
3175
|
// EXPECTED == MATERIALIZED. Preflight proved every declared resource resolves;
|
|
3103
3176
|
// this proves the copies actually landed, so "the composition is complete" is
|
|
@@ -3119,18 +3192,14 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
3119
3192
|
if (!chosen.has(name)) incomplete.push(`skill "${name}", promised by ${r.source} (${r.declared}), is missing from the composed set`);
|
|
3120
3193
|
}
|
|
3121
3194
|
}
|
|
3122
|
-
// Prepared spawn: every module's declared skill must have been copied
|
|
3123
|
-
// .agents/skills/<
|
|
3124
|
-
//
|
|
3195
|
+
// Prepared spawn: every module's declared skill must have been copied to
|
|
3196
|
+
// .agents/skills/<skill>/ by materialize — verify the copies landed as
|
|
3197
|
+
// readable skills, one level deep, where the harnesses discover them.
|
|
3125
3198
|
if (o.prepared) {
|
|
3126
|
-
for (const m of o.prepared.resolution.modules) for (const s of m.manifest.skills || []) {
|
|
3127
|
-
const sk = join(home, ".agents", "skills", m.name);
|
|
3128
|
-
if (!existsSync(sk)) { incomplete.push(`module "${m.name}" declares skills but .agents/skills/${m.name} is absent`); break; }
|
|
3129
|
-
}
|
|
3130
3199
|
for (const r of expectedResources) {
|
|
3131
3200
|
if (r.deferred !== "materialize" || r.type !== "skill-tree") continue;
|
|
3132
|
-
if (!r.path) { incomplete.push(`module "${r.module}" declares skill tree ${r.declared} but .agents/skills
|
|
3133
|
-
if (!r.entries.length) incomplete.push(`module "${r.module}" declares skill tree ${r.declared} but
|
|
3201
|
+
if (!r.path) { incomplete.push(`module "${r.module}" declares skill tree ${r.declared} but .agents/skills is absent`); continue; }
|
|
3202
|
+
if (!r.entries.length) incomplete.push(`module "${r.module}" declares skill tree ${r.declared} but none of its skills was copied into .agents/skills`);
|
|
3134
3203
|
for (const name of r.entries) if (!hasSkillDoc(join(r.path, name))) incomplete.push(`skill "${name}" (module ${r.module}) did not materialize as a readable SKILL.md`);
|
|
3135
3204
|
}
|
|
3136
3205
|
}
|
|
@@ -3463,6 +3532,8 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3463
3532
|
// instance.json beside its rendering, which is executed in the instance's
|
|
3464
3533
|
// tmux window. Capabilities contributed harness-specific arguments and
|
|
3465
3534
|
// environment through their spawn hook; both are recorded with provenance.
|
|
3535
|
+
// The home's `oats` is this kernel, launched now or later (--no-launch).
|
|
3536
|
+
const shimTarget = writeKernelShim(home);
|
|
3466
3537
|
const recipe = {
|
|
3467
3538
|
version: LAUNCH_RECIPE_VERSION, harness,
|
|
3468
3539
|
launchConfig: launchConfig?.name || null, launchConfigSource: launchConfig?.source || null,
|
|
@@ -3470,12 +3541,12 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3470
3541
|
args: [...(launchConfig?.args || [])], env: { ...(launchConfig?.env || {}) },
|
|
3471
3542
|
model: model || null, ...(yolo !== undefined ? { yolo } : {}),
|
|
3472
3543
|
hooks: { launch: { ...hookRes.launch }, env: { ...hookRes.env }, contributions: hookRes.contributions || [] },
|
|
3473
|
-
prompt: LAUNCH_PROMPT,
|
|
3544
|
+
prompt: LAUNCH_PROMPT, kernelBin: shimTarget,
|
|
3474
3545
|
};
|
|
3475
3546
|
if (triggerEventFile) recipe.env.OATS_TRIGGER_EVENT_FILE = triggerEventFile;
|
|
3476
3547
|
const cmdline = renderLaunchRecipe(recipe, { home, instance });
|
|
3477
3548
|
|
|
3478
|
-
// Module skills as materialize landed them (.agents/skills/<
|
|
3549
|
+
// Module skills as materialize landed them (flat, .agents/skills/<skill>/),
|
|
3479
3550
|
// beside the soul's own: per-skill provenance `module:<cap>` (lead decision c3),
|
|
3480
3551
|
// `from` = the home's module copy the skill was copied from.
|
|
3481
3552
|
const moduleSkills = (materializeOutcome?.skills || []).map((row) => ({ name: row.name, source: `module:${row.module}`, from: join(home, row.from) }));
|
|
@@ -4879,6 +4950,9 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4879
4950
|
try { state = inspectSessionTarget(target, o.io); } catch (e) { if (backend === "tmux" && lostTmuxServer(e)) { serverGone = true; state = { present: false, state: "stopped" }; } else throw oatsError("E_SESSION_UNKNOWN", `after the stop, cannot establish the state of ${meta.instance}: ${String(e.stderr ?? e.message ?? "").trim() || e.message}`); }
|
|
4880
4951
|
if (state.present && state.state !== "shell") throw oatsError("E_SESSION_UNKNOWN", `${meta.instance} read as stopped and then as ${state.state} again; nothing was started`);
|
|
4881
4952
|
}
|
|
4953
|
+
// The kernel that launches the harness is the one the agent's plain `oats` runs.
|
|
4954
|
+
checkRoots();
|
|
4955
|
+
writeKernelShim(realHome);
|
|
4882
4956
|
const startedAt = new Date().toISOString();
|
|
4883
4957
|
const id = randomUUID();
|
|
4884
4958
|
checkRoots();
|
package/lib/herdr.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/** Herdr 0.8.x/0.9.x host-local backend (explicit protocols 20/22).
|
|
2
|
-
* SSH belongs to the CLI router
|
|
2
|
+
* SSH belongs to the CLI router. A new session records the server's protocol once; a recorded
|
|
3
|
+
* target is never renegotiated. */
|
|
3
4
|
import { execFileSync, spawn } from "node:child_process";
|
|
4
5
|
import { existsSync, mkdirSync } from "node:fs";
|
|
5
6
|
import { homedir } from "node:os";
|
|
6
7
|
import { dirname, join, resolve } from "node:path";
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
+
// The legacy protocol literal; a new session records the protocol its server reports (ensureHerdr).
|
|
9
10
|
export const HERDR_PROTOCOL = 20;
|
|
10
11
|
export const HERDR_SUPPORTED_PROTOCOLS = Object.freeze([20, 22]);
|
|
11
12
|
export const isSupportedHerdrProtocol = (value) => HERDR_SUPPORTED_PROTOCOLS.includes(value);
|
|
@@ -21,10 +22,15 @@ export function herdrSocket(session = "oats") {
|
|
|
21
22
|
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "herdr", "sessions", session, "herdr.sock");
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
/** One Herdr CLI call on the target's socket, its stdout as text. */
|
|
26
|
+
function herdrOutput(target, args, { timeout = 10000, exec = execFileSync } = {}) {
|
|
25
27
|
const env = { ...process.env, HERDR_SOCKET_PATH: target.socket };
|
|
26
28
|
delete env.HERDR_SESSION;
|
|
27
|
-
|
|
29
|
+
return exec(target.binary || "herdr", args, { env, encoding: "utf8", timeout, maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function herdrCommand(target, args, io) {
|
|
33
|
+
const output = herdrOutput(target, args, io);
|
|
28
34
|
// Some successful terminal mutations have no output; reads below require
|
|
29
35
|
// their documented result shape independently.
|
|
30
36
|
if (!output.trim()) return {};
|
|
@@ -42,9 +48,20 @@ export function herdrSnapshot(target, io) {
|
|
|
42
48
|
return snapshot;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
/** The target for a NEW session on the server at `socket`: it records the supported protocol that server
|
|
52
|
+
* reports (Herdr 0.8 speaks 20, 0.9 speaks 22). Every later call checks the snapshot against that recorded
|
|
53
|
+
* protocol, so a recorded target is never renegotiated. */
|
|
54
|
+
function newSessionTarget(target, io) {
|
|
55
|
+
const protocol = herdrCommand(target, ["api", "snapshot"], io).snapshot?.protocol;
|
|
56
|
+
if (!isSupportedHerdrProtocol(protocol)) throw Object.assign(new Error(`Herdr server speaks protocol ${protocol ?? "unknown"}; OATS supports ${HERDR_SUPPORTED_PROTOCOLS.join(" and ")}`), { unsupportedProtocol: true });
|
|
57
|
+
const selected = { ...target, protocol };
|
|
58
|
+
herdrSnapshot(selected, io);
|
|
59
|
+
return selected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ensureHerdr({ binary = "herdr", socket, session = "oats", io } = {}) {
|
|
63
|
+
const target = { backend: "herdr", binary, socket: resolve(socket || herdrSocket(session)) };
|
|
64
|
+
try { return newSessionTarget(target, io); }
|
|
48
65
|
catch (e) {
|
|
49
66
|
// An explicit socket belongs to an operator-managed server. Never start a
|
|
50
67
|
// different server when it cannot be inspected or is incompatible.
|
|
@@ -59,7 +76,8 @@ export function ensureHerdr({ binary = "herdr", socket, session = "oats" } = {})
|
|
|
59
76
|
let error;
|
|
60
77
|
for (let i = 0; i < 30; i++) {
|
|
61
78
|
pause(100);
|
|
62
|
-
|
|
79
|
+
// A server that answers with an unsupported protocol is up: waiting longer cannot change its answer.
|
|
80
|
+
try { return newSessionTarget(target, io); } catch (e) { if (e.unsupportedProtocol) throw e; error = e; }
|
|
63
81
|
}
|
|
64
82
|
throw new Error(`Herdr did not start: ${error?.message || "no socket"}`);
|
|
65
83
|
}
|
|
@@ -81,8 +99,27 @@ export function inspectHerdr(target, io) {
|
|
|
81
99
|
return { present: !!pane, pane, agent, status: agent?.agent_status || "unknown" };
|
|
82
100
|
}
|
|
83
101
|
|
|
102
|
+
/** Wait until a new pane's shell has drawn something (its prompt) before typing into it. Text typed
|
|
103
|
+
* earlier sits in the terminal's canonical line buffer, which drops a line past 1024 bytes on macOS:
|
|
104
|
+
* a launch command is longer than that, and arrived cut. Bounded; a Herdr that cannot read a pane
|
|
105
|
+
* leaves the old behaviour (type at once). */
|
|
106
|
+
function awaitShellReady(target, io) {
|
|
107
|
+
const deadline = Date.now() + (io?.shellReadyMs ?? 10000);
|
|
108
|
+
for (;;) {
|
|
109
|
+
let text;
|
|
110
|
+
// `pane read` answers the screen as plain text, not a JSON envelope.
|
|
111
|
+
const remaining = Math.max(100, deadline - Date.now());
|
|
112
|
+
try { text = herdrOutput(target, ["pane", "read", target.paneId, "--source", "visible"], { ...io, timeout: Math.min(2000, remaining) }); }
|
|
113
|
+
catch { return; }
|
|
114
|
+
if (typeof text === "string" && text.trim()) return;
|
|
115
|
+
if (Date.now() >= deadline) return;
|
|
116
|
+
pause(100);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
84
120
|
export function launchHerdr(target, command, io) {
|
|
85
121
|
if (!inspectHerdr(target, io).present) throw new Error("Herdr launch pane disappeared");
|
|
122
|
+
awaitShellReady(target, io);
|
|
86
123
|
// This is the allocated shell's one launch command. Exec avoids a fallback
|
|
87
124
|
// shell that could accidentally consume a subsequent agent notification.
|
|
88
125
|
herdrCommand(target, ["pane", "run", target.paneId, `exec /bin/sh -c ${quote(command)}`], io);
|
package/lib/instance-inspect.mjs
CHANGED
|
@@ -221,7 +221,7 @@ function capabilityRows(t) {
|
|
|
221
221
|
return { ...op, argv: [m.command ?? null, op.command], available: !reason, reason };
|
|
222
222
|
});
|
|
223
223
|
// composedFrom (feature desktop-facts): where the soul's composition took it from — "workspace" |
|
|
224
|
-
// "
|
|
224
|
+
// "soul" (layers-from vocabulary); null for a home (not recorded at spawn). `from` is the module's origin.
|
|
225
225
|
return { id: name, version: m.version ?? null, layer: m.layer ?? null, command: m.command ?? null, from, composedFrom: t.capabilitiesFrom?.[name] ?? null, dir,
|
|
226
226
|
settings: obj(t.payloads[name]) ? t.payloads[name] : {}, declares: declaredSettings(m), compatibility: compatibilityRow(name, m), missingRequires: missing, operations };
|
|
227
227
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* its Git remotes in the operator's access context → find the soul among the
|
|
6
6
|
* confirmed members (or external souls) → resolve every capability by `from:`
|
|
7
7
|
* (member = latest state, package = the locked version) → materialize
|
|
8
|
-
* each capability WHOLE into the new home (`.oats/modules/`, `.agents/skills/`) →
|
|
8
|
+
* each capability WHOLE into the new home (`.oats/modules/`, skills flat in `.agents/skills/`) →
|
|
9
9
|
* compose AGENTS.md → launch the harness normally.
|
|
10
10
|
*
|
|
11
11
|
* This module owns the async half (discover + resolve) and the materialize call;
|
|
@@ -24,7 +24,7 @@ import { resolveSoul, kernelCompatibility } from "./resolve.mjs";
|
|
|
24
24
|
import { BY_TEAM_REMOVED, recordedTeams, reportRows, soulKeyOf, soulTeams, teamModel } from "./teams.mjs";
|
|
25
25
|
import { launchLayers } from "./launch-preference.mjs";
|
|
26
26
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
27
|
-
import { materialize, MODULES_DIR, SKILLS_DIR } from "./materialize.mjs";
|
|
27
|
+
import { materialize, moduleSkills, MODULES_DIR, SKILLS_DIR } from "./materialize.mjs";
|
|
28
28
|
import { fetchRemoteTree } from "./remote.mjs";
|
|
29
29
|
import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, symlinkSync, statSync } from "node:fs";
|
|
30
30
|
import { tmpdir } from "node:os";
|
|
@@ -524,9 +524,11 @@ export function toCapabilityRows(resolution, home) {
|
|
|
524
524
|
for (const m of resolution.modules) {
|
|
525
525
|
const dir = join(home, MODULES_DIR, m.name);
|
|
526
526
|
const manifest = m.manifest;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
527
|
+
// Skills are flat under <home>/.agents/skills/<skill>/; which of them this module
|
|
528
|
+
// contributed is read from its own copy, enumerated exactly as materialize placed them.
|
|
529
|
+
const skills = existsSync(dir)
|
|
530
|
+
? moduleSkills(resolution, m, dir).map((s) => join(home, SKILLS_DIR, s.name)).filter((p) => existsSync(p))
|
|
531
|
+
: [];
|
|
530
532
|
const inject = manifest.inject ? join(dir, manifest.inject) : undefined;
|
|
531
533
|
rows.push({
|
|
532
534
|
id: m.name, capability: m.name, manifest, layer: manifest.layer ?? undefined, command: manifest.command,
|
|
@@ -564,8 +566,12 @@ function requiredHooksOf(manifest) {
|
|
|
564
566
|
return Object.entries(hooks).filter(([, s]) => s && typeof s === "object" && s.required === true).map(([e]) => e);
|
|
565
567
|
}
|
|
566
568
|
|
|
567
|
-
/** What a preview shows about modules: from/commit/digest per module
|
|
568
|
-
* it changed since the newest existing instance of the same soul in `agentsRoot
|
|
569
|
+
/** What a preview shows about modules: from/commit/digest per module, whether
|
|
570
|
+
* it changed since the newest existing instance of the same soul in `agentsRoot`,
|
|
571
|
+
* and `composedFrom` (feature preview-composed-from): why the soul's composition
|
|
572
|
+
* has it — "soul" | "workspace", the resolution's capabilitiesFrom. Provenance
|
|
573
|
+
* only: it is outside every fingerprint. `null` only for a resolution without
|
|
574
|
+
* capabilitiesFrom (an older in-process caller); never for a resolved module. */
|
|
569
575
|
export function modulesPreview(resolution, agentsRoot, soulName) {
|
|
570
576
|
let previous = null;
|
|
571
577
|
const instancesDir = join(agentsRoot, soulName, "instances");
|
|
@@ -583,7 +589,8 @@ export function modulesPreview(resolution, agentsRoot, soulName) {
|
|
|
583
589
|
return resolution.modules.map((m) => {
|
|
584
590
|
const prev = previous?.modules?.[m.name];
|
|
585
591
|
const changedSince = !previous ? null : !prev ? { instance: previous.name, was: null } : (prev.commit !== m.from.commit ? { instance: previous.name, was: prev.commit } : false);
|
|
586
|
-
return { name: m.name, from: m.from, layer: m.manifest.layer ?? null, private: !!m.private, declares: declaredSettings(m.manifest), changedSince
|
|
592
|
+
return { name: m.name, from: m.from, layer: m.manifest.layer ?? null, private: !!m.private, declares: declaredSettings(m.manifest), changedSince,
|
|
593
|
+
composedFrom: resolution.capabilitiesFrom?.[m.name] ?? null };
|
|
587
594
|
});
|
|
588
595
|
}
|
|
589
596
|
|
package/lib/materialize.mjs
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* materialize(resolution, home, options)
|
|
7
7
|
* For every module of the resolution: fetch its capability directory whole into
|
|
8
8
|
* <home>/.oats/modules/<name>/, verify the copy's content digest, copy its skills as
|
|
9
|
-
* FULL copies into <home>/.agents/skills/<
|
|
9
|
+
* FULL copies into <home>/.agents/skills/<skill>/ — FLAT, one level deep, where every
|
|
10
|
+
* harness discovers them (Claude Code through .claude/skills/<skill>/); skill names are
|
|
11
|
+
* unique across the modules (E_SKILL_DUPLICATE) — compose <home>/AGENTS.md
|
|
10
12
|
* (soul body + each module's inject, same marker comments as the kernel composer),
|
|
11
13
|
* keep the CLAUDE.md / .claude/skills aliases, and record modules + providers in
|
|
12
14
|
* <home>/instance.json.
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
* place. Any failure before the commit removes the staging directory and leaves the home
|
|
17
19
|
* exactly as it was (a `.oats/` directory created only for staging is removed too). The
|
|
18
20
|
* commit re-checks the home's shape (no symlink planted at .oats/.agents/.claude or the
|
|
19
|
-
* module targets during the fetch), then runs a short sequence of renames — modules,
|
|
21
|
+
* module and skill targets during the fetch), then runs a short sequence of renames — modules,
|
|
20
22
|
* skills, AGENTS.md, instance.json, and the CLAUDE.md / .claude/skills aliases — with a
|
|
21
23
|
* rollback journal: should any step fail, everything already placed is undone and the
|
|
22
24
|
* previous AGENTS.md / instance.json restored before a named error (E_MATERIALIZE_HOME)
|
|
@@ -205,7 +207,7 @@ function hasSkillDoc(dir) {
|
|
|
205
207
|
/** The skills a module contributes → [{ name, path }] (path relative to the module root).
|
|
206
208
|
* Precedence: resolution.skills rows for the module; else manifest.skills; else every
|
|
207
209
|
* skills/<dir>/SKILL.md in the fetched tree. */
|
|
208
|
-
function moduleSkills(resolution, module, moduleDir) {
|
|
210
|
+
export function moduleSkills(resolution, module, moduleDir) {
|
|
209
211
|
const declared = (resolution.skills || []).filter((s) => s && s.module === module.name);
|
|
210
212
|
const rows = [];
|
|
211
213
|
if (declared.length) {
|
|
@@ -335,13 +337,22 @@ export async function materialize(resolution, home, options = {}) {
|
|
|
335
337
|
const parents = [join(homeAbs, ".oats"), modulesRoot, join(homeAbs, ".agents"), skillsRoot, join(homeAbs, ".claude")];
|
|
336
338
|
/** The home must still be what we checked: module targets absent, every parent a real directory (or absent).
|
|
337
339
|
* Run before the fetch AND immediately before the commit — a symlink planted in between must not be written through. */
|
|
340
|
+
// Skill targets are known only once each module is fetched; the commit-time re-check covers them.
|
|
341
|
+
// skill name, lower-cased → { module, path, name }: one directory per skill, and on a
|
|
342
|
+
// case-insensitive filesystem `Foo` and `foo` are one directory.
|
|
343
|
+
const skillOwners = new Map();
|
|
338
344
|
const assertHomeShape = () => {
|
|
339
345
|
for (const { module } of sources) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
346
|
+
const target = join(modulesRoot, module.name);
|
|
347
|
+
let st = null;
|
|
348
|
+
try { st = lstatSync(target); } catch {}
|
|
349
|
+
if (st) throw fail("E_MATERIALIZE_HOME", `${target} already exists — the instance already carries module ${module.name}; materialize never overwrites a module in place`, { home: homeAbs, module: module.name, path: target });
|
|
350
|
+
}
|
|
351
|
+
for (const owner of skillOwners.values()) {
|
|
352
|
+
const name = owner.name, target = join(skillsRoot, name);
|
|
353
|
+
let st = null;
|
|
354
|
+
try { st = lstatSync(target); } catch {}
|
|
355
|
+
if (st) throw fail("E_MATERIALIZE_HOME", `${target} already exists — the instance already carries a skill named ${name}; materialize never overwrites a skill in place`, { home: homeAbs, module: owner.module, skill: name, path: target });
|
|
345
356
|
}
|
|
346
357
|
for (const p of parents) {
|
|
347
358
|
let st = null;
|
|
@@ -378,7 +389,6 @@ export async function materialize(resolution, home, options = {}) {
|
|
|
378
389
|
mkdirSync(join(staging, "skills"), { recursive: true, mode: 0o755 });
|
|
379
390
|
|
|
380
391
|
const moduleRows = [], skillRows = [], capabilityBlocks = [], recorded = {};
|
|
381
|
-
const skillOwners = new Map();
|
|
382
392
|
for (const { module, source } of sources) {
|
|
383
393
|
const stagedModule = join(staging, "modules", module.name);
|
|
384
394
|
const finalModule = join(modulesRoot, module.name);
|
|
@@ -412,15 +422,21 @@ export async function materialize(resolution, home, options = {}) {
|
|
|
412
422
|
}
|
|
413
423
|
|
|
414
424
|
for (const skill of moduleSkills(resolution, module, stagedModule)) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
425
|
+
// Skills land flat, so a name is one directory in the home: two sources for one
|
|
426
|
+
// name — two modules, or two paths of one module — are a duplicate.
|
|
427
|
+
const key = skill.name.toLowerCase();
|
|
428
|
+
const owner = skillOwners.get(key);
|
|
429
|
+
if (owner && owner.module === module.name && owner.path === skill.path && owner.name === skill.name) continue;
|
|
430
|
+
if (owner) {
|
|
431
|
+
const detail = owner.module === module.name ? `${module.name} (${owner.path} and ${skill.path})` : `both ${owner.module} and ${module.name}`;
|
|
432
|
+
const names = owner.name === skill.name ? JSON.stringify(skill.name) : `${JSON.stringify(owner.name)} / ${JSON.stringify(skill.name)} (one directory on a case-insensitive filesystem)`;
|
|
433
|
+
throw fail("E_SKILL_DUPLICATE", `skill ${names} is contributed by ${detail}`, { name: skill.name, modules: [owner.module, module.name] });
|
|
418
434
|
}
|
|
419
|
-
skillOwners.set(
|
|
435
|
+
skillOwners.set(key, { module: module.name, path: skill.path, name: skill.name });
|
|
420
436
|
const src = join(stagedModule, ...skill.path.split("/"));
|
|
421
|
-
const staged = join(staging, "skills",
|
|
437
|
+
const staged = join(staging, "skills", skill.name);
|
|
422
438
|
copyTree(src, staged, `${module.name}/${skill.path}`);
|
|
423
|
-
skillRows.push({ module: module.name, name: skill.name, from: `${MODULES_DIR}/${module.name}/${skill.path}`.split(sep).join("/"), path: join(skillsRoot,
|
|
439
|
+
skillRows.push({ module: module.name, name: skill.name, from: `${MODULES_DIR}/${module.name}/${skill.path}`.split(sep).join("/"), path: join(skillsRoot, skill.name) });
|
|
424
440
|
}
|
|
425
441
|
|
|
426
442
|
for (const rel of moduleInjects(resolution, module, stagedModule)) {
|
|
@@ -478,10 +494,8 @@ export async function materialize(resolution, home, options = {}) {
|
|
|
478
494
|
};
|
|
479
495
|
try {
|
|
480
496
|
for (const p of [join(homeAbs, ".oats"), modulesRoot, join(homeAbs, ".agents"), skillsRoot, join(homeAbs, ".claude")]) mkdirTracked(p);
|
|
481
|
-
for (const { module } of sources)
|
|
482
|
-
|
|
483
|
-
if (existsSync(join(staging, "skills", module.name))) placeDir(join(staging, "skills", module.name), join(skillsRoot, module.name));
|
|
484
|
-
}
|
|
497
|
+
for (const { module } of sources) placeDir(join(staging, "modules", module.name), join(modulesRoot, module.name));
|
|
498
|
+
for (const row of skillRows) placeDir(join(staging, "skills", row.name), row.path);
|
|
485
499
|
swapIn(join(staging, "AGENTS.md"), join(homeAbs, "AGENTS.md"), "AGENTS.md");
|
|
486
500
|
swapIn(join(staging, "instance.json"), instanceFile, "instance.json");
|
|
487
501
|
// Aliases (relative symlinks), only when absent — the canonical-plus-alias construction stays; inside the transaction.
|
package/package-catalog.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.3",
|
|
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,7 +60,7 @@ 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.
|
|
63
|
+
oats.framework: v1.4.1 # bare versions resolve through the official catalog
|
|
64
64
|
oats.okf: v4.0.5
|
|
65
65
|
defaults:
|
|
66
66
|
capabilities: { oats.core: { from: package } }
|