@awebai/oats 0.27.2 → 0.29.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 (75) hide show
  1. package/bin/oats.mjs +445 -96
  2. package/capabilities/oats-okf/bin/oats-okf.mjs +55 -30
  3. package/capabilities/oats-okf/injects/okf.md +36 -28
  4. package/capabilities/oats-okf/lib/binding-wire.mjs +4 -1
  5. package/capabilities/oats-okf/lib/config.mjs +6 -1
  6. package/capabilities/oats-okf/lib/consult.mjs +496 -0
  7. package/capabilities/oats-okf/lib/harvest-status.mjs +88 -0
  8. package/capabilities/oats-okf/lib/harvest-switch.mjs +81 -0
  9. package/capabilities/oats-okf/lib/inspection.mjs +11 -3
  10. package/capabilities/oats-okf/lib/io.mjs +9 -2
  11. package/capabilities/oats-okf/lib/okf-validate.mjs +123 -0
  12. package/capabilities/oats-okf/lib/sources.mjs +42 -55
  13. package/capabilities/oats-okf/lib/stores.mjs +19 -11
  14. package/capabilities/oats-okf/lib/worker.mjs +90 -8
  15. package/capabilities/oats-okf/oats.json +24 -9
  16. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +144 -0
  17. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
  18. package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +104 -0
  19. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +140 -0
  20. package/capabilities/oats-okf-harvest/injects/harvester.md +12 -0
  21. package/capabilities/oats-okf-harvest/oats.json +26 -0
  22. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +168 -0
  23. package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +192 -0
  24. package/capabilities/{oats-okf/skills/okf → oats-okf-harvest/skills/okf-authoring}/SKILL.md +15 -22
  25. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +149 -0
  26. package/capabilities/oats-okf-maintenance/injects/maintainer.md +12 -0
  27. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +45 -0
  28. package/capabilities/oats-okf-maintenance/oats.json +21 -0
  29. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +144 -0
  30. package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +192 -0
  31. package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +151 -0
  32. package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +123 -0
  33. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +146 -0
  34. package/capabilities/oats-review/injects/review.md +3 -2
  35. package/capabilities/oats-review/oats.json +3 -4
  36. package/docs/capabilities.md +41 -9
  37. package/docs/capability-manifest.schema.json +0 -7
  38. package/docs/design/2026-09-24-phase-d-plan.md +11 -0
  39. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +241 -0
  40. package/docs/design/2026-09-26-okf-knowledge-operations.md +389 -0
  41. package/docs/desktop-cli-api.md +342 -10
  42. package/docs/implementation.md +1 -1
  43. package/docs/knowledge-capability-authoring.md +8 -2
  44. package/docs/knowledge-reference/package-craft.md +8 -5
  45. package/docs/knowledge.md +101 -0
  46. package/docs/oats-local.schema.json +33 -2
  47. package/docs/oats-package.schema.json +39 -0
  48. package/docs/official-catalog.md +7 -4
  49. package/docs/packages.md +76 -6
  50. package/docs/release-lane.md +1 -1
  51. package/docs/release-notes/v0.28.0.md +144 -0
  52. package/docs/release-notes/v0.29.0.md +240 -0
  53. package/docs/schedules.md +230 -4
  54. package/docs/souls-and-instances.md +11 -9
  55. package/docs/workspaces.md +18 -3
  56. package/lib/automations.mjs +369 -0
  57. package/lib/core.mjs +87 -158
  58. package/lib/instance-inspect.mjs +16 -8
  59. package/lib/instance-resolution.mjs +90 -197
  60. package/lib/materialize.mjs +18 -7
  61. package/lib/operator-dispatch.mjs +1 -2
  62. package/lib/packages.mjs +107 -6
  63. package/lib/remote.mjs +21 -1
  64. package/lib/resolve.mjs +71 -9
  65. package/lib/schedule.mjs +228 -45
  66. package/lib/triggers.mjs +678 -0
  67. package/lib/workspace.mjs +81 -4
  68. package/package-catalog.json +6 -4
  69. package/package.json +1 -1
  70. package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +0 -21
  71. package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +0 -5
  72. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +0 -285
  73. package/capabilities/oats-review/agents/reviewer/AGENTS.md +0 -53
  74. package/capabilities/oats-review/agents/reviewer/soul.yaml +0 -6
  75. /package/capabilities/{oats-okf/skills/okf → oats-okf-harvest/skills/okf-authoring}/scripts/okf-validate.mjs +0 -0
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { randomUUID } from 'node:crypto';
3
- import { fs, join, dirname, resolve, readJSON, save, safePath, cliPath, oats, fail, unlock } from '../lib/io.mjs';
4
- import { loadBindings, declaration, splitRef } from '../lib/config.mjs';
5
- import { register, registerCaptured, loadInvocationSourceReceipt, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, settleRetiredSchedule, service, markerPath, views } from '../lib/sources.mjs';
2
+ import { fs, join, resolve, readJSON, safePath, oats, fail, unlock } from '../lib/io.mjs';
3
+ import { loadBindings } from '../lib/config.mjs';
4
+ import { register, registerCaptured, loadInvocationSourceReceipt, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, settleRetiredSchedule, service, markerPath, harvestOffRecord } from '../lib/sources.mjs';
5
+ import { harvestStatus, setupHarvest } from '../lib/harvest-status.mjs';
6
+ import { settings } from '../lib/config.mjs';
7
+ import { CONSULT } from '../lib/consult.mjs';
6
8
  import { runSource, complete, retry, readRun, requireQualifiedHelper } from '../lib/worker.mjs';
