@awebai/oats 0.30.0 → 0.30.1

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 (118) hide show
  1. package/docs/desktop-cli-api.md +3 -3
  2. package/docs/implementation.md +2 -1
  3. package/docs/integrations.md +1 -1
  4. package/docs/knowledge.md +4 -4
  5. package/docs/official-catalog.md +3 -3
  6. package/docs/packages.md +8 -8
  7. package/docs/plans/0.30-close-out.md +24 -2
  8. package/docs/release-lane.md +7 -2
  9. package/docs/release-notes/v0.30.1.md +123 -0
  10. package/docs/souls-and-instances.md +2 -2
  11. package/docs/workspaces.md +6 -1
  12. package/lib/packages.mjs +1 -1
  13. package/lib/resolve.mjs +1 -1
  14. package/package-catalog.json +2 -2
  15. package/package.json +1 -3
  16. package/skills/oats-getting-started/SKILL.md +1 -1
  17. package/capabilities/oats-authoring/LICENSE +0 -21
  18. package/capabilities/oats-authoring/oats-package.json +0 -11
  19. package/capabilities/oats-authoring/oats.json +0 -12
  20. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
  21. package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
  22. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
  23. package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
  24. package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1672
  25. package/capabilities/oats-aweb/injects/aweb.md +0 -47
  26. package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -365
  27. package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
  28. package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
  29. package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
  30. package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
  31. package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
  32. package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
  33. package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
  34. package/capabilities/oats-aweb/oats.json +0 -201
  35. package/capabilities/oats-aweb/skills/LICENSE +0 -21
  36. package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
  37. package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
  38. package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
  39. package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
  40. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
  41. package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
  42. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -286
  43. package/capabilities/oats-code-review/injects/reviewer.md +0 -26
  44. package/capabilities/oats-code-review/oats.json +0 -16
  45. package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +0 -66
  46. package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +0 -30
  47. package/capabilities/oats-code-review/skills/security-review/SKILL.md +0 -56
  48. package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +0 -34
  49. package/capabilities/oats-developer/injects/developer.md +0 -38
  50. package/capabilities/oats-developer/oats.json +0 -17
  51. package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +0 -43
  52. package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +0 -47
  53. package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +0 -65
  54. package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +0 -37
  55. package/capabilities/oats-developer/skills/worktrees/SKILL.md +0 -36
  56. package/capabilities/oats-engineering-expert/injects/expert.md +0 -37
  57. package/capabilities/oats-engineering-expert/oats.json +0 -17
  58. package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +0 -37
  59. package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +0 -52
  60. package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +0 -50
  61. package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +0 -53
  62. package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +0 -49
  63. package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
  64. package/capabilities/oats-jira/injects/jira.md +0 -10
  65. package/capabilities/oats-jira/oats.json +0 -22
  66. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
  67. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
  68. package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
  69. package/capabilities/oats-linear/injects/linear.md +0 -8
  70. package/capabilities/oats-linear/oats.json +0 -24
  71. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
  72. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
  73. package/capabilities/oats-okf/bin/oats-okf.mjs +0 -213
  74. package/capabilities/oats-okf/injects/okf.md +0 -42
  75. package/capabilities/oats-okf/lib/binding-wire.mjs +0 -380
  76. package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
  77. package/capabilities/oats-okf/lib/config.mjs +0 -124
  78. package/capabilities/oats-okf/lib/consult.mjs +0 -518
  79. package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
  80. package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
  81. package/capabilities/oats-okf/lib/inspection.mjs +0 -138
  82. package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
  83. package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
  84. package/capabilities/oats-okf/lib/io.mjs +0 -118
  85. package/capabilities/oats-okf/lib/migration.mjs +0 -137
  86. package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
  87. package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
  88. package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
  89. package/capabilities/oats-okf/lib/sources.mjs +0 -438
  90. package/capabilities/oats-okf/lib/stores.mjs +0 -473
  91. package/capabilities/oats-okf/lib/worker.mjs +0 -486
  92. package/capabilities/oats-okf/oats.json +0 -151
  93. package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
  94. package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
  95. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
  96. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
  97. package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
  98. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
  99. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
  100. package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
  101. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
  102. package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
  103. package/capabilities/oats-okf-harvest/oats.json +0 -26
  104. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
  105. package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
  106. package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
  107. package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  108. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
  109. package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
  110. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
  111. package/capabilities/oats-okf-maintenance/oats.json +0 -21
  112. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
  113. package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
  114. package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
  115. package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  116. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -134
  117. package/capabilities/oats-workspace-experts/injects/oats-experts.md +0 -26
  118. package/capabilities/oats-workspace-experts/oats.json +0 -9
