@volter/supercode-orchestrator 0.4.43 → 0.4.45
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 +3 -2
- package/board/dispatch.mjs +14 -10
- package/board/workflow.mjs +21 -4
- package/package.json +1 -1
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 { backingOf, boardExists, boardPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, tx } from './store.mjs';
|
|
14
14
|
import * as engine from './engine.mjs';
|
|
15
|
-
import { adoptSession, boardLabel, boardSettings, lanesOf, localMachine, observeFleet, paused, plan, startCard, tick } from './dispatch.mjs';
|
|
15
|
+
import { adoptSession, boardAddress, 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
|
|
|
@@ -1050,7 +1050,8 @@ function closeSession(board, id) {
|
|
|
1050
1050
|
if (!task) throw new Error(`no card ${id}`);
|
|
1051
1051
|
if (task.status === 'running') throw new Error(`${id} is running; block, complete or reclaim it before closing its session`);
|
|
1052
1052
|
if (!session?.pane) return null;
|
|
1053
|
-
|
|
1053
|
+
// the board closes its card's pane, whichever session ran `workflow close`
|
|
1054
|
+
const r = spawnSync(process.env.SUPERCODE_BIN || 'supercode', ['teams', 'panes', 'close', session.pane, '--machine', session.machine], { cwd: tmpdir(), encoding: 'utf8', timeout: 60_000, env: { ...process.env, SUPERCODE_CALLER: boardAddress(board.label) } });
|
|
1054
1055
|
if (r.status !== 0) throw new Error(`closing pane ${session.pane} on ${session.machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
1055
1056
|
tx(board, (db) => event(db, id, 'session_closed', { session_id: session.session_id, machine: session.machine, pane: session.pane }));
|
|
1056
1057
|
return session;
|
package/board/dispatch.mjs
CHANGED
|
@@ -54,9 +54,11 @@ export function localMachine() {
|
|
|
54
54
|
/** The supercode command the dispatcher drives. */
|
|
55
55
|
export const supercodeBin = () => process.env.SUPERCODE_BIN || 'supercode';
|
|
56
56
|
|
|
57
|
-
function sc(args, { input = null, timeout = 120_000 } = {}) {
|
|
58
|
-
// a neutral directory: `supercode teams …` resolves its entry from a checkout at the cwd
|
|
59
|
-
|
|
57
|
+
function sc(args, { input = null, timeout = 120_000, as = null } = {}) {
|
|
58
|
+
// a neutral directory: `supercode teams …` resolves its entry from a checkout at the cwd. `as`: the board this
|
|
59
|
+
// command acts for, so a card's pane is started and closed by its board, whichever session ran the board's verb
|
|
60
|
+
const env = as ? { ...process.env, SUPERCODE_CALLER: as } : process.env;
|
|
61
|
+
const r = spawnSync(supercodeBin(), args, { cwd: tmpdir(), input: input ?? undefined, encoding: 'utf8', timeout, env, maxBuffer: 512 * 1024 * 1024 });
|
|
60
62
|
return { code: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error ? r.error.message : '') };
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -236,7 +238,7 @@ function limitsOf(db, board, entry, task, session) {
|
|
|
236
238
|
return out;
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
function stopSession(session) {
|
|
241
|
+
function stopSession(board, session) {
|
|
240
242
|
if (!session) return;
|
|
241
243
|
if (session.surface === 'headless' && pidAlive(session.pid)) {
|
|
242
244
|
// SIGTERM to the worker's process group, then SIGKILL if it is still there after a grace, as Hermes stops its own
|
|
@@ -246,7 +248,7 @@ function stopSession(session) {
|
|
|
246
248
|
if (pidAlive(session.pid)) signal('SIGKILL');
|
|
247
249
|
return;
|
|
248
250
|
}
|
|
249
|
-
if (session.pane) sc(['teams', 'panes', 'close', session.pane, ...(session.machine ? ['--machine', session.machine] : [])], { timeout: 60_000 });
|
|
251
|
+
if (session.pane) sc(['teams', 'panes', 'close', session.pane, ...(session.machine ? ['--machine', session.machine] : [])], { timeout: 60_000, as: boardAddress(board.label) });
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
// ------------------------------------------------------------------ starting, as the workflow's dispatch says
|
|
@@ -383,7 +385,9 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
383
385
|
const surface = lane.surface === 'pane' ? 'pane' : 'headless';
|
|
384
386
|
const scope = tx(board, (db) => scopeOf(db, board, task, { params: entry.params }));
|
|
385
387
|
const where = whereOf(board, task);
|
|
386
|
-
|
|
388
|
+
// a subtask's run also sees its arc: the arc's text, tasks and thread (where its owner's rulings are)
|
|
389
|
+
const arcContext = task.subtask_of ? tx(board, (db) => cardContext(db, task.subtask_of)) : '';
|
|
390
|
+
const prompt = render(spec.prompt, { ...scope, where, context: tx(board, (db) => cardContext(db, id)), arc_context: arcContext, doors: doors(entry.workflow, status, spec.role, id, where, surface) });
|
|
387
391
|
// the run's session record knows its status, role and slot (a reviewer's slot is never the implementer's)
|
|
388
392
|
const meta = { status, role: spec.role, slot: spec.session };
|
|
389
393
|
return surface === 'pane'
|
|
@@ -414,7 +418,7 @@ function closeRequested(board, report) {
|
|
|
414
418
|
for (const row of rows) {
|
|
415
419
|
const session = json(row.metadata, {})?.supercode;
|
|
416
420
|
if (!session?.pane) continue;
|
|
417
|
-
try { stopSession(session); } catch (error) { report.undelivered.push([row.task_id, session.address, `closing it: ${String(error.message).slice(-200)}`]); continue; }
|
|
421
|
+
try { stopSession(board, session); } catch (error) { report.undelivered.push([row.task_id, session.address, `closing it: ${String(error.message).slice(-200)}`]); continue; }
|
|
418
422
|
tx(board, (db) => setRunSession(db, row.id, { ...session, closed_at: now() }));
|
|
419
423
|
}
|
|
420
424
|
}
|
|
@@ -468,7 +472,7 @@ export async function sessionEffect(board, e) {
|
|
|
468
472
|
// the session that sent the event is not closed under its own command: the dispatcher closes it next tick
|
|
469
473
|
const caller = process.env.CLAUDE_CODE_SESSION_ID || process.env.CODEX_THREAD_ID;
|
|
470
474
|
if (caller && caller === session.session_id) { tx(board, (db) => setRunSession(db, row.id, { ...session, close_requested: now() })); return; }
|
|
471
|
-
stopSession(session);
|
|
475
|
+
stopSession(board, session);
|
|
472
476
|
tx(board, (db) => setRunSession(db, row.id, { ...session, closed_at: now() }));
|
|
473
477
|
return;
|
|
474
478
|
}
|
|
@@ -574,7 +578,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
574
578
|
if (input) deliverNow(board, prev.address ?? `sc:${prev.machine}:claude-code:${prev.session_id}`, input, `board-run-${task.id}-r${runId}`);
|
|
575
579
|
} else if (prev.resumable !== false) {
|
|
576
580
|
const r = sc([...base, '--key', `board-${task.id}-r${runId}-resume`, '--input',
|
|
577
|
-
`The board resumed this session after it was lost; you are still on card ${task.id}: ${task.title}. Carry on.`, '--', '--resume', prev.session_id, ...tail], { timeout: 180_000 });
|
|
581
|
+
`The board resumed this session after it was lost; you are still on card ${task.id}: ${task.title}. Carry on.`, '--', '--resume', prev.session_id, ...tail], { timeout: 180_000, as: boardAddress(board.label) });
|
|
578
582
|
const pane = r.code === 0 ? lastJson(r.stdout)?.pane ?? null : null;
|
|
579
583
|
// a resume the harness refuses (its transcript is gone) ends the pane at once; another machine's launch
|
|
580
584
|
// answers before that shows, so the pane is looked for again a moment later
|
|
@@ -584,7 +588,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
584
588
|
} else if (prev) replaced = { session_id: prev.session_id, why: `the card moved from ${prev.machine} to ${machine}` };
|
|
585
589
|
if (!session) {
|
|
586
590
|
const sid = randomUUID();
|
|
587
|
-
const r = sc([...base, '--key', `board-${task.id}-r${runId}-${sid.slice(0, 8)}`, '--input', input ?? `Card ${task.id}: ${task.title}`, '--', '--session-id', sid, ...tail], { timeout: 180_000 });
|
|
591
|
+
const r = sc([...base, '--key', `board-${task.id}-r${runId}-${sid.slice(0, 8)}`, '--input', input ?? `Card ${task.id}: ${task.title}`, '--', '--session-id', sid, ...tail], { timeout: 180_000, as: boardAddress(board.label) });
|
|
588
592
|
if (r.code !== 0) throw new Error(`supercode open --on ${task.machine}: ${(r.stderr || r.stdout).trim().slice(-500)}`);
|
|
589
593
|
session = { surface: 'pane', session_id: sid, machine, pane: lastJson(r.stdout)?.pane ?? null, mode: 'fresh', launched_at: now(), ...(replaced ? { replaces: replaced.session_id } : {}) };
|
|
590
594
|
}
|
package/board/workflow.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// must hold and what happens; this module applies it to the board file and keeps no rule of its own. Its actions are
|
|
5
5
|
// the engine's primitives (the rows Hermes's tables hold); the I/O an action asks for (stopping a worker, closing a
|
|
6
6
|
// pane, sending a session a message, cleaning a workspace) is answered as effects the caller runs after commit.
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
7
8
|
import { event as record, json, now } from './store.mjs';
|
|
8
9
|
import { evaluate, holds, render } from './expr.mjs';
|
|
9
10
|
|
|
@@ -69,11 +70,21 @@ function pattern(p) {
|
|
|
69
70
|
return text.startsWith('(?i)') ? new RegExp(text.slice(4), 'i') : new RegExp(text);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// the time of the latest commit in a repository checkout, or null when it is none (asked only when an expression does)
|
|
74
|
+
function lastCommitAt(path) {
|
|
75
|
+
if (!path) return null;
|
|
76
|
+
const r = spawnSync('git', ['-C', path, 'log', '-1', '--format=%ct', 'HEAD'], { encoding: 'utf8', timeout: 10_000 });
|
|
77
|
+
const t = Number(r.stdout?.trim());
|
|
78
|
+
return r.status === 0 && Number.isFinite(t) && t > 0 ? t : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
/** The scope an expression is evaluated in: the card (with its relations), the event, params, time, extras. */
|
|
73
82
|
export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}) {
|
|
74
83
|
const status = statusOf(db, task);
|
|
75
|
-
|
|
76
|
-
const
|
|
84
|
+
// a related card's status is the workflow's, as the card's own is (`stored_as`: what kanban.db holds)
|
|
85
|
+
const related = 'COALESCE(c.stage, t.status) AS status, t.status AS stored_as';
|
|
86
|
+
const parents = all(db, `SELECT t.id, ${related}, t.title FROM tasks t JOIN task_links l ON l.parent_id = t.id LEFT JOIN supercode_cards c ON c.task_id = t.id WHERE l.child_id = ?`, task.id);
|
|
87
|
+
const children = all(db, `SELECT t.id, ${related} FROM tasks t JOIN task_links l ON l.child_id = t.id LEFT JOIN supercode_cards c ON c.task_id = t.id WHERE l.parent_id = ?`, task.id);
|
|
77
88
|
const lastRun = one(db, 'SELECT * FROM task_runs WHERE task_id = ? AND ended_at IS NOT NULL ORDER BY ended_at DESC, id DESC LIMIT 1', task.id);
|
|
78
89
|
const lastCompleted = one(db, "SELECT * FROM task_runs WHERE task_id = ? AND outcome = 'completed' ORDER BY ended_at DESC LIMIT 1", task.id);
|
|
79
90
|
const asked = json(one(db, "SELECT payload FROM task_events WHERE task_id = ? AND kind = 'review_requested' ORDER BY id DESC LIMIT 1", task.id)?.payload, {}) ?? {};
|
|
@@ -94,9 +105,9 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
|
|
|
94
105
|
goal_mode: Boolean(task.goal_mode),
|
|
95
106
|
implementer: asked.implementer ?? task.assignee,
|
|
96
107
|
last_reviewer: changes.reviewer ?? null,
|
|
97
|
-
arc: task.subtask_of ? one(db,
|
|
108
|
+
arc: task.subtask_of ? one(db, `SELECT t.id, ${related}, t.title, t.body, t.assignee FROM tasks t LEFT JOIN supercode_cards c ON c.task_id = t.id WHERE t.id = ?`, task.subtask_of) ?? null : null,
|
|
98
109
|
// the cards filed as its subtasks (`create --subtask-of`), in the order they were filed
|
|
99
|
-
subtasks: all(db,
|
|
110
|
+
subtasks: all(db, `SELECT t.id, ${related}, 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),
|
|
100
111
|
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",
|
|
101
112
|
task.id, Number(one(db, 'SELECT since FROM supercode_cards WHERE task_id = ?', task.id)?.since ?? 0))?.payload, {})?.reason ?? null,
|
|
102
113
|
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,
|
|
@@ -109,6 +120,12 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
|
|
|
109
120
|
last_events: Object.fromEntries(all(db, 'SELECT e.kind, e.created_at, e.payload FROM task_events e JOIN (SELECT kind, MAX(id) AS id FROM task_events WHERE task_id = ? GROUP BY kind) l ON l.id = e.id', task.id)
|
|
110
121
|
.map((r) => [r.kind, { created_at: r.created_at, payload: json(r.payload, {}) ?? {} }])),
|
|
111
122
|
comments_since: (t) => all(db, 'SELECT author, body, created_at FROM task_comments WHERE task_id = ? AND created_at >= ?', task.id, t),
|
|
123
|
+
// the latest commit in the card's workspace (a subtask's: its arc's), as epoch seconds; null outside a repository
|
|
124
|
+
last_commit_at: () => {
|
|
125
|
+
const own = task.workspace_kind !== 'scratch' ? task.workspace_path : null;
|
|
126
|
+
const arcWs = !own && task.subtask_of ? one(db, "SELECT workspace_path FROM tasks WHERE id = ? AND workspace_kind != 'scratch'", task.subtask_of)?.workspace_path : null;
|
|
127
|
+
return lastCommitAt(own ?? arcWs ?? null);
|
|
128
|
+
},
|
|
112
129
|
};
|
|
113
130
|
return {
|
|
114
131
|
card,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.45",
|
|
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": {
|