@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.
- package/README.md +12 -6
- package/bin/oats.mjs +194 -50
- package/docs/capabilities.md +160 -171
- package/docs/capability-manifest.schema.json +6 -11
- package/docs/configuration.md +213 -64
- package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
- package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
- package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
- package/docs/design/2026-09-27-team-model-v2.md +97 -117
- package/docs/design/2026-09-28-automations-trust.md +38 -0
- package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
- package/docs/design/HISTORY.md +65 -0
- package/docs/design/README.md +23 -54
- package/docs/desktop-cli-api.md +1787 -1777
- package/docs/desktop.md +30 -91
- package/docs/execution-targets.md +146 -292
- package/docs/first-team.md +31 -17
- package/docs/implementation.md +77 -288
- package/docs/integrations.md +118 -320
- package/docs/knowledge-capability-authoring.md +25 -52
- package/docs/knowledge-reference/acceptance.md +3 -3
- package/docs/knowledge-reference/adoption.md +1 -1
- package/docs/knowledge-reference/harvester.md +2 -2
- package/docs/knowledge-reference/package-craft.md +3 -3
- package/docs/knowledge-reference/provider-mapping.md +3 -6
- package/docs/knowledge-reference/reader-capture.md +3 -3
- package/docs/knowledge-theory.md +62 -166
- package/docs/knowledge.md +225 -404
- package/docs/layers.md +42 -97
- package/docs/oats-local.schema.json +58 -5
- package/docs/oats-membership.schema.json +1 -8
- package/docs/oats-package.schema.json +5 -5
- package/docs/oats-workspace.schema.json +8 -22
- package/docs/official-catalog.md +25 -28
- package/docs/packages.md +45 -63
- package/docs/plans/0.30-close-out.md +83 -0
- package/docs/release-lane.md +82 -0
- package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
- package/docs/release-notes/v0.19.0.md +48 -147
- package/docs/release-notes/v0.19.1.md +2 -3
- package/docs/release-notes/v0.19.3.md +2 -15
- package/docs/release-notes/v0.20.0.md +0 -15
- package/docs/release-notes/v0.22.0.md +71 -138
- package/docs/release-notes/v0.22.1.md +42 -90
- package/docs/release-notes/v0.22.10.md +1 -1
- package/docs/release-notes/v0.22.11.md +1 -47
- package/docs/release-notes/v0.22.12.md +4 -13
- package/docs/release-notes/v0.22.13.md +1 -42
- package/docs/release-notes/v0.22.14.md +3 -11
- package/docs/release-notes/v0.22.15.md +1 -46
- package/docs/release-notes/v0.22.16.md +6 -8
- package/docs/release-notes/v0.22.18.md +1 -99
- package/docs/release-notes/v0.22.19.md +3 -14
- package/docs/release-notes/v0.22.2.md +6 -15
- package/docs/release-notes/v0.22.3.md +0 -1
- package/docs/release-notes/v0.22.4.md +1 -14
- package/docs/release-notes/v0.22.5.md +2 -12
- package/docs/release-notes/v0.22.6.md +0 -3
- package/docs/release-notes/v0.23.0.md +9 -25
- package/docs/release-notes/v0.23.1.md +9 -25
- package/docs/release-notes/v0.23.2.md +2 -4
- package/docs/release-notes/v0.24.0.md +56 -97
- package/docs/release-notes/v0.24.1.md +7 -11
- package/docs/release-notes/v0.24.10.md +34 -45
- package/docs/release-notes/v0.24.11.md +12 -20
- package/docs/release-notes/v0.24.12.md +35 -48
- package/docs/release-notes/v0.24.13.md +34 -41
- package/docs/release-notes/v0.24.2.md +9 -13
- package/docs/release-notes/v0.24.3.md +7 -11
- package/docs/release-notes/v0.24.4.md +6 -6
- package/docs/release-notes/v0.24.5.md +6 -10
- package/docs/release-notes/v0.24.6.md +2 -5
- package/docs/release-notes/v0.24.7.md +46 -75
- package/docs/release-notes/v0.24.8.md +58 -96
- package/docs/release-notes/v0.24.9.md +38 -54
- package/docs/release-notes/v0.25.0.md +59 -76
- package/docs/release-notes/v0.25.1.md +57 -81
- package/docs/release-notes/v0.25.2.md +51 -70
- package/docs/release-notes/v0.25.3.md +11 -13
- package/docs/release-notes/v0.25.4.md +9 -13
- package/docs/release-notes/v0.25.5.md +3 -5
- package/docs/release-notes/v0.25.6.md +20 -29
- package/docs/release-notes/v0.25.7.md +5 -7
- package/docs/release-notes/v0.25.8.md +26 -39
- package/docs/release-notes/v0.26.0.md +175 -646
- package/docs/release-notes/v0.27.0.md +4 -5
- package/docs/release-notes/v0.27.1.md +4 -6
- package/docs/release-notes/v0.27.2.md +1 -1
- package/docs/release-notes/v0.28.0.md +57 -124
- package/docs/release-notes/v0.29.0.md +89 -208
- package/docs/release-notes/v0.29.1.md +1 -1
- package/docs/release-notes/v0.29.2.md +3 -4
- package/docs/release-notes/v0.30.0.md +205 -0
- package/docs/release-notes/v0.30.1.md +123 -0
- package/docs/schedules.md +280 -363
- package/docs/servers.md +99 -117
- package/docs/soul.schema.json +2 -9
- package/docs/souls-and-instances.md +145 -158
- package/docs/workspaces.md +137 -215
- package/lib/automations.mjs +21 -6
- package/lib/core.mjs +226 -74
- package/lib/instance-events.mjs +1 -1
- package/lib/instance-inspect.mjs +109 -34
- package/lib/instance-lifecycle.mjs +14 -1
- package/lib/instance-resolution.mjs +26 -27
- package/lib/launch-preference.mjs +87 -0
- package/lib/materialize.mjs +3 -3
- package/lib/packages.mjs +1 -1
- package/lib/resolve.mjs +30 -88
- package/lib/schedule.mjs +1 -1
- package/lib/teams-verbs.mjs +195 -0
- package/lib/teams.mjs +190 -0
- package/lib/triggers.mjs +2 -2
- package/lib/workspace.mjs +54 -147
- package/package-catalog.json +10 -16
- package/package.json +1 -3
- package/skills/oats-getting-started/SKILL.md +25 -13
- package/capabilities/oats-authoring/LICENSE +0 -21
- package/capabilities/oats-authoring/oats-package.json +0 -11
- package/capabilities/oats-authoring/oats.json +0 -12
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
- package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
- package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1338
- package/capabilities/oats-aweb/injects/aweb.md +0 -47
- package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -356
- package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
- package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
- package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
- package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
- package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
- package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
- package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
- package/capabilities/oats-aweb/oats.json +0 -208
- package/capabilities/oats-aweb/skills/LICENSE +0 -21
- package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
- package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
- package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
- package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
- package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -216
- package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
- package/capabilities/oats-jira/injects/jira.md +0 -10
- package/capabilities/oats-jira/oats.json +0 -22
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
- package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
- package/capabilities/oats-linear/injects/linear.md +0 -8
- package/capabilities/oats-linear/oats.json +0 -24
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
- package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
- package/capabilities/oats-okf/bin/oats-okf.mjs +0 -209
- package/capabilities/oats-okf/injects/okf.md +0 -42
- package/capabilities/oats-okf/lib/binding-wire.mjs +0 -348
- package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
- package/capabilities/oats-okf/lib/config.mjs +0 -124
- package/capabilities/oats-okf/lib/consult.mjs +0 -518
- package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
- package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
- package/capabilities/oats-okf/lib/inspection.mjs +0 -119
- package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
- package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
- package/capabilities/oats-okf/lib/io.mjs +0 -118
- package/capabilities/oats-okf/lib/migration.mjs +0 -137
- package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
- package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
- package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
- package/capabilities/oats-okf/lib/sources.mjs +0 -424
- package/capabilities/oats-okf/lib/stores.mjs +0 -473
- package/capabilities/oats-okf/lib/worker.mjs +0 -497
- package/capabilities/oats-okf/oats.json +0 -148
- package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
- package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
- package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
- package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
- package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
- package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
- package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
- package/capabilities/oats-okf-harvest/oats.json +0 -26
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
- package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
- package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
- package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
- package/capabilities/oats-okf-maintenance/oats.json +0 -21
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
- package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -146
- package/capabilities/oats-review/injects/review.md +0 -69
- package/capabilities/oats-review/oats.json +0 -10
- package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
- package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
- package/docs/conventions.md +0 -90
- package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
- package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
- package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
- package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
- package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
- package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
- package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
- package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
- package/docs/design/2026-09-15-captured-dispatch.md +0 -127
- package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
- package/docs/design/2026-09-15-package-preparation.md +0 -100
- package/docs/design/2026-09-15-portable-data-contract.md +0 -121
- package/docs/design/2026-09-15-portable-declarations.md +0 -189
- package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
- package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
- package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
- package/docs/design/2026-09-15-source-observation.md +0 -119
- package/docs/design/2026-09-16-captured-admission.md +0 -77
- package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
- package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
- package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
- package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
- package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
- package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
- package/docs/design/2026-09-16-portable-onboarding.md +0 -179
- package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
- package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
- package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
- package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
- package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
- package/docs/design/2026-09-17-captured-native-start.md +0 -58
- package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
- package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
- package/docs/design/2026-09-17-public-captured-start.md +0 -108
- package/docs/design/2026-09-17-public-prepare-request.md +0 -90
- package/docs/design/2026-09-18-captured-pi-host.md +0 -205
- package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
- package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
- package/docs/design/2026-09-20-redesign-program-board.md +0 -142
- package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
- package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
- package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
- package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
- package/docs/design/2026-09-24-phase-d-plan.md +0 -305
- package/docs/design/2026-09-25-teams-contract.md +0 -258
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
- package/docs/design/desktop-ux-plan.md +0 -362
- package/docs/design/launch-configurations.md +0 -168
- package/docs/design/okf-mirror-provenance.md +0 -105
- package/docs/design/operations-contract.md +0 -141
- package/docs/oats-member.schema.json +0 -38
- package/skills/integration-authoring/SKILL.md +0 -84
- package/skills/oats-support/SKILL.md +0 -79
- package/skills/skill-craft/SKILL.md +0 -109
- 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
|
-
}
|