@volter/supercode-orchestrator 0.4.37 → 0.4.38

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 CHANGED
@@ -12,7 +12,7 @@ import { read } from '../orchestration.mjs';
12
12
  import { holdDispatcherLock } from '../hermes-locks.mjs';
13
13
  import { boardPath, DEFAULT_BOARD, event, json, listBoards, now, openBoard, tx } from './store.mjs';
14
14
  import * as engine from './engine.mjs';
15
- import { adoptSession, boardLabel, boardSettings, lanesOf, localMachine, observeFleet, paused, startCard, tick } from './dispatch.mjs';
15
+ import { adoptSession, boardLabel, boardSettings, lanesOf, localMachine, observeFleet, paused, plan, startCard, tick } from './dispatch.mjs';
16
16
  import { loadWorkflow, statusOf, useWorkflow, workflowEntry, workflowFor } from './workflow.mjs';
17
17
  import { evaluate } from './expr.mjs';
18
18
 
@@ -681,8 +681,9 @@ export async function runWorkflow(argv) {
681
681
  const { lanes, settings } = await laneState(root);
682
682
  const params = { ...(args['failure-limit'] ? { failure_limit: Number(args['failure-limit']) } : {}), ...(args.max ? { max_spawn: Number(args.max) } : {}) };
683
683
  if (args['dry-run']) {
684
- const would = withBoard((b) => engine.listCards(b.db, { status: 'ready' }).filter((t) => t.assignee && lanes[t.assignee] && !t.subtask_of).map((t) => ({ id: t.id, assignee: t.assignee, machine: t.machine, surface: lanes[t.assignee].surface })));
685
- say(args, would.length ? would.map((w) => `would start ${w.id} as ${w.surface} for ${w.assignee}${w.machine ? ` on ${w.machine}` : ''}`).join('\n') : 'nothing would start', would);
684
+ // planned through the workflow's own dispatch (its start event, eligible, order and limits), writing nothing
685
+ const would = (await plan({ root, lanes, settings, params, slugs: [slug] })).flatMap((r) => r.would);
686
+ say(args, would.length ? would.map((w) => `would start ${w.id} (${w.status}${w.role ? `, its ${w.role}` : ''}) as ${w.surface} for ${w.assignee}${w.machine ? ` on ${w.machine}` : ''}`).join('\n') : 'nothing would start', would);
686
687
  return 0;
687
688
  }
688
689
  const reports = await tick({ root, lanes, settings, params });
@@ -266,7 +266,7 @@ const hostOf = (pressure) => ({ memory_pressure: pressure, max_in_progress: deri
266
266
  * the cards a reserve names while any waits). Each is sent the start event (its validators are Hermes's respawn
267
267
  * guard) and what its new status runs is launched.
268
268
  */
269
- function startCards(board, fleet, lanes, settings, report, entry, effects, { only = null, hostRunning = 0 } = {}) {
269
+ function startCards(board, fleet, lanes, settings, report, entry, effects, { only = null, hostRunning = 0, dry = false } = {}) {
270
270
  const stop = paused(board.root);
271
271
  if (stop) { report.paused = stop; return; }
272
272
  const { workflow, params } = entry;
@@ -321,10 +321,12 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
321
321
  if (evaluate(l.group, { card: {} }) === 'home') l.tally.set(JSON.stringify('home'), (l.tally.get(JSON.stringify('home')) ?? 0) + Math.max(0, hostRunning - here));
322
322
  }
323
323
  const reserveWaits = (l) => l.reserve && candidates.some((c) => holds(l.reserve.for, c.scope));
324
+ // a dry run (dispatch --dry-run) plans the same way and writes nothing: no notes, no moves, no launches
325
+ const note = (id, why) => { if (!dry) tx(board, (db) => noteGuard(db, id, why)); };
324
326
  for (const c of candidates) {
325
327
  const { task, plan, scope } = c;
326
- if (!plan.profile) { report.unassigned.push(task.id); tx(board, (db) => noteGuard(db, task.id, 'no profile runs it (no assignee, and the workflow names none)')); continue; }
327
- if (!plan.lane) { report.nonspawnable.push(task.id); tx(board, (db) => noteGuard(db, task.id, `${plan.profile} is no lane of this home`)); continue; }
328
+ if (!plan.profile) { report.unassigned.push(task.id); note(task.id, 'no profile runs it (no assignee, and the workflow names none)'); continue; }
329
+ if (!plan.lane) { report.nonspawnable.push(task.id); note(task.id, `${plan.profile} is no lane of this home`); continue; }
328
330
  let why = null;
329
331
  for (const l of limits) {
330
332
  const max = evaluate(l.max, scope);
@@ -335,7 +337,12 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
335
337
  }
336
338
  // the launcher's own conditions: a lane that can start this card where it names
337
339
  if (!why) why = launcherCheck(board.db, task, plan.lane, fleet);
338
- if (why) { tx(board, (db) => noteGuard(db, task.id, why)); report.queued.push([task.id, why]); continue; }
340
+ if (why) { note(task.id, why); report.queued.push([task.id, why]); continue; }
341
+ if (dry) {
342
+ report.would.push({ id: task.id, status: plan.target, role: plan.runSpec?.role ?? null, assignee: plan.profile, machine: task.machine ?? null, surface: plan.lane.surface });
343
+ for (const l of limits) if (holds(l.counts, scope)) { const g = JSON.stringify(evaluate(l.group, scope)); l.tally.set(g, (l.tally.get(g) ?? 0) + 1); }
344
+ continue;
345
+ }
339
346
  let moved;
340
347
  try {
341
348
  moved = tx(board, (db) => {
@@ -1029,14 +1036,25 @@ async function pool(items, n, fn) {
1029
1036
  }));
1030
1037
  }
1031
1038
 
1032
- /**
1033
- * One tick over every board of the home at `root`. `lanes` from `lanesOf`, `settings` from `boardSettings`.
1034
- * `only` limits starting to one card (a create that answers whether its card started). Answers one report per board.
1035
- */
1036
1039
  /** A board file's fingerprint: a corrupt board is skipped until the file changes (Hermes retries it then). */
1037
1040
  const fingerprint = (path) => { try { const st = statSync(path); return `${st.size}:${st.mtimeMs}`; } catch { return 'missing'; } };
1038
1041
  const CORRUPT = /malformed|not a database|corrupt|disk image/i;
1039
1042
 
1043
+ /** What a tick would start on each board of the home, planned through the workflow's `dispatch`, writing nothing. */
1044
+ export async function plan({ root, lanes, settings, slugs = null, params = {} }) {
1045
+ const loaded = await loadWorkflow(root, { fresh: true });
1046
+ const entry = { ...loaded, params: { ...loaded.params, ...params } };
1047
+ const fleet = Object.values(lanes).some((l) => l.surface === 'pane') ? observeFleet() : { ok: false, alive: new Set(), online: new Set(), sessions: [], error: 'no pane lanes' };
1048
+ const out = [];
1049
+ for (const slug of slugs ?? listBoards(root)) {
1050
+ const report = { board: slug, would: [], queued: [], unassigned: [], nonspawnable: [] };
1051
+ const board = openBoard(root, slug);
1052
+ try { board.label = boardLabel(root, slug); startCards(board, fleet, lanes, settings, report, entry, [], { dry: true }); } finally { board.close(); }
1053
+ out.push(report);
1054
+ }
1055
+ return out;
1056
+ }
1057
+
1040
1058
  /**
1041
1059
  * One tick over every board of the home at `root`. `lanes` from `lanesOf`, `settings` from `boardSettings`.
1042
1060
  * `only` limits starting to one card (a create that answers whether its card started). Each board is ticked under its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.37",
3
+ "version": "0.4.38",
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": {