@awebai/oats 0.30.0 → 0.30.2

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 (128) hide show
  1. package/bin/oats.mjs +1 -1
  2. package/docs/capabilities.md +3 -3
  3. package/docs/design/2026-09-23-workspace-module-contracts.md +2 -1
  4. package/docs/desktop-cli-api.md +23 -11
  5. package/docs/first-team.md +1 -1
  6. package/docs/implementation.md +2 -1
  7. package/docs/integrations.md +1 -1
  8. package/docs/knowledge-capability-authoring.md +1 -1
  9. package/docs/knowledge.md +4 -4
  10. package/docs/official-catalog.md +4 -4
  11. package/docs/packages.md +13 -13
  12. package/docs/plans/0.30-close-out.md +24 -2
  13. package/docs/release-lane.md +7 -2
  14. package/docs/release-notes/v0.30.1.md +123 -0
  15. package/docs/release-notes/v0.30.2.md +85 -0
  16. package/docs/souls-and-instances.md +6 -5
  17. package/docs/workspaces.md +7 -2
  18. package/lib/core.mjs +42 -28
  19. package/lib/instance-inspect.mjs +1 -1
  20. package/lib/instance-resolution.mjs +15 -8
  21. package/lib/materialize.mjs +33 -19
  22. package/lib/packages.mjs +1 -1
  23. package/lib/resolve.mjs +1 -1
  24. package/package-catalog.json +3 -3
  25. package/package.json +1 -3
  26. package/skills/oats-getting-started/SKILL.md +2 -2
  27. package/capabilities/oats-authoring/LICENSE +0 -21
  28. package/capabilities/oats-authoring/oats-package.json +0 -11
  29. package/capabilities/oats-authoring/oats.json +0 -12
  30. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
  31. package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
  32. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
  33. package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
  34. package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1672
  35. package/capabilities/oats-aweb/injects/aweb.md +0 -47
  36. package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -365
  37. package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
  38. package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
  39. package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
  40. package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
  41. package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
  42. package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
  43. package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
  44. package/capabilities/oats-aweb/oats.json +0 -201
  45. package/capabilities/oats-aweb/skills/LICENSE +0 -21
  46. package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
  47. package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
  48. package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
  49. package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
  50. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
  51. package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
  52. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -286
  53. package/capabilities/oats-code-review/injects/reviewer.md +0 -26
  54. package/capabilities/oats-code-review/oats.json +0 -16
  55. package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +0 -66
  56. package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +0 -30
  57. package/capabilities/oats-code-review/skills/security-review/SKILL.md +0 -56
  58. package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +0 -34
  59. package/capabilities/oats-developer/injects/developer.md +0 -38
  60. package/capabilities/oats-developer/oats.json +0 -17
  61. package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +0 -43
  62. package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +0 -47
  63. package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +0 -65
  64. package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +0 -37
  65. package/capabilities/oats-developer/skills/worktrees/SKILL.md +0 -36
  66. package/capabilities/oats-engineering-expert/injects/expert.md +0 -37
  67. package/capabilities/oats-engineering-expert/oats.json +0 -17
  68. package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +0 -37
  69. package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +0 -52
  70. package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +0 -50
  71. package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +0 -53
  72. package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +0 -49
  73. package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
  74. package/capabilities/oats-jira/injects/jira.md +0 -10
  75. package/capabilities/oats-jira/oats.json +0 -22
  76. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
  77. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
  78. package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
  79. package/capabilities/oats-linear/injects/linear.md +0 -8
  80. package/capabilities/oats-linear/oats.json +0 -24
  81. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
  82. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
  83. package/capabilities/oats-okf/bin/oats-okf.mjs +0 -213
  84. package/capabilities/oats-okf/injects/okf.md +0 -42
  85. package/capabilities/oats-okf/lib/binding-wire.mjs +0 -380
  86. package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
  87. package/capabilities/oats-okf/lib/config.mjs +0 -124
  88. package/capabilities/oats-okf/lib/consult.mjs +0 -518
  89. package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
  90. package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
  91. package/capabilities/oats-okf/lib/inspection.mjs +0 -138
  92. package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
  93. package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
  94. package/capabilities/oats-okf/lib/io.mjs +0 -118
  95. package/capabilities/oats-okf/lib/migration.mjs +0 -137
  96. package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
  97. package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
  98. package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
  99. package/capabilities/oats-okf/lib/sources.mjs +0 -438
  100. package/capabilities/oats-okf/lib/stores.mjs +0 -473
  101. package/capabilities/oats-okf/lib/worker.mjs +0 -486
  102. package/capabilities/oats-okf/oats.json +0 -151
  103. package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
  104. package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
  105. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
  106. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
  107. package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
  108. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
  109. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
  110. package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
  111. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
  112. package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
  113. package/capabilities/oats-okf-harvest/oats.json +0 -26
  114. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
  115. package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
  116. package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
  117. package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  118. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
  119. package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
  120. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
  121. package/capabilities/oats-okf-maintenance/oats.json +0 -21
  122. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
  123. package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
  124. package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
  125. package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  126. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -134
  127. package/capabilities/oats-workspace-experts/injects/oats-experts.md +0 -26
  128. 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
- }