@awebai/oats 0.29.4 → 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 (263) hide show
  1. package/README.md +12 -6
  2. package/bin/oats.mjs +194 -50
  3. package/docs/capabilities.md +160 -171
  4. package/docs/capability-manifest.schema.json +6 -11
  5. package/docs/configuration.md +213 -64
  6. package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
  7. package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
  8. package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
  9. package/docs/design/2026-09-27-team-model-v2.md +97 -117
  10. package/docs/design/2026-09-28-automations-trust.md +38 -0
  11. package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
  12. package/docs/design/HISTORY.md +65 -0
  13. package/docs/design/README.md +23 -54
  14. package/docs/desktop-cli-api.md +1787 -1777
  15. package/docs/desktop.md +30 -91
  16. package/docs/execution-targets.md +146 -292
  17. package/docs/first-team.md +31 -17
  18. package/docs/implementation.md +77 -288
  19. package/docs/integrations.md +118 -320
  20. package/docs/knowledge-capability-authoring.md +25 -52
  21. package/docs/knowledge-reference/acceptance.md +3 -3
  22. package/docs/knowledge-reference/adoption.md +1 -1
  23. package/docs/knowledge-reference/harvester.md +2 -2
  24. package/docs/knowledge-reference/package-craft.md +3 -3
  25. package/docs/knowledge-reference/provider-mapping.md +3 -6
  26. package/docs/knowledge-reference/reader-capture.md +3 -3
  27. package/docs/knowledge-theory.md +62 -166
  28. package/docs/knowledge.md +225 -404
  29. package/docs/layers.md +42 -97
  30. package/docs/oats-local.schema.json +58 -5
  31. package/docs/oats-membership.schema.json +1 -8
  32. package/docs/oats-package.schema.json +5 -5
  33. package/docs/oats-workspace.schema.json +8 -22
  34. package/docs/official-catalog.md +25 -28
  35. package/docs/packages.md +45 -63
  36. package/docs/plans/0.30-close-out.md +83 -0
  37. package/docs/release-lane.md +82 -0
  38. package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
  39. package/docs/release-notes/v0.19.0.md +48 -147
  40. package/docs/release-notes/v0.19.1.md +2 -3
  41. package/docs/release-notes/v0.19.3.md +2 -15
  42. package/docs/release-notes/v0.20.0.md +0 -15
  43. package/docs/release-notes/v0.22.0.md +71 -138
  44. package/docs/release-notes/v0.22.1.md +42 -90
  45. package/docs/release-notes/v0.22.10.md +1 -1
  46. package/docs/release-notes/v0.22.11.md +1 -47
  47. package/docs/release-notes/v0.22.12.md +4 -13
  48. package/docs/release-notes/v0.22.13.md +1 -42
  49. package/docs/release-notes/v0.22.14.md +3 -11
  50. package/docs/release-notes/v0.22.15.md +1 -46
  51. package/docs/release-notes/v0.22.16.md +6 -8
  52. package/docs/release-notes/v0.22.18.md +1 -99
  53. package/docs/release-notes/v0.22.19.md +3 -14
  54. package/docs/release-notes/v0.22.2.md +6 -15
  55. package/docs/release-notes/v0.22.3.md +0 -1
  56. package/docs/release-notes/v0.22.4.md +1 -14
  57. package/docs/release-notes/v0.22.5.md +2 -12
  58. package/docs/release-notes/v0.22.6.md +0 -3
  59. package/docs/release-notes/v0.23.0.md +9 -25
  60. package/docs/release-notes/v0.23.1.md +9 -25
  61. package/docs/release-notes/v0.23.2.md +2 -4
  62. package/docs/release-notes/v0.24.0.md +56 -97
  63. package/docs/release-notes/v0.24.1.md +7 -11
  64. package/docs/release-notes/v0.24.10.md +34 -45
  65. package/docs/release-notes/v0.24.11.md +12 -20
  66. package/docs/release-notes/v0.24.12.md +35 -48
  67. package/docs/release-notes/v0.24.13.md +34 -41
  68. package/docs/release-notes/v0.24.2.md +9 -13
  69. package/docs/release-notes/v0.24.3.md +7 -11
  70. package/docs/release-notes/v0.24.4.md +6 -6
  71. package/docs/release-notes/v0.24.5.md +6 -10
  72. package/docs/release-notes/v0.24.6.md +2 -5
  73. package/docs/release-notes/v0.24.7.md +46 -75
  74. package/docs/release-notes/v0.24.8.md +58 -96
  75. package/docs/release-notes/v0.24.9.md +38 -54
  76. package/docs/release-notes/v0.25.0.md +59 -76
  77. package/docs/release-notes/v0.25.1.md +57 -81
  78. package/docs/release-notes/v0.25.2.md +51 -70
  79. package/docs/release-notes/v0.25.3.md +11 -13
  80. package/docs/release-notes/v0.25.4.md +9 -13
  81. package/docs/release-notes/v0.25.5.md +3 -5
  82. package/docs/release-notes/v0.25.6.md +20 -29
  83. package/docs/release-notes/v0.25.7.md +5 -7
  84. package/docs/release-notes/v0.25.8.md +26 -39
  85. package/docs/release-notes/v0.26.0.md +175 -646
  86. package/docs/release-notes/v0.27.0.md +4 -5
  87. package/docs/release-notes/v0.27.1.md +4 -6
  88. package/docs/release-notes/v0.27.2.md +1 -1
  89. package/docs/release-notes/v0.28.0.md +57 -124
  90. package/docs/release-notes/v0.29.0.md +89 -208
  91. package/docs/release-notes/v0.29.1.md +1 -1
  92. package/docs/release-notes/v0.29.2.md +3 -4
  93. package/docs/release-notes/v0.30.0.md +205 -0
  94. package/docs/release-notes/v0.30.1.md +123 -0
  95. package/docs/schedules.md +280 -363
  96. package/docs/servers.md +99 -117
  97. package/docs/soul.schema.json +2 -9
  98. package/docs/souls-and-instances.md +145 -158
  99. package/docs/workspaces.md +137 -215
  100. package/lib/automations.mjs +21 -6
  101. package/lib/core.mjs +226 -74
  102. package/lib/instance-events.mjs +1 -1
  103. package/lib/instance-inspect.mjs +109 -34
  104. package/lib/instance-lifecycle.mjs +14 -1
  105. package/lib/instance-resolution.mjs +26 -27
  106. package/lib/launch-preference.mjs +87 -0
  107. package/lib/materialize.mjs +3 -3
  108. package/lib/packages.mjs +1 -1
  109. package/lib/resolve.mjs +30 -88
  110. package/lib/schedule.mjs +1 -1
  111. package/lib/teams-verbs.mjs +195 -0
  112. package/lib/teams.mjs +190 -0
  113. package/lib/triggers.mjs +2 -2
  114. package/lib/workspace.mjs +54 -147
  115. package/package-catalog.json +10 -16
  116. package/package.json +1 -3
  117. package/skills/oats-getting-started/SKILL.md +25 -13
  118. package/capabilities/oats-authoring/LICENSE +0 -21
  119. package/capabilities/oats-authoring/oats-package.json +0 -11
  120. package/capabilities/oats-authoring/oats.json +0 -12
  121. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
  122. package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
  123. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
  124. package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
  125. package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1338
  126. package/capabilities/oats-aweb/injects/aweb.md +0 -47
  127. package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -356
  128. package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
  129. package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
  130. package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
  131. package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
  132. package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
  133. package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
  134. package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
  135. package/capabilities/oats-aweb/oats.json +0 -208
  136. package/capabilities/oats-aweb/skills/LICENSE +0 -21
  137. package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
  138. package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
  139. package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
  140. package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
  141. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
  142. package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
  143. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -216
  144. package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
  145. package/capabilities/oats-jira/injects/jira.md +0 -10
  146. package/capabilities/oats-jira/oats.json +0 -22
  147. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
  148. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
  149. package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
  150. package/capabilities/oats-linear/injects/linear.md +0 -8
  151. package/capabilities/oats-linear/oats.json +0 -24
  152. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
  153. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
  154. package/capabilities/oats-okf/bin/oats-okf.mjs +0 -209
  155. package/capabilities/oats-okf/injects/okf.md +0 -42
  156. package/capabilities/oats-okf/lib/binding-wire.mjs +0 -348
  157. package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
  158. package/capabilities/oats-okf/lib/config.mjs +0 -124
  159. package/capabilities/oats-okf/lib/consult.mjs +0 -518
  160. package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
  161. package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
  162. package/capabilities/oats-okf/lib/inspection.mjs +0 -119
  163. package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
  164. package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
  165. package/capabilities/oats-okf/lib/io.mjs +0 -118
  166. package/capabilities/oats-okf/lib/migration.mjs +0 -137
  167. package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
  168. package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
  169. package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
  170. package/capabilities/oats-okf/lib/sources.mjs +0 -424
  171. package/capabilities/oats-okf/lib/stores.mjs +0 -473
  172. package/capabilities/oats-okf/lib/worker.mjs +0 -497
  173. package/capabilities/oats-okf/oats.json +0 -148
  174. package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
  175. package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
  176. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
  177. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
  178. package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
  179. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
  180. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
  181. package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
  182. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
  183. package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
  184. package/capabilities/oats-okf-harvest/oats.json +0 -26
  185. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
  186. package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
  187. package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
  188. package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  189. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
  190. package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
  191. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
  192. package/capabilities/oats-okf-maintenance/oats.json +0 -21
  193. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
  194. package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
  195. package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
  196. package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  197. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -146
  198. package/capabilities/oats-review/injects/review.md +0 -69
  199. package/capabilities/oats-review/oats.json +0 -10
  200. package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
  201. package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
  202. package/docs/conventions.md +0 -90
  203. package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
  204. package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
  205. package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
  206. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
  207. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
  208. package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
  209. package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
  210. package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
  211. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
  212. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
  213. package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
  214. package/docs/design/2026-09-15-captured-dispatch.md +0 -127
  215. package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
  216. package/docs/design/2026-09-15-package-preparation.md +0 -100
  217. package/docs/design/2026-09-15-portable-data-contract.md +0 -121
  218. package/docs/design/2026-09-15-portable-declarations.md +0 -189
  219. package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
  220. package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
  221. package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
  222. package/docs/design/2026-09-15-source-observation.md +0 -119
  223. package/docs/design/2026-09-16-captured-admission.md +0 -77
  224. package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
  225. package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
  226. package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
  227. package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
  228. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
  229. package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
  230. package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
  231. package/docs/design/2026-09-16-portable-onboarding.md +0 -179
  232. package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
  233. package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
  234. package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
  235. package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
  236. package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
  237. package/docs/design/2026-09-17-captured-native-start.md +0 -58
  238. package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
  239. package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
  240. package/docs/design/2026-09-17-public-captured-start.md +0 -108
  241. package/docs/design/2026-09-17-public-prepare-request.md +0 -90
  242. package/docs/design/2026-09-18-captured-pi-host.md +0 -205
  243. package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
  244. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
  245. package/docs/design/2026-09-20-redesign-program-board.md +0 -142
  246. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
  247. package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
  248. package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
  249. package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
  250. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
  251. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
  252. package/docs/design/2026-09-24-phase-d-plan.md +0 -305
  253. package/docs/design/2026-09-25-teams-contract.md +0 -258
  254. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
  255. package/docs/design/desktop-ux-plan.md +0 -362
  256. package/docs/design/launch-configurations.md +0 -168
  257. package/docs/design/okf-mirror-provenance.md +0 -105
  258. package/docs/design/operations-contract.md +0 -141
  259. package/docs/oats-member.schema.json +0 -38
  260. package/skills/integration-authoring/SKILL.md +0 -84
  261. package/skills/oats-support/SKILL.md +0 -79
  262. package/skills/skill-craft/SKILL.md +0 -109
  263. package/skills/soul-craft/SKILL.md +0 -116
