@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.
- package/bin/oats.mjs +185 -26
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +8 -3
- package/capabilities/oats-okf/bin/oats-okf.mjs +33 -28
- package/capabilities/oats-okf/injects/okf.md +29 -21
- package/capabilities/oats-okf/lib/config.mjs +5 -1
- package/capabilities/oats-okf/lib/consult.mjs +500 -0
- package/capabilities/oats-okf/lib/inspection.mjs +11 -3
- package/capabilities/oats-okf/lib/io.mjs +9 -2
- package/capabilities/oats-okf/lib/sources.mjs +15 -53
- package/capabilities/oats-okf/lib/stores.mjs +10 -7
- package/capabilities/oats-okf/lib/worker.mjs +9 -1
- package/capabilities/oats-okf/oats.json +13 -4
- package/capabilities/oats-okf/skills/okf/SKILL.md +14 -6
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +142 -0
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
- package/docs/design/2026-09-24-phase-d-plan.md +11 -0
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +241 -0
- package/docs/design/2026-09-26-okf-knowledge-operations.md +389 -0
- package/docs/desktop-cli-api.md +93 -7
- package/docs/oats-local.schema.json +2 -1
- package/docs/oats-package.schema.json +39 -0
- package/docs/packages.md +67 -3
- package/docs/release-notes/v0.28.0.md +144 -0
- package/docs/schedules.md +99 -1
- package/docs/souls-and-instances.md +7 -3
- package/docs/workspaces.md +8 -2
- package/lib/core.mjs +22 -4
- package/lib/instance-inspect.mjs +4 -4
- package/lib/instance-resolution.mjs +62 -18
- package/lib/materialize.mjs +13 -0
- package/lib/packages.mjs +90 -6
- package/lib/resolve.mjs +20 -2
- package/lib/schedule.mjs +24 -11
- package/lib/triggers.mjs +545 -0
- package/lib/workspace.mjs +80 -3
- package/package-catalog.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
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,
|
|
3
|
-
import { loadBindings, declaration,
|
|
4
|
-
import {
|
|
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
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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')
|
|
138
|
+
const file=join(dir,'source.json');fs.mkdirSync(dir,{recursive:true,mode:0o700});
|
|
171
139
|
try {
|
|
172
|
-
source.acceptedView=
|
|
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)))
|
|
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=
|
|
184
|
+
source.acceptedView=acceptedResolution(bindings,decl);
|
|
220
185
|
pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
|
|
221
|
-
source.acceptedNodes=
|
|
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,
|
|
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": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"compatibility": {
|
|
6
|
-
"oats": ">=0.
|
|
6
|
+
"oats": ">=0.26.0"
|
|
7
7
|
},
|
|
8
8
|
"layer": "knowledge",
|
|
9
|
-
"description": "External OKF bases with owned nodes,
|
|
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,
|
|
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
|
|
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.
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
148
|
-
|
|
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
|