@awebai/oats 0.27.2 → 0.28.0

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.
Files changed (37) hide show
  1. package/bin/oats.mjs +185 -26
  2. package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +8 -3
  3. package/capabilities/oats-okf/bin/oats-okf.mjs +33 -28
  4. package/capabilities/oats-okf/injects/okf.md +29 -21
  5. package/capabilities/oats-okf/lib/config.mjs +5 -1
  6. package/capabilities/oats-okf/lib/consult.mjs +500 -0
  7. package/capabilities/oats-okf/lib/inspection.mjs +11 -3
  8. package/capabilities/oats-okf/lib/io.mjs +9 -2
  9. package/capabilities/oats-okf/lib/sources.mjs +15 -53
  10. package/capabilities/oats-okf/lib/stores.mjs +10 -7
  11. package/capabilities/oats-okf/lib/worker.mjs +9 -1
  12. package/capabilities/oats-okf/oats.json +13 -4
  13. package/capabilities/oats-okf/skills/okf/SKILL.md +14 -6
  14. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +142 -0
  15. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
  16. package/docs/design/2026-09-24-phase-d-plan.md +11 -0
  17. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +241 -0
  18. package/docs/design/2026-09-26-okf-knowledge-operations.md +389 -0
  19. package/docs/desktop-cli-api.md +93 -7
  20. package/docs/oats-local.schema.json +2 -1
  21. package/docs/oats-package.schema.json +39 -0
  22. package/docs/packages.md +67 -3
  23. package/docs/release-notes/v0.28.0.md +144 -0
  24. package/docs/schedules.md +99 -1
  25. package/docs/souls-and-instances.md +7 -3
  26. package/docs/workspaces.md +8 -2
  27. package/lib/core.mjs +22 -4
  28. package/lib/instance-inspect.mjs +4 -4
  29. package/lib/instance-resolution.mjs +62 -18
  30. package/lib/materialize.mjs +13 -0
  31. package/lib/packages.mjs +90 -6
  32. package/lib/resolve.mjs +20 -2
  33. package/lib/schedule.mjs +24 -11
  34. package/lib/triggers.mjs +545 -0
  35. package/lib/workspace.mjs +80 -3
  36. package/package-catalog.json +1 -1
  37. package/package.json +1 -1
@@ -57,9 +57,16 @@ export function materialize(root, files) {
57
57
  safePath(root); fs.mkdirSync(root, { recursive: true });
58
58
  for (const [p, content] of Object.entries(files)) atomic(join(root, relPath(p)), Buffer.from(content, 'base64'));
59
59
  }
