@awebai/oats 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/oats.mjs CHANGED
@@ -36,9 +36,8 @@ import {
36
36
  } from "../lib/core.mjs";
37
37
  import {
38
38
  assertNoSymlinkedParents, writeFileAtomic,
39
- LOCK_FILE, readLock, writeLock, resolvePackages, approve as approvePackage, executablesDigest, readPackageTree,
40
- classifyPackageValue, manifestExecutables, parsePackageRequest,
41
- } from "../lib/packages.mjs";
39
+ LOCK_FILE, readLock, writeLock, resolvePackages, approve as approvePackage,
40
+ classifyPackageValue, parsePackageRequest, executablesDigestAt } from "../lib/packages.mjs";
42
41
  import { loadLocal, discoverWorkspace, validateWorkspace } from "../lib/workspace.mjs";
43
42
  import * as remoteModule from "../lib/remote.mjs";
44
43
  import { createInterface } from "node:readline/promises";
@@ -1941,10 +1940,12 @@ function printTable(header, rows) {
1941
1940
 
1942
1941
  /** Executables digest of a locked package, read over the remote at its locked commit. */
1943
1942
  async function lockedExecutablesDigest(id, entry, workspace, catalog, remoteOptions) {
1943
+ // ONE definition of the approval digest (lib/packages.mjs executablesDigestAt) — the
1944
+ // same function resolveSoul re-runs at spawn (M3), so sync and spawn can never disagree.
1944
1945
  const req = parsePackageRequest(id, workspace.packages[id], catalog);
1945
- const tree = await readPackageTree(remoteModule, req.remoteRef, entry.commit, entry.path, { remoteOptions });
1946
- const targets = tree.manifests.flatMap((m) => manifestExecutables(m.manifest).map((x) => `${m.name}: ${x.kind} ${x.name} → ${x.target}`));
1947
- return { digest: executablesDigest(tree), targets };
1946
+ const { digest, executables } = await executablesDigestAt(remoteModule, req.remoteRef, entry.commit, entry.path, entry.capabilities ?? null, { remoteOptions });
1947
+ const targets = executables.map((x) => `${x.capability}: ${x.kind} ${x.name} → ${x.target}`);
1948
+ return { digest, targets };
1948
1949
  }
1949
1950
 
1950
1951
  /** One yes/no question on the terminal (TTY only; the caller checks). */
@@ -3001,18 +3002,27 @@ function createCmd() {
3001
3002
  * oats <namespace> <command> [args…] — run a command an active capability
3002
3003
  * declares in its manifest (`commands: { name: "script args" }`).
3003
3004
  * Kernel subcommands take precedence over capability namespaces.
3005
+ *
3006
+ * Three contexts, one contract (OATS_CAPABILITY / OATS_SETTINGS / OATS_CLI_BIN):
3007
+ * - inside an instance home: the home's materialized modules (instance.json.modules);
3008
+ * - from a v2 DEPLOYMENT (oats-local.yaml in reach, no home): operator-level
3009
+ * dispatch — resolve exactly as `oats spawn --soul <x>` would, fetch the
3010
+ * namespace's capability into <deployment>/.oats/modules/<cap>@<commit12>/
3011
+ * and run THAT copy with the soul's merged payload (lib/operator-dispatch.mjs;
3012
+ * contracts doc, "Post-0.25.0 clarifications");
3013
+ * - otherwise the classic config chain.
3004
3014
  */
3005
- function capabilityCommand() {
3015
+ async function capabilityCommand() {
3006
3016
  // JSON-aware boundary: in --json mode every dispatch failure — inactive or
3007
3017
  // untrusted capability, duplicate namespace, unknown subcommand, broken
3008
3018
  // metadata/manifests, malformed command values — must still emit exactly
3009
3019
  // one envelope object on stdout. The WHOLE dispatcher runs inside the
3010
3020
  // boundary; only "no namespace matched" escapes (returns false to the help
3011
3021
  // fallthrough).
3012
- const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
3022
+ const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
3013
3023
  const NOT_DISPATCHED = Symbol("not-dispatched");
3014
3024
  let outcome;
3015
- try { outcome = dispatch(); }
3025
+ try { outcome = await dispatch(); }
3016
3026
  catch (e) {
3017
3027
  // Unexpected throw from discovery/trust/decoding: keep the envelope contract.
3018
3028
  bail("E_CAPABILITY_BROKEN", e.message || e);
@@ -3020,7 +3030,23 @@ function capabilityCommand() {
3020
3030
  }
3021
3031
  return outcome !== NOT_DISPATCHED;
3022
3032
 
3023
- function dispatch() {
3033
+ /** Operator-level dispatch from a deployment directory (no instance home). */
3034
+ async function operatorDispatch() {
3035
+ let hit;
3036
+ try {
3037
+ const { resolveOperatorDispatch } = await import("../lib/operator-dispatch.mjs");
3038
+ let catalog = null; try { catalog = officialPackageCatalog(); } catch { /* the lock carries url for catalog packages */ }
3039
+ hit = await resolveOperatorDispatch(process.cwd(), cmd, flag("soul"), { remoteOptions: remoteOptionsFromEnv(), catalog });
3040
+ } catch (e) {
3041
+ if (typeof e?.code === "string" && e.code.startsWith("E_")) bail(e.code, e.message, e.details);
3042
+ throw e;
3043
+ }
3044
+ if (!hit) return NOT_DISPATCHED;
3045
+ const teamCtx = hit.soul?.team ? { name: hit.soul.team } : undefined;
3046
+ return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, hit.settings, teamCtx, hit.ensureTree);
3047
+ }
3048
+
3049
+ async function dispatch() {
3024
3050
  let activeIds;
3025
3051
  let context = process.cwd();
3026
3052
  let teamCtx;
@@ -3032,6 +3058,7 @@ function capabilityCommand() {
3032
3058
  // namespace the operator typed on the command line.
3033
3059
  let capSettings = Object.create(null);
3034
3060
  let instanceModules = false;
3061
+ let deployment = null;
3035
3062
  try {
3036
3063
  if (metaFile && existsSync(metaFile)) {
3037
3064
  const meta = JSON.parse(readFileSync(metaFile, "utf8"));
@@ -3043,12 +3070,19 @@ function capabilityCommand() {
3043
3070
  // spawned before a team: block was declared have no snapshot.
3044
3071
  teamCtx = meta.team || resolveOatsConfig(context).team;
3045
3072
  } else {
3046
- const resolved = resolveOatsConfig(context, flag("soul"));
3047
- activeIds = resolved.capabilities.map((c) => c.id);
3048
- for (const c of resolved.capabilities) capSettings[c.id] = c.settings || {};
3049
- teamCtx = resolved.team;
3073
+ // Not inside a home: a v2 deployment (oats-local.yaml in reach) resolves
3074
+ // through the workspace, exactly as a spawn of --soul would (below).
3075
+ try { const { deploymentOf } = await import("../lib/operator-dispatch.mjs"); deployment = deploymentOf(context); }
3076
+ catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) bail(e.code, e.message, e.details); throw e; }
3077
+ if (!deployment) {
3078
+ const resolved = resolveOatsConfig(context, flag("soul"));
3079
+ activeIds = resolved.capabilities.map((c) => c.id);
3080
+ for (const c of resolved.capabilities) capSettings[c.id] = c.settings || {};
3081
+ teamCtx = resolved.team;
3082
+ }
3050
3083
  }
3051
3084
  } catch (e) { bail("E_CONFIG_BROKEN", e.message || e); throw e; }
3085
+ if (deployment) return operatorDispatch();
3052
3086
  // Workspace model: an instance's own materialized modules are the command
3053
3087
  // namespaces available to it (instance.json.modules → <home>/.oats/modules).
3054
3088
  const mans = Object.values(capabilityManifests(instanceModules ? instanceHome : context)).filter((m) => m.command === cmd && m.commands);
@@ -3058,6 +3092,14 @@ function capabilityCommand() {
3058
3092
  if (!activeIds.includes(m.capability)) bail("E_CAPABILITY_INACTIVE", `${m.capability} command namespace is not active in the current context/instance`);
3059
3093
  const trust = capabilityTrust(m, context);
3060
3094
  if (!trust.trusted) bail("E_CAPABILITY_BLOCKED", `${m.capability} executable command is blocked: ${trust.reason}`);
3095
+ return runManifestCommand(m, capSettings[m.capability] || {}, teamCtx, () => m._dir);
3096
+ }
3097
+
3098
+ /** Help / unknown-command / spec validation / exec — shared by every context.
3099
+ * `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
3100
+ * resolves the directory holding the executable — the operator branch fetches
3101
+ * the module tree only when a command is actually going to run). */
3102
+ async function runManifestCommand(m, settings, teamCtx, ensureDir) {
3061
3103
  const sub = args[1];
3062
3104
  const cmds = Object.keys(m.commands);
3063
3105
  // `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
@@ -3083,17 +3125,22 @@ function capabilityCommand() {
3083
3125
  const spec = m.commands[sub];
3084
3126
  if (typeof spec !== "string" || !spec.trim()) bail("E_CAPABILITY_BROKEN", `oats ${cmd} ${sub}: manifest command must be a non-empty string (got ${JSON.stringify(spec)})`);
3085
3127
  const [script, ...rest] = spec.trim().split(/\s+/);
3128
+ let dir;
3129
+ try { dir = await ensureDir(); }
3130
+ catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) bail(e.code, e.message, e.details); throw e; }
3131
+ const withDir = { ...m, _dir: dir };
3086
3132
  let abs;
3087
- try { abs = capabilityExecutablePath(m, script); }
3133
+ try { abs = capabilityExecutablePath(withDir, script); }
3088
3134
  catch (e) { bail("E_CAPABILITY_BROKEN", e.message); }
3089
- if (!abs) bail("E_CAPABILITY_BROKEN", `${cmd} ${sub}: script not found (${join(m._dir, script)})`);
3135
+ if (!abs) bail("E_CAPABILITY_BROKEN", `${cmd} ${sub}: script not found (${join(dir, script)})`);
3090
3136
  const r = spawnSync("node", [abs, ...rest, ...args.slice(2)], { stdio: "inherit", env: {
3091
3137
  ...process.env, OATS_CAPABILITY: m.capability,
3092
3138
  // Package-runtime boundary: dispatched commands receive the active
3093
- // capability's EFFECTIVE settings (instance snapshot or resolved context),
3094
- // same contract as lifecycle hooks — capabilities read their settings
3095
- // here instead of importing the kernel resolver.
3096
- OATS_SETTINGS: JSON.stringify(capSettings[m.capability] || {}),
3139
+ // capability's EFFECTIVE settings (instance snapshot, resolved context, or
3140
+ // the soul's merged payload on operator-level dispatch), same contract as
3141
+ // lifecycle hooks — capabilities read their settings here instead of
3142
+ // importing the kernel resolver.
3143
+ OATS_SETTINGS: JSON.stringify(settings || {}),
3097
3144
  // PATH is not a trusted runtime boundary (maintainer finding 1): pass the
3098
3145
  // canonical absolute executable of THIS CLI; official consumers execFile
3099
3146
  // it directly and never resolve `oats` from PATH or a shell.
@@ -3675,7 +3722,7 @@ else if (cmd && Object.hasOwn(REMOVED_VERBS, cmd)) {
3675
3722
  console.log(usageText());
3676
3723
  process.exit(1);
3677
3724
  }
3678
- else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && capabilityCommand()) { /* dispatched */ }
3725
+ else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && await capabilityCommand()) { /* dispatched */ }
3679
3726
  // No matching kernel command or capability namespace: in --json mode the help
3680
3727
  // text must NOT contaminate stdout — still one envelope object, nonzero exit.
3681
3728
  else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && JSON_MODE) jsonFail("E_UNKNOWN_COMMAND", `unknown command "${cmd}" — no kernel subcommand or active capability namespace matches`);
@@ -31,43 +31,70 @@ instance/.claude/skills -> ../.agents/skills
31
31
 
32
32
  Spawn copies kernel + soul-private + active capability skills into real
33
33
  instance-local directories there. Directory symlinks are not used because
34
- harness recursive discovery may not descend through them. Packages retain
35
- skills in their own artifact; activation selects them for materialization. Config-level `.agents/skills` is not an OATS capability source
34
+ harness recursive discovery may not descend through them. Under the workspace
35
+ model (0.25) every capability is copied whole into
36
+ `instance/.oats/modules/<capability>/` and its skills into
37
+ `instance/.agents/skills/<capability>/<skill>/`; nothing is installed at a
38
+ config level. Config-level `.agents/skills` is not an OATS capability source
36
39
  or an ambient runtime discovery root.
37
40
 
38
- Pi starts spawned sessions with ambient skill and context discovery disabled
39
- and the one instance path explicit; its globally configured extensions remain
40
- enabled. Claude runs provider-native: it reads the instance's `.claude/skills`
41
- and `CLAUDE.md` symlinks, and the operator's own user and project
42
- configuration — skills, plugins, settings — stays in effect. Neither runtime
43
- gets a redirected config home. `composition.materialized.runtimePosture` in
44
- `instance.json` records what each instance actually exposes.
45
- `oats-getting-started` is the sole pre-workspace ambient bootstrap.
46
-
47
- Duplicate skill directory names are errors unless config's `skill-overrides`
48
- selects a source.
41
+ Harnesses start normally (workspace-model decision 13, and the behaviour of
42
+ every launch a 0.25 kernel performs, classic homes included): Pi starts with
43
+ cwd = the instance home, the composed `AGENTS.md` appended to its system
44
+ prompt, and its own skill, context and extension discovery intact — the
45
+ instance's copied skills are found because they sit under cwd; machine-level
46
+ and repo-level skills resolve exactly as without OATS. *(0.24 kernels started
47
+ Pi with ambient skill and context discovery disabled and the one instance path
48
+ explicit; that exclusion is gone.)* Claude runs provider-native: it reads the
49
+ instance's `.claude/skills` and `CLAUDE.md` symlinks, and the operator's own
50
+ user and project configuration — skills, plugins, settings — stays in effect.
51
+ Neither runtime gets a redirected config home.
52
+ `composition.materialized.runtimePosture` in `instance.json` records what each
53
+ instance actually exposes. `oats-getting-started` is the sole pre-workspace
54
+ ambient bootstrap.
55
+
56
+ Duplicate skill directory names within the OATS-composed set are errors
57
+ (`E_SKILL_DUPLICATE`, naming both capabilities) unless the classic config's
58
+ `skill-overrides` selects a source; between a composed skill and an ambient
59
+ repo/machine skill the harness's own precedence decides (decision 16).
49
60
 
50
61
  ## Package locations
51
62
 
63
+ **Workspace model (0.25, current):** nothing is installed. A capability lives
64
+ where its owner keeps it and is copied whole into each instance at spawn:
65
+
66
+ ```text
67
+ <member repo>/capabilities/<name>/oats.json # member-tier capability, latest state (membership is the trust)
68
+ <package repo>/oats-package/oats-package.json # package-tier: versioned via oats-workspace.yaml packages:
69
+ <deployment>/oats-lock.json # lockfileVersion 3: package commit, integrity, per-version approval
70
+ <instance>/.oats/modules/<capability>/ # the copy this instance runs
71
+ ```
72
+
73
+ **Classic 0.24 layout** (still launched by the 0.24 kernel; a 0.25 kernel
74
+ reads none of it as configuration — see [rebuild-to-v2.md](rebuild-to-v2.md)):
75
+
52
76
  ```text
53
77
  <package>/capabilities/<name>/oats.json # the official marketplace (install source, not ambient)
54
78
  <level>/.agents/capabilities/installed/<name>/oats.json # acquired (gitignored, restorable)
55
79
  <level>/.agents/capabilities/owned/<name>/oats.json # authored at this scope (source; committed where the scope is a repo)
56
- <level>/oats-lock.json # external source/integrity/trust
80
+ <level>/oats-lock.json # lockfileVersion 2: external source/integrity/trust
57
81
  ```
58
82
 
59
83
  ## Quick map
60
84
 
61
- | Thing | Canonical location |
62
- |---|---|
63
- | Config | `<level>/oats-config.yaml` |
64
- | Acquisition lock | `<level>/oats-lock.json` |
65
- | Soul operating doc | `soul/AGENTS.md` |
66
- | Soul Claude view | `soul/CLAUDE.md -> AGENTS.md` |
67
- | Soul-private skills | `soul/skills/` |
68
- | Instance operating doc | `instance/AGENTS.md` (generated) |
69
- | Instance skill set | `instance/.agents/skills/` |
70
- | Instance metadata | `instance/instance.json` |
85
+ | Thing | Canonical location (0.25 workspace model) | 0.24 classic |
86
+ |---|---|---|
87
+ | Shared declaration | `oats-workspace.yaml` in the host repo; `oats-membership.yaml` in every member | `oats-config.yaml` chain, `oats.yaml` |
88
+ | Per-machine config | `<deployment>/oats-local.yaml` (uncommitted) | `oats-config.yaml` `settings:` |
89
+ | Acquisition lock | `<deployment>/oats-lock.json` (v3) | `<level>/oats-lock.json` (v2) |
90
+ | Soul source | `<member repo>/souls/<name>/` | `agents/<name>/soul/` |
91
+ | Soul operating doc | `souls/<name>/AGENTS.md` | `soul/AGENTS.md` |
92
+ | Soul Claude view | `souls/<name>/CLAUDE.md -> AGENTS.md` | `soul/CLAUDE.md -> AGENTS.md` |
93
+ | Soul-private skills | `souls/<name>/skills/` | `soul/skills/` |
94
+ | Instance operating doc | `instance/AGENTS.md` (generated) | same |
95
+ | Instance skill set | `instance/.agents/skills/` | same |
96
+ | Instance modules | `instance/.oats/modules/<capability>/` | `.agents/capabilities/installed/` (shared) |
97
+ | Instance metadata | `instance/instance.json` (`modules{}`, `providers{}`, `workspace{}`) | `instance/instance.json` |
71
98
 
72
99
  Symlinks prevent compatibility paths from drifting. Generated regular files
73
100
  separate canonical portable identity from scope-dependent runtime policy.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
4
4
 
5
- **Last update:** 2026-09-23 17:40Z · **Workspace model v2 phases A–C ON MAIN (`59ae22df`, PR99) → `v0.25.0` tagged (release run in flight)** · Phase D next (framework repos as the first workspace; six expert souls; `oats.core`/`oats.setup` rewrite) · ⏸ parity pipeline still paused except 10B-0 (→ 0.24.14 when the engineer hands off; note: 0.24.14 must be cut from the 0.24 line, not main).
5
+ **Last update:** 2026-09-23 17:40Z · **Workspace model v2 phases A–C ON MAIN (`59ae22df`, PR99) → **`v0.25.0` PUBLISHED** (bump `dc333e4e`)** · Phase D next (framework repos as the first workspace; six expert souls; `oats.core`/`oats.setup` rewrite) · ⏸ parity pipeline still paused except 10B-0 (→ 0.24.14 when the engineer hands off; note: 0.24.14 must be cut from the 0.24 line, not main).
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -510,7 +510,7 @@ Today's `capabilities.layers.messaging.{capability, from, global, souls, setting
510
510
  |---|---|---|
511
511
  | True of every instance of the soul | `soul.yaml` → `messaging:` / `knowledge:` | `messaging: { channels: [northwind-eng] }`, `knowledge: { owns: release-manager }` |
512
512
  | A fact about this machine | `oats-local.yaml` → `settings.<capability>.<key>` (absolute paths refused in the workspace file) | `settings.oats.okf.state-dir: /Users/ana/.oats/okf` |
513
- | A fact about **this spawn** | `oats spawn … --provider <cap> key=value` → `instance.json` `providers.<cap>` (the Desktop's confirmed apply carries the same map) | `--provider oats.aweb identity.source=retained:release-seat` — one instance takes the retained seat; other instances of the soul mint fresh |
513
+ | A fact about **this spawn** | `oats spawn … --provider <cap> key=value` → `instance.json` `providers.<cap>` (the Desktop's confirmed apply carries the same map) | `--provider oats.aweb identity.source=/abs/path/to/retained/.aw` — one instance takes the retained seat; other instances of the soul mint fresh |
514
514
 
515
515
  The provider's `binding` contract (`normalize → bind → check`) runs over the merged payload exactly as today; the provider still enforces its own rules (e.g. a state root outside the work tree).
516
516
 
@@ -307,3 +307,154 @@ Appended, not edited in place; each item names the section it refines. Decision
307
307
  **§6 `oats package remove <id>` (M15).** BOTH branches — the file tracked by the checkout (edited) and untracked/absent (not edited) — answer `E_PACKAGE_MISSING { id, path? }` when `<id>` is not declared in `packages:`. The tracked receipt is `{ action: "remove", id, value: null, previous: <old value>, edited: true, file }`; the untracked receipt is `{ action: "remove", id, value: null, edited: false, file: null, line: null, hint }` — `previous` is absent and `line` is `null` (a removal has no line to add).
308
308
 
309
309
  **§6 features.** `catalog` is no longer advertised in `oats version --json` `features` (the verb is removed; the official catalog is reached through `packages:` + `sync`, not a command).
310
+
311
+ ## Post-0.25.0 clarifications (team review, 2026-09-23)
312
+
313
+ **Capability commands outside an instance home (`oats <ns> <cmd>` from the
314
+ deployment).** Inside an instance home the dispatcher resolves the namespace from
315
+ the home's materialized modules (`instance.json.modules` → `<home>/.oats/modules`);
316
+ that shipped in 0.25.0. Outside a home — the operator acts a knowledge layer needs
317
+ before any instance exists (`oats okf init`, base migration) — the intended rule
318
+ is: **resolve exactly as a spawn of `--soul <name>` would** (`prepareInstance` →
319
+ the soul's Resolution), fetch the namespace's capability into the deployment's
320
+ module store `<deployment>/.oats/modules/<cap>@<commit>/` (the same per-commit
321
+ store capability-defined agents use), and dispatch to that copy with the soul's
322
+ merged payload as `OATS_SETTINGS`. Never "the newest instance's copy" (an
323
+ instance is not an authority for the deployment) and never an unlocked cache
324
+ read (the lock's approval is the gate, as for spawn). `--soul` is required when
325
+ the namespace's capability is not a workspace default. **Status: 0.25.x
326
+ follow-up** — 0.25.0 still answers `E_CAPABILITY_INACTIVE` there (the pre-v2
327
+ chain); the interim is to run the module binary directly with `OATS_SETTINGS`
328
+ and `OATS_CLI_BIN`, as the tarball smoke does.
329
+
330
+ **`work: workspace` is kept.** A coordination soul's `./work` is the deployment
331
+ boundary — the directory holding `oats-local.yaml` (the taught
332
+ `<name>-workspace/`, with member clones beside it) — read-only across member
333
+ clones, no branch recorded. The clone map in `oats-local.yaml` (`clones:`) is
334
+ how such a soul finds a member whose clone is elsewhere. **Status: the
335
+ directory link is the intent; 0.25.0's kernel still derives the boundary from
336
+ the classic `team:` scope (`docs/souls-and-instances.md` open thread) — 0.25.x
337
+ follow-up binds it to the `oats-local.yaml` directory.**
338
+
339
+ **`identity.source` (oats.aweb) is the absolute path of the `.aw` directory to
340
+ retain**, given per spawn (`--provider oats.aweb identity.source=/abs/.aw`) or
341
+ per machine (`oats-local.yaml settings.oats.aweb.identity.source`); the kernel
342
+ resolves no symbolic seat names. Absolute paths never enter the workspace file
343
+ (decision 14).
344
+
345
+ **Member capabilities are a code-execution boundary** (decision 2: membership is
346
+ the trust; hooks and scripts of every member's default branch run on every
347
+ operator's machine at spawn). For a mixed public/private organisation the
348
+ recommendation is: **souls only in public members; executable capabilities come
349
+ from packages (approved per version) or from private members.** The onboarding
350
+ skill (Phase E) states this beside the hosting rule (decision 26).
351
+
352
+
353
+ ### 0.25.1 fix round (team review, 2026-09-23) — appended, not edited in place
354
+
355
+ Each item names the section it refines and the review finding it closes. The
356
+ implementation lands in kernel 0.25.1 (`docs/release-notes/v0.25.1.md`); no
357
+ API integer or feature name changes.
358
+
359
+ **§3 slot `none` (L1).** A soul's `none` for a slot **empties the slot and drops
360
+ any layer-bearing capability the WORKSPACE DEFAULTS contributed for that
361
+ layer** — whether it arrived through `defaults.<slot>`, `defaults.capabilities`
362
+ or `defaults.byTeam[team].capabilities`. A layer-bearing capability **the soul
363
+ itself declares** in its own `capabilities:` alongside `none` for that layer is
364
+ `E_SLOT_CONFLICT { reason: "none" }` (spell `<cap>: off` to remove it). This
365
+ replaces the Phase B reading under which a layered default arriving via
366
+ `defaults.capabilities` was itself a conflict: the workspace's choice is a
367
+ default and `none` is the soul's answer to it; only the soul contradicting
368
+ itself is loud.
369
+
370
+ **§2/§5 per-commit soul cache (M1).** `ensureWorkspaceSoul` fetches a soul's
371
+ source at `(repoKey, commit)` into `<deployment>/agents/<name>/souls/<commit12>/`
372
+ — **immutable once written** (staged, then renamed in; never removed by the
373
+ kernel) — and maintains `<deployment>/agents/<name>/soul` as a **symlink to the
374
+ current commit's directory**, swapped atomically (symlink to a temp name +
375
+ rename over) so classic readers (`findAgent`, `doctor`, the classic spawn path)
376
+ keep seeing "current". A spawned home's `<home>/soul` links **its own commit's
377
+ directory** (the realpath of `souls/<commit12>/`), never the swapped pointer:
378
+ a running instance's soul never changes under it (decision 7), a `--preview`
379
+ may fetch a new commit and swap the pointer without touching any directory an
380
+ instance links, and OKF 2's owner pin (`owners.json` = `realpath(<home>/soul)`)
381
+ stays valid for the instance that registered it. `.oats-soul-source.json`
382
+ remains the stamp of "current". A 0.25.0 layout (`agents/<name>/soul` a real
383
+ directory, no `souls/`) is migrated in place on first use: the directory moves
384
+ to `souls/<commit from the stamp, else unknown>/` and the pointer replaces it.
385
+ A soul symlink whose target lies inside the same `agents/<name>/souls/` is the
386
+ one kernel-owned symlink soul readers accept.
387
+
388
+ **§1 transport (M2).** `parseRepoRef(ref).key` is unchanged — `<host>/<path>`
389
+ is the identity everywhere and every comparison is by key. The **fetch url
390
+ honours the form written**: `git@host:org/repo(.git)` and `ssh://…` fetch over
391
+ SSH as written; `https://…` fetches over HTTPS; the bare scheme
392
+ `git:host/org/repo` fetches over HTTPS by default **unless
393
+ `remoteOptions.transport === "ssh"`** (a per-machine choice; `oats-local.yaml`
394
+ may carry it once the schema admits it — reported by lane 3, not landed here).
395
+ The operator's SSH access is therefore used when the operator wrote an SSH ref,
396
+ and a private repo no longer degrades to `not-found` → standalone through an
397
+ unintended HTTPS probe. When the standalone fallback engages the discovery
398
+ carries `standaloneReason`/`hostFailure { code, reason, url }` so the CLI can
399
+ print *why*.
400
+
401
+ **§3/§4 approval re-verified at spawn (M3).** For a `from: package` module
402
+ `resolveSoul` recomputes `executablesDigest` over the package tree **at the
403
+ locked `entry.commit`** and requires equality with `entry.approved.executables`
404
+ → else `E_PACKAGE_UNAPPROVED { reason: "digest-mismatch", approved, actual }`.
405
+ The one digest definition is `lib/packages.mjs#executablesDigestAt(remote, ref,
406
+ commit, path, capabilities)`, shared by `sync` and `resolve`; a hand-edited lock
407
+ (same id/version, different commit, copied approval) can no longer materialize
408
+ and run unapproved hooks. Cached per `(id, commit)` within a process.
409
+
410
+ **§1 peeled commit OIDs (M4).** `observeRemote(ref, { at })` accepts an
411
+ annotated tag's OID (or name) but **records the peeled commit** (`<oid>^{commit}`)
412
+ as `commit` — in its result, in the lock, in `fetchRemoteTree`'s errors and in
413
+ every `instance.json` record. A tag OID is never stored where a commit is
414
+ expected.
415
+
416
+ **§1 listing hygiene (L3, L4).** `listRemoteTree` filters by depth **before**
417
+ asserting entry-name safety, so one unsafe deep name does not blank a member's
418
+ souls (unsafe names at the kept depth are still `E_REMOTE_TREE_UNSAFE`). A git
419
+ child killed for `maxBuffer` (`ENOBUFS`) is not reported as `timeout`; an
420
+ unclassified listing failure is wrapped as `E_REMOTE_UNREADABLE { reason:
421
+ "unknown" }` so discovery records a problem row instead of aborting.
422
+
423
+ **§2 `validateWorkspace` absolute paths (L2).** The absolute-path refusal
424
+ applies to **ref/path fields only** — `members[]`, `packages` values, `stores`
425
+ values, `external[].source` / `external[].soul`, `defaults.*.from` — never to
426
+ `teams.<label>.description` or to the opaque `messaging` payload.
427
+
428
+ **§3 revision (L6).** `Resolution.revision = hash(declRevision, payloadRevision)`
429
+ where `declRevision` covers the declarations (member/package commits, the
430
+ composed capability set, skills, injects) and `payloadRevision` covers the
431
+ payload layers (soul slot payloads ⊕ `oats-local.yaml settings` ⊕
432
+ `--provider`). Both are exposed on the Resolution; decision binding keeps using
433
+ `revision`, so a settings-only difference still refuses a stale apply, while
434
+ `spawn --preview` can say **`changed since: declarations | payload | both`**
435
+ instead of a bare `changedSince`.
436
+
437
+ **§6 operator-level dispatch (B3, implements the rule stated above).** Outside a
438
+ home, with `oats-local.yaml` present, `oats <ns> <cmd> … --soul <name>` runs
439
+ `prepareInstance(dir, name)`, picks the module whose `manifest.command === <ns>`,
440
+ ensures its tree in `<deployment>/.oats/modules/<cap>@<commit12>/` (member: the
441
+ member repo at `module.from.commit`, `module.dir`; package: the lock entry) and
442
+ dispatches to that copy with `OATS_SETTINGS = resolution.payloads[cap]` and
443
+ `OATS_CLI_BIN`. `--soul` absent → `E_BAD_ARGS` naming it; a namespace no module
444
+ provides → `E_UNKNOWN_COMMAND`. Trust is the resolution's (membership;
445
+ `E_PACKAGE_UNAPPROVED` for an unapproved package).
446
+
447
+ **§5/§6 `work: workspace` under v2 (B2, implements the rule stated above).**
448
+ With `prepared` present, a `work: workspace` soul's `./work` links the
449
+ deployment directory (`prepared.deployment`, the one holding `oats-local.yaml`);
450
+ no branch is recorded; the "needs a declared boundary" remedy names
451
+ `oats-local.yaml`, not `oats-config.yaml`. The classic root is unchanged.
452
+
453
+ **Decision 13 reach (L7).** "Harnesses start normally" is a property of the
454
+ 0.25 **launcher**: every `pi` launch a 0.25 kernel performs — module homes and
455
+ classic 0.24 homes alike — starts pi with cwd = home, the composed `AGENTS.md`
456
+ appended, and pi's own skill/context discovery intact. Consequently `oats
457
+ session recompose` refuses a **module home** (`instance.json.modules` present)
458
+ with `E_UNSUPPORTED_MODE` ("re-spawn"); the `session-recompose` feature name
459
+ stays advertised because the verb still serves classic homes
460
+ (`docs/desktop-cli-api.md`).
@@ -15,7 +15,7 @@ A clean v2 of the kernel's declaration, resolution and launch path: read `oats-w
15
15
 
16
16
  ## Work packages
17
17
 
18
- Each is one PR (or two small ones), reviewed by me, on `main`, behind the v2 seam until W7. Order is dependency order; W1–W3 can proceed in parallel lanes.
18
+ Each is one PR (or two small ones), reviewed by me, on `main`, as canonical code from the first phase (no seam — see the approach above). Order is dependency order; W1–W3 can proceed in parallel lanes. **Status (2026-09-23): W1–W8 and W10 shipped in OATS 0.25.0 (PR99 `59ae22df`); W8's last part — folding the classic no-`oats-local.yaml` spawn path into the one pipeline and deleting the v1 residue — is a follow-up; W9/W9b/W11/W12 are Phases D–F.**
19
19
 
20
20
  | # | Package | Delivers | Owner | Size |
21
21
  |---|---|---|---|---|
@@ -52,7 +52,7 @@ Each phase = one developer-swarm workflow (parallel agents, disjoint files, agai
52
52
  | Risk | Mitigation |
53
53
  |---|---|
54
54
  | Remote access context is subtle (SSH vs HTTPS vs `gh` token; private repos) | W2 uses git itself (`git ls-remote`, `git archive`/shallow fetch) with the operator's configured credential helpers — no new credential store; typed `cannot read` never guesses |
55
- | `core.mjs` entanglement makes W8 risky | W8 is deletion behind a green W7; the seam guarantees nothing live depends on what is deleted; CI + the Northwind fixture + Desktop suites are the gate |
55
+ | `core.mjs` entanglement makes W8 risky | W8 is deletion behind a green W7; `rg` proof of zero importers per module before each deletion (the v1-residue table from the Phase C review); CI + the Northwind fixture + Desktop suites are the gate |
56
56
  | Package approval UX ("asks once") in non-interactive spawns | `oats sync` is where approval is asked; `spawn` refuses `E_PACKAGE_UNAPPROVED` pointing at `sync` — never prompts mid-spawn |
57
57
  | Latest-state members drift between preview and apply | The resolution records the member commit; apply re-observes and refuses `E_DECISION_STALE` if it moved — same mechanism as today's decision revision |
58
58
  | Desktop relying on "installed" | Removed as a state in W12; until then the Capabilities view keeps working on 0.24.x DTOs (contracts unchanged) |
@@ -450,6 +450,26 @@ the pre-fix marker and is never accepted for dispatch.
450
450
 
451
451
  ## Readiness quartet, signatures, enforced policy (`oats readiness`, `readinessApi: 1`, OATS 0.24.8+)
452
452
 
453
+ > **0.25 status — the producers below are the 0.24 tier.** The readiness DTO
454
+ > (`readinessApi: 1`) still ships unchanged, but its four checks are *produced*
455
+ > by the classic observers: `installed` by `oats list` over the
456
+ > `.agents/capabilities/installed/` tier, `trusted` by the per-artifact approval
457
+ > that `oats trust` wrote, `configured` by `oats-config.yaml` activation, and
458
+ > `enrolled` by the `oats.yaml` backlink. On a **workspace deployment**
459
+ > (`oats-local.yaml` present) none of those sources exists: nothing is installed,
460
+ > approval is per package version in `oats-lock.json` v3 (`oats sync`), activation
461
+ > is derived (workspace defaults ⊕ soul `capabilities:`), and membership is
462
+ > `oats-membership.yaml` observed over the remotes. `oats list` and `oats trust`
463
+ > are removed verbs (`E_UNKNOWN_COMMAND`), so a `remedy` naming them cannot be
464
+ > run. Treat the field names and producer strings as the stable wire shape they
465
+ > are; for the workspace-model facts read `oats spawn <soul> --preview --json`
466
+ > (`modules[]` with from/commit/digest — the "installed" and "configured"
467
+ > truth), `oats sync --json` `approvalNeeded[]` (the "trusted" truth) and
468
+ > `oats workspace status --json` (the "enrolled" truth). Re-basing the quartet on
469
+ > those producers is an open thread of [the workspace model](#workspace-model-workspaceapi-2);
470
+ > when it lands it will be announced as a new feature name, not a silent change of
471
+ > `readinessApi: 1`.
472
+
453
473
  `oats readiness [--soul <name>] [--home <abs>] [--verify-signatures] [--policy] [--dir <d>] --json`
454
474
  is the first-run readiness view (frame 09) and the Capabilities readiness rows
455
475
  (frame 04). Every fact is derived from the **same** data `oats inspect` reports
@@ -1026,6 +1046,18 @@ refreshes the home in place:
1026
1046
  schedule; the receipt's `note` says so. Refuses a retiring home
1027
1047
  (`E_INSTANCE_RETIRING`), captured incarnations and capability-defined souls
1028
1048
  (`E_UNSUPPORTED_MODE`: those are refreshed by a new resolution / package).
1049
+ - **Module homes (0.25+) are `E_UNSUPPORTED_MODE` too.** A home whose
1050
+ `instance.json` carries `modules{}` (spawned on a workspace deployment,
1051
+ `instance-modules`) answers `E_UNSUPPORTED_MODE` ("recompose from
1052
+ materialized modules is not supported yet; re-spawn"): its `AGENTS.md` was
1053
+ composed from the soul at a recorded member commit plus the materialized
1054
+ modules' injects, and the instance never changes under itself (decision 7).
1055
+ The refresh path for such a home is a new spawn (the soul is re-fetched at
1056
+ the member's current commit). `session-recompose` **stays advertised** in
1057
+ `features[]` because the verb still works for classic homes; gate the UI
1058
+ action on the feature AND on the absence of `instance.json.modules`
1059
+ (`oats status --json` `instances[].modules` is non-empty for a module home),
1060
+ and render the typed refusal otherwise.
1029
1061
  - Gate on `features.includes("session-recompose")`. It is an **operator
1030
1062
  action** (the human or the instance's parent), never something a Desktop
1031
1063
  poll or an agent runs on itself.
@@ -12,10 +12,17 @@ the OATS Desktop app (`packages/desktop/` in the framework repo):
12
12
 
13
13
  ## Migrating a deployment that used `oats.web`
14
14
 
15
+ Under the **0.25 workspace model** there is nothing to uninstall: remove
16
+ `oats.web` from `oats-workspace.yaml` `packages:` / `defaults.capabilities`
17
+ and from any `soul.yaml` `capabilities:`, run `oats sync` (the lock v3 entry
18
+ disappears with the declaration), and use the Desktop app (step 3 below).
19
+
20
+ For a **0.24 classic** deployment:
21
+
15
22
  1. Remove the `oats.web` entry from `capabilities.additive` in every
16
23
  `oats-config.yaml` in your config chain.
17
- 2. Remove the `oats.web` entry from `oats-lock.json` at the same scope(s), and
18
- delete any stale installed copy under `.agents/capabilities/installed/`.
24
+ 2. Remove the `oats.web` entry from `oats-lock.json` (v2) at the same scope(s),
25
+ and delete any stale installed copy under `.agents/capabilities/installed/`.
19
26
  3. Use the OATS Desktop app instead: `cd packages/desktop && npm install &&
20
27
  npm run rebuild && npm start` (see `packages/desktop/README.md`).
21
28
 
package/docs/desktop.md CHANGED
@@ -84,10 +84,15 @@ The probe/mutation contract is specified in
84
84
 
85
85
  The app starts on the directory it was launched with (its own folder by
86
86
  default). To view a deployment, open the workspace switcher in the sidebar
87
- and choose **Add workspace → Browse**, then point it at an OATS workspace —
88
- a directory containing `agents/`, or `local-agents/` for machine-local
89
- souls, or a team scope whose `oats-config.yaml` declares `team:`. Team scopes
90
- show every member repo's agents under one roster with a workspace switcher.
87
+ and choose **Add workspace → Browse**, then point it at an OATS deployment —
88
+ a directory containing `agents/` (under the 0.25 workspace model that is the
89
+ `<name>-workspace/` directory holding `oats-local.yaml` and `agents/`;
90
+ under 0.24, an `agents/` root, a `local-agents/` root for machine-local souls,
91
+ or a team scope whose `oats-config.yaml` declares `team:`). *The Desktop's own
92
+ multi-repo roster ("team scopes show every member repo's agents under one
93
+ roster") still keys on the 0.24 `oats-config.yaml` `team:` declaration; reading
94
+ the member set from `oats-local.yaml` / `oats workspace status` is the Phase F
95
+ follow-up named in the [0.25.0 notes](release-notes/v0.25.0.md#desktop).*
91
96
  Added workspaces are remembered and offered as suggestions next time.
92
97
 
93
98
  Local souls (uncommitted, machine-local agents under `local-agents/`) are
@@ -231,10 +231,22 @@ and needs equivalent registration glue when switched to session delivery.
231
231
 
232
232
  ## Shared permission setting
233
233
 
234
- Set `yolo: true` in an `oats-config.yaml` to apply it to that scope. The closest
235
- scope wins; an optional `yolo` in soul.yaml overrides it; `oats spawn --yolo` or
236
- `--no-yolo` overrides both. `oats create` accepts those flags too. Desktop offers
237
- the same per-launch choice. With no setting, native policy is retained.
234
+ The opt-in is per launch or per soul: `oats spawn --yolo` / `--no-yolo`
235
+ (`oats create` accepts the same flags), an optional `yolo` in `soul.yaml`
236
+ (0.24 schema; the v2 `soul.yaml` schema does not carry it — use the spawn flag
237
+ or a launch configuration), and the Desktop's per-launch choice. With no
238
+ setting, native policy is retained.
239
+
240
+ *0.24 classic deployments* may also set `yolo: true` in an `oats-config.yaml`
241
+ to apply it to that scope; the closest scope wins, soul overrides scope, the
242
+ spawn flag overrides both. *Under the workspace model* `oats-config.yaml` is
243
+ not configuration ([configuration.md](configuration.md)); the kernel's
244
+ `composeInstance` still consults the classic chain for the machine-level knobs
245
+ `yolo` and `launch-configs` when such a file happens to sit above the
246
+ deployment, but nothing writes one and the rebuild guide tells you to delete
247
+ it — treat a scope-level `yolo` as a 0.24 feature and prefer the explicit
248
+ spawn flag.
249
+
238
250
  Autonomous or unattended execution is not permission to synthesize `yolo: true`.
239
251
  Explicit user CLI/UI input or a user-selected configuration is the opt-in; explicit
240
252
  `false` remains false.