@volter/supercode-orchestrator 0.4.129 → 0.4.131
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/ci.mjs +65 -0
- package/board/cli.mjs +3 -3
- package/board/dispatch.mjs +47 -22
- package/board/engine.mjs +8 -5
- package/board/tools.mjs +3 -3
- package/board/wake.mjs +20 -3
- package/board/workflow.mjs +28 -7
- package/mcp/hermes-kanban-tools.json +63 -35
- package/package.json +1 -1
package/board/ci.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Deployment evidence is observed before a board mutation. No network or Git
|
|
2
|
+
// process runs inside the optimistic transaction that decides completion.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
const observations = new Map();
|
|
6
|
+
const key = (board, id) => `${board.root}\0${board.slug}\0${id}`;
|
|
7
|
+
const run = (program, args, cwd) => {
|
|
8
|
+
const result = spawnSync(program, args, { cwd, encoding: 'utf8', timeout: 30_000, maxBuffer: 2 * 1024 * 1024 });
|
|
9
|
+
if (result.status !== 0) throw new Error(`${program}: ${(result.stderr || result.error?.message || 'command failed').trim().slice(-500)}`);
|
|
10
|
+
return result.stdout.trim();
|
|
11
|
+
};
|
|
12
|
+
const codePath = path => path && !/^(?:docs\/|\.volter\/tracker\/)/.test(path) && (!/\.(?:md|txt|png|jpg|svg)$/i.test(path) || /(?:^|\/)prompts\//.test(path));
|
|
13
|
+
|
|
14
|
+
export function completionEvidence(board, id) {
|
|
15
|
+
return observations.get(key(board, id)) ?? { required: true, ready: false, reason: 'Deployment evidence has not been observed; request review or done through the board CLI with --commit and --ci-run.' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function observeCompletionEvidence(board, id, entry, payload = {}) {
|
|
19
|
+
const state = { required: true, ready: false, reason: null };
|
|
20
|
+
observations.set(key(board, id), state);
|
|
21
|
+
if (!entry.params.completion_ci) { Object.assign(state, { required: false, ready: true }); return state; }
|
|
22
|
+
try {
|
|
23
|
+
const task = board.db.prepare('SELECT t.*,c.subtask_of,c.workspace_anchor FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(id);
|
|
24
|
+
const parent = task?.subtask_of && board.db.prepare('SELECT workspace_path FROM tasks WHERE id=?').get(task.subtask_of);
|
|
25
|
+
const path = task?.workspace_path ?? parent?.workspace_path;
|
|
26
|
+
const old = board.db.prepare("SELECT payload FROM task_events WHERE task_id=? AND kind IN ('review_requested','state_requested','review_pass') ORDER BY id DESC LIMIT 20").all(id)
|
|
27
|
+
.map(row => { try { return JSON.parse(row.payload)?.ci; } catch { return null; } }).find(ci => ci?.commit);
|
|
28
|
+
const supplied = payload.commit ?? payload.metadata?.commit ?? old?.commit;
|
|
29
|
+
if (!path) {
|
|
30
|
+
if (supplied) throw new Error('The named commit needs a resolved repository workspace.');
|
|
31
|
+
Object.assign(state, { required: false, ready: true }); return state;
|
|
32
|
+
}
|
|
33
|
+
const history = run('git', ['log', '--all', '--format=%H%x00', '--name-only', '--fixed-strings', `--grep=${id}`], path);
|
|
34
|
+
const records = [...history.matchAll(/([a-f0-9]{40})\0([^\0]*?)(?=[a-f0-9]{40}\0|$)/gs)];
|
|
35
|
+
const latest = records.find(([, , names]) => names.split('\n').some(codePath))?.[1];
|
|
36
|
+
if (!latest && !supplied) { Object.assign(state, { required: false, ready: true }); return state; }
|
|
37
|
+
const commit = run('git', ['rev-parse', '--verify', `${supplied ?? latest}^{commit}`], path);
|
|
38
|
+
if (latest) run('git', ['merge-base', '--is-ancestor', latest, commit], path);
|
|
39
|
+
const remote = run('git', ['remote', 'get-url', 'origin'], path);
|
|
40
|
+
const repo = /(?:github\.com[:/])([^/]+\/[^/]+?)(?:\.git)?$/.exec(remote)?.[1];
|
|
41
|
+
if (!repo) throw new Error('Deployment evidence requires this workspace\'s GitHub origin.');
|
|
42
|
+
Object.assign(state, { commit, repository: repo });
|
|
43
|
+
const configured = entry.params.completion_ci.workflows?.[repo] ?? entry.params.completion_ci.workflows?.['*'];
|
|
44
|
+
if (!Array.isArray(configured) || !configured.length) throw new Error(`No deployment workflow is declared for ${repo} in completion_ci.workflows.`);
|
|
45
|
+
const matchedRules = (entry.params.completion_ci.path_workflows?.[repo] ?? []).filter(rule =>
|
|
46
|
+
records.some(([, , names]) => names.split('\n').some(path => rule.paths.some(prefix => path.startsWith(prefix)))));
|
|
47
|
+
const deploymentWorkflows = matchedRules.length ? configured.filter(name => matchedRules.every(rule => rule.workflows.includes(name))) : configured;
|
|
48
|
+
const requestedRun = payload.ci_run ?? payload['ci-run'] ?? payload.metadata?.ci_run ?? (old?.commit === commit ? old?.run_id : null);
|
|
49
|
+
let receipt;
|
|
50
|
+
if (requestedRun) {
|
|
51
|
+
receipt = JSON.parse(run('gh', ['run', 'view', String(requestedRun), '--repo', repo, '--json', 'databaseId,workflowName,headSha,status,conclusion,url'], path));
|
|
52
|
+
} else {
|
|
53
|
+
const runs = JSON.parse(run('gh', ['run', 'list', '--repo', repo, '--commit', commit, '--limit', '100', '--json', 'databaseId,workflowName,headSha,status,conclusion,url'], path));
|
|
54
|
+
receipt = runs.find(row => deploymentWorkflows.includes(row.workflowName));
|
|
55
|
+
}
|
|
56
|
+
if (!receipt) throw new Error(`No deployment CI run exists for ${repo}@${commit}.`);
|
|
57
|
+
Object.assign(state, { run_id: receipt.databaseId, workflow: receipt.workflowName, status: receipt.status, conclusion: receipt.conclusion, url: receipt.url });
|
|
58
|
+
if (receipt.headSha !== commit) throw new Error(`CI run ${receipt.databaseId} is for ${receipt.headSha}, not ${commit}.`);
|
|
59
|
+
if (!deploymentWorkflows.includes(receipt.workflowName)) throw new Error(`${receipt.workflowName} is not a declared deployment workflow for ${repo}.`);
|
|
60
|
+
if (receipt.status !== 'completed' || receipt.conclusion !== 'success') throw new Error(`Deployment CI ${receipt.databaseId} is ${receipt.status}/${receipt.conclusion || 'pending'}.`);
|
|
61
|
+
state.ready = true;
|
|
62
|
+
state.reason = null;
|
|
63
|
+
} catch (error) { state.reason = error.message; }
|
|
64
|
+
return state;
|
|
65
|
+
}
|
package/board/cli.mjs
CHANGED
|
@@ -537,7 +537,7 @@ export async function runWorkflow(argv) {
|
|
|
537
537
|
case 'complete': {
|
|
538
538
|
need(1, 'a card id');
|
|
539
539
|
const metadata = metadataOf(args.metadata);
|
|
540
|
-
for (const t of rest) await withBoardAsync((b) => engine.complete(b, t, { summary: args.summary ?? null, result: args.result ?? null, metadata: metadata ?? null }));
|
|
540
|
+
for (const t of rest) await withBoardAsync((b) => engine.complete(b, t, { summary: args.summary ?? null, result: args.result ?? null, metadata: metadata ?? null, commit: args.commit ?? null, ci_run: args['ci-run'] ?? null }));
|
|
541
541
|
say(args, rest.map((t) => `Completed ${t}`).join('\n'), { ok: true, completed: rest }); return 0;
|
|
542
542
|
}
|
|
543
543
|
case 'edit': {
|
|
@@ -567,7 +567,7 @@ export async function runWorkflow(argv) {
|
|
|
567
567
|
if (args.reason) for (const [t] of results) await withBoardAsync((b) => engine.comment(b, t, callerName(), `UNBLOCK: ${args.reason}`));
|
|
568
568
|
say(args, results.map(([t, s]) => `Unblocked ${t} → ${s}`).join('\n'), { ok: true, results }); return 0;
|
|
569
569
|
}
|
|
570
|
-
case 'request-review': need(1, 'a card id'); await withBoardAsync((b) => engine.requestReview(b, id, { summary: args.summary ?? null, reviewer: args.reviewer ?? null, metadata: metadataOf(args.metadata) ?? null, force: Boolean(args.force), expectedRunId: workerRun(b.db, id, sid) })); say(args, `${id} → review`, { ok: true }); return 0;
|
|
570
|
+
case 'request-review': need(1, 'a card id'); await withBoardAsync((b) => engine.requestReview(b, id, { summary: args.summary ?? null, reviewer: args.reviewer ?? null, metadata: metadataOf(args.metadata) ?? null, commit: args.commit ?? null, ci_run: args['ci-run'] ?? null, force: Boolean(args.force), expectedRunId: workerRun(b.db, id, sid) })); say(args, `${id} → review`, { ok: true }); return 0;
|
|
571
571
|
case 'request-changes': need(2, 'a card id and the reason'); await withBoardAsync((b) => engine.requestChanges(b, id, { reason: rest.slice(1).join(' '), reviewer: args.reviewer ?? null })); say(args, `${id} sent back for changes`, { ok: true }); return 0;
|
|
572
572
|
case 'reopen': {
|
|
573
573
|
// a done card back to work; the cards that built on it are invalidated with it (Hermes's reopen)
|
|
@@ -856,7 +856,7 @@ export async function runWorkflow(argv) {
|
|
|
856
856
|
if (!declared) throw new Error(`no workflow verb ${verb}; \`supercode workflow help\` lists them`);
|
|
857
857
|
need(1, 'a card id');
|
|
858
858
|
const { _: _words, root: _r, board: _b, json: _j, at: _a, ids: _i, ...fields } = args;
|
|
859
|
-
const payload = { ...fields, waiting_on: args['waiting-on'] ?? fields.waiting_on, reason: args.reason ?? (rest.slice(1).join(' ') || null) };
|
|
859
|
+
const payload = { ...fields, ci_run: args['ci-run'] ?? fields.ci_run, deployed_sha: args['deployed-sha'] ?? fields.deployed_sha, waiting_on: args['waiting-on'] ?? fields.waiting_on, reason: args.reason ?? (rest.slice(1).join(' ') || null) };
|
|
860
860
|
const results = [];
|
|
861
861
|
for (const t of [id, ...(args.ids ?? [])]) results.push([t, (await withBoardAsync((b) => engine.act(b, t, name, { payload }))).to]);
|
|
862
862
|
say(args, results.map(([t, s]) => name === 'request_state' ? `${t}: ${payload.state} requested; state remains ${s}. Mail delivery is recorded on the card.` : `${t} → ${s}`).join('\n'), { ok: true, results }); return 0;
|
package/board/dispatch.mjs
CHANGED
|
@@ -450,16 +450,23 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
450
450
|
return { target, runSpec: spec, profile, lane: profile ? lanes[profile] ?? null : null };
|
|
451
451
|
};
|
|
452
452
|
const candidates = [];
|
|
453
|
+
const waiting = (task, why) => {
|
|
454
|
+
report.queued.push([task.id, why]);
|
|
455
|
+
if (!dry) tx(board, db => noteGuard(db, task.id, why));
|
|
456
|
+
};
|
|
453
457
|
for (const task of everyCard) {
|
|
454
458
|
if (only && task.id !== only) continue;
|
|
455
459
|
const state = scopeFor(task).card;
|
|
456
|
-
if (state.
|
|
457
|
-
if (
|
|
458
|
-
if (
|
|
460
|
+
if (task.subtask_of || state.archived || task.current_run_id || ['done', 'cancelled'].includes(state.status)) continue;
|
|
461
|
+
if (state.dispatch_hold) { waiting(task, 'dispatch held: an explicit start or verified adoption must release it'); continue; }
|
|
462
|
+
if (workflow.run && state.paused) { waiting(task, `paused: ${state.paused.reason ?? 'manager pause'}`); continue; }
|
|
463
|
+
if (workflow.run && state.blocked_by.length) { waiting(task, `blocked by ${state.blocked_by.map(b => b.id).join(', ')}`); continue; }
|
|
464
|
+
if (workflow.run && state.every && state.next_run_at > now()) { waiting(task, `next scheduled run at ${state.next_run_at}`); continue; }
|
|
465
|
+
if (!startable.has(state.status)) { waiting(task, `no ${d.start} transition from ${state.status}`); continue; }
|
|
459
466
|
const plan = planOf(task, scopeFor(task));
|
|
460
|
-
if (!plan) continue;
|
|
467
|
+
if (!plan) { waiting(task, `start guard refused: ${transitionsFor(workflow, state.status, d.start).map(t => t.guard ?? 'sender role').join(' | ')}`); continue; }
|
|
461
468
|
const scope = scopeFor(task, { lane: plan.lane ?? {}, run: { surface: plan.lane?.surface ?? null, local: plan.lane?.surface !== 'pane' || normalMachine(task.machine ?? '') === localMachine() } });
|
|
462
|
-
if (!holds(d.eligible ?? null, scope)) continue;
|
|
469
|
+
if (!holds(d.eligible ?? null, scope)) { waiting(task, `dispatch eligibility refused (memory ${pressure}, local starts ${tickState.local_started}): ${d.eligible}`); continue; }
|
|
463
470
|
candidates.push({ task, plan, scope });
|
|
464
471
|
}
|
|
465
472
|
const keyOf = (c) => (d.order ?? []).map((o) => { const desc = o.startsWith('-'); const v = evaluate(desc ? o.slice(1) : o, c.scope); return { v, desc }; });
|
|
@@ -770,16 +777,17 @@ export async function sessionNotice(board, e) {
|
|
|
770
777
|
return true;
|
|
771
778
|
}
|
|
772
779
|
e = { ...e, address: e.address ?? addressOf(prior) };
|
|
773
|
-
let error = null;
|
|
780
|
+
let error = null, receipt = null;
|
|
774
781
|
try {
|
|
775
782
|
if (!e.address) throw new Error('lifecycle notice awaits the owned launch session identity');
|
|
776
|
-
await send(boardAddress(board.label), board.label, e.address, e.text, `board-run-notice-${e.id}-${e.durableId}
|
|
783
|
+
receipt = await send(boardAddress(board.label), board.label, e.address, e.text, e.messageKey ?? `board-run-notice-${e.id}-${e.durableId}`);
|
|
784
|
+
if (!receipt.delivered) error = receipt.reason ?? 'notice queued; native transcript confirmation pending'; }
|
|
777
785
|
catch (failure) { error = String(failure.message).slice(-500); }
|
|
778
786
|
tx(board, (db) => {
|
|
779
787
|
const session = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(e.runId)?.metadata, {})?.supercode;
|
|
780
788
|
if (session) setRunSession(db, e.runId, { ...session,
|
|
781
789
|
...(error ? { lifecycle_notice_error: error } : { lifecycle_notice_error: null, ...(session.lifecycle_notice_pending === e.durableId ? { lifecycle_notice_pending: null } : {}) }) });
|
|
782
|
-
if (!error || session?.lifecycle_notice_error !== error) event(db, e.id, error ? 'session_notice_failed' : 'session_notice_delivered', { address: e.address, verb: e.verb, source_event: e.sourceEvent, notice: e.durableId, ...(error ? { error, retry: true } : {}) }, e.runId);
|
|
790
|
+
if (!error || session?.lifecycle_notice_error !== error) event(db, e.id, error ? (receipt?.queued ? 'session_notice_queued' : 'session_notice_failed') : 'session_notice_delivered', { address: e.address, verb: e.verb, source_event: e.sourceEvent, notice: e.durableId, receipt, ...(error ? { error, retry: true } : {}) }, e.runId);
|
|
783
791
|
});
|
|
784
792
|
return !error;
|
|
785
793
|
}
|
|
@@ -787,12 +795,21 @@ export async function sessionNotice(board, e) {
|
|
|
787
795
|
/** A session effect a committed move asked for: a message to the card's current actor, or closing a slot's pane. */
|
|
788
796
|
export async function sessionEffect(board, e) {
|
|
789
797
|
if (e.kind === 'send_to_actor') {
|
|
790
|
-
const
|
|
791
|
-
const session =
|
|
798
|
+
const row = view(board, db => e.runId ? db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(e.runId) : null);
|
|
799
|
+
const session = json(row?.metadata, {})?.supercode;
|
|
792
800
|
const address = addressOf(session);
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
801
|
+
let receipt;
|
|
802
|
+
try {
|
|
803
|
+
if (!address) throw new Error('actor session identity unavailable');
|
|
804
|
+
receipt = await send(boardAddress(board.label), board.label, address, e.text, `board-actor-${e.id}-${e.durableId}`);
|
|
805
|
+
} catch (error) { receipt = { delivered: false, reason: error.message }; }
|
|
806
|
+
const kind = receipt.delivered ? 'actor_notice_delivered' : receipt.queued ? 'actor_notice_queued' : 'actor_notice_failed';
|
|
807
|
+
tx(board, db => {
|
|
808
|
+
const previous = db.prepare("SELECT kind,payload FROM task_events WHERE task_id=? AND kind IN ('actor_notice_delivered','actor_notice_queued','actor_notice_failed') ORDER BY id DESC LIMIT 1").get(e.id);
|
|
809
|
+
const payload = { notice: e.durableId, address, receipt };
|
|
810
|
+
if (previous?.kind !== kind || JSON.stringify(json(previous?.payload, {})?.receipt) !== JSON.stringify(receipt)) event(db, e.id, kind, payload, e.runId);
|
|
811
|
+
});
|
|
812
|
+
return receipt.delivered;
|
|
796
813
|
}
|
|
797
814
|
if (e.kind === 'close_session' || e.kind === 'run_ended') {
|
|
798
815
|
tx(board, (db) => requestSessionClose(db, e.id, e.kind === 'run_ended' ? { runId: e.runId } : { slot: e.slot, through: e.runId }));
|
|
@@ -1074,7 +1091,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1074
1091
|
try { deliverNow(board, addressOf(prev), input, `board-run-${task.id}-r${runId}`); }
|
|
1075
1092
|
catch (error) {
|
|
1076
1093
|
tx(board, (db) => {
|
|
1077
|
-
const effect = { kind: 'session_notice', id: task.id, runId, address: addressOf(prev), text: input, verb: 'start' };
|
|
1094
|
+
const effect = { kind: 'session_notice', id: task.id, runId, address: addressOf(prev), text: input, verb: 'start', messageKey: `board-run-${task.id}-r${runId}` };
|
|
1078
1095
|
const saved = db.prepare('INSERT INTO supercode_effects(effect) VALUES(?)').run(JSON.stringify(effect));
|
|
1079
1096
|
setRunSession(db, runId, { lifecycle_notice_pending: Number(saved.lastInsertRowid), lifecycle_notice_error: error.message });
|
|
1080
1097
|
event(db, task.id, 'session_notice_failed', { verb: 'start', error: error.message, retry: true }, runId);
|
|
@@ -1286,6 +1303,7 @@ function deliverNow(board, to, body, key) {
|
|
|
1286
1303
|
const r = sc(['message', 'remote-deliver'], { input: JSON.stringify(request), timeout: 120_000 });
|
|
1287
1304
|
const answer = lastJson(r.stdout);
|
|
1288
1305
|
if (answer?.code !== 0) throw new Error((answer?.text || r.stderr || `no delivery answer (exit ${r.code})`).trim().slice(-300));
|
|
1306
|
+
if (!answer.receipt?.delivered) throw new Error(answer.receipt?.reason ?? 'native delivery confirmation pending');
|
|
1289
1307
|
}
|
|
1290
1308
|
|
|
1291
1309
|
async function send(from, label, to, body, key, { queue = false, ask = false } = {}) {
|
|
@@ -1299,6 +1317,7 @@ async function send(from, label, to, body, key, { queue = false, ask = false } =
|
|
|
1299
1317
|
const r = await scAsync(['message', 'remote-deliver'], { input: JSON.stringify(request), timeout: 180_000 });
|
|
1300
1318
|
const answer = lastJson(r.stdout) ?? {};
|
|
1301
1319
|
if (answer.code !== 0) throw new Error((answer.text || r.stderr || `no answer (exit ${r.code})`).trim().slice(-300));
|
|
1320
|
+
return answer.receipt ?? { delivered: false, reason: 'mail door returned no delivery receipt' };
|
|
1302
1321
|
}
|
|
1303
1322
|
|
|
1304
1323
|
/**
|
|
@@ -1313,8 +1332,9 @@ export async function sendBoardMessages(board, report, taskId = null) {
|
|
|
1313
1332
|
if (taskId && row.task_id !== taskId) continue;
|
|
1314
1333
|
const m = json(row.payload, {}) ?? {};
|
|
1315
1334
|
try {
|
|
1316
|
-
await send(boardAddress(board.label), board.label, m.to, m.text, m.message_id ?? `board-message-${row.id}`, { ask: Boolean(m.message_id) });
|
|
1317
|
-
|
|
1335
|
+
const receipt = await send(boardAddress(board.label), board.label, m.to, m.text, m.message_id ?? `board-message-${row.id}`, { ask: Boolean(m.message_id) });
|
|
1336
|
+
if (!receipt.delivered) throw new Error(receipt.reason ?? 'queued; native transcript confirmation pending');
|
|
1337
|
+
tx(board, (db) => event(db, row.task_id, 'board_message_sent', { event: row.id, to: m.to, receipt, ...(m.message_id ? { message_id: m.message_id } : {}) }));
|
|
1318
1338
|
} catch (error) {
|
|
1319
1339
|
const reason = String(error.message).slice(-200);
|
|
1320
1340
|
report.undelivered.push([row.task_id, m.to, reason]);
|
|
@@ -1386,7 +1406,7 @@ async function notify(board, fleet, report, entry) {
|
|
|
1386
1406
|
// the card as the workflow names it (its status the workflow's: triaging, reviewing, stopped; `stored_as` kanban.db's)
|
|
1387
1407
|
const task = view(board, (db) => { const t = getTask(db, sub.task_id); return t ? { ...t, status: statusOf(db, t), stored_as: t.status } : t; });
|
|
1388
1408
|
let cursor = Number(sub.last_event_id);
|
|
1389
|
-
let failed = null;
|
|
1409
|
+
let failed = null, pending = false;
|
|
1390
1410
|
try {
|
|
1391
1411
|
for (const ev of events) {
|
|
1392
1412
|
const p = json(ev.payload, {}) ?? {};
|
|
@@ -1399,23 +1419,28 @@ async function notify(board, fleet, report, entry) {
|
|
|
1399
1419
|
text = c ? noticeOf(entry, role, ev.kind, task, p, c) : null;
|
|
1400
1420
|
} else text = noticeOf(entry, role, ev.kind, task, p);
|
|
1401
1421
|
if (text) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
tx(board, (db) => event(db, sub.task_id, 'notice_delivered', { source_event: ev.id, kind: ev.kind, address: sub.chat_id }));
|
|
1422
|
+
const receipt = await send(from, board.label, sub.chat_id, text, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { queue: !entry.workflow.run && sub.delivery_mode === 'notify' });
|
|
1423
|
+
if (!receipt.delivered) { pending = Boolean(receipt.queued); throw new Error(receipt.reason ?? 'native delivery confirmation pending'); }
|
|
1424
|
+
tx(board, (db) => event(db, sub.task_id, 'notice_delivered', { source_event: ev.id, kind: ev.kind, address: sub.chat_id, receipt }));
|
|
1405
1425
|
report.notified.push([sub.task_id, ev.kind, sub.chat_id]);
|
|
1406
1426
|
}
|
|
1407
1427
|
cursor = ev.id;
|
|
1408
1428
|
}
|
|
1409
1429
|
} catch (error) {
|
|
1410
1430
|
failed = String(error.message).slice(-200);
|
|
1411
|
-
tx(board, (db) =>
|
|
1431
|
+
tx(board, (db) => {
|
|
1432
|
+
const kind = pending ? 'notice_queued' : 'notice_failed';
|
|
1433
|
+
const previous = db.prepare('SELECT payload FROM task_events WHERE task_id=? AND kind=? ORDER BY id DESC LIMIT 1').get(sub.task_id, kind);
|
|
1434
|
+
const payload = { after_event: cursor, address: sub.chat_id, error: failed, retry: true };
|
|
1435
|
+
if (JSON.stringify(json(previous?.payload, {})) !== JSON.stringify(payload)) event(db, sub.task_id, kind, payload);
|
|
1436
|
+
});
|
|
1412
1437
|
report.undelivered.push([sub.task_id, sub.chat_id, failed]);
|
|
1413
1438
|
}
|
|
1414
1439
|
// the cursor moves past what was delivered (a failed delivery is retried next tick; the mail id dedupes)
|
|
1415
1440
|
tx(board, (db) => {
|
|
1416
1441
|
const where = "WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?";
|
|
1417
1442
|
db.prepare(`UPDATE kanban_notify_subs SET last_event_id = ? ${where} AND last_event_id < ?`).run(cursor, sub.task_id, sub.chat_id, cursor);
|
|
1418
|
-
const failures = failed ? Number(sub.delivery_metadata?.failures ?? 0) + 1 : 0;
|
|
1443
|
+
const failures = failed && !pending ? Number(sub.delivery_metadata?.failures ?? 0) + 1 : 0;
|
|
1419
1444
|
if (failures >= MAX_SEND_FAILURES) { db.prepare(`DELETE FROM kanban_notify_subs ${where}`).run(sub.task_id, sub.chat_id); return; }
|
|
1420
1445
|
if (failures !== Number(sub.delivery_metadata?.failures ?? 0)) db.prepare(`UPDATE kanban_notify_subs SET delivery_metadata = ? ${where}`).run(JSON.stringify({ ...sub.delivery_metadata, failures }), sub.task_id, sub.chat_id);
|
|
1421
1446
|
if (task?.status === 'archived' && cursor >= events.at(-1).id) db.prepare(`DELETE FROM kanban_notify_subs ${where}`).run(sub.task_id, sub.chat_id);
|
package/board/engine.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { join, resolve } from 'node:path';
|
|
|
13
13
|
import { checkWorkspace, assertWorkspaceOwner, declareWorkspace, isLocalWorkspace, isWorkspacePath } from './workspace.mjs';
|
|
14
14
|
import { DatabaseSync } from 'node:sqlite';
|
|
15
15
|
import { view, actorOf, claimer, event, json, newTaskId, now, tx } from './store.mjs';
|
|
16
|
+
import { observeCompletionEvidence } from './ci.mjs';
|
|
16
17
|
import { addDependency, retireSupersededRuns, refreshCardInputs, requestSessionClose, eventTo, fire, loadWorkflow, setStatus, settle, startNew, statusOf, runEvent, workflowFor } from './workflow.mjs';
|
|
17
18
|
|
|
18
19
|
export const STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'review', 'done', 'archived'];
|
|
@@ -347,6 +348,7 @@ export function setCaller(fn) { callerRoles = fn; }
|
|
|
347
348
|
export async function act(board, id, name, { roles = null, payload = {}, extra = {} } = {}) {
|
|
348
349
|
const entry = await loadWorkflow(board.root);
|
|
349
350
|
refreshCardInputs(board, id, { commits: ['complete', 'request_review'].includes(name) });
|
|
351
|
+
if (['complete', 'request_review', 'request_state'].includes(name)) payload = { ...payload, ci: observeCompletionEvidence(board, id, entry, payload) };
|
|
350
352
|
const result = tx(board, (db) => {
|
|
351
353
|
roles ??= callerRoles(db, id);
|
|
352
354
|
const r = fire(db, board, entry, id, name, { roles, payload, extra });
|
|
@@ -364,6 +366,7 @@ export async function act(board, id, name, { roles = null, payload = {}, extra =
|
|
|
364
366
|
export async function goto(board, id, status, { roles = null, payload = {} } = {}) {
|
|
365
367
|
const entry = await loadWorkflow(board.root);
|
|
366
368
|
refreshCardInputs(board, id, { commits: true });
|
|
369
|
+
observeCompletionEvidence(board, id, entry, payload);
|
|
367
370
|
const { name, refusal, from } = (() => {
|
|
368
371
|
const db=board.db;
|
|
369
372
|
roles ??= callerRoles(db, id);
|
|
@@ -397,7 +400,7 @@ export async function runEffects(board, effects = [], { retryNotices = false } =
|
|
|
397
400
|
else if (e.kind === 'close_session' || e.kind === 'send_to_actor' || e.kind === 'run_ended') {
|
|
398
401
|
if (e.kind === 'run_ended' && e.outcome === 'reclaimed') stopWorker(board, e.runId);
|
|
399
402
|
const { sessionEffect } = await import('./dispatch.mjs');
|
|
400
|
-
await sessionEffect(board, e);
|
|
403
|
+
if (await sessionEffect(board, e) === false) continue;
|
|
401
404
|
}
|
|
402
405
|
if (e.durableId) tx(board, (db) => db.prepare('DELETE FROM supercode_effects WHERE id = ?').run(e.durableId));
|
|
403
406
|
}
|
|
@@ -454,14 +457,14 @@ function invalidateDescendants(db, board, id) {
|
|
|
454
457
|
|
|
455
458
|
// each of Hermes's verbs is an event of the workflow; the words it takes are the event's payload
|
|
456
459
|
|
|
457
|
-
export const complete = (board, id, { result = null, summary = null, metadata = null, roles, runId = null } = {}) =>
|
|
458
|
-
act(board, id, 'complete', { roles, payload: { result, summary, metadata, run_id: runId } });
|
|
460
|
+
export const complete = (board, id, { result = null, summary = null, metadata = null, roles, runId = null, commit = null, ci_run = null } = {}) =>
|
|
461
|
+
act(board, id, 'complete', { roles, payload: { result, summary, metadata, run_id: runId, commit, ci_run } });
|
|
459
462
|
// 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`)
|
|
460
463
|
export const block = (board, id, { reason = null, on = null, waitingOn = null, roles, runId = null } = {}) =>
|
|
461
464
|
act(board, id, 'block', { roles, payload: { reason, on, waiting_on: waitingOn, run_id: runId } });
|
|
462
465
|
export const unblock = (board, id, { reason = null, on = null, roles } = {}) => act(board, id, 'unblock', { roles, payload: { reason, on } });
|
|
463
|
-
export const requestReview = (board, id, { summary = null, metadata = null, reviewer = null, force = false, expectedRunId = null, roles } = {}) =>
|
|
464
|
-
act(board, id, 'request_review', { roles, payload: { summary, metadata, reviewer, force, run_id: expectedRunId } });
|
|
466
|
+
export const requestReview = (board, id, { summary = null, metadata = null, reviewer = null, force = false, expectedRunId = null, roles, commit = null, ci_run = null } = {}) =>
|
|
467
|
+
act(board, id, 'request_review', { roles, payload: { summary, metadata, reviewer, force, run_id: expectedRunId, commit, ci_run } });
|
|
465
468
|
export const requestChanges = (board, id, { reason = null, reviewer = null, roles } = {}) => act(board, id, 'request_changes', { roles, payload: { reason, reviewer } });
|
|
466
469
|
export const reopen = (board, id, { author = 'user', reason = null, roles } = {}) => act(board, id, 'reopen', { roles, payload: { author, reason } });
|
|
467
470
|
export const reopenReview = (board, id, { roles } = {}) => act(board, id, 'reopen_review', { roles });
|
package/board/tools.mjs
CHANGED
|
@@ -105,7 +105,7 @@ export async function boardTool(root, name, args = {}, env = {}) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
if (env.HERMES_SESSION_ID && own === tid) metadata = { ...(metadata ?? {}), worker_session_id: env.HERMES_SESSION_ID };
|
|
108
|
-
await engine.complete(board, tid, { summary, result, metadata });
|
|
108
|
+
await engine.complete(board, tid, { summary, result, metadata, commit: args.commit ?? null, ci_run: args.ci_run ?? null });
|
|
109
109
|
return ok({ task_id: tid, status: 'done' });
|
|
110
110
|
}
|
|
111
111
|
case 'kanban_block': {
|
|
@@ -115,12 +115,12 @@ export async function boardTool(root, name, args = {}, env = {}) {
|
|
|
115
115
|
}
|
|
116
116
|
case 'kanban_request_state': {
|
|
117
117
|
const stale = ownRun(); if (stale) return error(stale);
|
|
118
|
-
await engine.act(board, tid, 'request_state', { payload: { state: args.state, reason: args.reason, on: args.on ?? null, waiting_on: args.waiting_on ?? null, evidence: args.evidence ?? null } });
|
|
118
|
+
await engine.act(board, tid, 'request_state', { payload: { state: args.state, reason: args.reason, on: args.on ?? null, waiting_on: args.waiting_on ?? null, evidence: args.evidence ?? null, commit: args.commit ?? null, ci_run: args.ci_run ?? null, deployed_sha: args.deployed_sha ?? null } });
|
|
119
119
|
return ok({ task_id: tid, requested: args.state, changed: false });
|
|
120
120
|
}
|
|
121
121
|
case 'kanban_request_review': {
|
|
122
122
|
const stale = ownRun(); if (stale) return error(stale);
|
|
123
|
-
await engine.requestReview(board, tid, { summary: args.summary ? redact(String(args.summary)) : null, metadata: args.metadata ? redact(args.metadata) : null, reviewer: args.reviewer ?? null, expectedRunId: own === tid && runId ? runId : null });
|
|
123
|
+
await engine.requestReview(board, tid, { summary: args.summary ? redact(String(args.summary)) : null, metadata: args.metadata ? redact(args.metadata) : null, commit: args.commit ?? null, ci_run: args.ci_run ?? null, reviewer: args.reviewer ?? null, expectedRunId: own === tid && runId ? runId : null });
|
|
124
124
|
return ok({ task_id: tid, status: 'review' });
|
|
125
125
|
}
|
|
126
126
|
case 'kanban_request_changes': {
|
package/board/wake.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export function signalBoardWrite(root, actor) {
|
|
|
20
20
|
/** One coalescing queue shared by standalone serve and the hosted dispatcher. */
|
|
21
21
|
export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
22
22
|
const watchers = new Map();
|
|
23
|
-
let pending = null, waiter = null, closed = false, child = null, retry = null, lastError = null;
|
|
23
|
+
let pending = null, waiter = null, closed = false, child = null, retry = null, lastError = null, catalogue = null;
|
|
24
24
|
const wake = (source, at = Date.now()) => {
|
|
25
25
|
if (closed) return;
|
|
26
26
|
if (!pending) pending = { at, sources: new Set() };
|
|
@@ -83,7 +83,7 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
83
83
|
if (closed) return;
|
|
84
84
|
let buffer = '', error = '';
|
|
85
85
|
const command = env.SUPERCODE_TEAMS_ENTRY ? process.execPath : binary;
|
|
86
|
-
const args = [...(env.SUPERCODE_TEAMS_ENTRY ? [env.SUPERCODE_TEAMS_ENTRY] : []), 'sessions', 'list', '--watch', '--json'];
|
|
86
|
+
const args = [...(env.SUPERCODE_TEAMS_ENTRY ? [env.SUPERCODE_TEAMS_ENTRY] : []), 'sessions', 'list', '--watch', '--json', '--limit', '200'];
|
|
87
87
|
child = spawn(command, args, { cwd: tmpdir(), env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
88
88
|
child.stdout.setEncoding('utf8').on('data', chunk => {
|
|
89
89
|
buffer += chunk;
|
|
@@ -94,7 +94,24 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
94
94
|
try {
|
|
95
95
|
const event = JSON.parse(line);
|
|
96
96
|
lastError = null;
|
|
97
|
-
if (
|
|
97
|
+
if (['heartbeat', 'ping', 'keepalive'].includes(event.type)) continue;
|
|
98
|
+
if (event.type === 'catalog_reset' && Array.isArray(event.data?.items)) {
|
|
99
|
+
// Transcript revisions and sort order are not scheduling events.
|
|
100
|
+
// The safety sweep reads quiet timestamps; wake for session identity,
|
|
101
|
+
// availability and available delivery/control doors changing now.
|
|
102
|
+
const next = JSON.stringify(event.data.items.map(item => ({
|
|
103
|
+
id:item.id, harness:item.harness, machines:[...(item.machine_ids ?? [])].sort(),
|
|
104
|
+
bindings:(item.bindings ?? []).map(binding => ({ id:binding.id, machine:binding.machine_id,
|
|
105
|
+
incarnation:binding.incarnation_id, runtime:binding.native_runtime_id, session:binding.native_session_id,
|
|
106
|
+
harness:binding.harness, state:binding.state, capabilities:[...(binding.native_capabilities ?? [])].sort(),
|
|
107
|
+
})).sort((a,b)=>String(a.id).localeCompare(String(b.id))), actions:item.actions,
|
|
108
|
+
presence:item.activity?.presence, turn:item.activity?.turn,
|
|
109
|
+
status:item.live_status, delivery:item.delivery,
|
|
110
|
+
})).sort((a,b)=>String(a.id).localeCompare(String(b.id))));
|
|
111
|
+
if (next === catalogue) continue;
|
|
112
|
+
catalogue = next;
|
|
113
|
+
}
|
|
114
|
+
wake('session-catalogue');
|
|
98
115
|
} catch { /* CLI diagnostics are not events */ }
|
|
99
116
|
}
|
|
100
117
|
});
|
package/board/workflow.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { resolve, relative, isAbsolute } from 'node:path';
|
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
12
|
import { actorOf, appendRunObservation, event as record, json, now } from './store.mjs';
|
|
13
13
|
import { evaluate, holds, render } from './expr.mjs';
|
|
14
|
+
import { completionEvidence } from './ci.mjs';
|
|
14
15
|
|
|
15
16
|
const loaded = new Map();
|
|
16
17
|
|
|
@@ -222,6 +223,7 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
|
|
|
222
223
|
const card = {
|
|
223
224
|
...task,
|
|
224
225
|
creator_address: creatorOf(db, task.id),
|
|
226
|
+
ci: completionEvidence(board, task.id),
|
|
225
227
|
dispatch_hold: one(db, "SELECT kind FROM task_events WHERE task_id=? AND kind IN ('dispatch_held','dispatch_released','session_adopted') ORDER BY id DESC LIMIT 1", task.id)?.kind === 'dispatch_held',
|
|
226
228
|
status,
|
|
227
229
|
blocked_by: blocks,
|
|
@@ -359,9 +361,17 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
|
|
|
359
361
|
const allowed = candidates.filter((t) => !t.by?.length || t.by.some((r) => roles.includes(r)));
|
|
360
362
|
if (!allowed.length) throw new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} there is for ${[...new Set(candidates.flatMap((t) => t.by ?? []))].join(' or ')}. ${candidates.map((t) => t.description).filter(Boolean).join(' ')}`);
|
|
361
363
|
const taken = allowed.find((t) => holds(t.guard ?? null, scope));
|
|
362
|
-
if (!taken) {
|
|
364
|
+
if (!taken) {
|
|
365
|
+
const state = scope.card;
|
|
366
|
+
const reasons = [state.paused && `paused: ${state.paused.reason ?? 'manager pause'}`,
|
|
367
|
+
state.blocked_by.length && `blocked by ${state.blocked_by.map(b => b.id).join(', ')}`,
|
|
368
|
+
state.dispatch_hold && 'dispatch held', state.current_run_id && `current run ${state.current_run_id}`,
|
|
369
|
+
state.archived && 'archived'].filter(Boolean);
|
|
370
|
+
const none = new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} cannot proceed: ${reasons.join('; ') || `guard ${allowed.map(t => t.guard).join(' | ')}`}`);
|
|
371
|
+
none.noTransition = true; throw none;
|
|
372
|
+
}
|
|
363
373
|
for (const v of taken.validate ?? []) {
|
|
364
|
-
if (!holds(v.expr, scope)) { const refusal = new Error(
|
|
374
|
+
if (!holds(v.expr, scope)) { const reason = render(v.message, scope); const refusal = new Error(reason); refusal.refusal = reason; throw refusal; }
|
|
365
375
|
}
|
|
366
376
|
const effects = [];
|
|
367
377
|
const to = take(db, board, entry, task, from, taken, scope, effects);
|
|
@@ -507,12 +517,12 @@ const firstLine = (text) => String(text ?? '').trim().split('\n')[0].slice(0, 40
|
|
|
507
517
|
const FORMAT = {
|
|
508
518
|
completed: ({ e }) => {
|
|
509
519
|
const artifacts = Array.isArray(e.metadata?.artifacts) ? e.metadata.artifacts.filter((a) => typeof a === 'string' && a.trim()) : [];
|
|
510
|
-
return { result_len: e.result ? String(e.result).length : 0, summary: firstLine(e.summary ?? e.result), ...(artifacts.length ? { artifacts } : {}) };
|
|
520
|
+
return { ci: e.ci ?? null, result_len: e.result ? String(e.result).length : 0, summary: firstLine(e.summary ?? e.result), ...(artifacts.length ? { artifacts } : {}) };
|
|
511
521
|
},
|
|
512
522
|
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 }),
|
|
513
523
|
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 }),
|
|
514
524
|
dependency_wait: ({ e, source }) => ({ reason: e.reason ?? null, on: e.on ?? null, source_status: source }),
|
|
515
|
-
review_requested: ({ e, before, after }) => ({ summary: firstLine(e.summary), implementer: before.assignee, reviewer: after.assignee !== before.assignee ? after.assignee : (e.reviewer ?? null) }),
|
|
525
|
+
review_requested: ({ e, before, after }) => ({ summary: firstLine(e.summary), ci: e.ci ?? null, implementer: before.assignee, reviewer: after.assignee !== before.assignee ? after.assignee : (e.reviewer ?? null) }),
|
|
516
526
|
// the reviewer is the lane that reviewed (the card's assignee while in review), unless the verdict names one
|
|
517
527
|
changes_requested: ({ e, before, after }) => ({ reason: e.reason ?? null, reviewer: e.reviewer ?? before.assignee ?? null, implementer: after.assignee }),
|
|
518
528
|
review_reopened: ({ after, to }) => ({ status: to, implementer: after.assignee }),
|
|
@@ -559,7 +569,14 @@ const ACTIONS = {
|
|
|
559
569
|
}
|
|
560
570
|
if (remove) {
|
|
561
571
|
if (!String(reason ?? '').trim()) throw new Error('unblock must say what arrived');
|
|
562
|
-
|
|
572
|
+
let resolved = remove;
|
|
573
|
+
if (remove !== '*' && remove !== 'messages') {
|
|
574
|
+
const matches = blocks.filter(b => b.id === remove || (b.kind === 'message' && String(remove).startsWith('m-') &&
|
|
575
|
+
(String(remove).startsWith(b.id) || b.id.startsWith(String(remove)))));
|
|
576
|
+
if (matches.length !== 1) throw new Error(matches.length ? `ambiguous dependency ${remove}; name one of ${matches.map(b => b.id).join(', ')}` : `no dependency ${remove}; current blockers: ${blocks.map(b => b.id).join(', ') || 'none'}`);
|
|
577
|
+
resolved = matches[0].id;
|
|
578
|
+
}
|
|
579
|
+
blocks = blocks.filter((b) => resolved === 'messages' ? b.kind !== 'message' : resolved !== '*' && b.id !== resolved);
|
|
563
580
|
}
|
|
564
581
|
cardField(ctx, 'blocked_by', JSON.stringify(blocks));
|
|
565
582
|
// task_links is the Hermes projection of the canonical card relations, including within this transaction.
|
|
@@ -763,7 +780,11 @@ const ACTIONS = {
|
|
|
763
780
|
record(ctx.db, ctx.id, 'board_message', { to: String(to), text: `${String(text ?? '')}\n\nAnswer with reply_to ${id}.`, message_id: id }, null);
|
|
764
781
|
ctx.effects.push({ kind: 'board_message', id: ctx.id });
|
|
765
782
|
},
|
|
766
|
-
send_to_actor(ctx, text) {
|
|
783
|
+
send_to_actor(ctx, text) {
|
|
784
|
+
const effect = { kind: 'send_to_actor', id: ctx.id, runId: ctx.runId, text };
|
|
785
|
+
const inserted = run(ctx.db, 'INSERT INTO supercode_effects (effect) VALUES (?)', JSON.stringify(effect));
|
|
786
|
+
ctx.effects.push({ ...effect, durableId: Number(inserted.lastInsertRowid) });
|
|
787
|
+
},
|
|
767
788
|
};
|
|
768
789
|
|
|
769
790
|
/** Binding a newer run transfers ownership; stale run references cannot veto its later stop. */
|
|
@@ -879,7 +900,7 @@ export function fireRun(db, board, entry, id, name, { roles = [], payload = {},
|
|
|
879
900
|
const candidates = [...(machine.states[from]?.on?.[name] ?? []), ...(machine.on?.[name] ?? [])];
|
|
880
901
|
const t = candidates.find((t) => (!t.from?.length || t.from.includes(from)) && (!t.by?.length || t.by.some((r) => roles.includes(r))) && holds(t.guard ?? null, scope));
|
|
881
902
|
if (!t) { const error = new Error(`${id}: run event ${name} does not apply`); error.noTransition = true; throw error; }
|
|
882
|
-
for (const v of t.validate ?? []) if (!holds(v.expr, scope)) throw new Error(v.message);
|
|
903
|
+
for (const v of t.validate ?? []) if (!holds(v.expr, scope)) throw new Error(render(v.message, scope));
|
|
883
904
|
const to = t.target ? String(templateValue(t.target, scope)) : from;
|
|
884
905
|
if (!machine.states[to]) throw new Error(`unknown run state ${to}`);
|
|
885
906
|
const effects = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"name": "kanban_show",
|
|
4
|
-
"description": "Read a task's full state
|
|
4
|
+
"description": "Read a task's full state \u2014 title, body, assignee, parent task handoffs, your prior attempts on this task if any, comments, and recent events. Use this to (re)orient yourself before starting work, especially on retries. The response includes a pre-formatted ``worker_context`` string suitable for inclusion verbatim in your reasoning.",
|
|
5
5
|
"inputSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"board": {
|
|
13
13
|
"type": "string",
|
|
14
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
14
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"required": []
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"name": "kanban_list",
|
|
22
|
-
"description": "List Kanban task summaries so an orchestrator profile can discover work to route. Supports the same core filters as the CLI: assignee, status, tenant, include_archived, and limit. Returns compact rows with ids, title, status, assignee, priority, parent/child ids, and counts. Bounded to 50 rows by default, 200 max, with truncation metadata. Also recomputes ready tasks before listing, matching the CLI. Orchestrator-only
|
|
22
|
+
"description": "List Kanban task summaries so an orchestrator profile can discover work to route. Supports the same core filters as the CLI: assignee, status, tenant, include_archived, and limit. Returns compact rows with ids, title, status, assignee, priority, parent/child ids, and counts. Bounded to 50 rows by default, 200 max, with truncation metadata. Also recomputes ready tasks before listing, matching the CLI. Orchestrator-only \u2014 dispatcher-spawned task workers never see this tool.",
|
|
23
23
|
"inputSchema": {
|
|
24
24
|
"type": "object",
|
|
25
25
|
"properties": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"board": {
|
|
56
56
|
"type": "string",
|
|
57
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
57
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"required": []
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"name": "kanban_complete",
|
|
65
|
-
"description": "Complete through the home's declared workflow permissions with a structured handoff. Homes may reserve terminal closure to the recorded creator or manager; use kanban_request_state when you need their decision. Prefer ``summary`` for a human-readable 1-3 sentence description of what you did; put machine-readable facts in ``metadata`` (changed_files, tests_run, decisions, findings, etc). At least one of ``summary`` or ``result`` is required. If you created new tasks via ``kanban_create`` during this run, list their ids in ``created_cards``
|
|
65
|
+
"description": "Complete through the home's declared workflow permissions with a structured handoff. Homes may reserve terminal closure to the recorded creator or manager; use kanban_request_state when you need their decision. Prefer ``summary`` for a human-readable 1-3 sentence description of what you did; put machine-readable facts in ``metadata`` (changed_files, tests_run, decisions, findings, etc). At least one of ``summary`` or ``result`` is required. If you created new tasks via ``kanban_create`` during this run, list their ids in ``created_cards`` \u2014 the kernel verifies them so phantom references are caught before they leak into downstream automation. If you produced deliverable files (charts, PDFs, spreadsheets, generated images), list their absolute paths in ``artifacts`` \u2014 the gateway notifier will upload them as native attachments to the human who subscribed to the task, so the deliverable lands in their chat alongside the summary instead of being a path they have to fetch by hand.",
|
|
66
66
|
"inputSchema": {
|
|
67
67
|
"type": "object",
|
|
68
68
|
"properties": {
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"metadata": {
|
|
78
78
|
"type": "object",
|
|
79
|
-
"description": "Free-form dict of structured facts about this attempt
|
|
79
|
+
"description": "Free-form dict of structured facts about this attempt \u2014 {\"changed_files\": [...], \"tests_run\": 12, \"findings\": [...]}. Surfaced to downstream workers alongside ``summary``."
|
|
80
80
|
},
|
|
81
81
|
"result": {
|
|
82
82
|
"type": "string",
|
|
@@ -87,18 +87,26 @@
|
|
|
87
87
|
"items": {
|
|
88
88
|
"type": "string"
|
|
89
89
|
},
|
|
90
|
-
"description": "Optional structured manifest of task ids you created via ``kanban_create`` during this run. The kernel verifies each id exists and was created by this worker's profile; any phantom id blocks the completion with an error listing what went wrong (auditable in the task's events). Only list ids you got back from a successful ``kanban_create`` call
|
|
90
|
+
"description": "Optional structured manifest of task ids you created via ``kanban_create`` during this run. The kernel verifies each id exists and was created by this worker's profile; any phantom id blocks the completion with an error listing what went wrong (auditable in the task's events). Only list ids you got back from a successful ``kanban_create`` call \u2014 do not invent or remember ids from prose. Omit the field if you did not create any cards."
|
|
91
91
|
},
|
|
92
92
|
"artifacts": {
|
|
93
93
|
"type": "array",
|
|
94
94
|
"items": {
|
|
95
95
|
"type": "string"
|
|
96
96
|
},
|
|
97
|
-
"description": "Optional list of absolute paths to deliverable files you produced during this run
|
|
97
|
+
"description": "Optional list of absolute paths to deliverable files you produced during this run \u2014 generated charts, PDFs, spreadsheets, images, archives. Examples: [\"/tmp/q3-revenue.png\", \"/tmp/report.pdf\"]. The gateway notifier uploads each path as a native attachment to the subscribed chat (images embed inline, everything else uploads as a file) so the deliverable lands with the completion notification. Skip intermediate scratch files and references that are not the deliverable. The path must exist on disk at completion. Files inside a managed scratch workspace are copied to durable task attachments before cleanup; a missing declared scratch artifact keeps the task in-flight so you can fix the path and retry."
|
|
98
98
|
},
|
|
99
99
|
"board": {
|
|
100
100
|
"type": "string",
|
|
101
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
101
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
102
|
+
},
|
|
103
|
+
"commit": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "Exact commit deployed by the CI run."
|
|
106
|
+
},
|
|
107
|
+
"ci_run": {
|
|
108
|
+
"type": "integer",
|
|
109
|
+
"description": "GitHub Actions deployment run id for that exact commit."
|
|
102
110
|
}
|
|
103
111
|
},
|
|
104
112
|
"required": []
|
|
@@ -124,11 +132,11 @@
|
|
|
124
132
|
},
|
|
125
133
|
"waiting_on": {
|
|
126
134
|
"type": "string",
|
|
127
|
-
"description": "The id of the message (m
|
|
135
|
+
"description": "The id of the message (m-\u2026 or a-\u2026) whose answer this waits for."
|
|
128
136
|
},
|
|
129
137
|
"board": {
|
|
130
138
|
"type": "string",
|
|
131
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
139
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
132
140
|
}
|
|
133
141
|
},
|
|
134
142
|
"required": [
|
|
@@ -138,7 +146,7 @@
|
|
|
138
146
|
},
|
|
139
147
|
{
|
|
140
148
|
"name": "kanban_request_review",
|
|
141
|
-
"description": "Hand the task off for review: implementation, self-review, and verification are complete and you want a human (or reviewer) to look before it is marked done. Moves the task to the 'review' column and notifies the subscriber. Unlike ``kanban_block`` this is NOT a blocker
|
|
149
|
+
"description": "Hand the task off for review: implementation, self-review, and verification are complete and you want a human (or reviewer) to look before it is marked done. Moves the task to the 'review' column and notifies the subscriber. Unlike ``kanban_block`` this is NOT a blocker \u2014 it never counts toward unblock-loop detection, so a task can cycle through review across follow-ups without ever being falsely escalated to triage. Use this instead of blocking with a free-form 'review-required:' reason.",
|
|
142
150
|
"inputSchema": {
|
|
143
151
|
"type": "object",
|
|
144
152
|
"properties": {
|
|
@@ -148,7 +156,7 @@
|
|
|
148
156
|
},
|
|
149
157
|
"summary": {
|
|
150
158
|
"type": "string",
|
|
151
|
-
"description": "What was implemented and how it was verified, in one or two sentences
|
|
159
|
+
"description": "What was implemented and how it was verified, in one or two sentences \u2014 shown to the reviewer. Don't paste the whole diff; the reviewer has the board and the PR."
|
|
152
160
|
},
|
|
153
161
|
"reviewer": {
|
|
154
162
|
"type": "string",
|
|
@@ -161,7 +169,15 @@
|
|
|
161
169
|
},
|
|
162
170
|
"board": {
|
|
163
171
|
"type": "string",
|
|
164
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
172
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
173
|
+
},
|
|
174
|
+
"commit": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"description": "Exact commit deployed by the CI run."
|
|
177
|
+
},
|
|
178
|
+
"ci_run": {
|
|
179
|
+
"type": "integer",
|
|
180
|
+
"description": "GitHub Actions deployment run id for that exact commit."
|
|
165
181
|
}
|
|
166
182
|
},
|
|
167
183
|
"required": [
|
|
@@ -185,7 +201,7 @@
|
|
|
185
201
|
},
|
|
186
202
|
"board": {
|
|
187
203
|
"type": "string",
|
|
188
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
204
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
189
205
|
}
|
|
190
206
|
},
|
|
191
207
|
"required": [
|
|
@@ -195,7 +211,7 @@
|
|
|
195
211
|
},
|
|
196
212
|
{
|
|
197
213
|
"name": "kanban_heartbeat",
|
|
198
|
-
"description": "Signal that you're still alive during a long operation (training, encoding, large crawls). Call every few minutes so humans see liveness separately from PID checks. Pure side effect
|
|
214
|
+
"description": "Signal that you're still alive during a long operation (training, encoding, large crawls). Call every few minutes so humans see liveness separately from PID checks. Pure side effect \u2014 no work changes.",
|
|
199
215
|
"inputSchema": {
|
|
200
216
|
"type": "object",
|
|
201
217
|
"properties": {
|
|
@@ -209,7 +225,7 @@
|
|
|
209
225
|
},
|
|
210
226
|
"board": {
|
|
211
227
|
"type": "string",
|
|
212
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
228
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
213
229
|
}
|
|
214
230
|
},
|
|
215
231
|
"required": []
|
|
@@ -217,13 +233,13 @@
|
|
|
217
233
|
},
|
|
218
234
|
{
|
|
219
235
|
"name": "kanban_comment",
|
|
220
|
-
"description": "Append a comment to a task's thread. Use for durable notes that should outlive this run (questions for the next worker, partial findings, rationale). Ephemeral reasoning doesn't belong here
|
|
236
|
+
"description": "Append a comment to a task's thread. Use for durable notes that should outlive this run (questions for the next worker, partial findings, rationale). Ephemeral reasoning doesn't belong here \u2014 use your normal response instead.",
|
|
221
237
|
"inputSchema": {
|
|
222
238
|
"type": "object",
|
|
223
239
|
"properties": {
|
|
224
240
|
"task_id": {
|
|
225
241
|
"type": "string",
|
|
226
|
-
"description": "Task id. Required (may be your own task or another's
|
|
242
|
+
"description": "Task id. Required (may be your own task or another's \u2014 comment threads are per-task)."
|
|
227
243
|
},
|
|
228
244
|
"body": {
|
|
229
245
|
"type": "string",
|
|
@@ -231,7 +247,7 @@
|
|
|
231
247
|
},
|
|
232
248
|
"board": {
|
|
233
249
|
"type": "string",
|
|
234
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
250
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
235
251
|
}
|
|
236
252
|
},
|
|
237
253
|
"required": [
|
|
@@ -242,7 +258,7 @@
|
|
|
242
258
|
},
|
|
243
259
|
{
|
|
244
260
|
"name": "kanban_attach",
|
|
245
|
-
"description": "Attach a file to a task by passing its bytes inline (base64). Use for genuine file artifacts the next worker or a human should be able to download
|
|
261
|
+
"description": "Attach a file to a task by passing its bytes inline (base64). Use for genuine file artifacts the next worker or a human should be able to download \u2014 generated reports, images, exports. The file is stored as a real attachment (not a comment link) under the task's attachments dir, capped at 25 MB. Prefer kanban_attach_url when you only have a URL.",
|
|
246
262
|
"inputSchema": {
|
|
247
263
|
"type": "object",
|
|
248
264
|
"properties": {
|
|
@@ -264,7 +280,7 @@
|
|
|
264
280
|
},
|
|
265
281
|
"board": {
|
|
266
282
|
"type": "string",
|
|
267
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
283
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
268
284
|
}
|
|
269
285
|
},
|
|
270
286
|
"required": [
|
|
@@ -275,7 +291,7 @@
|
|
|
275
291
|
},
|
|
276
292
|
{
|
|
277
293
|
"name": "kanban_attach_url",
|
|
278
|
-
"description": "Attach a file to a task by URL
|
|
294
|
+
"description": "Attach a file to a task by URL \u2014 Hermes downloads it server-side and stores it as a real attachment (capped at 25 MB). Use when you have a link rather than the bytes. Only http/https URLs are accepted.",
|
|
279
295
|
"inputSchema": {
|
|
280
296
|
"type": "object",
|
|
281
297
|
"properties": {
|
|
@@ -297,7 +313,7 @@
|
|
|
297
313
|
},
|
|
298
314
|
"board": {
|
|
299
315
|
"type": "string",
|
|
300
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
316
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
301
317
|
}
|
|
302
318
|
},
|
|
303
319
|
"required": [
|
|
@@ -317,7 +333,7 @@
|
|
|
317
333
|
},
|
|
318
334
|
"board": {
|
|
319
335
|
"type": "string",
|
|
320
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
336
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
321
337
|
}
|
|
322
338
|
},
|
|
323
339
|
"required": []
|
|
@@ -325,7 +341,7 @@
|
|
|
325
341
|
},
|
|
326
342
|
{
|
|
327
343
|
"name": "kanban_create",
|
|
328
|
-
"description": "Create a new kanban task, optionally as a child of the current one (pass the current task id in ``parents``). Used by orchestrator workers to fan out
|
|
344
|
+
"description": "Create a new kanban task, optionally as a child of the current one (pass the current task id in ``parents``). Used by orchestrator workers to fan out \u2014 decompose work into child tasks with specific assignees, link them into a pipeline, then complete your own task. The dispatcher picks up the new tasks on its next tick and spawns the assigned profiles.",
|
|
329
345
|
"inputSchema": {
|
|
330
346
|
"type": "object",
|
|
331
347
|
"properties": {
|
|
@@ -335,7 +351,7 @@
|
|
|
335
351
|
},
|
|
336
352
|
"assignee": {
|
|
337
353
|
"type": "string",
|
|
338
|
-
"description": "Profile name that should execute this task (e.g. 'researcher-a', 'reviewer', 'writer'). Required
|
|
354
|
+
"description": "Profile name that should execute this task (e.g. 'researcher-a', 'reviewer', 'writer'). Required \u2014 tasks without an assignee are never dispatched."
|
|
339
355
|
},
|
|
340
356
|
"body": {
|
|
341
357
|
"type": "string",
|
|
@@ -375,7 +391,7 @@
|
|
|
375
391
|
},
|
|
376
392
|
"triage": {
|
|
377
393
|
"type": "boolean",
|
|
378
|
-
"description": "If true, task lands in 'triage' instead of 'todo'
|
|
394
|
+
"description": "If true, task lands in 'triage' instead of 'todo' \u2014 a specifier profile is expected to flesh out the body before work starts."
|
|
379
395
|
},
|
|
380
396
|
"idempotency_key": {
|
|
381
397
|
"type": "string",
|
|
@@ -398,7 +414,7 @@
|
|
|
398
414
|
"items": {
|
|
399
415
|
"type": "string"
|
|
400
416
|
},
|
|
401
|
-
"description": "Skill names to force-load into the dispatched worker. The kanban lifecycle is already injected automatically; use this to pin a task to a specialist context
|
|
417
|
+
"description": "Skill names to force-load into the dispatched worker. The kanban lifecycle is already injected automatically; use this to pin a task to a specialist context \u2014 e.g. ['translation'] for a translation task, ['github-code-review'] for a reviewer task. The names must match skills installed on the assignee's profile."
|
|
402
418
|
},
|
|
403
419
|
"goal_mode": {
|
|
404
420
|
"type": "boolean",
|
|
@@ -414,11 +430,11 @@
|
|
|
414
430
|
},
|
|
415
431
|
"provider": {
|
|
416
432
|
"type": "string",
|
|
417
|
-
"description": "Provider the 'model' belongs to (e.g. 'openrouter', 'anthropic', 'nous'). Set this whenever the model is not from the assignee profile's configured provider
|
|
433
|
+
"description": "Provider the 'model' belongs to (e.g. 'openrouter', 'anthropic', 'nous'). Set this whenever the model is not from the assignee profile's configured provider \u2014 a model name alone is resolved against the profile's provider and will fail if it belongs to a different one. Requires 'model'."
|
|
418
434
|
},
|
|
419
435
|
"board": {
|
|
420
436
|
"type": "string",
|
|
421
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
437
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
422
438
|
}
|
|
423
439
|
},
|
|
424
440
|
"required": [
|
|
@@ -429,7 +445,7 @@
|
|
|
429
445
|
},
|
|
430
446
|
{
|
|
431
447
|
"name": "kanban_unblock",
|
|
432
|
-
"description": "Unblock a Kanban task. It moves to ready when all parents are done, or todo while any parent remains open. Orchestrator-only
|
|
448
|
+
"description": "Unblock a Kanban task. It moves to ready when all parents are done, or todo while any parent remains open. Orchestrator-only \u2014 only profiles with the kanban toolset can unblock routed work; dispatcher-spawned task workers never see this tool.",
|
|
433
449
|
"inputSchema": {
|
|
434
450
|
"type": "object",
|
|
435
451
|
"properties": {
|
|
@@ -439,7 +455,7 @@
|
|
|
439
455
|
},
|
|
440
456
|
"board": {
|
|
441
457
|
"type": "string",
|
|
442
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
458
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
443
459
|
}
|
|
444
460
|
},
|
|
445
461
|
"required": [
|
|
@@ -449,7 +465,7 @@
|
|
|
449
465
|
},
|
|
450
466
|
{
|
|
451
467
|
"name": "kanban_link",
|
|
452
|
-
"description": "Add a parent
|
|
468
|
+
"description": "Add a parent\u2192child dependency edge after both tasks already exist. The child won't promote to 'ready' until all parents are 'done'. Cycles and self-links are rejected.",
|
|
453
469
|
"inputSchema": {
|
|
454
470
|
"type": "object",
|
|
455
471
|
"properties": {
|
|
@@ -463,7 +479,7 @@
|
|
|
463
479
|
},
|
|
464
480
|
"board": {
|
|
465
481
|
"type": "string",
|
|
466
|
-
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env
|
|
482
|
+
"description": "Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env \u2192 HERMES_KANBAN_BOARD env \u2192 the 'current' symlink under the kanban home \u2192 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call."
|
|
467
483
|
}
|
|
468
484
|
},
|
|
469
485
|
"required": [
|
|
@@ -501,6 +517,18 @@
|
|
|
501
517
|
},
|
|
502
518
|
"evidence": {
|
|
503
519
|
"type": "string"
|
|
520
|
+
},
|
|
521
|
+
"commit": {
|
|
522
|
+
"type": "string",
|
|
523
|
+
"description": "Exact commit deployed by the CI run."
|
|
524
|
+
},
|
|
525
|
+
"ci_run": {
|
|
526
|
+
"type": "integer",
|
|
527
|
+
"description": "GitHub Actions deployment run id for that exact commit."
|
|
528
|
+
},
|
|
529
|
+
"deployed_sha": {
|
|
530
|
+
"type": "string",
|
|
531
|
+
"description": "The deployed commit the independent reviewer actually inspected."
|
|
504
532
|
}
|
|
505
533
|
},
|
|
506
534
|
"required": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.131",
|
|
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": {
|