@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 +25 -4
- package/board/engine.mjs +13 -1
- package/board/workflow.mjs +21 -1
- package/package.json +1 -1
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} →
|
|
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:
|
|
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) {
|
package/board/workflow.mjs
CHANGED
|
@@ -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.
|
|
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": {
|