@awebai/oats 0.29.3 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/bin/oats.mjs +203 -54
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +538 -204
- package/capabilities/oats-aweb/injects/aweb.md +1 -1
- package/capabilities/oats-aweb/lib/binding-wire.mjs +31 -22
- package/capabilities/oats-aweb/oats.json +5 -12
- package/capabilities/oats-aweb/skills/VENDORED.md +4 -4
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +1 -1
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +83 -13
- package/capabilities/oats-code-review/injects/reviewer.md +26 -0
- package/capabilities/oats-code-review/oats.json +16 -0
- package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +66 -0
- package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +30 -0
- package/capabilities/oats-code-review/skills/security-review/SKILL.md +56 -0
- package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +34 -0
- package/capabilities/oats-developer/injects/developer.md +38 -0
- package/capabilities/oats-developer/oats.json +17 -0
- package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +43 -0
- package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +47 -0
- package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +65 -0
- package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +37 -0
- package/capabilities/oats-developer/skills/worktrees/SKILL.md +36 -0
- package/capabilities/oats-engineering-expert/injects/expert.md +37 -0
- package/capabilities/oats-engineering-expert/oats.json +17 -0
- package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +37 -0
- package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +52 -0
- package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +50 -0
- package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +53 -0
- package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +49 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +16 -9
- package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/consult.mjs +26 -4
- package/capabilities/oats-okf/lib/harvest-switch.mjs +16 -3
- package/capabilities/oats-okf/lib/inspection.mjs +26 -7
- package/capabilities/oats-okf/lib/io.mjs +9 -1
- package/capabilities/oats-okf/lib/sources.mjs +34 -3
- package/capabilities/oats-okf/lib/stores.mjs +8 -6
- package/capabilities/oats-okf/lib/worker.mjs +7 -17
- package/capabilities/oats-okf/oats.json +6 -3
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
- package/capabilities/oats-okf-harvest/oats.json +3 -3
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +37 -16
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +6 -1
- package/capabilities/oats-okf-maintenance/oats.json +2 -2
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +17 -2
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
- package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
- package/capabilities/oats-workspace-experts/oats.json +9 -0
- package/docs/capabilities.md +160 -171
- package/docs/capability-manifest.schema.json +7 -10
- 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 +116 -0
- 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 +76 -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 +61 -0
- package/docs/release-lane.md +77 -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.29.4.md +90 -0
- package/docs/release-notes/v0.30.0.md +205 -0
- package/docs/schedules.md +280 -349
- 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 +132 -215
- package/lib/automations.mjs +28 -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 +2 -5
- package/lib/resolve.mjs +29 -87
- package/lib/schedule.mjs +32 -18
- package/lib/teams-verbs.mjs +195 -0
- package/lib/teams.mjs +190 -0
- package/lib/triggers.mjs +53 -17
- package/lib/workspace.mjs +54 -147
- package/package-catalog.json +9 -15
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +25 -13
- 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,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { fs, join, resolve, readJSON, safePath, oats, fail, unlock } from '../lib/io.mjs';
|
|
2
|
+
import { fs, join, resolve, readJSON, safePath, oats, fail, unlock, redactUrls } from '../lib/io.mjs';
|
|
3
3
|
import { loadBindings } from '../lib/config.mjs';
|
|
4
|
-
import { register, registerCaptured, loadInvocationSourceReceipt, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, settleRetiredSchedule, service, markerPath, harvestOffRecord } from '../lib/sources.mjs';
|
|
4
|
+
import { register, registerCaptured, loadInvocationSourceReceipt, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, settleRetiredSchedule, service, markerPath, harvestOffRecord, sourceSwitch, retireHarvestOff, consultSource } from '../lib/sources.mjs';
|
|
5
5
|
import { harvestStatus, setupHarvest } from '../lib/harvest-status.mjs';
|
|
6
6
|
import { settings } from '../lib/config.mjs';
|
|
7
7
|
import { CONSULT } from '../lib/consult.mjs';
|
|
8
8
|
import { runSource, complete, retry, readRun, requireQualifiedHelper } from '../lib/worker.mjs';
|
|
9
9
|
import { initBase, migrate, deliverMigration, cutoverMigration, migrateSource, forgetMigration } from '../lib/migration.mjs';
|
|
10
|
-
import { inspect } from '../lib/inspection.mjs';
|
|
10
|
+
import { inspect, inspectConsultOnly } from '../lib/inspection.mjs';
|
|
11
11
|
import { loadInvocationKnowledgeBinding } from '../lib/binding-wire.mjs';
|
|
12
12
|
import { loadCapturedOkfInvocation, loadOkfSourceReceiptInput, assertOkfInvocationAction, requireOkfAdmittedAction, assertOkfSourceContext, assertOkfRegisteredSourceReplay } from '../lib/invocation-context.mjs';
|
|
13
13
|
const HELP=`oats okf inspect [--home PATH | --source FILE] [--json]
|
|
@@ -98,6 +98,10 @@ else {
|
|
|
98
98
|
return source;
|
|
99
99
|
} catch(error) {if(captured && ['ENOENT','ENOTDIR'].includes(error.code)) fail('E_SOURCE','captured command requires its durable registered source descriptor');throw error;}
|
|
100
100
|
};
|
|
101
|
+
// okf 4.0.3: consultation never depends on harvest. A home spawned with
|
|
102
|
+
// harvest off has no registered source by design; it consults through its
|
|
103
|
+
// soul's declaration and the deployment's bindings (consultSource).
|
|
104
|
+
const consultOnly=!flags.source && !captured && !execution && !fs.existsSync(markerPath(home)) && !!harvestOffRecord(home);
|
|
101
105
|
// Deliberate old registered-source replay is a separate qualified contract,
|
|
102
106
|
// never a way to create a source or synthesize generic admission. A present
|
|
103
107
|
// invalid/unadmitted generic invocation cannot enter this compatibility path.
|
|
@@ -123,7 +127,7 @@ else {
|
|
|
123
127
|
};
|
|
124
128
|
let result;
|
|
125
129
|
if(event==='read') fail('E_REMOVED','okf 4.0.0 removed read: use `oats okf cat --base ALIAS PATH` (same path, text and receipt)');
|
|
126
|
-
if(consult) {const answer=CONSULT[event](src(),flags,positionals);result=answer.result;text=answer.text;}
|
|
130
|
+
if(consult) {const answer=CONSULT[event](consultOnly?consultSource(home):src(),flags,positionals);result=answer.result;text=answer.text;}
|
|
127
131
|
else if(event==='refresh') fail('E_REMOVED','okf 3.0.0 has no per-instance views; index/cat always read the accepted state: run `oats okf index`, then `oats okf cat --base ALIAS PATH`');
|
|
128
132
|
else if(event==='harvest-status') result=harvestStatus({home,flags});
|
|
129
133
|
else if(event==='soul-scaffold') {
|
|
@@ -151,7 +155,10 @@ else {
|
|
|
151
155
|
if(['STATE.md','log.md','notes','.okf-harvest-record.json','.okf-harvest-record.next.json'].some(p=>fs.existsSync(join(home,p)))) fail('E_MIGRATION','unregistered/legacy source has memory; explicitly migrate/register before retirement');
|
|
152
156
|
result={meta:{retired:true,reason:'nothing-to-delete'}};
|
|
153
157
|
} else {
|
|
154
|
-
const s=src()
|
|
158
|
+
const s=src(),sw=sourceSwitch(s);
|
|
159
|
+
// okf 4.0.1 #6: harvest switched off since spawn (deployment or soul) → no final capture.
|
|
160
|
+
if(sw.effective!=='on') result={meta:retireHarvestOff(s,sw),brief:`Harvest is now off (${sw.reason}): no final capture was taken; earlier inputs stay in custody.`};
|
|
161
|
+
else {scheduleSource(s);const r=capture(s,{final:true});const schedule=settleRetiredSchedule(s);result={meta:{retired:r.complete===true,source:s.file,capture:r,schedule},brief:'Final input is in durable custody. Delivery remains asynchronous.'};}
|
|
155
162
|
}
|
|
156
163
|
} else if(event==='harvest') {
|
|
157
164
|
if(capturedHarvest) {
|
|
@@ -169,13 +176,13 @@ else {
|
|
|
169
176
|
// The deployment can switch harvest off after a source registered: the
|
|
170
177
|
// job then captures and processes nothing (the soul's opt-out was already
|
|
171
178
|
// applied at registration). Nothing drains when it is switched back on.
|
|
172
|
-
const source=src();
|
|
173
|
-
result=
|
|
179
|
+
const source=src(),sw=sourceSwitch(source);
|
|
180
|
+
result=sw.effective!=='on'?{status:'harvest-off',source:source.file,reason:`${sw.reason}; nothing was captured`}
|
|
174
181
|
:runSource(source,{manual:!!flags.manual,noLaunch:!!flags['no-launch']});
|
|
175
182
|
}
|
|
176
183
|
else if(event==='complete') {const s=src();if(captured) retainedRun(s);result=complete(s,flags.run,flags.judgment && resolve(flags.judgment));}
|
|
177
184
|
else if(event==='retry') {const s=src();if(captured && !flags.run && !flags.rejudge && !flags.launch && !flags['adopt-home']) retainedRun(s);result=retry(s,{run:flags.run,rejudge:!!flags.rejudge,launch:!!flags.launch,adoptHome:flags['adopt-home']});}
|
|
178
|
-
else if(event==='inspect') result=inspect(src());
|
|
185
|
+
else if(event==='inspect') result=consultOnly?inspectConsultOnly(consultSource(home)):inspect(src());
|
|
179
186
|
else if(event==='setup' && flags.harvest!==undefined) {
|
|
180
187
|
if(flags.source || flags.enable || flags.disable || flags['install-host']) fail('E_USAGE','setup --harvest takes no other setup flag');
|
|
181
188
|
result=setupHarvest(flags.harvest);
|
|
@@ -196,7 +203,7 @@ else {
|
|
|
196
203
|
} else if(event==='unlock') result=unlock(resolve(flags.lock),flags.token);
|
|
197
204
|
else fail('E_USAGE',`unknown command ${event}; see --help`);
|
|
198
205
|
answer=hook?result:{schemaVersion:1,ok:true,result};
|
|
199
|
-
} catch(e) {const code=e.code || 'E_OKF';exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:
|
|
206
|
+
} catch(e) {const code=e.code || 'E_OKF',message=redactUrls(e.message);exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:message}:{})},warning:`oats-okf ${code}: ${message}`}:{schemaVersion:1,ok:false,error:{code,message}};}
|
|
200
207
|
// Consult commands print text unless --json; every other answer is JSON.
|
|
201
208
|
// Let Node drain the pipe; no process.exit after a possibly large answer.
|
|
202
209
|
if(textMode && exit) process.stderr.write(`oats okf ${event}: ${answer.error.code}: ${answer.error.message}\n`);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { TextDecoder } from 'node:util';
|
|
2
|
-
import { validateInvocationShape } from './invocation-shape.mjs';
|
|
3
2
|
import { tmpdir } from 'node:os';
|
|
4
3
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
4
|
import { fs, safePath } from './io.mjs';
|
|
@@ -14,7 +13,7 @@ import {
|
|
|
14
13
|
renderKnowledgeRuntime,
|
|
15
14
|
sameJson,
|
|
16
15
|
} from './portable-binding.mjs';
|
|
17
|
-
import { validateBindings } from './config.mjs';
|
|
16
|
+
import { declaration as soulDeclaration, loadBindings, validateBindings } from './config.mjs';
|
|
18
17
|
import { stageBase, validateBase } from './stores.mjs';
|
|
19
18
|
|
|
20
19
|
export const BINDING_WIRE_LIMITS=Object.freeze({bytes:1024*1024,depth:32,entries:16384});
|
|
@@ -254,14 +253,20 @@ export function loadInvocationKnowledgeBinding(env=process.env) {
|
|
|
254
253
|
// binding.reasons (byte-exact) — the manifest test pins that.
|
|
255
254
|
const checkReasons=Object.freeze({
|
|
256
255
|
'action:not-admitted':'check action is not an admitted knowledge operation',
|
|
257
|
-
'
|
|
256
|
+
'settings:missing':'OKF settings missing bindings-file: set settings.oats.okf bindings-file, or deactivate oats.okf',
|
|
257
|
+
'soul:missing':'OKF soul declaration missing: create okf.json for this soul, or deactivate oats.okf',
|
|
258
|
+
'harvest-runtime:missing':settingMessages['harvest-runtime:missing'],
|
|
259
|
+
'harvest-runtime:invalid':settingMessages['harvest-runtime:invalid'],
|
|
260
|
+
'harvest-model:invalid':settingMessages['harvest-model:invalid'],
|
|
261
|
+
'binding:not-configured':'OKF binding missing or invalid: provision soul OKF declaration and bindings file, or deactivate oats.okf',
|
|
262
|
+
'bindings:invalid':'OKF bindings file missing or invalid: repair configured bindings file, or deactivate oats.okf',
|
|
258
263
|
'bases:too-many':'more than 64 git knowledge bases declared',
|
|
259
264
|
'base:stage-failed':'declared knowledge base could not be staged from its git source',
|
|
260
265
|
'base:not-validated':'declared knowledge base is not a validated knowledge tree',
|
|
261
266
|
'base:owner-unmet':'knowledge base owner or remote custody requirement not met',
|
|
262
267
|
'base:source-mismatch':'staged git source does not match the declared knowledge base',
|
|
263
268
|
'runtime:command-missing':'harvest runtime command is not installed on this host',
|
|
264
|
-
'runtime:not-qualified':'harvest runtime
|
|
269
|
+
'runtime:not-qualified':'harvest runtime is not qualified: install/configure the selected harvest runtime or set harvest off',
|
|
265
270
|
});
|
|
266
271
|
function problem(code,reason) {
|
|
267
272
|
if(reason===undefined) return {code};
|
|
@@ -276,15 +281,40 @@ function providerActionName(action) {
|
|
|
276
281
|
if(typeof action.name==='string' && action.name.startsWith('okf:')) return action.name.slice(4);
|
|
277
282
|
return null;
|
|
278
283
|
}
|
|
284
|
+
function readinessRuntime(settings,env=process.env) {
|
|
285
|
+
if(Object.hasOwn(settings,'bindings-file')) {
|
|
286
|
+
try {
|
|
287
|
+
if(typeof settings['bindings-file']!=='string' || !absolute(settings['bindings-file'])) return {problem:problem('needs-configuration','settings:missing')};
|
|
288
|
+
if(settings.harvest!==undefined && !['on','off'].includes(settings.harvest)) return {problem:problem('needs-configuration','runtime:not-qualified')};
|
|
289
|
+
if(settings.harvest!=='off') {
|
|
290
|
+
if(settings['harvest-runtime']===undefined) return {problem:problem('needs-configuration','harvest-runtime:missing')};
|
|
291
|
+
if(!['pi','claude','codex'].includes(settings['harvest-runtime'])) return {problem:problem('needs-configuration','harvest-runtime:invalid')};
|
|
292
|
+
if(settings['harvest-model']!==undefined && (typeof settings['harvest-model']!=='string' || !settings['harvest-model'].trim())) return {problem:problem('needs-configuration','harvest-model:invalid')};
|
|
293
|
+
}
|
|
294
|
+
const bindings=loadBindings(settings['bindings-file']);
|
|
295
|
+
if(!env.OATS_SOUL) return {problem:problem('needs-configuration','soul:missing')};
|
|
296
|
+
const decl=soulDeclaration(env.OATS_SOUL);
|
|
297
|
+
return {runtime:{descriptorFile:bindings.file,bindings:{version:1,stateDir:bindings.stateDir,bases:bindings.bases},declaration:decl}};
|
|
298
|
+
} catch(error) {
|
|
299
|
+
const message=String(error?.message || '');
|
|
300
|
+
if(error?.code==='E_CONFIG' && /okf\.json|soul has no/.test(message)) return {problem:problem('needs-configuration','soul:missing')};
|
|
301
|
+
return {problem:problem('needs-configuration','bindings:invalid')};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if(settings.schemaVersion!==undefined) {
|
|
305
|
+
try { return {runtime:bindingPayload(settings,{diagnoseSettings:true}).runtime}; }
|
|
306
|
+
catch(error) {if(['invalid-binding','needs-configuration'].includes(error?.wireCode)) return {problem:problem('needs-configuration','binding:not-configured')};throw error;}
|
|
307
|
+
}
|
|
308
|
+
return {problem:problem('needs-configuration','settings:missing')};
|
|
309
|
+
}
|
|
279
310
|
function checkPhase(req) {
|
|
280
|
-
keys(req.input,['
|
|
311
|
+
keys(req.input,['context','action'],['context','action'],'check input');
|
|
281
312
|
if(!obj(req.input.context) || !obj(req.input.action)) wireError('invalid-binding');
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
if(name && (unsupportedCapturedCommands.has(name) || name==='run-source' || (name==='harvest' && !admittedHarvest))) return {status:'needs-configuration',problems:[problem('provider-not-qualified','action:not-admitted')]};
|
|
313
|
+
const action=req.input.action,name=providerActionName(action);
|
|
314
|
+
const prepared=readinessRuntime(req.settings);
|
|
315
|
+
if(prepared.problem) return {status:'needs-configuration',problems:[prepared.problem]};
|
|
316
|
+
const runtime=prepared.runtime;
|
|
317
|
+
if(name && (unsupportedCapturedCommands.has(name) || name==='run-source' || name==='harvest')) return {status:'needs-configuration',problems:[problem('provider-not-qualified','action:not-admitted')]};
|
|
288
318
|
if(action.kind==='hook' && action.name==='soul-scaffold') return {status:'ready',problems:[]};
|
|
289
319
|
let bindings;try{bindings=validateBindings(runtime.bindings,runtime.descriptorFile);}catch{return {status:'needs-configuration',problems:[problem('needs-configuration','bindings:invalid')]};}
|
|
290
320
|
const accepted={},gitBases=Object.entries(bindings.bases).filter(([,base])=>base.kind==='git');
|
|
@@ -296,14 +326,16 @@ function checkPhase(req) {
|
|
|
296
326
|
stage=base.kind==='directory'?'validate':'stage';
|
|
297
327
|
accepted[alias]=(base.kind==='directory'?validateBase(base.path,base):stageBase(base,join(scratch,alias),{alias})).meta;
|
|
298
328
|
}
|
|
299
|
-
|
|
300
|
-
|
|
329
|
+
if(req.settings.harvest!=='off') {
|
|
330
|
+
stage='runtime';
|
|
331
|
+
checkKnowledgeRuntime({rendered:runtime,accepted});
|
|
332
|
+
}
|
|
301
333
|
} catch(error) {
|
|
302
|
-
if(error.code==='E_COMMAND') return {status:'
|
|
334
|
+
if(error.code==='E_COMMAND') return {status:'needs-configuration',problems:[problem('needs-configuration','runtime:command-missing')]};
|
|
303
335
|
if(error.code==='E_OWNER') return {status:'needs-configuration',problems:[problem('provider-not-qualified','base:owner-unmet')]};
|
|
304
336
|
if(error.code==='E_CONFIRM') return {status:'needs-configuration',problems:[problem('provider-not-qualified','base:source-mismatch')]};
|
|
305
337
|
if(['E_BASE','E_VALIDATION','E_DIRECTORY_GIT'].includes(error.code)) return {status:'needs-configuration',problems:[problem('provider-not-qualified',stage==='stage'?'base:stage-failed':'base:not-validated')]};
|
|
306
|
-
if(stage==='runtime') return {status:'
|
|
338
|
+
if(stage==='runtime') return {status:'needs-configuration',problems:[problem('needs-configuration','runtime:not-qualified')]};
|
|
307
339
|
return {status:'unavailable',problems:[problem('provider-unavailable')]};
|
|
308
340
|
} finally {
|
|
309
341
|
if(scratch) fs.rmSync(scratch,{recursive:true,force:true});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAbsolute } from 'node:path';
|
|
2
|
-
import { fs, join, resolve, dirname, fail, readJSON, safePath, relPath, identifier, overlaps, hash } from './io.mjs';
|
|
2
|
+
import { fs, join, resolve, dirname, fail, readJSON, safePath, relPath, identifier, overlaps, hash, embedsCredential } from './io.mjs';
|
|
3
3
|
const obj = v => v && typeof v === 'object' && !Array.isArray(v);
|
|
4
4
|
function keys(value, allowed, label, code='E_CONFIG') {
|
|
5
5
|
if(!obj(value)) fail(code, `${label} must be an object`);
|
|
@@ -48,6 +48,7 @@ export function validateBindings(doc, file, { sourceHome, sourceWork } = {}) {
|
|
|
48
48
|
keys(raw,['id','kind','repository','root','acceptedBranch','pr'],'git base');
|
|
49
49
|
keys(raw.pr,['repository'],'git pr');
|
|
50
50
|
if(typeof raw.repository !== 'string' || !raw.repository || raw.repository.startsWith('-') || /[\r\n\0]/.test(raw.repository) || 'path' in raw) fail('E_CONFIG', 'git base requires repository');
|
|
51
|
+
if(embedsCredential(raw.repository)) fail('E_CONFIG', `git base "${alias}": the repository locator embeds a credential (user:token@); bind the plain URL and let a Git credential helper or SSH key supply access`);
|
|
51
52
|
const root = relPath(raw.root, true);
|
|
52
53
|
if (typeof raw.acceptedBranch !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(raw.acceptedBranch) || raw.acceptedBranch.includes('..') || raw.acceptedBranch.endsWith('/') || raw.acceptedBranch.endsWith('.lock')) fail('E_CONFIG','invalid acceptedBranch');
|
|
53
54
|
if (!obj(raw.pr) || typeof raw.pr.repository !== 'string' || !/^[\w.-]+\/[\w.-]+$/.test(raw.pr.repository)) fail('E_CONFIG','git pr requires repository: owner/repo (same-repository PRs)');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { hostname } from 'node:os';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { fs, join, dirname, safePath, readJSON, save, digest, tree, withLock, fail, syncDir, identifier } from './io.mjs';
|
|
8
|
+
import { fs, join, dirname, resolve, safePath, readJSON, save, digest, tree, withLock, fail, syncDir, identifier, relPath, within, displayRepo } from './io.mjs';
|
|
9
9
|
import { noGit, gitTimeoutMs, consultMaxAgeMs, splitRef, resolveNodes } from './config.mjs';
|
|
10
10
|
import { git, gitEnv, validateBase, baseLock, journalPath, preflightLocalRepository, requireNotShallow, verifyRemote, unavailable } from './stores.mjs';
|
|
11
11
|
|
|
@@ -248,6 +248,23 @@ export function withBase(bindings, alias, { fresh = false } = {}, fn) {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// ---------------------------------------------------------------- validation
|
|
251
|
+
/** A tree entry name, from an untrusted accepted tree, as a path under the base
|
|
252
|
+
* root. Git accepts literal `..`, absolute-looking and control-character entry
|
|
253
|
+
* names (`hash-object --literally`), and they survive a partial clone, so every
|
|
254
|
+
* name gets relPath's canonical rules (no empty, `.`, `..`, absolute, backslash,
|
|
255
|
+
* NUL or `.git` segment) plus no control characters or percent-escapes. */
|
|
256
|
+
export function treeEntryPath(name) {
|
|
257
|
+
const bad = () => fail('E_PATH', `invalid tree entry in knowledge base: ${JSON.stringify(String(name)).slice(0, 200)}`);
|
|
258
|
+
if (typeof name !== 'string' || /[\u0000-\u001f\u007f]/.test(name) || /%[0-9a-fA-F]{2}/.test(name)) bad();
|
|
259
|
+
try { return relPath(name); } catch { return bad(); }
|
|
260
|
+
}
|
|
261
|
+
/** Where a tree entry lands under `root`, refused unless it stays strictly
|
|
262
|
+
* inside it after resolution (the second, independent guard). */
|
|
263
|
+
export function containedTarget(root, name) {
|
|
264
|
+
const target = resolve(root, treeEntryPath(name));
|
|
265
|
+
if (target === resolve(root) || !within(root, target)) fail('E_PATH', `tree entry escapes its materialization root: ${JSON.stringify(name).slice(0, 200)}`);
|
|
266
|
+
return safePath(target);
|
|
267
|
+
}
|
|
251
268
|
const VALIDATION_CODES = new Set(['E_VALIDATION', 'E_BASE', 'E_PATH', 'E_ID', 'E_CONFIG']);
|
|
252
269
|
/** Whether the accepted state is a valid OKF base. Git verdicts are cached per
|
|
253
270
|
* commit inside the host cache; computing one materializes the base root into
|
|
@@ -264,10 +281,15 @@ export function verdict(ctx) {
|
|
|
264
281
|
try {
|
|
265
282
|
const result = judge(() => {
|
|
266
283
|
const rows = lsTree(ctx, '', { recursive: true });
|
|
267
|
-
|
|
284
|
+
// Every entry is judged before anything is written: the first bad name or
|
|
285
|
+
// mode refuses the whole base, and nothing lands outside the scratch.
|
|
286
|
+
for (const r of rows) {
|
|
287
|
+
if (!['100644', '100755'].includes(r.mode) || r.type !== 'blob') fail('E_PATH', 'symlink or submodule in knowledge base is not allowed');
|
|
288
|
+
r.target = containedTarget(scratch, r.name);
|
|
289
|
+
}
|
|
268
290
|
prefetch(ctx, '');
|
|
269
291
|
for (const r of rows) {
|
|
270
|
-
const target =
|
|
292
|
+
const target = r.target; fs.mkdirSync(dirname(target), { recursive: true });
|
|
271
293
|
const fd = fs.openSync(target, 'wx', 0o600);
|
|
272
294
|
let w; try { w = spawnSync('git', ['--no-replace-objects', '-c', 'core.hooksPath=/dev/null', '-C', ctx.cache, 'cat-file', 'blob', r.oid], { env: gitEnv(), timeout: gitTimeoutMs(), stdio: ['ignore', fd, 'pipe'] }); } finally { fs.closeSync(fd); }
|
|
273
295
|
if (w.error || w.status !== 0) unavailable(ctx.base, ctx.alias, 'read', gitError(w));
|
|
@@ -346,7 +368,7 @@ export function bases(source, flags) {
|
|
|
346
368
|
const rows = [], receipts = [];
|
|
347
369
|
for (const [alias, base] of Object.entries(source.bindings.bases)) withBase(source.bindings, alias, { fresh: !!flags.fresh }, ctx => {
|
|
348
370
|
const v = verdict(ctx), mine = relation => source.decl[relation].filter(ref => splitRef(ref)[0] === alias).map(ref => splitRef(ref)[1]);
|
|
349
|
-
rows.push({ alias, id: base.id, kind: base.kind, ...(base.kind === 'git' ? { repository: base.repository, acceptedBranch: base.acceptedBranch, root: base.root, commit: ctx.commit } : { path: base.path, digest: ctx.receipt.digest }),
|
|
371
|
+
rows.push({ alias, id: base.id, kind: base.kind, ...(base.kind === 'git' ? { repository: displayRepo(base.repository), acceptedBranch: base.acceptedBranch, root: base.root, commit: ctx.commit } : { path: base.path, digest: ctx.receipt.digest }),
|
|
350
372
|
fetchedAt: ctx.receipt.fetchedAt, stale: ctx.receipt.stale, ...(ctx.receipt.reason ? { reason: ctx.receipt.reason } : {}),
|
|
351
373
|
validated: v.ok ? { ok: true } : { ok: false, error: v.error }, nodes: v.ok ? Object.keys(v.nodes) : null, owns: mine('owns'), reads: mine('reads'), receipt: ctx.receipt });
|
|
352
374
|
receipts.push(ctx.receipt);
|
|
@@ -60,11 +60,24 @@ function shaped(value) {
|
|
|
60
60
|
* A soul `on` is ignored and reported: a soul cannot switch a host on. It also
|
|
61
61
|
* hides the host's value (the kernel's merged `harvest` is then the soul's),
|
|
62
62
|
* so with a soul `on` the switch stays off until the soul drops the line. */
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/** OATS_SETTINGS_ORIGINS (kernel 0.29.0): JSON pointer → { kind, at } of the LAST
|
|
64
|
+
* layer that set each leaf. {} when absent or unreadable. */
|
|
65
|
+
export function parseOrigins(text = process.env.OATS_SETTINGS_ORIGINS) {
|
|
66
|
+
try { const o = JSON.parse(text || '{}'); return o && typeof o === 'object' && !Array.isArray(o) ? o : {}; } catch { return {}; }
|
|
67
|
+
}
|
|
68
|
+
/** okf 4.0.1: origins name the last layer only, and the host's settings merge
|
|
69
|
+
* AFTER the soul's slot, so a host `harvest: on` hides a soul's `off` there.
|
|
70
|
+
* soul.yaml therefore stays the authority for the absolute opt-out; origins
|
|
71
|
+
* are an extra signal: a merged value whose origin is the soul is never the
|
|
72
|
+
* deployment switching harvest on (a soul `on` is ignored, a soul `off` is off). */
|
|
73
|
+
export function harvestSwitch({ settings = {}, soulDir = process.env.OATS_SOUL, origins = parseOrigins() } = {}) {
|
|
74
|
+
const origin = origins?.['/harvest'] && typeof origins['/harvest'] === 'object' ? origins['/harvest'] : null;
|
|
75
|
+
const fromSoul = origin?.kind === 'soul';
|
|
76
|
+
const deployment = !fromSoul && VALUES.includes(settings.harvest) ? settings.harvest : 'off';
|
|
65
77
|
const soul = soulHarvest(soulDir);
|
|
78
|
+
if (fromSoul && VALUES.includes(settings.harvest) && soul.value === null) Object.assign(soul, { value: settings.harvest, readable: true, why: `OATS_SETTINGS_ORIGINS: harvest: ${settings.harvest} came from the soul (${String(origin.at || 'soul.yaml')})` });
|
|
66
79
|
const rows = [
|
|
67
|
-
{ layer: 'deployment', value: deployment, why: settings.harvest === undefined ? 'harvest is not set (default off)' : `settings.oats.okf.harvest: ${settings.harvest}` },
|
|
80
|
+
{ layer: 'deployment', value: deployment, ...(origin ? { origin: origin.kind } : {}), why: fromSoul ? 'the merged harvest value came from the soul, so the deployment does not switch harvest on' : settings.harvest === undefined ? 'harvest is not set (default off)' : `settings.oats.okf.harvest: ${settings.harvest}` },
|
|
68
81
|
{ layer: 'soul', value: soul.value, readable: soul.readable, why: soul.why },
|
|
69
82
|
];
|
|
70
83
|
const warnings = [];
|
|
@@ -53,10 +53,8 @@ function liveHome(source,status) {
|
|
|
53
53
|
return {available:false,reason:'unverified-home',error:{code:e.code || 'E_SOURCE',message:e.message}};
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const unavailable=state=>({liveMemory:{available:false,reason:state.reason,observedAt,...(state.error?{error:state.error}:{})},documents:[]});
|
|
59
|
-
if(!before.available) return unavailable(before);
|
|
56
|
+
/** The home's working memory (STATE.md, log.md, notes/*.md), each capped. */
|
|
57
|
+
function homeDocuments(home) {
|
|
60
58
|
const documents=[];
|
|
61
59
|
const doc=(label,file)=>{
|
|
62
60
|
const content=regular(file,DOCUMENT_BYTES);
|
|
@@ -64,9 +62,9 @@ export function workingDocuments(source,status=loadStatus(source)) {
|
|
|
64
62
|
};
|
|
65
63
|
let error;
|
|
66
64
|
try {
|
|
67
|
-
doc('Working state (STATE.md)',join(
|
|
68
|
-
doc('Log (log.md)',join(
|
|
69
|
-
const notes=safePath(join(
|
|
65
|
+
doc('Working state (STATE.md)',join(home,'STATE.md'));
|
|
66
|
+
doc('Log (log.md)',join(home,'log.md'));
|
|
67
|
+
const notes=safePath(join(home,'notes'));
|
|
70
68
|
function walk(dir,prefix='') {
|
|
71
69
|
let entries;try {entries=fs.readdirSync(dir,{withFileTypes:true});} catch(e) {if(e.code==='ENOENT') return;throw e;}
|
|
72
70
|
for(const entry of entries.sort((a,b)=>a.name<b.name?-1:a.name>b.name?1:0)) {
|
|
@@ -79,6 +77,13 @@ export function workingDocuments(source,status=loadStatus(source)) {
|
|
|
79
77
|
}
|
|
80
78
|
walk(notes);
|
|
81
79
|
} catch(e) {error=e;}
|
|
80
|
+
return {documents,error};
|
|
81
|
+
}
|
|
82
|
+
export function workingDocuments(source,status=loadStatus(source)) {
|
|
83
|
+
const before=liveHome(source,status),observedAt=new Date().toISOString();
|
|
84
|
+
const unavailable=state=>({liveMemory:{available:false,reason:state.reason,observedAt,...(state.error?{error:state.error}:{})},documents:[]});
|
|
85
|
+
if(!before.available) return unavailable(before);
|
|
86
|
+
const {documents,error}=homeDocuments(source.home);
|
|
82
87
|
const after=liveHome(source,loadStatus(source));
|
|
83
88
|
if(!after.available) return unavailable(after);
|
|
84
89
|
if(before.dev!==after.dev || before.ino!==after.ino) return unavailable({reason:'home-changed'});
|
|
@@ -117,3 +122,17 @@ export function inspect(source) {
|
|
|
117
122
|
acceptedView:source.acceptedView,legacyLocalView:legacy,status,scheduler:health,liveMemory:working.liveMemory,documents
|
|
118
123
|
};
|
|
119
124
|
}
|
|
125
|
+
/** okf 4.0.3: inspect a harvest-off home (consultSource): its declaration, the
|
|
126
|
+
* bases it reads, and its own working memory. There is no source, custody or
|
|
127
|
+
* schedule to report; `oats okf bases` shows the accepted state it reads. */
|
|
128
|
+
export function inspectConsultOnly(source) {
|
|
129
|
+
const {documents,error}=homeDocuments(source.home),observedAt=new Date().toISOString();
|
|
130
|
+
if(error) fail(error.code==='E_PATH'?'E_PATH':'E_INSPECT_FAILED',error.message);
|
|
131
|
+
const path=join(source.home,'knowledge');let legacy=null;
|
|
132
|
+
try {const stat=fs.lstatSync(path);legacy={status:'legacy-local-view',path,ignored:true,...(stat.isSymbolicLink()?{symlink:true}:{}),note:'okf 3.0.0 ignores this okf 2.x snapshot and reads knowledge remotely; it is safe to delete by hand'};} catch(e) {if(!missing(e)) throw e;}
|
|
133
|
+
return {
|
|
134
|
+
summary:`OKF harvest off for this instance (${source.harvest.reason}): no source is registered and nothing is captured; ${documents.length} working-memory documents${legacy?'; legacy-local-view ./knowledge/ (ignored, safe to delete)':''}`,
|
|
135
|
+
source:null,harvest:source.harvest,owns:source.decl.owns,reads:source.decl.reads,bases:source.bindings.bases,
|
|
136
|
+
acceptedView:null,legacyLocalView:legacy,status:null,scheduler:null,liveMemory:{available:true,reason:'live',observedAt},documents
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -8,6 +8,14 @@ export const fail = (code, message) => { throw Object.assign(new Error(message),
|
|
|
8
8
|
export const hash = value => createHash('sha256').update(typeof value === 'string' || Buffer.isBuffer(value) ? value : JSON.stringify(value)).digest('hex');
|
|
9
9
|
export const readJSON = path => JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
10
10
|
export const within = (root, path) => { const r = relative(root, path); return r === '' || (!r.startsWith('..' + sep) && r !== '..' && !isAbsolute(r)); };
|
|
11
|
+
// okf 4.0.1 #2: a URL's userinfo is a credential (https://user:token@host).
|
|
12
|
+
// Strip it from anything shown to agents or users; an SSH user alone
|
|
13
|
+
// (ssh://git@host) is not a secret and stays.
|
|
14
|
+
const USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/@'"]+)@/gi;
|
|
15
|
+
export const redactUrls = text => String(text).replace(USERINFO, (m, scheme, info) => /^ssh:/i.test(scheme) && !info.includes(':') ? m : scheme);
|
|
16
|
+
export const displayRepo = repository => typeof repository === 'string' ? redactUrls(repository) : repository;
|
|
17
|
+
/** Whether a repository locator embeds a credential (any userinfo except a bare SSH user). */
|
|
18
|
+
export const embedsCredential = repository => { USERINFO.lastIndex = 0; return typeof repository === 'string' && redactUrls(repository) !== repository; };
|
|
11
19
|
export const overlaps = (a, b) => within(a, b) || within(b, a);
|
|
12
20
|
export function safePath(path) {
|
|
13
21
|
path = resolve(path);
|
|
@@ -77,7 +85,7 @@ export function unlock(path, token) {
|
|
|
77
85
|
try { process.kill(o.pid, 0); fail('E_LOCKED', 'lock owner is still alive'); } catch (e) { if(e.code !== 'ESRCH') throw e; }
|
|
78
86
|
fs.rmSync(path, { recursive: true }); syncDir(dirname(path)); return { unlocked: path };
|
|
79
87
|
}
|
|
80
|
-
export const identityKeys = ['OATS_INSTANCE','OATS_INSTANCE_HOME','OATS_HOME','PI_AGENT_HOME','PI_AGENT_NAME','PI_AGENT_INSTANCE','PI_AGENTS_ROOT','OATS_ROOT','OATS_SOUL','OATS_SOUL_ID','OATS_AGENT','OATS_KIND','OATS_EVENT','OATS_CONTEXT','OATS_REPO','OATS_WORK','OATS_BRANCH','OATS_META','OATS_SETTINGS','OATS_BINDING_FILE','OATS_SOURCE_RECEIPT_FILE','OATS_INVOCATION_CONTEXT_FILE','OATS_DEPLOYMENT','OATS_RESOLUTION'];
|
|
88
|
+
export const identityKeys = ['OATS_INSTANCE','OATS_INSTANCE_HOME','OATS_HOME','PI_AGENT_HOME','PI_AGENT_NAME','PI_AGENT_INSTANCE','PI_AGENTS_ROOT','OATS_ROOT','OATS_SOUL','OATS_SOUL_ID','OATS_AGENT','OATS_KIND','OATS_EVENT','OATS_CONTEXT','OATS_REPO','OATS_WORK','OATS_BRANCH','OATS_META','OATS_SETTINGS','OATS_SETTINGS_ORIGINS','OATS_BINDING_FILE','OATS_SOURCE_RECEIPT_FILE','OATS_INVOCATION_CONTEXT_FILE','OATS_DEPLOYMENT','OATS_RESOLUTION'];
|
|
81
89
|
export function cleanEnv(env = process.env) {
|
|
82
90
|
return Object.fromEntries(Object.entries(env).filter(([k]) => !/^(OATS_(?!HOME_DIR$|PACKAGE_CATALOG$)|PI_AGENT|GIT_)/.test(k)));
|
|
83
91
|
}
|
|
@@ -108,14 +108,28 @@ function finishRegistration(source) {
|
|
|
108
108
|
scheduleSource(source);return source;
|
|
109
109
|
}
|
|
110
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.
|
|
111
|
+
* a small record so retire knows there is nothing to capture, and (4.0.3) the
|
|
112
|
+
* soul's declaration, so consultation works without a source. */
|
|
112
113
|
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
|
+
const record={version:1,harvest:'off',reason:sw.reason,rows:sw.rows,warnings:sw.warnings,...(extra.decl?{decl:extra.decl}:{}),at:new Date().toISOString()};
|
|
114
115
|
atomic(instanceRecordPath(home),JSON.stringify(record,null,2)+'\n');
|
|
115
116
|
ensureInstanceKnowledge(home);
|
|
116
117
|
return {harvestOff:true,switch:sw,home,...extra};
|
|
117
118
|
}
|
|
118
119
|
export const harvestOffRecord = home => fs.existsSync(instanceRecordPath(home))?readJSON(instanceRecordPath(home)):null;
|
|
120
|
+
/** okf 4.0.3: what a harvest-off home consults. Consultation never depends on
|
|
121
|
+
* the harvest switch: such a home has no registered source by design, so it
|
|
122
|
+
* reads its soul's declaration (recorded at spawn; the kernel's OATS_SOUL for
|
|
123
|
+
* a home spawned before 4.0.3) and the deployment's bindings as they are now.
|
|
124
|
+
* Nothing is registered, captured or scheduled. */
|
|
125
|
+
export function consultSource(home) {
|
|
126
|
+
home=safePath(home);
|
|
127
|
+
const record=harvestOffRecord(home);
|
|
128
|
+
if(!record) fail('E_SOURCE',`no okf source for ${home}: it was not spawned with oats.okf as its knowledge capability`);
|
|
129
|
+
const decl=record.decl || (process.env.OATS_SOUL?declaration(fs.realpathSync(process.env.OATS_SOUL)):fail('E_OATS_SOUL_MISSING','OATS_SOUL is not set; oats.okf commands run only under the OATS kernel'));
|
|
130
|
+
const work=fs.existsSync(join(home,'work'))?fs.realpathSync(join(home,'work')):join(home,'work');
|
|
131
|
+
return {home,work,decl,bindings:loadBindings(undefined,{sourceHome:home,sourceWork:work}),harvest:{status:'off',reason:record.reason,at:record.at}};
|
|
132
|
+
}
|
|
119
133
|
/** The source's tasks provider (its instance.json tasks-layer capability), or null. */
|
|
120
134
|
function tasksProvider(meta) {
|
|
121
135
|
const row=Array.isArray(meta?.capabilities)?meta.capabilities.find(c=>c && c.layer==='tasks' && typeof c.id==='string'):null;
|
|
@@ -202,7 +216,7 @@ export function register(home) {
|
|
|
202
216
|
const roleFile=safePath(join(soul,'AGENTS.md'));
|
|
203
217
|
const role=fs.existsSync(roleFile)?fs.readFileSync(roleFile,'utf8'):'';
|
|
204
218
|
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,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()};
|
|
219
|
+
const source={version:1,id,home,work,context,agent,instance,owner:decl.owner,soulDir:soul,decl,role,bindings,bindingFingerprint:bindingFingerprint(bindings),execution:{runtime:settings()['harvest-runtime']||'pi',model:settings()['harvest-model']||null},soulId,tasksProvider:tasksProvider(meta),created:new Date().toISOString()};
|
|
206
220
|
const file=join(dir,'source.json');
|
|
207
221
|
fs.mkdirSync(dir,{recursive:true,mode:0o700});
|
|
208
222
|
try {
|
|
@@ -256,6 +270,23 @@ export function input(source,id) {
|
|
|
256
270
|
* that is still running (or has unresolved effects) stays disabled and is
|
|
257
271
|
* removed on the worker's next settle. Idempotent; a scheduler failure is
|
|
258
272
|
* recorded, never thrown — the evidence is already safe. */
|
|
273
|
+
/** okf 4.0.1 #6: the switch for an already registered source, re-read now: the
|
|
274
|
+
* deployment setting AND the soul's opt-out (the current OATS_SOUL the kernel
|
|
275
|
+
* hands run-source/retire, else the soul directory recorded at registration). */
|
|
276
|
+
export function sourceSwitch(source) {
|
|
277
|
+
const current = process.env.OATS_SOUL && fs.existsSync(process.env.OATS_SOUL) ? process.env.OATS_SOUL : null;
|
|
278
|
+
return harvestSwitch({ settings: settings(), soulDir: current || source.soulDir || undefined });
|
|
279
|
+
}
|
|
280
|
+
/** Retire a registered source whose harvest is now off: no final capture; the
|
|
281
|
+
* inputs already in custody stay; the schedule is settled as for any retire. */
|
|
282
|
+
export function retireHarvestOff(source, sw) {
|
|
283
|
+
updateStatus(source, current => {
|
|
284
|
+
current.retired = true; current.retiredAt = new Date().toISOString(); current.harvestOff = { reason: sw.reason, at: current.retiredAt };
|
|
285
|
+
// As a final capture would: the schedule stays only while earlier inputs await processing.
|
|
286
|
+
current.auto = current.auto && !current.captured.inputs.every(id => current.processed.includes(id));
|
|
287
|
+
});
|
|
288
|
+
return { retired: true, reason: 'harvest-off', switch: sw.reason, source: source.file, schedule: settleRetiredSchedule(source) };
|
|
289
|
+
}
|
|
259
290
|
export function settleRetiredSchedule(source) {
|
|
260
291
|
const status=loadStatus(source);
|
|
261
292
|
if(status.schedule?.removed===true) return {status:'already-removed',id:status.schedule.id};
|
|
@@ -2,14 +2,14 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { fs, join, dirname, safePath, readJSON, save, atomic, tree, materialize, digest, hash, withLock, exec, cleanEnv, fail, relPath, overlaps, resolve } from './io.mjs';
|
|
5
|
+
import { fs, join, dirname, safePath, readJSON, save, atomic, tree, materialize, digest, hash, withLock, exec, cleanEnv, fail, relPath, overlaps, resolve, within, redactUrls, displayRepo } from './io.mjs';
|
|
6
6
|
import { metadata, noGit, gitTimeoutMs } from './config.mjs';
|
|
7
7
|
const validator = fileURLToPath(new URL('./okf-validate.mjs', import.meta.url));
|
|
8
8
|
// Never let local replace refs reinterpret frozen OIDs, including inside Git's
|
|
9
9
|
// transport subprocesses. Override even an explicitly supplied command env.
|
|
10
10
|
export const gitEnv = (env = cleanEnv()) => ({...env,GIT_NO_REPLACE_OBJECTS:'1'});
|
|
11
11
|
export const git = (cwd,args,opts={}) => exec('git',['--no-replace-objects','-c','core.hooksPath=/dev/null','-c','protocol.ext.allow=never','-C',cwd,...args],{cwd,...opts,env:gitEnv(opts.env)});
|
|
12
|
-
function baseError(code,message,base,alias,step,reason) {throw Object.assign(new Error(message),{code,base:alias,repository:base.repository,step,reason});}
|
|
12
|
+
function baseError(code,message,base,alias,step,reason) {throw Object.assign(new Error(redactUrls(message)),{code,base:alias,repository:displayRepo(base.repository),step,reason});}
|
|
13
13
|
function baseRemedy(alias) {return `fix the binding for base alias "${alias}" in the bindings file, or remove the base from the bindings`;}
|
|
14
14
|
function classifyGitFailure(error) {
|
|
15
15
|
const text=String(error?.message || '');
|
|
@@ -20,13 +20,13 @@ function classifyGitFailure(error) {
|
|
|
20
20
|
return 'unknown';
|
|
21
21
|
}
|
|
22
22
|
export function unavailable(base,alias,step,error) {
|
|
23
|
-
const reason=classifyGitFailure(error),detail=reason==='unknown'?`; original Git failure: ${String(error?.message || 'unknown failure')}`:'';
|
|
24
|
-
const message=`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but ${step} failed (reason: ${reason}${detail}); ${baseRemedy(alias)}`;
|
|
23
|
+
const reason=classifyGitFailure(error),detail=reason==='unknown'?`; original Git failure: ${redactUrls(String(error?.message || 'unknown failure'))}`:'';
|
|
24
|
+
const message=`Git base "${alias}" repository "${displayRepo(base.repository)}" is required by the deployment's bindings, but ${step} failed (reason: ${reason}${detail}); ${baseRemedy(alias)}`;
|
|
25
25
|
baseError('E_BASE_UNAVAILABLE',message,base,alias,step,reason);
|
|
26
26
|
}
|
|
27
27
|
export function requireNotShallow(base,alias,cwd) {
|
|
28
28
|
const shallow=git(cwd,['rev-parse','--is-shallow-repository']);
|
|
29
|
-
if(shallow==='true') baseError('E_BASE_SHALLOW',`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but the repository is shallow; oats.okf requires full accepted history before staging; ${baseRemedy(alias)}`,base,alias,'clone','shallow');
|
|
29
|
+
if(shallow==='true') baseError('E_BASE_SHALLOW',`Git base "${alias}" repository "${displayRepo(base.repository)}" is required by the deployment's bindings, but the repository is shallow; oats.okf requires full accepted history before staging; ${baseRemedy(alias)}`,base,alias,'clone','shallow');
|
|
30
30
|
}
|
|
31
31
|
export function preflightLocalRepository(base,alias) {
|
|
32
32
|
if(!base.repository.startsWith('/')) return;
|
|
@@ -77,7 +77,9 @@ function materializeGitObjects(base,dest,head) {
|
|
|
77
77
|
// that an entry outside the root is expected to be absent (see outsideBase).
|
|
78
78
|
for(const [p,entry] of entries) {
|
|
79
79
|
if(outsideBase(base,p)) continue;
|
|
80
|
-
|
|
80
|
+
// relPath above already refused traversal; containment is asserted again
|
|
81
|
+
// on the resolved target before any write (okf 4.0.1 #1).
|
|
82
|
+
const target=resolve(dest,p);if(target===resolve(dest) || !within(dest,target)) fail('E_PATH','Git tree entry escapes the stage');safePath(target);fs.mkdirSync(dirname(target),{recursive:true});
|
|
81
83
|
writeBlob(dest,entry.oid,target,entry.mode==='100755'?0o755:0o644);
|
|
82
84
|
}
|
|
83
85
|
}
|
|
@@ -37,7 +37,7 @@ export function completionCommand(source,id,judgmentFile) {return command(source
|
|
|
37
37
|
// It homes in agents/oats-okf--knowledge-harvester/. Its instances get an
|
|
38
38
|
// exact --name okf-harvester-<run> (50 characters): a derived
|
|
39
39
|
// <agent>-<purpose> name would exceed the kernel's 64-character cap.
|
|
40
|
-
export const HARVESTER_SOUL='oats.okf/knowledge-harvester',HARVESTER_AGENT='oats-okf--knowledge-harvester'
|
|
40
|
+
export const HARVESTER_SOUL='oats.okf/knowledge-harvester',HARVESTER_AGENT='oats-okf--knowledge-harvester';
|
|
41
41
|
export const harvesterInstance=id=>`okf-harvester-${id}`;
|
|
42
42
|
/** The harvester's own completion and status commands (oats.okf-harvest). The
|
|
43
43
|
* completion wrapper runs this source's frozen `oats okf complete` from the
|
|
@@ -47,15 +47,6 @@ export function harvesterCommands(source,id) {
|
|
|
47
47
|
return {complete:['oats','okf-harvest','complete',...tail,'--judgment','<absolute-judgment.json>'].map(quote).join(' '),
|
|
48
48
|
status:['oats','okf-harvest','harvest-status',...tail].map(quote).join(' ')};
|
|
49
49
|
}
|
|
50
|
-
/** The messaging capability the harvester soul resolves (from spawn --preview),
|
|
51
|
-
* so it can join the okf team; null when it resolves none. */
|
|
52
|
-
function harvesterMessaging(source) {
|
|
53
|
-
try {
|
|
54
|
-
const preview=oats(['spawn',HARVESTER_SOUL,'--dir',source.context,'--preview','--json'],source.context,{timeout:90000});
|
|
55
|
-
const row=(Array.isArray(preview?.modules)?preview.modules:[]).find(m=>m?.layer==='messaging' && typeof m.name==='string');
|
|
56
|
-
return {messaging:row?row.name:null};
|
|
57
|
-
} catch(e) {return {messaging:null,error:`${e.code || 'E_RUNTIME'}: ${e.message}`};}
|
|
58
|
-
}
|
|
59
50
|
export function runSource(source,{noLaunch=false,manual=false,capturedInvocation,nativeRequest}={}) {
|
|
60
51
|
const plan=capturedSource(source)?qualifyCapturedWorker(source,{context:capturedInvocation,nativeRequest}):null;
|
|
61
52
|
if(!plan) requireQualifiedHelper(source);
|
|
@@ -117,7 +108,7 @@ function spawnWorker(source,run,{parent=false}={}) {
|
|
|
117
108
|
task=`Process only durable OKF run ${id}. Load the knowledge-harvest skill first.${recovery}\n\n${evidence}\n\nWrite ./work/judgment.json per the skill, then execute the completion command below, replacing only the quoted placeholder with the absolute judgment file path (shell-quote it). A successful command, not this task, is the delivery receipt. On failure retain the worker and report it; do not self-retire. On success report receipt then retire normally.\n\n${complete}\n`;
|
|
118
109
|
} else {
|
|
119
110
|
const cmd=harvesterCommands(source,id);
|
|
120
|
-
task=`Process only durable OKF run ${id}. Load the knowledge-harvest skill first.${recovery}\n\n${evidence}\n\nWrite ./work/judgment.json per the skill, then run the completion command below, replacing only the quoted placeholder with the absolute judgment file path (shell-quote it). It runs this source's frozen completion in the source deployment. A successful command, not this task, is the delivery receipt. On failure keep your home and report it; do not retire.\n\nAfter a successful completion, stay alive
|
|
111
|
+
task=`Process only durable OKF run ${id}. Load the knowledge-harvest skill first.${recovery}\n\n${evidence}\n\nWrite ./work/judgment.json per the skill, then run the completion command below, replacing only the quoted placeholder with the absolute judgment file path (shell-quote it). It runs this source's frozen completion in the source deployment. A successful command, not this task, is the delivery receipt. On failure keep your home and report it; do not retire.\n\nAfter a successful completion, stay alive until your PR is merged or closed. On every wake run the status command first, and retire only when it says retire or max-age. Never close the PR yourself.\n\nComplete: ${cmd.complete}\nStatus: ${cmd.status}\n`;
|
|
121
112
|
}
|
|
122
113
|
const taskFile=join(dirname(runPath(source,id)),'TASK.md');
|
|
123
114
|
const actualTask=run.capturedWorker?task.replace('On success report receipt then retire normally.',`On success report the actual receipt and include run ${id} in your final assistant reply. RETAIN this home/history. Public captured retirement is not qualified; never use legacy retirement or self-retire.`) :task;
|
|
@@ -143,11 +134,9 @@ function spawnWorker(source,run,{parent=false}={}) {
|
|
|
143
134
|
const args=['spawn',HARVESTER_SOUL,'--name',harvesterInstance(id),'--dir',source.context,harnessFlag(source),source.execution.runtime,'--no-launch','--task-file',taskFile,'--json'];
|
|
144
135
|
if(source.execution.model) args.push('--model',source.execution.model);
|
|
145
136
|
if(parent) args.push('--parent',source.instance);
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
if(team.messaging) args.push('--provider',team.messaging,`join=${HARVESTER_TEAM}`);
|
|
150
|
-
run.team={team:HARVESTER_TEAM,messaging:team.messaging,...(team.error?{error:team.error}:{})};
|
|
137
|
+
// okf 4.0.2: no team join. The harvester lives in the deployment's default
|
|
138
|
+
// team, where the maintainer reaches it; a deployment that wants it in
|
|
139
|
+
// another team opts in locally, as for any soul.
|
|
151
140
|
try {
|
|
152
141
|
run.worker=oats(args,source.context,{timeout:90000});
|
|
153
142
|
if(!run.worker.instance || !run.worker.home) fail('E_RUNTIME','spawn receipt lacks worker identity');
|
|
@@ -340,7 +329,8 @@ function harvesterAlias(run) {
|
|
|
340
329
|
export function provenance(source,run) {
|
|
341
330
|
const identity=source.sourceIdentity;
|
|
342
331
|
return {version:1,run:run.id,input:[...run.inputs],
|
|
343
|
-
|
|
332
|
+
// owner: the source's okf.json owner, which okf-base.json nodes record (okf 4.0.1 #3).
|
|
333
|
+
source:{soul:identity?.name ?? identity?.soul ?? source.agent,soulId:source.soulId ?? (identity?JSON.stringify(identity):null),owner:source.owner ?? null,instance:source.instance,
|
|
344
334
|
ownedNodes:[...source.decl.owns],readNodes:[...source.decl.reads],
|
|
345
335
|
bases:Object.entries(source.bindings.bases).map(([alias,b])=>({alias,id:b.id,kind:b.kind,...(b.kind==='git'?{root:b.root,repository:b.pr.repository}:{})}))},
|
|
346
336
|
tasks:{provider:source.tasksProvider ?? null,refs:[...new Set(run.judgment?.tasks?.refs || [])]},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"capability": "oats.okf",
|
|
3
3
|
"command": "okf",
|
|
4
|
-
"version": "4.0.
|
|
4
|
+
"version": "4.0.4",
|
|
5
5
|
"compatibility": {
|
|
6
6
|
"oats": ">=0.29.0"
|
|
7
7
|
},
|
|
@@ -82,14 +82,17 @@
|
|
|
82
82
|
"setting harvest-runtime must be pi, claude or codex",
|
|
83
83
|
"setting harvest-model must be null or a non-empty string",
|
|
84
84
|
"check action is not an admitted knowledge operation",
|
|
85
|
-
"
|
|
85
|
+
"OKF settings missing bindings-file: set settings.oats.okf bindings-file, or deactivate oats.okf",
|
|
86
|
+
"OKF soul declaration missing: create okf.json for this soul, or deactivate oats.okf",
|
|
87
|
+
"OKF binding missing or invalid: provision soul OKF declaration and bindings file, or deactivate oats.okf",
|
|
88
|
+
"OKF bindings file missing or invalid: repair configured bindings file, or deactivate oats.okf",
|
|
86
89
|
"more than 64 git knowledge bases declared",
|
|
87
90
|
"declared knowledge base could not be staged from its git source",
|
|
88
91
|
"declared knowledge base is not a validated knowledge tree",
|
|
89
92
|
"knowledge base owner or remote custody requirement not met",
|
|
90
93
|
"staged git source does not match the declared knowledge base",
|
|
91
94
|
"harvest runtime command is not installed on this host",
|
|
92
|
-
"harvest runtime
|
|
95
|
+
"harvest runtime is not qualified: install/configure the selected harvest runtime or set harvest off"
|
|
93
96
|
]
|
|
94
97
|
},
|
|
95
98
|
"inject": "injects/okf.md",
|
|
@@ -74,7 +74,7 @@ export function complete(flags, env = process.env) {
|
|
|
74
74
|
let answer; try { answer = JSON.parse(r.stdout); } catch { /* below */ }
|
|
75
75
|
if (answer?.schemaVersion === 1 && answer.ok === true) return { deployment: source.context, ...answer.result };
|
|
76
76
|
const code = answer?.error?.code || 'E_COMPLETE', message = answer?.error?.message || (r.error?.message || r.stderr || `exit ${r.status}`).trim();
|
|
77
|
-
if (INACTIVE.has(code)) fail('E_SOURCE_INACTIVE', `oats.okf cannot run for source soul ${source.agent} in ${source.context} (${code}: ${message}). Nothing was published: report this to
|
|
77
|
+
if (INACTIVE.has(code)) fail('E_SOURCE_INACTIVE', `oats.okf cannot run for source soul ${source.agent} in ${source.context} (${code}: ${message}). Nothing was published: report this to your operator, and stay.`, { cause: code });
|
|
78
78
|
fail(code, `${message} (completion ran in ${source.context}; keep your home and report)`);
|
|
79
79
|
}
|
|
80
80
|
/** "7d" | "48h" | "90m" | seconds → milliseconds. */
|
|
@@ -112,7 +112,7 @@ export function harvestStatus(flags, env = process.env, { now = Date.now(), view
|
|
|
112
112
|
if (!judged || pending.length) { action = age >= limit ? 'max-age' : 'stay'; reason = !judged ? 'the run is not completed yet' : `destinations not delivered: ${pending.map((d) => d.alias).join(', ')}`; }
|
|
113
113
|
else if (open.length) { action = age >= limit ? 'max-age' : 'stay'; reason = `open PR: ${open.map((d) => d.pr.url).join(', ')}`; }
|
|
114
114
|
else { action = 'retire'; reason = destinations.some((d) => d.pr) ? 'every PR is merged or closed' : 'no PR was needed (no-change or directory publication)'; }
|
|
115
|
-
if (action === 'max-age') reason += `; older than harvester-max-age (${Math.round(limit / 3600000)}h): tell
|
|
115
|
+
if (action === 'max-age') reason += `; older than harvester-max-age (${Math.round(limit / 3600000)}h): tell your operator and retire, never close the PR`;
|
|
116
116
|
return { run: run.id, status: run.status, ageSeconds: Math.round(age / 1000), maxAgeSeconds: limit / 1000, destinations, action, reason };
|
|
117
117
|
}
|
|
118
118
|
function text(event, r) {
|