@awebai/oats 0.25.5 → 0.25.7

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
@@ -31,7 +31,7 @@ import {
31
31
  packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir,
32
32
  resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, stripInternalAnnotations, withConfigFile, teamAgentRoots,
33
33
  findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf, stopInstanceSession, recomposeInstanceInstructions,
34
- ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
34
+ ensureRoot, findRoot, findAgent, listAgents, listInstances, servedIdentityLine, servedIdentityOf, listAgentDefs, createAgent as coreCreateAgent,
35
35
  spawnInstance, spawnInstanceAsync, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS, validateLaunchConfig, resolveLaunchSelection, resolveLaunchExecutable, checkLaunchExecutable, missingLaunchEnvRefs, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, LAUNCH_RECIPE_VERSION, parseLaunchCommand, resolveYolo, planLaunch, redactLaunchCommand, restartInstanceSession,
36
36
  } from "../lib/core.mjs";
37
37
  import {
@@ -935,7 +935,9 @@ function computeInspect({ onFail } = {}) {
935
935
  const result = {
936
936
  operationsApi: 1, kernel: OATS_VERSION,
937
937
  scope: { context: ctx, requestedContext: requestedContext === ctx ? null : requestedContext, workspace: roots.length ? workspaceOf(roots[0]) : ctx, team: r.team || null, chain: chain.map((c) => ({ file: c._file, level: c._level, levelKind: levelOf(c._level) })), agentsRoots: roots },
938
- selected: { soul: selectedSoul?.name || null, agentsRoot: selectedSoul?.agentsRoot || null, home: home || null, source: meta ? "snapshot" : "config" },
938
+ selected: { soul: selectedSoul?.name || null, agentsRoot: selectedSoul?.agentsRoot || null, home: home || null, source: meta ? "snapshot" : "config",
939
+ // Decision 27 (K2): the principal this home acts as, from its messaging provider's hook meta.
940
+ ...(meta ? { identity: servedIdentityOf(meta) } : {}) },
939
941
  souls, sources, layers, capabilities, knowledge, snapshot, currentConfig,
940
942
  problems: [...(lockError ? [lockError] : []), ...packagedDiagnostics.map((d) => ({ code: d.code, message: d.message, capability: d.capability })),
941
943
  ...(meta ? snapshotCaps.filter((c) => !mans[c.id]).map((c) => ({ code: "captured-capability-missing", message: `${c.id} was active when this home was composed but no manifest for it is acquired now`, capability: c.id })) : [])],
@@ -947,6 +949,7 @@ function printInspect(result) {
947
949
  const { ctx, selectedSoul, home } = result._print; delete result._print;
948
950
  const { souls, layers, capabilities } = result;
949
951
  console.log(`oats inspect — ${shortPath(ctx)}${selectedSoul ? ` soul ${selectedSoul.name}` : ""}${home ? ` home ${shortPath(home)}` : ""}`);
952
+ if (result.selected?.identity) console.log(` identity: ${servedIdentityLine(result.selected.identity)}`);
950
953
  for (const s of souls) console.log(` soul ${s.name} [${s.kind}${s.capability ? ` ${s.capability}` : ""}] runtime ${s.runtime}${s.model ? ` model ${s.model}` : ""} work ${s.work}${s.editable.fields.length ? "" : " (read-only)"}`);
951
954
  for (const l of LAYERS) console.log(` layer ${l}: ${layers[l].id || (layers[l].disabled ? "disabled" : "none")}${layers[l].provenance ? ` (${layers[l].provenance})` : ""}`);
952
955
  for (const c of capabilities) console.log(` ${c.id}@${c.version || "?"} ${c.health.status}${c.activation.enabled ? ` active:${c.activation.target}` : " inactive"}${c.operations.length ? ` ops: ${c.operations.map((o) => `${o.name}${o.available ? "" : "(unavailable)"}`).join(", ")}` : ""}`);
@@ -2343,6 +2346,7 @@ async function status() {
2343
2346
  for (const i of a.instances) {
2344
2347
  console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
2345
2348
  const key = i.home ?? `${a.name}/${i.instance}`;
2349
+ if (i.identity) console.log(` identity: ${servedIdentityLine(i.identity)}`);
2346
2350
  const s = ws?.soul.get(key);
2347
2351
  if (s && (verbose || s.status !== "current")) console.log(` ${soulDriftLine(s, a.name)}`);
2348
2352
  const rows = ws?.drift.get(key) || [];
@@ -3366,7 +3370,7 @@ function versionCmd() {
3366
3370
  // Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
3367
3371
  // runs on resolve/materialize (contract §6); a feature the binary does not implement is
3368
3372
  // never listed.
3369
- console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["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", "session-recompose", "readiness-verify", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 1, lifecycleApi: 1, readinessApi: 1, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
3373
+ console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["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", "session-recompose", "readiness-verify", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 1, lifecycleApi: 1, readinessApi: 1, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
3370
3374
  return;
3371
3375
  }
3372
3376
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -282,6 +282,14 @@ for this contract. Hyphenated vendors are also excluded because translating a
282
282
  hyphen to `_` would let `aweb-evil.*` collide with names already inside
283
283
  `aweb.*`'s `AWEB_*` namespace.
284
284
 
285
+ A manifest's `settings.<key>` may carry `hostOnly: true` (decision 27). Such a
286
+ key is a fact about the machine — a custody directory, a state root — and the
287
+ resolver accepts it only from the deployment's own `oats-local.yaml`
288
+ `settings.<capability>`; a committed workspace or soul file or a `--provider`
289
+ flag carrying it is refused (`E_WORKSPACE_SCHEMA`, reason `host-only-key`).
290
+ Declare it for any key whose value points at something a committed file must
291
+ never be able to choose.
292
+
285
293
  A hook may return only names in its manifest's exact `environment` declaration.
286
294
  For package capabilities that declaration is part of the integrity-locked tree
287
295
  and of what the per-version approval showed; for member capabilities it is
@@ -263,6 +263,10 @@
263
263
  },
264
264
  "description": {
265
265
  "type": "string"
266
+ },
267
+ "hostOnly": {
268
+ "type": "boolean",
269
+ "description": "When true, this key is a host fact (a custody path, a state directory): the resolver accepts it only from the deployment's oats-local.yaml settings.<capability> and refuses it in the workspace file, byTeam payloads, a soul's slot payload and --provider flags (E_WORKSPACE_SCHEMA reason host-only-key). Decision 27."
266
270
  }
267
271
  },
268
272
  "additionalProperties": false
@@ -50,7 +50,12 @@ refused (`E_WORKSPACE_SCHEMA`).
50
50
  Every `oats` command that needs the workspace (`sync`, `workspace status`,
51
51
  `capabilities`, `souls`, `spawn`, `status` drift) walks **up** from the current
52
52
  directory (or `--dir`) to the nearest `oats-local.yaml`; its directory is the
53
- deployment. Not found → `E_LOCAL_MISSING`. Beside it:
53
+ deployment. Not found → `E_LOCAL_MISSING`. The deployment is also a
54
+ **configuration boundary**: nothing above the directory holding
55
+ `oats-local.yaml` composes into it (a deployment created inside another
56
+ scope — a scratch deployment under a repository, a fixture under an operator
57
+ workspace — sees only its own files; `oats inspect` reports it, not the outer
58
+ scope, as the workspace). Beside it:
54
59
 
55
60
  ```
56
61
  ~/acme-workspace/
@@ -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 20:00Z · **0.25.0–0.25.4 PUBLISHED** (workspace model A–C; team-review fixes; operator-rebuild round; OATS_SOUL_ID; quarantine-retry fix) · **OKF 2.1.4 content complete on oats-okf main, tag pending human GO** · **oats.aweb 1.12.0 (PR #107) in rehearsal round 4; decision 27 (per-spawn identity) proposed to the human** · Phase D plan next · **Desktop engineer paused by the human** (10B-0 uncommitted foundation preserved; resume is the human's call; 0.24.14 to be cut from a maintenance branch off `e5cdaf95` when its PR lands) · parity pipeline ⏸.
5
+ **Last update:** 2026-09-24 13:30Z · **0.25.0–0.25.6 PUBLISHED** (0.25.6 = decision 27 kernel half + Desktop 10B-0 terminal owner leases, native gate 23/23) (workspace model A–C; team-review fixes; operator-rebuild round; OATS_SOUL_ID; quarantine-retry fix; launch meta + catalog pins) · **OKF v2.1.4 + oats.aweb v1.12.0 TAGGED and pinned** · **decision 27 accepted; K1′/K1″/K2 kernel PR next** · **oats.aweb 1.13.0 (#110) queued** · **Desktop 10B-0 resumed** · Phase D plan next · **Desktop engineer paused by the human** (10B-0 uncommitted foundation preserved; resume is the human's call; 0.24.14 to be cut from a maintenance branch off `e5cdaf95` when its PR lands) · parity pipeline ⏸.
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -565,6 +565,21 @@ is a member; a soul that lives in it may need a work clone). Under an explicit
565
565
  `oats-local.yaml` `standalone:` header the next steps say the view is standalone
566
566
  and list only that repo.
567
567
 
568
+ ### 0.25.6 — decision 27: served identity is a messaging-layer fact (K1′, K1″, K2)
569
+
570
+ - **K1′** `decision.effective.providers` = the resolution's merged per-module payloads
571
+ (exactly what reaches `OATS_SETTINGS`), bound by the decision revision. No new flag.
572
+ - **K1″** manifest `settings.<key>.hostOnly: true` → the resolver refuses that key in
573
+ the workspace base, `byTeam[*]`, the soul's slot and `--provider` layers with
574
+ `E_WORKSPACE_SCHEMA { reason: "host-only-key", path, key, capability }`; only
575
+ `oats-local.yaml settings.<cap>` may carry it. Generalises decision 23's reserved
576
+ `byTeam` into a capability-declared attribute. Schema: `docs/capability-manifest.schema.json`.
577
+ - **K2** `oats status --json instances[].identity` and `oats inspect … selected.identity`
578
+ copy `capabilityMeta[<cap>].identity` (messaging-layer capability preferred; `provider`
579
+ added); text `identity: acts as <address> via grant, expires <t>` / `alias <a> on <team>`.
580
+ Layer contract shape in `docs/integrations.md`.
581
+ - `features[]` gains `served-identity`.
582
+
568
583
  ### 0.25.5 — launch-hook `meta` is persisted
569
584
 
570
585
  `runLifecycleHooks("launch")` collected each capability's `meta` and the
@@ -0,0 +1,180 @@
1
+ # Desktop Phase F — the Desktop is built FOR workspace model v2
2
+
3
+ **Status**: boundary for the Desktop engineer, issued 2026-09-24 by the lead under
4
+ the human's direction: *"the desktop should not just adapt to the new version,
5
+ it should be natively built for it."* Supersedes the Phase 3 parity plan's
6
+ assumptions about what the Desktop reads; keeps its visual deliverable (the
7
+ redesign frames, excl. 05/06).
8
+
9
+ **Human's second directive, verbatim intent**: make ultra sure the Desktop is set
10
+ up to work in the new setup — new versions, new CLI, new deployment shape.
11
+
12
+ ## 0. Why this is a rebuild of the model, not a patch
13
+
14
+ The Desktop today is a 0.24 product that *tolerates* 0.25 kernels: its
15
+ `ACCEPT_RANGE` admits `0.25.x`, so it launches, and the few CLI verbs it drives
16
+ (`version`, `session *`, `spawn`, `retire`, `schedule`, `catalog`) still answer.
17
+ But its **model of a deployment is 0.24's**, reimplemented in
18
+ `packages/desktop/server/deployment.mjs`: it reads `oats-config.yaml`,
19
+ `agents/<name>/soul`, `local-agents/`, and capability manifests from
20
+ `.agents/capabilities/installed/`, and derives the roster itself. None of these
21
+ is how a v2 deployment is shaped:
22
+
23
+ | 0.24 (what the Desktop reads) | v2 (what a deployment IS) |
24
+ |---|---|
25
+ | `oats-config.yaml` at the repo root | `oats-local.yaml` at the deployment directory → `workspace:` URL |
26
+ | souls at `agents/<name>/soul` | souls at `souls/<name>` **in member repos**, materialised per commit into `agents/<name>/souls/<commit>` |
27
+ | capabilities installed into `.agents/capabilities/installed/` | packages resolved through `oats-workspace.yaml` + the official catalog, locked in `oats-lock.json`, **copied whole into each instance home** (`.oats/modules/<cap>`) |
28
+ | `team:` block | `oats-membership.yaml` `team:` label per member; `messaging.byTeam.<label>` payloads |
29
+ | `oats catalog` DTO | removed; the catalog is `package-catalog.json` resolved by `oats sync` |
30
+ | roster derived by the Desktop | `oats status --json` is the roster (agents, instances, `modules[]` drift, `soul` source drift, `identity`) |
31
+
32
+ A Desktop that keeps the left column and adds a few right-column fields is the
33
+ "adapt" outcome the human rejected. Phase F replaces the left column.
34
+
35
+ ## 1. The principle: the kernel is the model; the Desktop renders and drives it
36
+
37
+ - **Read model**: every fact the Desktop shows about a deployment comes from
38
+ the kernel's JSON surfaces — `oats status --json`, `oats inspect --json`,
39
+ `oats workspace status --json`, `oats spawn --preview --json`, `oats
40
+ readiness --json`, `oats version --json`, `oats sync --json`. The Desktop
41
+ does not parse `oats-config.yaml`, `oats-local.yaml`, `soul.yaml`,
42
+ manifests or lock files itself. Where a fact is missing from a kernel
43
+ surface, the fix is a kernel PR (lead's lane), not a Desktop-side parser.
44
+ - **Write model**: every mutation is a kernel verb with `--json`: `sync`,
45
+ `sync --approve`, `spawn` (preview → `--expect-decision` apply), `retire`,
46
+ `session start|restart|recompose`, `schedule *`, `onboard`. The Desktop
47
+ never writes a deployment file.
48
+ - **Version contract**: `oats version --json` `features[]` is the capability
49
+ probe. The Desktop's `ACCEPT_RANGE` moves to `>=0.25.6 <0.27.0` (the first
50
+ kernel with `served-identity`), and each feature the UI depends on is gated
51
+ on its `features[]` name, not on a version number.
52
+
53
+ ## 2. Deliverables (slices; each is one PR against main, each reviewed by the lead)
54
+
55
+ **F1 — Deployment model on kernel JSON.** Replace
56
+ `server/deployment.mjs`'s own readers with `oats status --json` (+ `oats
57
+ workspace status --json` for the workspace header: name, key, members, packages,
58
+ lock state, `approvalNeeded`). Roster rows carry `modules[]` drift, `soul`
59
+ source (`repo: <member> @ <c7>`, "member moved since"), `identity`. Legacy
60
+ `local-agents/`/`tmp-agents/` paths are dropped. Remove `server/catalog.mjs`'s
61
+ `oats catalog` DTO validation (the verb no longer exists).
62
+
63
+ **F2 — Workspace onboarding and sync.** A "Open deployment" flow that: detects a
64
+ directory with `oats-local.yaml` (v2), or offers `oats onboard` for one without
65
+ (the kernel asks for the deployment directory and the workspace URL — the
66
+ Desktop collects both, never invents a folder name; decision 9). A "Sync"
67
+ action runs `oats sync --json`; exit 2 with `approvalNeeded[]` renders an
68
+ approval sheet showing each package's `executables` digest and applies with
69
+ `oats sync --approve <id>@<version>` (the version string is what
70
+ `approvalNeeded[].version` reports — for a git-pinned package, the full OID).
71
+ Lock drift and `E_PACKAGE_INTEGRITY` are surfaced verbatim.
72
+
73
+ **F3 — Spawn dialog on the v2 preview.** The preview already carries
74
+ `modules`, `providers`, `settings.<cap>`, `team`, `resolution`, `decision`.
75
+ Render: which modules the instance will get and from where (package vs member,
76
+ commit); the merged `settings.<cap>` per provider (read-only); **Identity**
77
+ select (local | global) with a Resident field for global, prefilled from
78
+ `settings.<messaging cap>.identity`, forwarded as `--provider <cap>
79
+ identity.mode=… identity.resident=…` (decision 27 — there is no kernel flag);
80
+ `decision.effective.providers` is what the confirm binds. `cli-adapter.mjs`
81
+ `SPAWN_ARG_RULES` gains one `provider` rule (capability id, dotted key, value
82
+ grammar); no identity-named rules. Work mode select includes `workspace`.
83
+
84
+ **F4 — Instance card and roster on v2 facts.** The served-identity line (`acts
85
+ as <address> via grant, expires <t>` / `alias <a> on <team>`); module rows with
86
+ "moved since" markers and a **re-spawn** action (preview → apply, then retire
87
+ the old instance — module homes answer `E_UNSUPPORTED_MODE` to
88
+ `session recompose` by design; an instance never changes under itself); the
89
+ soul-source row; quarantine state from `rollbackIncomplete` with the retry
90
+ action (`oats retire`) and `--force` behind a confirm.
91
+
92
+ **F5 — Redesign frames** (the original Phase 3 deliverable, excl. 05/06),
93
+ implemented on top of F1–F4 rather than on the 0.24 model.
94
+
95
+ **F6 — Version and doctor surface.** `oats version --json` and `oats doctor
96
+ --json` in an About/Health pane; `ACCEPT_RANGE` and the three pins move to
97
+ `>=0.25.6`; a kernel below the floor is refused with the upgrade command shown.
98
+
99
+ Order: F1 → F2 → F3 → F4 → F5 → F6, or F1 then F3/F4 in parallel if the engineer
100
+ spawns children (its call; the lead reviews each PR).
101
+
102
+ ## 3. What the engineer must LEARN first (before F1)
103
+
104
+ Read, in this order, in the checked-out main:
105
+ 1. `docs/workspaces.md` — the v2 model end to end (deployment vs workspace,
106
+ members, packages, payloads, `byTeam`, hosting).
107
+ 2. `docs/rebuild-to-v2.md` — how an operator builds a deployment (this is the
108
+ flow F2 wraps).
109
+ 3. `docs/desktop-cli-api.md` — every JSON surface, with examples; note
110
+ `features[]`, `decision.effective.providers`, `instances[].identity`.
111
+ 4. `docs/design/2026-09-23-workspace-module-contracts.md` §0.25.x — what
112
+ changed per release and why.
113
+ 5. `agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md` and
114
+ `…/served-identity-is-a-messaging-layer-fact.md` — the decisions.
115
+ 6. `test/fixtures/northwind/build.mjs` — the two-team fixture workspace; run
116
+ `node --test test/spawn-workspace.test.mjs` once and read what a v2 spawn
117
+ produces on disk (`.oats/modules/`, `instance.json` `modules`/`providers`/
118
+ `workspace.soul.id`).
119
+
120
+ Then build a scratch deployment by hand with the CLI (`oats onboard`, `oats
121
+ sync`, `oats sync --approve`, `oats spawn --preview`, `oats spawn --no-launch`,
122
+ `oats status`, `oats inspect`, `oats retire`) against the Northwind fixture
123
+ remotes, and keep the transcript: F1's tests are written against exactly those
124
+ JSON shapes.
125
+
126
+ ## 3b. Native rework, not a compatibility layer (human, 2026-09-24)
127
+
128
+ The human's rule, verbatim intent: *"do a native rework — do not keep v1-specific
129
+ things, and no v1 modules calling v2 modules."* Concretely:
130
+
131
+ - **Remove, do not wrap.** A 0.24 reader (`server/deployment.mjs`'s
132
+ `oats-config.yaml`/`soul.yaml`/manifest parsing, `local-agents/`,
133
+ `.agents/capabilities/installed/`, the `oats catalog` DTO) is deleted in the
134
+ slice that replaces it — never kept behind a flag, a fallback branch, or an
135
+ "if the kernel is old" path. The Desktop supports one kernel line
136
+ (`ACCEPT_RANGE` from 0.25.6) and refuses older ones with the upgrade command.
137
+ - **No adapters between generations.** No module whose job is to translate a
138
+ v1-shaped object into a v2-shaped one or vice versa (no `legacyRosterToV2()`,
139
+ no `toOldCard()`); the v2 kernel JSON is consumed where it is read and shaped
140
+ once for rendering. If a v1 module still needs a v2 fact, the v1 module is
141
+ the thing being replaced — replace it, do not feed it.
142
+ - **Names and types follow v2.** Types, fields and UI labels use the kernel's
143
+ vocabulary (workspace, member, package, module, deployment, soul source,
144
+ served identity); 0.24 vocabulary (installed capability, config chain, team
145
+ block, agents root as identity) leaves the codebase with the code that used
146
+ it. `git grep` for the old terms is part of each slice's exit check.
147
+ - **Tests follow the same rule.** Fixtures shaped like 0.24 deployments are
148
+ deleted with the readers; new fixtures are v2 deployments produced by the
149
+ kernel (Northwind or a hand-built scratch deployment), not hand-written
150
+ JSON imitating old shapes.
151
+ - **One exception, stated per case.** Where a 0.24 concept has a genuine v2
152
+ successor with the same meaning and the Desktop code is already correct for
153
+ it (a terminal broker, a tmux target admission), it stays — the PR names it
154
+ as "unchanged, v2-agnostic", not as "kept for compatibility".
155
+
156
+ Exit check for Phase F as a whole: no file under `packages/desktop/` reads a
157
+ deployment file, names a 0.24 concept, or contains a code path that exists
158
+ only for a kernel below the floor.
159
+
160
+ ## 4. Rules that do not change
161
+
162
+ - `packages/desktop/**` only; kernel gaps go to the lead as a written ask
163
+ (they become kernel PRs; the engineer never adds a Desktop-side parser to
164
+ work around one).
165
+ - No native tmux/PTY/Electron execution by the agent; the lead's native gate
166
+ at review time is the acceptance.
167
+ - Every slice: focused tests + the intended mutants on the new code; the
168
+ Desktop suite green; one PR per slice against main; lead's pr-review.
169
+ - The design frames are the visual authority; the kernel JSON is the data
170
+ authority; where a frame shows a 0.24 concept (an "installed capability"
171
+ list, a `team:` block), the frame is adapted to the v2 concept and the
172
+ adaptation noted in the PR.
173
+
174
+ ## 5. Acceptance for Phase F as a whole
175
+
176
+ The lead builds a fresh v2 deployment from the Northwind fixture using ONLY the
177
+ Desktop (open → onboard → sync → approve → spawn with a global identity →
178
+ inspect → retire) on kernel 0.25.6+, and every fact shown matches `oats status
179
+ --json` / `oats inspect --json` byte for byte. Nothing in
180
+ `packages/desktop/server` reads a deployment file.
@@ -0,0 +1,164 @@
1
+ # Phase D — the OATS project runs on the architecture it offers (plan)
2
+
3
+ **Status**: plan, 2026-09-24, lead. Decisions 18–22 of the workspace model, the
4
+ five-soul roster and its 2026-09-24 amendment, the human's sequencing
5
+ ("knowledge centralisation first"; "do not retire live souls until their
6
+ instances retire"). Mailed to the human and the OSS coordinator before the
7
+ first swarm. Method per standing instruction: write → swarm build →
8
+ adversarial-review swarm → PR → main; releases under delegated authority.
9
+
10
+ ## Co-leads and lanes (human, 2026-09-24)
11
+
12
+ The human made the two `oats-expert` instances — the redesign lead
13
+ (`oats-expert-knowledge-reworks`) and the maintainer of the second deployment
14
+ (`oats-expert-antares`) — **co-leads with equivalent authority**, who agree on
15
+ pushes. Agreed by both on 2026-09-24:
16
+
17
+ | Lane | Owner | Status |
18
+ |---|---|---|
19
+ | Desktop Phase F (engineer PRs, native gates) | lead | assigned |
20
+ | Kernel (`lib/`, `bin/`; kernel gaps raised by anyone) | lead | assigned |
21
+ | Releases 0.25.7 and D5 (catalog + 0.26.0) | lead | assigned |
22
+ | oats.aweb 1.13.0 re-land end to end | Antares | assigned |
23
+ | D1 operator node + integrations node | Antares | assigned (draft PR `d1/operator-node` handed over) |
24
+ | aweb and okf package-expert seams | Antares | assigned |
25
+ | D2, D3, D4, desktop + kernel bundle migrations (D1 remainder) | Antares | assigned (both humans agreed, 2026-09-24). D2's machine-bound onboard of the lead's deployment is run by the lead on Antares' word; kernel gaps come to the lead as asks. |
26
+
27
+ **Push protocol.**
28
+ - **Class A** — notify after, one line: stewardship, docs and knowledge inside
29
+ one's own lane or node; merging a PR the other co-lead approved in writing;
30
+ merging one's own lane's PR after the other co-lead's review; the bump PR of
31
+ an agreed release.
32
+ - **Class B** — `PUSH INTENT: <what> @ <commit/PR>` → `ACK <message-id>` before
33
+ acting: tags, releases, npm publishes, catalog pins; framework-behaviour
34
+ changes (kernel semantics, OKF/memory contracts, workspace config semantics,
35
+ published skills); merges into the other's lane; reverts, force-anything,
36
+ branch or tag deletion. A blocking intent unanswered for about 45 minutes goes
37
+ to the human, never to action.
38
+ - Every PR is reviewed by the co-lead who did not author it (a helper's PR is
39
+ reviewed by its own lead, inside that lead's lane). A disagreement not settled
40
+ in two mails goes to the human. Standing rules unchanged: PR CI is the full
41
+ gate; published tags never move; no destructive git on shared checkouts.
42
+ - `main` on these repositories carries no branch or tag protection: the parity
43
+ gate and the cross-review are the only things between a merge and `main`.
44
+
45
+ ## Slices, in order
46
+
47
+ ### D1 — Knowledge centralisation (IN PROGRESS)
48
+
49
+ Goal: every roster soul's knowledge lives in the central base
50
+ `awebai/oats-knowledge` (OKF 2.1.4), one owned node each; the in-repo
51
+ `agents/*/soul/knowledge` bundles stop receiving writes and are decommissioned
52
+ when no live instance links them.
53
+
54
+ Done: nodes `oats-operator-expert` and `integrations-expert` chartered
55
+ (oats-knowledge PR #3); eight attributed seeds landed in
56
+ `agents/oats-expert/soul/knowledge/inbox` (oats PR #121); the roster amendment
57
+ accepted; the rule "no per-soul knowledge merges" in force.
58
+
59
+ Work:
60
+ 1. **Copy-migrate by judgement** (the 2026-09-21 method: two-part test plus the
61
+ 2026-09-24 recipe refinement; not copying): `agents/oats-expert/soul/knowledge`
62
+ (~130 files) → node `oats-expert`; `agents/oats-desktop-engineer/soul/knowledge`
63
+ (~120) → `oats-desktop-expert`; `agents/cli-dev/soul/knowledge` (~155) →
64
+ `oats-kernel-expert`; `agents/integrations-expert/soul/knowledge` (13) →
65
+ `integrations-expert`. Others (`dev-coordinator`, `docs-expert`, `ux-designer`,
66
+ `lead`, `oats-coordinator`) are assessed for the few universal concepts they
67
+ hold and otherwise not carried. One PR per node on oats-knowledge, reviewed
68
+ by the lead; the OSS coordinator reviews the operator and integrations PRs.
69
+ 2. **Seed the operator node** — attributed to `oats-expert-antares`:
70
+ MOVE (not copy) from the oats-expert bundle: `lessons/the-aweb-team-root-must-sit-where-the-spawn-hook-looks`,
71
+ `lessons/okf-state-directory-must-sit-outside-every-work-tree`,
72
+ `lessons/capability-trust-hash-covers-the-installation-record`,
73
+ `lessons/a-locally-minted-oats-identity-has-no-cross-team-first-contact-address`,
74
+ `lessons/stale-checkout-serves-stale-soul`,
75
+ `playbooks/rebuild-a-deployment-in-scratch-against-local-bare-remotes`;
76
+ generalise the R1–R10 rebuild findings and the published-combination
77
+ verifications from `stewardship/delivery-log` (the log keeps the record).
78
+ From the inbox: `check-the-record-before-redesigning-identity`,
79
+ `grant-custody-service-and-renewal-belong-on-the-custody-host` (operator
80
+ half), `the-wake-broker-accepts-a-grant-home`.
81
+ 3. **Seed the integrations node** — from the inbox:
82
+ `a-merged-provider-payload-cannot-enforce-host-only-keys`,
83
+ `aw-grant-commands-resolve-the-identity-from-cwd-only` (discipline half),
84
+ `oats-runtime-requirements-for-grant-backed-resident-operation`; from the
85
+ integrations-expert bundle: `fake-aw-must-model-real-refusals` and the rest
86
+ of this week's harvested lessons.
87
+ 4. **Route the remainder of the inbox**: aweb package expert (D3) gets
88
+ `aweb-grants-are-team-bound-to-the-custody-identitys-active-team`,
89
+ `a-grant-signed-send-must-name-the-subject-as-sender` and the aw halves;
90
+ kernel expert gets `path-keyed-owner-registry-breaks-under-per-commit-soul-copies`.
91
+ 5. **Rebind the souls** in `souls/<name>/`: the `knowledge:` grammar there is
92
+ still 0.24's (`capability` + `source: git:…@v2.1.2#oats-package`); v2 is
93
+ `capabilities: { oats.okf: { from: package } }` plus `okf.json`
94
+ (`{ version: 1, owner: <node owner uuid>, owns: ["oats/<node>"], reads: [...] }`)
95
+ and the store `oats` naming `awebai/oats-knowledge` / `knowledge` / `main`.
96
+ Add `souls/oats-operator-expert` (rename of `oats-setup-expert`, keeps the
97
+ `oats.setup` charter) and `souls/integrations-expert`.
98
+ 6. **Do NOT delete** `agents/<n>/soul/knowledge` or the legacy souls while a
99
+ live instance links them (human rule). Record which are live; decommission
100
+ as they retire; new spawns use `souls/<n>`.
101
+ 7. **Release playbook** (`oats-expert` node, stewardship area): landing order
102
+ for provider PRs (tag → pin on the branch → squash; a bundled provider never
103
+ lands ahead of its tag), the version literals to bump on a catalog bump,
104
+ the mirror checker, the bump-PR step. First entries: today's two lessons.
105
+
106
+ ### D2 — The OATS workspace as a v2 workspace (decisions 18, 19, 21)
107
+
108
+ `oats-workspace.yaml` at the `oats` repo (name `oats`; members = the seven
109
+ framework repos incl. `oats` itself; `packages:` = oats.framework / okf / aweb /
110
+ jira / linear / authoring / dev pinned from the catalog; `defaults.capabilities`
111
+ = `oats.core` from package + knowledge `oats.okf`); `oats-membership.yaml` ×7
112
+ (each package repo is a member AND a package publisher — non-collapse:
113
+ `packages:` resolves it as a package, membership only grants trust and a soul).
114
+ `package-catalog.json` stays the official marketplace (decision 21); the `oats`
115
+ repo keeps `oats-dev` as dev capabilities. A fresh deployment directory (asked
116
+ for, never named by convention — decision 9) is the acceptance: `oats onboard`
117
+ → `sync` → `approve` → spawn every roster soul `--no-launch`.
118
+
119
+ ### D3 — Souls to `souls/<name>` and the six package-expert souls (decision 20)
120
+
121
+ Each package repo carries `souls/<pkg>-expert` (okf-expert, aweb-expert,
122
+ jira-expert, linear-expert, authoring-expert, dev-expert), the expert in that
123
+ package, with a node in the central base from day one. **Seams named in the
124
+ charters** (roster amendment): `aweb-expert` READS
125
+ `aweb-protocol-expert` in base `aweb-oss-knowledge` (repo
126
+ `github.com/awebai/aweb`, root `knowledge/`, branch `main`, OKF 2.1.x
127
+ descriptor at `knowledge/okf-base.json`; owner `1913b77b-…`) through a
128
+ read-only store reference; `okf-expert` names its seam to the knowledge-theory
129
+ material in `oats-expert`. Whether the aweb bookshelf decisions the program
130
+ rests on are published into that node is the aweb side's call (asked).
131
+
132
+ ### D4 — `oats.core` and `oats.setup` rewritten (decision 22, W9b)
133
+
134
+ Not patched: written for the v2 world. `oats.core`: home layout, `oats status`
135
+ with modules/soul/identity rows, spawn preview → apply, `sync --approve`, the
136
+ two-directory boundary, what a module is. `oats.setup` (held by
137
+ `oats-operator-expert`): onboarding that ASKS for the deployment directory and
138
+ the workspace URL, the hosting rule (decision 26), the public-member executable
139
+ rule, the rebuild guide as procedure with the operator node as rationale.
140
+ Developer souls in `oats.dev` gain `promotesTo: <node>` (roster amendment);
141
+ the harvester delivers to that node as a PR the owning expert reviews.
142
+
143
+ ### D5 — Catalog update and 0.26.0
144
+
145
+ Catalog pins for the new package versions; **widen Desktop `ACCEPT_RANGE` and
146
+ the three pins FIRST** (minor bump rule); release notes; tag; the fresh
147
+ deployment from D2 rebuilt on the published artefacts by the OSS coordinator
148
+ (outsider verification) before the program board marks Phase D done.
149
+
150
+ ## Adversarial review (per slice)
151
+
152
+ Each slice's swarm is followed by a review swarm with the standing lenses
153
+ (direction against the decisions; correctness by reproduction; security —
154
+ trust at acquisition, hoisted paths, hook approval, host-only keys; docs as
155
+ contract — every guide claim has a test), plus two Phase-D-specific ones: **the
156
+ outsider** (can a reader who was not in the room set OATS up from `oats.setup`
157
+ alone?) and **the seam** (does every cross-project read resolve to a real node
158
+ with a real owner?).
159
+
160
+ ## Out of scope
161
+
162
+ Desktop (Phase F, its own boundary); oats.aweb 1.13.0 re-land (held on the
163
+ aweb release); legacy `~/OATS` deployment cutover (the operator's, on the
164
+ published 0.26.0).
@@ -376,12 +376,32 @@ the pre-fix marker and is never accepted for dispatch.
376
376
  import; `--instructions-file`/`--def-file` refused with `E_BAD_ARGS`). Test:
377
377
  the deployment tree is byte-identical after a success, a refusal and an
378
378
  unknown-soul preview.
379
+ **Workspace deployments (0.25.1+) — one stated exception**: the first preview
380
+ of a workspace soul may populate the deployment's per-commit soul cache
381
+ (`agents/<soul>/souls/<commit>/`, the swappable `agents/<soul>/soul` pointer,
382
+ `soulFetched: true` in the result). That cache is derived, content-addressed
383
+ and idempotent — the same member commit yields the same bytes, a later
384
+ preview of the same commit writes nothing — and nothing else moves: no lock,
385
+ no event, no home, no instance. A Desktop treats a preview as
386
+ side-effect-free for everything it shows; it must not assume the deployment
387
+ directory's byte-identity across the FIRST preview of a soul or commit.
379
388
  - **Exact root**: `spawn <soul> --agents-root <abs>` binds the soul to that root
380
389
  (as inspect/readiness take it) — no team-soul / capability-agent / importable-
381
390
  def fallback; mismatch → `E_SOUL_UNKNOWN`. The preview echoes
382
391
  `subject {soul, agentsRoot|null, dir|null}` **as given, byte-exact**.
383
392
  - **Decision binding**: `decision {instance, home, branch, base{ref,oid},
384
- revision}` (24-hex). Apply with `spawn … --expect-decision <revision>`: the
393
+ effective{…, providers}, resolution, revision}` (24-hex). From 0.25.6
394
+ (`features: served-identity`) `effective.providers` is the merged
395
+ per-module payload each provider will receive (`{ "<cap>": {…} }`, exactly
396
+ `settings.<cap>` of the preview) — so a confirmed apply binds every
397
+ provider fact (an identity choice, a delivery mode) **by value**; a Desktop
398
+ that changes a provider field re-previews. `instances[].identity` (status)
399
+ and `selected.identity` (inspect) carry the served principal a messaging
400
+ provider reported: `{ mode: "local"|"global", alias, team, address|null,
401
+ resident|null, grant?: { id, expiresAt, scopes }, provider }`; absent when
402
+ no provider emitted one. A Desktop offers the identity choice as
403
+ `--provider <cap> identity.mode=… identity.resident=…` — there is no
404
+ kernel flag for it. Apply with `spawn … --expect-decision <revision>`: the
385
405
  kernel recomputes name/home/branch/base under the same placement path and
386
406
  refuses **`E_DECISION_STALE`** with `details.decision` (the fresh one) on ANY
387
407
  drift — no auto-suffix, no silent re-base, nothing created. A GUI re-previews
@@ -774,9 +794,12 @@ written. Captured selectors are refused (`E_BAD_ARGS`).
774
794
 
775
795
  - `sync` is the `syncApi: 1` report of the first sync (members, packages,
776
796
  changes, `approvalNeeded`, `problems`); `lock` is the lock it wrote.
777
- - **Exit `2` with `ok: true`** when `sync.approvalNeeded` is non-empty (approval
778
- is interactive-only; tell the operator to run `oats sync --dir <dir>` in a
779
- terminal). Exit `0` otherwise.
797
+ - **Exit `2` with `ok: true`** when `sync.approvalNeeded` is non-empty. Approve
798
+ non-interactively with `oats sync --dir <dir> --approve <id>@<version> …`
799
+ (0.25.2+; `<version>` is `approvalNeeded[].version` verbatim — a catalog
800
+ version, or the full commit OID for a git-pinned package); a Desktop renders
801
+ `approvalNeeded[].executables` (the digest of the executable set it is
802
+ approving) and `targets`, then runs that command. Exit `0` otherwise.
780
803
  - `hosting` states decision 26 (the kernel cannot see forge visibility, so it
781
804
  reports `hostIsMember` and the rule rather than judging).
782
805
  - `next.clone[]` is one row per **confirmed** member (`url` = what the
@@ -801,9 +824,10 @@ written. Captured selectors are refused (`E_BAD_ARGS`).
801
824
 
802
825
  Discovers, confirms membership, resolves `packages:` to commits, writes
803
826
  `oats-lock.json` (lockfileVersion 3), reports. **Exit `2` with `ok: true`** when
804
- the lock was written but approvals are pending (`approvalNeeded` non-empty);
805
- approval is interactive-only, so a Desktop must tell the operator to run
806
- `oats sync` in a terminal. Exit `0` otherwise.
827
+ the lock was written but approvals are pending (`approvalNeeded` non-empty).
828
+ Approve with repeatable `--approve <id>@<version>` (0.25.2+, non-interactive;
829
+ `<version>` = `approvalNeeded[].version` verbatim); the interactive prompt is
830
+ the TTY fallback, not the contract. Exit `0` otherwise.
807
831
 
808
832
  ```json
809
833
  {"syncApi":1,
@@ -1052,6 +1076,10 @@ refreshes the home in place:
1052
1076
  materialized modules is not supported yet; re-spawn"): its `AGENTS.md` was
1053
1077
  composed from the soul at a recorded member commit plus the materialized
1054
1078
  modules' injects, and the instance never changes under itself (decision 7).
1079
+ **Desktop contract (Phase F, F4)**: for a module home, show the drift rows
1080
+ and offer *re-spawn* (preview → apply of the same soul/purpose, then retire
1081
+ the old instance); do not offer "recompose". A kernel recompose for module
1082
+ homes is not planned — an instance never changes under itself.
1055
1083
  The refresh path for such a home is a new spawn (the soul is re-fetched at
1056
1084
  the member's current commit). `session-recompose` **stays advertised** in
1057
1085
  `features[]` because the verb still works for classic homes; gate the UI
@@ -137,8 +137,25 @@ the event first" in the inject and skill; contribute launch arguments so the
137
137
  session is woken; enforce the soul type's `reach` on both sides; state
138
138
  whether the address outlives the instance; and keep task coordination out.
139
139
  Any messaging provider emits `identity: { mode, alias, team, address|null,
140
- resident|null, grant?: { id, expiresAt, scopes } }` in its spawn meta;
141
- `oats.aweb` is the reference implementation.
140
+ resident|null, grant?: { id, expiresAt, scopes } }` in its spawn meta (and
141
+ in its launch meta when it renews); `oats.aweb` is the reference
142
+ implementation. **This is a messaging-layer contract, not an oats.aweb
143
+ detail** (decision 27): from kernel 0.25.6 the kernel copies it through as
144
+ the principal the instance *acts as* — `oats status --json
145
+ instances[].identity`, the roster's `identity:` line, `oats inspect --home …
146
+ selected.identity` — preferring the capability whose captured layer is
147
+ `messaging`, adding `provider: <capability id>`, and never interpreting
148
+ `grant`. The kernel offers no `--identity` flag: the choice travels as
149
+ `--provider <cap> identity.mode=… identity.resident=…` and is bound by the
150
+ spawn decision's `effective.providers`.
151
+
152
+ A provider whose settings include a **host fact** — a custody directory, a
153
+ state root — declares that key `hostOnly: true` in its manifest. The
154
+ resolver then accepts it **only** from the deployment's `oats-local.yaml`
155
+ `settings.<cap>` and refuses it in the workspace file, `byTeam` payloads, a
156
+ soul's slot payload and `--provider` flags (`E_WORKSPACE_SCHEMA`, reason
157
+ `host-only-key`, path and key named). The provider cannot enforce this
158
+ itself: it receives one merged payload without provenance.
142
159
 
143
160
  **Tasks.** Teach claim, update, block, hand off, and complete; identify the
144
161
  instance to the tracker in a way that survives it; keep conversation out.
@@ -0,0 +1,37 @@
1
+ # OATS 0.25.6
2
+
3
+ Decision 27 — per-spawn identity choice (local | global) — lands its kernel half.
4
+ No new CLI flags; three provider-neutral additions and one manifest attribute.
5
+
6
+ ## Added
7
+
8
+ - **`decision.effective.providers`** — the spawn decision now binds the merged
9
+ per-module payloads the providers will receive (exactly the preview's
10
+ `settings.<cap>`), so a confirmed apply covers every provider fact by value.
11
+ - **`hostOnly` settings keys** — a capability manifest may mark a settings key
12
+ `hostOnly: true`; the resolver accepts it only from the deployment's
13
+ `oats-local.yaml settings.<cap>` and refuses it in the workspace file, `byTeam`
14
+ payloads, a soul's slot payload and `--provider` flags (`E_WORKSPACE_SCHEMA`,
15
+ reason `host-only-key`). Closes the documented hole where a committed file
16
+ could point a messaging provider at a custody root.
17
+ - **Served identity on the roster** — `oats status --json instances[].identity`,
18
+ the text roster's `identity:` line and `oats inspect --home … selected.identity`
19
+ carry the principal an instance acts as, copied from its messaging provider's
20
+ hook meta (`{ mode, alias, team, address, resident, grant?, provider }`).
21
+ - `oats version --json` `features[]` gains `served-identity`.
22
+
23
+ ## Desktop (10B-0)
24
+
25
+ - **Terminal owner leases** (PR #117) — the Desktop's terminal wire is now
26
+ `terminalApi: 2`: every terminal is a lease (256-bit token) held by the
27
+ admitted renderer *document*; a navigation, reload or renderer crash revokes
28
+ the document and cleans its viewers; the 20-terminal cap is reserved
29
+ synchronously so parallel opens cannot overshoot; writes are bounded and
30
+ one-way. Maintainer's native gate (real Electron, tmux, PTY) passed 23/23:
31
+ open/ready/write/close, quota, reload revocation, source isolation, no
32
+ residue after quit. See `packages/desktop/docs/terminal-owner-leases.md`.
33
+
34
+ ## For providers
35
+
36
+ oats.aweb 1.13.0 (next) can declare `residents` `hostOnly`. Any messaging provider
37
+ that emits `identity` in its spawn/launch meta appears on the roster.
@@ -0,0 +1,11 @@
1
+ # OATS 0.25.7
2
+
3
+ ## Fixed
4
+
5
+ - **A v2 deployment is a configuration boundary.** The ancestor walk that
6
+ composes legacy `oats-config.yaml` levels now stops at the directory holding
7
+ `oats-local.yaml`. A deployment created inside another scope (a scratch
8
+ deployment under an operator's workspace, a fixture under a repository) no
9
+ longer inherits the outer scope's files, and `oats inspect` no longer names
10
+ the outer checkout as its `scope.workspace` or lists the outer files in
11
+ `scope.chain`. Found by the Desktop engineer's Phase F study.
package/lib/core.mjs CHANGED
@@ -781,6 +781,13 @@ export function configChain(startDir) {
781
781
  while (true) {
782
782
  const cfg = loadLevelConfig(d);
783
783
  if (cfg) chain.push(cfg);
784
+ // A v2 deployment root (the directory holding oats-local.yaml) is a
785
+ // configuration boundary: nothing above it composes into it. Without this
786
+ // stop, a deployment created under another scope (a scratch deployment
787
+ // inside an operator's workspace, a fixture under a repo) inherited the
788
+ // ancestors' oats-config.yaml and `oats inspect` reported the outer scope
789
+ // as its workspace (Desktop Phase F study, 2026-09-24).
790
+ if (existsSync(join(d, "oats-local.yaml"))) break;
784
791
  const parent = dirname(d);
785
792
  if (parent === d) break;
786
793
  d = parent;
@@ -6603,7 +6610,14 @@ function* spawnBody(root, agent, o = {}) {
6603
6610
  // Workspace model: the decision binds WHAT WILL BE MATERIALIZED — the
6604
6611
  // resolution revision (member commits, package commits, payloads). A member
6605
6612
  // that moved between preview and apply changes it → E_DECISION_STALE.
6606
- if (o.prepared) d.resolution = o.prepared.resolution.revision;
6613
+ if (o.prepared) {
6614
+ d.resolution = o.prepared.resolution.revision;
6615
+ // Decision 27 (K1′): the decision also binds the merged per-module payloads the spawn will
6616
+ // hand each provider (exactly what reaches OATS_SETTINGS) — so a confirmed apply covers every
6617
+ // provider fact (an identity choice, a delivery mode) by value, not only through the
6618
+ // resolution revision. Kernel-agnostic: whatever keys the payloads carry.
6619
+ d.effective.providers = structuredClone(o.prepared.resolution.payloads ?? {});
6620
+ }
6607
6621
  d.revision = createHash("sha256").update(canonicalJson(d)).digest("hex").slice(0, 24);
6608
6622
  return d;
6609
6623
  };
@@ -7356,6 +7370,25 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
7356
7370
  }
7357
7371
  }
7358
7372
 
7373
+ /** Decision 27 (K2): the principal an instance ACTS AS, as its messaging-layer provider reported
7374
+ * it — a documented layer contract `{ mode: "local"|"global", alias, team, address|null, resident|null,
7375
+ * grant?: { id, expiresAt, scopes } }` under `identity` in the provider's hook meta. The kernel copies it
7376
+ * through and never interprets `grant`. Preference: the capability whose captured layer is "messaging";
7377
+ * else any capability that emitted an `identity` object. Null when none did. */
7378
+ export function servedIdentityOf(meta) {
7379
+ const cm = meta?.capabilityMeta;
7380
+ if (!cm || typeof cm !== "object") return null;
7381
+ const runtime = Array.isArray(meta.capabilityRuntime) ? meta.capabilityRuntime : [];
7382
+ const messaging = runtime.filter((c) => c && c.layer === "messaging").map((c) => c.id);
7383
+ const pick = (ids) => { for (const id of ids) { const v = cm[id]?.identity; if (v && typeof v === "object" && !Array.isArray(v)) return { ...v, provider: id }; } return null; };
7384
+ return pick(messaging) ?? pick(Object.keys(cm));
7385
+ }
7386
+ /** One line for a roster/inspect: `acts as <address> via grant, expires <t>` or `alias <alias> on <team>`. */
7387
+ export function servedIdentityLine(identity) {
7388
+ if (!identity) return null;
7389
+ if (identity.mode === "global" && identity.grant) return `acts as ${identity.address || identity.alias || identity.resident || "?"} via grant${identity.grant.expiresAt ? `, expires ${identity.grant.expiresAt}` : ""}`;
7390
+ return `alias ${identity.alias || "?"} on ${identity.team || "?"}`;
7391
+ }
7359
7392
  export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
7360
7393
  const windows = tmuxWindows(tmuxSession);
7361
7394
  const readInstancesOf = (agentDir) => {
@@ -7394,7 +7427,8 @@ export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
7394
7427
  try { const state = inspectHerdr({ ...meta.sessionTarget, binary: "herdr" }); liveness = { running: state.present, runtimeState: state.status }; }
7395
7428
  catch (error) { liveness = { running: null, runtimeState: "unreachable", runtimeError: error.message }; }
7396
7429
  }
7397
- return { ...meta, ...(meta.launch && typeof meta.launch === "object" ? { launch: redactLaunchRecipe(meta.launch) } : {}), ...(typeof meta.command === "string" ? { command: redactLaunchCommand(meta.command) } : {}), ...liveness, ...(rollbackIncomplete ? { rollbackIncomplete } : {}), ...(retirePending ? { retirePending } : {}) };
7430
+ const identity = servedIdentityOf(meta);
7431
+ return { ...meta, ...(identity ? { identity } : {}), ...(meta.launch && typeof meta.launch === "object" ? { launch: redactLaunchRecipe(meta.launch) } : {}), ...(typeof meta.command === "string" ? { command: redactLaunchCommand(meta.command) } : {}), ...liveness, ...(rollbackIncomplete ? { rollbackIncomplete } : {}), ...(retirePending ? { retirePending } : {}) };
7398
7432
 
7399
7433
  });
7400
7434
  };
package/lib/resolve.mjs CHANGED
@@ -121,6 +121,23 @@ function assertNoReservedKey(value, path) {
121
121
  }
122
122
  }
123
123
 
124
+ /** Settings keys a manifest marks `hostOnly: true` (decision 27, K1″). */
125
+ export function hostOnlyKeys(manifest) {
126
+ const out = new Set();
127
+ const decl = isObject(manifest?.settings) ? manifest.settings : {};
128
+ for (const [key, spec] of Object.entries(decl)) if (isObject(spec) && spec.hostOnly === true) out.add(key);
129
+ return out;
130
+ }
131
+ /** Refuse a hostOnly key in a committed or per-spawn payload layer: only oats-local.yaml `settings.<cap>` may carry it. */
132
+ function assertNoHostOnlyKey(value, path, hostOnly, capability) {
133
+ if (!hostOnly.size || !isObject(value)) return;
134
+ for (const key of hostOnly) {
135
+ if (Object.hasOwn(value, key)) {
136
+ throw fail("E_WORKSPACE_SCHEMA", `${path}/${key}: ${show(key)} is a host-only setting of ${capability} (its manifest marks it hostOnly) — it may appear only in the deployment's oats-local.yaml under settings.${capability}, never in a committed workspace or soul file or a --provider flag (decision 27)`, { path: `${path}/${key}`, key, capability, reason: "host-only-key" });
137
+ }
138
+ }
139
+ }
140
+
124
141
  /** Canonical JSON: object keys sorted (recursively), arrays in order, no whitespace. */
125
142
  export function canonicalJson(value) {
126
143
  if (value === undefined) return "null";
@@ -569,15 +586,21 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
569
586
  for (const slot of SLOTS) if (isObject(definition[slot])) assertNoReservedKey(definition[slot], `/${slot}`);
570
587
  for (const m of modules) {
571
588
  const layers = [];
589
+ // Decision 27 (K1″): a manifest may mark a settings key `hostOnly` — a host fact (a custody
590
+ // path, a state directory) that only the deployment's own oats-local.yaml may supply. Every
591
+ // committed or per-spawn layer is refused with that key present, BEFORE the merge, because the
592
+ // provider receives one merged payload without provenance and cannot enforce this itself.
593
+ const hostOnly = hostOnlyKeys(m.manifest);
594
+ const committed = (payload, path) => { assertNoHostOnlyKey(payload, path, hostOnly, m.name); layers.push(payload); };
572
595
  if (m.layer === "messaging" && isObject(workspace?.messaging)) {
573
596
  // Decision 23: base ⊕ byTeam[soul.team]; `byTeam` never reaches the provider (nor may a team's own payload nest one).
574
597
  const { byTeam, ...base } = workspace.messaging;
575
- layers.push(base);
576
- if (team !== null && isObject(byTeam) && isObject(byTeam[team])) { assertNoReservedKey(byTeam[team], `/messaging/byTeam/${team}`); layers.push(byTeam[team]); }
598
+ committed(base, "/messaging");
599
+ if (team !== null && isObject(byTeam) && isObject(byTeam[team])) { assertNoReservedKey(byTeam[team], `/messaging/byTeam/${team}`); committed(byTeam[team], `/messaging/byTeam/${team}`); }
577
600
  }
578
- if (m.layer && isObject(definition[m.layer])) { assertNoReservedKey(definition[m.layer], `/${m.layer}`); layers.push(definition[m.layer]); }
579
- if (Object.hasOwn(settings, m.name) && isObject(settings[m.name])) { assertNoReservedKey(settings[m.name], `/settings/${m.name}`); layers.push(settings[m.name]); }
580
- if (Object.hasOwn(providers, m.name) && isObject(providers[m.name])) layers.push(providers[m.name]);
601
+ if (m.layer && isObject(definition[m.layer])) { assertNoReservedKey(definition[m.layer], `/${m.layer}`); committed(definition[m.layer], `/${m.layer}`); }
602
+ if (Object.hasOwn(settings, m.name) && isObject(settings[m.name])) { assertNoReservedKey(settings[m.name], `/settings/${m.name}`); layers.push(settings[m.name]); } // the host layer: hostOnly keys are legal here
603
+ if (Object.hasOwn(providers, m.name) && isObject(providers[m.name])) committed(providers[m.name], `/spawn/providers/${m.name}`);
581
604
  payloads[m.name] = mergePayload(...layers);
582
605
  }
583
606
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.25.5",
3
+ "version": "0.25.7",
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",