@volter/supercode-orchestrator 0.4.25 → 0.4.27
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 +15 -5
- package/board/dispatch.mjs +5 -6
- package/board/engine.mjs +29 -17
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -20,10 +20,10 @@ Hermes Kanban's verbs, with their flags:
|
|
|
20
20
|
init · boards list|create|rm|switch|show|rename|set-default-workdir|export|import
|
|
21
21
|
create <title> [--body] [--assignee] [--parent]... [--workspace scratch|worktree[:<path>]|dir:<path>] [--branch] [--project]
|
|
22
22
|
[--tenant] [--priority] [--triage] [--idempotency-key] [--max-runtime 30m] [--created-by] [--skill]... [--max-retries]
|
|
23
|
-
[--model] [--provider] [--goal] [--goal-max-turns] [--initial-status blocked|running]
|
|
23
|
+
[--model] [--provider] [--goal|--no-goal] [--goal-max-turns] [--initial-status blocked|running]
|
|
24
24
|
swarm <goal> --worker PROFILE:TITLE[:SKILL,SKILL]... --verifier P --synthesizer P
|
|
25
25
|
list [--mine] [--assignee] [--status] [--tenant] [--session] [--archived] [--sort] show <id> context <id> runs <id>
|
|
26
|
-
assign <id> <profile|none> reassign <id> <profile> [--reclaim] [--reason] set-model <id> [model] [--provider]
|
|
26
|
+
assign <id> <profile|none> reassign <id> <profile> [--reclaim] [--reason] set-model <id> [model] [--provider] set-goal <id>... [--goal-max-turns N] [--off]
|
|
27
27
|
claim <id> [--ttl] reclaim <id> [--reason] heartbeat <id> [--note] link|unlink <parent> <child>
|
|
28
28
|
comment <id> <text> [--author] [--max-len] attach <id> <path> [--name] [--content-type] attachments <id> attach-rm <n>
|
|
29
29
|
complete <id>... [--result] [--summary] [--metadata JSON] edit <id> --result [--summary] [--metadata]
|
|
@@ -51,7 +51,7 @@ supercode's own:
|
|
|
51
51
|
|
|
52
52
|
const MULTI = new Set(['parent', 'skill', 'worker', 'child', 'ids', 'rm']);
|
|
53
53
|
const FLAGS = new Set(['json', 'triage', 'no-notify', 'no-start', 'all', 'force', 'help', 'mine', 'archived', 'reclaim', 'dry-run',
|
|
54
|
-
'goal', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose']);
|
|
54
|
+
'goal', 'no-goal', 'off', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose']);
|
|
55
55
|
// flags that take every following word up to the next flag (Hermes's nargs='+')
|
|
56
56
|
const GREEDY = new Set(['ids', 'rm']);
|
|
57
57
|
|
|
@@ -235,13 +235,15 @@ export async function runWorkflow(argv) {
|
|
|
235
235
|
const meta = boardMeta(root, slug);
|
|
236
236
|
let workspace = args.workspace;
|
|
237
237
|
if ((workspace === 'dir' || workspace === 'worktree') && meta.default_workdir) workspace = `${workspace}:${meta.default_workdir}`;
|
|
238
|
+
// a home whose `kanban.goal_mode_default` is true makes every new card a goal card (a subtask is never started alone)
|
|
239
|
+
const goalDefault = !args.goal && !args['no-goal'] && !args['subtask-of'] && (await laneState(root)).kanban.goal_mode_default === true;
|
|
238
240
|
const created = withBoard((b) => engine.createCard(b, {
|
|
239
241
|
title: rest.join(' '), body: args.body, assignee: args.assignee === 'none' ? null : args.assignee, machine: args.machine,
|
|
240
242
|
workspace, branch: args.branch, parents: args.parent, priority: args.priority, tenant: args.tenant,
|
|
241
243
|
triage: args.triage, idempotencyKey: args['idempotency-key'], maxRuntime: duration(args['max-runtime']),
|
|
242
244
|
maxRetries, skills: args.skill, model: args.model, provider: args.provider, createdBy: args['created-by'] ?? callerName(),
|
|
243
245
|
sessionId: process.env.CLAUDE_CODE_SESSION_ID ?? null, subtaskOf: args['subtask-of'] ?? null, initialStatus: args['initial-status'] ?? null,
|
|
244
|
-
project: args.project ?? meta.project_id ?? null, goal: args.goal ? { maxTurns: args['goal-max-turns'] ? Number(args['goal-max-turns']) : null } : null,
|
|
246
|
+
project: args.project ?? meta.project_id ?? null, goal: args.goal || goalDefault ? { maxTurns: args['goal-max-turns'] ? Number(args['goal-max-turns']) : null } : null,
|
|
245
247
|
}), true);
|
|
246
248
|
let answer = { id: created.id, existing: created.existing };
|
|
247
249
|
if (!args['no-start'] && !created.existing && !args['subtask-of']) {
|
|
@@ -377,6 +379,14 @@ export async function runWorkflow(argv) {
|
|
|
377
379
|
say(args, `Reassigned ${id} to ${rest[1]}`, { ok: true }); return 0;
|
|
378
380
|
}
|
|
379
381
|
case 'set-model': need(1, 'a card id'); withBoard((b) => engine.setModel(b, id, rest[1] ?? null, args.provider ?? null)); say(args, rest[1] ? `${id} runs on model ${rest[1]}${args.provider ? ` (${args.provider})` : ''} from its next start` : `${id} uses its profile's model`, { ok: true }); return 0;
|
|
382
|
+
case 'set-goal': {
|
|
383
|
+
// a card into goal mode (or out of it with --off) from its next idle turn: the heartbeat keeps its session going
|
|
384
|
+
need(1, 'a card id');
|
|
385
|
+
const maxTurns = args['goal-max-turns'] ? Number(args['goal-max-turns']) : null;
|
|
386
|
+
if (maxTurns !== null && !(Number.isInteger(maxTurns) && maxTurns >= 1)) throw new Error('--goal-max-turns is a whole number of turns, at least 1');
|
|
387
|
+
for (const t of ids()) withBoard((b) => engine.setGoal(b, t, { on: !args.off, maxTurns }));
|
|
388
|
+
say(args, ids().map((t) => args.off ? `${t} is no longer a goal card` : `${t} is a goal card${maxTurns ? ` (${maxTurns} turns)` : ''}`).join('\n'), { ok: true }); return 0;
|
|
389
|
+
}
|
|
380
390
|
case 'move': need(2, 'a card id and a machine (or none)'); withBoard((b) => engine.setMachine(b, id, rest[1] === 'none' ? null : rest[1])); say(args, `${id} runs on ${rest[1]}`, { ok: true }); return 0;
|
|
381
391
|
|
|
382
392
|
case 'claim': {
|
|
@@ -942,7 +952,7 @@ function authorize(db, verb, rest, args, own) {
|
|
|
942
952
|
}
|
|
943
953
|
if (verb === 'boards') { if (!['list', 'ls', 'show', 'current', undefined].includes(rest[0])) refuse('does not change the home\'s boards'); return; }
|
|
944
954
|
// verbs that act on the board rather than on a card are the manager's and the board's, never a card's session's
|
|
945
|
-
if (['pause', 'resume', 'swarm', 'dispatch', 'daemon', 'serve', 'gc', 'repair', 'adopt', 'close', 'move', 'reassign', 'assign', 'set-model', 'claim', 'reclaim', 'archive', 'promote', 'schedule', 'link', 'unlink', 'edit', 'reopen-review', 'reopen', 'attach-rm'].includes(verb)) refuse(`does not ${verb}; the fleet's manager does`);
|
|
955
|
+
if (['pause', 'resume', 'swarm', 'dispatch', 'daemon', 'serve', 'gc', 'repair', 'adopt', 'close', 'move', 'reassign', 'assign', 'set-model', 'set-goal', 'claim', 'reclaim', 'archive', 'promote', 'schedule', 'link', 'unlink', 'edit', 'reopen-review', 'reopen', 'attach-rm'].includes(verb)) refuse(`does not ${verb}; the fleet's manager does`);
|
|
946
956
|
if (args.all) refuse(`does not ${verb} every card; only its own`);
|
|
947
957
|
const card = engine.getTask(db, rest[0]);
|
|
948
958
|
if (!card) return; // the verb itself answers "no card"
|
package/board/dispatch.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
23
23
|
import { boardPath, event, json, listBoards, now, openBoard, tx } from './store.mjs';
|
|
24
24
|
import { holdBoardTickLock } from '../hermes-locks.mjs';
|
|
25
25
|
import {
|
|
26
|
-
cardContext, claimIn, claimTtl, currentRun, DEFAULT_FAILURE_LIMIT, extendClaim, getTask, lastSession,
|
|
26
|
+
blockIn, cardContext, claimIn, claimTtl, currentRun, DEFAULT_FAILURE_LIMIT, extendClaim, getTask, lastSession,
|
|
27
27
|
recomputeReady, recordFailure, release, setRunSession, subscribeIn, subscriptions,
|
|
28
28
|
} from './engine.mjs';
|
|
29
29
|
|
|
@@ -474,8 +474,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null } = {}) {
|
|
|
474
474
|
let replaced = null;
|
|
475
475
|
if (prev && prev.machine === machine) {
|
|
476
476
|
if (sessionAlive(fleet, prev)) {
|
|
477
|
-
session
|
|
478
|
-
|
|
477
|
+
// the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
|
|
478
|
+
const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _i, worked_since_ask: _w, ...kept } = prev;
|
|
479
|
+
session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
|
|
479
480
|
} else if (prev.resumable !== false) {
|
|
480
481
|
const r = sc([...base, '--key', `board-${task.id}-r${runId}-resume`, '--input',
|
|
481
482
|
`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 });
|
|
@@ -942,9 +943,7 @@ function pursueGoals(board, fleet, settings, report) {
|
|
|
942
943
|
const turns = session.worked_since_ask && !task.goal_mode ? 0 : Number(session.goal_turns ?? 0);
|
|
943
944
|
const budget = task.goal_max_turns ?? settings.goalMaxTurns;
|
|
944
945
|
if (turns >= budget) {
|
|
945
|
-
db
|
|
946
|
-
const ended = endRun(db, id, { outcome: 'blocked', summary: `idle ${budget} times without deciding done or blocked` });
|
|
947
|
-
event(db, id, 'blocked', { reason: `its session went idle ${budget} times without deciding done or blocked; review it`, kind: 'needs_input', source_status: 'running' }, ended);
|
|
946
|
+
blockIn(db, id, { reason: `its session went idle ${budget} times without deciding done or blocked; review it`, kind: 'needs_input' });
|
|
948
947
|
return null;
|
|
949
948
|
}
|
|
950
949
|
setRunSession(db, runRow.id, { ...session, goal_turns: turns + 1, goal_nudged_at: now(), worked_since_ask: false });
|
package/board/engine.mjs
CHANGED
|
@@ -396,25 +396,28 @@ function completeIn(board, id, { result = null, summary = null, metadata = null
|
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
export function block(board, id, { reason = null, kind = null } = {}) {
|
|
399
|
+
return tx(board, (db) => blockIn(db, id, { reason, kind }));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** `block` inside the caller's transaction (the dispatcher's own blocks keep the same recurrence accounting). */
|
|
403
|
+
export function blockIn(db, id, { reason = null, kind = null } = {}) {
|
|
399
404
|
if (kind && !BLOCK_KINDS.includes(kind)) throw new Error(`block kind is one of ${BLOCK_KINDS.join(', ')}`);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
run(db, "UPDATE tasks SET status = 'todo', claim_lock = NULL, claim_expires = NULL, worker_pid = NULL, block_kind = ? WHERE id = ?", kind, id);
|
|
406
|
-
const runId = endRun(db, id, { outcome: 'blocked', summary: reason }) ?? (reason ? synthesizeRun(db, id, { outcome: 'blocked', summary: reason }) : null);
|
|
407
|
-
event(db, id, 'dependency_wait', { reason, kind, source_status: source }, runId);
|
|
408
|
-
return 'todo';
|
|
409
|
-
}
|
|
410
|
-
const recurrences = task.block_kind === kind ? Number(task.block_recurrences || 0) + 1 : 1;
|
|
411
|
-
const loop = recurrences >= BLOCK_RECURRENCE_LIMIT;
|
|
412
|
-
const status = loop ? 'triage' : 'blocked';
|
|
413
|
-
run(db, 'UPDATE tasks SET status = ?, claim_lock = NULL, claim_expires = NULL, worker_pid = NULL, block_kind = ?, block_recurrences = ? WHERE id = ?', status, kind, recurrences, id);
|
|
405
|
+
const task = must(db, id);
|
|
406
|
+
if (!['running', 'ready'].includes(task.status)) throw new Error(`${id} is ${task.status}; only a running or ready card blocks`);
|
|
407
|
+
const source = task.status === 'running' ? retryStatus(db, id) : 'ready';
|
|
408
|
+
if (kind === 'dependency') {
|
|
409
|
+
run(db, "UPDATE tasks SET status = 'todo', claim_lock = NULL, claim_expires = NULL, worker_pid = NULL, block_kind = ? WHERE id = ?", kind, id);
|
|
414
410
|
const runId = endRun(db, id, { outcome: 'blocked', summary: reason }) ?? (reason ? synthesizeRun(db, id, { outcome: 'blocked', summary: reason }) : null);
|
|
415
|
-
event(db, id,
|
|
416
|
-
return
|
|
417
|
-
}
|
|
411
|
+
event(db, id, 'dependency_wait', { reason, kind, source_status: source }, runId);
|
|
412
|
+
return 'todo';
|
|
413
|
+
}
|
|
414
|
+
const recurrences = task.block_kind === kind ? Number(task.block_recurrences || 0) + 1 : 1;
|
|
415
|
+
const loop = recurrences >= BLOCK_RECURRENCE_LIMIT;
|
|
416
|
+
const status = loop ? 'triage' : 'blocked';
|
|
417
|
+
run(db, 'UPDATE tasks SET status = ?, claim_lock = NULL, claim_expires = NULL, worker_pid = NULL, block_kind = ?, block_recurrences = ? WHERE id = ?', status, kind, recurrences, id);
|
|
418
|
+
const runId = endRun(db, id, { outcome: 'blocked', summary: reason }) ?? (reason ? synthesizeRun(db, id, { outcome: 'blocked', summary: reason }) : null);
|
|
419
|
+
event(db, id, loop ? 'block_loop_detected' : 'blocked', { reason, kind, recurrences, ...(loop ? { limit: BLOCK_RECURRENCE_LIMIT } : {}), source_status: source }, runId);
|
|
420
|
+
return status;
|
|
418
421
|
}
|
|
419
422
|
|
|
420
423
|
export function unblock(board, id) {
|
|
@@ -768,6 +771,15 @@ export function setModel(board, id, model = null, provider = null) {
|
|
|
768
771
|
});
|
|
769
772
|
}
|
|
770
773
|
|
|
774
|
+
/** Goal mode on a card (Hermes's goal loop, which Hermes sets only at creation): on or off, with its turn budget. */
|
|
775
|
+
export function setGoal(board, id, { on = true, maxTurns = null } = {}) {
|
|
776
|
+
return tx(board, (db) => {
|
|
777
|
+
must(db, id);
|
|
778
|
+
run(db, 'UPDATE tasks SET goal_mode = ?, goal_max_turns = ? WHERE id = ?', on ? 1 : 0, on ? maxTurns : null, id);
|
|
779
|
+
event(db, id, 'goal_set', { goal_mode: Boolean(on), goal_max_turns: on ? maxTurns : null });
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
771
783
|
export function addAttachment(board, id, { file, name, contentType = null, author = 'user', dir }) {
|
|
772
784
|
return tx(board, (db) => {
|
|
773
785
|
must(db, 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.27",
|
|
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": {
|