@volter/supercode-orchestrator 0.4.16 → 0.4.17
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 +1 -1
- package/board/dispatch.mjs +66 -9
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -269,7 +269,7 @@ function callerName() {
|
|
|
269
269
|
export function summary(r) {
|
|
270
270
|
const parts = [`board ${r.board}:`];
|
|
271
271
|
if (r.error) parts.push(`error ${r.error}`);
|
|
272
|
-
for (const [k, label] of [['started', 'started'], ['queued', 'waiting'], ['failed', 'failed'], ['lost', 'lost'], ['crashed', 'crashed'], ['timed_out', 'timed out'], ['reclaimed', 'reclaimed'], ['auto_blocked', 'gave up'], ['notified', 'notices']]) {
|
|
272
|
+
for (const [k, label] of [['mailed', 'mailed updates'], ['started', 'started'], ['queued', 'waiting'], ['failed', 'failed'], ['lost', 'lost'], ['crashed', 'crashed'], ['timed_out', 'timed out'], ['reclaimed', 'reclaimed'], ['auto_blocked', 'gave up'], ['notified', 'notices']]) {
|
|
273
273
|
if (r[k]?.length) parts.push(`${label} ${r[k].length}`);
|
|
274
274
|
}
|
|
275
275
|
if (r.promoted) parts.push(`promoted ${r.promoted}`);
|
package/board/dispatch.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// subscriber's cursor moves only after its notice was delivered, so two dispatchers over one board never both
|
|
16
16
|
// launch and a crash between the two steps is answered by the next tick.
|
|
17
17
|
import { spawn as spawnChild, spawnSync } from 'node:child_process';
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
18
|
+
import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync } from 'node:fs';
|
|
19
19
|
import { hostname, tmpdir } from 'node:os';
|
|
20
20
|
import { basename, dirname, join } from 'node:path';
|
|
21
21
|
import { randomUUID } from 'node:crypto';
|
|
@@ -276,10 +276,11 @@ function opening(board, task, label) {
|
|
|
276
276
|
const skills = task.skills?.length ? `\nSkills pinned to this card: ${task.skills.join(', ')}.` : '';
|
|
277
277
|
const at = `--root ${board.root}${board.slug !== 'default' ? ` --board ${board.slug}` : ''}`;
|
|
278
278
|
// the board's own door is on the machine that keeps it; a session elsewhere writes to its card through its manager
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
const local = normalMachine(task.machine) === localMachine();
|
|
280
|
+
const where = local ? at : `--at ${boardAddress(label)}`;
|
|
281
|
+
const door = `\nYour card is yours to keep current: \`supercode workflow comment ${task.id} "<text>" ${where}\`, \`block ${task.id} "<why>" --kind <needs_input|capability|transient|dependency> ${where}\` `
|
|
282
|
+
+ `and \`review ${task.id} --summary "<what>" ${where}\` update it; \`create "<title>" --subtask-of ${task.id} ${where}\` files a subtask, and \`complete <subtask> ${where}\` marks one done`
|
|
283
|
+
+ `${local ? '' : ' (each is filed with the board and answered by a board notice)'}. Its reviewers, not you, mark the card itself done.`;
|
|
283
284
|
return `You are the session working card ${task.id} on the ${label} board. The board started you; updates to the card `
|
|
284
285
|
+ `(comments, reassignment, blocks, completion) and to the cards it waits on reach you as board notices.${door}${skills}\n\n${context}`;
|
|
285
286
|
}
|
|
@@ -479,11 +480,19 @@ function send(from, label, to, body, key) {
|
|
|
479
480
|
if (r.code !== 0 || answer.code !== 0) throw new Error((answer.text || r.stderr || 'no answer').trim().slice(-300));
|
|
480
481
|
}
|
|
481
482
|
|
|
482
|
-
|
|
483
|
-
|
|
483
|
+
/** The board's own address: where its notices come from and where card updates are mailed. */
|
|
484
|
+
export const boardAddress = (label) => `sc:${localMachine()}:board:${label.replace(/[^A-Za-z0-9_.-]/g, '-')}`;
|
|
485
|
+
|
|
486
|
+
function notify(board, fleet, report) {
|
|
487
|
+
const from = boardAddress(board.label);
|
|
484
488
|
for (const sub of subscriptions(board.db)) {
|
|
485
489
|
if (sub.platform !== 'supercode') continue;
|
|
486
490
|
const role = ['worker', 'dependency'].includes(sub.delivery_metadata?.role) ? sub.delivery_metadata.role : 'creator';
|
|
491
|
+
// a session that worked or watched a card and is gone hears nothing more; a creator is retried until it is back
|
|
492
|
+
if (role !== 'creator' && fleet.ok && !fleet.alive.has(sub.chat_id)) {
|
|
493
|
+
tx(board, (db) => db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(sub.task_id, sub.chat_id));
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
487
496
|
const kinds = MAIL_KINDS[role];
|
|
488
497
|
const events = board.db.prepare(`SELECT id, kind, payload, created_at FROM task_events WHERE task_id = ? AND id > ? AND kind IN (${kinds.map(() => '?').join(', ')}) ORDER BY id`)
|
|
489
498
|
.all(sub.task_id, sub.last_event_id, ...kinds);
|
|
@@ -516,6 +525,53 @@ function notify(board, report) {
|
|
|
516
525
|
}
|
|
517
526
|
}
|
|
518
527
|
|
|
528
|
+
// ------------------------------------------------------------------ the board's mailbox
|
|
529
|
+
|
|
530
|
+
const ENTRY = fileURLToPath(new URL('../bin/orchestrator.mjs', import.meta.url));
|
|
531
|
+
const NOT_BY_MAIL = new Set(['serve', 'dispatch', 'init', 'attach']);
|
|
532
|
+
|
|
533
|
+
/** The board's mailbox on this machine: the directory supercode files mail for `address` in, or null. */
|
|
534
|
+
function mailboxOf(address) {
|
|
535
|
+
const config = process.env.SUPERCODE_HOME || (process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'supercode') : join(process.env.HOME ?? '', '.config', 'supercode'));
|
|
536
|
+
const root = join(config, 'mail');
|
|
537
|
+
if (!existsSync(root)) return null;
|
|
538
|
+
for (const dir of readdirSync(root)) {
|
|
539
|
+
if (!dir.startsWith('board-')) continue;
|
|
540
|
+
try { if (readFileSync(join(root, dir, 'address'), 'utf8').trim() === address) return join(root, dir); } catch { /* not a mailbox */ }
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Card updates filed as mail (`supercode workflow <verb> … --at <this board's address>`, from any machine): each is
|
|
547
|
+
* run as its sender, the session supercode found the command running under, so a card's session gets exactly what
|
|
548
|
+
* it may do on this machine; the answer goes back as a notice and the envelope is filed as read.
|
|
549
|
+
*/
|
|
550
|
+
function readMailbox(board, address, report) {
|
|
551
|
+
const box = mailboxOf(address);
|
|
552
|
+
if (!box || !existsSync(join(box, 'new'))) return;
|
|
553
|
+
for (const name of readdirSync(join(box, 'new')).filter((n) => n.endsWith('.json')).sort()) {
|
|
554
|
+
const path = join(box, 'new', name);
|
|
555
|
+
let envelope;
|
|
556
|
+
try { envelope = JSON.parse(readFileSync(path, 'utf8')); } catch { continue; }
|
|
557
|
+
try { renameSync(path, join(box, 'cur', name)); } catch { continue; } // another reader took it
|
|
558
|
+
const argv = json(envelope.body, {})?.workflow;
|
|
559
|
+
let answer;
|
|
560
|
+
if (!Array.isArray(argv) || !argv.length || NOT_BY_MAIL.has(argv[0])) answer = `not applied: a mailed card update is one \`supercode workflow\` verb other than ${[...NOT_BY_MAIL].join(', ')}`;
|
|
561
|
+
else {
|
|
562
|
+
const clean = [];
|
|
563
|
+
for (let i = 0; i < argv.length; i += 1) { if (argv[i] === '--root' || argv[i] === '--board') i += 1; else clean.push(String(argv[i])); }
|
|
564
|
+
const sid = String(envelope.from ?? '').split(':')[3] ?? '';
|
|
565
|
+
const r = spawnSync(process.execPath, [ENTRY, 'workflow', ...clean, '--root', board.root, '--board', board.slug], {
|
|
566
|
+
encoding: 'utf8', timeout: 180_000, env: { ...process.env, CLAUDE_CODE_SESSION_ID: sid, CODEX_THREAD_ID: '' },
|
|
567
|
+
});
|
|
568
|
+
answer = `${clean.slice(0, 2).join(' ')}: ${(r.status === 0 ? r.stdout : r.stderr || r.stdout).trim().slice(-600) || `exit ${r.status}`}`;
|
|
569
|
+
}
|
|
570
|
+
report.mailed.push([envelope.from, argv?.[0] ?? '?']);
|
|
571
|
+
try { send(address, board.label, envelope.from, answer, `board-answer-${envelope.id}`); } catch (error) { report.undelivered.push(['mail', envelope.from, String(error.message).slice(-200)]); }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
519
575
|
// ------------------------------------------------------------------ the tick
|
|
520
576
|
|
|
521
577
|
/** The board's name in notices: the home's owner folder (`…/volter-manager/home` → `volter-manager`), and the slug. */
|
|
@@ -533,15 +589,16 @@ export function tick({ root, lanes, settings, only = null, slugs = null, dispatc
|
|
|
533
589
|
const fleet = needFleet ? observeFleet() : { ok: false, alive: new Set(), online: new Set(), error: 'no pane lanes' };
|
|
534
590
|
const out = [];
|
|
535
591
|
for (const slug of slugs ?? listBoards(root)) {
|
|
536
|
-
const report = { board: slug, started: [], queued: [], failed: [], lost: [], crashed: [], timed_out: [], reclaimed: [], auto_blocked: [], promoted: 0, unassigned: [], nonspawnable: [], notified: [], undelivered: [], fleet: fleet.ok ? 'read' : fleet.error };
|
|
592
|
+
const report = { board: slug, mailed: [], started: [], queued: [], failed: [], lost: [], crashed: [], timed_out: [], reclaimed: [], auto_blocked: [], promoted: 0, unassigned: [], nonspawnable: [], notified: [], undelivered: [], fleet: fleet.ok ? 'read' : fleet.error };
|
|
537
593
|
let board;
|
|
538
594
|
try {
|
|
539
595
|
board = openBoard(root, slug);
|
|
540
596
|
board.label = boardLabel(root, slug);
|
|
597
|
+
readMailbox(board, boardAddress(board.label), report);
|
|
541
598
|
supervise(board, fleet, settings, report);
|
|
542
599
|
report.promoted = recomputeReady(board, settings.failureLimit);
|
|
543
600
|
if (dispatch) start(board, fleet, lanes, settings, report, { only });
|
|
544
|
-
notify(board, report);
|
|
601
|
+
notify(board, fleet, report);
|
|
545
602
|
// what a Hermes gateway serves around its dispatcher and this one does not: said by the caller
|
|
546
603
|
report.chat_subscriptions = Number(board.db.prepare("SELECT COUNT(*) AS n FROM kanban_notify_subs WHERE platform != 'supercode'").get().n);
|
|
547
604
|
report.triage = Number(board.db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE status = 'triage'").get().n);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.17",
|
|
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": {
|