@volter/supercode-orchestrator 0.4.140 → 0.4.142
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/board/cli.mjs +2 -2
- package/board/dispatch.mjs +28 -10
- package/board/engine.mjs +11 -1
- package/board/rows.mjs +17 -2
- package/board/workflow.mjs +2 -2
- package/board/ztrack.mjs +1 -0
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -385,7 +385,7 @@ export async function runWorkflow(argv) {
|
|
|
385
385
|
const card = withBoard((b) => {
|
|
386
386
|
const got = engine.showCard(b.db, id);
|
|
387
387
|
got.runs = engine.runs(b.db, id, { stateType: args['state-type'] ?? null, stateName: args['state-name'] ?? null });
|
|
388
|
-
got.diagnostics = engine.diagnose(b.db, got.task, { failureLimit: settings.failureLimit });
|
|
388
|
+
got.diagnostics = engine.diagnose(b.db, got.task, { failureLimit: settings.failureLimit, launchWindowSeconds: settings.launchWindow });
|
|
389
389
|
got.stage = statusOf(b.db, got.task);
|
|
390
390
|
got.time = scopeOf(b.db, b, got.task, { params: workflowFor(root).params }).card.time;
|
|
391
391
|
return got;
|
|
@@ -644,7 +644,7 @@ export async function runWorkflow(argv) {
|
|
|
644
644
|
const order = ['warning', 'error', 'critical'];
|
|
645
645
|
const found = withBoard((b) => {
|
|
646
646
|
const cards = args.task ? [engine.getTask(b.db, args.task)].filter(Boolean) : engine.listCards(b.db, {});
|
|
647
|
-
return cards.flatMap((t) => engine.diagnose(b.db, t, { failureLimit: settings.failureLimit, fleet }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))
|
|
647
|
+
return cards.flatMap((t) => engine.diagnose(b.db, t, { failureLimit: settings.failureLimit, fleet, launchWindowSeconds: settings.launchWindow }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))
|
|
648
648
|
.filter((d) => !args.severity || order.indexOf(d.severity) >= order.indexOf(args.severity));
|
|
649
649
|
});
|
|
650
650
|
if (args.json) { say(args, '', found); return 0; }
|
package/board/dispatch.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { spawn as spawnChild, spawnSync } from 'node:child_process';
|
|
|
21
21
|
import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { hostname, tmpdir } from 'node:os';
|
|
23
23
|
import { basename, dirname, join } from 'node:path';
|
|
24
|
-
import { randomUUID } from 'node:crypto';
|
|
24
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
26
|
import { view, boardPath, claimer, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
27
27
|
import { doors, fire, loadWorkflow, resumeOf, runSpec, requestSessionClose, retireSupersededRuns, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, templateValue, workflowFor } from './workflow.mjs';
|
|
@@ -195,6 +195,7 @@ export function boardSettings(params = {}) {
|
|
|
195
195
|
failureLimit: pos(params.failure_limit) ?? DEFAULT_FAILURE_LIMIT,
|
|
196
196
|
ttl: claimTtl(params.claim_ttl_seconds),
|
|
197
197
|
grace: pos(params.session_grace_seconds) ?? 120,
|
|
198
|
+
launchWindow: pos(params.session_launch_window_seconds) ?? 300,
|
|
198
199
|
interval: Math.max(Number(params.dispatch_interval_seconds) || 60, 1),
|
|
199
200
|
idleQuiet: pos(params.session_idle_quiet_seconds) ?? 600,
|
|
200
201
|
logRotateBytes: pos(params.worker_log_rotate_bytes) ?? 2 * 1024 * 1024,
|
|
@@ -258,10 +259,6 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
258
259
|
if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
|
|
259
260
|
const runRow = currentRun(db, id);
|
|
260
261
|
let session = runRow?.metadata?.supercode ?? null;
|
|
261
|
-
if (session?.launch_pending || session?.launch_deferred) {
|
|
262
|
-
extendClaim(db, id, { ttl: settings.ttl, reason: 'launch_receipt_pending' });
|
|
263
|
-
return; // An uncertain launch owns this run until a machine receipt settles it.
|
|
264
|
-
}
|
|
265
262
|
if (session?.pane) {
|
|
266
263
|
let found;
|
|
267
264
|
try { found = continuationPane(fleet, session, observed.get(session.machine)); }
|
|
@@ -286,6 +283,20 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
286
283
|
}
|
|
287
284
|
};
|
|
288
285
|
const limits = limitsOf(db, board, entry, task, session);
|
|
286
|
+
if (runRow && (session?.launch_pending || session?.launch_deferred || (!session?.session_id && !session?.pid))) {
|
|
287
|
+
const age = t - Number(session?.launched_at ?? runRow.started_at);
|
|
288
|
+
if (age >= settings.launchWindow) {
|
|
289
|
+
const reason = session?.launch_deferred_error ?? session?.launch_error ?? 'no session or process was bound to the dispatched run';
|
|
290
|
+
const error = `launch window ${settings.launchWindow}s expired after ${age}s: ${reason}`;
|
|
291
|
+
if (!session?.launch_expired_at) {
|
|
292
|
+
setRunSession(db, runRow.id, { launch_expired_at: t, launch_error: error });
|
|
293
|
+
event(db, id, 'session_launch_expired', { error, age_seconds: age, window_seconds: settings.launchWindow, key: session?.launch_key ?? null, pane: session?.pane ?? null }, runRow.id);
|
|
294
|
+
}
|
|
295
|
+
observe('spawn_failed', { error, launch_window_seconds: settings.launchWindow }, null);
|
|
296
|
+
report.failed.push([id, error]);
|
|
297
|
+
} else extendClaim(db, id, { ttl: settings.ttl, reason: 'launch_receipt_pending' });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
289
300
|
// Availability is the same retained linked/unlinked feed subscribers hear.
|
|
290
301
|
// A failed stream is unknown, not evidence that every remote machine left.
|
|
291
302
|
if (session?.session_id && session.surface !== 'headless') {
|
|
@@ -681,7 +692,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
681
692
|
if (observed.error) throw new Error(observed.error);
|
|
682
693
|
if (observed.live.has(session.pane)) {
|
|
683
694
|
const pendingNotice = view(board, (db) => db.prepare("SELECT id FROM supercode_effects WHERE json_extract(effect, '$.kind')='session_notice' AND json_extract(effect, '$.runId')=? LIMIT 1").get(row.id));
|
|
684
|
-
if (pendingNotice || session.lifecycle_notice_pending) throw new Error(`close deferred: lifecycle notice ${session.lifecycle_notice_pending} has not been delivered: ${session.lifecycle_notice_error ?? 'pending'}`);
|
|
695
|
+
if (pendingNotice || session.lifecycle_notice_pending) throw new Error(`close deferred: lifecycle notice ${pendingNotice?.id ?? session.lifecycle_notice_pending} has not been delivered: ${session.lifecycle_notice_error ?? 'pending'}`);
|
|
685
696
|
const protectedNow = view(board, (db) => db.prepare(`SELECT metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)`).all())
|
|
686
697
|
.some((r) => { const s = json(r.metadata, {})?.supercode; return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine); });
|
|
687
698
|
if (protectedNow) throw new Error(`close deferred: pane ${session.pane} is owned by an unended run`);
|
|
@@ -767,6 +778,12 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
767
778
|
}
|
|
768
779
|
}
|
|
769
780
|
|
|
781
|
+
function effectMessageKey(effect) {
|
|
782
|
+
if (effect.messageKey) return effect.messageKey;
|
|
783
|
+
const identity = JSON.stringify([effect.kind, effect.id, effect.runId, effect.sourceEvent, effect.durableId, effect.verb, effect.text]);
|
|
784
|
+
return `board-effect-${createHash('sha256').update(identity).digest('hex')}`;
|
|
785
|
+
}
|
|
786
|
+
|
|
770
787
|
/** The verb drains this durable notice immediately; the dispatcher retries after a failure. */
|
|
771
788
|
export async function sessionNotice(board, e) {
|
|
772
789
|
const prior = view(board, (db) => json(db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(e.runId)?.metadata, {})?.supercode);
|
|
@@ -781,7 +798,7 @@ export async function sessionNotice(board, e) {
|
|
|
781
798
|
let error = null, receipt = null;
|
|
782
799
|
try {
|
|
783
800
|
if (!e.address) throw new Error('lifecycle notice awaits the owned launch session identity');
|
|
784
|
-
receipt = await send(boardAddress(board.label), board.label, e.address, e.text, e
|
|
801
|
+
receipt = await send(boardAddress(board.label), board.label, e.address, e.text, effectMessageKey(e));
|
|
785
802
|
if (!receipt.delivered) error = receipt.reason ?? 'notice queued; native transcript confirmation pending'; }
|
|
786
803
|
catch (failure) { error = String(failure.message).slice(-500); }
|
|
787
804
|
tx(board, (db) => {
|
|
@@ -802,7 +819,7 @@ export async function sessionEffect(board, e) {
|
|
|
802
819
|
let receipt;
|
|
803
820
|
try {
|
|
804
821
|
if (!address) throw new Error('actor session identity unavailable');
|
|
805
|
-
receipt = await send(boardAddress(board.label), board.label, address, e.text,
|
|
822
|
+
receipt = await send(boardAddress(board.label), board.label, address, e.text, effectMessageKey(e));
|
|
806
823
|
} catch (error) { receipt = { delivered: false, reason: error.message }; }
|
|
807
824
|
const kind = receipt.delivered ? 'actor_notice_delivered' : receipt.queued ? 'actor_notice_queued' : 'actor_notice_failed';
|
|
808
825
|
tx(board, db => {
|
|
@@ -951,8 +968,9 @@ export function reconcileCallerLaunch(board, sid) {
|
|
|
951
968
|
}
|
|
952
969
|
|
|
953
970
|
function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
|
|
954
|
-
const deferred=view(board,db=>db.prepare("SELECT t.id FROM tasks t JOIN task_runs r ON r.id=t.current_run_id WHERE r.ended_at IS NULL AND json_extract(r.metadata,'$.supercode.launch_deferred')=1").all());
|
|
955
|
-
for(const {id} of deferred) {
|
|
971
|
+
const deferred=view(board,db=>db.prepare("SELECT t.id,r.started_at FROM tasks t JOIN task_runs r ON r.id=t.current_run_id WHERE r.ended_at IS NULL AND json_extract(r.metadata,'$.supercode.launch_deferred')=1").all());
|
|
972
|
+
for(const {id,started_at} of deferred) {
|
|
973
|
+
if (now() - started_at >= settings.launchWindow) continue;
|
|
956
974
|
try {
|
|
957
975
|
const task=getTaskNow(board,id),status=view(board,db=>statusOf(db,task)),spec=runSpec(entry,status,task);
|
|
958
976
|
const profile=spec&&view(board,db=>evaluate(entry.workflow.roles?.[spec.role]??'card.assignee',scopeOf(db,board,task,{params:entry.params})));
|
package/board/engine.mjs
CHANGED
|
@@ -710,11 +710,21 @@ const hours = (s) => s / 3600;
|
|
|
710
710
|
* same thing can go wrong, and supercode's own where only its sessions can): each `{ kind, severity, title, data,
|
|
711
711
|
* actions }`, worst first.
|
|
712
712
|
*/
|
|
713
|
-
export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null } = {}) {
|
|
713
|
+
export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null, launchWindowSeconds = 300 } = {}) {
|
|
714
714
|
const out = [];
|
|
715
715
|
const t = now();
|
|
716
716
|
const events = all(db, 'SELECT kind, payload, created_at FROM task_events WHERE task_id = ? ORDER BY id', task.id);
|
|
717
717
|
const failures = Number(task.consecutive_failures || 0);
|
|
718
|
+
const active = task.current_run_id ? one(db, 'SELECT id,started_at,metadata FROM task_runs WHERE id=?', task.current_run_id) : null;
|
|
719
|
+
const session = json(active?.metadata, {})?.supercode;
|
|
720
|
+
if (active && (session?.launch_pending || session?.launch_deferred || (!session?.session_id && !session?.pid))) {
|
|
721
|
+
const age = t - Number(session?.launched_at ?? active.started_at);
|
|
722
|
+
const reason = session?.launch_deferred_error ?? session?.launch_error ?? 'no session or process has been bound';
|
|
723
|
+
out.push({ kind: 'session_launch_pending', severity: age >= launchWindowSeconds ? 'error' : 'warning',
|
|
724
|
+
title: `run ${active.id} has no confirmed session after ${age}s: ${reason}`,
|
|
725
|
+
data: { run_id: active.id, age_seconds: age, window_seconds: launchWindowSeconds, reason, launch_key: session?.launch_key ?? null, pane: session?.pane ?? null },
|
|
726
|
+
actions: [`supercode workflow log ${task.id}`] });
|
|
727
|
+
}
|
|
718
728
|
if (failures >= Math.max(1, (task.max_retries ?? failureLimit) - 1) && task.status !== 'done') {
|
|
719
729
|
out.push({ kind: 'repeated_failures', severity: task.status === 'blocked' ? 'error' : 'warning', title: `${failures} consecutive failures`,
|
|
720
730
|
data: { failures, last_error: task.last_failure_error }, actions: [`supercode workflow reclaim ${task.id}`, `supercode workflow reassign ${task.id} <profile>`] });
|
package/board/rows.mjs
CHANGED
|
@@ -5,7 +5,7 @@ const quote = s => `"${s.replaceAll('"', '""')}"`;
|
|
|
5
5
|
const RUNTIME = new Set(['claim_lock','claim_expires','worker_pid','last_heartbeat_at','current_run_id','consecutive_failures','last_failure_error','block_recurrences','session_id','workspace_path']);
|
|
6
6
|
const OMIT = new Set(['tasks','supercode_cards','task_links','sqlite_sequence']);
|
|
7
7
|
export class RuntimeRows {
|
|
8
|
-
constructor(db) { this.db=db; this.revision=null; this.tables=new Map(); this.runtime={}; this.clocks={}; this.cachedTasks={}; this.cachedCards={}; }
|
|
8
|
+
constructor(db) { this.db=db; this.revision=null; this.tables=new Map(); this.runtime={}; this.clocks={}; this.sequences={}; this.cachedTasks={}; this.cachedCards={}; }
|
|
9
9
|
normalized() { return Boolean(this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='runtime_rows'").get()); }
|
|
10
10
|
watch(mem) {
|
|
11
11
|
mem.exec('CREATE TEMP TABLE IF NOT EXISTS _runtime_dirty (name TEXT, key TEXT)');
|
|
@@ -40,16 +40,24 @@ export class RuntimeRows {
|
|
|
40
40
|
const value=json(row.value,null);
|
|
41
41
|
if(row.name==='@runtime') { if(value)this.runtime[row.key]=value;else delete this.runtime[row.key]; }
|
|
42
42
|
else if(row.name==='@clock') { if(value)this.clocks[row.key]=value;else delete this.clocks[row.key]; }
|
|
43
|
+
else if(row.name==='@sequence') { if(value)this.sequences[row.key]=value;else delete this.sequences[row.key]; }
|
|
43
44
|
else if(row.name==='@task_cache') { if(value)this.cachedTasks[row.key]=value;else delete this.cachedTasks[row.key]; }
|
|
44
45
|
else if(row.name==='@card_cache') { if(value)this.cachedCards[row.key]=value;else delete this.cachedCards[row.key]; }
|
|
45
46
|
else this.insert(mem,row.name,value,row.key);
|
|
46
47
|
}
|
|
48
|
+
if (mem.prepare("SELECT 1 FROM sqlite_master WHERE name='sqlite_sequence'").get()) {
|
|
49
|
+
for (const [name, value] of Object.entries(this.sequences)) {
|
|
50
|
+
const sequence = mem.prepare('SELECT seq FROM sqlite_sequence WHERE name=?').get(name);
|
|
51
|
+
if (sequence) mem.prepare('UPDATE sqlite_sequence SET seq=MAX(seq,?) WHERE name=?').run(value.seq,name);
|
|
52
|
+
else mem.prepare('INSERT INTO sqlite_sequence(name,seq) VALUES(?,?)').run(name,value.seq);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
47
55
|
this.revision=Number(this.db.prepare("SELECT value FROM state WHERE key='rows_revision'").get()?.value??0);
|
|
48
56
|
return true;
|
|
49
57
|
}
|
|
50
58
|
changes(mem,{all=false}={}) {
|
|
51
59
|
const dirty=all?[...this.tables.values()].flatMap(t=>mem.prepare(`SELECT ${t.keys.map(quote).join(',')} FROM ${quote(t.name)}`).all().map(row=>({name:t.name,key:JSON.stringify(t.keys.map(k=>row[k]))}))):mem.prepare('SELECT DISTINCT name,key FROM _runtime_dirty').all();
|
|
52
|
-
|
|
60
|
+
const changes = dirty.map(({name,key})=>{
|
|
53
61
|
const table=this.tables.get(name);
|
|
54
62
|
const row=mem.prepare(`SELECT * FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).get(...JSON.parse(key))??null;
|
|
55
63
|
if(name==='tasks')return {name:'@runtime',key:JSON.parse(key)[0],value:row&&Object.fromEntries(Object.entries(row).filter(([k])=>RUNTIME.has(k))),source:{name,key,row}};
|
|
@@ -72,6 +80,12 @@ export class RuntimeRows {
|
|
|
72
80
|
if(name==='@clock')return JSON.stringify(source.row)!==JSON.stringify(this.cachedCards[key]??null);
|
|
73
81
|
return false;
|
|
74
82
|
});
|
|
83
|
+
if (mem.prepare("SELECT 1 FROM sqlite_master WHERE name='sqlite_sequence'").get()) {
|
|
84
|
+
for (const row of mem.prepare('SELECT name,seq FROM sqlite_sequence').all()) {
|
|
85
|
+
if (Number(this.sequences[row.name]?.seq) !== Number(row.seq)) changes.push({name:'@sequence',key:row.name,value:row,source:{name:'sqlite_sequence',key:JSON.stringify([row.name]),row}});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return changes;
|
|
75
89
|
}
|
|
76
90
|
mirror(changes, revision) {
|
|
77
91
|
const previous=Number(this.db.prepare("SELECT value FROM state WHERE key='read_revision'").get()?.value??-1);
|
|
@@ -108,6 +122,7 @@ export class RuntimeRows {
|
|
|
108
122
|
}
|
|
109
123
|
if(name==='@runtime') { if(value)this.runtime[key]=value;else delete this.runtime[key]; }
|
|
110
124
|
if(name==='@clock') { if(value)this.clocks[key]=value;else delete this.clocks[key]; }
|
|
125
|
+
if(name==='@sequence') { if(value)this.sequences[key]=value;else delete this.sequences[key]; }
|
|
111
126
|
}
|
|
112
127
|
for(const {name,sql} of this.tables.values())if(!OMIT.has(name))this.db.prepare('INSERT OR IGNORE INTO runtime_schema VALUES(?,?)').run(name,sql);
|
|
113
128
|
this.db.prepare("INSERT INTO state VALUES('rows_revision',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(revision));
|
package/board/workflow.mjs
CHANGED
|
@@ -338,7 +338,7 @@ function queueTransitionNotice(db, board, entry, task, from, name, payload, befo
|
|
|
338
338
|
const scope = scopeOf(db, board, after, { params: entry.params, event: { ...payload, verb: name, from, to }, extra: { run: { id: beforeRun.id } } });
|
|
339
339
|
const text = render(entry.workflow.notify?.worker?.run_changed ?? '{{card.id}}: {{event.verb}} changed run {{run.id}} ({{event.from}} → {{event.to}}). Stop work under the previous run; follow the board’s current state. {{event.reason ?? ""}}', scope);
|
|
340
340
|
const source = one(db, 'SELECT MAX(id) AS id FROM task_events WHERE task_id = ?', task.id)?.id;
|
|
341
|
-
const effect = { kind: 'session_notice', id: task.id, runId: beforeRun.id, address, text, sourceEvent: source, verb: name };
|
|
341
|
+
const effect = { kind: 'session_notice', id: task.id, runId: beforeRun.id, address, text, sourceEvent: source, verb: name, messageKey: `board-run-notice-${randomBytes(16).toString('hex')}` };
|
|
342
342
|
const inserted = run(db, 'INSERT INTO supercode_effects (effect) VALUES (?)', JSON.stringify(effect));
|
|
343
343
|
effects.push({ ...effect, durableId: Number(inserted.lastInsertRowid) });
|
|
344
344
|
const current = json(one(db, 'SELECT metadata FROM task_runs WHERE id = ?', beforeRun.id)?.metadata, {}) ?? {};
|
|
@@ -791,7 +791,7 @@ const ACTIONS = {
|
|
|
791
791
|
ctx.effects.push({ kind: 'board_message', id: ctx.id });
|
|
792
792
|
},
|
|
793
793
|
send_to_actor(ctx, text) {
|
|
794
|
-
const effect = { kind: 'send_to_actor', id: ctx.id, runId: ctx.runId, text };
|
|
794
|
+
const effect = { kind: 'send_to_actor', id: ctx.id, runId: ctx.runId, text, messageKey: `board-actor-${randomBytes(16).toString('hex')}` };
|
|
795
795
|
const inserted = run(ctx.db, 'INSERT INTO supercode_effects (effect) VALUES (?)', JSON.stringify(effect));
|
|
796
796
|
ctx.effects.push({ ...effect, durableId: Number(inserted.lastInsertRowid) });
|
|
797
797
|
},
|
package/board/ztrack.mjs
CHANGED
|
@@ -625,6 +625,7 @@ class ZtrackBoard {
|
|
|
625
625
|
if (id) sources.set(`tasks:${JSON.stringify([id])}`, {name:'tasks',key:JSON.stringify([id]),row:this.mem.prepare('SELECT * FROM tasks WHERE id=?').get(id)});
|
|
626
626
|
}
|
|
627
627
|
for (const {name,key,row:source} of sources.values()) {
|
|
628
|
+
if (name === 'sqlite_sequence') continue;
|
|
628
629
|
if (!compat.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name)) continue;
|
|
629
630
|
const table = this.rows.tables.get(name);
|
|
630
631
|
if (name === 'task_events') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.142",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|