7
9
  import { initBase, migrate, deliverMigration, cutoverMigration, migrateSource, forgetMigration } from '../lib/migration.mjs';
8
10
  import { inspect } from '../lib/inspection.mjs';
@@ -13,9 +15,18 @@ oats okf harvest [--home PATH] [--no-launch] [--json]
13
15
  oats okf run-source --source FILE [--manual] [--no-launch] [--json]
14
16
  oats okf complete --source FILE --run ID --judgment FILE [--json]
15
17
  oats okf retry --source FILE [--run ID --rejudge | --rejudge | --launch | --adopt-home PATH] [--json]
16
- oats okf read [--home PATH | --source FILE] --base ALIAS [--path node/index.md] [--json]
17
- oats okf refresh [--home PATH | --source FILE] [--json]
18
+ oats okf bases [--fresh] [--json]
19
+ oats okf index [--base ALIAS] [NODE | ALIAS/NODE] [--fresh] [--json]
20
+ oats okf cat --base ALIAS PATH [--from PATH] [--fresh] [--json]
21
+ oats okf ls --base ALIAS [DIR] [--fresh] [--json]
22
+ oats okf links --base ALIAS PATH [--fresh] [--json]
23
+ oats okf search [--base ALIAS | --all] [--node NODE] [--regex] [--case-sensitive] TEXT [--fresh] [--json]
24
+ Consult commands read the accepted state remotely (host cache, no local copy);
25
+ also accept --home PATH | --source FILE. PATH is /node/x.md from the base root,
26
+ relative to --from's directory, or bare node/x.md from the root.
18
27
  oats okf setup --source FILE [--enable | --disable] [--install-host] [--json]
28
+ oats okf setup --harvest on|off [--json] (writes oats-local.yaml settings.oats.okf.harvest)
29
+ oats okf harvest-status [--soul NAME] [--json] (the effective harvest switch, why, and the registered sources)
19
30
  oats okf init --base ALIAS --nodes FILE [--output PATH | --confirm] [--json]
20
31
  oats okf migrate --legacy PATH --base ALIAS --node NODE --output PATH [--json]
21
32
  oats okf migrate --deliver FILE | --cutover FILE --soul-dir PATH [--json]
