@volter/supercode-orchestrator 0.4.160 → 0.5.2

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.
@@ -15,7 +15,14 @@
15
15
  // Every side effect follows a committed transition: a card is claimed before its session is launched, and a
16
16
  // subscriber's cursor moves only after its notice was delivered, so two dispatchers over one board never both
17
17
  // launch and a crash between the two steps is answered by the next tick.
18
+ import { monitorsRuns, failedObservation, surfaceIncident, auditEndedRuns } from './incidents.mjs';
19
+ import { readEventFleet } from './event-fleet.mjs';
20
+ let eventFleetRoot = null;
18
21
  import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
22
+ import { runtimeDoor } from './runtime-door.mjs';
23
+ import { agentDoor, agentScope, teamsUnavailable, ensureCardAgent, localCardAgent, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, pendingAgentRuns, synchronizeAgentRun,agentProfileMismatch,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds } from '../agents.mjs';
24
+ import { materializeAgentHome } from '../agent-home.mjs';
25
+ import { memorySnapshotSync } from '../hermes-cli.mjs';
19
26
  import { createRequire } from 'node:module';
20
27
  import { spawn as spawnChild, spawnSync } from 'node:child_process';
21
28
  import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
@@ -24,7 +31,7 @@ import { basename, dirname, join } from 'node:path';
24
31
  import { createHash, randomUUID } from 'node:crypto';
25
32
  import { fileURLToPath } from 'node:url';
26
33
  import { view, boardCopyOf, boardPath, claimer, copyRefusal, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
27
- import { doors, fire, loadWorkflow, resumeOf, runSpec, runWorkingSeconds, requestSessionClose, retireSupersededRuns, refreshCardInputs, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, storedStatus, templateValue, workflowFor } from './workflow.mjs';
34
+ import { doors, fire, loadWorkflow, resumeOf, runSpec, runWorkingSeconds, requestSessionClose, retireSupersededRuns, refreshCardInputs, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, projectStatus, storedStatus, templateValue, workflowFor } from './workflow.mjs';
28
35
  import { checkWorkspace, assertWorkspaceOwner, notePresence, resolveWorkspace } from './workspace.mjs';
29
36
  import { capacity, harnessDefaults, holdings } from './capacity.mjs';
30
37
  import { evaluate, holds, render } from './expr.mjs';
@@ -46,34 +53,26 @@ export function normalMachine(name) {
46
53
  }
47
54
 
48
55
  /** This machine's name as supercode's addresses spell it: its Teams enrollment, else its host name. */
56
+ let localMachineName;
49
57
  export function localMachine() {
50
- const home = process.env.SUPERCODE_TEAMS_HOME || join(process.env.SUPERCODE_HOME || (process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'supercode') : join(process.env.HOME ?? '', '.config', 'supercode')), 'teams');
58
+ if (localMachineName) return localMachineName;
51
59
  try {
52
- const contexts = JSON.parse(readFileSync(join(home, 'workspaces', 'contexts.json'), 'utf8'));
53
- const ctx = contexts.contexts?.[contexts.current];
54
- const enrollment = JSON.parse(readFileSync(join(home, 'workspaces', 'connectors', ctx.server_id, ctx.team_id, 'enrollment.json'), 'utf8'));
55
- if (enrollment?.machine?.name) return normalMachine(enrollment.machine.name);
56
- } catch { /* not enrolled: the host name */ }
57
- return normalMachine(hostname());
60
+ const reply=runtimeDoor(null,'harness.v1.machine.describe',{}, {timeout:15_000});
61
+ if(reply.code===0){const machine=JSON.parse(reply.stdout);return localMachineName=normalMachine(machine.name??machine.hostname);}
62
+ } catch { /* no local daemon: a plain host still has its OS machine name */ }
63
+ return localMachineName=normalMachine(hostname());
58
64
  }
59
65
 
60
66
  /** The supercode command the dispatcher drives. */
61
67
  export const supercodeBin = () => process.env.SUPERCODE_BIN || 'supercode';
62
68
 
63
- // Installed board workers use their sibling Teams package, not a CLI build's retained checkout.
64
- // An explicit operator override remains authoritative for source development.
65
- let teamsEntry;
69
+ /** Public CLI calls carry caller attribution; installed clients own transport selection. */
66
70
  export function commandEnv(as = null) {
67
- if (teamsEntry === undefined) {
68
- try { teamsEntry = createRequire(import.meta.url).resolve('@volter/supercode-teams/bin'); }
69
- catch { teamsEntry = null; }
70
- }
71
- return { ...process.env, ...(!process.env.SUPERCODE_TEAMS_ENTRY && teamsEntry ? { SUPERCODE_TEAMS_ENTRY: teamsEntry } : {}),
72
- ...(as ? { SUPERCODE_CALLER: as } : {}) };
71
+ return { ...process.env, ...(as ? { SUPERCODE_CALLER: as } : {}) };
73
72
  }
74
73
 