@@ -1,424 +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. */
112
- function harvestOff(home,sw,extra={}) {
113
- const record={version:1,harvest:'off',reason:sw.reason,rows:sw.rows,warnings:sw.warnings,at:new Date().toISOString()};
114
- atomic(instanceRecordPath(home),JSON.stringify(record,null,2)+'\n');
115
- ensureInstanceKnowledge(home);
116
- return {harvestOff:true,switch:sw,home,...extra};
117
- }
118
- export const harvestOffRecord = home => fs.existsSync(instanceRecordPath(home))?readJSON(instanceRecordPath(home)):null;
119
- /** The source's tasks provider (its instance.json tasks-layer capability), or null. */
120
- function tasksProvider(meta) {
121
- const row=Array.isArray(meta?.capabilities)?meta.capabilities.find(c=>c && c.layer==='tasks' && typeof c.id==='string'):null;
122
- return row?row.id:null;
123
- }
124
- function capturedOwner(bindings,owner,identity) {
125
- const file=join(bindings.stateDir,'owners.json'),row={schemaVersion:1,kind:'captured-qualified-soul',identity};
126
- withLock(join(bindings.stateDir,'owners.lock'),()=>{
127
- 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;
128
- if(typeof prior==='string') fail('E_MIGRATION','legacy owner registry evidence requires explicit qualified-identity migration');
129
- 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');
130
- if(prior===undefined) {owners[owner]=row;save(file,owners);}
131
- });
132
- }
133
- function sameCapturedReceipt(source,receipt,validated) {
134
- return source.registration?.schemaVersion===1 && source.registration.kind==='captured'
135
- && source.home===validated.home && source.work===validated.work && source.context===validated.context
136
- && source.agent===receipt.agent && source.instance===receipt.instance && source.role===receipt.role
137
- && sameJson(source.sourceIdentity,validated.sourceIdentity) && sameJson(source.executionBinding,validated.execution)
138
- && sameJson(source.responsibleHuman,validated.responsibleHuman) && sameJson(source.providerBinding,receipt.binding);
139
- }
140
- export function registerCaptured(home,receipt) {
141
- home=safePath(home);const captured=validateCapturedReceipt(home,receipt),invocation=loadInvocationKnowledgeBinding();
142
- if(invocation.kind==='captured' && !sameJson(invocation.binding,receipt.binding)) fail('E_SOURCE','lifecycle receipt binding differs from invocation snapshot');
143
- if(receipt.kind==='helper') return {skipped:'service'};
144
- if(fs.existsSync(markerPath(home))) {
145
- const source=homeSource(home);if(!sameCapturedReceipt(source,receipt,captured)) fail('E_SOURCE','captured registration receipt differs from durable source');
146
- return finishRegistration(source);
147
- }
148
- 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');
149
- // The captured path takes its authority from the frozen binding and never
150
- // reads live settings or the soul, so the 4.0.0 harvest switch (a live host
151
- // setting) does not apply here. No released kernel drives this path.
152
- if(overlaps(home,captured.context) && captured.context.startsWith(home)) fail('E_PATH','captured deployment context cannot be in disposable home');
153
- const {file:bindingsFile,...bindingsDoc}=captured.binding.bindings;
154
- const bindings={file:bindingsFile,...validateBindings(bindingsDoc,bindingsFile,{sourceHome:home,sourceWork:captured.work})};
155
- fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});capturedOwner(bindings,captured.binding.owner,captured.sourceIdentity);
156
- const id=randomUUID(),dir=join(bindings.stateDir,'sources',id),source={version:1,id,home,work:captured.work,context:captured.context,
157
- agent:receipt.agent,instance:receipt.instance,owner:captured.binding.owner,decl:captured.binding.decl,role:receipt.role,bindings,
158
- bindingFingerprint:bindingFingerprint(bindings),execution:captured.binding.execution,providerBinding:JSON.parse(JSON.stringify(receipt.binding)),
159
- registration:{schemaVersion:1,kind:'captured'},sourceIdentity:captured.sourceIdentity,executionBinding:captured.execution,
160
- responsibleHuman:captured.responsibleHuman,created:new Date().toISOString()};
161
- const file=join(dir,'source.json');fs.mkdirSync(dir,{recursive:true,mode:0o700});
162
- try {
163
- source.acceptedView=acceptedResolution(bindings,source.decl);source.acceptedNodes=acceptedNodes(source.acceptedView);
164
- save(file,source);save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
165
- save(markerPath(home),{version:1,id,source:file});
166
- } catch(error) {
167
- if(!fs.existsSync(markerPath(home))) fs.rmSync(dir,{recursive:true,force:true});
168
- throw error;
169
- }
170
- return finishRegistration({...source,file});
171
- }
172
-
173
- export function register(home) {
174
- home=safePath(home);
175
- const invocation=loadInvocationKnowledgeBinding();
176
- if(invocation.kind==='captured') {
177
- if(fs.existsSync(markerPath(home))) return finishRegistration(homeSource(home));
178
- fail('E_MIGRATION','captured provider binding requires durable captured-source registration; current soul/config fallback is forbidden');
179
- }
180
- if(service(home)) return {skipped:'service'};
181
- if(fs.existsSync(markerPath(home))) return finishRegistration(homeSource(home));
182
- 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');
183
- const meta=fs.existsSync(join(home,'instance.json'))?readJSON(join(home,'instance.json')):{};
184
- 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');
185
- const soul=fs.realpathSync(process.env.OATS_SOUL);
186
- const work=fs.existsSync(join(home,'work'))?fs.realpathSync(join(home,'work')):join(home,'work');
187
- const decl=declaration(soul);
188
- const soulId=process.env.OATS_SOUL_ID || null;
189
- const bindings=loadBindings(undefined,{sourceHome:home,sourceWork:work});
190
- const context=fs.realpathSync(process.env.OATS_CONTEXT || meta.repo || fail('E_CONFIG','source requires durable config context'));
191
- if(overlaps(home,context) && context.startsWith(home)) fail('E_PATH','config context cannot be in disposable home');
192
- const agent=process.env.OATS_AGENT || meta.agent;
193
- const instance=process.env.OATS_INSTANCE || meta.instance;
194
- if(!agent || !instance) fail('E_SOURCE','source instance/agent required');
195
- const sw=harvestSwitch({settings:settings(),soulDir:soul});
196
- if(sw.effective!=='on') {acceptedResolution(bindings,decl);return harvestOff(home,sw,{decl});}
197
- fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
198
- const ownersFile=join(bindings.stateDir,'owners.json');
199
- const id=randomUUID(); const dir=join(bindings.stateDir,'sources',id);
200
- // Copy only the role document, never instance.json wholesale, launch recipes,
201
- // environment, credentials, source worktree, or third-party message stores.
202
- const roleFile=safePath(join(soul,'AGENTS.md'));
203
- const role=fs.existsSync(roleFile)?fs.readFileSync(roleFile,'utf8'):'';
204
- if(Buffer.byteLength(role)>128*1024) fail('E_SOURCE','role document exceeds 128KiB; provide a concise role before registering');
205
- 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()};
206
- const file=join(dir,'source.json');
207
- fs.mkdirSync(dir,{recursive:true,mode:0o700});
208
- try {
209
- source.acceptedView=acceptedResolution(bindings,decl);
210
- pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
211
- source.acceptedNodes=acceptedNodes(source.acceptedView);
212
- save(file,source);
213
- save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
214
- save(markerPath(home),{version:1,id,source:file});
215
- } catch(e) {
216
- // Until the pointer is durable no capture or schedule can reference these
217
- // files. Leave an installed pointer's state intact even if fsync failed.
218
- if(!fs.existsSync(markerPath(home))) fs.rmSync(dir,{recursive:true,force:true});
219
- throw e;
220
- }
221
- return finishRegistration({...source,file});
222
- }
223
- /** Pin a stable owner id to the soul it identifies. The kernel names a soul by
224
- * identity (OATS_SOUL_ID: repository key plus soul name) so the pin survives
225
- * the per-commit soul copies a workspace deployment materializes; a classic
226
- * soul keeps the resolved path. A row written by an earlier version as a path
227
- * under agents/<same soul name>/(soul|souls/<commit>) is rewritten to the
228
- * identity once; any other mismatch is a different soul and is refused. */
229
- export function pinOwner(ownersFile,owner,{id,soulName,path}) {
230
- const value=id || path;
231
- return withLock(join(dirname(ownersFile),'owners.lock'),()=>{
232
- const owners=fs.existsSync(ownersFile)?readJSON(ownersFile):{};
233
- if(!obj(owners)) fail('E_OWNER','invalid owner registry');
234
- const prior=Object.hasOwn(owners,owner)?owners[owner]:undefined;
235
- const samePath=typeof prior==='string' && new RegExp(`/agents/${soulName.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}/(soul|souls/[^/]+)$`).test(prior);
236
- 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.`);
237
- if(prior!==value) {owners[owner]=value;save(ownersFile,owners);}
238
- return value;
239
- });
240
- }
241
- function enqueue(source,status,payload) {
242
- const id=hash(payload);const path=join(dirname(source.file),'inputs',`${id}.json`);
243
- if(!fs.existsSync(path)) save(path,payload);
244
- if(!status.captured.inputs.includes(id)) status.captured.inputs.push(id);
245
- return id;
246
- }
247
- export function input(source,id) {
248
- if(!/^[0-9a-f]{64}$/.test(id)) fail('E_INPUT','bad input identity');
249
- const value=readJSON(join(dirname(source.file),'inputs',`${id}.json`));
250
- if(hash(value)!==id) fail('E_INPUT','durable evidence hash mismatch');return value;
251
- }
252
- /** Take a drained, retired source's job out of the kernel scheduler. The job is
253
- * first switched off, then removed: the definition it carried is kept as
254
- * evidence in this source's own schedule.json, so nothing about the run is
255
- * lost, and `oats schedule list` stops accumulating dead okf-<id> rows. A job
256
- * that is still running (or has unresolved effects) stays disabled and is
257
- * removed on the worker's next settle. Idempotent; a scheduler failure is
258
- * recorded, never thrown — the evidence is already safe. */
259
- /** okf 4.0.1 #6: the switch for an already registered source, re-read now: the
260
- * deployment setting AND the soul's opt-out (the current OATS_SOUL the kernel
261
- * hands run-source/retire, else the soul directory recorded at registration). */
262
- export function sourceSwitch(source) {
263
- const current = process.env.OATS_SOUL && fs.existsSync(process.env.OATS_SOUL) ? process.env.OATS_SOUL : null;
264
- return harvestSwitch({ settings: settings(), soulDir: current || source.soulDir || undefined });
265
- }
266
- /** Retire a registered source whose harvest is now off: no final capture; the
267
- * inputs already in custody stay; the schedule is settled as for any retire. */
268
- export function retireHarvestOff(source, sw) {
269
- updateStatus(source, current => {
270
- current.retired = true; current.retiredAt = new Date().toISOString(); current.harvestOff = { reason: sw.reason, at: current.retiredAt };
271
- // As a final capture would: the schedule stays only while earlier inputs await processing.
272
- current.auto = current.auto && !current.captured.inputs.every(id => current.processed.includes(id));
273
- });
274
- return { retired: true, reason: 'harvest-off', switch: sw.reason, source: source.file, schedule: settleRetiredSchedule(source) };
275
- }
276
- export function settleRetiredSchedule(source) {
277
- const status=loadStatus(source);
278
- if(status.schedule?.removed===true) return {status:'already-removed',id:status.schedule.id};
279
- if(!status.retired || status.auto || status.schedule?.id===undefined) return {status:'kept'};
280
- const id=status.schedule.id;
281
- const mark=(patch)=>updateStatus(source,current=>{current.schedule={...current.schedule,...patch};});
282
- try {
283
- if(status.schedule.settled!==true) {
284
- oats(['schedule','disable',id,'--dir',source.context,'--json'],source.context);
285
- mark({settled:true,settledAt:new Date().toISOString(),settleError:undefined});
286
- }
287
- } catch(e) {
288
- if(e.code!=='E_SCHEDULE_UNKNOWN') {mark({settled:false,settleError:e.message});return {status:'disable-failed',id};}
289
- }
290
- try {
291
- oats(['schedule','remove',id,'--dir',source.context,'--json'],source.context);
292
- mark({removed:true,removedAt:new Date().toISOString(),removeError:undefined});
293
- return {status:'removed',id};
294
- } catch(e) {
295
- if(e.code==='E_SCHEDULE_UNKNOWN') {mark({removed:true,removedAt:new Date().toISOString(),removeError:undefined});return {status:'already-removed',id};}
296
- mark({removed:false,removeError:e.message});
297
- return {status:e.code==='E_SCHEDULE_RUNNING'?'disabled-pending-removal':'remove-failed',id};
298
- }
299
- }
300
- export function capture(source,{final=false,deadlineMs=85000}={}) {
301
- return withLock(join(dirname(source.file),'capture.lock'),()=>{
302
- const status=loadStatus(source);
303
- if(status.retired) return {status:'complete',complete:true,retired:true};
304
- if(!fs.existsSync(markerPath(source.home))) fail('E_SOURCE','source home gone without final capture; existing evidence is retained');
305
- if(homeSource(source.home).id!==source.id) fail('E_SOURCE','source name reused');
306
- const start=Date.now(); const meta=fs.existsSync(join(source.home,'instance.json'))?readJSON(join(source.home,'instance.json')):{};
307
- const noLaunch=meta.launched!==true;status.launchObserved=!noLaunch;
308
- // Notes AND record, every pass. Note content versions remain captured even
309
- // when the live file is rewritten while a worker is judging a prior version.
310
- const notes=join(source.home,'notes');
311
- if(fs.existsSync(notes)) for(const [name,b64] of Object.entries(tree(notes))) {
312
- if(!name.endsWith('.md')) continue;
313
- const text=Buffer.from(b64,'base64').toString('utf8');const key=hash({name,text});
314
- if(status.captured.notes.includes(key)) continue;
315
- enqueue(source,status,{version:1,kind:'note',name,contentHash:hash(text),text});
316
- status.captured.notes.push(key);saveCapture(source,status);
317
- }
318
- let report;
319
- try {
320
- report=oats(['capture','--home',source.home,'--quiet'],source.context,{native:true,timeout:Math.min(deadlineMs,60000)});
321
- if(!Array.isArray(report.sessions)) fail('E_CAPTURE','capture response has no sessions');
322
- for(const session of report.sessions) {
323
- if(!session.thread || !session.lastTurnId) continue;
324
- let after=status.captured.threads[session.thread] || null;
325
- while(after!==session.lastTurnId) {
326
- if(Date.now()-start>deadlineMs) fail('E_CAPTURE','capture deadline: backlog preserved; retire must retry');
327
- const args=['recall','--thread',session.thread,'--until',session.lastTurnId,'--limit','60','--json','--ids-only'];
328
- if(after) args.push('--after',after);
329
- const remainingTime=()=>{
330
- const remaining=deadlineMs-(Date.now()-start);
331
- if(remaining<=0) fail('E_CAPTURE','capture deadline: backlog preserved; retire must retry');
332
- return Math.max(1000,remaining);
333
- };
334
- let plan;
335
- try { plan=oats(args,source.context,{native:true,timeout:remainingTime()}); }
336
- catch(e) { if(after && /--after: no turn/.test(e.message)) {after=null;continue;} throw e; }
337
- if(!Array.isArray(plan.turns) || !plan.turns.length || plan.turns.length>60) fail('E_CAPTURE','capture/recall boundaries disagree');
338
- // Plan BEFORE requesting text. Native bytes describe each turn in
339
- // pretty JSON. Nested response indentation adds bytes: reserve 3x
340
- // that estimate plus envelope space, always below the 16MiB pipe.
341
- // The 96k target is soft only for one individually legal turn.
342
- let offset=0;
343
- while(offset<plan.turns.length) {
344
- const window=[];let bytes=0;
345
- for(const t of plan.turns.slice(offset)) {
346
- 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');
347
- if(window.length && bytes+t.bytes>96000) break;
348
- 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');
349
- window.push(t);bytes+=t.bytes;
350
- }
351
- const until=window.at(-1).id;
352
- const read=['recall','--thread',session.thread,'--until',until,'--limit',String(window.length),'--json'];
353
- if(after) read.push('--after',after);
354
- const full=oats(read,source.context,{native:true,timeout:remainingTime()});
355
- if(!Array.isArray(full.turns) || full.turns.length!==window.length || full.remaining!==0) fail('E_CAPTURE','planned record window changed; retain source and retry');
356
- const turns=full.turns.map((t,i)=>{
357
- const turn={id:t.id,ts:t.ts,thread:t.thread,kind:t.kind,source:t.source,text:t.text};
358
- 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');
359
- if(Buffer.byteLength(JSON.stringify(turn))>1024*1024) fail('E_CAPTURE','single captured turn exceeds 1MiB; input retained at source, manual intervention required');
360
- return turn;
361
- });
362
- // Commit evidence before its cursor; failures retain earlier windows.
363
- enqueue(source,status,{version:1,kind:'record',thread:session.thread,after,until,turns});
364
- after=until;status.captured.threads[session.thread]=after;saveCapture(source,status);
365
- offset+=window.length;
366
- }
367
- if(plan.remaining===0 && after!==session.lastTurnId) fail('E_CAPTURE','final record boundary missing');
368
- }
369
- }
370
- status.lastCapture={status:report.status,complete:report.complete===true,ignored:report.ignored||0,at:new Date().toISOString()};
371
- if(report.complete!==true) fail('E_CAPTURE',`capture ${report.status || 'uncertified'}: retain source and retry`);
372
- if(final) {
373
- status.retired=true;status.retiredAt=new Date().toISOString();
374
- // A retired source whose every captured input is already processed has
375
- // no further work: its schedule is switched off and removed from the
376
- // kernel scheduler (settleRetiredSchedule); the definition stays as
377
- // evidence in this source's schedule.json. Anything still pending keeps
378
- // the job enabled until the worker drains it (see worker.mjs).
379
- const drained=status.captured.inputs.every(id=>status.processed.includes(id));
380
- status.auto=status.auto && !noLaunch && !drained;
381
- }
382
- saveCapture(source,status);return {...status.lastCapture,inputs:status.captured.inputs.length};
383
- } catch(e) {
384
- status.lastCapture={status:'incomplete',complete:false,error:e.message,at:new Date().toISOString()}; saveCapture(source,status);throw e;
385
- }
386
- });
387
- }
388
- export function scheduleSource(source) {
389
- // A retired, drained source whose job was already taken out of the scheduler
390
- // has nothing left to run: do not recreate the job (retire is re-entrant).
391
- const current=loadStatus(source);
392
- if(current.retired && current.schedule?.removed===true) return current.schedule.result;
393
- const captured=source.registration?.schemaVersion===1 && source.registration.kind==='captured';
394
- const argv=captured?['oats','okf','run-source','--source',source.file,'--deployment',source.executionBinding.deployment,'--resolution',source.executionBinding.resolution.id,'--json']
395
- :['oats','okf','run-source','--source',source.file,'--soul',source.agent,'--json'];
396
- const spec={id:`okf-${source.id}`,kind:'command',enabled:current.auto,cron:source.bindings.cron,tz:source.bindings.tz,cwd:source.context,argv,
397
- ...(captured?{definitionVersion:2,recurrencePolicy:'capture',responsibleHuman:source.responsibleHuman}: {})};
398
- const file=join(dirname(source.file),'schedule.json');
399
- try {
400
- save(file,spec);
401
- let result;
402
- try {result=oats(['schedule','add',spec.id,'--file',file,'--dir',source.context,'--json'],source.context);}
403
- catch(e) {
404
- // Never overwrite a colliding job or re-enable an operator-disabled job.
405
- // Retry after an uncertain add must verify the actual definition, not a
406
- // local receipt. A deleted definition is recreated by the add above.
407
- if(e.code!=='E_SCHEDULE_EXISTS') throw e;
408
- result=oats(['schedule','show',spec.id,'--dir',source.context,'--json'],source.context);
409
- }
410
- const actual=result?.schedule;
411
- 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');
412
- if(captured) {
413
- const responsible=actual.execution && Object.hasOwn(actual.execution,'responsibleHuman')?actual.execution.responsibleHuman
414
- :Object.hasOwn(actual,'responsibleHuman')?actual.responsibleHuman:undefined;
415
- if(!sameJson(responsible,spec.responsibleHuman)) fail('E_SCHEDULE','captured source schedule responsible human differs');
416
- 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');
417
- }
418
- // A job already settled off for a retired, drained source stays settled:
419
- // registration re-verifies the definition but does not forget the switch-off.
420
- 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;
421
- } catch(e) {
422
- updateStatus(source,status=>{status.schedule={...(status.schedule || {}),id:spec.id,status:'failed',error:e.message};});throw e;
423
- }
424
- }