@volter/supercode-orchestrator 0.4.58 → 0.4.60
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 +2 -2
- package/board/dispatch.mjs +4 -0
- package/board/engine.mjs +3 -5
- package/board/tools.mjs +1 -1
- package/board/worker-prompt.mjs +2 -2
- package/board/workflow.mjs +38 -5
- package/mcp/hermes-kanban-tools.json +7 -9
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -29,7 +29,7 @@ Hermes Kanban's verbs, with their flags:
|
|
|
29
29
|
claim <id> [--ttl] reclaim <id> [--reason] heartbeat <id> [--note] [--until <time|30m>] link|unlink <parent> <child>
|
|
30
30
|
comment <id> <text> [--author] [--max-len] attach <id> <path> [--name] [--content-type] attachments <id> attach-rm <n>
|
|
31
31
|
complete <id>... [--result] [--summary] [--metadata JSON] edit <id> --result [--summary] [--metadata]
|
|
32
|
-
block <id> [reason]
|
|
32
|
+
block <id> [reason] (--on <card> | --waiting-on <message id>) [--ids ...] schedule <id> [reason] [--ids ...]
|
|
33
33
|
unblock <id>... [--reason] request-review <id> [--summary] [--reviewer] [--force] request-changes <id> <reason> reopen-review <id>... reopen <id>... [--reason]
|
|
34
34
|
promote <id> [reason] [--ids ...] [--force] [--dry-run] archive <id>... [--rm <id>...]
|
|
35
35
|
specify <id> [--title --body] [--all] decompose <id> [--child "<title>[@<profile>]"]... [--all]
|
|
@@ -500,7 +500,7 @@ export async function runWorkflow(argv) {
|
|
|
500
500
|
need(1, 'a card id');
|
|
501
501
|
const reason = rest.slice(1).join(' ') || null;
|
|
502
502
|
const results = [];
|
|
503
|
-
for (const t of [id, ...(args.ids ?? [])]) results.push([t, (await withBoardAsync((b) => engine.block(b, t, { reason,
|
|
503
|
+
for (const t of [id, ...(args.ids ?? [])]) results.push([t, (await withBoardAsync((b) => engine.block(b, t, { reason, on: args.on ?? null, waitingOn: args['waiting-on'] ?? null }))).to]);
|
|
504
504
|
say(args, results.map(([t, s]) => `${s === 'todo' ? 'Waiting on dependencies' : s === 'triage' ? 'Routed to triage' : 'Blocked'} ${t}`).join('\n'), { ok: true, results }); return 0;
|
|
505
505
|
}
|
|
506
506
|
case 'schedule': {
|
package/board/dispatch.mjs
CHANGED
|
@@ -500,6 +500,10 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
500
500
|
|
|
501
501
|
/** A session effect a committed move asked for: a message to the card's current actor, or closing a slot's pane. */
|
|
502
502
|
export async function sessionEffect(board, e) {
|
|
503
|
+
if (e.kind === 'tell') {
|
|
504
|
+
await send(boardAddress(board.label), board.label, e.to, e.text, `board-tell-${e.id}-${now()}`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
503
507
|
if (e.kind === 'send_to_actor') {
|
|
504
508
|
const run = tx(board, (db) => currentRun(db, e.id));
|
|
505
509
|
const session = run?.metadata?.supercode;
|
package/board/engine.mjs
CHANGED
|
@@ -15,7 +15,6 @@ import { claimer, event, json, newTaskId, now, tx } from './store.mjs';
|
|
|
15
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
|
-
export const BLOCK_KINDS = ['dependency', 'needs_input', 'capability', 'transient'];
|
|
19
18
|
export const WORKSPACE_KINDS = ['scratch', 'dir', 'worktree'];
|
|
20
19
|
/** A card blocked this many times in a row for the same kind goes to triage for a person (Hermes's limit). */
|
|
21
20
|
export const BLOCK_RECURRENCE_LIMIT = 2;
|
|
@@ -408,10 +407,9 @@ function invalidateDescendants(db, board, id) {
|
|
|
408
407
|
|
|
409
408
|
export const complete = (board, id, { result = null, summary = null, metadata = null, roles, runId = null } = {}) =>
|
|
410
409
|
act(board, id, 'complete', { roles, payload: { result, summary, metadata, run_id: runId } });
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
};
|
|
410
|
+
// a block names the one thing that ends it: a card it waits on (`on`), or the message whose answer it waits for (`waiting_on`)
|
|
411
|
+
export const block = (board, id, { reason = null, on = null, waitingOn = null, roles, runId = null } = {}) =>
|
|
412
|
+
act(board, id, 'block', { roles, payload: { reason, on, waiting_on: waitingOn, run_id: runId } });
|
|
415
413
|
export const unblock = (board, id, { reason = null, roles } = {}) => act(board, id, 'unblock', { roles, payload: { reason } });
|
|
416
414
|
export const requestReview = (board, id, { summary = null, metadata = null, reviewer = null, force = false, expectedRunId = null, roles } = {}) =>
|
|
417
415
|
act(board, id, 'request_review', { roles, payload: { summary, metadata, reviewer, force, run_id: expectedRunId } });
|
package/board/tools.mjs
CHANGED
|
@@ -101,7 +101,7 @@ export async function boardTool(root, name, args = {}, env = {}) {
|
|
|
101
101
|
}
|
|
102
102
|
case 'kanban_block': {
|
|
103
103
|
const stale = ownRun(); if (stale) return error(stale);
|
|
104
|
-
const { to: status } = await engine.block(board, tid, { reason: args.reason ? redact(String(args.reason)) : null,
|
|
104
|
+
const { to: status } = await engine.block(board, tid, { reason: args.reason ? redact(String(args.reason)) : null, on: args.on ?? null, waitingOn: args.waiting_on ?? null });
|
|
105
105
|
return ok({ task_id: tid, status });
|
|
106
106
|
}
|
|
107
107
|
case 'kanban_request_review': {
|
package/board/worker-prompt.mjs
CHANGED
|
@@ -14,8 +14,8 @@ export function taskProtocol(taskId) {
|
|
|
14
14
|
'- `kanban_heartbeat` says you are still working on a long step; `kanban_comment` leaves a note on any task.',
|
|
15
15
|
'- `kanban_create` files follow-up work (with `parents` to make it wait on another task); `kanban_link` adds a dependency.',
|
|
16
16
|
'- When the work is done, `kanban_complete` with a `summary` a person can read (and `metadata`, `artifacts`, `created_cards` where they apply).',
|
|
17
|
-
'- If the work needs review, `kanban_request_review` instead. If you cannot go on, `kanban_block` with a `reason` and
|
|
18
|
-
'
|
|
17
|
+
'- If the work needs review, `kanban_request_review` instead. If you cannot go on, `kanban_block` with a `reason` and the one',
|
|
18
|
+
' thing that ends the wait: `on` (the task it waits on) or `waiting_on` (the id of the message whose answer it waits for).',
|
|
19
19
|
'End every run with exactly one of complete, request_review or block: a run that ends without one is counted as a failure.',
|
|
20
20
|
'You may change only your own task\'s lifecycle; other tasks hear from you through comments.',
|
|
21
21
|
].join('\n');
|
package/board/workflow.mjs
CHANGED
|
@@ -79,6 +79,20 @@ function lastCommitAt(path, id) {
|
|
|
79
79
|
return r.status === 0 && Number.isFinite(t) && t > 0 ? t : null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** The first answer to message `id` in its sender's mailbox (`supercode message show --replies`), as its id; or null. */
|
|
83
|
+
function answerTo(id) {
|
|
84
|
+
if (!id) return null;
|
|
85
|
+
const r = spawnSync(process.env.SUPERCODE_BIN || 'supercode', ['message', 'show', '--replies', id, '--json'], { encoding: 'utf8', timeout: 20_000 });
|
|
86
|
+
try { return JSON.parse(r.stdout)?.[0]?.replies?.[0]?.id ?? null; } catch { return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The session message `id` was filed for (`supercode message show`): who was asked; or null. */
|
|
90
|
+
function askedOf(id) {
|
|
91
|
+
if (!id) return null;
|
|
92
|
+
const r = spawnSync(process.env.SUPERCODE_BIN || 'supercode', ['message', 'show', id, '--json'], { encoding: 'utf8', timeout: 20_000 });
|
|
93
|
+
try { return JSON.parse(r.stdout)?.[0]?.message?.session ?? null; } catch { return null; }
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
/** The scope an expression is evaluated in: the card (with its relations), the event, params, time, extras. */
|
|
83
97
|
export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}) {
|
|
84
98
|
const status = statusOf(db, task);
|
|
@@ -93,6 +107,8 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
|
|
|
93
107
|
const interruption = json(one(db, `SELECT payload FROM task_events WHERE task_id = ? AND kind IN ('blocked', 'block_loop_detected', 'dependency_wait', 'gave_up',
|
|
94
108
|
'unblocked', 'changes_requested', 'review_reopened', 'status', 'reclaimed', 'stale', 'timed_out', 'crashed', 'spawn_failed', 'rate_limited', 'session_lost')
|
|
95
109
|
ORDER BY id DESC LIMIT 1`, task.id)?.payload, {}) ?? {};
|
|
110
|
+
const lastBlock = one(db, "SELECT kind, payload FROM task_events WHERE task_id = ? AND kind IN ('blocked', 'unblocked', 'dependency_wait') ORDER BY id DESC LIMIT 1", task.id);
|
|
111
|
+
const waitingOn = lastBlock?.kind === 'blocked' ? json(lastBlock.payload, {})?.waiting_on ?? null : null;
|
|
96
112
|
const currentRun = task.current_run_id ? one(db, 'SELECT metadata FROM task_runs WHERE id = ?', task.current_run_id) : null;
|
|
97
113
|
const session = json(currentRun?.metadata, {})?.supercode ?? {};
|
|
98
114
|
const card = {
|
|
@@ -111,6 +127,10 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
|
|
|
111
127
|
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),
|
|
112
128
|
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",
|
|
113
129
|
task.id, Number(one(db, 'SELECT since FROM supercode_cards WHERE task_id = ?', task.id)?.since ?? 0))?.payload, {})?.reason ?? null,
|
|
130
|
+
// the message whose answer the standing block waits for (`block --waiting-on`), and that answer's id once it has landed
|
|
131
|
+
waiting_on: waitingOn,
|
|
132
|
+
answer: () => answerTo(waitingOn),
|
|
133
|
+
asked_of: () => askedOf(waitingOn),
|
|
114
134
|
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,
|
|
115
135
|
// where the latest interruption says the card resumes (any status the workflow declares); none recorded: null
|
|
116
136
|
resume_status: interruption.resume_status ?? interruption.retry_status ?? interruption.source_status ?? null,
|
|
@@ -321,14 +341,15 @@ const FORMAT = {
|
|
|
321
341
|
const artifacts = Array.isArray(e.metadata?.artifacts) ? e.metadata.artifacts.filter((a) => typeof a === 'string' && a.trim()) : [];
|
|
322
342
|
return { result_len: e.result ? String(e.result).length : 0, summary: firstLine(e.summary ?? e.result), ...(artifacts.length ? { artifacts } : {}) };
|
|
323
343
|
},
|
|
324
|
-
blocked: ({ e, after, source }) => ({ reason: e.reason ?? null,
|
|
344
|
+
blocked: ({ e, after, source }) => ({ reason: e.reason ?? null, on: e.on ?? null, waiting_on: e.waiting_on ?? null, recurrences: Number(after.block_recurrences || 0), source_status: source }),
|
|
325
345
|
block_loop_detected: ({ e, after, source, params }) => ({ reason: e.reason ?? null, kind: e.kind ?? null, recurrences: Number(after.block_recurrences || 0), limit: params.block_recurrence_limit, source_status: source }),
|
|
326
|
-
dependency_wait: ({ e, source }) => ({ reason: e.reason ?? null,
|
|
346
|
+
dependency_wait: ({ e, source }) => ({ reason: e.reason ?? null, on: e.on ?? null, source_status: source }),
|
|
327
347
|
review_requested: ({ e, before, after }) => ({ summary: firstLine(e.summary), implementer: before.assignee, reviewer: after.assignee !== before.assignee ? after.assignee : (e.reviewer ?? null) }),
|
|
328
348
|
// the reviewer is the lane that reviewed (the card's assignee while in review), unless the verdict names one
|
|
329
349
|
changes_requested: ({ e, before, after }) => ({ reason: e.reason ?? null, reviewer: e.reviewer ?? before.assignee ?? null, implementer: after.assignee }),
|
|
330
350
|
review_reopened: ({ after, to }) => ({ status: to, implementer: after.assignee }),
|
|
331
|
-
|
|
351
|
+
// what arrived (an unblock says it), and where the card resumes
|
|
352
|
+
unblocked: ({ e, to }) => ({ reason: e.reason ?? null, ...(to !== 'ready' ? { status: to, resume_status: to } : {}) }),
|
|
332
353
|
gave_up: ({ e, after, params, outcome }) => ({ error: String(e.error ?? '').slice(0, 500) || null, failures: Number(after.consecutive_failures || 0), limit: after.max_retries ?? params.failure_limit, outcome }),
|
|
333
354
|
promoted_manual: ({ e }) => ({ actor: e.actor ?? 'user', reason: e.reason ?? null, forced: Boolean(e.force) }),
|
|
334
355
|
promoted: ({ to }) => (to !== 'ready' ? { status: to } : null),
|
|
@@ -361,7 +382,10 @@ const ACTIONS = {
|
|
|
361
382
|
const { run_id: _r, force: _f, lock: _l, ...said } = e;
|
|
362
383
|
payload = { ...said, ...(RETRY_KINDS.has(kind) ? { retry_status: ctx.to } : { source_status: source }) };
|
|
363
384
|
}
|
|
364
|
-
|
|
385
|
+
// `on`: the event is another card's (a dependency hears what waits on it), recorded there with no run
|
|
386
|
+
const on = typeof args === 'object' && args?.on ? String(args.on) : null;
|
|
387
|
+
if (on && !one(ctx.db, 'SELECT id FROM tasks WHERE id = ?', on)) return;
|
|
388
|
+
record(ctx.db, on ?? ctx.id, kind, payload && Object.keys(payload).length ? payload : null, on ? null : ctx.runId);
|
|
365
389
|
},
|
|
366
390
|
assign(ctx, { field, value }) {
|
|
367
391
|
if (!ASSIGNABLE.has(field)) throw new Error(`the workflow assigns ${field}, which is not a card field it may set`);
|
|
@@ -432,9 +456,16 @@ const ACTIONS = {
|
|
|
432
456
|
const e = said ?? {};
|
|
433
457
|
const summary = e.summary ?? null;
|
|
434
458
|
run(ctx.db, 'UPDATE tasks SET result = COALESCE(?, result), completed_at = COALESCE(completed_at, ?) WHERE id = ?', e.result ?? summary ?? null, now(), ctx.id);
|
|
435
|
-
if (ctx.runId)
|
|
459
|
+
if (!ctx.runId) return;
|
|
460
|
+
// the completion's metadata joins the run's; the board's record of the run's session (`supercode`) stays, so the
|
|
461
|
+
// session is still closed when the card is done
|
|
462
|
+
const was = json(one(ctx.db, 'SELECT metadata FROM task_runs WHERE id = ?', ctx.runId)?.metadata, {}) ?? {};
|
|
463
|
+
const given = e.metadata && typeof e.metadata === 'object' ? e.metadata : null;
|
|
464
|
+
run(ctx.db, 'UPDATE task_runs SET summary = COALESCE(?, summary), metadata = ? WHERE id = ?', summary,
|
|
465
|
+
given ? JSON.stringify({ ...was, ...given, ...(was.supercode ? { supercode: was.supercode } : {}) }) : (Object.keys(was).length ? JSON.stringify(was) : null), ctx.runId);
|
|
436
466
|
},
|
|
437
467
|
link(ctx, parent) {
|
|
468
|
+
if (!one(ctx.db, 'SELECT id FROM tasks WHERE id = ?', parent)) throw new Error(`${parent} is no card on this board`);
|
|
438
469
|
run(ctx.db, 'INSERT OR IGNORE INTO task_links (parent_id, child_id) VALUES (?, ?)', parent, ctx.id);
|
|
439
470
|
},
|
|
440
471
|
unlink(ctx, parent) {
|
|
@@ -476,6 +507,8 @@ const ACTIONS = {
|
|
|
476
507
|
return;
|
|
477
508
|
}
|
|
478
509
|
},
|
|
510
|
+
// a message from the board to a session (`to`: its address)
|
|
511
|
+
tell(ctx, { to, text }) { if (to) ctx.effects.push({ kind: 'tell', id: ctx.id, to: String(to), text: String(text ?? '') }); },
|
|
479
512
|
send_to_actor(ctx, text) { ctx.effects.push({ kind: 'send_to_actor', id: ctx.id, text }); },
|
|
480
513
|
};
|
|
481
514
|
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"name": "kanban_block",
|
|
109
|
-
"description": "Stop work on this task
|
|
109
|
+
"description": "Stop work on this task until one thing ends the wait, and name it: `on`, the task it waits on (it goes to todo and resumes when that task closes), or `waiting_on`, the id of the message you sent whose answer it waits for (it resumes when an answer to that message lands in your mailbox). An outside event is a task on the card of whoever makes it happen or watches for it. `reason` is shown on the board. The same block twice goes to triage.",
|
|
110
110
|
"inputSchema": {
|
|
111
111
|
"type": "object",
|
|
112
112
|
"properties": {
|
|
@@ -118,15 +118,13 @@
|
|
|
118
118
|
"type": "string",
|
|
119
119
|
"description": "What you need answered or what stopped you, in one or two sentences. Don't paste the whole conversation; the human has the board and can ask follow-ups via comments."
|
|
120
120
|
},
|
|
121
|
-
"
|
|
121
|
+
"on": {
|
|
122
122
|
"type": "string",
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
],
|
|
129
|
-
"description": "Why you're blocked. 'dependency' waits in todo and resumes automatically; the others surface to a human. Omit only if none apply."
|
|
123
|
+
"description": "The task id this waits on."
|
|
124
|
+
},
|
|
125
|
+
"waiting_on": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "The id of the message (m-… or a-…) whose answer this waits for."
|
|
130
128
|
},
|
|
131
129
|
"board": {
|
|
132
130
|
"type": "string",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.60",
|
|
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": {
|