@awebai/oats 0.25.4 → 0.25.6

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.
@@ -3,7 +3,7 @@ import { spawnSync } from 'node:child_process';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { fs, join, dirname, safePath, readJSON, save, atomic, tree, materialize, digest, hash, withLock, exec, cleanEnv, fail, relPath, overlaps, resolve } from './io.mjs';
6
- import { metadata, noGit } from './config.mjs';
6
+ import { metadata, noGit, gitTimeoutMs } from './config.mjs';
7
7
  const validator = fileURLToPath(new URL('../skills/okf/scripts/okf-validate.mjs', import.meta.url));
8
8
  // Never let local replace refs reinterpret frozen OIDs, including inside Git's
9
9
  // transport subprocesses. Override even an explicitly supplied command env.
@@ -17,11 +17,19 @@ export function validateBase(root, base) {
17
17
  if(result.errors.length || result.warnings.length) fail('E_VALIDATION', [...result.errors,...result.warnings].join('; '));
18
18
  return {files,meta,digest:digest(files)};
19
19
  }
20
- function rawBlob(cwd,oid) {
21
- const result=spawnSync('git',['--no-replace-objects','-c','core.hooksPath=/dev/null','-C',cwd,'cat-file','blob',oid],{cwd,env:gitEnv(),timeout:30000,maxBuffer:16*1024*1024});
20
+ /** Write a blob straight to a file descriptor: no in-memory buffer, so object
21
+ * size never limits what a base may hold. The remote budget applies because a
22
+ * partial clone fetches a missing blob on its first read. */
23
+ function writeBlob(cwd,oid,target,mode) {
24
+ const fd=fs.openSync(target,'wx',mode);
25
+ let result; try { result=spawnSync('git',['--no-replace-objects','-c','core.hooksPath=/dev/null','-C',cwd,'cat-file','blob',oid],{cwd,env:gitEnv(),timeout:gitTimeoutMs(),stdio:['ignore',fd,'pipe']}); } finally { fs.closeSync(fd); }
22
26
  if(result.error || result.status!==0) fail('E_COMMAND','Git object read failed');
23
- return result.stdout;
27
+ fs.chmodSync(target,mode);
24
28
  }
29
+ /** A tree entry the store never materializes: outside the knowledge base root.
30
+ * Its absence from a staging tree is by construction; its presence is a
31
+ * worker's doing and is judged like any other change. */
32
+ const outsideBase=(base,p)=>!(base.root==='.' || p===base.root || p.startsWith(base.root+'/'));
25
33
  function materializeGitObjects(base,dest,head) {
26
34
  immutableCommit(dest,head);
27
35
  const entries=gitTreeEntries(dest,head);
@@ -35,20 +43,32 @@ function materializeGitObjects(base,dest,head) {
35
43
  // Index/HEAD updates do not apply content filters. They preserve the normal
36
44
  // Git worktree needed by existing diff/publication guards without checkout.
37
45
  git(dest,['read-tree',head]);git(dest,['update-ref','--no-deref','HEAD',head]);
46
+ // Only the knowledge base is materialized: the index carries the whole tree
47
+ // for publication, but bytes outside the base root are never read, so the
48
+ // size of the rest of the repository does not matter. The scope checks know
49
+ // that an entry outside the root is expected to be absent (see outsideBase).
38
50
  for(const [p,entry] of entries) {
51
+ if(outsideBase(base,p)) continue;
39
52
  const target=safePath(join(dest,p));fs.mkdirSync(dirname(target),{recursive:true});
40
- if(entry.mode==='160000') {fs.mkdirSync(target);continue;}
41
- const bytes=rawBlob(dest,entry.oid);
42
- if(entry.mode==='120000') fs.symlinkSync(bytes.toString('utf8'),target);
43
- else {fs.writeFileSync(target,bytes,{flag:'wx',mode:entry.mode==='100755'?0o755:0o644});fs.chmodSync(target,entry.mode==='100755'?0o755:0o644);}
53
+ writeBlob(dest,entry.oid,target,entry.mode==='100755'?0o755:0o644);
44
54
  }
45
55
  }
46
56
  function clone(base, dest, selectedHead) {
47
57
  safePath(dest);
48
58
  if(fs.existsSync(dest)) fail('E_PATH',`staging destination exists: ${dest}`);
49
59
  fs.mkdirSync(dirname(dest),{recursive:true});
50
- git(dirname(dest),['clone','--no-hardlinks','--no-checkout','--',base.repository,dest]);
51
- git(dest,['fetch','origin',`refs/heads/${base.acceptedBranch}`]);
60
+ // Fetch only what the store reads: the accepted branch, trees now and blobs
61
+ // on demand. Only a remote that does not offer object filtering gets a plain
62
+ // single-branch clone instead; any other failure is the failure it is. The
63
+ // ancestry the publication checks walk is present either way.
64
+ const cloneArgs=['clone','--no-hardlinks','--no-checkout','--single-branch','--branch',base.acceptedBranch];
65
+ try { git(dirname(dest),[...cloneArgs,'--filter=blob:none','--',base.repository,dest],{timeout:gitTimeoutMs()}); }
66
+ catch(e) {
67
+ if(e.code!=='E_COMMAND' || !/filter/i.test(e.message)) throw e;
68
+ fs.rmSync(dest,{recursive:true,force:true});
69
+ git(dirname(dest),[...cloneArgs,'--',base.repository,dest],{timeout:gitTimeoutMs()});
70
+ }
71
+ git(dest,['fetch','origin',`refs/heads/${base.acceptedBranch}`],{timeout:gitTimeoutMs()});
52
72
  const head=selectedHead ?? git(dest,['rev-parse','FETCH_HEAD']);
53
73
  verifyRemote(base,dest);
54
74
  materializeGitObjects(base,dest,head);
@@ -142,7 +162,9 @@ function withVerificationIndex(cwd,fn) {
142
162
  }
143
163
  export function verifyGitScope(base, dest, baseline, { checkModes = true } = {}) {
144
164
  return withVerificationIndex(dest,read=>{
145
- const names=read(['diff','--name-only','-z',baseline,'--']).split('\0').filter(Boolean);
165
+ // Entries outside the root are never materialized, so their absence is
166
+ // not a deletion; a present outside file that differs still is a change.
167
+ const names=read(['diff','--name-only','-z',baseline,'--']).split('\0').filter(Boolean).filter(p=>!(outsideBase(base,p) && !fs.existsSync(join(dest,p))));
146
168
  // A restored working file can conceal a staged, unauthorized index entry.
147
169
  names.push(...read(['diff','--cached','--name-only','-z',baseline,'--']).split('\0').filter(Boolean));
148
170
  names.push(...read(['ls-files','--others','--exclude-standard','-z']).split('\0').filter(Boolean));
@@ -303,7 +325,7 @@ export function gitPublish(base, stage, proposal, receipt, persist, {beforePubli
303
325
  const baseline=immutableCommit(cwd,stage.head);
304
326
  verifyPublicationTree(base,cwd,baseline.tree,baseline.tree,proposal.before);
305
327
  // Baseline must still be accepted. Never rebase model output without rejudging it.
306
- git(cwd,['fetch','origin',`refs/heads/${base.acceptedBranch}`]);
328
+ git(cwd,['fetch','origin',`refs/heads/${base.acceptedBranch}`],{timeout:gitTimeoutMs()});
307
329
  const accepted=git(cwd,['rev-parse','FETCH_HEAD']);
308
330
  if(accepted!==stage.head) {
309
331
  // A known or uncertain previously created PR may be reconciled, but a
@@ -350,13 +372,13 @@ export function gitPublish(base, stage, proposal, receipt, persist, {beforePubli
350
372
  const publication=immutableCommit(cwd,receipt.commit);
351
373
  if(publication.parents.length!==1 || publication.parents[0]!==stage.head) fail('E_CONFIRM','publication commit must have exactly the frozen baseline as its parent');
352
374
  verifyPublicationTree(base,cwd,baseline.tree,publication.tree,proposal.after,proposal.before);
353
- const remoteTip=()=>git(cwd,['ls-remote','--heads','origin',`refs/heads/${branch}`]).split(/\s/)[0] || null;
375
+ const remoteTip=()=>git(cwd,['ls-remote','--heads','origin',`refs/heads/${branch}`],{timeout:gitTimeoutMs()}).split(/\s/)[0] || null;
354
376
  let tip=remoteTip();
355
377
  if(tip && tip!==receipt.commit) fail('E_PR','publication branch has unexpected commit; never force push');
356
378
  if(tip!==receipt.commit) {
357
379
  beforePublish();
358
380
  receipt.status='push-intent'; persist();
359
- try { verifyRemote(base,cwd);git(cwd,['push','--no-follow-tags','--recurse-submodules=no','origin',`${receipt.commit}:refs/heads/${branch}`]); }
381
+ try { verifyRemote(base,cwd);git(cwd,['push','--no-follow-tags','--recurse-submodules=no','origin',`${receipt.commit}:refs/heads/${branch}`],{timeout:gitTimeoutMs()}); }
360
382
  catch(e) { receipt.status='push-unknown'; receipt.error=e.message; persist(); throw e; }
361
383
  tip=remoteTip(); if(tip!==receipt.commit) {receipt.status='push-unknown';persist();fail('E_CONFIRM','pushed head not confirmed');}
362
384
  }
@@ -375,7 +397,7 @@ export function gitPublish(base, stage, proposal, receipt, persist, {beforePubli
375
397
  const pr=matching[0];receipt.pr=pr;receipt.status='delivered';receipt.deliveredAt ||= new Date().toISOString();persist();
376
398
  if(pr.state==='CLOSED' && !pr.mergedAt) {receipt.status='rejected';persist();fail('E_PR','PR closed without merge; retained proposal requires operator review');}
377
399
  if(pr.mergedAt) {
378
- git(cwd,['fetch','origin',`refs/heads/${base.acceptedBranch}`]);
400
+ git(cwd,['fetch','origin',`refs/heads/${base.acceptedBranch}`],{timeout:gitTimeoutMs()});
379
401
  const merge=pr.mergeCommit?.oid;
380
402
  if(!merge) fail('E_CONFIRM','merged PR lacks merge commit');
381
403
  git(cwd,['merge-base','--is-ancestor',merge,'FETCH_HEAD']);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "capability": "oats.okf",
3
3
  "command": "okf",
4
- "version": "2.1.3",
4
+ "version": "2.1.4",
5
5
  "compatibility": {
6
6
  "oats": ">=0.24.4"
7
7
  },
@@ -26,6 +26,9 @@
26
26
  },
27
27
  "state-dir": {
28
28
  "description": "Absolute host-owned durable state directory for portable bindings. It is never derived from an instance home."
29
+ },
30
+ "git-timeout": {
31
+ "description": "Seconds allowed for each Git operation that talks to a remote (clone, fetch, push, ls-remote); default 600. Local object reads keep a short fixed limit. Raise it for large repositories behind slow links."
29
32
  }
30
33
  },
31
34
  "agents": [
@@ -121,8 +121,9 @@ captured ProviderBinding as execution authority:
121
121
  `payload.bindings`. Workspace, adoption, and operator values remain separate
122
122
  inputs to the shared resolver; OKF does not select their precedence.
123
123
  - Durable placement is explicit selected settings: physical absolute
124
- `bindings-file` and `state-dir`, plus selected `harvest-runtime` and optional
125
- `harvest-model`. Never derive state from an instance home.
124
+ `bindings-file` and `state-dir`, plus selected `harvest-runtime`, optional
125
+ `harvest-model` and optional `git-timeout` (seconds for remote Git
126
+ operations, default 600). Never derive state from an instance home.
126
127
  - Provider codecs run only after exact retained executable approval. Their
127
128
  populated binding is not proof of readiness, enrollment, credentials, privacy
128
129
  or publication authority. Respect typed non-ready results.
@@ -262,6 +262,17 @@ return `meta`, `brief`, `warning`, or runtime-specific `launch` arguments. A
262
262
  **spawn hook only** may also return an `env` object for the launched process;
263
263
  returning `env` from retire or soul-scaffold is an explicit contract error.
264
264
 
265
+ A **launch hook** runs at every start and restart of a home for each provider
266
+ captured at spawn (under its captured settings). Its `launch` arguments and
267
+ `env` replace that provider's previous contribution whole. Its `meta`, when
268
+ returned, replaces that provider's entry in `instance.json.capabilityMeta`
269
+ after the start succeeds — the same record the spawn hook wrote and the retire
270
+ hook later reads as `OATS_META` — so a provider that re-issues a credential at
271
+ start (a renewed session grant, for example) leaves the CURRENT one on record. A
272
+ launch hook that answers without `meta` keeps its previous entry; a start whose
273
+ preparation fails changes nothing. (Kernel ≥ 0.25.5; earlier kernels collected
274
+ launch `meta` and discarded it.)
275
+
265
276
  Hook environment values are strings, at most 8192 UTF-8 bytes, with no NUL or
266
277
  newlines. Names use the portable environment grammar and must belong to an
267
278
  unambiguous vendor namespace. Only a dotted capability ID participates: its
@@ -271,6 +282,14 @@ for this contract. Hyphenated vendors are also excluded because translating a
271
282
  hyphen to `_` would let `aweb-evil.*` collide with names already inside
272
283
  `aweb.*`'s `AWEB_*` namespace.
273
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
+
274
293
  A hook may return only names in its manifest's exact `environment` declaration.
275
294
  For package capabilities that declaration is part of the integrity-locked tree
276
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
@@ -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.3 PUBLISHED** (workspace model A–C; team-review fixes; operator-rebuild round; OATS_SOUL_ID) · **OKF 2.1.4 pending human GO** (owner pin by id, clone/timeout, retire-schedule seam; Antares writes the PRs) · 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-23 20:00Z · **0.25.0–0.25.5 PUBLISHED** (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,33 @@ 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
+
583
+ ### 0.25.5 — launch-hook `meta` is persisted
584
+
585
+ `runLifecycleHooks("launch")` collected each capability's `meta` and the
586
+ start/restart path discarded it (only `contributions` and `env` were consumed).
587
+ From 0.25.5 a successful start merges `res.meta` per capability into
588
+ `instance.json.capabilityMeta` — the record spawn writes and retire reads as
589
+ `OATS_META`. A hook answering without `meta` keeps its prior entry; a failed
590
+ launch preparation writes nothing. No new field, flag or hook event; this is
591
+ the documented hook return finally honoured (decision 27, K3′). Driver: a
592
+ provider renewing a session grant at every start would otherwise leave the
593
+ original grant id on record and retire would revoke the wrong grant.
594
+
568
595
  ### 0.25.3 — `OATS_SOUL_ID` (stable soul identity for providers)
569
596
 
570
597
  The per-commit soul cache (0.25.1, M1) made `realpath(<home>/soul)` change with every
@@ -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,129 @@
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
+ ## Slices, in order
11
+
12
+ ### D1 — Knowledge centralisation (IN PROGRESS)
13
+
14
+ Goal: every roster soul's knowledge lives in the central base
15
+ `awebai/oats-knowledge` (OKF 2.1.4), one owned node each; the in-repo
16
+ `agents/*/soul/knowledge` bundles stop receiving writes and are decommissioned
17
+ when no live instance links them.
18
+
19
+ Done: nodes `oats-operator-expert` and `integrations-expert` chartered
20
+ (oats-knowledge PR #3); eight attributed seeds landed in
21
+ `agents/oats-expert/soul/knowledge/inbox` (oats PR #121); the roster amendment
22
+ accepted; the rule "no per-soul knowledge merges" in force.
23
+
24
+ Work:
25
+ 1. **Copy-migrate by judgement** (the 2026-09-21 method: two-part test plus the
26
+ 2026-09-24 recipe refinement; not copying): `agents/oats-expert/soul/knowledge`
27
+ (~130 files) → node `oats-expert`; `agents/oats-desktop-engineer/soul/knowledge`
28
+ (~120) → `oats-desktop-expert`; `agents/cli-dev/soul/knowledge` (~155) →
29
+ `oats-kernel-expert`; `agents/integrations-expert/soul/knowledge` (13) →
30
+ `integrations-expert`. Others (`dev-coordinator`, `docs-expert`, `ux-designer`,
31
+ `lead`, `oats-coordinator`) are assessed for the few universal concepts they
32
+ hold and otherwise not carried. One PR per node on oats-knowledge, reviewed
33
+ by the lead; the OSS coordinator reviews the operator and integrations PRs.
34
+ 2. **Seed the operator node** — attributed to `oats-expert-antares`:
35
+ MOVE (not copy) from the oats-expert bundle: `lessons/the-aweb-team-root-must-sit-where-the-spawn-hook-looks`,
36
+ `lessons/okf-state-directory-must-sit-outside-every-work-tree`,
37
+ `lessons/capability-trust-hash-covers-the-installation-record`,
38
+ `lessons/a-locally-minted-oats-identity-has-no-cross-team-first-contact-address`,
39
+ `lessons/stale-checkout-serves-stale-soul`,
40
+ `playbooks/rebuild-a-deployment-in-scratch-against-local-bare-remotes`;
41
+ generalise the R1–R10 rebuild findings and the published-combination
42
+ verifications from `stewardship/delivery-log` (the log keeps the record).
43
+ From the inbox: `check-the-record-before-redesigning-identity`,
44
+ `grant-custody-service-and-renewal-belong-on-the-custody-host` (operator
45
+ half), `the-wake-broker-accepts-a-grant-home`.
46
+ 3. **Seed the integrations node** — from the inbox:
47
+ `a-merged-provider-payload-cannot-enforce-host-only-keys`,
48
+ `aw-grant-commands-resolve-the-identity-from-cwd-only` (discipline half),
49
+ `oats-runtime-requirements-for-grant-backed-resident-operation`; from the
50
+ integrations-expert bundle: `fake-aw-must-model-real-refusals` and the rest
51
+ of this week's harvested lessons.
52
+ 4. **Route the remainder of the inbox**: aweb package expert (D3) gets
53
+ `aweb-grants-are-team-bound-to-the-custody-identitys-active-team`,
54
+ `a-grant-signed-send-must-name-the-subject-as-sender` and the aw halves;
55
+ kernel expert gets `path-keyed-owner-registry-breaks-under-per-commit-soul-copies`.
56
+ 5. **Rebind the souls** in `souls/<name>/`: the `knowledge:` grammar there is
57
+ still 0.24's (`capability` + `source: git:…@v2.1.2#oats-package`); v2 is
58
+ `capabilities: { oats.okf: { from: package } }` plus `okf.json`
59
+ (`{ version: 1, owner: <node owner uuid>, owns: ["oats/<node>"], reads: [...] }`)
60
+ and the store `oats` naming `awebai/oats-knowledge` / `knowledge` / `main`.
61
+ Add `souls/oats-operator-expert` (rename of `oats-setup-expert`, keeps the
62
+ `oats.setup` charter) and `souls/integrations-expert`.
63
+ 6. **Do NOT delete** `agents/<n>/soul/knowledge` or the legacy souls while a
64
+ live instance links them (human rule). Record which are live; decommission
65
+ as they retire; new spawns use `souls/<n>`.
66
+ 7. **Release playbook** (`oats-expert` node, stewardship area): landing order
67
+ for provider PRs (tag → pin on the branch → squash; a bundled provider never
68
+ lands ahead of its tag), the version literals to bump on a catalog bump,
69
+ the mirror checker, the bump-PR step. First entries: today's two lessons.
70
+
71
+ ### D2 — The OATS workspace as a v2 workspace (decisions 18, 19, 21)
72
+
73
+ `oats-workspace.yaml` at the `oats` repo (name `oats`; members = the seven
74
+ framework repos incl. `oats` itself; `packages:` = oats.framework / okf / aweb /
75
+ jira / linear / authoring / dev pinned from the catalog; `defaults.capabilities`
76
+ = `oats.core` from package + knowledge `oats.okf`); `oats-membership.yaml` ×7
77
+ (each package repo is a member AND a package publisher — non-collapse:
78
+ `packages:` resolves it as a package, membership only grants trust and a soul).
79
+ `package-catalog.json` stays the official marketplace (decision 21); the `oats`
80
+ repo keeps `oats-dev` as dev capabilities. A fresh deployment directory (asked
81
+ for, never named by convention — decision 9) is the acceptance: `oats onboard`
82
+ → `sync` → `approve` → spawn every roster soul `--no-launch`.
83
+
84
+ ### D3 — Souls to `souls/<name>` and the six package-expert souls (decision 20)
85
+
86
+ Each package repo carries `souls/<pkg>-expert` (okf-expert, aweb-expert,
87
+ jira-expert, linear-expert, authoring-expert, dev-expert), the expert in that
88
+ package, with a node in the central base from day one. **Seams named in the
89
+ charters** (roster amendment): `aweb-expert` READS
90
+ `aweb-protocol-expert` in base `aweb-oss-knowledge` (repo
91
+ `github.com/awebai/aweb`, root `knowledge/`, branch `main`, OKF 2.1.x
92
+ descriptor at `knowledge/okf-base.json`; owner `1913b77b-…`) through a
93
+ read-only store reference; `okf-expert` names its seam to the knowledge-theory
94
+ material in `oats-expert`. Whether the aweb bookshelf decisions the program
95
+ rests on are published into that node is the aweb side's call (asked).
96
+
97
+ ### D4 — `oats.core` and `oats.setup` rewritten (decision 22, W9b)
98
+
99
+ Not patched: written for the v2 world. `oats.core`: home layout, `oats status`
100
+ with modules/soul/identity rows, spawn preview → apply, `sync --approve`, the
101
+ two-directory boundary, what a module is. `oats.setup` (held by
102
+ `oats-operator-expert`): onboarding that ASKS for the deployment directory and
103
+ the workspace URL, the hosting rule (decision 26), the public-member executable
104
+ rule, the rebuild guide as procedure with the operator node as rationale.
105
+ Developer souls in `oats.dev` gain `promotesTo: <node>` (roster amendment);
106
+ the harvester delivers to that node as a PR the owning expert reviews.
107
+
108
+ ### D5 — Catalog update and 0.26.0
109
+
110
+ Catalog pins for the new package versions; **widen Desktop `ACCEPT_RANGE` and
111
+ the three pins FIRST** (minor bump rule); release notes; tag; the fresh
112
+ deployment from D2 rebuilt on the published artefacts by the OSS coordinator
113
+ (outsider verification) before the program board marks Phase D done.
114
+
115
+ ## Adversarial review (per slice)
116
+
117
+ Each slice's swarm is followed by a review swarm with the standing lenses
118
+ (direction against the decisions; correctness by reproduction; security —
119
+ trust at acquisition, hoisted paths, hook approval, host-only keys; docs as
120
+ contract — every guide claim has a test), plus two Phase-D-specific ones: **the
121
+ outsider** (can a reader who was not in the room set OATS up from `oats.setup`
122
+ alone?) and **the seam** (does every cross-project read resolve to a real node
123
+ with a real owner?).
124
+
125
+ ## Out of scope
126
+
127
+ Desktop (Phase F, its own boundary); oats.aweb 1.13.0 re-land (held on the
128
+ aweb release); legacy `~/OATS` deployment cutover (the operator's, on the
129
+ 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