@awebai/oats 0.24.4 → 0.24.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.
package/bin/oats.mjs CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  capabilityManifests, capabilityManifest, capabilityMissingRequires, capabilityIntegrity, capabilityTrust, capabilityExecutablePath, activateCapturedScaffold, loadCapturedDispatch, inspectPortableOnboarding, prepareCapturedComposition, resolveCapturedHelper, capturedNativeSessionAvailability, scaffoldCapturedInstance, startCapturedInstanceSession, withCapturedBindingFile, withCapturedInvocationContextFile,
27
27
  readCapabilityLocks, writeCapabilityLock, admitCapturedAction, beginCapturedIntent, settleCapturedIntent,
28
28
  parsePackageSource, inspectGitSourceRoot, acquirePackage, restorePackages, listInstalledPackages, readPackageLocks, readLockedConfigTemplates,
29
- officialCapabilityPackage, officialPackageCatalog, DEFAULT_PACKAGE_PATH,
29
+ officialCapabilityPackage, officialPackageCatalog, describeOfficialCatalog, DEFAULT_PACKAGE_PATH,
30
30
  approveCapability, approveAvailableCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
31
31
  packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir, installedCapabilitiesDir, ownedCapabilitiesDir, loadPackageManifestAt,
32
32
  resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, planInstanceResources, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
@@ -63,7 +63,7 @@ import { parsePortableSource } from "../lib/source-spec.mjs";
63
63
  const args = process.argv.slice(2);
64
64
  let cmd = args[0];
65
65
  const HELP_WORDS = new Set(["help", "--help", "-h"]);
