@volter/supercode-orchestrator 0.5.205 → 0.5.207
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/README.md +138 -4
- package/activation.mjs +93 -2
- package/adapters/rh2.mjs +7 -2
- package/agents.mjs +24 -2
- package/bin/agent-channel.mjs +41 -22
- package/bin/connection-runner.mjs +141 -0
- package/bin/orchestrator.mjs +11 -2
- package/bin/runtime.mjs +16 -9
- package/board/args.mjs +3 -2
- package/board/attachment-read.mjs +77 -0
- package/board/call.mjs +4 -2
- package/board/capacity.mjs +46 -9
- package/board/ci.mjs +157 -53
- package/board/cli.mjs +278 -97
- package/board/client.mjs +18 -2
- package/board/custody.mjs +109 -0
- package/board/dispatch.mjs +305 -80
- package/board/doors.mjs +47 -17
- package/board/engine.mjs +92 -27
- package/board/files.mjs +17 -3
- package/board/fleet-listeners.mjs +268 -0
- package/board/fleet-observer.mjs +79 -0
- package/board/fleet-targets.mjs +82 -0
- package/board/incidents.mjs +7 -0
- package/board/listing.mjs +8 -2
- package/board/machine-fleet.mjs +7 -0
- package/board/notice-receipts.mjs +67 -0
- package/board/notices.mjs +5 -0
- package/board/owner-door-worker.mjs +61 -0
- package/board/owner-door.mjs +46 -0
- package/board/owner-health.mjs +8 -2
- package/board/published.mjs +2 -2
- package/board/record-view.mjs +2 -0
- package/board/store.mjs +84 -3
- package/board/tools.mjs +11 -4
- package/board/workflow.mjs +96 -31
- package/board/ztrack.mjs +207 -32
- package/board-input.d.ts +2 -0
- package/board-input.mjs +16 -0
- package/connection.mjs +153 -0
- package/index.d.ts +7 -5
- package/index.mjs +1 -1
- package/installed-apps.mjs +31 -20
- package/loop.mjs +91 -36
- package/mcp/bridge.mjs +12 -0
- package/mcp/hermes-kanban-tools.json +22 -14
- package/mcp/mount.mjs +10 -9
- package/ontology.d.ts +37 -0
- package/operator.mjs +72 -4
- package/orchestration.mjs +7 -2
- package/package.json +9 -5
- package/reducer.mjs +74 -5
package/board/workflow.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// the engine's primitives (the rows Hermes's tables hold); the I/O an action asks for (stopping a worker, closing a
|
|
6
6
|
// pane, sending a session a message, cleaning a workspace) is answered as effects the caller runs after commit.
|
|
7
7
|
import { current } from './call.mjs';
|
|
8
|
+
import { detachedCard, detachedSession } from './custody.mjs';
|
|
8
9
|
import { notRuntime } from './owner-files.mjs';
|
|
9
10
|
import { machineCall } from './doors.mjs';
|
|
10
11
|
import { execFile } from 'node:child_process';
|
|
@@ -17,9 +18,24 @@ import { releaseAllSideSessions } from './side-sessions.mjs';
|
|
|
17
18
|
import { evaluate, holds, render, needsSources } from './expr.mjs';
|
|
18
19
|
import { completionEvidence, declaredWorkBranches, namedWorkBranch, reviewCandidate } from './ci.mjs';
|
|
19
20
|
import { cardCommitsOn, isLocalWorkspace, isWorkspacePath } from './workspace.mjs';
|
|
20
|
-
import { identifyNotice } from './notices.mjs';
|
|
21
|
+
import { cardNumber, identifyNotice, numbered } from './notices.mjs';
|
|
21
22
|
|
|
22
23
|
const loaded = new Map();
|
|
24
|
+
// the owner's read door holds the workflow the owner reads (owner-door.mjs), told when it is read again
|
|
25
|
+
const workflowWatchers = new Set();
|
|
26
|
+
const hold = (root, entry) => {
|
|
27
|
+
const again = loaded.get(root)?.orchestration !== entry.orchestration;
|
|
28
|
+
loaded.set(root, entry);
|
|
29
|
+
if (again) for (const watcher of workflowWatchers) watcher(root, entry);
|
|
30
|
+
};
|
|
31
|
+
/** `watcher(root, entry)` is told each workflow this process holds, now and whenever one is read again; answers its stop. */
|
|
32
|
+
export function watchWorkflows(watcher) {
|
|
33
|
+
workflowWatchers.add(watcher);
|
|
34
|
+
for (const [root, entry] of loaded) watcher(root, entry);
|
|
35
|
+
return () => workflowWatchers.delete(watcher);
|
|
36
|
+
}
|
|
37
|
+
/** Hold the workflow another thread of this process read for `root` (the owner's, on its read door). */
|
|
38
|
+
export function holdWorkflow(root, entry) { loaded.set(root, { ...entry, root }); }
|
|
23
39
|
const homes = new Map(); // root → Promise<orchestration>: the home as the IR reads it, held until it changes
|
|
24
40
|
|
|
25
41
|
/**
|
|
@@ -49,15 +65,15 @@ export async function loadWorkflow(root) {
|
|
|
49
65
|
entry = workflowEntry(orchestration);
|
|
50
66
|
entry.root = root;
|
|
51
67
|
entry.orchestration = orchestration;
|
|
52
|
-
|
|
68
|
+
hold(root, entry);
|
|
53
69
|
}
|
|
54
70
|
current().workflows.set(root, entry);
|
|
55
71
|
return entry;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
// What the board's view of a home is decoded from: the workflow, each profile's config, credentials and the agent
|
|
59
|
-
// layer, and which profiles there are. A session store or a run log changing is no change to it.
|
|
60
|
-
const DECODED = /^(?:(?:profiles\/[^/]+\/)?(?:config\.yaml|\.env|agents\.json)|workflow\.yaml|profiles(?:\/[^/]+)?)$/;
|
|
75
|
+
// layer, persona, and which profiles there are. A session store or a run log changing is no change to it.
|
|
76
|
+
const DECODED = /^(?:(?:profiles\/[^/]+\/)?(?:config\.yaml|\.env|agents\.json|AGENTS\.md)|workflow\.yaml|profiles(?:\/[^/]+)?)$/;
|
|
61
77
|
|
|
62
78
|
/**
|
|
63
79
|
* Follow the home's own files, so the held view is read again on its next use after one it is decoded from changes:
|
|
@@ -75,7 +91,7 @@ export function followHome(root, { onChange = () => {}, onLost = () => {} } = {}
|
|
|
75
91
|
const linked = new Map(); // target folder → watcher
|
|
76
92
|
const followLinks = () => {
|
|
77
93
|
const targets = new Map();
|
|
78
|
-
const decoded = ['workflow.yaml', 'config.yaml', '.env', 'agents.json'];
|
|
94
|
+
const decoded = ['workflow.yaml', 'config.yaml', '.env', 'agents.json', 'AGENTS.md'];
|
|
79
95
|
const watchIn = (folder, name) => { if (!targets.has(folder)) targets.set(folder, new Set()); targets.get(folder).add(name); };
|
|
80
96
|
let realRoot = root;
|
|
81
97
|
try { realRoot = realpathSync(root); } catch { /* the home's own watch reports it */ }
|
|
@@ -143,7 +159,7 @@ export function workflowFor(root) {
|
|
|
143
159
|
/** Use `entry` for `root` (a caller that read the IR itself). */
|
|
144
160
|
export function useWorkflow(root, entry) {
|
|
145
161
|
entry.root = root;
|
|
146
|
-
|
|
162
|
+
hold(root, entry);
|
|
147
163
|
current().workflows.set(root, entry);
|
|
148
164
|
return entry;
|
|
149
165
|
}
|
|
@@ -170,6 +186,11 @@ export function completionOwnerOf(db, id) {
|
|
|
170
186
|
return creatorOf(db, id);
|
|
171
187
|
}
|
|
172
188
|
|
|
189
|
+
/** A card's explicitly named review profile, retained in its own history. */
|
|
190
|
+
export function reviewerOf(db, id) {
|
|
191
|
+
return json(one(db, "SELECT payload FROM task_events WHERE task_id=? AND kind IN ('created','reviewer_set') ORDER BY id DESC LIMIT 1", id)?.payload, {})?.reviewer ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
173
194
|
export function latestReview(db, id) {
|
|
174
195
|
const row = one(db, "SELECT id,started_at,metadata FROM task_runs WHERE task_id=? AND json_extract(metadata,'$.supercode.role')='reviewer' ORDER BY id DESC LIMIT 1", id);
|
|
175
196
|
if (!row) return null;
|
|
@@ -363,7 +384,8 @@ export function runWorkingSeconds(row, at = now()) {
|
|
|
363
384
|
return Math.max(0, end - Number(row.started_at ?? end) - Number(json(row.metadata,{})?.supercode?.stopped_seconds??0));
|
|
364
385
|
}
|
|
365
386
|
|
|
366
|
-
|
|
387
|
+
/** Elapsed role totals need no review candidate or workflow guard scope. */
|
|
388
|
+
export function timeOf(db, task) {
|
|
367
389
|
const t = now();
|
|
368
390
|
const out = { coding: 0, reviewing: 0, auditing: 0, triaging: 0 };
|
|
369
391
|
const key = { implementer: 'coding', reviewer: 'reviewing', auditor: 'auditing', triager: 'triaging' };
|
|
@@ -394,6 +416,14 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {}, lazy
|
|
|
394
416
|
const lastRun = once(() => one(db, 'SELECT * FROM task_runs WHERE task_id = ? AND ended_at IS NOT NULL ORDER BY ended_at DESC, id DESC LIMIT 1', task.id));
|
|
395
417
|
const lastCompleted = once(() => one(db, "SELECT * FROM task_runs WHERE task_id = ? AND outcome = 'completed' ORDER BY ended_at DESC LIMIT 1", task.id));
|
|
396
418
|
const asked = once(() => json(one(db, "SELECT payload FROM task_events WHERE task_id = ? AND kind = 'review_requested' ORDER BY id DESC LIMIT 1", task.id)?.payload, {}) ?? {});
|
|
419
|
+
const entry = current().workflows.get(board.root) ?? loaded.get(board.root);
|
|
420
|
+
// Read the coding role, never the latest run of any role: after a review or a
|
|
421
|
+
// reassignment that run may belong to a different harness than the coder's.
|
|
422
|
+
const coding = once(() => one(db, "SELECT id,profile,metadata FROM task_runs WHERE task_id=? AND json_extract(metadata,'$.supercode.role')='implementer' ORDER BY id DESC LIMIT 1", task.id));
|
|
423
|
+
const codingSession = once(() => json(coding()?.metadata, {})?.supercode ?? {});
|
|
424
|
+
const codingProfile = once(() => coding()?.profile ?? asked().implementer ?? task.assignee);
|
|
425
|
+
const coderProfile = once(() => entry?.profiles?.[codingProfile()]);
|
|
426
|
+
const configuredCoder = once(() => coderProfile()?.worker);
|
|
397
427
|
const changes = once(() => json(one(db, "SELECT payload FROM task_events WHERE task_id = ? AND kind = 'changes_requested' ORDER BY id DESC LIMIT 1", task.id)?.payload, {}) ?? {});
|
|
398
428
|
const interruption = once(() => json(one(db, `SELECT payload FROM task_events WHERE task_id = ? AND kind IN ('blocked', 'block_loop_detected', 'dependency_wait', 'gave_up',
|
|
399
429
|
'unblocked', 'changes_requested', 'review_reopened', 'status', 'reclaimed', 'stale', 'timed_out', 'crashed', 'spawn_failed', 'rate_limited', 'session_lost')
|
|
@@ -415,6 +445,7 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {}, lazy
|
|
|
415
445
|
status,
|
|
416
446
|
blocked_by: blocks,
|
|
417
447
|
paused: json(state.paused, null),
|
|
448
|
+
detached: json(state.detached, null),
|
|
418
449
|
archived: Boolean(state.archived),
|
|
419
450
|
every: state.every ?? null,
|
|
420
451
|
next_run_at: state.next_run_at ?? null,
|
|
@@ -436,6 +467,15 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {}, lazy
|
|
|
436
467
|
get counters() { return { failures: Number(task.consecutive_failures || 0), block_recurrences: Number(task.block_recurrences || 0), goal_turns: Number(session().goal_turns || 0) }; },
|
|
437
468
|
goal_mode: Boolean(task.goal_mode),
|
|
438
469
|
get implementer() { return asked().implementer ?? task.assignee; },
|
|
470
|
+
get implementer_harness() { return codingSession().harness ?? configuredCoder()?.harness ?? null; },
|
|
471
|
+
get implementer_model() { return codingSession().model ?? (codingProfile() === task.assignee ? task.model_override : asked().implementer_model) ??
|
|
472
|
+
configuredCoder()?.model ?? coderProfile()?.residue?.config?.model?.default ?? null; },
|
|
473
|
+
get implementer_run() { return coding() ? { id: coding().id, profile: coding().profile, harness: codingSession().harness ?? null,
|
|
474
|
+
model: codingSession().model ?? null, machine: codingSession().machine ?? null } : null; },
|
|
475
|
+
get implementer_machine() { return Object.hasOwn(asked(), 'implementer_machine') ? asked().implementer_machine : task.machine ?? null; },
|
|
476
|
+
get implementer_model_override() { return Object.hasOwn(asked(), 'implementer_model_override') ? asked().implementer_model_override : task.model_override ?? null; },
|
|
477
|
+
get implementer_provider_override() { return Object.hasOwn(asked(), 'implementer_provider_override') ? asked().implementer_provider_override : task.provider_override ?? null; },
|
|
478
|
+
get reviewer() { return reviewerOf(db, task.id); },
|
|
439
479
|
get last_reviewer() { return changes().reviewer ?? null; },
|
|
440
480
|
get arc() { return task.subtask_of ? one(db, `SELECT t.id, ${related}, t.title, t.body, t.assignee FROM tasks t LEFT JOIN supercode_cards c ON c.task_id = t.id WHERE t.id = ?`, task.subtask_of) ?? null : null; },
|
|
441
481
|
// the cards filed as its subtasks (`create --subtask-of`), in the order they were filed
|
|
@@ -485,13 +525,7 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {}, lazy
|
|
|
485
525
|
Object.defineProperty(card, key, { get: read, enumerable: true, configurable: true });
|
|
486
526
|
if (!lazy) read();
|
|
487
527
|
}
|
|
488
|
-
const
|
|
489
|
-
for (const [key, expr] of Object.entries(derived)) {
|
|
490
|
-
// Preflight neither evaluates nor exposes a derived source value from a stale/missing observation.
|
|
491
|
-
const value = deferSources && needsSources(expr, derived) ? undefined : evaluate(expr, { card, event, params, now: now() });
|
|
492
|
-
Object.defineProperty(card, key, { value, writable: true, enumerable: true, configurable: true });
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
528
|
+
const scope = {
|
|
495
529
|
card,
|
|
496
530
|
requester: actorOf(),
|
|
497
531
|
event,
|
|
@@ -499,8 +533,15 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {}, lazy
|
|
|
499
533
|
now: now(),
|
|
500
534
|
board: board.label ?? board.slug,
|
|
501
535
|
matches: (text, p) => (text === null || text === undefined ? false : pattern(p).test(String(text))),
|
|
536
|
+
lanes: entry ? lanesIn(entry) : {},
|
|
537
|
+
profile_harnesses: Object.fromEntries(Object.entries(entry?.profiles ?? {}).filter(([, profile]) => profile?.worker?.harness)
|
|
538
|
+
.map(([name, profile]) => [name, profile.worker.harness])),
|
|
502
539
|
...extra,
|
|
503
540
|
};
|
|
541
|
+
// Derived facts see the same names as transitions, including the home's
|
|
542
|
+
// lanes. The pinned entry remains the basis if the home reloads mid-call.
|
|
543
|
+
for (const [key, expr] of Object.entries(entry?.workflow?.derived ?? {})) card[key] = deferSources && needsSources(expr, entry.workflow.derived) ? undefined : evaluate(expr, scope);
|
|
544
|
+
return scope;
|
|
504
545
|
}
|
|
505
546
|
|
|
506
547
|
// ------------------------------------------------------------------ events
|
|
@@ -528,13 +569,19 @@ function noticeRun(db, task) {
|
|
|
528
569
|
: all(db, 'SELECT id, metadata FROM task_runs WHERE task_id = ? ORDER BY id DESC', task.id).find((r) => json(r.metadata, {})?.supercode?.session_id) ?? null;
|
|
529
570
|
}
|
|
530
571
|
|
|
531
|
-
function queueTransitionNotice(db, board, entry, task, from, name, payload, beforeRun, effects) {
|
|
572
|
+
function queueTransitionNotice(db, board, entry, task, from, name, payload, beforeRun, effects, noticeSession = null) {
|
|
532
573
|
const after = cardOf(db, task.id), to = statusOf(db, after);
|
|
574
|
+
// A verified adoption starts a new run before binding its session. Its notice
|
|
575
|
+
// belongs to that new run and verified identity, never the last ended session
|
|
576
|
+
// that noticeRun found before the transition.
|
|
577
|
+
const targetRun = noticeSession && after.current_run_id
|
|
578
|
+
? one(db, 'SELECT id, metadata FROM task_runs WHERE id = ?', after.current_run_id) : beforeRun;
|
|
533
579
|
const state = (t, status) => JSON.stringify([status, t.current_run_id, t.paused, t.archived, t.blocked_by]);
|
|
534
580
|
const beforeState = json(beforeRun?.metadata, {})?.supercode?.state;
|
|
535
581
|
const afterState = beforeRun && json(one(db, 'SELECT metadata FROM task_runs WHERE id=?', beforeRun.id)?.metadata, {})?.supercode?.state;
|
|
536
|
-
if (!
|
|
537
|
-
const session = json(
|
|
582
|
+
if (!targetRun || (state(task, from) === state(after, to) && beforeState === afterState) || effects.some((e) => e.kind === 'session_notice' && e.runId === targetRun.id)) return;
|
|
583
|
+
const session = noticeSession ?? json(targetRun.metadata, {})?.supercode;
|
|
584
|
+
if (detachedSession(db, session)) return;
|
|
538
585
|
const address = session?.address ?? (session?.session_id && session.machine ? `sc:${session.machine}:${session.harness ?? 'claude-code'}:${session.session_id}` : null);
|
|
539
586
|
if (!address && !session?.launch_key && !session?.pane) return;
|
|
540
587
|
// The session that made this change learns it from its own command's answer; a notice about
|
|
@@ -542,16 +589,19 @@ function queueTransitionNotice(db, board, entry, task, from, name, payload, befo
|
|
|
542
589
|
// confirmed is not told either: its next run resumes it with the card's state in its prompt.
|
|
543
590
|
if (address && address === actorOf()) return;
|
|
544
591
|
if (session?.close_confirmed_at) return;
|
|
545
|
-
const scope = scopeOf(db, board, after, { params: entry.params, event: { ...payload, verb: name, from, to }, extra: { run: { id:
|
|
546
|
-
const
|
|
592
|
+
const scope = scopeOf(db, board, after, { params: entry.params, event: { ...payload, verb: name, from, to }, extra: { run: { id: targetRun.id } } });
|
|
593
|
+
const defaultNotice = noticeSession
|
|
594
|
+
? '{{card.id}}: verified adoption started run {{run.id}} ({{event.from}} → {{event.to}}). Follow the board’s current state before continuing.'
|
|
595
|
+
: '{{card.id}}: {{event.verb}} changed run {{run.id}} ({{event.from}} → {{event.to}}). Stop work under the previous run; follow the board’s current state. {{event.reason ?? ""}}';
|
|
596
|
+
const body = numbered(cardNumber(db, task.id), render(entry.workflow.notify?.worker?.run_changed ?? defaultNotice, scope));
|
|
547
597
|
const source = one(db, 'SELECT MAX(id) AS id FROM task_events WHERE task_id = ?', task.id)?.id;
|
|
548
|
-
const text = identifyNotice({ id: source, kind: name, run_id:
|
|
549
|
-
const effect = { kind: 'session_notice', id: task.id, runId:
|
|
598
|
+
const text = identifyNotice({ id: source, kind: name, run_id: targetRun.id, created_at: now() }, body);
|
|
599
|
+
const effect = { kind: 'session_notice', id: task.id, runId: targetRun.id, address, text, sourceEvent: source, verb: name, messageKey: `board-run-notice-${randomBytes(16).toString('hex')}` };
|
|
550
600
|
const inserted = run(db, 'INSERT INTO supercode_effects (effect) VALUES (?)', JSON.stringify(effect));
|
|
551
601
|
effects.push({ ...effect, durableId: Number(inserted.lastInsertRowid) });
|
|
552
|
-
const current = json(one(db, 'SELECT metadata FROM task_runs WHERE id = ?',
|
|
602
|
+
const current = json(one(db, 'SELECT metadata FROM task_runs WHERE id = ?', targetRun.id)?.metadata, {}) ?? {};
|
|
553
603
|
current.supercode = { ...current.supercode, lifecycle_notice_pending: Number(inserted.lastInsertRowid) };
|
|
554
|
-
run(db, 'UPDATE task_runs SET metadata = ? WHERE id = ?', JSON.stringify(current),
|
|
604
|
+
run(db, 'UPDATE task_runs SET metadata = ? WHERE id = ?', JSON.stringify(current), targetRun.id);
|
|
555
605
|
}
|
|
556
606
|
|
|
557
607
|
/**
|
|
@@ -644,7 +694,7 @@ export function validateEvent(db, board, entry, id, name, { roles = [], payload
|
|
|
644
694
|
return { task, from, taken, scope, beforeRun: localOnly ? null : noticeRun(db, task) };
|
|
645
695
|
}
|
|
646
696
|
|
|
647
|
-
export function fire(db, board, entry, id, name, { roles = [], payload = {}, extra = {} } = {}) {
|
|
697
|
+
export function fire(db, board, entry, id, name, { roles = [], payload = {}, extra = {}, noticeSession = null } = {}) {
|
|
648
698
|
const checked = validateEvent(db, board, entry, id, name, { roles, payload, extra });
|
|
649
699
|
if (!checked) return fireRun(db, board, entry, id, name, { roles, payload, extra });
|
|
650
700
|
const { task, from, taken, scope, beforeRun } = checked;
|
|
@@ -658,8 +708,10 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
|
|
|
658
708
|
const ended = task.current_run_id && one(db, 'SELECT ended_at FROM task_runs WHERE id = ?', task.current_run_id)?.ended_at != null ? task.current_run_id : null;
|
|
659
709
|
if ((scope.card.dispatch_hold || ended) && roles.some(role => ['manager','creator'].includes(role)) &&
|
|
660
710
|
(['resume','reclaim','replace_session','reopen','reopen_review'].includes(name) || (name === 'set' && ['assignee','machine','model'].includes(payload.field))))
|
|
661
|
-
record(db,id,'dispatch_released',{reason:`explicit ${name}`,incident:scope.card.last_events.run_incident?.payload?.run ?? null,...(ended ? { ended_run: ended } : {})
|
|
662
|
-
|
|
711
|
+
record(db,id,'dispatch_released',{reason:`explicit ${name}`,incident:scope.card.last_events.run_incident?.payload?.run ?? null,...(ended ? { ended_run: ended } : {}),
|
|
712
|
+
...(ended && name === 'set' && payload.field === 'assignee' && (payload.value ?? null) !== (task.assignee ?? null)
|
|
713
|
+
? { reassigned_from: task.assignee ?? null, reassigned_to: payload.value ?? null, abandon_waiting_input: true } : {})});
|
|
714
|
+
queueTransitionNotice(db, board, entry, task, from, name, payload, beforeRun, effects, noticeSession);
|
|
663
715
|
return { from, to: statusOf(db, cardOf(db, id)), moved: to !== from, effects };
|
|
664
716
|
}
|
|
665
717
|
|
|
@@ -690,7 +742,7 @@ function cardOf(db, id) {
|
|
|
690
742
|
const row = one(db, 'SELECT * FROM tasks WHERE id = ?', id);
|
|
691
743
|
if (!row) throw new Error(`no card ${id}`);
|
|
692
744
|
const ours = one(db, 'SELECT * FROM supercode_cards WHERE task_id = ?', id);
|
|
693
|
-
return { ...row, blocked_by: json(ours?.blocked_by, []) ?? [], paused: json(ours?.paused, null), archived: Boolean(ours?.archived), skills: json(row.skills, []) ?? [], machine: ours?.machine ?? null, subtask_of: ours?.subtask_of ?? null };
|
|
745
|
+
return { ...row, blocked_by: json(ours?.blocked_by, []) ?? [], paused: json(ours?.paused, null), detached: json(ours?.detached, null), archived: Boolean(ours?.archived), skills: json(row.skills, []) ?? [], machine: ours?.machine ?? null, subtask_of: ours?.subtask_of ?? null };
|
|
694
746
|
}
|
|
695
747
|
|
|
696
748
|
// one transition: the old status's exit actions, the move, the transition's actions, the new status's entry actions
|
|
@@ -724,6 +776,7 @@ export function setStatus(db, workflow, id, status) {
|
|
|
724
776
|
/** Take every `always` transition that holds, from wherever the card is, until none does. */
|
|
725
777
|
export function settle(db, board, entry, id, effects = []) {
|
|
726
778
|
const { workflow, params } = entry;
|
|
779
|
+
if (detachedCard(db, id)) return effects;
|
|
727
780
|
settleRelations(db, board, entry, id, effects);
|
|
728
781
|
for (let hops = 0; hops < 16; hops += 1) {
|
|
729
782
|
const task = cardOf(db, id);
|
|
@@ -772,12 +825,14 @@ function dueAfter(db, workflow, task, status, scope) {
|
|
|
772
825
|
|
|
773
826
|
/**
|
|
774
827
|
* Where a new card starts: the first `initial` transition that holds (`event` is the create's own words), its status
|
|
775
|
-
* set and its actions run (a goal card by the workflow's default, for one). Answers the status.
|
|
828
|
+
* set and its actions run (a goal card by the workflow's default, for one). Answers the status. A validator that fails
|
|
829
|
+
* refuses the create, as it refuses any other event (a home that keeps its root unassigned says so here).
|
|
776
830
|
*/
|
|
777
831
|
export function startNew(db, board, entry, task, event) {
|
|
778
832
|
const scope = scopeOf(db, board, task, { params: entry.params, event, extra: { lanes: lanesIn(entry) } });
|
|
779
833
|
const t = entry.workflow.initial.find((x) => holds(x.guard ?? null, scope));
|
|
780
834
|
if (!t?.target) throw new Error('the workflow says nowhere for a new card to start');
|
|
835
|
+
for (const v of t.validate ?? []) if (!holds(v.expr, scope)) throw new Error(render(v.message, scope));
|
|
781
836
|
const status = String(templateValue(t.target, scope));
|
|
782
837
|
if (!entry.workflow.statuses[status]) throw new Error(`the workflow starts a new card in ${status}, which it does not declare`);
|
|
783
838
|
setStatus(db, entry.workflow, task.id, status);
|
|
@@ -813,7 +868,9 @@ const FORMAT = {
|
|
|
813
868
|
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 }),
|
|
814
869
|
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 }),
|
|
815
870
|
dependency_wait: ({ e, source }) => ({ reason: e.reason ?? null, on: e.on ?? null, source_status: source }),
|
|
816
|
-
review_requested: ({ e, before, after }) => ({ summary: firstLine(e.summary), ci: e.ci ?? null, pr: e.ci?.pr ?? null, commit: e.ci?.commit ?? null,
|
|
871
|
+
review_requested: ({ e, before, after }) => ({ summary: firstLine(e.summary), ci: e.ci ?? null, pr: e.ci?.pr ?? null, commit: e.ci?.commit ?? null,
|
|
872
|
+
implementer: before.assignee, implementer_machine: before.machine ?? null, implementer_model: before.implementer_model ?? null,
|
|
873
|
+
implementer_model_override: before.model_override ?? null, implementer_provider_override: before.provider_override ?? null, reviewer: after.assignee }),
|
|
817
874
|
// the reviewer is the lane that reviewed (the card's assignee while in review), unless the verdict names one
|
|
818
875
|
changes_requested: ({ e, before, after }) => ({ reason: e.reason ?? null, reviewer: e.reviewer ?? before.assignee ?? null, implementer: after.assignee }),
|
|
819
876
|
review_reopened: ({ after, to }) => ({ status: to, implementer: after.assignee }),
|
|
@@ -922,9 +979,15 @@ const ACTIONS = {
|
|
|
922
979
|
} else record(ctx.db, on ?? ctx.id, kind, payload && Object.keys(payload).length ? payload : null, on ? null : ctx.runId);
|
|
923
980
|
},
|
|
924
981
|
assign(ctx, { field, value }) {
|
|
982
|
+
if (field === 'reviewer') {
|
|
983
|
+
if (value != null && (typeof value !== 'string' || !ctx.entry.profiles?.[value]?.worker?.harness)) throw new Error('A card reviewer names a runnable profile in this home.');
|
|
984
|
+
record(ctx.db, ctx.id, 'reviewer_set', { reviewer: value ?? null }, ctx.runId);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
925
987
|
if (field === 'machine') {
|
|
926
988
|
const task = one(ctx.db, 'SELECT t.current_run_id,t.workspace_kind,c.workspace_anchor,c.machine FROM tasks t JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?', ctx.id);
|
|
927
989
|
if (task.current_run_id) throw new Error('End the current run before moving its card.');
|
|
990
|
+
if ((value ?? null) === task.machine) return;
|
|
928
991
|
if (task.workspace_kind !== 'scratch' && (!task.workspace_anchor || isWorkspacePath(task.workspace_anchor))) throw new Error('declare the repository and branch with workflow specify --workspace before moving a legacy path card');
|
|
929
992
|
if ((value ?? null) !== task.machine) run(ctx.db, 'UPDATE tasks SET workspace_path=NULL WHERE id=?', ctx.id);
|
|
930
993
|
cardField(ctx, 'machine', value ?? null);
|
|
@@ -984,6 +1047,7 @@ const ACTIONS = {
|
|
|
984
1047
|
run(ctx.db, 'UPDATE tasks SET current_run_id = NULL, claim_lock = NULL, claim_expires = NULL, worker_pid = NULL WHERE id = ?', ctx.id);
|
|
985
1048
|
},
|
|
986
1049
|
claim(ctx, role, scope) {
|
|
1050
|
+
if (detachedCard(ctx.db, ctx.id)) throw new Error('a detached card must receive a verified adoption before starting another run');
|
|
987
1051
|
const task = cardOf(ctx.db, ctx.id);
|
|
988
1052
|
const t = now();
|
|
989
1053
|
const ttl = Number(ctx.entry.params.claim_ttl_seconds) || 900;
|
|
@@ -1124,7 +1188,7 @@ const ACTIONS = {
|
|
|
1124
1188
|
send_to_actor(ctx, text) {
|
|
1125
1189
|
const effect = { kind: 'send_to_actor', id: ctx.id, runId: ctx.runId, text, messageKey: `board-actor-${randomBytes(16).toString('hex')}` };
|
|
1126
1190
|
const inserted = run(ctx.db, 'INSERT INTO supercode_effects (effect) VALUES (?)', JSON.stringify(effect));
|
|
1127
|
-
effect.text = identifyNotice({ id: `notice-${inserted.lastInsertRowid}`, kind: 'actor_notice', run_id: ctx.runId, created_at: now() }, text);
|
|
1191
|
+
effect.text = identifyNotice({ id: `notice-${inserted.lastInsertRowid}`, kind: 'actor_notice', run_id: ctx.runId, created_at: now() }, numbered(cardNumber(ctx.db, ctx.id), text));
|
|
1128
1192
|
run(ctx.db, 'UPDATE supercode_effects SET effect=? WHERE id=?', JSON.stringify(effect), Number(inserted.lastInsertRowid));
|
|
1129
1193
|
ctx.effects.push({ ...effect, durableId: Number(inserted.lastInsertRowid) });
|
|
1130
1194
|
},
|
|
@@ -1139,7 +1203,7 @@ export function retireSupersededRuns(db, id, runId, session) {
|
|
|
1139
1203
|
OR (? IS NOT NULL AND json_extract(metadata, '$.supercode.pane') = ?))`,
|
|
1140
1204
|
runId, session.machine ?? null, session.session_id ?? null, session.session_id ?? null, session.pane ?? null, session.pane ?? null)) {
|
|
1141
1205
|
const whole = json(old.metadata, {}) ?? {}, s = whole.supercode;
|
|
1142
|
-
if (!s || s.machine !== session.machine || !((session.session_id && s.session_id === session.session_id) || (session.pane && s.pane === session.pane))) continue;
|
|
1206
|
+
if (!s || detachedSession(db, s) || s.machine !== session.machine || !((session.session_id && s.session_id === session.session_id) || (session.pane && s.pane === session.pane))) continue;
|
|
1143
1207
|
if (old.task_id !== id && one(db, 'SELECT 1 FROM tasks WHERE current_run_id = ?', old.id)) throw new Error(`session is still owned by active run ${old.id} of ${old.task_id}`);
|
|
1144
1208
|
if (s.pane && s.pane === session.pane) whole.supercode = { ...s, close_transferred_to: runId, close_requested: null };
|
|
1145
1209
|
run(db, "UPDATE task_runs SET status='superseded', outcome='superseded', ended_at=?, claim_lock=NULL, claim_expires=NULL, worker_pid=NULL, metadata=? WHERE id=?", now(), JSON.stringify(whole), old.id);
|
|
@@ -1159,6 +1223,7 @@ export function requestSessionClose(db, id, { runId = null, slot = null, through
|
|
|
1159
1223
|
if ((runId != null && row.id !== runId) || (through != null && row.id > through)) continue;
|
|
1160
1224
|
const whole = json(row.metadata, {}) ?? {};
|
|
1161
1225
|
const session = whole.supercode;
|
|
1226
|
+
if (detachedSession(db, session)) continue;
|
|
1162
1227
|
if ((!session?.pane && !session?.launch_key && !(session?.surface === 'native' && session.session_id)) || session.close_transferred_to || session.close_confirmed_at || (slot != null && (session.slot ?? 'worker') !== slot)) continue;
|
|
1163
1228
|
if (session.close_requested && !session.closed_at) continue;
|
|
1164
1229
|
whole.supercode = { ...session, close_requested: session.close_requested ?? now() };
|