@@ -36,11 +47,13 @@ if(args.includes('--help') || args.includes('-h')) {process.stdout.write(HELP);}
36
47
  else {
37
48
  const event=process.env.OATS_EVENT || args[0];
38
49
  const hook=['spawn','retire','soul-scaffold'].includes(event);
39
- let exit=0,answer;
50
+ const consult=Object.hasOwn(CONSULT,event);
51
+ let exit=0,answer,text,textMode=consult && !args.includes('--json');
40
52
  try {
41
- const flags={}; const boolean=new Set(['json','no-launch','manual','rejudge','launch','enable','disable','install-host','confirm']);
53
+ const flags={},positionals=[]; const boolean=new Set(['json','no-launch','manual','rejudge','launch','enable','disable','install-host','confirm','fresh','all','regex','case-sensitive']);
42
54
  for(let i=1;i<args.length;i++) {
43
- if(!args[i].startsWith('--')) fail('E_USAGE',`unexpected argument ${args[i]}`);
55
+ if(consult && args[i]==='--') {positionals.push(...args.slice(i+1));break;}
56
+ if(!args[i].startsWith('--')) {if(!consult) fail('E_USAGE',`unexpected argument ${args[i]}`);positionals.push(args[i]);continue;}
44
57
  const k=args[i].slice(2);if(k in flags) fail('E_USAGE',`duplicate --${k}`);
45
58
  if(boolean.has(k)) flags[k]=true;
46
59
  else {if(!args[i+1] || args[i+1].startsWith('--')) fail('E_USAGE',`--${k} needs a value`);flags[k]=args[++i];}
@@ -49,8 +62,10 @@ else {
49
62
  spawn:[],retire:['home'], 'soul-scaffold':[],
50
63
  harvest:['home','no-launch','native-request','worker-mode'],inspect:['home','source'],
51
64
  'run-source':['source','manual','no-launch'],complete:['source','run','judgment'],
52
- retry:['source','run','rejudge','launch','adopt-home'],read:['home','source','base','path'],refresh:['home','source'],
53
- setup:['source','enable','disable','install-host'],init:['base','nodes','output','confirm'],
65
+ retry:['source','run','rejudge','launch','adopt-home'],read:['home','source','base','path','fresh'],refresh:['home','source'],'harvest-status':['home'],
66
+ bases:['home','source','fresh'],index:['home','source','base','fresh'],cat:['home','source','base','from','fresh'],ls:['home','source','base','fresh'],
67
+ links:['home','source','base','fresh'],search:['home','source','base','all','node','regex','case-sensitive','fresh'],
68
+ setup:['source','enable','disable','install-host','harvest'],init:['base','nodes','output','confirm'],
54
69
  migrate:['source-home','legacy','base','node','output','deliver','cutover','soul-dir'],unlock:['lock','token']
55
70
  };
56
71
  for(const k of Object.keys(flags)) if(!['json','soul',...(accepted[event] || [])].includes(k)) fail('E_USAGE',`unknown flag --${k} for ${event}`);
@@ -107,15 +122,22 @@ else {
107
122
  readRun(s,id);return s;
108
123
  };
109
124
  let result;
110
- if(event==='soul-scaffold') {
125
+ if(event==='read') fail('E_REMOVED','okf 4.0.0 removed read: use `oats okf cat --base ALIAS PATH` (same path, text and receipt)');
126
+ if(consult) {const answer=CONSULT[event](src(),flags,positionals);result=answer.result;text=answer.text;}
127
+ else if(event==='refresh') fail('E_REMOVED','okf 3.0.0 has no per-instance views; index/cat always read the accepted state: run `oats okf index`, then `oats okf cat --base ALIAS PATH`');
128
+ else if(event==='harvest-status') result=harvestStatus({home,flags});
129
+ else if(event==='soul-scaffold') {
111
130
  // Souls are portable declarations, never an implicit knowledge store.
112
131
  result={meta:{scaffolded:false},brief:'OKF requires explicit external bindings and soul/okf.json before a working instance can spawn. Use init or migrate; no knowledge was created in this soul.'};
113
132
  } else if(event==='spawn') {
114
133
  const s=sourceReceipt.mode==='captured'?registerCaptured(home,sourceReceipt.receipt):register(home);
134
+ const nodes=(list)=>list.join(', ') || 'none';
135
+ const brief=decl=>`Your soul knowledge is read remotely at its accepted state; there is no local copy. Start every task with your instance knowledge (STATE.md, log.md, notes/), then \`oats okf index\` (owns: ${nodes(decl.owns)}; reads: ${nodes(decl.reads.filter(r=>!decl.owns.includes(r)))}) and \`oats okf cat --base ALIAS PATH\` for the concepts the task needs; \`oats okf search\` before re-deriving a decision. Load okf-consultation and okf-instance-knowledge. Never edit accepted knowledge.`;
115
136
  if(s.skipped) result={meta:{memory:'none'},brief:'Service agent: follow your own task; no working-memory upkeep.'};
137
+ else if(s.harvestOff) result={meta:{memory:'okf-v2',harvest:'off',reason:s.switch.reason},brief:`${brief(s.decl)} Harvest is off for this instance: nothing of this session is captured.`,...(s.switch.warnings.length?{warning:`oats-okf: ${s.switch.warnings.join('; ')}`}:{})};
116
138
  else {
117
139
  const schedule=loadStatus(s).schedule.result;
118
- result={meta:{memory:'okf-v2',source:s.file,schedule},brief:`Knowledge is an immutable accepted snapshot at ./knowledge/. Read knowledge/view.json for base paths under knowledge/bases/<alias>/, then the indexes for ${[...new Set([...s.decl.owns,...s.decl.reads])].join(', ')}. Follow only relevant links. All configured bases are available. Use oats okf read for current accepted text; old views stay stable. Keep STATE.md/log.md/notes/ current; never edit knowledge.`};
140
+ result={meta:{memory:'okf-v2',harvest:'on',source:s.file,schedule},brief:`${brief(s.decl)} Harvest is on: your notes and session are captured for the knowledge harvester.`};
119
141
  }
120
142
  } else if(event==='retire') {
121
143
  if(captured) {
@@ -124,6 +146,7 @@ else {
124
146
  else {if(!fs.existsSync(markerPath(home))) fail('E_MIGRATION','captured retire requires a durable registered source or explicit helper receipt');s=src();}
125
147
  if(s) {scheduleSource(s);const r=capture(s,{final:true});const schedule=settleRetiredSchedule(s);result={meta:{retired:r.complete===true,source:s.file,capture:r,schedule},brief:'Final input is in durable custody. Delivery remains asynchronous.'};}
126
148
  } else if(service(home)) result={meta:{retired:true}};
149
+ else if(!fs.existsSync(markerPath(home)) && harvestOffRecord(home)) result={meta:{retired:true,reason:'harvest-off'}};
127
150
  else if(!fs.existsSync(markerPath(home))) {
128
151
  if(['STATE.md','log.md','notes','.okf-harvest-record.json','.okf-harvest-record.next.json'].some(p=>fs.existsSync(join(home,p)))) fail('E_MIGRATION','unregistered/legacy source has memory; explicitly migrate/register before retirement');
129
152
  result={meta:{retired:true,reason:'nothing-to-delete'}};
@@ -139,32 +162,30 @@ else {
139
162
  // Snapshot absence does not turn a persisted captured source into legacy.
140
163
  if(fs.existsSync(markerPath(home))) requireQualifiedHelper(src());
141
164
  const s=register(home);
165
+ if(s.harvestOff) fail('E_HARVEST_OFF',`harvest is off for this instance: ${s.switch.reason}. Nothing was captured.`);
142
166
  result=s.skipped?{status:'skipped',reason:'service'}:runSource(s,{manual:true,noLaunch:!!flags['no-launch']});
143
167
  }
144
- } else if(event==='run-source') result=runSource(src(),{manual:!!flags.manual,noLaunch:!!flags['no-launch']});
168
+ } else if(event==='run-source') {
169
+ // The deployment can switch harvest off after a source registered: the
170
+ // job then captures and processes nothing (the soul's opt-out was already
171
+ // applied at registration). Nothing drains when it is switched back on.
172
+ const source=src();
173
+ result=settings().harvest!=='on'?{status:'harvest-off',source:source.file,reason:'the deployment does not switch harvest on (settings.oats.okf.harvest); nothing was captured'}
174
+ :runSource(source,{manual:!!flags.manual,noLaunch:!!flags['no-launch']});
175
+ }
145
176
  else if(event==='complete') {const s=src();if(captured) retainedRun(s);result=complete(s,flags.run,flags.judgment && resolve(flags.judgment));}
146
177
  else if(event==='retry') {const s=src();if(captured && !flags.run && !flags.rejudge && !flags.launch && !flags['adopt-home']) retainedRun(s);result=retry(s,{run:flags.run,rejudge:!!flags.rejudge,launch:!!flags.launch,adoptHome:flags['adopt-home']});}
147
178
  else if(event==='inspect') result=inspect(src());
179
+ else if(event==='setup' && flags.harvest!==undefined) {
180
+ if(flags.source || flags.enable || flags.disable || flags['install-host']) fail('E_USAGE','setup --harvest takes no other setup flag');
181
+ result=setupHarvest(flags.harvest);
182
+ }
148
183
  else if(event==='setup') {
149
184
  const s=src();if(flags.enable && flags.disable) fail('E_USAGE','choose enable or disable');
150
185
  scheduleSource(s);
151
186
  if(flags.enable || flags.disable) {oats(['schedule',flags.enable?'enable':'disable',`okf-${s.id}`,'--dir',s.context,'--json'],s.context);updateStatus(s,current=>{current.auto=!!flags.enable;});}
152
187
  if(flags['install-host']) oats(['schedule','host','install','--dir',s.context,'--json'],s.context);
153
188
  result={source:s.file,scheduler:oats(['schedule','list','--dir',s.context,'--json'],s.context).scheduler};
154
- } else if(event==='read' || event==='refresh') {
155
- const s=src();
156
- // A descriptor-selected read is independent of any invoking/source home.
157
- // In particular, retired sources must not leave caches in context/repo.
158
- const target=join(flags.source?join(dirname(s.file),'views'):home,`knowledge-view-${randomUUID()}`);
159
- const receipts=views(s.bindings,s.decl,target);
160
- if(event==='refresh') result={path:target,receipts};
161
- else {
162
- if(!Object.hasOwn(s.bindings.bases,flags.base || '')) fail('E_CONFIG','unknown --base');
163
- const basePath=join(target,receipts[flags.base].path);
164
- const p=safePath(join(basePath,flags.path || 'index.md'));
165
- if(!p.startsWith(basePath+'/') || !p.endsWith('.md')) fail('E_PATH','read only contained Markdown');
166
- result={path:p,text:fs.readFileSync(p,'utf8'),receipt:receipts[flags.base]};
167
- }
168
189
  } else if(event==='init') result=initBase(loadBindings(),flags.base,flags.nodes,flags.output,{confirm:!!flags.confirm});
169
190
  else if(event==='migrate') {
170
191
  if(flags['source-home']) result=migrateSource(loadBindings(),flags['source-home']);
@@ -176,6 +197,10 @@ else {
176
197
  else fail('E_USAGE',`unknown command ${event}; see --help`);
177
198
  answer=hook?result:{schemaVersion:1,ok:true,result};
178
199
  } catch(e) {const code=e.code || 'E_OKF';exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:e.message}:{})},warning:`oats-okf ${code}: ${e.message}`}:{schemaVersion:1,ok:false,error:{code,message:e.message}};}
179
- // Let Node drain the pipe; no process.exit after a possibly large view.
180
- process.stdout.write(JSON.stringify(answer)+'\n');process.exitCode=exit;
200
+ // Consult commands print text unless --json; every other answer is JSON.
201
+ // Let Node drain the pipe; no process.exit after a possibly large answer.
202
+ if(textMode && exit) process.stderr.write(`oats okf ${event}: ${answer.error.code}: ${answer.error.message}\n`);
203
+ else if(textMode) process.stdout.write(text+'\n');
204
+ else process.stdout.write(JSON.stringify(answer)+'\n');
205
+ process.exitCode=exit;
181
206
  }
@@ -1,34 +1,42 @@
1
1
  ## Knowledge: OKF
2
2
 
3
- Your knowledge is external to the soul. Your task identifies accepted reader
4
- views at ./knowledge/ (or a later explicit view). Read view.json for each
5
- base's relative path (bases/<alias>/), then the indexes of your owned and read nodes at session start, after compaction, and
6
- when resuming. Follow only links relevant to the task: do not bulk-load bases.
7
- Each base is one link namespace: `/node/concept.md` resolves from that base's
8
- root, not the filesystem root. All configured bases are discoverable; owns
9
- means responsibility and reads means starting context, neither is an ACL.
3
+ You have two kinds of knowledge. Work WITH both: consult them at the start of
4
+ every task, after compaction, and every so often while you work, to decide,
5
+ to situate the task and to stay coherent with what your soul already knows.
10
6
 
11
- Consult prior decisions before re-deriving them. Cite base/node/concept paths.
12
- Views are immutable snapshots, not live mounts; `oats okf read --base ALIAS
13
- --path node/index.md` retrieves current accepted text. `oats okf refresh`
14
- returns a fresh view path and provider freshness receipts; re-read its indexes.
15
- A Git PR is not accepted knowledge until merge is visible on the accepted
16
- branch. A directory publication in progress blocks fresh views rather than
17
- showing partially published knowledge. Report missing configuration or blocked
18
- reads; do not create an empty substitute.
7
+ - **Soul knowledge**: your soul's accepted OKF bases, external to the soul and
8
+ read remotely at their accepted state (there is no local copy).
9
+ **Consultation** reads them with the `oats okf` CLI from your instance home.
10
+ Load the **okf-consultation** skill at the start of every task.
11
+ - **Instance knowledge**: this instance's own STATE.md, log.md and notes/ in
12
+ instance home. Load the **okf-instance-knowledge** skill: it teaches what is
13
+ worth capturing, not only where to put it.
19
14
 
20
- **Never write accepted knowledge or soul knowledge.** This is an instruction
21
- boundary, not a filesystem sandbox. Read through the provided views, not by
22
- editing the base behind them. Skills remain curated soul artifacts.
15
+ The work mode:
16
+ - **At task start and after compaction:** read STATE.md, recent log.md
17
+ entries and the relevant notes/, then `oats okf index` and
18
+ `oats okf cat --base ALIAS PATH` for the concepts the task needs. Follow
19
+ links; do not bulk-load.
20
+ - **Before compaction and before a task boundary:** update STATE.md, log.md
21
+ and notes/ first, so your future self can continue.
22
+ - **Every so often while working, and always before a design decision or
23
+ before re-deriving something:** `oats okf search` / `cat` the relevant
24
+ concepts and re-read your own notes. Consult prior decisions before
25
+ re-deriving them.
26
+ - Cite what you relied on as `alias/node/concept.md@<short-oid>`. A Git PR is
27
+ not accepted knowledge until it is merged. Report missing configuration or
28
+ blocked reads; do not create a substitute.
23
29
 
24
- Keep your task-local memory in instance home, not ./work:
25
- - STATE.md: rewrite the current task and progress; # Next names one next step.
26
- - log.md: append dated significant events; never rewrite history.
27
- - notes/: one Markdown concept per non-obvious insight, with type, title,
28
- description and observed provenance. Capture without judging importance.
29
- Record decisions, rejected alternatives, limitations and conclusions as
30
- they happen. Never include credentials or third-party messages verbatim.
30
+ **Capture with judgment**, as it happens: decisions and why, rejected
31
+ alternatives, costly discoveries, limitations and the workaround that worked,
32
+ conclusions, blockers, human direction and corrections, and surprises that
33
+ contradict soul knowledge (flag those as candidate supersessions). Not code
34
+ descriptions, command logs, retries, secrets, verbatim third-party messages,
35
+ or what the tracker and docs already hold. One concept per note in notes/;
36
+ STATE.md is the current picture (rewritten); log.md is dated events
37
+ (append-only). The knowledge harvester, not you, decides what is promoted.
31
38
 
32
- After compaction re-read STATE.md and the relevant knowledge indexes before
33
- continuing. Update memory before task boundaries. These files are not a second
34
- code manual: code and repository documentation remain truth about code.
39
+ **Never write accepted knowledge or soul knowledge.** This is an instruction
40
+ boundary, not a filesystem sandbox. Skills remain curated soul artifacts.
41
+ Instance knowledge lives in instance home, not ./work; code and repository
42
+ documentation remain the truth about code.
@@ -115,7 +115,10 @@ function contract(value,{required=false}={}) {
115
115
  return value;
116
116
  }
117
117
  function runtimeSettings(settings) {
118
- keys(settings,['bindings-file','state-dir','harvest-runtime','harvest-model'],[], 'OKF settings');
118
+ // `harvest` (on|off) is read by the lifecycle hooks, not bound: accept it here
119
+ // so a deployment can set it, and refuse any other value.
120
+ keys(settings,['bindings-file','state-dir','harvest-runtime','harvest-model','harvest'],[], 'OKF settings');
121
+ if(settings.harvest!==undefined && !['on','off'].includes(settings.harvest)) wireError('invalid-binding');
119
122
  const descriptorFile=settings['bindings-file'],stateDir=settings['state-dir'],runtime=settings['harvest-runtime'],model=settings['harvest-model'] ?? null;
120
123
  for(const name of ['bindings-file','state-dir']) {
121
124
  if(settings[name]===undefined) settingError(`${name}:missing`);
@@ -7,8 +7,10 @@ function keys(value, allowed, label, code='E_CONFIG') {
7
7
  }
8
8
  export function settings() {
9
9
  const s = JSON.parse(process.env.OATS_SETTINGS || '{}');
10
- keys(s,['bindings-file','state-dir','harvest-runtime','harvest-model','git-timeout'],'OATS_SETTINGS');
10
+ keys(s,['bindings-file','state-dir','harvest-runtime','harvest-model','git-timeout','consult-max-age','harvest'],'OATS_SETTINGS');
11
+ if(s.harvest!==undefined && !['on','off'].includes(s.harvest)) fail('E_CONFIG','harvest must be on or off');
11
12
  if(s['git-timeout']!==undefined && (!Number.isInteger(s['git-timeout']) || s['git-timeout']<1)) fail('E_CONFIG','git-timeout must be a positive integer number of seconds');
13
+ if(s['consult-max-age']!==undefined && (!Number.isInteger(s['consult-max-age']) || s['consult-max-age']<0)) fail('E_CONFIG','consult-max-age must be a non-negative integer number of seconds');
12
14
  if(s['state-dir']!==undefined && (typeof s['state-dir']!=='string' || !isAbsolute(s['state-dir']) || resolve(s['state-dir'])!==s['state-dir'])) fail('E_CONFIG','state-dir must be a normalized absolute path');
13
15
  if(s['harvest-runtime']!==undefined && !['pi','claude','codex'].includes(s['harvest-runtime'])) fail('E_CONFIG','invalid harvest-runtime');
14
16
  if(s['harvest-model']!==undefined && (typeof s['harvest-model']!=='string' || !s['harvest-model'].trim())) fail('E_CONFIG','harvest-model must be a nonempty string');
@@ -17,6 +19,9 @@ export function settings() {
17
19
  /** Time budget for Git operations that talk to a remote (clone, fetch, push,
18
20
  * ls-remote). Local object reads keep the short exec default. */
19
21
  export function gitTimeoutMs() { return (settings()['git-timeout'] ?? 600)*1000; }
22
+ /** How old (seconds) the host cache's accepted commit may be before a consult
23
+ * read refetches the accepted branch; 0 fetches on every read. */
24
+ export function consultMaxAgeMs() { return (settings()['consult-max-age'] ?? 300)*1000; }
20
25
  export function noGit(path) {
21
26
  for (let p = safePath(path); ; p = dirname(p)) {
22
27
  if (fs.existsSync(join(p, '.git')) || (fs.existsSync(join(p, 'HEAD')) && fs.existsSync(join(p, 'objects')) && fs.existsSync(join(p, 'refs')))) fail('E_DIRECTORY_GIT', `directory store is in Git custody: ${p}; use kind git`);