60
- export function withLock(path, fn) {
60
+ const pause = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
61
+ /** Cooperative directory lock. `waitMs` lets a reader queue behind a live
62
+ * holder for a bounded time; an abandoned lock is still never reclaimed. */
63
+ export function withLock(path, fn, { waitMs = 0 } = {}) {
61
64
  safePath(path); fs.mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
62
- try { fs.mkdirSync(path, { mode: 0o700 }); } catch(e) { if(e.code === 'EEXIST') fail('E_LOCKED', `busy or abandoned lock: ${path}; inspect owner.json, never reclaim by age`); throw e; }
65
+ const deadline = Date.now() + waitMs;
66
+ for (let delay = 20;; delay = Math.min(delay * 2, 250)) {
67
+ try { fs.mkdirSync(path, { mode: 0o700 }); break; }
68
+ catch(e) { if(e.code !== 'EEXIST') throw e; if(Date.now() >= deadline) fail('E_LOCKED', `busy or abandoned lock: ${path}; inspect owner.json, never reclaim by age`); pause(delay); }
69
+ }
63
70
  const owner = { token: randomUUID(), pid: process.pid, host: hostname() };
64
71
  save(join(path, 'owner.json'), owner);
65
72
  try { return fn(); } finally { if (readJSON(join(path, 'owner.json')).token === owner.token) { fs.rmSync(path, { recursive: true }); syncDir(dirname(path)); } }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { fs, join, dirname, resolve, safePath, readJSON, save, atomic, materialize, hash, withLock, oats, fail, tree, overlaps, syncDir, identifier, relPath } from './io.mjs';
3
- import { loadBindings, declaration, metadata, resolveNodes, bindingFingerprint, settings, validateBindings } from './config.mjs';
4
- import { stageBase } from './stores.mjs';
2
+ import { fs, join, dirname, resolve, safePath, readJSON, save, atomic, hash, withLock, oats, fail, tree, overlaps, identifier } from './io.mjs';
3
+ import { loadBindings, declaration, bindingFingerprint, settings, validateBindings } from './config.mjs';
4
+ import { acceptedResolution } from './consult.mjs';
5
5
  import { loadInvocationKnowledgeBinding, readPrivateInvocationJson, sourceRuntimeFromKnowledgeBinding } from './binding-wire.mjs';
6
6
  import { sameJson } from './portable-binding.mjs';
7
7
  import { qualifiedSoulIdentity } from './source-contract.mjs';
@@ -89,44 +89,14 @@ export function service(home) {
89
89
  if(fs.existsSync(join(home,'instance.json'))) return readJSON(join(home,'instance.json')).kind==='capability';
90
90
  return process.env.OATS_KIND==='capability';
91
91
  }
92
- // Control files live at the view root; arbitrary legal aliases live ONLY in
93
- // bases/. Receipts use paths relative to the view so moving a prepared view
94
- // into its final location cannot invalidate its navigation.
95
- export function views(bindings, decl, target) {
96
- target=safePath(target); if(fs.existsSync(target)) fail('E_VIEW','view exists; use a new immutable view destination');
97
- const all={}; const receipts={};
98
- fs.mkdirSync(dirname(target),{recursive:true});
99
- const pending=fs.mkdtempSync(join(dirname(target),'.okf-view-'));
100
- try {
101
- for(const [alias,base] of Object.entries(bindings.bases)) {
102
- const scratch=fs.mkdtempSync(join(bindings.stateDir,'read-'));
103
- try {
104
- const staged=stageBase(base,join(scratch,'base'),{alias});
105
- all[alias]=staged.meta;
106
- const path=`bases/${alias}`;
107
- materialize(join(pending,path),staged.files);
108
- receipts[alias]={path,id:base.id,digest:staged.digest,head:staged.head || null,nodes:staged.meta.nodes};
109
- } finally { fs.rmSync(scratch,{recursive:true,force:true}); }
110
- }
111
- resolveNodes(decl,bindings,all);
112
- save(join(pending,'view.json'),{version:1,at:new Date().toISOString(),bases:receipts,owns:decl.owns,reads:decl.reads});
113
- // Never expose a partial view or remove/replace a caller's existing view.
114
- if(fs.existsSync(target)) fail('E_VIEW','view exists; use a new immutable view destination');
115
- fs.renameSync(pending,target);syncDir(dirname(target));
116
- return receipts;
117
- } finally { fs.rmSync(pending,{recursive:true,force:true}); }
118
- }
119
- const registrationView = source => join(source.home,`.okf-view-${source.id}`);
92
+ // okf 3.0.0 materializes no instance copy of any base: registration records
93
+ // the accepted resolution (per base: commit or digest, and its nodes) and the
94
+ // instance consults the bases remotely through `oats okf`. A ./knowledge/ left
95
+ // by okf 2.x is not touched; inspect reports it as a legacy local view.
96
+ const acceptedNodes = view => Object.fromEntries(Object.entries(view).map(([alias,row])=>[alias,row.nodes]));
120
97
  function finishRegistration(source) {
121
- const pending=safePath(registrationView(source)),target=safePath(join(source.home,'knowledge'));
122
- if(fs.existsSync(pending)) {
123
- if(fs.existsSync(target)) fail('E_VIEW','knowledge already exists; refusing to replace it with the registered view');
124
- fs.renameSync(pending,target);syncDir(source.home);
125
- }
126
98
  // A durable home pointer precedes publication. Failures after it was saved
127
- // resume this same source/view; they never reset captured evidence or IDs.
128
- const receipt=readJSON(join(target,'view.json'));
129
- if(source.acceptedView && JSON.stringify(receipt.bases)!==JSON.stringify(source.acceptedView)) fail('E_VIEW','registered accepted view receipt differs; preserve it and inspect');
99
+ // resume this same source; they never reset captured evidence or IDs.
130
100
  for(const [p,text] of [['STATE.md','# Working state\n\n# Task\n\n# Next\n'],['log.md','# Instance log\n']]) if(!fs.existsSync(join(source.home,p))) atomic(join(source.home,p),text);
131
101
  fs.mkdirSync(join(source.home,'notes'),{recursive:true});
132
102
  scheduleSource(source);return source;
@@ -155,8 +125,6 @@ export function registerCaptured(home,receipt) {
155
125
  const source=homeSource(home);if(!sameCapturedReceipt(source,receipt,captured)) fail('E_SOURCE','captured registration receipt differs from durable source');
156
126
  return finishRegistration(source);
157
127
  }
158
- safePath(join(home,'knowledge'));
159
- if(fs.existsSync(join(home,'knowledge'))) fail('E_VIEW','unregistered knowledge view exists; preserve it and inspect before registering');
160
128
  if(['.okf-harvest-record.json','.okf-harvest-record.next.json'].some(path=>fs.existsSync(join(home,path)))) fail('E_MIGRATION','legacy source watermarks require explicit migration before captured registration');
161
129
  if(overlaps(home,captured.context) && captured.context.startsWith(home)) fail('E_PATH','captured deployment context cannot be in disposable home');
162
130
  const {file:bindingsFile,...bindingsDoc}=captured.binding.bindings;
@@ -167,13 +135,13 @@ export function registerCaptured(home,receipt) {
167
135
  bindingFingerprint:bindingFingerprint(bindings),execution:captured.binding.execution,providerBinding:JSON.parse(JSON.stringify(receipt.binding)),
168
136
  registration:{schemaVersion:1,kind:'captured'},sourceIdentity:captured.sourceIdentity,executionBinding:captured.execution,
169
137
  responsibleHuman:captured.responsibleHuman,created:new Date().toISOString()};
170
- const file=join(dir,'source.json'),pending=registrationView(source);fs.mkdirSync(dir,{recursive:true,mode:0o700});
138
+ const file=join(dir,'source.json');fs.mkdirSync(dir,{recursive:true,mode:0o700});
171
139
  try {
172
- source.acceptedView=views(bindings,source.decl,pending);source.acceptedNodes=Object.fromEntries(Object.entries(source.acceptedView).map(([alias,row])=>[alias,row.nodes]));
140
+ source.acceptedView=acceptedResolution(bindings,source.decl);source.acceptedNodes=acceptedNodes(source.acceptedView);
173
141
  save(file,source);save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
174
142
  save(markerPath(home),{version:1,id,source:file});
175
143
  } catch(error) {
176
- if(!fs.existsSync(markerPath(home))) {fs.rmSync(pending,{recursive:true,force:true});fs.rmSync(dir,{recursive:true,force:true});}
144
+ if(!fs.existsSync(markerPath(home))) fs.rmSync(dir,{recursive:true,force:true});
177
145
  throw error;
178
146
  }
179
147
  return finishRegistration({...source,file});
@@ -188,8 +156,6 @@ export function register(home) {
188
156
  }
189
157
  if(service(home)) return {skipped:'service'};
190
158
  if(fs.existsSync(markerPath(home))) return finishRegistration(homeSource(home));
191
- safePath(join(home,'knowledge'));
192
- if(fs.existsSync(join(home,'knowledge'))) fail('E_VIEW','unregistered knowledge view exists; preserve it and inspect before registering');
193
159
  if(['.okf-harvest-record.json','.okf-harvest-record.next.json'].some(p=>fs.existsSync(join(home,p))) && !fs.existsSync(join(home,'.okf-v1-migration.json'))) fail('E_MIGRATION','legacy source watermarks require explicit oats okf migrate --source-home PATH before v2 registration; no cursor is silently trusted');
194
160
  const meta=fs.existsSync(join(home,'instance.json'))?readJSON(join(home,'instance.json')):{};
195
161
  if(!process.env.OATS_SOUL) fail('E_OATS_SOUL_MISSING','OATS_SOUL is not set; oats.okf hooks and commands run only under the OATS kernel');
@@ -213,22 +179,18 @@ export function register(home) {
213
179
  if(Buffer.byteLength(role)>128*1024) fail('E_SOURCE','role document exceeds 128KiB; provide a concise role before registering');
214
180
  const source={version:1,id,home,work,context,agent,instance,owner:decl.owner,decl,role,bindings,bindingFingerprint:bindingFingerprint(bindings),execution:{runtime:settings()['harvest-runtime']||'pi',model:settings()['harvest-model']||null},created:new Date().toISOString()};
215
181
  const file=join(dir,'source.json');
216
- const pending=registrationView(source);
217
182
  fs.mkdirSync(dir,{recursive:true,mode:0o700});
218
183
  try {
219
- source.acceptedView=views(bindings,decl,pending);
184
+ source.acceptedView=acceptedResolution(bindings,decl);
220
185
  pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
221
- source.acceptedNodes=Object.fromEntries(Object.entries(source.acceptedView).map(([alias,r])=>[alias,r.nodes]));
186
+ source.acceptedNodes=acceptedNodes(source.acceptedView);
222
187
  save(file,source);
223
188
  save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
224
189
  save(markerPath(home),{version:1,id,source:file});
225
190
  } catch(e) {
226
191
  // Until the pointer is durable no capture or schedule can reference these
227
192
  // files. Leave an installed pointer's state intact even if fsync failed.
228
- if(!fs.existsSync(markerPath(home))) {
229
- fs.rmSync(pending,{recursive:true,force:true});
230
- fs.rmSync(dir,{recursive:true,force:true});
231
- }
193
+ if(!fs.existsSync(markerPath(home))) fs.rmSync(dir,{recursive:true,force:true});
232
194
  throw e;
233
195
  }
234
196
  return finishRegistration({...source,file});
@@ -7,7 +7,7 @@ import { metadata, noGit, gitTimeoutMs } from './config.mjs';
7
7
  const validator = fileURLToPath(new URL('../skills/okf/scripts/okf-validate.mjs', import.meta.url));
8
8
  // Never let local replace refs reinterpret frozen OIDs, including inside Git's
9
9
  // transport subprocesses. Override even an explicitly supplied command env.
10
- const gitEnv = (env = cleanEnv()) => ({...env,GIT_NO_REPLACE_OBJECTS:'1'});
10
+ export const gitEnv = (env = cleanEnv()) => ({...env,GIT_NO_REPLACE_OBJECTS:'1'});
11
11
  export const git = (cwd,args,opts={}) => exec('git',['--no-replace-objects','-c','core.hooksPath=/dev/null','-c','protocol.ext.allow=never','-C',cwd,...args],{cwd,...opts,env:gitEnv(opts.env)});
12
12
  function baseError(code,message,base,alias,step,reason) {throw Object.assign(new Error(message),{code,base:alias,repository:base.repository,step,reason});}
13
13
  function baseRemedy(alias) {return `fix the binding for base alias "${alias}" in the bindings file, or remove the base from the bindings`;}
@@ -19,22 +19,25 @@ function classifyGitFailure(error) {
19
19
  if(/could not resolve host|failed to connect|connection refused|network is unreachable|no route to host|proxy/i.test(text)) return 'network';
20
20
  return 'unknown';
21
21
  }
22
- function unavailable(base,alias,step,error) {
22
+ export function unavailable(base,alias,step,error) {
23
23
  const reason=classifyGitFailure(error),detail=reason==='unknown'?`; original Git failure: ${String(error?.message || 'unknown failure')}`:'';
24
24
  const message=`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but ${step} failed (reason: ${reason}${detail}); ${baseRemedy(alias)}`;
25
25
  baseError('E_BASE_UNAVAILABLE',message,base,alias,step,reason);
26
26
  }
27
- function requireNotShallow(base,alias,cwd) {
27
+ export function requireNotShallow(base,alias,cwd) {
28
28
  const shallow=git(cwd,['rev-parse','--is-shallow-repository']);
29
29
  if(shallow==='true') baseError('E_BASE_SHALLOW',`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but the repository is shallow; oats.okf requires full accepted history before staging; ${baseRemedy(alias)}`,base,alias,'clone','shallow');
30
30
  }
31
- function preflightLocalRepository(base,alias) {
31
+ export function preflightLocalRepository(base,alias) {
32
32
  if(!base.repository.startsWith('/')) return;
33
33
  if(!fs.existsSync(base.repository)) unavailable(base,alias,'clone',Object.assign(new Error('repository not found'),{code:'ENOENT'}));
34
34
  try { requireNotShallow(base,alias,base.repository); }
35
35
  catch(e) { if(e.code==='E_BASE_SHALLOW') throw e; unavailable(base,alias,'clone',e); }
36
36
  }
37
37
  export const baseLock = b => `${b.path}.okf-lock`;
38
+ // okf consult readers hold a directory base's lock briefly; staging and
39
+ // publication queue behind them for a bounded time instead of failing.
40
+ const CONSULT_READER_WAIT_MS = 10000;
38
41
  export const journalPath = b => `${b.path}.okf-publication.json`;
39
42
  export function validateBase(root, base) {
40
43
  const files=tree(root,{git:base.kind==='git' && base.root==='.'}); const meta=metadata(files,base);
@@ -123,7 +126,7 @@ export function stageBase(base, dest, { alias = base.id } = {}) {
123
126
  if(fs.existsSync(journalPath(base))) fail('E_RECOVERY',`publication pending: ${journalPath(base)}; retry its recorded run before reading`);
124
127
  const result=validateBase(base.path,base); materialize(dest,result.files);
125
128
  return {...result,root:dest};
126
- });
129
+ },{waitMs:CONSULT_READER_WAIT_MS});
127
130
  }
128
131
  export function allowedChanges(before, after, meta, owned) {
129
132
  const changed=[...new Set([...Object.keys(before),...Object.keys(after)])].filter(p=>before[p]!==after[p]);
@@ -287,7 +290,7 @@ export function directoryPublish(base, proposal, receipt, persist, { afterWrite
287
290
  receipt.status='accepted'; receipt.acceptedDigest=final.digest; receipt.acceptedAt=new Date().toISOString(); persist();
288
291
  fs.rmSync(jp); syncParent(jp);
289
292
  return receipt;
290
- });
293
+ },{waitMs:CONSULT_READER_WAIT_MS});
291
294
  }
292
295
  function syncParent(p) { const fd=fs.openSync(dirname(p),'r'); try {fs.fsyncSync(fd);} finally {fs.closeSync(fd);} }
293
296
  function prRows(base,branch,cwd,{identity,allBases=false}={}) {
@@ -303,7 +306,7 @@ function prRows(base,branch,cwd,{identity,allBases=false}={}) {
303
306
  const raw=exec('gh',['pr','list','--repo',base.pr.repository,'--head',branch,...(allBases?[]:['--base',base.acceptedBranch]),'--state','all','--json',fields],{cwd,env:gitEnv()});
304
307
  const rows=JSON.parse(raw); if(!Array.isArray(rows)) fail('E_PR','invalid gh PR list'); return rows;
305
308
  }
306
- function verifyRemote(base,cwd) {
309
+ export function verifyRemote(base,cwd) {
307
310
  for(const mode of [[],['--push']]) {
308
311
  const urls=git(cwd,['remote','get-url',...mode,'--all','origin']).split('\n');
309
312
  if(!urls.length || urls.some(url=>url!==base.repository)) fail('E_OWNER','worker changed frozen Git publication remote or effective push destination');
@@ -108,7 +108,7 @@ function spawnWorker(source,run,{parent=false}={}) {
108
108
  persist(source,run);throw error;
109
109
  }
110
110
  }
111
- const args=['spawn','memory-harvest','--purpose',`okf-${id}`,'--work','directory','--repo',source.context,'--dir',source.context,'--runtime',source.execution.runtime,'--no-launch','--task-file',taskFile,'--json'];
111
+ const args=['spawn','memory-harvest','--purpose',`okf-${id}`,'--work','directory','--repo',source.context,'--dir',source.context,harnessFlag(source),source.execution.runtime,'--no-launch','--task-file',taskFile,'--json'];
112
112
  if(!['pi','claude','codex'].includes(source.execution.runtime)) fail('E_CONFIG','invalid harvest runtime');
113
113
  if(source.execution.model) args.push('--model',source.execution.model);
114
114
  if(parent) args.push('--parent',source.instance);
@@ -123,6 +123,14 @@ function spawnWorker(source,run,{parent=false}={}) {
123
123
  finally {fs.rmSync(taskFile,{force:true});}
124
124
  }
125
125
 
126
+ /** OATS 0.27 names the spawn harness --harness (--runtime is its deprecated
127
+ * alias). Ask the kernel; an older kernel, or one that cannot answer, gets
128
+ * --runtime, which every supported kernel accepts. */
129
+ export function harnessFlag(source) {
130
+ let version;
131
+ try {version=oats(['version','--json'],source.context,{native:true,timeout:15000});} catch {return '--runtime';}
132
+ return Array.isArray(version?.features) && version.features.includes('harness')?'--harness':'--runtime';
133
+ }
126
134
  function workerHome(run,source) {
127
135
  const home=safePath(run.worker.home);const meta=readJSON(join(home,'instance.json'));
128
136
  if(meta.instance!==run.worker.instance || meta.agent!=='memory-harvest' || meta.work!=='directory') fail('E_WORKER','worker receipt does not identify a directory-mode harvester');
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "capability": "oats.okf",
3
3
  "command": "okf",
4
- "version": "2.1.5",
4
+ "version": "3.0.0",
5
5
  "compatibility": {
6
- "oats": ">=0.24.4"
6
+ "oats": ">=0.26.0"
7
7
  },
8
8
  "layer": "knowledge",
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.",
9
+ "description": "External OKF bases with owned nodes, consulted remotely at their accepted state (no per-instance copy), durable per-source evidence, independent workers, verified Git PR and recoverable non-Git directory delivery.",
10
10
  "requires": [],
11
11
  "settings": {
12
12
  "harvest-runtime": {
@@ -29,6 +29,9 @@
29
29
  },
30
30
  "git-timeout": {
31
31
  "description": "Seconds allowed for each Git operation that talks to a remote (clone, fetch, push, ls-remote); default 600. Local object reads keep a short fixed limit. Raise it for large repositories behind slow links."
32
+ },
33
+ "consult-max-age": {
34
+ "description": "Seconds a Git base's host-cached accepted commit may age before an `oats okf` consult read refetches the accepted branch; default 300, 0 fetches on every read. `--fresh` always refetches."
32
35
  }
33
36
  },
34
37
  "agents": [
@@ -44,6 +47,12 @@
44
47
  "run-source": "bin/oats-okf.mjs run-source",
45
48
  "complete": "bin/oats-okf.mjs complete",
46
49
  "retry": "bin/oats-okf.mjs retry",
50
+ "bases": "bin/oats-okf.mjs bases",
51
+ "index": "bin/oats-okf.mjs index",
52
+ "cat": "bin/oats-okf.mjs cat",
53
+ "ls": "bin/oats-okf.mjs ls",
54
+ "links": "bin/oats-okf.mjs links",
55
+ "search": "bin/oats-okf.mjs search",
47
56
  "read": "bin/oats-okf.mjs read",
48
57
  "refresh": "bin/oats-okf.mjs refresh",
49
58
  "init": "bin/oats-okf.mjs init",
@@ -107,7 +116,7 @@
107
116
  "kind": "view",
108
117
  "command": "inspect",
109
118
  "context": "home",
110
- "description": "Inspect matching live working-memory documents, durable receipts, bindings, registered view freshness and scheduler health."
119
+ "description": "Inspect matching live working-memory documents, durable receipts, bindings, the accepted resolution, any legacy local view and scheduler health."
111
120
  },
112
121
  "harvest": {
113
122
  "kind": "action",
@@ -4,11 +4,12 @@ description: >-
4
4
  Open Knowledge Format (OKF) craft: how to author, maintain, consume, and
5
5
  validate OKF knowledge bundles (directories of markdown concepts with YAML
6
6
  frontmatter, per Google Cloud's OKF v0.1 spec). Use when writing or editing
7
- concepts in a knowledge/ bundle or notes/, adding or renaming concept files,
7
+ concepts in a knowledge bundle or notes/, adding or renaming concept files,
8
8
  updating index.md or log.md, answering questions from a bundle, triaging a
9
- knowledge inbox, or when asked to validate a bundle. This skill is HOW to do
10
- OKF well; instance session protocol lives in the okf AGENTS.md injection,
11
- and promotion judgment in the memory-harvest skill.
9
+ knowledge inbox, or when asked to validate a bundle. To consult your own
10
+ soul's knowledge with `oats okf`, load the okf-consultation skill instead.
11
+ Instance session protocol lives in the okf AGENTS.md injection, and
12
+ promotion judgment in the memory-harvest skill.
12
13
  ---
13
14
 
14
15
  # OKF craft — author, maintain, consume
@@ -20,6 +21,13 @@ An external base is one bundle and link namespace. Owned nodes are
20
21
  nonoverlapping subdirectories, not separate root-link namespaces. Instance
21
22
  `notes/` files are task-local concepts; no knowledge lives in the soul.
22
23
 
24
+ ## Consulting your knowledge
25
+
26
+ Your soul's knowledge is read remotely with `oats okf` (`index`, `cat`, `ls`,
27
+ `links`, `search`, `bases`): there is no local copy. The **okf-consultation**
28
+ skill teaches the procedure; load it at the start of every task and whenever
29
+ you look something up. This skill is about the format itself.
30
+
23
31
  ## The format in one screen
24
32
 
25
33
  - **Concept = one file.** Concept ID = path minus `.md`. Small and specific
@@ -144,8 +152,8 @@ Those private mode-0600 files exist only for one synchronous captured invocation
144
152
 
145
153
  ## External bases and native tools
146
154
 
147
- Ordinary working agents consult `oats okf read --base ALIAS --path node/index.md`
148
- and `oats okf refresh`. When inspecting a source, treat the additive `authority`
155
+ Ordinary working agents consult with `oats okf index` / `cat` / `search`
156
+ (the okf-consultation skill). When inspecting a source, treat the additive `authority`
149
157
  object literally: `captured` includes recorded qualified identity/execution;
150
158
  `legacy` or `invalid` means migration/evidence is still required. A
151
159
  responsible-human status of `disabled` comes only from explicit null; `unknown`
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: okf-consultation
3
+ description: >-
4
+ Consulting your soul's knowledge with the `oats okf` CLI: reading its
5
+ external OKF bases remotely at the accepted commit (index, cat, ls, links,
6
+ search, bases) with no local copy, then citing what you relied on. Use when
7
+ starting a task or resuming after compaction, when looking up a prior
8
+ decision, lesson or concept, when asked "what do we know about X" or to
9
+ "check the knowledge base", before re-deriving a design decision, when a
10
+ question touches your domain, or when an `oats okf` receipt says STALE or a
11
+ command errors. Authoring and validating OKF bundles is the okf skill.
12
+ ---
13
+
14
+ # Consulting your knowledge with `oats okf`
15
+
16
+ Your soul's accumulated judgment — decisions and their rationale, lessons,
17
+ rejected alternatives, limits — lives outside the soul, in external OKF
18
+ **bases**. You read it remotely; nothing is copied into your home. Consulting
19
+ it before you act is how you avoid re-deciding what was already decided.
20
+
21
+ ## The model
22
+
23
+ - **Base / alias.** Each bound base has an alias (`oats okf bases` lists
24
+ them). A base is one OKF bundle and one link namespace.
25
+ - **Node.** A subdirectory of a base with its own `index.md`. Your soul
26
+ **owns** some nodes (you are responsible for them) and **reads** others
27
+ (starting context). Neither is an access list: every bound base is readable.
28
+ - **Accepted commit.** `oats okf` serves only a base's accepted state: the
29
+ accepted branch of a Git base, the in-place files of a directory base. An
30
+ open PR is not accepted knowledge until it is merged.
31
+ - **Host cache, no local copy.** A Git base is read from one host-wide cache
32
+ that fetches file contents on first read. Your home has no `./knowledge/`
33
+ and no view directory. A `./knowledge/` left by okf 2.x is stale; ignore it.
34
+ - **Receipts.** Every answer ends with `— alias@<short-oid> (fetched <age>)`.
35
+ With `--json`, the result carries `receipt: {base, kind, commit|digest,
36
+ fetchedAt, stale}`.
37
+
38
+ Run every command from your instance home.
39
+
40
+ ## At the start of every task, and after compaction
41
+
42
+ 1. `oats okf index` prints your owned then read nodes' indexes, each headed
43
+ `## alias/node (owns|reads)`.
44
+ 2. Choose the entries relevant to *this* task by their one-line descriptions.
45
+ 3. `oats okf cat --base ALIAS /node/path.md` for each of those, and follow
46
+ their links only as far as the task needs (see Navigating).
47
+ 4. Write down in STATE.md which concepts shaped your plan, with their
48
+ citations.
49
+
50
+ Do not skip this because the task looks small: prior decisions are cheapest
51
+ to find before you have written the code that contradicts them.
52
+
53
+ ## Consult again while working
54
+
55
+ The start-of-task read is not enough. Consult again:
56
+
57
+ - **Before a design decision.** Search for the decision's subject, and read
58
+ any prior decision on it before choosing.
59
+ - **Before re-deriving something.** If you are about to work out why
60
+ something is the way it is, search first; the rationale may be recorded.
61
+ - **When a question touches your domain**, including a teammate's question.
62
+ - **Before saying "we decided…" or "we tried…"**: cite it, or don't claim it.
63
+ - **When something surprises you.** A lesson may already explain it.
64
+
65
+ ## Commands
66
+
67
+ | Command | What it answers |
68
+ | --- | --- |
69
+ | `oats okf bases [--fresh]` | aliases, accepted commit, freshness, validity, your owns/reads |
70
+ | `oats okf index [--base A] [NODE \| A/NODE]` | one node's `index.md`; no args: all your nodes; `--base A` alone: the base root index |
71
+ | `oats okf cat --base A PATH [--from P]` | one Markdown file, in full |
72
+ | `oats okf ls --base A [DIR]` | directory entries, each concept's `type` / `title` / `description` |
73
+ | `oats okf links --base A PATH` | the file's outgoing links, resolved, `ok` / `MISSING` / `REFUSED` / `external` |
74
+ | `oats okf search [--base A \| --all] [--node N] [--regex] [--case-sensitive] TEXT` | matching lines `{base, path, line, snippet}` |
75
+
76
+ All take `--json`. `read --base A --path P` is the okf 2.x spelling of `cat`
77
+ and still works.
78
+
79
+ ## Navigating
80
+
81
+ Paths resolve like OKF links, **from the base root, not the filesystem**:
82
+
83
+ - `/node/decisions/x.md` is base-root absolute (the usual form in indexes'
84
+ absolute links and in citations);
85
+ - a relative link (`../lessons/y.md`, `decisions/x.md`) is relative to the file
86
+ it appeared in: pass that file as `--from`;
87
+ - a bare `node/x.md` without `--from` resolves from the root.
88
+
89
+ Worked example (a lesson linked from a decision):
90
+
91
+ ```sh
92
+ oats okf index expert
93
+ oats okf cat --base project /expert/decisions/retry-policy.md
94
+ oats okf links --base project /expert/decisions/retry-policy.md
95
+ oats okf cat --base project ../lessons/storm.md --from /expert/decisions/retry-policy.md
96
+ ```
97
+
98
+ `links` shows every target already resolved, so you can `cat` the resolved
99
+ path directly. `ls` is for choosing what to read from its descriptions, not
100
+ for reading everything.
101
+
102
+ ## Searching
103
+
104
+ `oats okf search TEXT` searches your nodes' bases (fixed string,
105
+ case-insensitive, Markdown only), capped at 50 hits with "and N more". Narrow
106
+ with `--node`, a longer phrase or `--base`; widen with `--all`. Search finds
107
+ words, not meaning: when it finds nothing, try a synonym, then navigate from
108
+ the index. A hit is a pointer: `cat` the concept before relying on it.
109
+
110
+ ## Citing
111
+
112
+ Cite what you relied on as `alias/node/concept.md@<short-oid>`, using the oid
113
+ from the receipt line (a directory base cites `alias@dir:<digest>`). Cite in
114
+ STATE.md, notes/, PR descriptions and answers, so a reader knows which
115
+ accepted state you saw.
116
+
117
+ ## Freshness
118
+
119
+ Reads use the cached accepted commit while it is younger than the
120
+ `consult-max-age` setting (default 300 s); after that the accepted branch is
121
+ refetched first. `--fresh` refetches now (for example right after a PR
122
+ merged). If a fetch fails, the last fetched accepted commit is still served,
123
+ marked `stale: true` / `STALE:` with the reason: say so when it matters. With
124
+ nothing cached and no network, the command fails with `E_BASE_UNAVAILABLE`:
125
+ report it, and do not answer from memory or from an old `./knowledge/`.
126
+
127
+ ## Gotchas
128
+
129
+ - Links resolve from the base root. `/node/x.md` is never a filesystem path,
130
+ and filesystem paths, `..` escapes, URLs, hidden paths and symlinks are
131
+ refused (`E_PATH`).
132
+ - Never bulk-`cat` a whole node or loop `cat` over `ls` output. Index first,
133
+ then follow the few relevant links.
134
+ - Don't edit knowledge. The write path is notes/ → harvest → PR or
135
+ publication. Write insights to notes/, not into a base.
136
+ - `oats okf refresh` is gone (`E_REMOVED`): every read already sees the
137
+ accepted state.
138
+ - `cat` reads `.md` files only (`E_NOT_MARKDOWN` otherwise); `E_NOT_FOUND`
139
+ lists the nearest directory's entries to try instead.
140
+
141
+ Read [references/consult.md](references/consult.md) for more search examples,
142
+ the error table and freshness edge cases.
@@ -0,0 +1,86 @@
1
+ # okf-consultation — reference detail
2
+
3
+ The okf-consultation skill's detail. Read this when navigating links, when a search is noisy or empty, when a
4
+ receipt says `STALE`, or when a command errors. Run every command from your
5
+ instance home (`oats okf …` resolves your registered source there).
6
+
7
+ ## Navigation, worked example
8
+
9
+ Task: "change how retries back off". Your soul owns `project/expert`.
10
+
11
+ ```sh
12
+ oats okf index # ## project/expert (owns) … ## project/peer (reads)
13
+ # * [Retry policy](decisions/retry-policy.md) - Why retries back off exponentially.
14
+ oats okf cat --base project /expert/decisions/retry-policy.md
15
+ # … "superseded the fixed delay; see [the incident](../lessons/fixed-delay-storm.md)"
16
+ oats okf links --base project /expert/decisions/retry-policy.md
17
+ # ok /expert/lessons/fixed-delay-storm.md (../lessons/fixed-delay-storm.md)
18
+ # MISSING /expert/decisions/jitter.md
19
+ oats okf cat --base project ../lessons/fixed-delay-storm.md \
20
+ --from /expert/decisions/retry-policy.md
21
+ ```
22
+
23
+ - `index` with a node name (`oats okf index expert` or `project/expert`) prints
24
+ one index; `--base A` alone prints the base's root `index.md`.
25
+ - A link in a concept is relative to **that concept's directory**: pass the
26
+ concept as `--from`, or use `links` to see every target already resolved.
27
+ - `/node/x.md` is always from the base root; a bare `node/x.md` without
28
+ `--from` is also from the root.
29
+ - `ls --base A /node/decisions` shows each concept's `type`, `title` and
30
+ `description` — use it to pick what to `cat`, not to read everything.
31
+ - `MISSING` in `links` is a dangling link (allowed by OKF; not an error).
32
+ `REFUSED` is a link that would leave the base or is malformed.
33
+
34
+ ## Search
35
+
36
+ ```sh
37
+ oats okf search "backoff" # your nodes' bases, case-insensitive
38
+ oats okf search --node expert "jitter" # one of your nodes
39
+ oats okf search --base project --node peer "rate limit"
40
+ oats okf search --all "idempotency" # every bound base
41
+ oats okf search --regex "retr(y|ies)" # opt-in regex
42
+ ```
43
+
44
+ - Fixed string, case-insensitive by default (`--case-sensitive` to change);
45
+ `--regex` is extended regex in Git bases, JavaScript RegExp in directory ones.
46
+ - Only `.md` files inside the base root; results are `{base, path, line,
47
+ snippet}`, capped at 50 with "and N more" — narrow with `--node` or a longer
48
+ phrase rather than paging.
49
+ - Search finds words, not meaning: when it's empty, try a synonym, then
50
+ navigate from the index. A hit is a pointer; `cat` the concept before
51
+ relying on it.
52
+ - Git bases search with `git grep` at the accepted commit (the first search of
53
+ a node fetches its blobs in one batch); directory bases walk the files in
54
+ place, bounded to 20000 files.
55
+
56
+ ## Citing
57
+
58
+ Cite what you relied on as `alias/node/concept.md@<short-oid>`, the oid from
59
+ the receipt line (`— project@1a2b3c4d5e6f (fetched 2m ago)`). A directory base
60
+ cites its digest (`project@dir:9f8e…`). Cite in notes/, PR descriptions and
61
+ answers so a reader can see which accepted state you saw.
62
+
63
+ ## Freshness
64
+
65
+ - Reads use the host cache's accepted commit if it was fetched within
66
+ `consult-max-age` seconds (setting, default 300); older, the accepted branch
67
+ is fetched first. `--fresh` fetches now. Neither ever selects another
68
+ branch or commit: only the accepted one is served.
69
+ - Fetch failed (offline, auth, timeout): the last fetched accepted commit is
70
+ served with `stale: true` and the reason in the receipt (`STALE:` in text).
71
+ Say so if it matters to your answer; retry later with `--fresh`.
72
+ - No cached commit and no network → `E_BASE_UNAVAILABLE`: report it, don't
73
+ guess from memory or from an old `./knowledge/`.
74
+ - Merged a PR a minute ago? `--fresh` sees it once it is on the accepted branch.
75
+
76
+ ## Errors
77
+
78
+ | Code | Meaning / what to do |
79
+ | --- | --- |
80
+ | `E_BASE_UNKNOWN` | alias not bound; the message lists the bound aliases (`oats okf bases`) |
81
+ | `E_NOT_FOUND` | no such file at the accepted commit; the message lists the nearest directory |
82
+ | `E_NOT_MARKDOWN` | `cat` reads `.md` concepts only; use `ls` for a directory |
83
+ | `E_PATH` | escapes the base root, filesystem path, URL, hidden path, symlink or submodule |
84
+ | `E_RECOVERY` | a directory-base publication is pending; retry after it completes |
85
+ | `E_BASE_UNAVAILABLE` | the base can't be read and nothing is cached; report it |
86
+ | `E_REMOVED` | `refresh` no longer exists; use `index` / `cat` |
@@ -35,6 +35,17 @@ pushes. Agreed by both on 2026-09-24:
35
35
  published skills); merges into the other's lane; reverts, force-anything,
36
36
  branch or tag deletion. A blocking intent unanswered for about 45 minutes goes
37
37
  to the human, never to action.
38
+ - **Amendment (the human, 2026-09-26 ~14:00Z): "do the merge then fix if something broke".**
39
+ - A PR the lead has reviewed and approved is **merged immediately** by the lead (`gh pr merge --squash --match-head-commit <approved oid>`), without waiting for PR CI. CI runs on main after the merge.
40
+ - A red main is fixed forward at once, by the author or the lead, before anything else merges.
41
+ - **Tags still wait for main CI green on their exact SHA** (a tag never moves).
42
+ - Developers run only the affected suites locally (the nested globs included). The full glob and `smoke:tarball` run in CI, sharded ×6.
43
+ - The watcher no longer relays merges.
44
+ - **Amendment (the human, 2026-09-26 ~13:10Z): "lets approve and tag ourselves all of these PRs".**
45
+ - With the co-lead unresponsive since ~10:30Z, the lead's review + green CI is the full Class B gate, in BOTH lanes: merges, okf/aweb tags, catalog pins, releases.
46
+ - Every such act is logged, with its head/tag oid, in a running account mailed to the co-lead for after-the-fact review.
47
+ - The co-lead's objections then go to the human, and a revert follows only on the human's word.
48
+ - This ends when the co-lead resumes and the lead records that here.
38
49
  - Every PR is reviewed by the co-lead who did not author it (a helper's PR is
39
50
  reviewed by its own lead, inside that lead's lane). A disagreement not settled
40
51
  in two mails goes to the human. Standing rules unchanged: PR CI is the full