@@ -1,438 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
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
- import { loadInvocationKnowledgeBinding, readPrivateInvocationJson, sourceRuntimeFromKnowledgeBinding } from './binding-wire.mjs';
6
- import { sameJson } from './portable-binding.mjs';
7
- import { qualifiedSoulIdentity } from './source-contract.mjs';
8
- import { harvestSwitch, instanceRecordPath } from './harvest-switch.mjs';
9
-
10
- const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
11
- function exact(value,allowed,required,label) {
12
- if(!obj(value)) fail('E_SOURCE',`${label} must be an object`);
13
- for(const key of Object.keys(value)) if(!allowed.includes(key)) fail('E_SOURCE',`unknown ${label} property: ${key}`);
14
- for(const key of required) if(!Object.hasOwn(value,key)) fail('E_SOURCE',`${label} requires ${key}`);
15
- return value;
16
- }
17
- function absolute(value,label) {if(typeof value!=='string' || !value || resolve(value)!==value) fail('E_SOURCE',`${label} must be a normalized absolute path`);return value;}
18
- function executionBinding(value) {
19
- exact(value,['schemaVersion','deployment','resolution'],['schemaVersion','deployment','resolution'],'execution binding');
20
- exact(value.resolution,['schemaVersion','id'],['schemaVersion','id'],'resolution reference');
21
- if(value.schemaVersion!==1 || value.resolution.schemaVersion!==1 || typeof value.resolution.id!=='string' || !/^sha256-[a-f0-9]{64}$/.test(value.resolution.id)) fail('E_SOURCE','invalid execution binding');
22
- absolute(value.deployment,'execution deployment');return JSON.parse(JSON.stringify(value));
23
- }
24
- function human(value) {
25
- if(value===null) return null;
26
- exact(value,['provider','id'],['provider','id'],'responsible human');
27
- if(typeof value.provider!=='string' || !value.provider || typeof value.id!=='string' || !value.id) fail('E_SOURCE','invalid responsible human');
28
- return JSON.parse(JSON.stringify(value));
29
- }
30
- export function validateCapturedReceipt(home,receipt) {
31
- exact(receipt,['schemaVersion','kind','home','work','context','agent','instance','sourceIdentity','role','executionBinding','responsibleHuman','binding'],['schemaVersion','kind','home','work','context','agent','instance','sourceIdentity','role','executionBinding','responsibleHuman','binding'],'captured source receipt');
32
- if(receipt.schemaVersion!==1 || !['persistent','helper'].includes(receipt.kind)) fail('E_SOURCE','unsupported captured source receipt');
33
- home=absolute(home,'registration home');const receiptHome=absolute(receipt.home,'receipt home');if(home!==receiptHome) fail('E_SOURCE','registration home differs from receipt');
34
- const work=absolute(receipt.work,'receipt work'),context=absolute(receipt.context,'receipt context'),binding=sourceRuntimeFromKnowledgeBinding(receipt.binding),execution=executionBinding(receipt.executionBinding);
35
- if(context!==execution.deployment) fail('E_SOURCE','receipt context differs from execution deployment');
36
- identifier(receipt.agent);identifier(receipt.instance);
37
- if(typeof receipt.role!=='string' || Buffer.byteLength(receipt.role)>128*1024) fail('E_SOURCE','captured role must be text up to 128KiB');
38
- const sourceIdentity=receipt.sourceIdentity===null?null:qualifiedSoulIdentity(receipt.sourceIdentity);
39
- if((receipt.kind==='persistent')!==(sourceIdentity!==null)) fail('E_SOURCE','persistent receipt needs qualified source identity; helper needs null');
40
- return {home,work,context,binding,execution,sourceIdentity,responsibleHuman:human(receipt.responsibleHuman)};
41
- }
42
- export function loadInvocationSourceReceipt(home,env=process.env) {
43
- if(!Object.hasOwn(env,'OATS_SOURCE_RECEIPT_FILE')) return {mode:'legacy'};
44
- if(!Object.hasOwn(env,'OATS_BINDING_FILE')) fail('E_SOURCE','captured source receipt requires its provider binding snapshot');
45
- let receipt;try{receipt=readPrivateInvocationJson(env.OATS_SOURCE_RECEIPT_FILE);}catch{fail('E_SOURCE','invalid captured source receipt snapshot');}
46
- const validated=validateCapturedReceipt(safePath(home),receipt),invocation=loadInvocationKnowledgeBinding(env);
47
- if(invocation.kind!=='captured' || !sameJson(invocation.binding,receipt.binding)) fail('E_SOURCE','source receipt binding differs from invocation snapshot');
48
- return {mode:'captured',receipt,validated};
49
- }
50
- export const markerPath = home => join(home,'.okf-source.json');
51
- export const statusPath = source => join(dirname(source.file),'status.json');
52
- export const saveStatus = (source,status) => save(statusPath(source),status);
53
- export function updateStatus(source, mutate) {
54
- return withLock(join(dirname(source.file),'status.lock'),()=>{
55
- const status=loadStatus(source);mutate(status);saveStatus(source,status);return status;
56
- });
57
- }
58
- function saveCapture(source, status) {
59
- updateStatus(source,current=>{
60
- current.captured=status.captured;
61
- if(status.launchObserved!==undefined) current.launchObserved=status.launchObserved;
62
- if(status.lastCapture) current.lastCapture=status.lastCapture;
63
- if(status.retired) {current.retired=true;current.retiredAt=status.retiredAt;current.auto=current.auto && status.auto;}
64
- });
65
- }
66
- export const loadStatus = source => readJSON(statusPath(source));
67
- export function loadSource(file) {
68
- safePath(file); const s=readJSON(file);
69
- if(s.version!==1 || !/^[0-9a-f-]{36}$/.test(s.id) || resolve(file)!==join(s.bindings.stateDir,'sources',s.id,'source.json')) fail('E_SOURCE','invalid source descriptor path/identity');
70
- const {file:bindingsFile,...bindingsDoc}=s.bindings;
71
- const checked=validateBindings(bindingsDoc,bindingsFile,{sourceHome:s.home,sourceWork:s.work});
72
- if(bindingFingerprint(checked)!==s.bindingFingerprint) fail('E_SOURCE','frozen bindings fingerprint mismatch');
73
- if(s.providerBinding!==undefined) {
74
- let frozen;try{frozen=sourceRuntimeFromKnowledgeBinding(s.providerBinding);}catch{fail('E_SOURCE','invalid frozen provider binding');}
75
- const actual={owner:s.owner,bindings:{file:bindingsFile,version:checked.version,stateDir:checked.stateDir,bases:checked.bases},decl:s.decl,execution:s.execution};
76
- if(!sameJson(frozen,actual)) fail('E_SOURCE','frozen provider binding differs from source runtime');
77
- exact(s.registration,['schemaVersion','kind'],['schemaVersion','kind'],'source registration');
78
- if(s.registration.schemaVersion!==1 || s.registration.kind!=='captured' || !sameJson(qualifiedSoulIdentity(s.sourceIdentity),s.sourceIdentity)) fail('E_SOURCE','invalid captured source registration');
79
- executionBinding(s.executionBinding);human(s.responsibleHuman);
80
- } else if(s.registration!==undefined || s.sourceIdentity!==undefined || s.executionBinding!==undefined || s.responsibleHuman!==undefined) fail('E_SOURCE','partial captured source descriptor');
81
- const invocation=loadInvocationKnowledgeBinding();
82
- if(invocation.kind==='captured') {
83
- if(s.providerBinding===undefined) fail('E_MIGRATION','legacy source descriptor cannot consume a captured provider binding');
84
- if(!sameJson(invocation.binding,s.providerBinding)) fail('E_SOURCE','invocation provider binding differs from frozen source');
85
- }
86
- return {...s,file};
87
- }
88
- export function homeSource(home) { const m=readJSON(markerPath(home)); const s=loadSource(m.source); if(s.id!==m.id || s.home!==home) fail('E_SOURCE','home identity does not match durable source');return s; }
89
- export function service(home) {
90
- if(fs.existsSync(join(home,'instance.json'))) return readJSON(join(home,'instance.json')).kind==='capability';
91
- return process.env.OATS_KIND==='capability';
92
- }
93
- // okf 3.0.0 materializes no instance copy of any base: registration records
94
- // the accepted resolution (per base: commit or digest, and its nodes) and the
95
- // instance consults the bases remotely through `oats okf`. A ./knowledge/ left
96
- // by okf 2.x is not touched; inspect reports it as a legacy local view.
97
- const acceptedNodes = view => Object.fromEntries(Object.entries(view).map(([alias,row])=>[alias,row.nodes]));
98
- /** Instance knowledge (STATE.md, log.md, notes/) exists whether or not this
99
- * instance is harvested: it is the instance's own working memory. */
100
- export function ensureInstanceKnowledge(home) {
101
- 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(home,p))) atomic(join(home,p),text);
102
- fs.mkdirSync(join(home,'notes'),{recursive:true});
103
- }
104
- function finishRegistration(source) {
105
- // A durable home pointer precedes publication. Failures after it was saved
106
- // resume this same source; they never reset captured evidence or IDs.
107
- ensureInstanceKnowledge(source.home);
108
- scheduleSource(source);return source;
109
- }
110
- /** Harvest off (okf 4.0.0): no source, no custody, no schedule. The home keeps
111
- * a small record so retire knows there is nothing to capture, and (4.0.3) the
112
- * soul's declaration, so consultation works without a source. */
113
- function harvestOff(home,sw,extra={}) {
114
- const record={version:1,harvest:'off',reason:sw.reason,rows:sw.rows,warnings:sw.warnings,...(extra.decl?{decl:extra.decl}:{}),at:new Date().toISOString()};
115
- atomic(instanceRecordPath(home),JSON.stringify(record,null,2)+'\n');
116
- ensureInstanceKnowledge(home);
117
- return {harvestOff:true,switch:sw,home,...extra};
118
- }
119
- export const harvestOffRecord = home => fs.existsSync(instanceRecordPath(home))?readJSON(instanceRecordPath(home)):null;
120
- /** okf 4.0.3: what a harvest-off home consults. Consultation never depends on
121
- * the harvest switch: such a home has no registered source by design, so it
122
- * reads its soul's declaration (recorded at spawn; the kernel's OATS_SOUL for
123
- * a home spawned before 4.0.3) and the deployment's bindings as they are now.
124
- * Nothing is registered, captured or scheduled. */
125
- export function consultSource(home) {
126
- home=safePath(home);
127
- const record=harvestOffRecord(home);
128
- if(!record) fail('E_SOURCE',`no okf source for ${home}: it was not spawned with oats.okf as its knowledge capability`);
129
- const decl=record.decl || (process.env.OATS_SOUL?declaration(fs.realpathSync(process.env.OATS_SOUL)):fail('E_OATS_SOUL_MISSING','OATS_SOUL is not set; oats.okf commands run only under the OATS kernel'));
130
- const work=fs.existsSync(join(home,'work'))?fs.realpathSync(join(home,'work')):join(home,'work');
131
- return {home,work,decl,bindings:loadBindings(undefined,{sourceHome:home,sourceWork:work}),harvest:{status:'off',reason:record.reason,at:record.at}};
132
- }
133
- /** The source's tasks provider (its instance.json tasks-layer capability), or null. */
134
- function tasksProvider(meta) {
135
- const row=Array.isArray(meta?.capabilities)?meta.capabilities.find(c=>c && c.layer==='tasks' && typeof c.id==='string'):null;
136
- return row?row.id:null;
137
- }
138
- function capturedOwner(bindings,owner,identity) {
139
- const file=join(bindings.stateDir,'owners.json'),row={schemaVersion:1,kind:'captured-qualified-soul',identity};
140
- withLock(join(bindings.stateDir,'owners.lock'),()=>{
141
- const owners=fs.existsSync(file)?readJSON(file):{};if(!obj(owners)) fail('E_OWNER','invalid owner registry');const prior=Object.hasOwn(owners,owner)?owners[owner]:undefined;
142
- if(typeof prior==='string') fail('E_MIGRATION','legacy owner registry evidence requires explicit qualified-identity migration');
143
- if(prior!==undefined && (!obj(prior) || prior.schemaVersion!==1 || prior.kind!=='captured-qualified-soul' || !sameJson(prior.identity,identity))) fail('E_OWNER','stable owner ID already identifies a different qualified soul');
144
- if(prior===undefined) {owners[owner]=row;save(file,owners);}
145
- });
146
- }
147
- function sameCapturedReceipt(source,receipt,validated) {
148
- return source.registration?.schemaVersion===1 && source.registration.kind==='captured'
149
- && source.home===validated.home && source.work===validated.work && source.context===validated.context
150
- && source.agent===receipt.agent && source.instance===receipt.instance && source.role===receipt.role
151
- && sameJson(source.sourceIdentity,validated.sourceIdentity) && sameJson(source.executionBinding,validated.execution)
152
- && sameJson(source.responsibleHuman,validated.responsibleHuman) && sameJson(source.providerBinding,receipt.binding);
153
- }
154
- export function registerCaptured(home,receipt) {
155
- home=safePath(home);const captured=validateCapturedReceipt(home,receipt),invocation=loadInvocationKnowledgeBinding();
156
- if(invocation.kind==='captured' && !sameJson(invocation.binding,receipt.binding)) fail('E_SOURCE','lifecycle receipt binding differs from invocation snapshot');
157
- if(receipt.kind==='helper') return {skipped:'service'};
158
- if(fs.existsSync(markerPath(home))) {
159
- const source=homeSource(home);if(!sameCapturedReceipt(source,receipt,captured)) fail('E_SOURCE','captured registration receipt differs from durable source');
160
- return finishRegistration(source);
161
- }
162
- 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');
163
- // The captured path takes its authority from the frozen binding and never
164
- // reads live settings or the soul, so the 4.0.0 harvest switch (a live host
165
- // setting) does not apply here. No released kernel drives this path.
166
- if(overlaps(home,captured.context) && captured.context.startsWith(home)) fail('E_PATH','captured deployment context cannot be in disposable home');
167
- const {file:bindingsFile,...bindingsDoc}=captured.binding.bindings;
168
- const bindings={file:bindingsFile,...validateBindings(bindingsDoc,bindingsFile,{sourceHome:home,sourceWork:captured.work})};
169
- fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});capturedOwner(bindings,captured.binding.owner,captured.sourceIdentity);
170
- const id=randomUUID(),dir=join(bindings.stateDir,'sources',id),source={version:1,id,home,work:captured.work,context:captured.context,
171
- agent:receipt.agent,instance:receipt.instance,owner:captured.binding.owner,decl:captured.binding.decl,role:receipt.role,bindings,
172
- bindingFingerprint:bindingFingerprint(bindings),execution:captured.binding.execution,providerBinding:JSON.parse(JSON.stringify(receipt.binding)),
173
- registration:{schemaVersion:1,kind:'captured'},sourceIdentity:captured.sourceIdentity,executionBinding:captured.execution,
174
- responsibleHuman:captured.responsibleHuman,created:new Date().toISOString()};
175
- const file=join(dir,'source.json');fs.mkdirSync(dir,{recursive:true,mode:0o700});
176
- try {
177
- source.acceptedView=acceptedResolution(bindings,source.decl);source.acceptedNodes=acceptedNodes(source.acceptedView);
178
- save(file,source);save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
179
- save(markerPath(home),{version:1,id,source:file});
180
- } catch(error) {
181
- if(!fs.existsSync(markerPath(home))) fs.rmSync(dir,{recursive:true,force:true});
182
- throw error;
183
- }
184
- return finishRegistration({...source,file});
185
- }
186
-
187
- export function register(home) {
188
- home=safePath(home);
189
- const invocation=loadInvocationKnowledgeBinding();
190
- if(invocation.kind==='captured') {
191
- if(fs.existsSync(markerPath(home))) return finishRegistration(homeSource(home));
192
- fail('E_MIGRATION','captured provider binding requires durable captured-source registration; current soul/config fallback is forbidden');
193
- }
194
- if(service(home)) return {skipped:'service'};
195
- if(fs.existsSync(markerPath(home))) return finishRegistration(homeSource(home));
196
- 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');
197
- const meta=fs.existsSync(join(home,'instance.json'))?readJSON(join(home,'instance.json')):{};
198
- 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');
199
- const soul=fs.realpathSync(process.env.OATS_SOUL);
200
- const work=fs.existsSync(join(home,'work'))?fs.realpathSync(join(home,'work')):join(home,'work');
201
- const decl=declaration(soul);
202
- const soulId=process.env.OATS_SOUL_ID || null;
203
- const bindings=loadBindings(undefined,{sourceHome:home,sourceWork:work});
204
- const context=fs.realpathSync(process.env.OATS_CONTEXT || meta.repo || fail('E_CONFIG','source requires durable config context'));
205
- if(overlaps(home,context) && context.startsWith(home)) fail('E_PATH','config context cannot be in disposable home');
206
- const agent=process.env.OATS_AGENT || meta.agent;
207
- const instance=process.env.OATS_INSTANCE || meta.instance;
208
- if(!agent || !instance) fail('E_SOURCE','source instance/agent required');
209
- const sw=harvestSwitch({settings:settings(),soulDir:soul});
210
- if(sw.effective!=='on') {acceptedResolution(bindings,decl);return harvestOff(home,sw,{decl});}
211
- fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
212
- const ownersFile=join(bindings.stateDir,'owners.json');
213
- const id=randomUUID(); const dir=join(bindings.stateDir,'sources',id);
214
- // Copy only the role document, never instance.json wholesale, launch recipes,
215
- // environment, credentials, source worktree, or third-party message stores.
216
- const roleFile=safePath(join(soul,'AGENTS.md'));
217
- const role=fs.existsSync(roleFile)?fs.readFileSync(roleFile,'utf8'):'';
218
- if(Buffer.byteLength(role)>128*1024) fail('E_SOURCE','role document exceeds 128KiB; provide a concise role before registering');
219
- const source={version:1,id,home,work,context,agent,instance,owner:decl.owner,soulDir:soul,decl,role,bindings,bindingFingerprint:bindingFingerprint(bindings),execution:{runtime:settings()['harvest-runtime']||'pi',model:settings()['harvest-model']||null},soulId,tasksProvider:tasksProvider(meta),created:new Date().toISOString()};
220
- const file=join(dir,'source.json');
221
- fs.mkdirSync(dir,{recursive:true,mode:0o700});
222
- try {
223
- source.acceptedView=acceptedResolution(bindings,decl);
224
- pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
225
- source.acceptedNodes=acceptedNodes(source.acceptedView);
226
- save(file,source);
227
- save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
228
- save(markerPath(home),{version:1,id,source:file});
229
- } catch(e) {
230
- // Until the pointer is durable no capture or schedule can reference these
231
- // files. Leave an installed pointer's state intact even if fsync failed.
232
- if(!fs.existsSync(markerPath(home))) fs.rmSync(dir,{recursive:true,force:true});
233
- throw e;
234
- }
235
- return finishRegistration({...source,file});
236
- }
237
- /** Pin a stable owner id to the soul it identifies. The kernel names a soul by
238
- * identity (OATS_SOUL_ID: repository key plus soul name) so the pin survives
239
- * the per-commit soul copies a workspace deployment materializes; a classic
240
- * soul keeps the resolved path. A row written by an earlier version as a path
241
- * under agents/<same soul name>/(soul|souls/<commit>) is rewritten to the
242
- * identity once; any other mismatch is a different soul and is refused. */
243
- export function pinOwner(ownersFile,owner,{id,soulName,path}) {
244
- const value=id || path;
245
- return withLock(join(dirname(ownersFile),'owners.lock'),()=>{
246
- const owners=fs.existsSync(ownersFile)?readJSON(ownersFile):{};
247
- if(!obj(owners)) fail('E_OWNER','invalid owner registry');
248
- const prior=Object.hasOwn(owners,owner)?owners[owner]:undefined;
249
- const samePath=typeof prior==='string' && new RegExp(`/agents/${soulName.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}/(soul|souls/[^/]+)$`).test(prior);
250
- if(prior!==undefined && prior!==value && !(id && samePath)) fail('E_OWNER',`stable owner ID already identifies a different soul in this state namespace: existing soul ${prior}; new soul ${value}. Remedies: retire the existing registration first, or use a fresh state directory.`);
251
- if(prior!==value) {owners[owner]=value;save(ownersFile,owners);}
252
- return value;
253
- });
254
- }
255
- function enqueue(source,status,payload) {
256
- const id=hash(payload);const path=join(dirname(source.file),'inputs',`${id}.json`);
257
- if(!fs.existsSync(path)) save(path,payload);
258
- if(!status.captured.inputs.includes(id)) status.captured.inputs.push(id);
259
- return id;
260
- }
261
- export function input(source,id) {
262
- if(!/^[0-9a-f]{64}$/.test(id)) fail('E_INPUT','bad input identity');
263
- const value=readJSON(join(dirname(source.file),'inputs',`${id}.json`));
264
- if(hash(value)!==id) fail('E_INPUT','durable evidence hash mismatch');return value;
265
- }
266
- /** Take a drained, retired source's job out of the kernel scheduler. The job is
267
- * first switched off, then removed: the definition it carried is kept as
268
- * evidence in this source's own schedule.json, so nothing about the run is
269
- * lost, and `oats schedule list` stops accumulating dead okf-<id> rows. A job
270
- * that is still running (or has unresolved effects) stays disabled and is
271
- * removed on the worker's next settle. Idempotent; a scheduler failure is
272
- * recorded, never thrown — the evidence is already safe. */
273
- /** okf 4.0.1 #6: the switch for an already registered source, re-read now: the
274
- * deployment setting AND the soul's opt-out (the current OATS_SOUL the kernel
275
- * hands run-source/retire, else the soul directory recorded at registration). */
276
- export function sourceSwitch(source) {
277
- const current = process.env.OATS_SOUL && fs.existsSync(process.env.OATS_SOUL) ? process.env.OATS_SOUL : null;
278
- return harvestSwitch({ settings: settings(), soulDir: current || source.soulDir || undefined });
279
- }
280
- /** Retire a registered source whose harvest is now off: no final capture; the
281
- * inputs already in custody stay; the schedule is settled as for any retire. */
282
- export function retireHarvestOff(source, sw) {
283
- updateStatus(source, current => {
284
- current.retired = true; current.retiredAt = new Date().toISOString(); current.harvestOff = { reason: sw.reason, at: current.retiredAt };
285
- // As a final capture would: the schedule stays only while earlier inputs await processing.
286
- current.auto = current.auto && !current.captured.inputs.every(id => current.processed.includes(id));
287
- });
288
- return { retired: true, reason: 'harvest-off', switch: sw.reason, source: source.file, schedule: settleRetiredSchedule(source) };
289
- }
290
- export function settleRetiredSchedule(source) {
291
- const status=loadStatus(source);
292
- if(status.schedule?.removed===true) return {status:'already-removed',id:status.schedule.id};
293
- if(!status.retired || status.auto || status.schedule?.id===undefined) return {status:'kept'};
294
- const id=status.schedule.id;
295
- const mark=(patch)=>updateStatus(source,current=>{current.schedule={...current.schedule,...patch};});
296
- try {
297
- if(status.schedule.settled!==true) {
298
- oats(['schedule','disable',id,'--dir',source.context,'--json'],source.context);
299
- mark({settled:true,settledAt:new Date().toISOString(),settleError:undefined});
300
- }
301
- } catch(e) {
302
- if(e.code!=='E_SCHEDULE_UNKNOWN') {mark({settled:false,settleError:e.message});return {status:'disable-failed',id};}
303
- }
304
- try {
305
- oats(['schedule','remove',id,'--dir',source.context,'--json'],source.context);
306
- mark({removed:true,removedAt:new Date().toISOString(),removeError:undefined});
307
- return {status:'removed',id};
308
- } catch(e) {
309
- if(e.code==='E_SCHEDULE_UNKNOWN') {mark({removed:true,removedAt:new Date().toISOString(),removeError:undefined});return {status:'already-removed',id};}
310
- mark({removed:false,removeError:e.message});
311
- return {status:e.code==='E_SCHEDULE_RUNNING'?'disabled-pending-removal':'remove-failed',id};
312
- }
313
- }
314
- export function capture(source,{final=false,deadlineMs=85000}={}) {
315
- return withLock(join(dirname(source.file),'capture.lock'),()=>{
316
- const status=loadStatus(source);
317
- if(status.retired) return {status:'complete',complete:true,retired:true};
318
- if(!fs.existsSync(markerPath(source.home))) fail('E_SOURCE','source home gone without final capture; existing evidence is retained');
319
- if(homeSource(source.home).id!==source.id) fail('E_SOURCE','source name reused');
320
- const start=Date.now(); const meta=fs.existsSync(join(source.home,'instance.json'))?readJSON(join(source.home,'instance.json')):{};
321
- const noLaunch=meta.launched!==true;status.launchObserved=!noLaunch;
322
- // Notes AND record, every pass. Note content versions remain captured even
323
- // when the live file is rewritten while a worker is judging a prior version.
324
- const notes=join(source.home,'notes');
325
- if(fs.existsSync(notes)) for(const [name,b64] of Object.entries(tree(notes))) {
326
- if(!name.endsWith('.md')) continue;
327
- const text=Buffer.from(b64,'base64').toString('utf8');const key=hash({name,text});
328
- if(status.captured.notes.includes(key)) continue;
329
- enqueue(source,status,{version:1,kind:'note',name,contentHash:hash(text),text});
330
- status.captured.notes.push(key);saveCapture(source,status);
331
- }
332
- let report;
333
- try {
334
- report=oats(['capture','--home',source.home,'--quiet'],source.context,{native:true,timeout:Math.min(deadlineMs,60000)});
335
- if(!Array.isArray(report.sessions)) fail('E_CAPTURE','capture response has no sessions');
336
- for(const session of report.sessions) {
337
- if(!session.thread || !session.lastTurnId) continue;
338
- let after=status.captured.threads[session.thread] || null;
339
- while(after!==session.lastTurnId) {
340
- if(Date.now()-start>deadlineMs) fail('E_CAPTURE','capture deadline: backlog preserved; retire must retry');
341
- const args=['recall','--thread',session.thread,'--until',session.lastTurnId,'--limit','60','--json','--ids-only'];
342
- if(after) args.push('--after',after);
343
- const remainingTime=()=>{
344
- const remaining=deadlineMs-(Date.now()-start);
345
- if(remaining<=0) fail('E_CAPTURE','capture deadline: backlog preserved; retire must retry');
346
- return Math.max(1000,remaining);
347
- };
348
- let plan;
349
- try { plan=oats(args,source.context,{native:true,timeout:remainingTime()}); }
350
- catch(e) { if(after && /--after: no turn/.test(e.message)) {after=null;continue;} throw e; }
351
- if(!Array.isArray(plan.turns) || !plan.turns.length || plan.turns.length>60) fail('E_CAPTURE','capture/recall boundaries disagree');
352
- // Plan BEFORE requesting text. Native bytes describe each turn in
353
- // pretty JSON. Nested response indentation adds bytes: reserve 3x
354
- // that estimate plus envelope space, always below the 16MiB pipe.
355
- // The 96k target is soft only for one individually legal turn.
356
- let offset=0;
357
- while(offset<plan.turns.length) {
358
- const window=[];let bytes=0;
359
- for(const t of plan.turns.slice(offset)) {
360
- if(typeof t.id!=='string' || !t.id || t.thread!==session.thread || t.kind!=='session' || !Number.isSafeInteger(t.bytes) || t.bytes<=0) fail('E_CAPTURE','invalid ids-only record metadata');
361
- if(window.length && bytes+t.bytes>96000) break;
362
- if(3*(bytes+t.bytes)+4096>16*1024*1024) fail('E_CAPTURE','single captured turn exceeds bounded transport; input retained at source, manual intervention required');
363
- window.push(t);bytes+=t.bytes;
364
- }
365
- const until=window.at(-1).id;
366
- const read=['recall','--thread',session.thread,'--until',until,'--limit',String(window.length),'--json'];
367
- if(after) read.push('--after',after);
368
- const full=oats(read,source.context,{native:true,timeout:remainingTime()});
369
- if(!Array.isArray(full.turns) || full.turns.length!==window.length || full.remaining!==0) fail('E_CAPTURE','planned record window changed; retain source and retry');
370
- const turns=full.turns.map((t,i)=>{
371
- const turn={id:t.id,ts:t.ts,thread:t.thread,kind:t.kind,source:t.source,text:t.text};
372
- if(['id','ts','thread','kind','source'].some(k=>turn[k]!==window[i][k]) || !Array.isArray(turn.text) || turn.text.some(p=>typeof p.role!=='string' || typeof p.text!=='string')) fail('E_CAPTURE','unsupported or changed captured session record');
373
- if(Buffer.byteLength(JSON.stringify(turn))>1024*1024) fail('E_CAPTURE','single captured turn exceeds 1MiB; input retained at source, manual intervention required');
374
- return turn;
375
- });
376
- // Commit evidence before its cursor; failures retain earlier windows.
377
- enqueue(source,status,{version:1,kind:'record',thread:session.thread,after,until,turns});
378
- after=until;status.captured.threads[session.thread]=after;saveCapture(source,status);
379
- offset+=window.length;
380
- }
381
- if(plan.remaining===0 && after!==session.lastTurnId) fail('E_CAPTURE','final record boundary missing');
382
- }
383
- }
384
- status.lastCapture={status:report.status,complete:report.complete===true,ignored:report.ignored||0,at:new Date().toISOString()};
385
- if(report.complete!==true) fail('E_CAPTURE',`capture ${report.status || 'uncertified'}: retain source and retry`);
386
- if(final) {
387
- status.retired=true;status.retiredAt=new Date().toISOString();
388
- // A retired source whose every captured input is already processed has
389
- // no further work: its schedule is switched off and removed from the
390
- // kernel scheduler (settleRetiredSchedule); the definition stays as
391
- // evidence in this source's schedule.json. Anything still pending keeps
392
- // the job enabled until the worker drains it (see worker.mjs).
393
- const drained=status.captured.inputs.every(id=>status.processed.includes(id));
394
- status.auto=status.auto && !noLaunch && !drained;
395
- }
396
- saveCapture(source,status);return {...status.lastCapture,inputs:status.captured.inputs.length};
397
- } catch(e) {
398
- status.lastCapture={status:'incomplete',complete:false,error:e.message,at:new Date().toISOString()}; saveCapture(source,status);throw e;
399
- }
400
- });
401
- }
402
- export function scheduleSource(source) {
403
- // A retired, drained source whose job was already taken out of the scheduler
404
- // has nothing left to run: do not recreate the job (retire is re-entrant).
405
- const current=loadStatus(source);
406
- if(current.retired && current.schedule?.removed===true) return current.schedule.result;
407
- const captured=source.registration?.schemaVersion===1 && source.registration.kind==='captured';
408
- const argv=captured?['oats','okf','run-source','--source',source.file,'--deployment',source.executionBinding.deployment,'--resolution',source.executionBinding.resolution.id,'--json']
409
- :['oats','okf','run-source','--source',source.file,'--soul',source.agent,'--json'];
410
- const spec={id:`okf-${source.id}`,kind:'command',enabled:current.auto,cron:source.bindings.cron,tz:source.bindings.tz,cwd:source.context,argv,
411
- ...(captured?{definitionVersion:2,recurrencePolicy:'capture',responsibleHuman:source.responsibleHuman}: {})};
412
- const file=join(dirname(source.file),'schedule.json');
413
- try {
414
- save(file,spec);
415
- let result;
416
- try {result=oats(['schedule','add',spec.id,'--file',file,'--dir',source.context,'--json'],source.context);}
417
- catch(e) {
418
- // Never overwrite a colliding job or re-enable an operator-disabled job.
419
- // Retry after an uncertain add must verify the actual definition, not a
420
- // local receipt. A deleted definition is recreated by the add above.
421
- if(e.code!=='E_SCHEDULE_EXISTS') throw e;
422
- result=oats(['schedule','show',spec.id,'--dir',source.context,'--json'],source.context);
423
- }
424
- const actual=result?.schedule;
425
- if(!actual || typeof actual.enabled!=='boolean' || ['id','kind','cron','tz','cwd','argv','definitionVersion','recurrencePolicy'].some(k=>JSON.stringify(actual[k])!==JSON.stringify(spec[k]))) fail('E_SCHEDULE','source schedule definition differs; inspect and repair explicitly');
426
- if(captured) {
427
- const responsible=actual.execution && Object.hasOwn(actual.execution,'responsibleHuman')?actual.execution.responsibleHuman
428
- :Object.hasOwn(actual,'responsibleHuman')?actual.responsibleHuman:undefined;
429
- if(!sameJson(responsible,spec.responsibleHuman)) fail('E_SCHEDULE','captured source schedule responsible human differs');
430
- if(actual.execution && (actual.execution.deployment!==source.executionBinding.deployment || actual.execution.resolution?.id!==source.executionBinding.resolution.id)) fail('E_SCHEDULE','captured source schedule execution binding differs');
431
- }
432
- // A job already settled off for a retired, drained source stays settled:
433
- // registration re-verifies the definition but does not forget the switch-off.
434
- updateStatus(source,status=>{const settled=status.schedule?.settled===true?{settled:true,settledAt:status.schedule.settledAt}:{};status.schedule={id:spec.id,status:'ready',result,...settled};});return result;
435
- } catch(e) {
436
- updateStatus(source,status=>{status.schedule={...(status.schedule || {}),id:spec.id,status:'failed',error:e.message};});throw e;
437
- }
438
- }