75
74
  function sc(args, { input = null, timeout = 120_000, as = null } = {}) {
76
- // a neutral directory: `supercode teams …` resolves its entry from a checkout at the cwd. `as`: the board this
75
+ // `as`: the board this
77
76
  // command acts for, so a card's pane is started and closed by its board, whichever session ran the board's verb
78
77
  const env = commandEnv(as);
79
78
  const r = spawnSync(supercodeBin(), args, { cwd: tmpdir(), input: input ?? undefined, encoding: 'utf8', timeout, env, maxBuffer: 512 * 1024 * 1024 });
@@ -116,19 +115,11 @@ export function callerSession() {
116
115
  return claude;
117
116
  }
118
117
 
119
- export function observeFleet() {
120
- const list = sc(['message', 'list', '--json']);
121
- const sessions = list.code === 0 ? json(list.stdout.trim(), null) : null;
122
- if (!Array.isArray(sessions)) return { ok: false, alive: new Set(), online: new Set(), error: `message list: ${(list.stderr || list.stdout).trim().slice(-300)}` };
123
- const machines = sc(['teams', 'machines', 'events', '--json']);
124
- const presence = machines.code === 0 ? lastJson(machines.stdout) : null;
125
- const online = new Set([localMachine()]);
126
- for (const m of presence?.items ?? []) if (m.linked) online.add(normalMachine(m.name));
127
- presenceReading = { known: presence?.connected === true, online };
118
+ export function observeFleet(root = eventFleetRoot) {
119
+ const fleet = readEventFleet(root);
120
+ presenceReading = { known: fleet.presenceKnown === true, online: new Set([localMachine(), ...fleet.online]) };
128
121
  notePresence(presenceReading);
129
- return { ok: true, sessions, alive: new Set(sessions.map((s) => s.address).filter(Boolean)), online,
130
- presenceKnown: presence?.connected === true, machineEvents: presence,
131
- error: presence?.connected ? null : 'machine event stream unavailable; retaining existing runs until availability is known' };
122
+ return fleet;
132
123
  }
133
124
 
134
125
  // how the launcher opens each harness a pane lane can run, and how it names the session: Claude Code is told the id
@@ -142,7 +133,23 @@ const HARNESSES = {
142
133
  const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
143
134
  const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
144
135
 
145
- const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ? fleet.native.get(addressOf(session)).alive : Boolean(session?.session_id) && fleet.alive.has(addressOf(session));
136
+ // A run Teams never published (one outside a synced root, a scratch workspace's) is read on its own machine: a
137
+ // headless worker by its pid, a pane by that machine's pane list. Null only when the machine cannot answer.
138
+ const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ? fleet.native.get(addressOf(session)).alive : unpublishedAlive(session);
139
+ const paneLists = new Map();
140
+ function unpublishedAlive(session) {
141
+ if (!session) return null;
142
+ if (session.surface === 'headless' && session.pid) return pidAlive(session.pid);
143
+ if (!session.pane || !session.machine) return null;
144
+ let entry = paneLists.get(session.machine);
145
+ if (!entry || Date.now() - entry.at > 5000) {
146
+ const r = doorOn(session.machine, 'harness.v1.panes.list', {}, { timeout: 60_000 });
147
+ const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
148
+ entry = { at: Date.now(), panes: Array.isArray(panes) ? panes : null };
149
+ paneLists.set(session.machine, entry);
150
+ }
151
+ return entry.panes ? entry.panes.some((p) => p.pane === session.pane && !p.dead) : null;
152
+ }
146
153
 
147
154
  /** Native activity comes from the machine's harness discovery, not catalogue age. */
148
155
  function nativeSessions(machine, harness) {
@@ -224,7 +231,6 @@ export function lanesOf(profiles = {}) {
224
231
  return out;
225
232
  }
226
233
 
227
- const onMachine = machine => normalMachine(machine ?? localMachine()) === localMachine() ? [] : ['--machine', machine];
228
234
  // The latest presence reading (observeFleet). A machine it reads as offline is not asked: each remote call would
229
235
  // wait out its timeout, and one offline machine held every close and launch on this one.
230
236
  let presenceReading = null;
@@ -232,45 +238,24 @@ const offlineMachine = (machine) => {
232
238
  const m = normalMachine(machine ?? localMachine());
233
239
  return m !== localMachine() && presenceReading?.known === true && !presenceReading.online.has(m);
234
240
  };
235
- /** `supercode <args>` for `machine`: on another machine through its Teams server, answered at once when it is offline. */
236
- const scOn = (machine, args, options) => offlineMachine(machine)
241
+ /** A machine's public door (`harness.v1.*`), answered at once when the machine reads as offline. */
242
+ const doorOn = (machine, method, params, options) => offlineMachine(machine)
237
243
  ? { code: 1, stdout: '', stderr: `machine ${normalMachine(machine)} is offline` }
238
- : sc([...args, ...onMachine(machine)], options);
244
+ : runtimeDoor(normalMachine(machine ?? localMachine()) === localMachine() ? null : machine, method, params, options);
239
245
  const unavailable = message => Object.assign(new Error(message), { launchDeferred: true });
240
246
 
241
247
  // ------------------------------------------------------------------ supervise
242
248
 
243
249
  function supervise(board, fleet, settings, report, entry, effects) {
244
250
  const rows = board.db.prepare(entry.workflow.run ? "SELECT id FROM tasks WHERE current_run_id IS NOT NULL" : "SELECT id FROM tasks WHERE status = 'running'").all();
245
- // The catalog can lag a pane. Read the actual machines once, before any board mutation.
246
- const observed = new Map();
251
+ // Published native observations are the only liveness input. Missing data remains unknown. A launch the
252
+ // observations show stopped is asked, through its machine's door, how it ended.
247
253
  const exits = new Map();
248
- const native = new Map(); fleet.native = new Map();
249
254
  for (const { id } of rows) {
250
255
  const session = currentRun(board.db, id)?.metadata?.supercode;
251
- if (!session?.session_id || session.pane || session.surface === 'headless') continue;
252
- const key = `${session.machine}:${session.harness}`;
253
- if (!native.has(key)) {
254
- try { native.set(key, nativeSessions(session.machine, session.harness)); }
255
- catch (error) { native.set(key, null); report.failed.push([id, error.message]); }
256
- }
257
- fleet.native.set(addressOf(session), native.get(key) ? nativeReading(native.get(key), session) : { alive: null });
258
- }
259
- for (const { id } of rows) {
260
- const s = currentRun(board.db, id)?.metadata?.supercode;
261
- if (!s?.pane || observed.has(s.machine)) continue;
262
- const r = scOn(s.machine, ['teams', 'panes', 'ls'], { timeout: 60_000 });
263
- const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
264
- observed.set(s.machine, Array.isArray(list) ? new Set(list.filter(p => !p.dead).map((p) => p.pane)) : null);
265
- if (!Array.isArray(list)) report.failed.push([id, `pane liveness unavailable; retaining run: ${(r.stderr || r.stdout).trim().slice(-200)}`]);
266
- }
267
- for (const {id} of rows) {
268
- const session=currentRun(board.db,id)?.metadata?.supercode;
269
- const panes=observed.get(session?.machine);
270
- if(session?.launch_key && panes instanceof Set && !panes.has(session.pane)) {
271
- try { const receipt=launchReceipt(board,session);if(receipt?.live===false&&receipt.exit)exits.set(id,receipt.exit); }
272
- catch(error){report.failed.push([id,`exit receipt unavailable; retaining liveness observation: ${error.message}`]);}
273
- }
256
+ if (!session?.launch_key || !fleet.ok || sessionAlive(fleet, session) !== false) continue;
257
+ try { const receipt = launchReceipt(board, session); if (receipt?.live === false && receipt.exit) exits.set(id, receipt.exit); }
258
+ catch (error) { report.failed.push([id, `exit receipt unavailable; retaining liveness observation: ${error.message}`]); }
274
259
  }
275
260
  for (const { id } of rows) {
276
261
  tx(board, (db) => {
@@ -278,20 +263,20 @@ function supervise(board, fleet, settings, report, entry, effects) {
278
263
  if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
279
264
  const runRow = currentRun(db, id);
280
265
  let session = runRow?.metadata?.supercode ?? null;
281
- if (session?.pane) {
282
- let found;
283
- try { found = continuationPane(fleet, session, observed.get(session.machine)); }
284
- catch (error) { report.failed.push([id, error.message]); return; }
285
- if (found && found !== session.pane) {
286
- session = { ...session, pane: found, seen_at: now(), missing_since: null, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
287
- setRunSession(db, runRow.id, session);
288
- event(db, id, 'session_rebound', { session_id: session.session_id, pane: found }, runRow.id);
289
- }
266
+ const liveBinding=fleet.sessions?.find(item=>item.address===addressOf(session)&&item.state==='running'&&item.pane);
267
+ if(liveBinding?.pane&&liveBinding.pane!==session.pane){
268
+ session={...session,pane:liveBinding.pane,missing_since:null};setRunSession(db,runRow.id,session);
269
+ event(db,id,'session_rebound',{session_id:session.session_id,pane:session.pane},runRow.id);
290
270
  }
291
271
  const t = now();
292
272
  // what the dispatcher saw becomes a platform event; the card's workflow says what it does
293
273
  const observe = (name, payload, bucket) => {
294
274
  try {
275
+ if (monitorsRuns(entry) && failedObservation(name)) {
276
+ surfaceIncident(db, board, entry, id, name, payload);
277
+ if (bucket) report[bucket].push(id);
278
+ return;
279
+ }
295
280
  const r = fire(db, board, entry, id, name, { roles: ['platform'], payload });
296
281
  effects.push(...r.effects);
297
282
  if (bucket) report[bucket].push(id);
@@ -319,8 +304,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
319
304
  // Availability is the same retained linked/unlinked feed subscribers hear.
320
305
  // A failed stream is unknown, not evidence that every remote machine left.
321
306
  if (session?.session_id && session.surface !== 'headless') {
322
- const panes = session.pane ? observed.get(session.machine) : null;
323
- const reachable = panes instanceof Set || fleet.online.has(session.machine);
307
+ const reachable = fleet.online.has(session.machine);
324
308
  if (!reachable && !fleet.presenceKnown) return;
325
309
  if (!reachable) {
326
310
  const next = {...session, offline_since: session.offline_since ?? t};
@@ -341,8 +325,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
341
325
  }
342
326
  }
343
327
  // Hermes's orphan reconciliation: a running card whose claim bookkeeping is broken, with nothing live behind it
344
- const paneReading = session?.pane ? observed.get(session.machine) : undefined;
345
- const paneLive = paneReading instanceof Set ? paneReading.has(session.pane) : null;
328
+ const paneLive = session?.session_id ? sessionAlive(fleet,session) : null;
346
329
  const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) !== false : true) : false;
347
330
  // (whether it is reconciled is the workflow's: its `orphaned` transitions and their guard)
348
331
  if ((!task.claim_lock || !task.claim_expires) && !live) {
@@ -378,6 +361,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
378
361
  // follows, and it is not a crash to restart
379
362
  const ended = { code: exit.failed ? 1 : 0, error: `native session ${session.session_id} ended: ${exit.how}`, session_id: session.session_id, pane: session.pane };
380
363
  if (!exit.failed) {
364
+ if (monitorsRuns(entry)) { observe('session_closed', {...ended,exit},'crashed'); return; }
381
365
  try {
382
366
  const r = fire(db, board, entry, id, 'session_closed', { roles: ['platform'], payload: ended });
383
367
  effects.push(...r.effects); (report.closed_by_hand ??= []).push(id);
@@ -387,11 +371,10 @@ function supervise(board, fleet, settings, report, entry, effects) {
387
371
  observe('exited', ended, 'crashed');
388
372
  return;
389
373
  }
390
- if (session.pane && paneLive === null) return; // no machine reading, no loss verdict
391
- if (!session.pane && !fleet.ok) return;
392
- const alive = session.pane ? paneLive : sessionAlive(fleet, session);
393
- if (alive == null) return; // native discovery was inconclusive
394
- const machineUp = paneReading instanceof Set || fleet.online.has(session.machine);
374
+ if (!fleet.ok) return;
375
+ const alive = sessionAlive(fleet,session);
376
+ if (alive == null) return;
377
+ const machineUp = fleet.online.has(session.machine);
395
378
  const young = t - Number(session.launched_at ?? 0) < Number(limits.grace ?? settings.grace);
396
379
  const missing = !alive && machineUp && !young;
397
380
  const next = { ...session };
@@ -450,7 +433,7 @@ function stopSession(board, session) {
450
433
  return;
451
434
  }
452
435
  if (session.pane) {
453
- const r = sc(['teams', 'panes', 'close', session.pane, ...(session.machine ? ['--machine', session.machine] : [])], { timeout: 60_000, as: boardAddress(board.label) });
436
+ const r = runtimeDoor(normalMachine(session.machine??localMachine())===localMachine()?null:session.machine,'harness.v1.panes.close',{pane:session.pane},{env:commandEnv(),timeout:60_000,as:boardAddress(board.label)});
454
437
  if (r.code !== 0 || json(r.stdout.trim(), null)?.closed !== session.pane) throw new Error(`closing pane ${session.pane}: ${(r.stderr || r.stdout || `exit ${r.code}`).trim().slice(-500)}`);
455
438
  }
456
439
  }
@@ -537,6 +520,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
537
520
  const stop = paused(board.root);
538
521
  if (stop) { report.paused = stop; return; }
539
522
  const { workflow, params } = entry;
523
+ if (!dry) tx(board, db => auditEndedRuns(db, board, entry));
540
524
  const d = workflow.dispatch;
541
525
  const pressure = memoryPressure();
542
526
  const tickState = { local_started: 0 };
@@ -633,7 +617,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
633
617
  } catch (error) {
634
618
  if (error.launchPending) { report.queued.push([task.id, error.message]); continue; }
635
619
  const payload = { error: String(error.message).slice(-500), ...(error.resumeFailed ? { resume_failed: true, ...error.resumeFailed } : {}) };
636
- try { const r = tx(board, (db) => fire(db, board, entry, task.id, 'spawn_failed', { roles: ['platform'], payload })); effects.push(...r.effects); } catch { /* no transition for it */ }
620
+ try { const r = tx(board, db => monitorsRuns(entry) ? (surfaceIncident(db,board,entry,task.id,'spawn_failed',payload), {effects:[]}) : fire(db, board, entry, task.id, 'spawn_failed', { roles: ['platform'], payload })); effects.push(...r.effects); } catch { /* no transition for it */ }
637
621
  report.failed.push([task.id, String(error.message).slice(-300)]);
638
622
  }
639
623
  }
@@ -656,7 +640,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
656
640
  const where = whereOf(board, task);
657
641
  // a subtask's run also sees its arc: the arc's text, tasks and thread (where its owner's rulings are)
658
642
  const arcContext = task.subtask_of ? view(board, (db) => cardContext(db, task.subtask_of)) : '';
659
- const prompt = render(spec.prompt, { ...scope, where, context: view(board, (db) => cardContext(db, id)), arc_context: arcContext, doors: doors(entry.workflow, status, spec.role, id, where, surface) });
643
+ let prompt = render(spec.prompt, { ...scope, where, context: view(board, (db) => cardContext(db, id)), arc_context: arcContext, doors: doors(entry.workflow, status, spec.role, id, where, surface) });
660
644
  // the run's session record knows its status, role and slot (a reviewer's slot is never the implementer's); the slot
661
645
  // is a template, so a step can ask for isolation by naming a new one
662
646
  const slot = String(templateValue(spec.session ?? 'worker', scope) ?? 'worker');
@@ -668,12 +652,57 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
668
652
  // the model it runs is recorded with the session, for caps that count by model
669
653
  const meta = { status, role: spec.role, slot, model: task.model_override ?? lane.model ?? null, ...(spec.role === 'reviewer' ? { review_candidate: reviewCandidate(board, id) } : {}) };
670
654
  try {
655
+ const context=process.env.SUPERCODE_CONTEXT,team=agentScope(context),machine=normalMachine(task.machine)===localMachine()?null:task.machine;
656
+ const cardOptions={root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
657
+ profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null,machine,context};
658
+ // Situation 16: with no reachable Teams the card's agent is its locally minted identity, declared later under the same id.
659
+ let agent=null,previous=null,reader=null;
660
+ if(!team.offline){
661
+ try{
662
+ agent=ensureCardAgent(cardOptions);
663
+ previous=agentDoor('sessions',[agent.identity.id,{limit:200}],{context}).items??[];
664
+ reader=agentDoor('readerContext',[agent.identity.id,machine],{context,timeout:60000});
665
+ }catch(error){if(!teamsUnavailable(error))throw error;tx(board,db=>event(db,id,'agent_identity_local',{reason:error.message},runId));}
666
+ }
667
+ agent??=localCardAgent(cardOptions);
668
+ // G3: a run of an agent over its budget does not start; the reason waits on the card until the window passes
669
+ const budgets=[...(entry.profiles?.default?.budgets??[]),...(lane.name!=='default'?entry.profiles?.[lane.name]?.budgets??[]:[])];
670
+ const refused=budgets.length?runBudgetRefusal(board.root,agent.identity.id,budgets,new Date().toISOString()):null;
671
+ if(refused)throw Object.assign(new Error(`not started, over budget: ${refused}`),{launchDeferred:true});
672
+ previous??=localAgentSessions(board.root,team.org,agent.identity.id);
673
+ prompt=`${agentPreamble(agent,{previous:previous.map(item=>({session:item.session,machine:item.machine,ended_at:item.ended_at,end_reason:item.end_reason}))})}\n\n${prompt}`;
674
+ Object.assign(meta,{agent_id:agent.identity.id,agent_org:team.org,agent_context:context??null,
675
+ agent_profile_intent:structuredClone(agent.intended.resolved),
676
+ agent_env:agentEnvironment(agent,{root:board.root,context:reader?.context})});
677
+ // The agent's own memory reaches every session of it: headless runs open its home; a pane, which may sit on
678
+ // another machine, gets the home's snapshot in its first prompt. A shared profile's memory is never used.
679
+ const agentHome=materializeAgentHome(board.root,team.org,agent.identity.id,lane);
680
+ if(surface==='headless')meta.agent_env.HERMES_HOME=agentHome;
681
+ else{const memory=memorySnapshotSync(agentHome);if(memory)prompt=`${prompt}\n\n${memory}`;}
671
682
  const result = surface === 'pane'
672
683
  ? launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
673
684
  : launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
674
- tx(board,db=>setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null}));
685
+ tx(board,db=>{
686
+ setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null,restart_authorized:false});
687
+ event(db,id,'restart_authorization_consumed',{},runId);
688
+ });
689
+ if(result?.session_id){
690
+ const ref=`${result.harness}:${result.session_id}`;
691
+ recordAgentRun(board.root,team.org,agent.identity.id,{session:ref,machine:result.machine,run:runId});
692
+ if(agent.synced!==false){
693
+ try{synchronizeAgentRun(board.root,team.org,{agent_id:agent.identity.id,session_ref:ref,machine:result.machine},{context,intended:meta.agent_profile_intent});}
694
+ catch(error){tx(board,db=>event(db,id,'agent_session_sync_pending',{session:ref,agent:agent.identity.id,error:error.message},runId));}
695
+ }
696
+ }
675
697
  return result;
676
698
  } catch(error) {
699
+ // An issued asynchronous launch may bind later. Its original permission
700
+ // still belongs to this attempt, never a future lost-session restart. A
701
+ // deferred launch was never issued: its run keeps the permission for its retry.
702
+ if(!error.launchDeferred)tx(board,db=>{
703
+ setRunSession(db,runId,{restart_authorized:false});
704
+ event(db,id,'restart_authorization_consumed',{},runId);
705
+ });
677
706
  if(error.launchDeferred) {
678
707
  tx(board,db=>{
679
708
  const previous=currentRun(db,id)?.metadata?.supercode;
@@ -688,22 +717,33 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
688
717
 
689
718
  /** Start one card now by a person's word (a triage card's session, a manager's start): the workflow's start event. */
690
719
  export async function startCard(board, id, { roles = ['manager'], lanes, settings } = {}) {
720
+ // the board's own published fleet projection, as a tick reads it: a manager's or creator's start that continues the
721
+ // slot's session checks that session through it
722
+ eventFleetRoot = board.root;
691
723
  const entry = await loadWorkflow(board.root);
692
724
  const fleet = observeFleet();
693
725
  const before = getTaskNow(board, id), checked = checkWorkspace(board, before);
694
726
  const moved = tx(board, (db) => {
695
727
  assertWorkspaceOwner(db, before, checked);
696
728
  if (checked) db.prepare('UPDATE supercode_cards SET workspace_root=? WHERE task_id=?').run(checked.mainRoot, id);
697
- if (!roles.includes('manager')) throw new Error('Only the manager starts a held card; request its decision.');
729
+ if (!roles.some(role => ['manager', 'creator'].includes(role))) throw new Error('Only the manager or creator starts a held card; request its decision.');
698
730
  event(db, id, 'dispatch_released', { reason: 'explicit start' });
699
- return fire(db, board, entry, id, entry.workflow.dispatch.start, { roles, payload: { lock: claimer() } });
731
+ requestSessionClose(db,id);
732
+ const moved = fire(db, board, entry, id, entry.workflow.dispatch.start, { roles, payload: { lock: claimer() } });
733
+ setRunSession(db, getTask(db,id).current_run_id, { restart_authorized: true });
734
+ return moved;
700
735
  });
701
736
  const task = getTaskNow(board, id);
702
737
  const spec = runSpec(entry, moved.to, task);
703
738
  const profile = spec ? view(board, (db) => evaluate(entry.workflow.roles?.[spec.role] ?? 'card.assignee', scopeOf(db, board, task, { params: entry.params }))) : null;
704
739
  const lane = profile ? lanes[profile] : null;
705
740
  if (spec && !lane) throw new Error(`${id} is ${moved.to}, run by ${spec.role} (${profile ?? 'nobody'}), which is no lane of this home`);
706
- return lane ? launchRun(board, entry, id, lane, fleet, settings) : null;
741
+ if (!lane) return null;
742
+ try { return launchRun(board,entry,id,lane,fleet,settings); }
743
+ catch (error) {
744
+ if (monitorsRuns(entry) && !error.launchPending) tx(board,db=>surfaceIncident(db,board,entry,id,'spawn_failed',{error:error.message}));
745
+ throw error;
746
+ }
707
747
  }
708
748
 
709
749
  /** Reconcile durable close intent and ended runs, including old unverified closed_at records.
@@ -740,7 +780,9 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
740
780
  (report.closed ??= []).push({ task_id: row.task_id, ...session, receipt });
741
781
  continue;
742
782
  }
743
- if (receipt?.pane && receipt.status !== 'issued' && (receipt.status || receipt.live || receipt.exit)) { bindLaunch(board, row, session, receipt.pane, session.session_id); session = { ...session, pane: receipt.pane }; }
783
+ // A receipt that names its pane and reads it not live is terminal enough to settle, with or without a status (a
784
+ // daemon that predates launch statuses answers none): the board's own pane reading below confirms the close
785
+ if (receipt?.pane && receipt.status !== 'issued' && (receipt.status || typeof receipt.live === 'boolean' || receipt.exit)) { bindLaunch(board, row, session, receipt.pane, session.session_id); session = { ...session, pane: receipt.pane }; }
744
786
  else {
745
787
  const error = `close deferred: ${receiptError ?? `awaiting terminal launch receipt ${session.launch_key} (${receipt?.status ?? 'unknown'})`}`;
746
788
  tx(board, (db) => {
@@ -751,6 +793,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
751
793
  continue;
752
794
  }
753
795
  }
796
+ if (session?.monitor_held && !session.close_requested) continue; // only an explicit close action ends this process
754
797
  if ((!session?.pane && session?.surface !== 'native') || session.close_transferred_to || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
755
798
  if (caller && session.session_id === caller) continue; // let the command that ended its own run return
756
799
  const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)`).all());
@@ -769,7 +812,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
769
812
  }
770
813
  } else {
771
814
  if (!panes.has(machine)) {
772
- const r = scOn(machine, ['teams', 'panes', 'ls'], { timeout: 60_000, as: boardAddress(board.label) });
815
+ const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000, as: boardAddress(board.label) });
773
816
  const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
774
817
  panes.set(machine, Array.isArray(list) ? { live: new Set(list.filter(p => !p.dead).map((p) => p.pane)) } : { error: `cannot read panes on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}` });
775
818
  }
@@ -829,6 +872,9 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
829
872
  threshold_seconds: settings.idleQuiet, turns_before: Number(session.goal_turns ?? 0),
830
873
  };
831
874
  const retain = (decision, extra = {}) => {
875
+ // A reading that decides nothing is kept once per state: rewriting it each pass would wake the next pass.
876
+ const prior = session.idle_observation;
877
+ if (!['dispatched', 'dispatch_failed'].includes(decision) && prior?.decision === decision && prior.last_message_at_ms === reading.last_message_at_ms && prior.status === reading.status) return prior;
832
878
  const observation = { ...reading, decision, turns_after: reading.turns_before, ...extra };
833
879
  const patch = { idle_observation: observation };
834
880
  if (decision === 'below_threshold') patch.idle_below_threshold = observation;
@@ -996,9 +1042,9 @@ const getTaskNow = (board, id) => view(board, (db) => getTask(db, id));
996
1042
 
997
1043
  /** A transport error is not a negative launch receipt. The key remains owned by its run. */
998
1044
  function launchReceipt(board, session) {
999
- const r = scOn(session.machine, ['teams', 'launches', 'get', session.launch_key],
1045
+ const r = doorOn(session.machine, 'harness.v1.launches.get',{key:session.launch_key},
1000
1046
  { timeout: 30_000, as: boardAddress(board.label) });
1001
- if (r.code !== 0) throw unavailable(`launch receipt unavailable on ${session.machine}: install the matching Teams CLI and machine capability (teams launches get): ${(r.stderr || r.stdout).trim().slice(-300)}`);
1047
+ if (r.code !== 0) throw unavailable(`launch receipt unavailable on ${session.machine}: install the matching machine capability (harness.v1.launches.get): ${(r.stderr || r.stdout).trim().slice(-300)}`);
1002
1048
  if (r.stdout.trim() === 'null') return null;
1003
1049
  const receipt = lastJson(r.stdout);
1004
1050
  if (!receipt || receipt.key !== session.launch_key) throw unavailable(`invalid launch receipt for ${session.launch_key} on ${session.machine}`);
@@ -1006,7 +1052,7 @@ function launchReceipt(board, session) {
1006
1052
  }
1007
1053
 
1008
1054
  function cancelUnissuedLaunch(board, session) {
1009
- const result = scOn(session.machine, ['teams', 'launches', 'cancel', session.launch_key],
1055
+ const result = doorOn(session.machine, 'harness.v1.launches.cancel', {key:session.launch_key},
1010
1056
  { timeout: 30_000, as: boardAddress(board.label) });
1011
1057
  if (result.code !== 0) throw unavailable(`launch cancellation unavailable on ${session.machine}; retain ${session.launch_key}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
1012
1058
  const receipt = lastJson(result.stdout);
@@ -1034,7 +1080,8 @@ function retireUnissuedLaunch(board, row, session, receipt, { entry = null, effe
1034
1080
  forgotten: saved.mode === 'fresh' ? true : Boolean(saved.forgotten) });
1035
1081
  event(db, row.task_id, 'session_launch_cancelled', { key: session.launch_key, machine: session.machine, receipt }, row.id);
1036
1082
  if (!current.ended_at) {
1037
- const moved = fire(db, board, entry, row.task_id, 'spawn_failed', { roles: ['platform'], payload: { error: `launch ${session.launch_key} was not issued; its machine retired the key` } });
1083
+ const payload = { error: `launch ${session.launch_key} was not issued; its machine retired the key` };
1084
+ const moved = monitorsRuns(entry) ? (surfaceIncident(db,board,entry,row.task_id,'spawn_failed',payload), {effects:[]}) : fire(db,board,entry,row.task_id,'spawn_failed',{roles:['platform'],payload});
1038
1085
  collected.push(...moved.effects);
1039
1086
  }
1040
1087
  event(db, row.task_id, 'session_closed', { key: session.launch_key, machine: session.machine, reason: 'launch not issued', receipt }, row.id);
@@ -1065,6 +1112,7 @@ function bindLaunch(board, row, session, pane, sid) {
1065
1112
 
1066
1113
  /** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
1067
1114
  export function reconcileCallerLaunch(board, sid) {
1115
+ eventFleetRoot = board.root;
1068
1116
  if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
1069
1117
  WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
1070
1118
  const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
@@ -1089,7 +1137,63 @@ export function reconcileCallerLaunch(board, sid) {
1089
1137
  }
1090
1138
 
1091
1139
  function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
1140
+ // A delayed launch receipt/catalogue event binds the same declared agent, never another identity.
1141
+ // A resumed session is held by its newest run; an earlier run of the same session no longer owns it.
1142
+ const agentRuns=[...new Map(view(board,db=>db.prepare("SELECT id,task_id,metadata,ended_at FROM task_runs WHERE json_extract(metadata,'$.supercode.agent_id') IS NOT NULL ORDER BY id").all())
1143
+ .map(row=>{const session=json(row.metadata,{})?.supercode;return [session?.session_id?addressOf(session):`run:${row.id}`,row];})).values()];
1144
+ const orgs=new Map();
1145
+ for(const row of agentRuns){
1146
+ const session=json(row.metadata,{})?.supercode;
1147
+ if(!session?.session_id||!session.agent_org)continue;
1148
+ const observation=fleet.sessions?.find(item=>item.address===addressOf(session))?.identity;
1149
+ if(observation?.agent?.id===session.agent_id&&session.agent_profile_revision!==observation.revision){
1150
+ const mismatch=agentProfileMismatch(session.agent_profile_intent,observation.resolved);
1151
+ tx(board,db=>{
1152
+ // Rule 4: a run whose recorded profile differs from its agent's intent is shown and not counted.
1153
+ setRunSession(db,row.id,{agent_profile_recorded:observation.resolved,agent_profile_revision:observation.revision,agent_profile_mismatch:mismatch,counted:!mismatch.differences.length});
1154
+ event(db,row.task_id,mismatch.differences.length?'agent_profile_drift':'agent_profile_recorded',{agent:session.agent_id,session:session.session_id,comparison:mismatch},row.id);
1155
+ });
1156
+ }
1157
+ recordAgentRun(board.root,session.agent_org,session.agent_id,{session:`${session.harness}:${session.session_id}`,machine:session.machine,run:row.id});
1158
+ orgs.set(session.agent_org,session.agent_context);
1159
+ }
1160
+ // Identities minted while Teams was absent are declared under their ids before their sessions are joined.
1161
+ const context=process.env.SUPERCODE_CONTEXT,scope=agentScope(context);
1162
+ if(!scope.offline){
1163
+ orgs.set(scope.org,orgs.get(scope.org)??context);
1164
+ for(const pending of synchronizeLocalAgents({root:board.root,profiles:entry.profiles,context}))report.queued.push([pending.agent,`agent declaration pending: ${pending.error}`]);
1165
+ }
1166
+ orgs.delete('local');
1167
+ // G3: an ended run's session usage, read once from its harness's own records on its machine
1168
+ for(const org of new Set(agentRuns.map(row=>json(row.metadata,{})?.supercode?.agent_org).filter(Boolean))){
1169
+ let read=0;
1170
+ for(const row of runsAwaitingUsage(board.root,org)){
1171
+ const run=agentRuns.find(item=>String(item.id)===String(row.run_id));
1172
+ if(!run?.ended_at||read>=5)continue;
1173
+ const [harness,...rest]=row.session_ref.split(':'),machine=row.machine&&normalMachine(row.machine)!==localMachine()?row.machine:null;
1174
+ // the usage door loads the session by its whole locator, which discovery names
1175
+ const sessionId=rest.join(':'),found=runtimeDoor(machine,'harness.v1.sessions.discover',{query:sessionId,harnesses:[harness],limit:200},{timeout:30_000});
1176
+ const locator=found.code===0?json(found.stdout.trim(),{})?.sessions?.find(item=>item.locator?.harness===harness&&item.locator?.session_id===sessionId)?.locator:null;
1177
+ const reply=locator?runtimeDoor(machine,'harness.v1.sessions.usage',{locator},{timeout:30_000}):{code:1};read++;
1178
+ if(reply.code===0)recordRunUsage(board.root,org,row.session_ref,json(reply.stdout.trim(),{})?.usage??[]);
1179
+ // a session unreadable a day after its run ended is recorded as having no usage record, so it stops retrying
1180
+ else if(now()-Number(run.ended_at)>86400)recordRunUsage(board.root,org,row.session_ref,[]);
1181
+ }
1182
+ }
1183
+ for(const assignment of activeAgentAssignments(board.root)){
1184
+ if(assignment.kind!=='card')continue;
1185
+ const task=getTaskNow(board,assignment.target);
1186
+ if(task&&(task.archived||['done','cancelled'].includes(task.status)))finishAgentAssignment(board.root,assignment.org,assignment.id,`card ${assignment.target} ${task.archived?'archived':task.status}`);
1187
+ }
1188
+ for(const [org,context] of orgs)for(const pending of synchronizeAgentEnds(board.root,org,{context}))report.queued.push([pending.agent,`agent end synchronization pending: ${pending.error}`]);
1189
+ for(const [org,context] of orgs)for(const row of pendingAgentRuns(board.root,org)){
1190
+ const launch=agentRuns.find(run=>String(run.id)===String(row.run_id));
1191
+ try{synchronizeAgentRun(board.root,org,row,{context,intended:json(launch?.metadata,{})?.supercode?.agent_profile_intent});}
1192
+ catch(error){report.queued.push([row.run_id,`agent session sync pending: ${error.message}`]);}
1193
+ }
1092
1194
  const deferred=view(board,db=>db.prepare("SELECT t.id,r.started_at FROM tasks t JOIN task_runs r ON r.id=t.current_run_id WHERE r.ended_at IS NULL AND json_extract(r.metadata,'$.supercode.launch_deferred')=1").all());
1195
+ // A deferred launch was never issued (a reading or door was not yet available): it is the start its manager, creator
1196
+ // or workflow asked for, carried out within the launch window; supervise surfaces it once when the window ends
1093
1197
  for(const {id,started_at} of deferred) {
1094
1198
  if (now() - started_at >= settings.launchWindow) continue;
1095
1199
  try {
@@ -1101,7 +1205,7 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1101
1205
  if(session)report.started.push([id,session.address??session.session_id,session.mode]);
1102
1206
  }catch(error){
1103
1207
  if(error.launchPending){report.queued.push([id,error.message]);continue;}
1104
- const moved=tx(board,db=>fire(db,board,entry,id,'spawn_failed',{roles:['platform'],payload:{error:error.message}}));effects.push(...moved.effects);
1208
+ const moved=tx(board,db=>monitorsRuns(entry)?(surfaceIncident(db,board,entry,id,'spawn_failed',{error:error.message}),{effects:[]}):fire(db,board,entry,id,'spawn_failed',{roles:['platform'],payload:{error:error.message}}));effects.push(...moved.effects);
1105
1209
  report.failed.push([id,error.message]);
1106
1210
  }
1107
1211
  }
@@ -1130,7 +1234,9 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1130
1234
  const moved = tx(board, (db) => {
1131
1235
  if (getTask(db, row.task_id)?.current_run_id !== row.id) return null;
1132
1236
  setRunSession(db, row.id, { launch_pending: false });
1133
- return fire(db, board, entry, row.task_id, 'spawn_failed', { roles: ['platform'], payload: { error: `launch ${s.launch_key} ended before session binding` } });
1237
+ const payload = { error: `launch ${s.launch_key} ended before session binding` };
1238
+ if (monitorsRuns(entry)) { surfaceIncident(db, board, entry, row.task_id, 'spawn_failed', payload); return { effects: [] }; }
1239
+ return fire(db, board, entry, row.task_id, 'spawn_failed', { roles: ['platform'], payload });
1134
1240
  });
1135
1241
  if (moved) effects.push(...moved.effects);
1136
1242
  }
@@ -1153,11 +1259,13 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1153
1259
  task = { ...task, workspace_path: made };
1154
1260
  }
1155
1261
  const model = task.model_override ?? lane.model;
1262
+ if(meta.agent_profile_intent)meta.agent_profile_intent.worker={...meta.agent_profile_intent.worker,harness:lane.harness,model:model??null,cwd:task.workspace_path};
1156
1263
  const tail = model ? ['--model', model] : [];
1157
1264
  if (lane.harness === 'codex' && lane.reasoning_effort) tail.push('-c', `model_reasoning_effort=${JSON.stringify(lane.reasoning_effort)}`);
1158
1265
  const harness = HARNESSES[lane.harness] ? lane.harness : 'claude-code';
1159
1266
  const how = HARNESSES[harness];
1160
- const base = ['open', '--new', how.program, '--detach', `--window=${window}`, '--on', task.machine, '--cwd', task.workspace_path];
1267
+ const base = ['open', '--new', how.program, '--detach', `--window=${window}`, '--on', task.machine, '--cwd', task.workspace_path, '--env', 'SUPERCODE_BOARD_PROCESS_GUARD=1',
1268
+ ...Object.entries(meta.agent_env??{}).flatMap(([key,value])=>['--env',`${key}=${value}`])];
1161
1269
  // the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
1162
1270
  // a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
1163
1271
  // session starts (never two processes on one session)
@@ -1171,7 +1279,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1171
1279
  if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
1172
1280
  const fresh = observeFleet();
1173
1281
  if (!fresh.ok) throw unavailable(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
1174
- const r = scOn(machine, ['teams', 'panes', 'ls'], { timeout: 60_000 });
1282
+ const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
1175
1283
  const listed = r.code === 0 ? json(r.stdout.trim(), null) : null;
1176
1284
  if (!Array.isArray(listed)) throw unavailable(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1177
1285
  const pane = continuationPane(fresh, prev, new Set(listed.filter(p => !p.dead).map((p) => p.pane)));
@@ -1186,6 +1294,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1186
1294
  if (prev?.close_requested && !prev.closed_at) throw unavailable(`the slot's session ${prev.session_id} asked to be closed and its pane ${prev.pane} could not be: ${report.undelivered.map((u) => u[2]).join('; ') || 'no reason given'}`);
1187
1295
  }
1188
1296
  const openOwned = (key, sid, mode, args) => {
1297
+ const description = doorOn(machine,'harness.v1.machine.describe',{});
1298
+ const policy = description.code === 0 ? json(description.stdout,null)?.boardProcessPolicy : null;
1299
+ if (policy?.version !== 1 || !policy.harnesses?.includes(harness)) throw unavailable(`machine ${machine} has no native board process guard for ${harness}; no unguarded session was launched`);
1189
1300
  launchReceipt(board, { machine, launch_key: key }); // Prove the exact receipt door before starting a process.
1190
1301
  tx(board, (db) => setRunSession(db, runId, { ...meta, surface: 'pane', machine, harness, slot,
1191
1302
  session_id: sid, mode, launch_key: key, launch_pending: true, launched_at: now() }));
@@ -1236,7 +1347,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1236
1347
  if (paneLive && (prev.closed_at || prev.close_requested || prev.forgotten)) throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
1237
1348
  if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
1238
1349
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
1239
- const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, ...kept } = prev;
1350
+ const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, monitor_held: _mh, ...kept } = prev;
1240
1351
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
1241
1352
  // a kept session is told what this run is for (its card came back to it, with what happened since)
1242
1353
  tx(board, (db) => setRunSession(db, runId, { ...session, ...meta }));
@@ -1252,6 +1363,15 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1252
1363
  }
1253
1364
  }
1254
1365
  } else if (prev.resumable !== false) {
1366
+ const authorization = view(board, db => currentRun(db,task.id)?.metadata?.supercode?.restart_authorized);
1367
+ const released = view(board,db=>{
1368
+ const last = db.prepare("SELECT id,kind FROM task_events WHERE task_id=? AND kind IN ('dispatch_held','dispatch_released','restart_authorization_consumed') ORDER BY id DESC LIMIT 1").get(task.id);
1369
+ return last?.kind === 'dispatch_released';
1370
+ });
1371
+ // Only a session whose run ended in a monitored incident waits for its manager or creator; one a workflow
1372
+ // transition closed (a reviewer after its verdict, an implementer whose card came back) is the slot's
1373
+ // deliberate handoff, resumed for the slot's next run like any other
1374
+ if (monitorsRuns(workflowFor(board.root)) && prev.monitor_held && !authorization && !released) throw new Error(`session ${prev.session_id} is stopped by incident ${prev.incident ?? 'unknown'}; only its manager or creator may restart it with workflow start`);
1255
1375
  // a resumed session is told what this run is for, as a kept one is: the run's own prompt (a reviewer resumed for
1256
1376
  // its next round gets that round's prompt, its arc's context with it), not only that it was resumed
1257
1377
  const told = input ? `The board resumed this session for this run of card ${task.id}.\n\n${input}` : `The board resumed this session after it was lost; you are still on card ${task.id}: ${task.title}. Carry on.`;
@@ -1304,7 +1424,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1304
1424
 
1305
1425
  /** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
1306
1426
  function panePresent(machine, pane) {
1307
- const r = scOn(machine, ['teams', 'panes', 'ls'], { timeout: 60_000 });
1427
+ const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
1308
1428
  const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
1309
1429
  if (!Array.isArray(panes)) throw unavailable(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1310
1430
  return panes.some((p) => p.pane === pane && !p.dead);
@@ -1319,6 +1439,7 @@ function paneSurvives(machine, pane) {
1319
1439
  /** Hermes's own worker argv and task environment, run on this machine; watched by its pid. */
1320
1440
  function launchHeadless(board, task, runId, lane, settings, { prompt = null, skills: runSkills = [], meta = {} } = {}) {
1321
1441
  const workspace = resolveLocalWorkspace(board, task);
1442
+ if(meta.agent_profile_intent)meta.agent_profile_intent.worker={...meta.agent_profile_intent.worker,harness:lane.harness,model:task.model_override??lane.model??null,cwd:workspace};
1322
1443
  tx(board, (db) => db.prepare('UPDATE tasks SET workspace_path=?,branch_name=COALESCE(branch_name,?) WHERE id=?').run(workspace, task.workspace_kind === 'worktree' ? `wt/${task.id}` : null, task.id));
1323
1444
  // the card's own skills and the ones its status's run names (a reviewer's, for one)
1324
1445
  const skills = [...new Set([...task.skills, ...runSkills])];
@@ -1337,7 +1458,7 @@ function launchHeadless(board, task, runId, lane, settings, { prompt = null, ski
1337
1458
  env: {
1338
1459
  SUPERCODE_BOARD_EXIT_FILE: exitFile,
1339
1460
  ...(task.goal_mode ? { HERMES_KANBAN_GOAL_MODE: '1', ...(task.goal_max_turns ? { HERMES_KANBAN_GOAL_MAX_TURNS: String(task.goal_max_turns) } : {}) } : {}),
1340
- ...process.env, HERMES_HOME: lane.dir, HERMES_PROFILE: lane.name, HERMES_KANBAN_TASK: task.id, HERMES_KANBAN_WORKSPACE: workspace,
1461
+ ...process.env, ...meta.agent_env, HERMES_HOME: meta.agent_env?.HERMES_HOME??lane.dir, HERMES_PROFILE: lane.name, HERMES_KANBAN_TASK: task.id, HERMES_KANBAN_WORKSPACE: workspace,
1341
1462
  HERMES_KANBAN_RUN_ID: String(runId), HERMES_KANBAN_DB: board.path, HERMES_KANBAN_BOARD: board.slug, HERMES_SESSION_SOURCE: 'kanban',
1342
1463
  HERMES_KANBAN_CLAIM_LOCK: claimer(), ...(prompt ? { SUPERCODE_BOARD_PROMPT: prompt } : {}), ...(task.tenant ? { HERMES_TENANT: task.tenant } : {}),
1343
1464
  ...(task.branch_name ? { HERMES_KANBAN_BRANCH: task.branch_name } : {}),
@@ -1396,14 +1517,14 @@ export function requestAdoption(board, id, who, { workspace = null, assignee = n
1396
1517
  }
1397
1518
 
1398
1519
  /** Adoptions the manager requested and no tick has answered yet, completed from this tick's fleet reading. */
1399
- export function completeAdoptions(board, fleet, settings, report) {
1520
+ export function completeAdoptions(board, fleet, settings, report, lanes = null) {
1400
1521
  const pending = view(board, db => db.prepare(`SELECT e.task_id, e.payload FROM task_events e WHERE e.kind = 'dispatch_held'
1401
1522
  AND json_extract(e.payload, '$.reason') = 'adoption requested'
1402
1523
  AND e.id = (SELECT MAX(id) FROM task_events l WHERE l.task_id = e.task_id AND l.kind IN ('dispatch_held','dispatch_released','session_adopted','adoption_failed'))`).all());
1403
1524
  for (const row of pending) {
1404
1525
  const asked = json(row.payload, {}) ?? {};
1405
1526
  try {
1406
- const got = adoptSession(board, row.task_id, asked.session, { workspace: asked.workspace ?? null, assignee: asked.assignee ?? null, role: asked.role ?? null, ttl: settings.ttl, fleet: fleet?.ok ? fleet : null });
1527
+ const got = adoptSession(board, row.task_id, asked.session, { workspace: asked.workspace ?? null, assignee: asked.assignee ?? null, role: asked.role ?? null, ttl: settings.ttl, fleet: fleet?.ok ? fleet : null , lanes});
1407
1528
  (report.adopted ??= []).push([row.task_id, got.address]);
1408
1529
  } catch (error) {
1409
1530
  const message = String(error.message).slice(-400);
@@ -1413,7 +1534,7 @@ export function completeAdoptions(board, fleet, settings, report) {
1413
1534
  }
1414
1535
  }
1415
1536
 
1416
- function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null, fleet = null } = {}) {
1537
+ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
1417
1538
  fleet ??= observeFleet();
1418
1539
  if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
1419
1540
  const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who);
@@ -1433,10 +1554,21 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
1433
1554
  dir = nativeSessions(host, harness).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
1434
1555
  }
1435
1556
  if (!dir && found.pane) {
1436
- const r = sc(['teams', 'panes', 'ls', '--machine', host], { timeout: 60_000 });
1557
+ const r = runtimeDoor(normalMachine(host)===localMachine()?null:host,'harness.v1.panes.list',{}, { timeout: 60_000 });
1437
1558
  dir = (json(r.stdout.trim(), []) ?? []).find((p) => p.pane === found.pane)?.cwd ?? null;
1438
1559
  }
1439
- return tx(board, (db) => {
1560
+ // Rule 2: a session working a card role is that role's agent, however it started; adopted, it joins the agent as a
1561
+ // launch does, and its recorded profile is compared with the role's intent (rule 4).
1562
+ const entry0 = workflowFor(board.root), card0 = view(board, (db) => getTask(db, id));
1563
+ const spec0 = view(board, (db) => runSpec(entry0, entry0.workflow.verbs?.adopt?.[0]?.target ?? 'running', card0));
1564
+ const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[spec0?.role] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
1565
+ const lane = laneName ? lanes?.[laneName] : null, context = process.env.SUPERCODE_CONTEXT, team = agentScope(context);
1566
+ let agent = null;
1567
+ if (lane && !team.offline) {
1568
+ try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: spec0?.role ?? 'implementer', sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
1569
+ catch (error) { if (!teamsUnavailable(error)) throw error; }
1570
+ }
1571
+ const adopted = tx(board, (db) => {
1440
1572
  const task = getTask(db, id);
1441
1573
  if (!task) throw new Error(`no card ${id}`);
1442
1574
  if ((task.current_run_id ?? null) !== (beforeRun?.id ?? null)) throw new Error(`${id} changed runs during adoption; retry against its current run`);
@@ -1459,16 +1591,68 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
1459
1591
  if (implementer?.session_id === sessionId) throw new Error('a reviewer must be independent of the implementer session');
1460
1592
  }
1461
1593
  event(db, id, 'dispatch_released', { reason: 'verified adoption', session: found.address });
1462
- if (!beforeRun) fire(db, board, entry, id, entry.workflow.run ? entry.workflow.dispatch.start : 'adopt', { roles: [entry.workflow.run ? 'platform' : 'manager'], payload: { lock: claimer() } });
1594
+ // the manager's adoption is the workflow's own `adopt` verb where it declares one, sent as the manager; else its
1595
+ // dispatch start, which the platform sends for a verified adoption
1596
+ if (!beforeRun) { const verb = entry.workflow.verbs?.adopt || !entry.workflow.run ? 'adopt' : entry.workflow.dispatch.start;
1597
+ fire(db, board, entry, id, verb, { roles: [verb === 'adopt' ? 'manager' : 'platform'], payload: { lock: claimer() } }); }
1463
1598
  const runId = getTask(db, id).current_run_id;
1464
1599
  if (!runId) throw new Error(`${id} waits on parents that are not done`);
1465
- const session = { surface: found.pane ? 'pane' : 'native', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now() };
1600
+ const session = { surface: found.pane ? 'pane' : 'native', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now(),
1601
+ ...(agent ? { agent_id: agent.identity.id, agent_org: team.org, agent_context: context ?? null,
1602
+ agent_profile_intent: { ...structuredClone(agent.intended.resolved), worker: { ...agent.intended.resolved?.worker, harness: lane.harness, model: lane.model ?? null, cwd: dir ?? task.workspace_path ?? null } } } : {}) };
1466
1603
  setRunSession(db, runId, { ...(currentRun(db, id)?.metadata?.supercode ?? {}), ...session, slot: currentRun(db, id)?.metadata?.supercode?.slot ?? beforeSession?.slot ?? expected?.session ?? 'worker', close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null, missing_since: null });
1467
1604
  event(db, id, 'session_adopted', { session_id: sessionId, machine: host, pane: session.pane, address: found.address, mode: 'adopt', role: currentRun(db, id)?.metadata?.supercode?.role }, runId);
1468
1605
  subscribeIn(db, id, found.address, 'worker');
1469
1606
  for (const p of db.prepare('SELECT parent_id FROM task_links WHERE child_id = ?').all(id)) subscribeIn(db, p.parent_id, found.address, 'dependency');
1470
- return { ...session, workspace: dir };
1607
+ return { ...session, run: runId, workspace: dir };
1471
1608
  });
1609
+ if (agent) recordAgentRun(board.root, team.org, agent.identity.id, { session: `${harness}:${sessionId}`, machine: host, run: adopted.run });
1610
+ return adopted;
1611
+ }
1612
+
1613
+ /** A manager/creator's manual native resume rebinds its existing run; this opens no process. */
1614
+ function reconnectContinuedSessions(board, fleet, entry, report) {
1615
+ if (!monitorsRuns(entry) || !fleet.ok) return;
1616
+ const rows = view(board, db => db.prepare(`SELECT r.* FROM task_runs r JOIN tasks t ON t.id=r.task_id
1617
+ WHERE t.current_run_id IS NULL AND t.status NOT IN ('done','archived') AND r.id=(SELECT MAX(id) FROM task_runs WHERE task_id=t.id)
1618
+ AND r.ended_at IS NOT NULL`).all());
1619
+ const panes = new Map();
1620
+ for (const row of rows) {
1621
+ const saved = json(row.metadata,{})?.supercode;
1622
+ if (!saved?.session_id || saved.forgotten || saved.close_transferred_to) continue;
1623
+ if (!saved.monitor_held && !['crashed','closed','reclaimed','failed'].includes(row.outcome)) continue;
1624
+ const live = fleet.sessions?.find(s => s.address === addressOf(saved) && s.state === 'running' && s.pane);
1625
+ if (!live) continue;
1626
+ if (!panes.has(saved.machine)) {
1627
+ const answer = doorOn(saved.machine,'harness.v1.panes.list',{}, {timeout:60000});
1628
+ const list = answer.code === 0 ? json(answer.stdout,null) : null;
1629
+ panes.set(saved.machine,Array.isArray(list) ? new Set(list.filter(p=>!p.dead).map(p=>p.pane)) : null);
1630
+ }
1631
+ const pane = live.pane;
1632
+ if (!pane || !panes.get(saved.machine)?.has(pane)) continue;
1633
+ // A surviving timed-out process is not a manual restart. It needs a new pane.
1634
+ if (pane === saved.pane && !saved.closed_at && !['crashed','closed','reclaimed'].includes(row.outcome)) continue;
1635
+ tx(board, db => {
1636
+ const task = getTask(db,row.task_id), scope = scopeOf(db,board,task,{params:entry.params});
1637
+ const automaticPause = scope.card.paused?.reason?.startsWith('its session ended cleanly (');
1638
+ if (task.current_run_id || (scope.card.paused && !automaticPause) || scope.card.blocked_by.length || scope.card.archived || statusOf(db,task) !== saved.status) return;
1639
+ if (automaticPause) db.prepare('UPDATE supercode_cards SET paused=NULL WHERE task_id=?').run(row.task_id);
1640
+ const session = { ...saved,pane,mode:'manual-continuation',state:entry.workflow.run?.initial ?? 'active',monitor_held:false,
1641
+ prior_intervals:[...(saved.prior_intervals??[]),{ended_at:row.ended_at,outcome:row.outcome,error:row.error}],
1642
+ launch_pending:false,launch_deferred:false,close_requested:null,closed_at:null,close_confirmed_at:null,close_error:null,missing_since:null,seen_at:now(),launch_key:null,
1643
+ stopped_seconds:Number(saved.stopped_seconds??0) + Math.max(0,now()-row.ended_at) };
1644
+ // Preserve the ended interval as evidence before reopening supervision on the same run.
1645
+ event(db,row.task_id,'session_continued',{session_id:saved.session_id,from_pane:saved.pane,pane,prior_outcome:row.outcome,prior_ended_at:row.ended_at,incident:saved.incident??null},row.id);
1646
+ db.prepare("UPDATE task_runs SET ended_at=NULL,status='running',outcome=NULL,error=NULL,metadata=?,claim_lock=?,claim_expires=? WHERE id=?")
1647
+ .run(JSON.stringify({...json(row.metadata,{}),supercode:session}),claimer(),now()+900,row.id);
1648
+ db.prepare('UPDATE tasks SET current_run_id=?,claim_lock=?,claim_expires=? WHERE id=?').run(row.id,claimer(),now()+900,row.task_id);
1649
+ projectStatus(db,board,entry,row.task_id);
1650
+ event(db,row.task_id,'dispatch_released',{reason:'verified manual continuation',session:addressOf(session),incident:saved.incident??null});
1651
+ subscribeIn(db,row.task_id,addressOf(session),'worker');
1652
+ for (const sub of db.prepare('SELECT task_id FROM supercode_cards WHERE subtask_of=?').all(row.task_id)) subscribeIn(db,sub.task_id,addressOf(session),'worker');
1653
+ (report.continued ??= []).push([row.task_id,addressOf(session),pane]);
1654
+ });
1655
+ }
1472
1656
  }
1473
1657
 
1474
1658
  // ------------------------------------------------------------------ notify
@@ -1599,12 +1783,6 @@ async function notify(board, fleet, report, entry) {
1599
1783
  // a worker whose run has ended is not woken by a notice (a second session would start working the card beside
1600
1784
  // the one the next run gets): the notice is filed in its mailbox, read when it next works
1601
1785
  const working = role !== 'worker' || view(board, (db) => addressOf(currentRun(db, sub.task_id)?.metadata?.supercode) === sub.chat_id);
1602
- // An idle Claude session cannot hold a notice in its mailbox (it starts a turn for every message it is given), so
1603
- // a notice that must not wake it waits, unsent, for the session's next work.
1604
- if (!working && fleet.ok && fleet.sessions.some((s) => s.address === sub.chat_id && s.harness === 'claude-code' && s.status === 'idle')) {
1605
- report.notices_waiting = (report.notices_waiting ?? 0) + 1;
1606
- return;
1607
- }
1608
1786
  let cursor = Number(sub.last_event_id);
1609
1787
  let failed = null;
1610
1788
  try {
@@ -1623,7 +1801,7 @@ async function notify(board, fleet, report, entry) {
1623
1801
  } else text = noticeOf(entry, role, ev.kind, task, p);
1624
1802
  if (text) {
1625
1803
  const body = ['worker', 'dependency'].includes(role) ? identifyNotice(ev, text) : text;
1626
- const receipt = await send(board, from, board.label, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { queue: !working || (!entry.workflow.run && sub.delivery_mode === 'notify'), subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
1804
+ const receipt = await send(board, from, board.label, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { queue: String(sub.chat_id).includes(':claude-code:') ? false : !working || (!entry.workflow.run && sub.delivery_mode === 'notify'), subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
1627
1805
  // A notice the recipient's mailbox took is filed: the machine delivers it at the session's next turn, so the
1628
1806
  // board does not send it again.
1629
1807
  if (!receipt.delivered && !receipt.queued) throw new Error(receipt.reason ?? 'notice was not filed');
@@ -1873,6 +2051,7 @@ const CORRUPT = /malformed|not a database|corrupt|disk image/i;
1873
2051
 
1874
2052
  /** What a tick would start on each board of the home, planned through the workflow's `dispatch`, writing nothing. */
1875
2053
  export async function plan({ root, lanes, settings, slugs = null, params = {} }) {
2054
+ eventFleetRoot = root;
1876
2055
  const loaded = await loadWorkflow(root, { fresh: true });
1877
2056
  const entry = { ...loaded, params: { ...loaded.params, ...params } };
1878
2057
  const fleet = Object.values(lanes).some((l) => l.surface === 'pane') ? observeFleet() : { ok: false, alive: new Set(), online: new Set(), sessions: [], error: 'no pane lanes' };
@@ -1901,8 +2080,8 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
1901
2080
  function noCodeReviewDue(board, id) {
1902
2081
  return view(board, (db) => {
1903
2082
  if (!db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id)) return false;
1904
- const last = db.prepare("SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('review_requested', 'changes_requested', 'review_pass') ORDER BY id DESC LIMIT 1").get(id);
1905
- return !last || last.kind === 'changes_requested';
2083
+ const last = db.prepare("SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('review_requested', 'changes_requested', 'review_pass','review_voided') ORDER BY id DESC LIMIT 1").get(id);
2084
+ return !last || ['changes_requested','review_voided'].includes(last.kind);
1906
2085
  });
1907
2086
  }
1908
2087
 
@@ -1978,6 +2157,7 @@ function capMax(cap, group, entry, lanes) {
1978
2157
  * version-checked card claims; no dispatcher file lock. Answers one report per board.
1979
2158
  */
1980
2159
  export async function tick({ root, lanes, settings, only = null, slugs = null, dispatch = true, params = {}, deploymentReceipt = false }) {
2160
+ eventFleetRoot = root;
1981
2161
  // a verb's flags (dispatch --max, --failure-limit) are params of this tick, read by the workflow as its own
1982
2162
  const deployment = deploymentReceipt ? workflowRevision(root) : null;
1983
2163
  const loaded = await loadWorkflow(root, { fresh: true });
@@ -2008,16 +2188,14 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2008
2188
  // where the tick's time goes, phase by phase (ms), printed by `serve --verbose` and in `--json`
2009
2189
  let mark = Date.now();
2010
2190
  const phase = (name) => { const t = Date.now(); report.ms[name] = t - mark; mark = t; };
2011
- if (!fleet.machineEvents?.subscriptions?.includes(boardAddress(board.label))) {
2012
- const subscription = sc(['teams','machines','subscribe',boardAddress(board.label),'--json']);
2013
- if (subscription.code !== 0) report.failed.push(['machine_events',(subscription.stderr || subscription.stdout).trim().slice(-200)]);
2014
- }
2191
+
2015
2192
  readMailbox(board, boardAddress(board.label), report); phase('mailbox');
2016
2193
  await refreshRelationAnswers(board); phase('answers');
2017
2194
  const effects = [];
2018
- completeAdoptions(board, fleet, settings, report); reconcileLaunches(board, fleet, report, entry, effects, settings, lanes); supervise(board, fleet, settings, report, entry, effects); await runEffects(board, effects.splice(0), { retryNotices: true }); closeRequested(board, report); phase('supervise');
2195
+ completeAdoptions(board, fleet, settings, report, lanes); reconnectContinuedSessions(board, fleet, entry, report); reconcileLaunches(board, fleet, report, entry, effects, settings, lanes); supervise(board, fleet, settings, report, entry, effects); await runEffects(board, effects.splice(0), { retryNotices: true }); closeRequested(board, report); phase('supervise');
2019
2196
  report.unsettled = [];
2020
2197
  report.promoted = recomputeReady(board, effects, report.unsettled);
2198
+ tx(board,db=>auditEndedRuns(db,board,entry));
2021
2199
  if (dispatch) await activatePullRequestReviews(board, entry, report, effects, { only });
2022
2200
  await sendBoardMessages(board, report);
2023
2201
  if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });