@volter/supercode-orchestrator 0.4.35 → 0.4.36

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
@@ -46,6 +46,7 @@ supercode's own:
46
46
  notify-subscribe <id> --address sc:<machine>:<harness>:<id> [--role creator|worker|dependency]
47
47
  Any verb … --at sc:<machine>:board:<home>: sent as mail to a board kept on another machine.
48
48
  <event> <id>... [reason] [--<field> value]: any other event the board's workflow declares, its flags the event's fields.
49
+ goto <id> <status> [reason] [--<field> value]: the event the workflow says takes the card to that status, sent.
49
50
 
50
51
  specify/decompose with no spec start a session on the triage card to write it. A card's own session may comment on,
51
52
  block (with --kind), heartbeat and request review of its card, specify or decompose it in triage, and file, complete,
@@ -740,6 +741,12 @@ export async function runWorkflow(argv) {
740
741
  say(args, `${id} is worked by ${got.address} (pane ${got.pane ?? '?'} in ${got.workspace ?? 'its card\'s directory'})`, { ok: true, ...got });
741
742
  return 0;
742
743
  }
744
+ case 'goto': {
745
+ need(2, 'a card id and the status to take it to');
746
+ const { _: _words, root: _r, board: _b, json: _j, at: _a, ...fields } = args;
747
+ const moved = await withBoardAsync((b) => engine.goto(b, id, rest[1], { payload: { ...fields, reason: args.reason ?? (rest.slice(2).join(' ') || null) } }));
748
+ say(args, moved.event ? `${id} → ${moved.to} (${moved.event.replace(/_/g, '-')})` : `${id} is already ${moved.to}`, { ok: true, ...moved }); return 0;
749
+ }
743
750
  case 'close': {
744
751
  need(1, 'a card id');
745
752
  const closed = withBoard((b) => closeSession(b, id));
package/board/engine.mjs CHANGED
@@ -12,7 +12,7 @@ import { existsSync, rmSync } from 'node:fs';
12
12
  import { join, resolve } from 'node:path';
13
13
  import { DatabaseSync } from 'node:sqlite';
14
14
  import { claimer, event, json, newTaskId, now, tx } from './store.mjs';
15
- import { fire, loadWorkflow, setStatus, settle, startNew, statusOf, workflowFor } from './workflow.mjs';
15
+ import { eventTo, fire, loadWorkflow, setStatus, settle, startNew, statusOf, workflowFor } from './workflow.mjs';
16
16
 
17
17
  export const STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'review', 'done', 'archived'];
18
18
  export const BLOCK_KINDS = ['dependency', 'needs_input', 'capability', 'transient'];
@@ -334,6 +334,18 @@ export async function act(board, id, name, { roles = null, payload = {}, extra =
334
334
  return { from: result.from, to: result.to, moved: result.moved };
335
335
  }
336
336
 
337
+ /** Take card `id` to `status` by the event the workflow says goes there for this caller (`goto`). */
338
+ export async function goto(board, id, status, { roles = null, payload = {} } = {}) {
339
+ const entry = await loadWorkflow(board.root);
340
+ const { name, from } = tx(board, (db) => {
341
+ roles ??= callerRoles(db, id);
342
+ return { name: eventTo(db, board, entry, id, status, { roles, payload }), from: statusOf(db, getTask(db, id)) };
343
+ });
344
+ if (from === status) return { from, to: status, moved: false, event: null };
345
+ if (!name) throw new Error(`${id} is ${from}; no event you may send takes it to ${status}`);
346
+ return { ...(await act(board, id, name, { roles, payload })), event: name };
347
+ }
348
+
337
349
  /** The I/O a committed move asked for. */
338
350
  export async function runEffects(board, effects = []) {
339
351
  for (const e of effects) {
@@ -168,6 +168,24 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
168
168
  return { from, to: statusOf(db, cardOf(db, id)), moved: to !== from, effects };
169
169
  }
170
170
 
171
+ /**
172
+ * The event that takes card `id` to `status` for a sender in `roles`: the first the card's status (then the workflow)
173
+ * declares whose transition for that sender, with `payload`, goes there. Null when none does. Its validators are
174
+ * the event's own, checked when it is sent.
175
+ */
176
+ export function eventTo(db, board, entry, id, status, { roles = [], payload = {} } = {}) {
177
+ const { workflow, params } = entry;
178
+ if (!workflow.statuses[status]) throw new Error(`the board's workflow declares no status ${status}`);
179
+ const task = cardOf(db, id);
180
+ const from = statusOf(db, task);
181
+ const scope = scopeOf(db, board, task, { params, event: payload, extra: { lanes: lanesIn(entry), sender: roles } });
182
+ for (const name of new Set([...Object.keys(workflow.statuses[from]?.on ?? {}), ...Object.keys(workflow.on ?? {})])) {
183
+ const taken = transitionsFor(workflow, from, name).filter((t) => !t.by?.length || t.by.some((r) => roles.includes(r))).find((t) => holds(t.guard ?? null, scope));
184
+ if (taken?.target && String(templateValue(taken.target, scope) ?? '') === status) return name;
185
+ }
186
+ return null;
187
+ }
188
+
171
189
  function cardOf(db, id) {
172
190
  const row = one(db, 'SELECT * FROM tasks WHERE id = ?', id);
173
191
  if (!row) throw new Error(`no card ${id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.35",
3
+ "version": "0.4.36",
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": {