@volter/supercode-orchestrator 0.4.33 → 0.4.35
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 +18 -4
- package/board/dispatch.mjs +4 -1
- package/board/engine.mjs +2 -40
- package/board/workflow.mjs +3 -1
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -45,6 +45,7 @@ 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.
|
|
48
49
|
|
|
49
50
|
specify/decompose with no spec start a session on the triage card to write it. A card's own session may comment on,
|
|
50
51
|
block (with --kind), heartbeat and request review of its card, specify or decompose it in triage, and file, complete,
|
|
@@ -555,8 +556,8 @@ export async function runWorkflow(argv) {
|
|
|
555
556
|
const results = [];
|
|
556
557
|
for (const t of targets) {
|
|
557
558
|
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 });
|
|
559
|
+
const moved = await withBoardAsync((b) => engine.specify(b, t, { title: args.title ?? null, body: args.body ?? null, author: args.author ?? callerAddress() ?? callerName() }));
|
|
560
|
+
results.push({ id: t, specified: true, status: moved.to });
|
|
560
561
|
} else if (verb === 'decompose' && args.child?.length) {
|
|
561
562
|
const children = args.child.map((c) => { const [title, assignee] = c.split(/@(?=[^@]*$)/); return { title: title.trim(), assignee: assignee?.trim() || undefined }; });
|
|
562
563
|
results.push({ id: t, children: await withBoardAsync((b) => engine.decompose(b, t, children, { author: args.author ?? callerAddress() ?? callerName() })) });
|
|
@@ -565,7 +566,7 @@ export async function runWorkflow(argv) {
|
|
|
565
566
|
results.push({ id: t, session: session.address, mode: session.mode });
|
|
566
567
|
}
|
|
567
568
|
}
|
|
568
|
-
say(args, results.map((r) => (r.specified ? `Specified ${r.id} →
|
|
569
|
+
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
570
|
return 0;
|
|
570
571
|
}
|
|
571
572
|
|
|
@@ -746,7 +747,20 @@ export async function runWorkflow(argv) {
|
|
|
746
747
|
return 0;
|
|
747
748
|
}
|
|
748
749
|
|
|
749
|
-
default:
|
|
750
|
+
default: {
|
|
751
|
+
// any other event the board's workflow declares is a verb of its own: `<event> <id>... [reason] [--<field> value]`,
|
|
752
|
+
// the flags its payload (the doors a session is told name these)
|
|
753
|
+
const name = verb.replace(/-/g, '_');
|
|
754
|
+
const { workflow } = workflowFor(root);
|
|
755
|
+
const declared = Object.values(workflow.statuses).some((s) => s.on?.[name]) || Boolean(workflow.on?.[name]);
|
|
756
|
+
if (!declared) throw new Error(`no workflow verb ${verb}; \`supercode workflow help\` lists them`);
|
|
757
|
+
need(1, 'a card id');
|
|
758
|
+
const { _: _words, root: _r, board: _b, json: _j, at: _a, ids: _i, ...fields } = args;
|
|
759
|
+
const payload = { ...fields, reason: args.reason ?? (rest.slice(1).join(' ') || null) };
|
|
760
|
+
const results = [];
|
|
761
|
+
for (const t of [id, ...(args.ids ?? [])]) results.push([t, (await withBoardAsync((b) => engine.act(b, t, name, { payload }))).to]);
|
|
762
|
+
say(args, results.map(([t, s]) => `${t} → ${s}`).join('\n'), { ok: true, results }); return 0;
|
|
763
|
+
}
|
|
750
764
|
}
|
|
751
765
|
}
|
|
752
766
|
|
package/board/dispatch.mjs
CHANGED
|
@@ -523,7 +523,10 @@ function memoryPressure() {
|
|
|
523
523
|
|
|
524
524
|
/** Record a guard reason once per change, not once per tick. */
|
|
525
525
|
function noteGuard(db, id, reason) {
|
|
526
|
-
|
|
526
|
+
// once per reason per entry into the status: a card back in ready (a new `since`) is noted again, so the workflow's
|
|
527
|
+
// after-timers that read the reason (card.queue_reason) see it
|
|
528
|
+
const since = Number(db.prepare('SELECT since FROM supercode_cards WHERE task_id = ?').get(id)?.since ?? 0);
|
|
529
|
+
const last = db.prepare("SELECT payload FROM task_events WHERE task_id = ? AND kind IN ('respawn_guarded', 'claimed') AND created_at >= ? ORDER BY id DESC LIMIT 1").get(id, since);
|
|
527
530
|
if (json(last?.payload, {})?.reason === reason) return;
|
|
528
531
|
event(db, id, 'respawn_guarded', { reason });
|
|
529
532
|
}
|
package/board/engine.mjs
CHANGED
|
@@ -130,15 +130,6 @@ function retryStatus(db, taskId) {
|
|
|
130
130
|
return source === 'review' || source === 'triage' ? source : 'ready';
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/** The phase a card blocked out of comes back to (Hermes reads it off the latest interruption). */
|
|
134
|
-
function resumeStatus(db, taskId) {
|
|
135
|
-
const row = one(db, `SELECT payload FROM task_events WHERE task_id = ? AND kind IN ('blocked', 'block_loop_detected', 'dependency_wait', 'gave_up',
|
|
136
|
-
'unblocked', 'changes_requested', 'review_reopened', 'status', 'reclaimed', 'stale', 'timed_out', 'crashed', 'spawn_failed', 'rate_limited', 'session_lost')
|
|
137
|
-
ORDER BY id DESC LIMIT 1`, taskId);
|
|
138
|
-
const p = json(row?.payload, {}) ?? {};
|
|
139
|
-
return ['resume_status', 'retry_status', 'source_status'].some((k) => p[k] === 'review') ? 'review' : 'ready';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
133
|
// ------------------------------------------------------------------ creating and linking
|
|
143
134
|
|
|
144
135
|
/**
|
|
@@ -290,35 +281,6 @@ export function extendClaim(db, id, { ttl, reason, extra = {} }) {
|
|
|
290
281
|
return true;
|
|
291
282
|
}
|
|
292
283
|
|
|
293
|
-
/** Put a running card back where its run came from, closing the run with `outcome`. Answers the closed run id. */
|
|
294
|
-
export function release(db, id, { outcome = 'reclaimed', error = null, metadata = undefined, kind = 'reclaimed', payload = {} } = {}) {
|
|
295
|
-
const task = one(db, 'SELECT status, claim_lock FROM tasks WHERE id = ?', id);
|
|
296
|
-
if (!task || task.status !== 'running') return null;
|
|
297
|
-
const retry = retryStatus(db, id);
|
|
298
|
-
run(db, 'UPDATE tasks SET status = ?, claim_lock = NULL, claim_expires = NULL, worker_pid = NULL WHERE id = ?', retry, id);
|
|
299
|
-
const runId = endRun(db, id, { outcome, status: outcome, error, metadata });
|
|
300
|
-
event(db, id, kind, { ...payload, retry_status: retry }, runId);
|
|
301
|
-
return runId;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Count a failure against the card (Hermes's circuit breaker): at the card's `max_retries`, else `failureLimit`,
|
|
306
|
-
* consecutive failures it is blocked with a `gave_up` event. Answers true when it tripped.
|
|
307
|
-
*/
|
|
308
|
-
export function recordFailure(db, id, error, { failureLimit = DEFAULT_FAILURE_LIMIT, outcome = 'crashed' } = {}) {
|
|
309
|
-
const task = one(db, 'SELECT consecutive_failures, max_retries, status FROM tasks WHERE id = ?', id);
|
|
310
|
-
if (!task) return false;
|
|
311
|
-
const failures = Number(task.consecutive_failures || 0) + 1;
|
|
312
|
-
run(db, 'UPDATE tasks SET consecutive_failures = ?, last_failure_error = ? WHERE id = ?', failures, String(error).slice(0, 500), id);
|
|
313
|
-
const limit = task.max_retries ?? failureLimit;
|
|
314
|
-
if (failures < limit) return false;
|
|
315
|
-
if (task.status === 'running') endRun(db, id, { outcome, error: String(error) });
|
|
316
|
-
run(db, "UPDATE tasks SET status = 'blocked', claim_lock = NULL, claim_expires = NULL, worker_pid = NULL WHERE id = ?", id);
|
|
317
|
-
event(db, id, 'gave_up', { error: String(error).slice(0, 500), failures, limit, outcome });
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
284
|
/**
|
|
323
285
|
* A completed card's workspace goes, as Hermes cleans up on completion: a scratch directory under the board's own
|
|
324
286
|
* workspaces, or a worktree (its branch stays); never while a child still waits on it, never a `dir`.
|
|
@@ -541,8 +503,8 @@ export const subscriptions = (db, id = null) => (id ? all(db, 'SELECT * FROM kan
|
|
|
541
503
|
|
|
542
504
|
export function listCards(db, { status = null, assignee = null, machine = null, includeArchived = false } = {}) {
|
|
543
505
|
const rows = all(db, `SELECT t.*, c.machine, c.subtask_of, c.stage FROM tasks t LEFT JOIN supercode_cards c ON c.task_id = t.id
|
|
544
|
-
WHERE (? IS NULL OR t.status = ?) AND (? IS NULL OR t.assignee = ?) AND (? IS NULL OR c.machine = ?) AND (? OR t.status != 'archived')
|
|
545
|
-
ORDER BY t.priority DESC, t.created_at ASC`, status, status, assignee, assignee, machine, machine, includeArchived ? 1 : 0);
|
|
506
|
+
WHERE (? IS NULL OR t.status = ? OR c.stage = ?) AND (? IS NULL OR t.assignee = ?) AND (? IS NULL OR c.machine = ?) AND (? OR t.status != 'archived')
|
|
507
|
+
ORDER BY t.priority DESC, t.created_at ASC`, status, status, status, assignee, assignee, machine, machine, includeArchived ? 1 : 0);
|
|
546
508
|
return rows.map((r) => ({ ...r, skills: json(r.skills, []) ?? [] }));
|
|
547
509
|
}
|
|
548
510
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.35",
|
|
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": {
|