@volter/supercode-orchestrator 0.5.29 → 0.5.31
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/dispatch.mjs +95 -19
- package/board/store.mjs +22 -0
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { homedir, hostname, tmpdir, totalmem } from 'node:os';
|
|
|
30
30
|
import { basename, dirname, join } from 'node:path';
|
|
31
31
|
import { createHash, randomUUID } from 'node:crypto';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
|
-
import { view, boardCopyOf, boardPath, claimer, copyRefusal, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
33
|
+
import { view, boardCopyOf, boardPath, claimer, claimHolds, copyRefusal, event, json, listBoards, logsDir, newClaim, now, openBoard, setActor, tx } from './store.mjs';
|
|
34
34
|
import { doors, fire, loadWorkflow, resumeOf, runSpec, runWorkingSeconds, requestSessionClose, retireSupersededRuns, refreshCardInputs, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, projectStatus, storedStatus, templateValue, workflowFor } from './workflow.mjs';
|
|
35
35
|
import { checkWorkspace, assertWorkspaceOwner, notePresence, resolveWorkspace } from './workspace.mjs';
|
|
36
36
|
import { capacity, harnessDefaults, holdings } from './capacity.mjs';
|
|
@@ -780,7 +780,7 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
|
|
|
780
780
|
|
|
781
781
|
/** Reconcile durable close intent and ended runs, including old unverified closed_at records.
|
|
782
782
|
* Reads and process I/O stay outside the writer. An active successor owns its pane until it ends. */
|
|
783
|
-
export function closeRequested(board, report, { taskId = null, caller = null } = {}) {
|
|
783
|
+
export function closeRequested(board, report, { taskId = null, caller = null, ttl = null } = {}) {
|
|
784
784
|
const rows = view(board, (db) => db.prepare(`SELECT id, task_id, ended_at, metadata FROM task_runs
|
|
785
785
|
WHERE metadata IS NOT NULL ${taskId ? 'AND task_id = ?' : ''}`).all(...(taskId ? [taskId] : [])));
|
|
786
786
|
// Repair pre-transfer history once: only identities with an older unended reference need a write.
|
|
@@ -849,8 +849,19 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
849
849
|
// every round (each attempt is a process). A deferred close (an offline machine, a run that still owns the pane) waits.
|
|
850
850
|
if (session.close_failed_at && session.close_error && !/^close deferred/.test(session.close_error)) continue;
|
|
851
851
|
const active = view(board, (db) => db.prepare(`SELECT r.id, r.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());
|
|
852
|
-
// Persist retries even for historical ends that did not have a close_session action.
|
|
853
|
-
|
|
852
|
+
// Persist retries even for historical ends that did not have a close_session action. The close is claimed in the
|
|
853
|
+
// same transaction, before the pane or process is touched: of two rounds (or a round and a verb) that read one close,
|
|
854
|
+
// only the one whose claim committed closes it, and the other never records the pane it finds gone as a failure.
|
|
855
|
+
const claimed = tx(board, (db) => {
|
|
856
|
+
const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
|
|
857
|
+
if (!current || current.close_confirmed_at || current.close_transferred_to || current.pane !== session.pane || current.session_id !== session.session_id || claimHolds(current.close_claim)) return false;
|
|
858
|
+
requestSessionClose(db, row.task_id, { runId: row.id });
|
|
859
|
+
// a close this process already claims, with a minute of its claim left, is tried again without a new claim
|
|
860
|
+
const own = current.close_claim?.lock === claimer() && Number(current.close_claim.expires) > now() + 60;
|
|
861
|
+
if (!own) setRunSession(db, row.id, { close_claim: newClaim(ttl ?? boardSettings(workflowFor(board.root).params).ttl) });
|
|
862
|
+
return true;
|
|
863
|
+
});
|
|
864
|
+
if (!claimed) continue;
|
|
854
865
|
let error = null;
|
|
855
866
|
try {
|
|
856
867
|
const machine = session.machine ?? localMachine();
|
|
@@ -887,6 +898,8 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
887
898
|
const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
|
|
888
899
|
if (!current || current.pane !== session.pane || current.session_id !== session.session_id) return;
|
|
889
900
|
const at = now();
|
|
901
|
+
// a close confirmed meanwhile (a claim that expired under a slow close) is not undone by this attempt's failure
|
|
902
|
+
if (error && current.close_confirmed_at) return;
|
|
890
903
|
if (error) {
|
|
891
904
|
const final = !/^close deferred/.test(error);
|
|
892
905
|
setRunSession(db, row.id, { ...current, close_attempted_at: at, close_attempts: Number(current.close_attempts ?? 0) + 1, close_error: error, ...(final ? { close_failed_at: at } : {}) });
|
|
@@ -1687,30 +1700,65 @@ export function requestAdoption(board, id, who, { workspace = null, assignee = n
|
|
|
1687
1700
|
/** How long a requested adoption waits for its session to appear in the fleet before it fails. */
|
|
1688
1701
|
const ADOPTION_LISTING_WINDOW_S = 180;
|
|
1689
1702
|
|
|
1690
|
-
/**
|
|
1703
|
+
/**
|
|
1704
|
+
* The events that answer an adoption request, or hold it: a request is open while it is the card's latest of these, and
|
|
1705
|
+
* a round that acts on it first records `adoption_claimed` (the request, its claim lock and when the claim expires).
|
|
1706
|
+
*/
|
|
1707
|
+
const ADOPTION_ANSWERS = "('dispatch_held','dispatch_released','session_adopted','adoption_failed','adoption_claimed')";
|
|
1708
|
+
const latestAnswer = (db, id) => db.prepare(`SELECT id, kind, payload FROM task_events WHERE task_id = ? AND kind IN ${ADOPTION_ANSWERS} ORDER BY id DESC LIMIT 1`).get(id) ?? null;
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Adoptions the manager requested and no tick has answered yet, completed from this tick's fleet reading. A round claims
|
|
1712
|
+
* a request in a transaction that finds it still open, before anything it does outside the board (`adoptSession`), so
|
|
1713
|
+
* of two rounds that read one request, one binds it and the other leaves it; the bind and the failure are each written
|
|
1714
|
+
* only while that claim is the card's latest.
|
|
1715
|
+
*/
|
|
1691
1716
|
export function completeAdoptions(board, fleet, settings, report, lanes = null) {
|
|
1692
|
-
const pending = view(board, db => db.prepare(`SELECT e.task_id, e.payload, e.created_at FROM task_events e
|
|
1693
|
-
AND json_extract(e.payload, '$.reason') = 'adoption requested'
|
|
1694
|
-
AND e.id = (SELECT MAX(id) FROM task_events l WHERE l.task_id = e.task_id AND l.kind IN
|
|
1717
|
+
const pending = view(board, db => db.prepare(`SELECT e.id, e.task_id, e.kind, e.payload, e.created_at FROM task_events e
|
|
1718
|
+
WHERE ((e.kind = 'dispatch_held' AND json_extract(e.payload, '$.reason') = 'adoption requested') OR e.kind = 'adoption_claimed')
|
|
1719
|
+
AND e.id = (SELECT MAX(id) FROM task_events l WHERE l.task_id = e.task_id AND l.kind IN ${ADOPTION_ANSWERS})`).all());
|
|
1695
1720
|
for (const row of pending) {
|
|
1696
|
-
|
|
1721
|
+
// a request another round has claimed is that round's while its claim holds; a claim whose round died is open again
|
|
1722
|
+
let request = row;
|
|
1723
|
+
if (row.kind === 'adoption_claimed') {
|
|
1724
|
+
const claim = json(row.payload, {}) ?? {};
|
|
1725
|
+
if (claimHolds(claim)) { (report.adoption_skipped ??= []).push([row.task_id, `claimed by ${claim.lock}`]); continue; }
|
|
1726
|
+
request = view(board, db => db.prepare('SELECT id, payload, created_at FROM task_events WHERE id = ?').get(claim.request));
|
|
1727
|
+
if (!request) continue;
|
|
1728
|
+
}
|
|
1729
|
+
const asked = json(request.payload, {}) ?? {};
|
|
1730
|
+
const progress = {};
|
|
1697
1731
|
try {
|
|
1698
|
-
const got = adoptSession(board, row.task_id, asked.session, { workspace: asked.workspace ?? null, assignee: asked.assignee ?? null, role: asked.role ?? null, ttl: settings.ttl, fleet: fleet?.ok ? fleet : null , lanes});
|
|
1732
|
+
const got = adoptSession(board, row.task_id, asked.session, { request: request.id, open: row.id, progress, workspace: asked.workspace ?? null, assignee: asked.assignee ?? null, role: asked.role ?? null, ttl: settings.ttl, fleet: fleet?.ok ? fleet : null , lanes});
|
|
1699
1733
|
(report.adopted ??= []).push([row.task_id, got.address]);
|
|
1700
1734
|
} catch (error) {
|
|
1701
1735
|
const message = String(error.message).slice(-400);
|
|
1702
1736
|
// A session opened moments before its adoption is asked may not be in the fleet's published set yet: the
|
|
1703
1737
|
// card stays held and each tick looks again, until the window closes
|
|
1704
|
-
if (error.notListed && Date.now() / 1000 - Number(
|
|
1738
|
+
if (error.notListed && Date.now() / 1000 - Number(request.created_at) < ADOPTION_LISTING_WINDOW_S) {
|
|
1705
1739
|
report.queued.push([row.task_id, `adoption of ${asked.session} waits: ${message}`]);
|
|
1706
1740
|
continue;
|
|
1707
1741
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1742
|
+
// the failure answers the request only while it is still open, or claimed by this round: an adoption that bound and
|
|
1743
|
+
// then failed after (its agent run not recorded) is bound, and a request another round claimed is that round's
|
|
1744
|
+
const answered = !error.superseded && tx(board, db => adoptionOpenTo(db, row.task_id, request.id, row.id) && (event(db, row.task_id, 'adoption_failed', { session: asked.session, error: message }), true));
|
|
1745
|
+
// A request another round answered or claimed first is that round's, not a failure of this one: it is noted where
|
|
1746
|
+
// no round prints it. A failure after this round's own claim (bound, then its agent run not recorded) is one.
|
|
1747
|
+
if (!answered && !progress.claimed) { (report.adoption_skipped ??= []).push([row.task_id, message]); continue; }
|
|
1748
|
+
report.failed.push([row.task_id, `adoption of ${asked.session}${answered ? '' : ' (bound by this round, then failed)'}: ${message}`]);
|
|
1710
1749
|
}
|
|
1711
1750
|
}
|
|
1712
1751
|
}
|
|
1713
1752
|
|
|
1753
|
+
/** Whether request `request` is still this round's to answer: the card's latest answer is `open`, or this process's claim of it. */
|
|
1754
|
+
function adoptionOpenTo(db, id, request, open) {
|
|
1755
|
+
const last = latestAnswer(db, id);
|
|
1756
|
+
if (!last) return false;
|
|
1757
|
+
if (last.id === open) return true;
|
|
1758
|
+
const claim = last.kind === 'adoption_claimed' ? json(last.payload, {}) ?? {} : null;
|
|
1759
|
+
return Boolean(claim) && claim.request === request && claim.lock === claimer();
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1714
1762
|
/**
|
|
1715
1763
|
* A session on this machine that Teams never published (one outside every synced root: a profile's own directory) is
|
|
1716
1764
|
* found through this machine's own doors: its panes, and the native session each pane's process runs. Only a full
|
|
@@ -1748,7 +1796,7 @@ function adoptedRole(db, board, entry, task, run) {
|
|
|
1748
1796
|
return runSpec(entry, t?.target ? String(templateValue(t.target, scope)) : status, task)?.role ?? null;
|
|
1749
1797
|
}
|
|
1750
1798
|
|
|
1751
|
-
function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
|
|
1799
|
+
function adoptSession(board, id, who, { request = null, open = request, progress = {}, workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
|
|
1752
1800
|
fleet ??= observeFleet();
|
|
1753
1801
|
if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
|
|
1754
1802
|
const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who) ?? localListing(who);
|
|
@@ -1777,6 +1825,20 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
|
|
|
1777
1825
|
const agentRole = view(board, (db) => adoptedRole(db, board, entry0, card0, beforeRun)) ?? 'implementer';
|
|
1778
1826
|
const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[agentRole] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
|
|
1779
1827
|
const lane = laneName ? lanes?.[laneName] : null, context = process.env.SUPERCODE_CONTEXT, team = agentScope(context);
|
|
1828
|
+
// The request is claimed before this adoption's first write outside the board (the agent's Teams record, below), in
|
|
1829
|
+
// a transaction that finds it still open and the card's run the one read: a request answered or claimed meanwhile, or
|
|
1830
|
+
// a run that changed, ends it here with nothing written anywhere, and no other round acts on a request claimed here.
|
|
1831
|
+
const stale = (db) => {
|
|
1832
|
+
if (request !== null && !adoptionOpenTo(db, id, request, open)) return Object.assign(new Error(`${id}'s adoption request was answered or claimed meanwhile`), { superseded: true });
|
|
1833
|
+
if ((getTask(db, id)?.current_run_id ?? null) !== (beforeRun?.id ?? null)) return new Error(`${id} changed runs during adoption; retry against its current run`);
|
|
1834
|
+
return null;
|
|
1835
|
+
};
|
|
1836
|
+
if (request !== null) tx(board, (db) => {
|
|
1837
|
+
const refused = stale(db);
|
|
1838
|
+
if (refused) throw refused;
|
|
1839
|
+
event(db, id, 'adoption_claimed', { request, session: who, ...newClaim(ttl) });
|
|
1840
|
+
});
|
|
1841
|
+
progress.claimed = request !== null;
|
|
1780
1842
|
let agent = null;
|
|
1781
1843
|
if (lane && !team.offline) {
|
|
1782
1844
|
try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
|
|
@@ -1789,7 +1851,9 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
|
|
|
1789
1851
|
const adopted = tx(board, (db) => {
|
|
1790
1852
|
const task = getTask(db, id);
|
|
1791
1853
|
if (!task) throw new Error(`no card ${id}`);
|
|
1792
|
-
|
|
1854
|
+
// the bind is made only while this round's claim is the card's latest and the run is the one read
|
|
1855
|
+
const changed = stale(db);
|
|
1856
|
+
if (changed) throw changed;
|
|
1793
1857
|
if (beforeRun && assignee && assignee !== task.assignee) throw new Error('rebinding a session does not reassign its active run');
|
|
1794
1858
|
if (task.machine && normalMachine(task.machine) !== host) throw new Error(`${id} runs on ${task.machine}, and ${found.address} is on ${host}; move the card first`);
|
|
1795
1859
|
const was = lastSession(db, id);
|
|
@@ -2193,17 +2257,22 @@ function fireHooks(board, settings, report) {
|
|
|
2193
2257
|
child.unref();
|
|
2194
2258
|
}
|
|
2195
2259
|
};
|
|
2196
|
-
|
|
2260
|
+
// the events are taken (the cursor moved past them) in one transaction and their hooks run once it commits: a
|
|
2261
|
+
// transaction run again on contention, or a second round, never runs a hook twice
|
|
2262
|
+
const due = tx(board, (db) => {
|
|
2197
2263
|
db.exec('CREATE TABLE IF NOT EXISTS supercode_board_state (key TEXT PRIMARY KEY, value TEXT)');
|
|
2198
2264
|
const max = Number(db.prepare('SELECT COALESCE(MAX(id), 0) AS m FROM task_events').get().m);
|
|
2199
2265
|
const row = db.prepare("SELECT value FROM supercode_board_state WHERE key = 'hooks_cursor'").get();
|
|
2200
2266
|
const cursor = row ? Number(row.value) : max; // the first tick starts at the present, never replays history
|
|
2267
|
+
const taken = [];
|
|
2201
2268
|
for (const ev of db.prepare('SELECT e.id, e.task_id, e.run_id, e.kind, e.payload, e.created_at, t.assignee FROM task_events e LEFT JOIN tasks t ON t.id = e.task_id WHERE e.id > ? ORDER BY e.id').all(cursor)) {
|
|
2202
2269
|
const hook = HOOK_OF[ev.kind];
|
|
2203
|
-
if (hook && hooks[hook])
|
|
2270
|
+
if (hook && hooks[hook]) taken.push([hook, { task_id: ev.task_id, run_id: ev.run_id, event: ev.kind, assignee: ev.assignee, payload: json(ev.payload, null), at: ev.created_at }]);
|
|
2204
2271
|
}
|
|
2205
2272
|
db.prepare("INSERT INTO supercode_board_state (key, value) VALUES ('hooks_cursor', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(max));
|
|
2273
|
+
return taken;
|
|
2206
2274
|
});
|
|
2275
|
+
for (const [hook, payload] of due) run(hook, payload);
|
|
2207
2276
|
if (hooks.on_kanban_dispatch_tick) run('on_kanban_dispatch_tick', { started: report.started.length, reclaimed: report.reclaimed.length, crashed: report.crashed.length, timed_out: report.timed_out.length, promoted: report.promoted });
|
|
2208
2277
|
}
|
|
2209
2278
|
|
|
@@ -2410,7 +2479,14 @@ function capMax(cap, group, entry, lanes) {
|
|
|
2410
2479
|
/**
|
|
2411
2480
|
* One tick over every board of the home at `root`. `lanes` from `lanesOf`, `settings` from `boardSettings`.
|
|
2412
2481
|
* `only` limits starting to one card (a create that answers whether its card started). Each board is ticked under its
|
|
2413
|
-
* version-checked card claims; no dispatcher file lock.
|
|
2482
|
+
* version-checked card claims; no dispatcher file lock. Two rounds may run at once (the dispatcher's and a one-shot
|
|
2483
|
+
* `workflow dispatch`). An adoption request and a session close are claimed in a transaction (`newClaim`) before
|
|
2484
|
+
* anything is done for them outside the board; card starts, supervision, review requests, mailed verbs and manual
|
|
2485
|
+
* continuations rely on their per-card transactions. Two rounds on one board at once can still both act on: queued
|
|
2486
|
+
* effects in `runEffects` (send_to_actor, session_notice, merge_reviewed, declare_card_agent), board mail,
|
|
2487
|
+
* subscription notices (a doubled notice_delivered has been seen live), deferred-launch retries (two panes),
|
|
2488
|
+
* launch-receipt work (cancelUnissuedLaunch, bindLaunch), the Teams agent-run sync in reconcileLaunches, and
|
|
2489
|
+
* board-only writes that do not re-check (a doubled agent_profile_recorded). Answers one report per board.
|
|
2414
2490
|
*/
|
|
2415
2491
|
export async function tick({ root, lanes, settings, only = null, slugs = null, dispatch = true, params = {}, deploymentReceipt = false }) {
|
|
2416
2492
|
eventFleetRoot = root;
|
|
@@ -2451,7 +2527,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
2451
2527
|
readMailbox(board, boardAddress(board.label), report); phase('mailbox');
|
|
2452
2528
|
await refreshRelationAnswers(board); phase('answers');
|
|
2453
2529
|
const effects = [];
|
|
2454
|
-
completeAdoptions(board, fleet, settings, report, lanes); reconnectContinuedSessions(board, fleet, entry, report); reconcileLaunches(board, fleet, report, entry, effects, settings, lanes); supervise(board, fleet, settings, report, entry, effects); await runEffects(board, effects.splice(0), { retryNotices: true }); closeRequested(board, report); phase('supervise');
|
|
2530
|
+
completeAdoptions(board, fleet, settings, report, lanes); reconnectContinuedSessions(board, fleet, entry, report); reconcileLaunches(board, fleet, report, entry, effects, settings, lanes); supervise(board, fleet, settings, report, entry, effects); await runEffects(board, effects.splice(0), { retryNotices: true }); closeRequested(board, report, { ttl: settings.ttl }); phase('supervise');
|
|
2455
2531
|
report.unsettled = [];
|
|
2456
2532
|
report.promoted = recomputeReady(board, effects, report.unsettled);
|
|
2457
2533
|
tx(board,db=>auditEndedRuns(db,board,entry));
|
package/board/store.mjs
CHANGED
|
@@ -259,6 +259,28 @@ export const newTaskId = () => `t_${randomBytes(4).toString('hex')}`;
|
|
|
259
259
|
/** The claim lock this process writes, in Hermes's `host:pid` form. */
|
|
260
260
|
export const claimer = () => `${hostname()}:${process.pid}`;
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* A claim on one request a round acts on outside the board (an adoption request, a session close): written in the
|
|
264
|
+
* transaction that finds the request still open, before anything outside the board is done for it, so of two rounds
|
|
265
|
+
* that read one request only the one whose claim committed acts on it. It is this process's claim lock and an expiry
|
|
266
|
+
* one claim TTL out, as a card's claim is.
|
|
267
|
+
*/
|
|
268
|
+
export const newClaim = (ttl) => ({ lock: claimer(), expires: now() + (Number(ttl) > 0 ? Number(ttl) : 15 * 60) });
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Whether a claim keeps the caller off its request: another process's, unexpired, and that process alive where its pid
|
|
272
|
+
* can be read (this machine). A claim whose process died is open again at once here, and at its expiry from another
|
|
273
|
+
* machine; the caller's own claim never keeps it off (its rounds run one after another).
|
|
274
|
+
*/
|
|
275
|
+
export function claimHolds(claim) {
|
|
276
|
+
if (!claim?.lock || claim.lock === claimer() || Number(claim.expires) <= now()) return false;
|
|
277
|
+
const lock = String(claim.lock), at = lock.lastIndexOf(':');
|
|
278
|
+
if (lock.slice(0, at) !== hostname()) return true;
|
|
279
|
+
const pid = Number(lock.slice(at + 1));
|
|
280
|
+
if (!(pid > 0)) return false;
|
|
281
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
|
|
282
|
+
}
|
|
283
|
+
|
|
262
284
|
export function json(text, fallback = null) {
|
|
263
285
|
if (text === null || text === undefined || text === '') return fallback;
|
|
264
286
|
try { return JSON.parse(text); } catch { return fallback; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.31",
|
|
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": {
|