@volter/supercode-orchestrator 0.4.24 → 0.4.26
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 +34 -14
- package/board/engine.mjs +9 -0
- package/kanban-worker.mjs +35 -26
- package/mcp/server.mjs +6 -2
- package/mcp/verbs.mjs +2 -1
- 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
|
@@ -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 });
|
|
@@ -911,13 +912,17 @@ function readMailbox(board, address, report) {
|
|
|
911
912
|
// ------------------------------------------------------------------ goals and triage
|
|
912
913
|
|
|
913
914
|
/**
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
915
|
+
* The board's heartbeat for a pane session: a running card's session never sits idle without deciding. Each tick, a
|
|
916
|
+
* session that is idle while its card is still running is asked to decide: `request-review` when the work is done,
|
|
917
|
+
* `block` with its kind when it cannot go on (waiting on a person, a capability, another card), or keep working. A
|
|
918
|
+
* goal card (Hermes's goal loop) is told to keep going. A session is asked once it has been idle a whole tick; one
|
|
919
|
+
* seen working since the last ask answered it, and its count starts again. After the turn budget (`goal_max_turns`, else
|
|
920
|
+
* `kanban.goal_max_turns`) of asks in a row with no work and no decision (a goal card: of turns), the card is blocked `needs_input` and its creator is told.
|
|
921
|
+
* Whether the work is done is the session's call through its card, not an auxiliary judge's.
|
|
917
922
|
*/
|
|
918
923
|
function pursueGoals(board, fleet, settings, report) {
|
|
919
924
|
if (!fleet.ok) return;
|
|
920
|
-
const cards = board.db.prepare("SELECT id FROM tasks WHERE status = 'running'
|
|
925
|
+
const cards = board.db.prepare("SELECT id FROM tasks WHERE status = 'running'").all();
|
|
921
926
|
for (const { id } of cards) {
|
|
922
927
|
const nudge = tx(board, (db) => {
|
|
923
928
|
const task = getTask(db, id);
|
|
@@ -925,22 +930,37 @@ function pursueGoals(board, fleet, settings, report) {
|
|
|
925
930
|
const session = runRow?.metadata?.supercode;
|
|
926
931
|
if (!session?.session_id || session.surface !== 'pane') return null;
|
|
927
932
|
const live = fleet.sessions.find((x) => x.address?.endsWith(`:${session.session_id}`));
|
|
928
|
-
if (live
|
|
933
|
+
if (!live) return null;
|
|
934
|
+
// a session seen working since the last ask answered it by working: its count starts again (a goal card keeps
|
|
935
|
+
// Hermes's budget of turns); one idle for less than a whole tick is between turns, not idle
|
|
936
|
+
if (live.status !== 'idle') {
|
|
937
|
+
if (session.idle_since || (session.goal_nudged_at && !session.worked_since_ask)) setRunSession(db, runRow.id, { ...session, idle_since: null, worked_since_ask: true });
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
if (!session.idle_since) { setRunSession(db, runRow.id, { ...session, idle_since: now() }); return null; }
|
|
941
|
+
if (now() - session.idle_since < settings.interval) return null;
|
|
929
942
|
if (session.goal_nudged_at && now() - session.goal_nudged_at < settings.interval * 2) return null;
|
|
930
|
-
const turns = Number(session.goal_turns ?? 0);
|
|
943
|
+
const turns = session.worked_since_ask && !task.goal_mode ? 0 : Number(session.goal_turns ?? 0);
|
|
931
944
|
const budget = task.goal_max_turns ?? settings.goalMaxTurns;
|
|
932
945
|
if (turns >= budget) {
|
|
933
946
|
db.prepare("UPDATE tasks SET status = 'blocked', claim_lock = NULL, claim_expires = NULL WHERE id = ?").run(id);
|
|
934
|
-
const ended = endRun(db, id, { outcome: 'blocked', summary: `
|
|
935
|
-
event(db, id, 'blocked', { reason: `
|
|
947
|
+
const ended = endRun(db, id, { outcome: 'blocked', summary: `idle ${budget} times without deciding done or blocked` });
|
|
948
|
+
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);
|
|
936
949
|
return null;
|
|
937
950
|
}
|
|
938
|
-
setRunSession(db, runRow.id, { ...session, goal_turns: turns + 1, goal_nudged_at: now() });
|
|
939
|
-
event(db, id, 'goal_continued', { turn: turns + 1, budget }, runRow.id);
|
|
940
|
-
|
|
951
|
+
setRunSession(db, runRow.id, { ...session, goal_turns: turns + 1, goal_nudged_at: now(), worked_since_ask: false });
|
|
952
|
+
event(db, id, task.goal_mode ? 'goal_continued' : 'decision_asked', { turn: turns + 1, budget }, runRow.id);
|
|
953
|
+
const local = normalMachine(task.machine) === localMachine();
|
|
954
|
+
const where = local ? `--root ${board.root}${board.slug !== 'default' ? ` --board ${board.slug}` : ''}` : `--at ${boardAddress(board.label)}`;
|
|
955
|
+
const decide = `When it is done, \`supercode workflow request-review ${id} --summary "<what>" ${where}\`; if you cannot go on, \`supercode workflow block ${id} "<why>" --kind <needs_input|capability|transient|dependency> ${where}\`.`;
|
|
956
|
+
const text = task.goal_mode
|
|
957
|
+
? `Card ${id} (${task.title}) is not done yet: keep working toward it (goal turn ${turns + 1} of ${budget}). ${decide}`
|
|
958
|
+
: `Card ${id} (${task.title}) is still running and you are idle: decide (${turns + 1} of ${budget}). If there is more to do, keep working. ${decide} `
|
|
959
|
+
+ `A session that stays idle without deciding has its card blocked for its creator after ${budget}.`;
|
|
960
|
+
return { address: live.address, text, kind: task.goal_mode ? 'goal_continued' : 'decision_asked' };
|
|
941
961
|
});
|
|
942
962
|
if (nudge) {
|
|
943
|
-
try { send(boardAddress(board.label), board.label, nudge.address, nudge.text, `board-goal-${id}-${now()}`); report.notified.push([id,
|
|
963
|
+
try { send(boardAddress(board.label), board.label, nudge.address, nudge.text, `board-goal-${id}-${now()}`); report.notified.push([id, nudge.kind, nudge.address]); }
|
|
944
964
|
catch (error) { report.undelivered.push([id, nudge.address, String(error.message).slice(-200)]); }
|
|
945
965
|
}
|
|
946
966
|
}
|
package/board/engine.mjs
CHANGED
|
@@ -768,6 +768,15 @@ export function setModel(board, id, model = null, provider = null) {
|
|
|
768
768
|
});
|
|
769
769
|
}
|
|
770
770
|
|
|
771
|
+
/** Goal mode on a card (Hermes's goal loop, which Hermes sets only at creation): on or off, with its turn budget. */
|
|
772
|
+
export function setGoal(board, id, { on = true, maxTurns = null } = {}) {
|
|
773
|
+
return tx(board, (db) => {
|
|
774
|
+
must(db, id);
|
|
775
|
+
run(db, 'UPDATE tasks SET goal_mode = ?, goal_max_turns = ? WHERE id = ?', on ? 1 : 0, on ? maxTurns : null, id);
|
|
776
|
+
event(db, id, 'goal_set', { goal_mode: Boolean(on), goal_max_turns: on ? maxTurns : null });
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
771
780
|
export function addAttachment(board, id, { file, name, contentType = null, author = 'user', dir }) {
|
|
772
781
|
return tx(board, (db) => {
|
|
773
782
|
must(db, id);
|
package/kanban-worker.mjs
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// starts a headless profile's worker as Hermes's dispatcher starts its own: `-p <assignee> --cli --accept-hooks
|
|
3
3
|
// [--skills <s>]… [-m <model> [--provider <p>]] [--reasoning <e>] chat -q "work kanban task <id>"`, detached, in the
|
|
4
4
|
// task's workspace, its output to the task's log, the task in its environment (`KANBAN_ENV`), and watches the pid.
|
|
5
|
-
// This is that process: it runs the assignee profile's own worker for the one query, with
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// This is that process: it runs the assignee profile's own worker for the one query, with the task protocol and the
|
|
6
|
+
// task's skills in its system prompt and the board's tools on its door, prints the answer to the log and exits, as
|
|
7
|
+
// `hermes chat -q` does. It runs no Hermes code: the worker is a session supercode starts and records (its id on the
|
|
8
|
+
// run), its memory is its harness's own, its requests are answered by supercode's unattended rule. A profile whose
|
|
9
|
+
// worker is Hermes runs Hermes itself, as its harness.
|
|
8
10
|
import { spawn } from 'node:child_process';
|
|
9
11
|
import { existsSync, statSync } from 'node:fs';
|
|
10
12
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
@@ -12,10 +14,12 @@ import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
|
12
14
|
import { load } from './orchestration.mjs';
|
|
13
15
|
import { INSTRUCTION_ARGUMENTS, activationCwd, activationEnv, sandboxArguments, approvalPolicy, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
|
|
14
16
|
import { workerRoute } from './worker-route.mjs';
|
|
15
|
-
import { KANBAN_ENV, untrustedServerVerdict,
|
|
17
|
+
import { KANBAN_ENV, untrustedServerVerdict, hermesInstall } from './hermes-cli.mjs';
|
|
16
18
|
import { pinnedSkills, taskProtocol } from './board/worker-prompt.mjs';
|
|
17
19
|
import { mountOrchestratorMcp } from './mcp/mount.mjs';
|
|
18
|
-
import { normalizeWorkerEvent
|
|
20
|
+
import { normalizeWorkerEvent } from './loop.mjs';
|
|
21
|
+
import { DEFAULT_BOARD, json, openBoard, tx } from './board/store.mjs';
|
|
22
|
+
import { setRunSession } from './board/engine.mjs';
|
|
19
23
|
import { permissionResponse } from './reducer.mjs';
|
|
20
24
|
|
|
21
25
|
/** Hermes's worker argv, as `_default_spawn` builds it. */
|
|
@@ -63,11 +67,9 @@ export async function runKanbanWorker(argv, env = process.env) {
|
|
|
63
67
|
materializeSkills(profile);
|
|
64
68
|
const route = workerRoute(profile, worker, vault);
|
|
65
69
|
const surfaceKey = `kanban|task|${taskId}`;
|
|
66
|
-
const runEnv = {
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
// the task protocol and the pinned skills, supercode's own (board/worker-prompt.mjs)
|
|
70
|
-
const instructions = [userHomePersona(profile), memory, taskProtocol(taskId), pinnedSkills(profile.dir, args.skills)].filter(Boolean).join('\n\n');
|
|
70
|
+
const runEnv = { HERMES_HOME: profile.dir, ...activationEnv({ root, profile, surfaceKey, vault }), ...route.env, ...task };
|
|
71
|
+
// the persona, the task protocol and the pinned skills, supercode's own (board/worker-prompt.mjs)
|
|
72
|
+
const instructions = [userHomePersona(profile), taskProtocol(taskId), pinnedSkills(profile.dir, args.skills)].filter(Boolean).join('\n\n');
|
|
71
73
|
|
|
72
74
|
const client = new SupercodeHarnessClient(env.SUPERCODE_BIN ? { command: env.SUPERCODE_BIN } : {});
|
|
73
75
|
try {
|
|
@@ -90,6 +92,7 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
|
|
|
90
92
|
const mount = mountOrchestratorMcp({ root, profile, harness, surfaceKey, launch, vault, task });
|
|
91
93
|
const policy = approvalPolicy(profile, harness);
|
|
92
94
|
const runtime = await client.startManagedRuntime({ harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}) });
|
|
95
|
+
try { recordSession(root, task, harness, runtime.handle.runtime_id); } catch (error) { say(`hermes-worker: recording the session on its run: ${error.message}`); }
|
|
93
96
|
let text = '';
|
|
94
97
|
// `agent.max_turns` for a worker that takes no cap at launch, held as the daemon holds it (loop.mjs `holdTurnLimit`)
|
|
95
98
|
const counter = turnCounter(profile, harness);
|
|
@@ -104,13 +107,12 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
|
|
|
104
107
|
if (w.kind === 'final' && counter) w = counter.settle(w);
|
|
105
108
|
if (w.kind === 'text') text += w.block && text ? `\n\n${w.delta}` : w.delta;
|
|
106
109
|
else if (w.kind === 'request') {
|
|
107
|
-
//
|
|
110
|
+
// nobody attends a dispatched worker: an untrusted server's tool is denied, anything else is answered by the
|
|
111
|
+
// profile's `worker.permission.unattended` (deny unless it says approve)
|
|
108
112
|
const gated = untrustedServerVerdict(profile, w.tool, 'single_query', { mcp: w.mcp && !String(w.tool ?? '').startsWith('mcp__orchestrator__') });
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
runtime.respond(JSON.parse(w.id), permissionResponse(w.options, approve, hermes?.message ?? null)).catch(() => {});
|
|
113
|
-
});
|
|
113
|
+
const approve = gated ? gated.verdict === 'allow' : profile.worker?.permission?.unattended === 'approve';
|
|
114
|
+
say(`hermes-worker: ${approve ? 'approved' : 'denied'} a permission request with nobody attending (${gated ? gated.why : 'worker.permission.unattended'}): ${w.subject}`);
|
|
115
|
+
runtime.respond(JSON.parse(w.id), permissionResponse(w.options, approve, gated?.message ?? (approve ? null : 'Denied: nobody is attending this run to approve it (worker.permission.unattended). Find an alternative approach that avoids this action.'))).catch(() => {});
|
|
114
116
|
} else if (w.kind === 'final') {
|
|
115
117
|
if (w.outcome === 'error') say(`hermes-worker: the turn failed: ${w.message ?? 'no reason given'}`);
|
|
116
118
|
done(w.outcome === 'error' ? 1 : 0);
|
|
@@ -125,20 +127,27 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
|
|
|
125
127
|
runtime.sendInput(args.query).catch((error) => { say(`hermes-worker: ${error.message}`); done(1); });
|
|
126
128
|
});
|
|
127
129
|
if (text) say(text);
|
|
128
|
-
// the run's transcript in the home's session store, as a Hermes worker's is: tagged `kanban`
|
|
129
|
-
await mirrorRun(client, profile, harness, runtime.handle.runtime_id).catch((error) => say(`hermes-worker: mirror: ${error.message}`));
|
|
130
130
|
await runtime.close().catch(() => {});
|
|
131
131
|
return outcome;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!
|
|
140
|
-
const
|
|
141
|
-
|
|
134
|
+
/**
|
|
135
|
+
* The worker's session on its run, where supercode keeps it (`metadata.supercode.session_id`, beside the pid the
|
|
136
|
+
* dispatcher recorded): the harness's own transcript, found and resumed by supercode like any session.
|
|
137
|
+
*/
|
|
138
|
+
function recordSession(root, task, harness, sessionId) {
|
|
139
|
+
if (!task.HERMES_KANBAN_RUN_ID || !sessionId) return;
|
|
140
|
+
const board = openBoard(root, task.HERMES_KANBAN_BOARD || DEFAULT_BOARD);
|
|
141
|
+
try {
|
|
142
|
+
tx(board, (db) => {
|
|
143
|
+
const row = db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(Number(task.HERMES_KANBAN_RUN_ID));
|
|
144
|
+
if (!row) return;
|
|
145
|
+
const meta = json(row.metadata, {}) ?? {};
|
|
146
|
+
setRunSession(db, Number(task.HERMES_KANBAN_RUN_ID), { ...(meta.supercode ?? {}), session_id: sessionId, harness });
|
|
147
|
+
});
|
|
148
|
+
} finally {
|
|
149
|
+
board.close();
|
|
150
|
+
}
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
/** A profile whose worker is Hermes: the dispatcher's own command, run as Hermes would have run it. */
|
package/mcp/server.mjs
CHANGED
|
@@ -48,8 +48,12 @@ export function bindingFromEnv(env = process.env) {
|
|
|
48
48
|
/** The profile's folder: the root is the `default` profile, `profiles/<name>` the rest (a Hermes home's layout). */
|
|
49
49
|
const profileDir = (binding) => (binding.profile === 'default' ? binding.root : join(binding.root, 'profiles', binding.profile));
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// the tools Hermes's own handlers answer (mcp/verbs.mjs): a worker the board dispatched is supercode's session, and a
|
|
52
|
+
// profile that wants Hermes's session search, memory and skills tools makes Hermes its worker's harness
|
|
53
|
+
const HERMES_ANSWERED = new Set(['session_search', 'memory', 'skills_list', 'skill_view']);
|
|
54
|
+
|
|
55
|
+
/** The tools this door publishes: the orchestrator's, and for a worker the board dispatched, the board's without Hermes's. */
|
|
56
|
+
const published = (binding) => (binding.kanban ? [...TOOLS.filter((t) => !HERMES_ANSWERED.has(t.name)), ...KANBAN_TOOLS] : TOOLS);
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
59
|
* A board tool, answered by supercode's own board (board/tools.mjs) on the home's board file: the task, its run and
|
package/mcp/verbs.mjs
CHANGED
|
@@ -217,7 +217,8 @@ export async function runVerb(orch, { verb, args = {}, profile: profileName, sur
|
|
|
217
217
|
const prompt = typeof args.prompt === 'string' && args.prompt.trim();
|
|
218
218
|
const skilled = Array.isArray(args.skills) && args.skills.some((x) => String(x).trim());
|
|
219
219
|
if (!gate.no_agent && !prompt && !skilled) throw new Error('an agent job needs a prompt or skills (a script alone needs no_agent)');
|
|
220
|
-
|
|
220
|
+
// a board-dispatched worker's schedules are parsed by supercode alone (its session runs no Hermes code)
|
|
221
|
+
const schedule = parseSchedule(args.schedule, now, String(surface ?? '').startsWith('kanban|') ? null : profile.dir);
|
|
221
222
|
// Hermes's tool contract (cron/jobs.py `_normalize_workdir`, measured in
|
|
222
223
|
// ORC-8's week): the workdir is trimmed, `~`-expanded, refused when
|
|
223
224
|
// relative (jobs run detached from any shell cwd), symlink-RESOLVED, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.26",
|
|
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": {
|