66
- const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
66
+ const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "catalog", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
67
67
  const flag = (name) => {
68
68
  const i = args.indexOf(`--${name}`);
69
69
  return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
@@ -3059,6 +3059,20 @@ function renderMergeRegion(r) {
3059
3059
  }
3060
3060
 
3061
3061
  /** oats list — installed packages, exported capabilities, scopes. */
3062
+ /** `oats catalog [--json]` — the effective official package catalog, read-only.
3063
+ * Identity/discovery for consumers that cannot import the kernel (Desktop):
3064
+ * never acquires, never trusts, never fetches. */
3065
+ function catalogCmd() {
3066
+ const described = describeOfficialCatalog();
3067
+ if (JSON_MODE) { jsonOk(described); return; }
3068
+ console.log(`Official package catalog (${described.catalog.origin}: ${shortPath(described.catalog.file)})`);
3069
+ for (const p of described.packages) console.log(` ${p.package} ${p.url ?? "?"}@${p.ref ?? "?"} path: ${p.path}`);
3070
+ if (described.capabilityAliases.length) {
3071
+ console.log("Capability aliases:");
3072
+ for (const a of described.capabilityAliases) console.log(` ${a.capability} -> ${a.package}${a.capabilityInPackage !== a.capability ? ` (exports ${a.capabilityInPackage})` : ""}${a.available ? "" : " [package not in catalog]"}`);
3073
+ }
3074
+ console.log("Catalog identity grants no executable trust; acquire with `oats install <package>` and approve separately.");
3075
+ }
3062
3076
  function listCmd() {
3063
3077
  const dir = dirFlag();
3064
3078
  // FAIL-CLOSED (maintainer finding 3): list RAISES on invalid locks — an
@@ -4328,7 +4342,14 @@ function onboardCmd() {
4328
4342
  if (findAgent(root, SETUP_EXPERT)) throw Object.assign(new Error("oats-setup-expert already exists; it will not be overwritten"), { code: "E_AGENT_EXISTS" });
4329
4343
  const edition = loadSetupExpertEdition(values.get("workspace"));
4330
4344
  for (const key of ["description", "runtime", "model"]) if (edition.declaration[key] !== undefined) assertSafeConfigValue(edition.declaration[key], `setup edition ${key}`);
4331
- const catalog = officialPackageCatalog(), entry = catalog["oats.framework"];
4345
+ const catalog = officialPackageCatalog();
4346
+ // Catalog precedence: an explicit OATS_PACKAGE_CATALOG override, else the entry the WORKSPACE
4347
+ // publishes at the edition's revision, else the kernel's bundled snapshot. The bundled copy lags
4348
+ // every oats.framework release cut after this kernel's tag, and a second operator has no main
4349
+ // checkout to point an override at (0.24.5; second-operator finding).
4350
+ const bundledEntry = catalog["oats.framework"];
4351
+ const entry = process.env.OATS_PACKAGE_CATALOG ? bundledEntry : (edition.catalogEntry ?? bundledEntry);
4352
+ const catalogOrigin = process.env.OATS_PACKAGE_CATALOG ? "override" : edition.catalogEntry ? "workspace" : "bundled";
4332
4353
  if (!Object.hasOwn(catalog, "oats.framework") || !entry?.url || !entry.ref
4333
4354
  || SETUP_CAPABILITIES.some(id => { const m = officialCapabilityPackage(id); return !m.available || m.package !== "oats.framework" || m.migratedCapability !== id; })) {
4334
4355
  throw Object.assign(new Error("official oats.framework with core/setup aliases and a published revision is required"), { code: "needs-configuration" });
@@ -4360,10 +4381,17 @@ function onboardCmd() {
4360
4381
  const text = replaceCapabilitiesBlock(before ?? `name: ${scaffoldConfigName(deployment)}\n`, caps);
4361
4382
  mkdirSync(deployment, { recursive: true });
4362
4383
  acquired = acquirePackage(deployment, "oats.framework", { expectPackage: "oats.framework",
4363
- catalog(id, selector) { const selected = Object.hasOwn(catalog, id) ? catalog[id] : null; return selected?.url ? { url: selected.url, ref: selector || selected.ref, path: selected.path } : undefined; },
4384
+ catalog(id, selector) {
4385
+ const selected = id === "oats.framework" ? entry : (Object.hasOwn(catalog, id) ? catalog[id] : null);
4386
+ return selected?.url ? { url: selected.url, ref: selector || selected.ref, path: selected.path } : undefined;
4387
+ },
4364
4388
  assertCommittable(plan) {
4365
4389
  const pkg = plan.packages.find(p => p.package === "oats.framework");
4366
- if (edition.packageIntegrity && pkg?.integrity !== edition.packageIntegrity) throw Object.assign(new Error("selected edition's same-repository package differs from the official acquisition; align the reviewed source and catalog explicitly"), { code: "integrity-drift" });
4390
+ if (edition.packageIntegrity && pkg?.integrity !== edition.packageIntegrity) {
4391
+ const lag = catalogOrigin === "bundled" ? " (the kernel's bundled catalog entry lags the edition's package; onboard from the workspace or point OATS_PACKAGE_CATALOG at the reviewed list)" : "";
4392
+ throw Object.assign(new Error(`selected edition's same-repository package differs from the official acquisition; align the reviewed source and catalog explicitly${lag}`),
4393
+ { code: "integrity-drift", details: { catalogOrigin, catalogRef: entry.ref, acquiredIntegrity: pkg?.integrity ?? null, editionPackageIntegrity: edition.packageIntegrity } });
4394
+ }
4367
4395
  for (const id of SETUP_CAPABILITIES) {
4368
4396
  const cap = plan.capabilities.find(c => c.capability === id);
4369
4397
  if (!cap || cap.package !== "oats.framework" || cap.layer || Object.values(cap.executableSurface || {}).some(value => Array.isArray(value) && value.length)) {
@@ -4394,7 +4422,7 @@ function onboardCmd() {
4394
4422
  const agent = findAgent(root, SETUP_EXPERT), composition = composeInstanceAgentsMd(created.soul, deployment, SETUP_EXPERT, "directory", "local");
4395
4423
  planInstanceResources({ resolved: composition.resolved, soulDir: created.soul, agent, contextDir: deployment, composition });
4396
4424
  const argv = [process.execPath, CLI_BIN, "spawn", SETUP_EXPERT, "--dir", deployment, "--no-yolo", "--task", "Help me configure this deployment and adopt my workspace with explicit approvals."];
4397
- const result = { mode: "classic", captured: false, deployment, agentsRoot: root, ...created, source: edition.source,
4425
+ const result = { mode: "classic", captured: false, deployment, agentsRoot: root, ...created, source: edition.source, catalog: { origin: catalogOrigin, ref: entry.ref },
4398
4426
  package: { id: pkg.package, version: pkg.version, commit: pkg.commit, path: pkg.path }, lockFile: acquired.lockFile,
4399
4427
  capabilities: [...SETUP_CAPABILITIES], launched: false, next: { argv, command: argv.map(shellQuote).join(" ") } };
4400
4428
  if (JSON_MODE) jsonOk(result);
@@ -4411,7 +4439,7 @@ function onboardCmd() {
4411
4439
  }
4412
4440
  } catch { /* never erase another writer's change or hide a failed rollback */ }
4413
4441
  }
4414
- fail(error.code || "E_ONBOARD_FAILED", error.message, { deployment, agentsRoot: root, packageAcquired: !!acquired, soul: created?.soul, configRestored, launched: false });
4442
+ fail(error.code || "E_ONBOARD_FAILED", error.message, { deployment, agentsRoot: root, packageAcquired: !!acquired, soul: created?.soul, configRestored, launched: false, ...(error.details && typeof error.details === "object" ? error.details : {}) });
4415
4443
  }
4416
4444
  }
4417
4445
 
@@ -5145,6 +5173,7 @@ else if (cmd === "install") install();
5145
5173
  else if (cmd === "config") configCmd();
5146
5174
  else if (cmd === "trust") trust();
5147
5175
  else if (cmd === "list") listCmd();
5176
+ else if (cmd === "catalog") catalogCmd();
5148
5177
  else if (cmd === "remove") removeCmd();
5149
5178
  else if (cmd === "migrate") migrateCmd();
5150
5179
  else if (cmd === "root") console.log(resolve(new URL("..", import.meta.url).pathname));
@@ -5337,6 +5366,8 @@ Usage:
5337
5366
  report under error.details)
5338
5367
  oats list [--dir <d>] [--json] installed packages, exported capabilities,
5339
5368
  scopes, trust state
5369
+ oats catalog [--json] the effective official package catalog (read-only:
5370
+ identity/discovery, no acquisition or trust)
5340
5371
  oats update <package> [<package>@<ref>] transactional package update: temp fetch,
5341
5372
  [--to <ref>] [--dir <d>] closure validation, diff, lock replace,
5342
5373
  all capability approvals invalidated; a
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "capability": "oats.aweb",
3
3
  "command": "aweb",
4
- "version": "1.11.0",
4
+ "version": "1.11.2",
5
5
  "compatibility": {
6
- "oats": ">=0.24.2"
6
+ "oats": ">=0.24.4"
7
7
  },
8
8
  "layer": "messaging",
9
9
  "description": "Messaging layer via aweb: per-instance team identities + native aw mail/chat skills + cross-machine team roster.",
@@ -62,6 +62,10 @@
62
62
  "skills/aweb-identity"
63
63
  ],
64
64
  "inject": "injects/aweb.md",
65
+ "helperInjection": {
66
+ "version": 1,
67
+ "mode": "omit"
68
+ },
65
69
  "commands": {
66
70
  "roster": "bin/oats-aweb.mjs roster",
67
71
  "setup": "bin/oats-aweb.mjs setup",
@@ -73,7 +77,44 @@
73
77
  "version": 1,
74
78
  "normalize": "binding-normalize",
75
79
  "bind": "binding-bind",
76
- "check": "binding-check"
80
+ "check": "binding-check",
81
+ "keys": [
82
+ "responsibleHuman",
83
+ "privateTeam",
84
+ "wider"
85
+ ],
86
+ "reasons": [
87
+ "messaging-enabled standalone preparation needs an explicit context key",
88
+ "messaging binding needs one soul declaration",
89
+ "messaging workspace must declare private: per-human",
90
+ "an explicit responsible-human binding is required",
91
+ "an explicit wider-membership consent list is required",
92
+ "a selected wider-team binding is required",
93
+ "a selected wider alias needs an explicit workspace team mapping",
94
+ "multiple soul messaging declarations",
95
+ "adoption team aliases have conflicting mappings",
96
+ "messaging settings and explicit binding selections are required",
97
+ "messaging declarations contain incompatible requirements",
98
+ "messaging input must match the supported binding contract",
99
+ "explicit native messaging authorization is required",
100
+ "a required native messaging host resource is unavailable",
101
+ "the selected messaging provider is unavailable",
102
+ "the requested messaging configuration is not qualified",
103
+ "messaging response exceeds the supported wire limits",
104
+ "an admitted captured instance intent is required for execution",
105
+ "an explicit private-team binding is required",
106
+ "selected binding and inline captured invocation are required",
107
+ "an explicit captured instance home is required",
108
+ "captured messaging requires explicit delivery: session",
109
+ "selected wider memberships need their explicitly qualified native setup; they were not omitted",
110
+ "caller-owned OATS_CLI_BIN and readable kernel version are required",
111
+ "oats >=0.24.2 is required for captured HOME custody and retained runtime inspection",
112
+ "the exact retained runtime profile must be readable",
113
+ "the kernel must report the exact retained resolution",
114
+ "a retained launchSelection runtime/model observation is required",
115
+ "Pi strict print does not support session input; retain messaging and configure an input-capable profile",
116
+ "a supported input-capable ordinary Claude/Codex profile is required"
117
+ ]
77
118
  },
78
119
  "hooks": {
79
120
  "spawn": {
@@ -7,6 +7,7 @@ import {
7
7
  KNOWLEDGE_CONTRACT,
8
8
  KNOWLEDGE_CONTRACT_VERSION,
9
9
  bindKnowledgeDomain,
10
+ bindingChoiceKey,
10
11
  checkKnowledgeRuntime,
11
12
  normalizeKnowledgeBindingCandidates,
12
13
  normalizeKnowledgeDeclaration,
@@ -24,6 +25,22 @@ const declarationKinds=new Set(['soul','workspace','adoption','operator']);
24
25
  const errorCodes=new Set(['needs-configuration','requirement-conflict','invalid-binding','authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified']);
25
26
  const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
26
27
  const wireError=code=>{throw Object.assign(new Error(code),{wireCode:code});};
28
+ // Private diagnostic marker and closed literal vocabulary. Never serialize a
29
+ // caught exception's message, supplied value, path, alias or unknown key.
30
+ const settingDiagnostic=Symbol('runtime-setting-diagnostic');
31
+ const settingMessages=Object.freeze({
32
+ 'bindings-file:missing':'setting bindings-file is required (absolute host path)',
33
+ 'bindings-file:invalid':'setting bindings-file must be a normalized absolute host path',
34
+ 'state-dir:missing':'setting state-dir is required (absolute host path)',
35
+ 'state-dir:invalid':'setting state-dir must be a normalized absolute host path',
36
+ 'harvest-runtime:missing':'setting harvest-runtime is required (pi, claude or codex)',
37
+ 'harvest-runtime:invalid':'setting harvest-runtime must be pi, claude or codex',
38
+ 'harvest-model:invalid':'setting harvest-model must be null or a non-empty string',
39
+ });
40
+ function settingError(reason) {
41
+ if(!Object.hasOwn(settingMessages,reason)) wireError('invalid-binding');
42
+ throw Object.assign(new Error('needs-configuration'),{wireCode:'needs-configuration',[settingDiagnostic]:settingMessages[reason]});
43
+ }
27
44
  function keys(value,allowed,required,label) {
28
45
  if(!obj(value)) wireError('invalid-binding');
29
46
  for(const key of Object.keys(value)) if(!allowed.includes(key)) wireError('invalid-binding');
@@ -100,13 +117,29 @@ function contract(value,{required=false}={}) {
100
117
  function runtimeSettings(settings) {
101
118
  keys(settings,['bindings-file','state-dir','harvest-runtime','harvest-model'],[], 'OKF settings');
102
119
  const descriptorFile=settings['bindings-file'],stateDir=settings['state-dir'],runtime=settings['harvest-runtime'],model=settings['harvest-model'] ?? null;
103
- if(!absolute(descriptorFile) || !absolute(stateDir) || !['pi','claude','codex'].includes(runtime) || (model!==null && (typeof model!=='string' || !model.trim()))) wireError('needs-configuration');
120
+ for(const name of ['bindings-file','state-dir']) {
121
+ if(settings[name]===undefined) settingError(`${name}:missing`);
122
+ if(!absolute(settings[name])) settingError(`${name}:invalid`);
123
+ }
124
+ if(runtime===undefined) settingError('harvest-runtime:missing');
125
+ if(!['pi','claude','codex'].includes(runtime)) settingError('harvest-runtime:invalid');
126
+ if(model!==null && (typeof model!=='string' || !model.trim())) settingError('harvest-model:invalid');
104
127
  return {descriptorFile,stateDir,execution:{runtime,model}};
105
128
  }
129
+ // Shared maps contain other providers' opaque values. Ownership comes from
130
+ // this source's declared/default/required knowledge addresses, not from a value
131
+ // looking like a locator or a blanket stores.* prefix. Validate OWN values later.
132
+ function ownedBindings(bindings,ownedKeys) {
133
+ return Object.fromEntries(Object.entries(bindings).filter(([name])=>{
134
+ let key;try{key=bindingChoiceKey(name);}catch{return false;}
135
+ return ownedKeys.has(key);
136
+ }));
137
+ }
106
138
  function normalizePhase(req) {
107
139
  keys(req.input,['declarations','context'],['declarations','context'],'normalize input');
108
140
  if(!Array.isArray(req.input.declarations) || !obj(req.input.context)) wireError('invalid-binding');
109
- const requirements=[],candidates=[];let domain=null;
141
+ const requirements=[],candidates=[],bindingInputs=[];let domain=null;
142
+ const collect=input=>{if(!obj(input.bindings)) wireError('invalid-binding');bindingInputs.push(input);};
110
143
  for(const raw of req.input.declarations) {
111
144
  const item=declaration(raw);
112
145
  if(item.kind==='soul') {
@@ -124,14 +157,19 @@ function normalizePhase(req) {
124
157
  if(rawStore.contract!==KNOWLEDGE_CONTRACT) return;
125
158
  const store=contract(rawStore);
126
159
  keys(store.payload,['bindings'],['bindings'],'workspace OKF payload');
127
- candidates.push(...normalizeKnowledgeBindingCandidates({bindings:store.payload.bindings,kind:'workspace-default',origin:item.origin,origins:item.origins,pointer:`/knowledge/stores/${index}/payload/bindings`}));
160
+ collect({bindings:store.payload.bindings,kind:'workspace-default',origin:item.origin,origins:item.origins,pointer:`/knowledge/stores/${index}/payload/bindings`});
128
161
  });
129
162
  continue;
130
163
  }
131
164
  if(item.value.bindings===undefined) continue;
132
- candidates.push(...normalizeKnowledgeBindingCandidates({bindings:item.value.bindings,kind:item.kind==='adoption'?'import-adoption':'operator',origin:item.origin,origins:item.origins,pointer:'/bindings'}));
165
+ collect({bindings:item.value.bindings,kind:item.kind==='adoption'?'import-adoption':'operator',origin:item.origin,origins:item.origins,pointer:'/bindings'});
133
166
  }
134
167
  if(!domain) wireError('needs-configuration');
168
+ // Collect first so declaration order cannot change who owns a binding. Keep
169
+ // all declared addresses, including dotted aliases and explicit/implicit
170
+ // write.default or custom inheritance; the kernel still resolves precedence.
171
+ const ownedKeys=new Set([...domain.requirements,...domain.candidates].map(item=>item.key));
172
+ for(const input of bindingInputs) candidates.push(...normalizeKnowledgeBindingCandidates({...input,bindings:ownedBindings(input.bindings,ownedKeys)}));
135
173
  requirements.push(...domain.requirements);candidates.unshift(...domain.candidates);
136
174
  return {requirements,candidates,model:{domain,runtime:runtimeSettings(req.settings)}};
137
175
  }
@@ -152,12 +190,20 @@ function bindPhase(req) {
152
190
  const runtime=renderKnowledgeRuntime({domain:bound.payload,...req.input.model.runtime});
153
191
  return {payloadContract:bound.contract,payloadVersion:bound.version,payload:{...bound.payload,runtime,execution:req.input.model.runtime.execution},credentialRefs:bound.credentialRefs,provenance:bound.provenance};
154
192
  }
155
- function bindingPayload(binding) {
193
+ function bindingPayload(binding,{diagnoseSettings=false}={}) {
156
194
  keys(binding,['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance'],['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance'],'provider binding');
157
195
  if(binding.schemaVersion!==1 || binding.capability!==CAPABILITY || binding.payloadContract!==KNOWLEDGE_CONTRACT || binding.payloadVersion!==KNOWLEDGE_CONTRACT_VERSION || !Array.isArray(binding.provenance)) wireError('invalid-binding');
158
196
  if(!obj(binding.credentialRefs) || Object.keys(binding.credentialRefs).length) wireError('invalid-binding');
159
197
  keys(binding.payload,['owner','stores','reads','owns','runtime','execution'],['owner','stores','reads','owns','runtime','execution'],'OKF binding payload');
160
198
  const domain={owner:binding.payload.owner,stores:binding.payload.stores,reads:binding.payload.reads,owns:binding.payload.owns};
199
+ if(diagnoseSettings) {
200
+ const bound=binding.payload;
201
+ if(!obj(bound.runtime) || !obj(bound.runtime.bindings) || !obj(bound.execution)) wireError('invalid-binding');
202
+ // Check the retained values, not mutable request settings. Existing full
203
+ // envelope/canonical-runtime/execution guards still run below on success.
204
+ runtimeSettings({'bindings-file':bound.runtime.descriptorFile,'state-dir':bound.runtime.bindings.stateDir,
205
+ 'harvest-runtime':bound.execution.runtime,'harvest-model':bound.execution.model});
206
+ }
161
207
  const runtime=renderKnowledgeRuntime({domain,stateDir:binding.payload.runtime?.bindings?.stateDir,descriptorFile:binding.payload.runtime?.descriptorFile});
162
208
  if(!sameJson(runtime,binding.payload.runtime)) wireError('invalid-binding');
163
209
  keys(binding.payload.execution,['runtime','model'],['runtime','model'],'OKF worker execution');
@@ -211,7 +257,7 @@ function checkPhase(req) {
211
257
  keys(req.input,['binding','context','action','invocation'],['binding','context','action'],'check input');
212
258
  if(!obj(req.input.context) || !obj(req.input.action)) wireError('invalid-binding');
213
259
  if(Object.hasOwn(req.input,'invocation')) validateInvocationShape(req.input.invocation,{capability:CAPABILITY,context:req.input.context,action:req.input.action});
214
- const {runtime}=bindingPayload(req.input.binding),action=req.input.action,name=providerActionName(action);
260
+ const {runtime}=bindingPayload(req.input.binding,{diagnoseSettings:true}),action=req.input.action,name=providerActionName(action);
215
261
  const harvestInvocation=req.input.invocation;
216
262
  const admittedHarvest=name==='harvest' && action.kind==='operation' && action.slot==='knowledge' && action.name==='harvest'
217
263
  && harvestInvocation?.subject.kind==='persistent' && harvestInvocation.instance!==null && !!harvestInvocation.intent;
@@ -244,6 +290,10 @@ export function handleBindingRequest(phase,value) {
244
290
  }
245
291
  function response(phase,body) {return {schemaVersion:1,phase,slot:SLOT,capability:CAPABILITY,...body};}
246
292
  function errorCode(error) {if(errorCodes.has(error?.wireCode)) return error.wireCode;if(error?.code==='E_OWNER') return 'requirement-conflict';return 'invalid-binding';}
293
+ function errorProblem(error) {
294
+ const code=errorCode(error),message=error?.[settingDiagnostic];
295
+ return {code,...(code==='needs-configuration' && Object.values(settingMessages).includes(message)?{message}:{})};
296
+ }
247
297
  function enforceOutputLimits(value,depth=1,state={entries:0}) {
248
298
  if(depth>BINDING_WIRE_LIMITS.depth) wireError('provider-not-qualified');
249
299
  if(value===null || typeof value!=='object') return;
@@ -259,7 +309,7 @@ export async function runBindingWire(phase,input=process.stdin,output=process.st
259
309
  for await(const chunk of input) {const bytes=Buffer.from(chunk);length+=bytes.length;if(length>BINDING_WIRE_LIMITS.bytes) wireError('invalid-binding');chunks.push(bytes);}
260
310
  const result=handleBindingRequest(phase,parseBindingJson(Buffer.concat(chunks,length)));
261
311
  answer=response(phase,{ok:true,result});
262
- } catch(error) {answer=response(phases.has(phase)?phase:'check',{ok:false,error:{code:errorCode(error)}});}
312
+ } catch(error) {answer=response(phases.has(phase)?phase:'check',{ok:false,error:errorProblem(error)});}
263
313
  let bytes;
264
314
  try {enforceOutputLimits(answer);bytes=Buffer.from(JSON.stringify(answer)+'\n');if(bytes.length>BINDING_WIRE_LIMITS.bytes) wireError('provider-not-qualified');}
265
315
  catch {bytes=Buffer.from(JSON.stringify(response(phases.has(phase)?phase:'check',{ok:false,error:{code:'provider-not-qualified'}}))+'\n');}
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "capability": "oats.okf",
3
3
  "command": "okf",
4
- "version": "2.1.1",
4
+ "version": "2.1.2",
5
5
  "compatibility": {
6
- "oats": ">=0.24.0"
6
+ "oats": ">=0.24.4"
7
7
  },
8
8
  "layer": "knowledge",
9
9
  "description": "External OKF bases with owned nodes, immutable reader views, durable per-source evidence, independent workers, verified Git PR and recoverable non-Git directory delivery.",
@@ -54,7 +54,16 @@
54
54
  "version": 1,
55
55
  "normalize": "binding-normalize",
56
56
  "bind": "binding-bind",
57
- "check": "binding-check"
57
+ "check": "binding-check",
58
+ "reasons": [
59
+ "setting bindings-file is required (absolute host path)",
60
+ "setting bindings-file must be a normalized absolute host path",
61
+ "setting state-dir is required (absolute host path)",
62
+ "setting state-dir must be a normalized absolute host path",
63
+ "setting harvest-runtime is required (pi, claude or codex)",
64
+ "setting harvest-runtime must be pi, claude or codex",
65
+ "setting harvest-model must be null or a non-empty string"
66
+ ]
58
67
  },
59
68
  "inject": "injects/okf.md",
60
69
  "helperInjection": {
@@ -285,6 +285,7 @@ oats install ../my-package # local path
285
285
  oats install oats.okf # official catalog id
286
286
  oats install # bare: exact restore of this chain's locks
287
287
  oats list # installed packages, exported capabilities, scopes
288
+ oats catalog [--json] # the effective official catalog: packages, refs, aliases, acquire argv (0.24.6+; read-only)
288
289
  oats update <package> # transactional re-resolve + diff + trust reset
289
290
  oats remove <package> # refuses while config/dependents reference it
290
291
  oats migrate [--dry-run] # map v1 capability locks to package locks
@@ -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-21 15:15Z · main `ef211d3e`+ · OATS v0.24.4 cutting (PR41 merged) · OKF v2.1.1 · oats-framework/v1.1.1 · aweb v1.11.0 · oats-knowledge 8d67eab4
5
+ **Last update:** 2026-09-22 07:00Z · main `508c4b5f`+ · **OATS v0.24.6 cutting** (PR45 slice 1a + `oats catalog`) · S8 slice 1b next
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -11,13 +11,13 @@ Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, n
11
11
  | # | Stream | State | Owner | Next action |
12
12
  |---|---|---|---|---|
13
13
  | S1 | Knowledge capability contract rework (kernel↔provider boundary, OKF 2.x) | ✅ OATS 0.24.1 / OKF 2.1.1 published | P | done for this phase |
14
- | S2 | Workspace/Portable Souls adoption of the OATS repos | ✅ 0.24.3 gate run: knowledge slot resolves via the workspace route; seams 1–4 fixed, 5 attributed · ✅ **messaging source defect found and fixed (906b1558: per-human policy + soul teams)**, imports repinned f3ee31e0 · 🔄 final re-run for a published resolution + OKF check probe | lead, Antares | re-run; then S2 closed |
14
+ | S2 | Workspace/Portable Souls adoption of the OATS repos | ✅ **FIRST SECOND-OPERATOR PUBLICATION (735296c5, 2026-09-21)**: fresh machine, published definition alone, no `launch` → `status: prepared`, resolution `sha256-f11c433c…`, zero problems. ❌ then OKF `check` → `needs-configuration / provider-not-qualified`, **no message/key/origins**, identical for `harvest-runtime` pi and claude — OKF's check phase is code-only and folds ≥4 causes into one code (P, OKF 2.1.3). ⏳ Pi `launch` blocked by kernel ifInstalled defect (L, 0.24.5). Earlier: ✅ knowledge: bound to public `oats-knowledge` via OKF 2.1.2 from a fresh machine, four kernel versions · ✅ messaging normalizes; `wider` deadlock gone; named reasons in one run · ❌ **operator's amended verdict (9816b8ec): no resolution can publish — behind `responsibleHuman`, helper composition refuses because `oats.core` and `oats.aweb` ship `inject` without `helperInjection`** (OKF adopted the contract; its siblings did not). Both remaining blockers are packaging, not operator input. Fix: [decision](https://github.com/awebai/oats/blob/main/agents/oats-expert/soul/knowledge/decisions/helper-injection-policy-on-every-injecting-capability.md) — core `inherit`, aweb `omit`, framework release check, attributed early refusal · then unchanged re-run | lead, Antares | oats.framework 1.1.3 + aweb 1.11.2 + pins; re-run |
15
15
  | S3 | Messaging capability readiness on the new infrastructure (aweb) | ✅ aweb 1.11.0 released · ✅ **catalog + six editions pin v1.11.0 (0.24.2)** · ⬜ second-operator re-run | P, lead | Antares re-run |
16
16
  | S4 | Official capabilities `oats.core` / `oats.setup` + explicit default + onboarding `oats-setup-expert` | ✅ D1, D2, **D3 merged (PR35)**: `oats onboard` verified live (acquire 1.1.1 → setup expert with both caps → scaffold composes the five capability skills, no legacy) · `oats.framework` 1.1.1 tagged | P, L | done; Desktop surfaces → S8 |
17
17
  | S5 | Official marketplace = reviewed list in oats repo | ✅ D4 merged · ✅ `oats.framework` 1.1.1 listed (`oats.core`, `oats.setup`, `oats.knowledge-theory` aliases) | M | Desktop view → S8 |
18
18
  | S6 | Five expert souls created in the oats repo (`souls/<name>/`) | ✅ five + `oats-setup-expert` on main, all declaring `oats.core`, exported + imported | M, L, lead | legacy `agents/` cutover after S7 proof |
19
- | S7 | Centralised per-soul knowledge in `oats-knowledge` (migration + PR-only learning) | ✅ **repo PUBLIC; PR #1 merged → main 8d67eab4, 25 accepted concepts**, owners = published souls, validator pinned OKF 2.1.1 · 🔄 fresh-reader proof assigned to Juan's side · ⬜ legacy in-soul knowledge decommission | lead, Antares/Juan | fresh-reader + PR-learning proof; then retire `agents/*/soul/knowledge` |
20
- | S8 | Desktop parity (marketplace view, soul creation with `oats.core`, onboarding flow) | ⬜ after D3 · **host offered: Juan's machine (has `claude`)** — accepted | fresh Desktop engineer on Juan's host | brief + spawn params after D3 |
19
+ | S7 | Centralised per-soul knowledge in `oats-knowledge` (migration + PR-only learning) | ✅ **MIGRATED — PR #2 merged → main `7148a36`, 58 concepts / 35k words from 399 legacy** (kernel 26, expert 18, desktop 13, assistant 1, market-research chartered empty); option A roster; theory + no-code-teaching rule; two workflow passes (24 agents) + lead review; validator 58/0/0; ownership 14/14 · ⏸ harvest OFF until new souls run from the base (human) | lead | next: legacy `agents/` decommission with the `~/OATS` cutover |
20
+ | S8 | Desktop parity with the redesign (Redesign v3, aweb palette, discovery-first control panel) | 🔄 **STARTED 2026-09-22**: existing `oats-desktop-engineer-1` retrofitted with an aweb identity (spawn hook replayed, `capabilityMeta` persisted, session reloaded); redesign artefacts copied into its home; brief sent (f16eed3f): merge main (0.24.5) first, then PR slices — shell/palette/sidebar → right panel follows selection → Souls view → **Capabilities view incl. official catalog (human's required feature; server-side via kernel catalog API, never auto-acquire/auto-trust)** → Knowledge/Tasks adapters → remainder. Lead reviews/merges each slice. **Decisions 2026-09-22:** human's later palette/row instructions supersede the HTML where they conflict; grouping is agent-group (cross-repo clusters), not repo; kernel seam `oats catalog --json` merged (PR46 `524180b7`, ships 0.24.6); the Juan-host fresh-engineer plan is superseded — this instance owns S8. | oats-desktop-engineer-1, lead | per-slice PRs | ✅ **Slice 1a MERGED `508c4b5f`** (PR45; 273/273 real jsdom; gates green) → v0.24.6. **Plan + seams recorded: [`docs/design/2026-09-22-desktop-parity-seams.md`](2026-09-22-desktop-parity-seams.md)** — slices 1a–8, seams K1–K8/P1, five policy decisions **DECIDED 2026-09-22 under delegation** — see `decisions/desktop-parity-lifecycle-and-policy-decisions.md`.
21
21
 
22
22
  ## S1 — Knowledge capability contract rework
23
23
  - ✅ Provider-neutral contract, binding wire v1, helper/input contract, retained execution: OATS 0.24.0 + OKF 2.1.0 (f20f8e57) published.
@@ -50,13 +50,53 @@ Fresh dir, local `@awebai/oats@0.24.1`, no prior state. `inspect --request` →
50
50
  - With `bindings-file`/`state-dir`/`harvest-runtime: pi` and the public `oats-knowledge` bound as base `oats`: **the knowledge slot binds** (OKF problem gone). `harvest-model` is optional per manifest; aweb 1.11.0 declares exactly one setting (`delivery`).
51
51
  - **aweb 1.11.0 is the only remaining hold** (`needs-configuration`, identical for `delivery: session` and `channel` on 0.24.2's unattributed output). No resolution publishes → OKF `check` probe not reachable yet. 0.24.3 run will show the attributed aweb message (expected: private-team binding / Pi session-input).
52
52
 
53
+ ## 0.24.5 — four small fixes, lead-implemented (2026-09-22, PR43 → `38b6c028`)
54
+
55
+ Human authorised lead implementation after new spawns in the development deployment were blocked by a correct `pi-profile` drift refusal (profile pins bridge 0.23.2; global Pi has 0.24.0 — deployment policy, the human's re-pin). Merged: (1) retire recovery derives the branch from the worktree (drift recorded in `recovery.json`; detached HEAD recovers at its OID) — unblocks the three peers; (2) captured launch: `ifInstalled` rows satisfied by absence, hard rows refuse attributed (slot/capability/runtime/package/manifest `install`); (3) helper-injection refusal attributed; (4) `onboard --workspace` reads `package-catalog.json` from the workspace revision (bundled = fallback; `OATS_PACKAGE_CATALOG` = override; result `catalog.origin`; drift refusal names the lag). Full gate 1665/1661/0/4. Rule: `prepare` never ends in a bare `needs-configuration` after selection. Lesson: `lessons/pinned-release-lags-installed-release.md` (three lag refusals in one day). ✅ **v0.24.5 PUBLISHED** (npm both, GH release 7 assets, bump #44). ✅ **`~/OATS` re-onboarded clean on 0.24.5 with NO override**: `catalog.origin: workspace`, framework 1.1.3, setup expert from `7416d84e` — fix 4 proven the way a second operator meets it. ✅ **Three stuck peers retired by the normal path** (fix 1 proven live): each recovery records `branchDrift` (recorded vs worktree branch) and preserved home + untracked bytes + nested repos; L's recovery shows it had begun the 0.24.5 assignment on its own branch (superseded by PR43, preserved). Roster: only the lead and the Desktop engineer remain in the development deployment. Next: spawn the setup expert in `~/OATS` (human runs).
56
+
57
+ ## Cutover started: `~/OATS` onboarded from the public workspace (lead, 2026-09-21 23:55Z)
58
+
59
+ Kernel 0.24.4 installed globally; `oats onboard --dir ~/OATS --workspace git:github.com/awebai/oats` → `oats.framework` 1.1.3 acquired and exact-locked (lock v2), `oats.core` + `oats.setup` selected, `oats-setup-expert` copied from the workspace's pinned edition (`7416d84e`, workspace revision `c82a96af`), layers `none`. First attempt refused `integrity-drift` because the kernel's **bundled catalog snapshot** still listed framework 1.1.1 — correct refusal, misleading read; remedy `OATS_PACKAGE_CATALOG=<main's list>`; lesson `lessons/bundled-catalog-lags-the-reviewed-list.md` (proposes onboarding read the catalog from the workspace revision). Next: spawn the setup expert to adopt the workspace (knowledge → public `oats-knowledge` via OKF 2.1.2; messaging → aweb 1.11.2 or none) with explicit approvals; then the five expert souls run from the central base and this checkout becomes secondary. Harvest stays off until then.
60
+
61
+ ## S2 — FIRST SECOND-OPERATOR PUBLICATION + `check` finding (Antares, 735296c5, 2026-09-21)
62
+
63
+ Verified wave (aweb v1.11.2, framework v1.1.3, main cd3d4913, imports 7416d84e). Fresh deployment, unchanged request minus `launch`, synthetic `responsibleHuman`, `wider: []` → `{"status":"prepared","resolution":{"id":"sha256-f11c433c…"},"executionBinding":{…},"problems":[]}`, exit 0, empty stderr. **An operator holding none of the authoring state produced an executable resolution from the published workspace definition — the first.** Helper-injection fix confirmed from outside; `ignored` shrank to `["operator"]` correctly.
64
+
65
+ Then P's OKF `check` probe against the published resolution (contacted the public repository): `{"status":"needs-configuration","problems":[{"code":"provider-not-qualified"}]}` — bare; the operator dumped the shape to prove nothing was elided. Second resolution with `harvest-runtime: claude`, no model (`sha256-1aa5ab9a…`, prepared) → identical. Traced by lead: OKF 2.1.2 `checkPhase` (`binding-wire.mjs` L256–282) returns `problem(code)` only and maps unadmitted action / >64 bases / `E_OWNER|E_BASE|E_VALIDATION|E_DIRECTORY_GIT|E_CONFIRM` all to `provider-not-qualified`; the kernel's check path already passes declared reasons. Fifth of the family. Operator's verdict adopted: *"A second operator reaches, resolves, approves and publishes an executable resolution from the public definition alone … The OKF check readiness probe then refuses as provider-not-qualified with no reason, no key and no origins, and the refusal is independent of the harvest runtime."* Decision amended: every check problem carries a fixed reason distinct per cause. **Assigned P: OKF 2.1.3** — ahead of 0.24.5 by the operator's priority, which the lead accepts (publication works; check is the next thing every operator hits). **Operator narrowed the cause from outside (9c9dc404), each elimination tested:** not action kind, not base count, not `validateBase` (validator from the 2.1.2 artifact against the public `knowledge/`: 25 concepts, 0/0), not directory-git, not runtime; staging IS reached (nonexistent repo → `unavailable/provider-unavailable` in 0.65s; real → `needs-configuration/provider-not-qualified` in 1.66s). Left: `verifyPublicationTree` (E_CONFIRM/E_BASE) or `checkKnowledgeRuntime` (E_OWNER; not a naive owner mismatch — public base owner = edition owner). Forwarded to P so 2.1.3 reasons split exactly these. Sharp edge noted: the validator at the repository root fails on bundle-root-absolute links — a wrong root will look like an invalid base. **Acceptance plan corrected by the operator (bbbb6bdd):** the two retained resolutions captured and approved the exact 2.1.2 artifact and `runCapturedProviderBinding` executes exactly that, so re-probing them must still return code-only — that is immutability working. After 2.1.3 is pinned: keep the old resolutions as before-evidence, prepare NEW resolutions from the same two request shapes with only the OKF source moved to v2.1.3, approve their exact artifact sets, run the same probe → valid before/after.
66
+
67
+ ## Helper-injection gap FIXED + next blocker found (lead, 2026-09-21 20:30Z)
68
+
69
+ - ✅ **oats.framework 1.1.3** (`oats-framework/v1.1.3` @ `6108bfbb`): `oats.core` 1.0.1 declares `helperInjection: inherit`. ✅ **aweb 1.11.2** (`v1.11.2` @ `a671884`): `helperInjection: omit`, manifest-only. Bundled copy byte-synced; catalog `oats.aweb` → v1.11.2, `oats.framework` → v1.1.3; six editions → aweb v1.11.2; six imports → `7416d84e`. Release check added: every injecting framework capability declares its helper policy (jira/linear/review pending upstream, not required by any edition). Notes: `docs/release-notes/oats-framework-v1.1.3.md`.
70
+ - **Lead probe from the published 0.24.4 tarball, operator's request shape + synthetic `responsibleHuman` + `wider: []`:** helper composition now passes. **Without a `launch` block the resolution PUBLISHES** (`status: prepared`, resolution + execution binding) — first end-to-end publication from public definitions. **With the Pi `launch` block it refuses** on a bare `needs-configuration: runtime package requirements need retained runtime roots and a qualified loader`: kernel defect — the captured path treats aweb's `ifInstalled: true` Pi floor (`npm:@awebai/pi ≥0.3.10`, only if present) as a hard block, and refuses with no `details`. `delivery: channel` gives the byte-identical message. Lesson: `lessons/captured-launch-ignores-ifinstalled-and-refuses-bare.md`. **Assigned to L with the retirement defect → 0.24.5 (kernel), plus attribution for both bare refusals.** Operator's re-run: after 0.24.5, or now without `launch` to witness publication.
71
+
72
+ ## S2 — operator's self-correction (9816b8ec, 2026-09-21): packaging gap behind `responsibleHuman`
73
+
74
+ The operator supplied a deliberately synthetic `responsibleHuman` (`gate-probe-not-a-real-human`; accepted — `humanRef` checks only provider + non-empty id, i.e. an accountability claim the code records but does not verify) and preparation refused: `needs-configuration: new helper injection requires an explicit capability policy` (`lib/helper-injection-policy.mjs:38`), a bare top-level error with no `details`/attribution. Cause verified from the published manifests: `oats.okf` declares `helperInjection {omit}`; **`oats.core` (`injects/oats.md`) and `oats.aweb` (`injects/aweb.md`) declare none**. Every edition declares `oats.core`, so no edition could publish once its OKF harvest helper composed. Fourth finding of the same family (contract adopted by one provider, not its siblings). Operator's restated verdict adopted verbatim: *"A second operator reaches, resolves and approves the entire graph from the public definition, binds knowledge to the public `oats-knowledge` base, and normalizes messaging. Preparation cannot publish a resolution … Both remaining blockers are packaging, not operator input."* The earlier "EXIT GATE MET" entry below is superseded by this one.
75
+
76
+ ## Kernel defect found at peer retirement (2026-09-21) — 0.25 lifecycle item
77
+
78
+ `oats retire oats-expert-scheduler-peer` (self and spawner retry) fails closed: *recovered Git index/status disagreed with the source*. Cause: `preserveRetirementWork` clones the branch recorded in `instance.json` (`feat/portable-scheduler-captures`, tip e0c3232c) while the worktree is checked out on `docs/okf-host-runtime-settings` (5919a547); the status comparison cannot agree. Verified by hand: worktree clean, both branches pushed, every deliverable merged (oats PR39, oats-okf PR5, oats-aweb PR4); the only dirt is two untracked files in a nested scratch clone that are already on aweb main since 1.11.0. **Nothing unpreserved.** Instance left `RETIRING`; no branch/metadata/force surgery. Fix scoped to **0.25 (L)**: derive the recovery branch from the worktree, record drift as a typed observation. Lesson: `lessons/retire-recovery-uses-recorded-branch-not-checked-out-branch.md`.
79
+
80
+ ## S2 — acceptance run on the 0.24.4 wave (Antares, Juan's machine, 2026-09-21) — superseded by the self-correction above (knowledge result stands)
81
+
82
+ Fresh directory and deployment, `@awebai/oats@0.24.4`, policy sources v2.1.2 / v1.11.1, `wider: []`, request otherwise unchanged from the 0.24.3 run; wave verified independently first (npm, both provider tags, main `86b5f924`, six imports @ `08c68ece`).
83
+
84
+ - inspect: exit 0, `ignored: ["operator","launch"]`, six imports @ `08c68ece`, `ready-for-preparation` — **seam 1 closed**.
85
+ - prepare → two `approval-required` → approved via the printed `--artifact-set` commands → prepare: **knowledge binds with `wider: []` present (deadlock fixed)**; the single remaining problem is `{code: needs-configuration, slot: messaging, key: /bindings/messaging/responsibleHuman}`.
86
+ - `bindings-file` removed → `setting bindings-file is required (absolute host path)` in one run — the named-reason criterion ("one run instead of six") met. `wider` removed → both messaging keys attributed separately.
87
+ - Operator's observation, adopted: `required value has no concrete binding` is generic text but the `key` carries the address; sufficient — no more spend on the string.
88
+ - **Operator's verdict (verbatim):** *"An outside operator on a fresh machine, holding none of the authoring state, now reaches a single, fully-named, legitimately-operator-owned requirement: `/bindings/messaging/responsibleHuman`. Everything else — source discovery, workspace membership, artifact resolution, exact approval, knowledge binding to the public `oats-knowledge` base through OKF 2.1.2, messaging normalize — resolves from the published definitions alone. That is the S2 exit gate met … not 'it works', but 'every remaining item is named and belongs to the operator'."*
89
+ - Coda: the operator has put `responsibleHuman` to Juan as a real decision; if supplied, R publishes and the OKF `check` probe runs (typed status/problems only). If declined, that is a legitimate end state and is recorded as such.
90
+ - Four acceptance trees retained on the operator's side (0.24.1–0.24.4) — every later check is a repeat, not a rebuild.
91
+
53
92
  ## S2 — 0.24.3 gate run (Antares, Juan's machine, 2026-09-21) — verdict
54
93
  - Seams 1, 3, 4 **fixed**; seam 2 fixed as typed (message does not echo the path — deliberate, consistent with no host diagnostics; expectation corrected); seam 5 **half**: attribution (slot/capability/full origins) fixed; naming the missing item only where the kernel knows it. The specificity half is provider-side: OKF 2.1.2 (assigned) and an aweb follow-up.
55
94
  - Six imports resolve at the repinned revision; workspace route selects aweb 1.11.0; **the knowledge slot fully resolves through the workspace route** with `bindings-file`/`state-dir`/`harvest-runtime` (`harvest-model` confirmed optional).
56
95
  - **Messaging was a SOURCE defect, not an operator gap**: aweb 1.11.0 normalize requires the workspace to declare `teams: {private: per-human}` and the soul a messaging declaration; neither was published, so the slot was unreachable by any input. Fixed on main **906b1558** (workspace policy + `teams: []` on the five messaging editions; verified against aweb v1.11.0 normalize) and imports repinned **f3ee31e0**. Remaining operator inputs after the fix: `responsibleHuman` (required) and `wider` (required, may be empty) — the private team is a later `check` outcome.
57
96
  - **S2 exit-gate verdict (amended per the operator):** knowledge — an outside operator on 0.24.3 discovers, resolves, approves and binds from public sources, remaining item attributed: **PASS**. Messaging — on 0.24.3 as published the operator hit a source defect wearing a configuration error; fixed on main 906b1558 and to be re-proven on the repinned imports before it is written as PASS.
58
97
  - **Re-run on 906b1558 (operator, b875669f):** messaging normalize passes; remaining problem is `responsibleHuman`, attributed. But end-to-end is **deadlocked**: `wider: []` (messaging requires) makes OKF 2.1.1 return `invalid-binding` on knowledge (it validates every `operator.bindings` key as a store locator); without it messaging needs `wider`. Operator's wording adopted for S2: *a second operator reaches a published, resolvable configuration boundary for knowledge; messaging is blocked by a kernel-level binding-namespace collision, not by operator input or identity.* Not Juan's to unblock; no identity requested. [Decision](https://github.com/awebai/oats/blob/main/agents/oats-expert/soul/knowledge/decisions/operator-bindings-ownership.md): flat map with declared ownership (`binding.keys`); providers ignore foreign keys (OKF 2.1.2, aweb 1.11.1); kernel attributes/refuses stray keys by name — **deferred to 0.25 by L's scope call (accepted): overlap refusal, filtered forwarding, owned/unowned attribution and the undeclared-provider path need their own regression matrix; the provider ignore rule is the floor and ships in 0.24.4's provider releases.**
59
- - ✅ **PR41 merged `ef211d3e`** (L): inspect accepts prepare's superset (`ignored`), provider reasons cross the wire by exact match (manifest `binding.reasons` / bundled lists), `binding.keys` shape-only, CLI renders `key`. Full gate 1657/1653/0/4 twice (L + maintainer). → **v0.24.4**, kernel-first; OKF 2.1.2 / aweb 1.11.1 floor `>=0.24.4` (closed validator on ≤0.24.3 rejects the fields — P's finding).
98
+ - ✅ **Provider wave published (2026-09-21 17:00Z):** OKF **2.1.2** (oats-okf PR5 → `f02a0d9a`, tag v2.1.2: named-setting reasons, source-owned operator bindings, `binding.reasons`, floor >=0.24.4), aweb **1.11.1** (oats-aweb PR4 → `d404da76`, tag v1.11.1: manifest-only reasons + keys, floor >=0.24.4), **oats.framework 1.1.2** (`oats-framework/v1.1.2` @ `6f98c7c9`), OKF mirror finalized + bundled aweb byte-synced (`08c68ece`), catalog refs, six editions repinned, six imports → `08c68ece`. **Lead's end-to-end probe from the published 0.24.4 tarball via the workspace route, Antares' exact request shape with `wider: []`:** inspect accepts the complete file (`ignored: [operator, launch]`), six imports resolve, both providers approve via the printed `trust` commands, **knowledge binds and messaging normalizes — the deadlock is gone**; the single remaining problem is `key: /bindings/messaging/responsibleHuman` (the operator's own decision). Named reasons cross in one run: `setting bindings-file is required (absolute host path)`; dropping `wider` yields both messaging keys attributed. Awaiting the independent operator's unchanged re-run to write it as PASS. **Seam 1 independently confirmed closed by the operator on 0.24.4** (same file that failed on 0.24.3 → exit 0, `ignored: [operator, launch]`).
99
+ - ✅ **PR41 merged `ef211d3e`** (L): inspect accepts prepare's superset (`ignored`), provider reasons cross the wire by exact match (manifest `binding.reasons` / bundled lists), `binding.keys` shape-only, CLI renders `key`. Full gate 1657/1653/0/4 twice (L + maintainer). → ✅ **v0.24.4 PUBLISHED** (npm both packages, GH release 7 assets, tarball probe: installed validator accepts `binding.reasons`/`keys`, `inspect --request` accepts `operator`/`launch`), kernel-first; OKF 2.1.2 / aweb 1.11.1 floor `>=0.24.4` (closed validator on ≤0.24.3 rejects the fields — P's finding).
60
100
  - Two further kernel findings from the repeat (assigned to L, 0.24.4): (a) seam 1 converged one way only — `inspect --request` still refuses prepare's `operator`/`launch`; (b) **the kernel discards the adapter's reason** at the wire (`provider-binding-wire.mjs:23`) and templates it; aweb already sends whitelisted safe reasons. New [decision](https://github.com/awebai/oats/blob/main/agents/oats-expert/soul/knowledge/decisions/provider-problem-reasons-cross-the-wire.md): whitelisted fixed reasons cross the wire (`binding.reasons`), free text still refused.
61
101
 
62
102
  ## S3 — Messaging (aweb) on the new infrastructure
@@ -0,0 +1,54 @@
1
+ # Desktop parity — slice plan and kernel/provider seams (S8)
2
+
3
+ Status: **proposal accepted for direction** by the lead on 2026-09-22; contracts K1–K8 and P1 are *proposed identifiers*, not shipped CLI grammar. Five policy decisions are routed to the human (below) before their slices start. Author of the plan: the Desktop engineer (`oats-desktop-engineer-1`); this document is the lead's record of it. Scope authority: the human's 2026-09-22 direction — the redesign to the letter, only frames 05 Knowledge and 06 Tasks excluded.
4
+
5
+ ## Slices (each a PR from main; lead reviews and merges)
6
+
7
+ | Slice | Delivers | Needs |
8
+ |---|---|---|
9
+ | 1a | Compact guides; engine-owned ⌘F / ⌘N; rebind-aware hints | — (PR45) |
10
+ | 1b | Shell 01a/01b: right panel Instance · Git & GitHub · Soul tabs, collapse rail, two editor groups, focus mode; panel state survives repaint | existing tabs/terminal lifecycle |
11
+ | 2a | Worktree branch/ahead/behind/path; changes list; bounded unified diff | **K1** |
12
+ | 2b | PR card: title/#/state/commits/closes; per-job checks bound to head OID; unresolved review threads; Open PR; Send threads | **P1**, **K2** |
13
+ | 2c | Remove / Stop confirmations with worktree/branch/open-PR/child/dirty facts and receipts | **K3** |
14
+ | 3 | 03 Souls + Sources: imported editions + local souls, requirements, provenance, editability | **K4** |
15
+ | 4 | 04 Capabilities: official catalog (`oats catalog --json`, 0.24.6+) + deployment inventory/readiness/used-by; Add capability = exact command | landed catalog + list/inspect + **K5** |
16
+ | 5 | 09 First-run readiness quartet; View policy; Skip/Enrol | **K5** + enrollment decision |
17
+ | 6 | 02 Spawn (two-column): soul chooser, provider/model, launch config (restored), work-area naming/worktree/base+branch, opening instruction, attach knowledge, child spawns, auto-PR, readiness, ⌘↵ | **K6** (+ knowledge-node JSON from the knowledge provider; 05 excluded but attach stays) |
18
+ | 7a | 07 Active overview: counts, relations, activity/waiting-on-you, actions, pan/zoom | **K7** |
19
+ | 7b | 08 Schedules: table/toggles/new/edit, next/last, recent runs, transcript handoff, captured-policy preservation | **K8** |
20
+ | 8 | 10 Components: dropdowns, workspace join/manage, Open in split, Detach to window, Open worktree in editor, actions, toasts, collapsed rail | existing seams + Desktop IPC review for detach/editor |
21
+
22
+ ## Shared JSON rules (accepted)
23
+
24
+ Envelope `{schemaVersion:1, ok, result|error}` unchanged. Requests address a server-admitted exact target (`{home, server}` / exact source+revision+soul), never a renderer cwd. Results echo target + `contract`, `version`, `observedAt`, opaque `revision`, typed `problems[]`, explicit completeness/truncation. `null` = not known; empty = observed empty only when complete. Per-section availability `available | not-applicable | unavailable | denied | unsupported | error`. No stack traces, auth stderr, tokens or token-bearing URLs in renderer data. Remote paths are provenance, never local authority. Read-only calls never install, trust, enroll, spawn, fetch into the operator's worktree, switch branches or repair config.
25
+
26
+ ## Seams
27
+
28
+ - **K1 `oats.instance-git` / `oats.instance-diff`** (kernel): typed per-instance Git state — worktree, head, upstream/merge-base comparison (missing upstream ≠ 0/0), NUL-delimited changes with rename paths and per-file counts; bounded unified diff by opaque file id + observation revision (stale selection refuses, never a different file). Replaces the Desktop-only `instance.git` aggregate whose fallback zeros can masquerade as clean.
29
+ - **P1 `oats.instance-github`** (provider, not kernel): PR summary/checks/reviews through an additive Git/review **capability** with its own credential policy (native custody; Desktop never runs `gh`, reads tokens or opens credential forms). Needs a kernel dispatch contract for additive-capability structured views (today `operation run` accepts only knowledge/messaging/tasks). "No PR" ≠ unavailable ≠ unauthenticated ≠ rate-limited. Checks bind to exact head OID. Review markdown is untrusted text.
30
+ - **K2 review-thread delivery**: explicit, confirmed send of selected threads to the exact home's session input with receipt (`delivered | refused | unknown`); delivered ≠ consumed. Typed producer events for commit / branch-renamed / PR-updated / review-request; **no prose parsing** to infer actions.
31
+ - **K3 lifecycle plan/apply**: read-only plan (runtime activity, children, worktree dirt, branch + open PRs, retention per artefact, per-option allowed/default/reason, warnings, blockers) → apply with plan revision + idempotency key, revalidated under the lifecycle lock; per-target `completed | retained | partial | unknown`. Today there is **no standalone stop**, and retire removes owned worktrees; the design's default Remove retains worktree/branch/PR.
32
+ - **K4 souls/sources enumeration**: qualified list of imported editions + authored local souls with identity/source/revision/requirements/declarations/editability; readiness separate from launchability and adoption; no renderer YAML.
33
+ - **K5 readiness quartet**: `installed | trusted | configured | enrolled`, each `pass | fail | unknown | not-applicable` with items (subject, requiredness, reason, producer, evidence, remedy); trust separates artifact approval from `signature {verified|unsigned|unknown|invalid, signer}`; policy view returns **enforced** child-spawn/worktree permissions with origins; native config items report labels/scope state, never secrets; unknown ≠ granted.
34
+ - **K6 spawn preview/apply**: kernel returns suggestions, canonical worktree/home/branch/base OID, resolved knowledge refs, enforced child policy, auto-PR policy, readiness, typed field problems; apply revalidates and captures; no Desktop-derived paths or branches.
35
+ - **K7 activity feed**: bounded typed events per instance with provenance; "waiting on you" only from a producer that reports it.
36
+ - **K8 schedule run history** + captured-policy-preserving edit contract; transcript access via the owning CLI/provider.
37
+
38
+ ## Decisions — DECIDED 2026-09-22 (lead, delegated by the human)
39
+
40
+ Recorded in `agents/oats-expert/soul/knowledge/decisions/desktop-parity-lifecycle-and-policy-decisions.md`: (1) Remove retains worktree/branch/PR by default and re-homes the worktree to the deployment `worktrees/` root before the home is removed; (2) `stop` is a first-class recursive lifecycle route retaining everything for restart; (3) Enrol = workspace member admission with a two-document receipt; "signed by" renders only on a verified Git signature with a named signer; policy rows render enforced policy only; (4) child-spawn permission is enforced by the spawn route (attributed refusal); (5) auto-PR is provider-owned, default off, first pushed commit, draft, human undrafts. Gemini illustrative. The original questions follow for the record.
41
+
42
+ ### Original questions
43
+
44
+ 1. **Remove semantics** (K3, slice 2c): the design's default Remove deletes the instance but *retains* worktree, branch and remote PR; today retirement removes owned worktrees and there is no standalone Stop. Decide: adopt the design's retention default (needs a kernel placement/custody rule for a worktree that outlives its home) or keep current semantics and label the UI accordingly.
45
+ 2. **Recursive Stop** (K3): Stop as a first-class lifecycle action (retain home/worktree for restart) including children — new kernel route.
46
+ 3. **Enrollment** (K5, slice 5): what "Enrol workspace" *is* (workspace admission? team/machine registration?), its authority and receipt; `oats onboard` is bootstrap, not enrollment. Also what counts as **signature evidence** for "Trusted · signed by …" (catalog URL/hash is not a signer).
47
+ 4. **Enforced child-spawn permission** (K6): "Allow child spawns" must be enforced by admitted spawn routes, not advisory — new kernel policy surface.
48
+ 5. **Automatic PR** (K6 / P1): trigger (proposed: first non-empty *pushed* commit), draft status, publication consent; provider-owned; default off; never commits/pushes local data on its own.
49
+
50
+ Also to confirm: prototype "Gemini" runtime is illustrative (not an OATS runtime) unless the human wants kernel work.
51
+
52
+ ## Ownership
53
+
54
+ Lead: K1, K4, K5 (readiness/policy shape), K6 preview/apply plumbing, K7/K8 projections, dispatch contract for additive-capability views — proposed as Decisions where they change contracts, implemented in small PRs otherwise. P1: a new capability (`oats.git`/GitHub backend) — spec'd as a Decision with the human; implementation lane TBD. Desktop engineer: all slices, Desktop IPC review items (detach, open-in-editor).
@@ -79,7 +79,14 @@ the observed revision. Missing or incompatible explicit sources refuse; they do
79
79
  not fall back to the packaged default. The copied edition's package must match
80
80
  the official acquisition; workspace policy, teams and provider adoption values
81
81
  are not silently adopted. Without this option, only the packaged definition and
82
- instruction text are used—no knowledge corpus is bundled.
82
+ instruction text are used—no knowledge corpus is bundled. From 0.24.5, a
83
+ `--workspace` onboarding also reads `package-catalog.json` **from the workspace
84
+ repository at its observed revision** and acquires the `oats.framework` that
85
+ catalog names; the kernel's bundled catalog is only the fallback (it is a
86
+ snapshot at the kernel's own release and lags every framework release cut
87
+ afterwards). `OATS_PACKAGE_CATALOG` still overrides both. The result reports
88
+ `catalog.origin` (`workspace` | `bundled` | `override`), and an integrity
89
+ refusal names the lag when the bundled entry caused it.
83
90
 
84
91
  The manual path below retains its stated older integration/version scope.
85
92
 
package/docs/layers.md CHANGED
@@ -80,7 +80,7 @@ Messaging is conversation, not automatically task state. Accepted knowledge may
80
80
 
81
81
  The current slot name is **`messaging`**. The capability owns native identity, addressing, team membership, transport, wake delivery and qualification. A team alias in a workspace is a declaration, not proof that an actor is enrolled or a privacy property is enforced.
82
82
 
83
- aweb 1.10.3 supports its legacy setup/lifecycle path but lacks the captured provider-binding interface. **aweb 1.11.0** (OATS >=0.24.2) adds it: `check` qualifies HOME-route operational custody for an input-capable Claude/Codex primary with an explicit private team and `delivery: session`; a strict-Pi print primary reports `needs-configuration` rather than dropping the requirement. Qualification is not account delegation, broker delivery or model consumption.
83
+ aweb 1.10.3 supports its legacy setup/lifecycle path but lacks the captured provider-binding interface. **aweb 1.11.0** (OATS >=0.24.2) adds it (1.11.2, OATS >=0.24.4, is code-identical and declares its fixed reasons and `helperInjection: omit`): `check` qualifies HOME-route operational custody for an input-capable Claude/Codex primary with an explicit private team and `delivery: session`; a strict-Pi print primary reports `needs-configuration` rather than dropping the requirement. Qualification is not account delegation, broker delivery or model consumption.
84
84
 
85
85
  The earlier proposed `reach` ladder is **not an enforced universal field**. In particular, aweb's `team_and_contacts` includes verified same-team senders; the compatibility spellings `contacts-only` and `contacts_only` do not establish owner-only admission. A config command succeeding proves neither inbound/outbound restrictions nor knowledge visibility. See the [identity/membership amendment](design/2026-09-08-expert-assisted-deployment-proposal.md#membership-reach-and-visibility-are-separate) and [messaging boundary](design/2026-09-16-messaging-capability-contract.md).
86
86
 
@@ -62,7 +62,7 @@ this policy does not invent new catalog or manifest fields.
62
62
 
63
63
  - Listed capabilities: `oats.okf`, `oats.aweb`, `oats.authoring`, `oats.jira`,
64
64
  `oats.linear`, `oats.dev`, `oats.knowledge-theory`, `oats.core` and `oats.setup`.
65
- - **`oats.framework` 1.1.1** is listed at tag `oats-framework/v1.1.1` in
65
+ - **`oats.framework` 1.1.3** is listed at tag `oats-framework/v1.1.3` in
66
66
  `awebai/oats`, payload root `oats-package`. The `oats.core`, `oats.setup` and
67
67
  `oats.knowledge-theory` aliases select that distribution; package identity is
68
68
  distinct from capability identity. Core supplies operation/soul guidance;
package/docs/packages.md CHANGED
@@ -402,7 +402,7 @@ sources; installing a kernel does not advance existing package locks:
402
402
  {
403
403
  "packages": {
404
404
  "oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v2.0.0", "path": "oats-package" },
405
- "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.1.1", "path": "oats-package" },
405
+ "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.1.3", "path": "oats-package" },
406
406
  "oats.dev": { "url": "https://github.com/awebai/oats-dev.git", "ref": "v1.0.0", "path": "oats-package" }
407
407
  },
408
408
  "capabilities": { "oats.review": "oats.dev" }
@@ -429,7 +429,7 @@ integrity checks. Git transport preserves the canonical source alias.
429
429
 
430
430
  The `oats.framework` distribution package is a separate Git payload in this
431
431
  repository's `oats-package/`, excluded from the kernel npm tarball. The catalog
432
- entry selects the published `oats-framework/v1.1.1` tag, which exports three
432
+ entry selects the published `oats-framework/v1.1.3` tag, which exports three
433
433
  capabilities: `oats.core` (day-to-day operation: `oats-operate`, `oats-souls`
434
434
  and the "you run on OATS" briefing — declared explicitly on every soul by
435
435
  default at creation and removable), `oats.setup` (OATS Soul Setup: `oats-config`,
@@ -0,0 +1,11 @@
1
+ # oats.framework 1.1.3 · aweb 1.11.2 — helper composition for every edition
2
+
3
+ Packaging fix found by the independent second operator on the 0.24.4 wave: behind the operator's `responsibleHuman` requirement, preparation refused with `needs-configuration: new helper injection requires an explicit capability policy`. `oats.okf` had adopted the helper-injection contract (`omit`); its siblings **`oats.core`** and **`oats.aweb`** shipped an `inject` without a `helperInjection` policy, so no edition's OKF harvest helper could compose and **no edition could publish a resolution**. Both remaining blockers were packaging, not operator input.
4
+
5
+ - **`oats.core` 1.0.1** (in oats.framework 1.1.3): `helperInjection: {version: 1, mode: inherit}` — a helper is still an OATS instance and keeps the "you run on OATS" briefing.
6
+ - **aweb 1.11.2**: `helperInjection: {version: 1, mode: omit}` — a harvest helper has no messaging identity. Manifest-only; code identical to 1.11.0.
7
+ - **Release check**: `test/release-packaging.test.mjs` now asserts every framework-shipped capability with an `inject` declares a `helperInjection` policy; the theory-package check pins core's `inherit`. The framework's own packages must pass the contracts the kernel imposes.
8
+ - Catalog: `oats.aweb` → `v1.11.2`, `oats.framework` → `oats-framework/v1.1.3`; six editions and workspace imports repinned.
9
+ - Still open (kernel, 0.25 unless a 0.24.5 is cut): this early refusal is a bare top-level error with no `details`/attribution — it must route through the same problem shape as every other preparation problem. Also noted: `responsibleHuman` is a captured accountability claim the code validates only by shape.
10
+
11
+ Decision: `agents/oats-expert/soul/knowledge/decisions/helper-injection-policy-on-every-injecting-capability.md`.
@@ -0,0 +1,12 @@
1
+ # OATS v0.24.5 — four small fixes from the second-operator wave
2
+
3
+ Kernel/Pi/Desktop **0.24.5**. No contract, schema or authority change. Every item was found while an independent operator (and then the lead) drove the published 0.24.4 wave to its first end-to-end resolution.
4
+
5
+ - **Retirement recovers a worktree on the branch it actually has.** `oats retire` derived the recovery clone's branch from `instance.json` (the spawn-time branch); a worktree that had legitimately switched branches during its task could never pass recovery verification and became unretirable by the normal path, even with nothing unpreserved. Recovery now derives the branch from the worktree while it exists (detached HEADs recover at their exact commit), falls back to the recorded branch only when the worktree is gone, and records the drift in `recovery.json` (`branchDrift`). Fail-closed behaviour is unchanged; the source of truth moved to the object.
6
+ - **Captured launch: `ifInstalled` runtime rows are not hard blocks, and refusals are attributed.** A provider's `requires` row marked `ifInstalled: true` (a version floor for an ambient package, if present) was treated as a hard requirement, so a Pi launch with aweb `delivery: session` could never publish a resolution — the same request without a `launch` block published fine. Such rows are now satisfied by absence in captured preparation. Remaining hard rows refuse through the ordinary preparation problem shape (`slot`, `capability`, `runtime`, `package`, the manifest's own `install` text) instead of one bare `needs-configuration`.
7
+ - **Helper-injection refusal is attributed.** A capability that ships an `inject` without a `helperInjection` policy now yields a preparation problem naming that capability (with `origins`), so the operator learns *which* sibling has not adopted the contract rather than only that one has.
8
+ - **`oats onboard --workspace` acquires the framework the workspace's catalog names.** The kernel tarball ships `package-catalog.json` as a snapshot at the kernel's tag, so it lags every `oats.framework` release cut afterwards; onboarding a current workspace edition against it refused as `integrity-drift` — correct, but a fresh machine has no reviewed list to point `OATS_PACKAGE_CATALOG` at. Onboarding now reads the workspace repository's `package-catalog.json` at the observed revision and uses its `oats.framework` entry; the bundled entry is the fallback, `OATS_PACKAGE_CATALOG` still overrides. The result reports `catalog.origin`, and a drift refusal names the lag and both integrities.
9
+
10
+ Rule going forward (recorded in the maintainer's knowledge): **`prepare` never ends in a bare `needs-configuration` after selection** — every refusal is a problem with a slot/capability, or it is a kernel defect. Remaining bare sites are pre-selection input errors, record-shape guards and the skill-name collision, none reachable from a published edition.
11
+
12
+ Not in this release: `binding.keys` enforcement and the dotted-exact-key grammar (0.25); OKF `check` per-cause reasons (OKF 2.1.3, deferred with harvest).
@@ -0,0 +1,9 @@
1
+ # OATS v0.24.6 — `oats catalog` and the first Desktop parity slice
2
+
3
+ Kernel/Pi/Desktop **0.24.6**. No contract, schema or authority change.
4
+
5
+ - **`oats catalog [--json]`** — read-only description of the *effective* official package catalog: where it came from (`bundled` snapshot or `OATS_PACKAGE_CATALOG` override), every package with url/ref/payload root and the exact `oats install` argv, and the capability→package alias map with each alias's resolution. Identity and discovery only: nothing is acquired, trusted or fetched (the contract test asserts no filesystem writes). Built for the Desktop's Capabilities view, which cannot import the kernel; the notes in the payload state that catalog identity grants no executable trust and that a bundled catalog may lag later package releases. Aliases are mappings, not an export inventory.
6
+ - **Desktop parity slice 1a** (`feat/desktop-sidebar-keyboard-parity`): compact relation guides in the roster (nesting 14→8px, elbows 7→4px, gutters 8→6px — the human's request for more horizontal room; rows stay 56px, three themes keep computed AA), engine-owned **⌘F** reveal-and-focus filter (un-hides the sidebar first) and **⌘N** explicit soul chooser (never auto-spawns), visible shortcut hints that follow user rebinds/unbinds. Renderer-only; terminal key passthrough and focus ownership unchanged.
7
+ - **CI**: the pull-request workflow now checks out full history — the workspace-layout guard reads pinned import revisions with `git cat-file`, and a shallow clone made main red from `f34207e9` to `475ea9bf`.
8
+
9
+ Direction for the rest of S8 is recorded in `docs/design/2026-09-22-desktop-parity-seams.md` (slices 1b–8; kernel seams K1–K8; provider seam P1) and the lifecycle/enrollment/permission/auto-PR decisions in the maintainer's knowledge base. Frames 05 Knowledge and 06 Tasks are excluded by direction.
@@ -106,26 +106,26 @@ onboarding and legacy roster/knowledge cutover remain separate.
106
106
  Store `oats` requires the explicit `stores.oats` binding; no publisher writer,
107
107
  production store or grants are supplied. An acceptance fixture is parent-owned
108
108
  and cannot be counted as production knowledge adoption.
109
- - Current authored expert editions require knowledge **oats.okf@2.1.1** and
110
- messaging **oats.aweb@1.11.0**, not optional defaults. These published revisions
109
+ - Current authored expert editions require knowledge **oats.okf@2.1.2** and
110
+ messaging **oats.aweb@1.11.2** (both OATS >=0.24.4), not optional defaults. These published revisions
111
111
  are **not proof that their combined bindings/runtime profile is ready**. The provider
112
112
  owner supplies that evidence and any subsequently reviewed compatible revision.
113
113
  Do not replace either requirement with none or erase a read edge to launch.
114
114
 
115
115
  At those authored revisions, the provider boundary is concrete:
116
116
 
117
- - Published OKF2.1.1 supports `inherit: stores.oats`, normalized to
117
+ - Published OKF 2.1.2 supports `inherit: stores.oats`, normalized to
118
118
  `/bindings/knowledge/stores/oats`. The explicit `destination: oats` preserves
119
119
  routing; omitting it would instead require `write.default`. No new schema,
120
120
  owner or production locator is needed for this declaration.
121
121
  - aweb 1.10.3 (`24efa6f9`) has no portable binding interface; **aweb 1.11.0**
122
- (`v1.11.0`, OATS >=0.24.2) adds it and the five editions now pin it. Its `check`
122
+ (`v1.11.0`, OATS >=0.24.2) adds it; **1.11.1/1.11.2** (OATS >=0.24.4) are code-identical and declare its fixed reasons, owned operator keys and `helperInjection: omit` (a harvest helper has no messaging identity); the five editions pin 1.11.2. Its `check`
123
123
  qualifies only an input-capable Claude/Codex primary with an explicit private team
124
124
  and `delivery: session`; strict-Pi print reports `needs-configuration`. Status is on
125
125
  the [program board](design/2026-09-20-redesign-program-board.md). Qualification
126
126
  is HOME-route operational custody only: not human/native-principal delegation,
127
127
  private grants, broker delivery or model consumption.
128
- - Published OKF2.1.1 accepts retained Claude/Codex helpers with the complete approved
128
+ - Published OKF 2.1.2 accepts retained Claude/Codex helpers with the complete approved
129
129
  capability closure and native-default model intent. Strict Pi still requires an
130
130
  explicit model and the sole-OKF profile; Pi plus messaging remains unqualified.
131
131
  This provider release alone is not combined-profile acceptance. Do not silently
@@ -143,7 +143,7 @@ workspace declares the per-human private team policy (`teams: {private: per-huma
143
143
  and every messaging edition carries its `teams: []` declaration — without them the
144
144
  aweb provider cannot normalize in workspace context, as the second operator found.
145
145
  Workspace update `f3ee31e0`. The five knowledge-owning experts therefore select
146
- OKF2.1.1 and aweb1.11.0 with explicit core; setup remains provider-independent,
146
+ OKF 2.1.2 and aweb 1.11.2 with explicit core; setup remains provider-independent,
147
147
  requiring core/setup and defaulting all three fundamental layers to none. It is
148
148
  not a sixth knowledge owner. This deliberate repin, not a catalog/kernel upgrade
149
149
  alone, advances the selected source requirements. Successful source inspection,
@@ -71,7 +71,7 @@ name: domain-expert
71
71
  requires:
72
72
  knowledge:
73
73
  capability: oats.okf
74
- source: git:github.com/awebai/oats-okf@v2.1.1#oats-package
74
+ source: git:github.com/awebai/oats-okf@v2.1.2#oats-package
75
75
  ```
76
76
 
77
77
  This illustrates software selection, not complete OKF provisioning: the chosen capability also needs its own valid knowledge declaration, bindings and accepted base.
@@ -32,8 +32,28 @@ export function validateCapturedLaunchRequest(value, validateConfig) {
32
32
  export function compileCapturedLaunchRequest(request,{artifacts,manifests,settings,resources},kernel) {
33
33
  if(request===undefined)return null;
34
34
  validateCapturedLaunchRequest(request,kernel.validateLaunchConfig);
35
- const required=kernel.runtimeRequirements(request.runtime,[...manifests].filter(([id])=>Object.hasOwn(artifacts.capabilities,id)).map(([id,manifest])=>({id,manifest,settings:settings[id]})));
36
- if(required.length)throw oatsError('needs-configuration','runtime package requirements need retained runtime roots and a qualified loader; no ambient package discovery was used');
35
+ const providers=[...manifests].filter(([id])=>Object.hasOwn(artifacts.capabilities,id)).map(([id,manifest])=>({id,manifest,settings:settings[id]}));
36
+ // `ifInstalled: true` rows constrain a package that may legitimately be absent (a version floor
37
+ // for an ambient extension, if any). Captured preparation retains no ambient packages, so such a
38
+ // row is satisfied by absence — treating it as a hard requirement made a Pi launch with aweb
39
+ // `delivery: session` unpublishable (second-operator finding, 2026-09-21). Hard rows refuse
40
+ // through the preparation problem shape, naming the capability and package; the remedy text is
41
+ // the manifest's own `install` string — declared data, never free text.
42
+ // Resolve each applicable row back to its declaration, honouring the same `when` predicate the
43
+ // kernel's requirement selection uses, so two rows for one package (channel vs session) stay distinct.
44
+ const holds=(provider,row)=>!row.when || Object.entries(row.when).every(([k,v])=>String(provider.settings?.[k] ?? '')===String(v));
45
+ const declarationOf=(row)=>{const provider=providers.find(p=>p.id===row.capability);
46
+ return {provider,declared:provider?.manifest?.requires?.find(r=>r && typeof r==='object' && r.runtime===request.runtime && r.package===row.package && holds(provider,r))};};
47
+ const required=kernel.runtimeRequirements(request.runtime,providers).filter(row=>declarationOf(row).declared?.ifInstalled!==true);
48
+ if(required.length){
49
+ const error=oatsError('needs-configuration','runtime package requirements need retained runtime roots and a qualified loader; no ambient package discovery was used');
50
+ error.problems=required.map(row=>{
51
+ const {provider,declared}=declarationOf(row);
52
+ return {code:'needs-configuration',message:`${request.runtime} launch requires runtime package ${row.package}, which captured preparation has not retained`,capability:row.capability,
53
+ ...(provider?.manifest?.layer?{slot:provider.manifest.layer}:{}),runtime:request.runtime,package:row.package,...(typeof declared?.install==='string'?{install:declared.install}:{})};
54
+ });
55
+ throw error;
56
+ }
37
57
  const base={version:1,runtime:request.runtime,args:request.args,env:request.env,model:request.model,yolo:request.yolo,
38
58
  hooks:{launch:{},env:{},contributions:[],pending:true},prompt:{kind:'task-file',file:'TASK.md'}};
39
59
  if(typeof request.executable==='string')return {...base,executable:request.executable,executableResolvedFrom:'explicit-host'};
package/lib/core.mjs CHANGED
@@ -2143,6 +2143,24 @@ function readCatalogFile() {
2143
2143
  export function officialCapabilityAliases() {
2144
2144
  return readCatalogFile().capabilities;
2145
2145
  }
2146
+ /** Read-only description of the EFFECTIVE official catalog for consumers that
2147
+ * cannot import this module (the zero-dependency Desktop): where the list came
2148
+ * from, every package entry, every alias, and — per capability alias — the
2149
+ * resolved package. Identity and discovery only: nothing here acquires,
2150
+ * trusts or verifies; the exact acquire argv is data the operator runs. */
2151
+ export function describeOfficialCatalog() {
2152
+ const { packages, capabilities, file } = readCatalogFile();
2153
+ const origin = process.env.OATS_PACKAGE_CATALOG ? "override" : "bundled";
2154
+ const entries = Object.entries(packages).map(([id, e]) => ({ package: id, url: e?.url ?? null, ref: e?.ref ?? null, path: e?.path ?? DEFAULT_PACKAGE_PATH,
2155
+ acquire: { argv: ["oats", "install", id] } }));
2156
+ const aliases = Object.entries(capabilities).map(([capability, target]) => {
2157
+ const resolved = officialCapabilityPackage(capability, { aliases: capabilities });
2158
+ return { capability, package: resolved.package, capabilityInPackage: resolved.migratedCapability, via: resolved.via, available: resolved.available };
2159
+ });
2160
+ return { schemaVersion: 1, catalog: { origin, file, kernelVersion: OATS_VERSION }, packages: entries, capabilityAliases: aliases,
2161
+ notes: ["catalog identity grants no executable trust; acquisition verifies bytes and approval is a separate explicit step",
2162
+ "a bundled catalog is a snapshot at this kernel's release and may lag later package releases; the reviewed list lives in the framework repository"] };
2163
+ }
2146
2164
 
2147
2165
  /** Which official package supplies a legacy (v1, `marketplace:`) capability.
2148
2166
  * Alias first, then identity (package id == capability id); `available` says
@@ -7080,6 +7098,15 @@ function fingerprintTree(root, { excludeRoot = new Set(), excludeGitMetadata = f
7080
7098
  return `sha256:${hash.digest("hex")}`;
7081
7099
  }
7082
7100
 
7101
+ /** The ref a worktree actually has checked out: `{branch, oid}` with
7102
+ * `branch === null` when HEAD is detached. Recovery derives truth from the
7103
+ * object, never from spawn-time metadata. */
7104
+ function worktreeRef(work) {
7105
+ const oid = execFileSync("git", ["-C", work, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).trim();
7106
+ let branch = null;
7107
+ try { branch = execFileSync("git", ["-C", work, "symbolic-ref", "--quiet", "--short", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).trim() || null; } catch { branch = null; }
7108
+ return { branch, oid };
7109
+ }
7083
7110
  function worktreeStatus(repo) {
7084
7111
  try {
7085
7112
  return execFileSync("git", ["-C", repo, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignored=matching", "--ignore-submodules=none"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
@@ -8304,9 +8331,21 @@ function preserveRetirementWork(observation, meta, instance) {
8304
8331
  if (fingerprintTree(observation.home, { excludeRoot: new Set(["work"]) }) !== fingerprintTree(recoveredHome)) {
8305
8332
  throw new Error("home recovery verification disagreed with the source");
8306
8333
  }
8334
+ let branchDrift;
8307
8335
  if (!homeOnly && meta.work === "worktree" && meta.repo && meta.branch && (existsSync(observation.work) || observation.branchExists)) {
8308
8336
  const recoveredRepo = join(staging, "repo");
8309
- execFileSync("git", ["clone", "--no-local", "--quiet", "--branch", meta.branch, meta.repo, recoveredRepo], { stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
8337
+ // The branch is derived from the worktree while it exists: an instance
8338
+ // that legitimately switched branches during its task must still be
8339
+ // recoverable, and the recorded spawn-time branch is only the fallback
8340
+ // when the worktree is gone. Detached HEADs recover at their exact OID.
8341
+ const ref = existsSync(observation.work) ? worktreeRef(observation.work) : { branch: meta.branch, oid: null };
8342
+ if (ref.branch !== meta.branch) branchDrift = { recordedBranch: meta.branch, worktreeBranch: ref.branch, detachedAt: ref.branch === null ? ref.oid : null };
8343
+ if (ref.branch !== null) execFileSync("git", ["clone", "--no-local", "--quiet", "--branch", ref.branch, meta.repo, recoveredRepo], { stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
8344
+ else {
8345
+ execFileSync("git", ["clone", "--no-local", "--quiet", "--no-checkout", meta.repo, recoveredRepo], { stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
8346
+ execFileSync("git", ["-C", recoveredRepo, "fetch", "--quiet", observation.work, ref.oid], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER });
8347
+ execFileSync("git", ["-C", recoveredRepo, "checkout", "--quiet", "--detach", ref.oid], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER });
8348
+ }
8310
8349
  const sourceGitContext = existsSync(observation.work) ? observation.work : meta.repo;
8311
8350
  detachRecoveryClone(sourceGitContext, recoveredRepo);
8312
8351
  if (existsSync(observation.work)) {
@@ -8325,7 +8364,8 @@ function preserveRetirementWork(observation, meta, instance) {
8325
8364
  if (worktreeStatus(observation.work) !== worktreeStatus(recoveredRepo)) throw new Error("recovered Git index/status disagreed with the source");
8326
8365
  }
8327
8366
  const recoveredHead = execFileSync("git", ["-C", recoveredRepo, "rev-parse", "HEAD"], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
8328
- const sourceHead = execFileSync("git", ["-C", meta.repo, "rev-parse", `refs/heads/${meta.branch}`], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
8367
+ const sourceHead = ref.branch === null ? ref.oid
8368
+ : execFileSync("git", ["-C", meta.repo, "rev-parse", `refs/heads/${ref.branch}`], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
8329
8369
  if (recoveredHead !== sourceHead) throw new Error("recovery clone does not retain the instance branch tip");
8330
8370
  }
8331
8371
  if (observation.directory && observation.directoryFingerprint) {
@@ -8336,7 +8376,7 @@ function preserveRetirementWork(observation, meta, instance) {
8336
8376
  }
8337
8377
  }
8338
8378
  const repoCopy = homeOnly ? { copied: false, reason: "Only instance-home bytes changed; no work state requires a repository copy", source: meta.repo, branch: meta.branch } : undefined;
8339
- writeFileSync(join(staging, "recovery.json"), JSON.stringify({ version: 1, instance, classes: observation.classes, sourceHome: observation.home, createdAt: new Date().toISOString(), ...(repoCopy ? { repoCopy } : {}) }, null, 2) + "\n", { mode: 0o600 });
8379
+ writeFileSync(join(staging, "recovery.json"), JSON.stringify({ version: 1, instance, classes: observation.classes, sourceHome: observation.home, createdAt: new Date().toISOString(), ...(repoCopy ? { repoCopy } : {}), ...(branchDrift ? { branchDrift } : {}) }, null, 2) + "\n", { mode: 0o600 });
8340
8380
  mkdirSync(dirname(recovery), { recursive: true });
8341
8381
  renameSync(staging, recovery);
8342
8382
  return { path: recovery, classes: observation.classes, ...(repoCopy ? { repoCopy } : {}) };
@@ -35,7 +35,13 @@ export function captureHelperInjectionChoices(plan, definitions) {
35
35
  const policy = helperInjectionFact(definition), id = definition.artifact.capability;
36
36
  if (policies.has(id)) throw oatsError('invalid-resolution', 'duplicate helper policy owner');
37
37
  policies.set(id, policy);
38
- if (!policy.fact && policy.manifest.inject) throw oatsError('needs-configuration', 'new helper injection requires an explicit capability policy');
38
+ if (!policy.fact && policy.manifest.inject) {
39
+ // Attributed like every other preparation problem: the operator learns WHICH
40
+ // capability lacks the declaration, not only that one does (second-operator finding).
41
+ const error = oatsError('needs-configuration', 'new helper injection requires an explicit capability policy');
42
+ error.problems = [{ code: 'needs-configuration', message: 'capability ships an inject without a helperInjection policy', capability: id, ...(policy.manifest.layer ? { slot: policy.manifest.layer } : {}) }];
43
+ throw error;
44
+ }
39
45
  if (policy.fact) requirements.push(policy.fact);
40
46
  }
41
47
  const resolved = resolveChoices({ requirements, candidates: plan.candidates });
@@ -31,6 +31,16 @@ function packageSubset(artifacts, root) {
31
31
 
32
32
  /** Repositories and kernel callbacks belong to this operation; the caller owns
33
33
  * their lifetime/scratch cleanup. No hook, launch, enrollment or approval here. */
34
+ /** Origins of the choices that selected a capability — the same derivation preparation
35
+ * bindings use, so completion refusals render beside binding problems. */
36
+ function originsOf(plan, capability) {
37
+ const seen = new Map();
38
+ for (const key of plan.capabilities?.[capability]?.choiceKeys ?? []) {
39
+ const origin = plan.choices?.[key]?.selectedBy;
40
+ if (origin) seen.set(canonicalJson(origin), origin);
41
+ }
42
+ return [...seen.values()];
43
+ }
34
44
  export function prepareComposition(input, { repositories, kernel, previous: suppliedPrevious }) {
35
45
  canonicalJson(input);
36
46
  objectAt(input, ["deployment", "directory", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "directory", "source", "origin"]);
@@ -114,8 +124,17 @@ export function prepareComposition(input, { repositories, kernel, previous: supp
114
124
  if (operator) declarations.push(operatorBindingDeclaration(operator));
115
125
  const bound = prepareProviderBindings({ seed, plan, manifests, declarations }, options => kernel.binding({ ...options, deployment }));
116
126
  plan = bound.plan; seed = bound.seed;
117
- const completion = bound.problems.length ? { record: null, problems: bound.problems }
118
- : kernel.complete({ seed, plan, manifests, mode, deployment, directory, launch: input.launch, helperLaunches: input.helperLaunches });
127
+ let completion;
128
+ if (bound.problems.length) completion = { record: null, problems: bound.problems };
129
+ else {
130
+ try { completion = kernel.complete({ seed, plan, manifests, mode, deployment, directory, launch: input.launch, helperLaunches: input.helperLaunches }); }
131
+ catch (error) {
132
+ // A typed completion refusal that names its capability is a preparation problem like any
133
+ // other (slot/capability/origins), never a bare top-level error. Anything else propagates.
134
+ if (!Array.isArray(error?.problems) || !["needs-configuration", "requirement-conflict"].includes(error.code)) throw error;
135
+ completion = { record: null, problems: error.problems.map(problem => ({ ...problem, origins: problem.origins ?? originsOf(plan, problem.capability) })) };
136
+ }
137
+ }
119
138
  let resolution = null;
120
139
  if (completion.record) resolution = commitCapturedResolution(deployment, completion.record);
121
140
  // Publishing valid immutable inputs/records need not roll back on a later
@@ -37,11 +37,28 @@ function repositoryRequest(source) {
37
37
  }
38
38
  }
39
39
 
40
+ /** `packages["oats.framework"]` from the repository's own `package-catalog.json` at the observed
41
+ * revision, or null when the file or entry is absent. Data only; the acquisition still verifies
42
+ * bytes, and the edition/package integrity comparison still applies. */
43
+ function frameworkCatalogEntry(transaction, observation) {
44
+ const file = transaction.readFile(observation, 'package-catalog.json', { optional: true });
45
+ if (!file) return null;
46
+ let doc;
47
+ try { doc = JSON.parse(file.bytes.toString('utf8')); } catch { throw oatsError('invalid-source', 'workspace package-catalog.json is not valid JSON'); }
48
+ const entry = doc && typeof doc === 'object' && !Array.isArray(doc) && doc.packages && typeof doc.packages === 'object' && !Array.isArray(doc.packages)
49
+ ? doc.packages['oats.framework'] : undefined;
50
+ if (entry === undefined) return null;
51
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof entry.url !== 'string' || typeof entry.ref !== 'string' || (entry.path !== undefined && typeof entry.path !== 'string')) {
52
+ throw oatsError('invalid-source', 'workspace package-catalog.json has an invalid oats.framework entry');
53
+ }
54
+ return { url: entry.url, ref: entry.ref, ...(entry.path === undefined ? {} : { path: entry.path }), revision: observation.source.commit };
55
+ }
56
+
40
57
  export function loadSetupExpertEdition(workspace, repositoryOptions = {}) {
41
58
  if (workspace === undefined) {
42
59
  const root = fileURLToPath(new URL(`../${EXPORT}/`, import.meta.url));
43
60
  return { declaration: validateEdition(readFileSync(join(root, 'soul.yaml'))), instructions: readFileSync(join(root, 'AGENTS.md'), 'utf8'),
44
- source: { kind: 'packaged-definition', captured: false }, packageIntegrity: null };
61
+ source: { kind: 'packaged-definition', captured: false }, packageIntegrity: null, catalogEntry: null };
45
62
  }
46
63
  const request = repositoryRequest(workspace), scratch = realpathSync(mkdtempSync(join(tmpdir(), 'oats-setup-source-'))), owned = lstatSync(scratch);
47
64
  let transaction;
@@ -64,7 +81,14 @@ export function loadSetupExpertEdition(workspace, repositoryOptions = {}) {
64
81
  || readdirSync(soulRoot).some(name => !['soul.yaml', 'AGENTS.md', 'CLAUDE.md'].includes(name))) {
65
82
  throw oatsError('source-incomplete', 'classic setup edition must have canonical AGENTS.md/CLAUDE.md and no omitted private skill or knowledge trees');
66
83
  }
84
+ // The workspace publishes the reviewed catalog: read the oats.framework entry at the WORKSPACE's
85
+ // observed revision (falling back to the edition's own revision when the source has no workspace
86
+ // document), so onboarding acquires the framework the workspace currently names instead of
87
+ // whatever the kernel tarball snapshotted at its own tag. The edition/package integrity check
88
+ // below still decides whether that acquisition matches the copied edition.
89
+ const catalogEntry = frameworkCatalogEntry(transaction, observed) ?? frameworkCatalogEntry(transaction, imported.observation);
67
90
  return { declaration, instructions: readFileSync(body, 'utf8'), packageIntegrity: packageIntegrity(join(projection, 'oats-package')),
91
+ catalogEntry,
68
92
  source: { kind: 'exported-edition-copy', source: imported.reference.source, revision: imported.observation.source.commit,
69
93
  path: imported.reference.soul, workspaceRevision: view?.source.commit ?? null, captured: false } };
70
94
  } finally {
@@ -3,12 +3,12 @@
3
3
  "packages": {
4
4
  "oats.okf": {
5
5
  "url": "https://github.com/awebai/oats-okf.git",
6
- "ref": "v2.1.1",
6
+ "ref": "v2.1.2",
7
7
  "path": "oats-package"
8
8
  },
9
9
  "oats.aweb": {
10
10
  "url": "https://github.com/awebai/oats-aweb.git",
11
- "ref": "v1.11.0",
11
+ "ref": "v1.11.2",
12
12
  "path": "oats-package"
13
13
  },
14
14
  "oats.jira": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "oats.framework": {
35
35
  "url": "https://github.com/awebai/oats.git",
36
- "ref": "oats-framework/v1.1.1",
36
+ "ref": "oats-framework/v1.1.3",
37
37
  "path": "oats-package"
38
38
  }
39
39
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.24.4",
3
+ "version": "0.24.6",
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",