@volter/supercode-orchestrator 0.4.34 → 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
@@ -45,6 +45,8 @@ supercode's own:
45
45
  move <id> <machine|none> adopt <id> <session> [--assignee P] close <id> serve (= daemon) list --machine M
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
+ <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.
48
50
 
49
51
  specify/decompose with no spec start a session on the triage card to write it. A card's own session may comment on,
50
52
  block (with --kind), heartbeat and request review of its card, specify or decompose it in triage, and file, complete,
@@ -555,8 +557,8 @@ export async function runWorkflow(argv) {
555
557
  const results = [];
556
558
  for (const t of targets) {
557
559
  if (verb === 'specify' && (args.title !== undefined || args.body !== undefined)) {
558
- await withBoardAsync((b) => engine.specify(b, t, { title: args.title ?? null, body: args.body ?? null, author: args.author ?? callerAddress() ?? callerName() }));
559
- results.push({ id: t, specified: true });
560
+ const moved = await withBoardAsync((b) => engine.specify(b, t, { title: args.title ?? null, body: args.body ?? null, author: args.author ?? callerAddress() ?? callerName() }));
561
+ results.push({ id: t, specified: true, status: moved.to });
560
562
  } else if (verb === 'decompose' && args.child?.length) {
561
563
  const children = args.child.map((c) => { const [title, assignee] = c.split(/@(?=[^@]*$)/); return { title: title.trim(), assignee: assignee?.trim() || undefined }; });
562
564
  results.push({ id: t, children: await withBoardAsync((b) => engine.decompose(b, t, children, { author: args.author ?? callerAddress() ?? callerName() })) });
@@ -565,7 +567,7 @@ export async function runWorkflow(argv) {
565
567
  results.push({ id: t, session: session.address, mode: session.mode });
566
568
  }
567
569
  }
568
- say(args, results.map((r) => (r.specified ? `Specified ${r.id} → todo` : r.children ? `Decomposed ${r.id} into ${r.children.join(', ')}` : `${r.id}: session ${r.session} (${r.mode}) is writing its ${verb === 'specify' ? 'spec' : 'breakdown'}`)).join('\n'), results);
570
+ say(args, results.map((r) => (r.specified ? `Specified ${r.id} → ${r.status}` : r.children ? `Decomposed ${r.id} into ${r.children.join(', ')}` : `${r.id}: session ${r.session} (${r.mode}) is writing its ${verb === 'specify' ? 'spec' : 'breakdown'}`)).join('\n'), results);
569
571
  return 0;
570
572
  }
571
573
 
@@ -739,6 +741,12 @@ export async function runWorkflow(argv) {
739
741
  say(args, `${id} is worked by ${got.address} (pane ${got.pane ?? '?'} in ${got.workspace ?? 'its card\'s directory'})`, { ok: true, ...got });
740
742
  return 0;
741
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
+ }
742
750
  case 'close': {
743
751
  need(1, 'a card id');
744
752
  const closed = withBoard((b) => closeSession(b, id));
@@ -746,7 +754,20 @@ export async function runWorkflow(argv) {
746
754
  return 0;
747
755
  }
748
756
 
749
- default: throw new Error(`no workflow verb ${verb}; \`supercode workflow help\` lists them`);
757
+ default: {
758
+ // any other event the board's workflow declares is a verb of its own: `<event> <id>... [reason] [--<field> value]`,
759
+ // the flags its payload (the doors a session is told name these)
760
+ const name = verb.replace(/-/g, '_');
761
+ const { workflow } = workflowFor(root);
762
+ const declared = Object.values(workflow.statuses).some((s) => s.on?.[name]) || Boolean(workflow.on?.[name]);
763
+ if (!declared) throw new Error(`no workflow verb ${verb}; \`supercode workflow help\` lists them`);
764
+ need(1, 'a card id');
765
+ const { _: _words, root: _r, board: _b, json: _j, at: _a, ids: _i, ...fields } = args;
766
+ const payload = { ...fields, reason: args.reason ?? (rest.slice(1).join(' ') || null) };
767
+ const results = [];
768
+ for (const t of [id, ...(args.ids ?? [])]) results.push([t, (await withBoardAsync((b) => engine.act(b, t, name, { payload }))).to]);
769
+ say(args, results.map(([t, s]) => `${t} → ${s}`).join('\n'), { ok: true, results }); return 0;
770
+ }
750
771
  }
751
772
  }
752
773
 
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) {
@@ -94,7 +94,9 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
94
94
  goal_mode: Boolean(task.goal_mode),
95
95
  implementer: asked.implementer ?? task.assignee,
96
96
  last_reviewer: changes.reviewer ?? null,
97
- arc: task.subtask_of ? one(db, 'SELECT id, status, title FROM tasks WHERE id = ?', task.subtask_of) ?? null : null,
97
+ arc: task.subtask_of ? one(db, 'SELECT id, status, title, assignee FROM tasks WHERE id = ?', task.subtask_of) ?? null : null,
98
+ // the cards filed as its subtasks (`create --subtask-of`), in the order they were filed
99
+ subtasks: all(db, 'SELECT t.id, t.status, t.title, t.assignee FROM tasks t JOIN supercode_cards c ON c.task_id = t.id WHERE c.subtask_of = ? ORDER BY t.created_at', task.id),
98
100
  queue_reason: json(one(db, "SELECT payload FROM task_events WHERE task_id = ? AND kind = 'respawn_guarded' AND created_at >= ? ORDER BY id DESC LIMIT 1",
99
101
  task.id, Number(one(db, 'SELECT since FROM supercode_cards WHERE task_id = ?', task.id)?.since ?? 0))?.payload, {})?.reason ?? null,
100
102
  last_block_event: one(db, "SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('blocked', 'unblocked') ORDER BY id DESC LIMIT 1", task.id)?.kind ?? null,
@@ -166,6 +168,24 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
166
168
  return { from, to: statusOf(db, cardOf(db, id)), moved: to !== from, effects };
167
169
  }
168
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
+
169
189
  function cardOf(db, id) {
170
190
  const row = one(db, 'SELECT * FROM tasks WHERE id = ?', id);
171
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.34",
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": {