@volter/supercode-orchestrator 0.4.159 → 0.5.1

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,12 +133,31 @@ 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) {
149
156
  if (offlineMachine(machine)) throw unavailable(`native liveness unavailable: machine ${normalMachine(machine)} is offline`);
150
- const result = sc(['discover', '--on', machine, '--harness', harness, '--limit', '2000'], { timeout: 120_000 });
157
+ const args = normalMachine(machine) === localMachine()
158
+ ? ['teams', 'rpc', 'sessions.discover', JSON.stringify({ harnesses: [harness], limit: 2000 })]
159
+ : ['discover', '--on', machine, '--harness', harness, '--limit', '2000'];
160
+ const result = sc(args, { timeout: 120_000 });
151
161
  const page = result.code === 0 ? lastJson(result.stdout) : null;
152
162
  if (!Array.isArray(page?.sessions)) throw unavailable(`native liveness unavailable on ${machine}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
153
163
  return page.sessions;
@@ -221,7 +231,6 @@ export function lanesOf(profiles = {}) {
221
231
  return out;
222
232
  }
223
233
 
224
- const onMachine = machine => normalMachine(machine ?? localMachine()) === localMachine() ? [] : ['--machine', machine];
225
234
  // The latest presence reading (observeFleet). A machine it reads as offline is not asked: each remote call would
226
235
  // wait out its timeout, and one offline machine held every close and launch on this one.
227
236
  let presenceReading = null;
@@ -229,45 +238,24 @@ const offlineMachine = (machine) => {
229
238
  const m = normalMachine(machine ?? localMachine());
230
239
  return m !== localMachine() && presenceReading?.known === true && !presenceReading.online.has(m);
231
240
  };
232
- /** `supercode <args>` for `machine`: on another machine through its Teams server, answered at once when it is offline. */
233
- 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)
234
243
  ? { code: 1, stdout: '', stderr: `machine ${normalMachine(machine)} is offline` }
235
- : sc([...args, ...onMachine(machine)], options);
244
+ : runtimeDoor(normalMachine(machine ?? localMachine()) === localMachine() ? null : machine, method, params, options);
236
245
  const unavailable = message => Object.assign(new Error(message), { launchDeferred: true });
237
246
 
238
247
  // ------------------------------------------------------------------ supervise
239
248
 
240
249
  function supervise(board, fleet, settings, report, entry, effects) {
241
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();
242
- // The catalog can lag a pane. Read the actual machines once, before any board mutation.
243
- 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.
244
253
  const exits = new Map();
245
- const native = new Map(); fleet.native = new Map();
246
254
  for (const { id } of rows) {
247
255
  const session = currentRun(board.db, id)?.metadata?.supercode;
248
- if (!session?.session_id || session.pane || session.surface === 'headless') continue;
249
- const key = `${session.machine}:${session.harness}`;
250
- if (!native.has(key)) {
251
- try { native.set(key, nativeSessions(session.machine, session.harness)); }
252
- catch (error) { native.set(key, null); report.failed.push([id, error.message]); }
253
- }
254
- fleet.native.set(addressOf(session), native.get(key) ? nativeReading(native.get(key), session) : { alive: null });
255
- }
256
- for (const { id } of rows) {
257
- const s = currentRun(board.db, id)?.metadata?.supercode;
258
- if (!s?.pane || observed.has(s.machine)) continue;
259
- const r = scOn(s.machine, ['teams', 'panes', 'ls'], { timeout: 60_000 });
260
- const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
261
- observed.set(s.machine, Array.isArray(list) ? new Set(list.filter(p => !p.dead).map((p) => p.pane)) : null);
262
- if (!Array.isArray(list)) report.failed.push([id, `pane liveness unavailable; retaining run: ${(r.stderr || r.stdout).trim().slice(-200)}`]);
263
- }
264
- for (const {id} of rows) {
265
- const session=currentRun(board.db,id)?.metadata?.supercode;
266
- const panes=observed.get(session?.machine);
267
- if(session?.launch_key && panes instanceof Set && !panes.has(session.pane)) {
268
- try { const receipt=launchReceipt(board,session);if(receipt?.live===false&&receipt.exit)exits.set(id,receipt.exit); }
269
- catch(error){report.failed.push([id,`exit receipt unavailable; retaining liveness observation: ${error.message}`]);}
270
- }
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}`]); }
271
259
  }
272
260
  for (const { id } of rows) {
273
261
  tx(board, (db) => {
@@ -275,20 +263,20 @@ function supervise(board, fleet, settings, report, entry, effects) {
275
263
  if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
276
264
  const runRow = currentRun(db, id);
277
265
  let session = runRow?.metadata?.supercode ?? null;
278
- if (session?.pane) {
279
- let found;
280
- try { found = continuationPane(fleet, session, observed.get(session.machine)); }
281
- catch (error) { report.failed.push([id, error.message]); return; }
282
- if (found && found !== session.pane) {
283
- session = { ...session, pane: found, seen_at: now(), missing_since: null, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
284
- setRunSession(db, runRow.id, session);
285
- event(db, id, 'session_rebound', { session_id: session.session_id, pane: found }, runRow.id);
286
- }
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);
287
270
  }
288
271
  const t = now();
289
272
  // what the dispatcher saw becomes a platform event; the card's workflow says what it does
290
273
  const observe = (name, payload, bucket) => {
291
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
+ }
292
280
  const r = fire(db, board, entry, id, name, { roles: ['platform'], payload });
293
281
  effects.push(...r.effects);
294
282
  if (bucket) report[bucket].push(id);
@@ -316,8 +304,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
316
304
  // Availability is the same retained linked/unlinked feed subscribers hear.
317
305
  // A failed stream is unknown, not evidence that every remote machine left.
318
306
  if (session?.session_id && session.surface !== 'headless') {
319
- const panes = session.pane ? observed.get(session.machine) : null;
320
- const reachable = panes instanceof Set || fleet.online.has(session.machine);
307
+ const reachable = fleet.online.has(session.machine);
321
308
  if (!reachable && !fleet.presenceKnown) return;
322
309
  if (!reachable) {
323
310
  const next = {...session, offline_since: session.offline_since ?? t};
@@ -338,8 +325,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
338
325
  }
339
326
  }
340
327
  // Hermes's orphan reconciliation: a running card whose claim bookkeeping is broken, with nothing live behind it
341
- const paneReading = session?.pane ? observed.get(session.machine) : undefined;
342
- const paneLive = paneReading instanceof Set ? paneReading.has(session.pane) : null;
328
+ const paneLive = session?.session_id ? sessionAlive(fleet,session) : null;
343
329
  const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) !== false : true) : false;
344
330
  // (whether it is reconciled is the workflow's: its `orphaned` transitions and their guard)
345
331
  if ((!task.claim_lock || !task.claim_expires) && !live) {
@@ -375,6 +361,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
375
361
  // follows, and it is not a crash to restart
376
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 };
377
363
  if (!exit.failed) {
364
+ if (monitorsRuns(entry)) { observe('session_closed', {...ended,exit},'crashed'); return; }
378
365
  try {
379
366
  const r = fire(db, board, entry, id, 'session_closed', { roles: ['platform'], payload: ended });
380
367
  effects.push(...r.effects); (report.closed_by_hand ??= []).push(id);
@@ -384,11 +371,10 @@ function supervise(board, fleet, settings, report, entry, effects) {
384
371
  observe('exited', ended, 'crashed');
385
372
  return;
386
373
  }
387
- if (session.pane && paneLive === null) return; // no machine reading, no loss verdict
388
- if (!session.pane && !fleet.ok) return;
389
- const alive = session.pane ? paneLive : sessionAlive(fleet, session);
390
- if (alive == null) return; // native discovery was inconclusive
391
- 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);
392
378
  const young = t - Number(session.launched_at ?? 0) < Number(limits.grace ?? settings.grace);
393
379
  const missing = !alive && machineUp && !young;
394
380
  const next = { ...session };
@@ -447,7 +433,7 @@ function stopSession(board, session) {
447
433
  return;
448
434
  }
449
435
  if (session.pane) {
450
- 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)});
451
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)}`);
452
438
  }
453
439
  }
@@ -534,6 +520,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
534
520
  const stop = paused(board.root);
535
521
  if (stop) { report.paused = stop; return; }
536
522
  const { workflow, params } = entry;
523
+ if (!dry) tx(board, db => auditEndedRuns(db, board, entry));
537
524
  const d = workflow.dispatch;
538
525
  const pressure = memoryPressure();
539
526
  const tickState = { local_started: 0 };
@@ -630,7 +617,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
630
617
  } catch (error) {
631
618
  if (error.launchPending) { report.queued.push([task.id, error.message]); continue; }
632
619
  const payload = { error: String(error.message).slice(-500), ...(error.resumeFailed ? { resume_failed: true, ...error.resumeFailed } : {}) };
633
- 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 */ }
634
621
  report.failed.push([task.id, String(error.message).slice(-300)]);
635
622
  }
636
623
  }
@@ -653,7 +640,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
653
640
  const where = whereOf(board, task);
654
641
  // a subtask's run also sees its arc: the arc's text, tasks and thread (where its owner's rulings are)
655
642
  const arcContext = task.subtask_of ? view(board, (db) => cardContext(db, task.subtask_of)) : '';
656
- 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) });
657
644
  // the run's session record knows its status, role and slot (a reviewer's slot is never the implementer's); the slot
658
645
  // is a template, so a step can ask for isolation by naming a new one
659
646
  const slot = String(templateValue(spec.session ?? 'worker', scope) ?? 'worker');
@@ -665,12 +652,57 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
665
652
  // the model it runs is recorded with the session, for caps that count by model
666
653
  const meta = { status, role: spec.role, slot, model: task.model_override ?? lane.model ?? null, ...(spec.role === 'reviewer' ? { review_candidate: reviewCandidate(board, id) } : {}) };
667
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}`;}
668
682
  const result = surface === 'pane'
669
683
  ? launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
670
684
  : launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
671
- 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
+ }
672
697
  return result;
673
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
+ });
674
706
  if(error.launchDeferred) {
675
707
  tx(board,db=>{
676
708
  const previous=currentRun(db,id)?.metadata?.supercode;
@@ -685,22 +717,33 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
685
717
 
686
718
  /** Start one card now by a person's word (a triage card's session, a manager's start): the workflow's start event. */
687
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;
688
723
  const entry = await loadWorkflow(board.root);
689
724
  const fleet = observeFleet();
690
725
  const before = getTaskNow(board, id), checked = checkWorkspace(board, before);
691
726
  const moved = tx(board, (db) => {
692
727
  assertWorkspaceOwner(db, before, checked);
693
728
  if (checked) db.prepare('UPDATE supercode_cards SET workspace_root=? WHERE task_id=?').run(checked.mainRoot, id);
694
- 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.');
695
730
  event(db, id, 'dispatch_released', { reason: 'explicit start' });
696
- 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;
697
735
  });
698
736
  const task = getTaskNow(board, id);
699
737
  const spec = runSpec(entry, moved.to, task);
700
738
  const profile = spec ? view(board, (db) => evaluate(entry.workflow.roles?.[spec.role] ?? 'card.assignee', scopeOf(db, board, task, { params: entry.params }))) : null;
701
739
  const lane = profile ? lanes[profile] : null;
702
740
  if (spec && !lane) throw new Error(`${id} is ${moved.to}, run by ${spec.role} (${profile ?? 'nobody'}), which is no lane of this home`);
703
- 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
+ }
704
747
  }
705
748
 
706
749
  /** Reconcile durable close intent and ended runs, including old unverified closed_at records.
@@ -737,7 +780,9 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
737
780
  (report.closed ??= []).push({ task_id: row.task_id, ...session, receipt });
738
781
  continue;
739
782
  }
740
- 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 }; }
741
786
  else {
742
787
  const error = `close deferred: ${receiptError ?? `awaiting terminal launch receipt ${session.launch_key} (${receipt?.status ?? 'unknown'})`}`;
743
788
  tx(board, (db) => {
@@ -748,6 +793,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
748
793
  continue;
749
794
  }
750
795
  }
796
+ if (session?.monitor_held && !session.close_requested) continue; // only an explicit close action ends this process
751
797
  if ((!session?.pane && session?.surface !== 'native') || session.close_transferred_to || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
752
798
  if (caller && session.session_id === caller) continue; // let the command that ended its own run return
753
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());
@@ -766,7 +812,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
766
812
  }
767
813
  } else {
768
814
  if (!panes.has(machine)) {
769
- 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) });
770
816
  const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
771
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)}` });
772
818
  }
@@ -826,6 +872,9 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
826
872
  threshold_seconds: settings.idleQuiet, turns_before: Number(session.goal_turns ?? 0),
827
873
  };
828
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;
829
878
  const observation = { ...reading, decision, turns_after: reading.turns_before, ...extra };
830
879
  const patch = { idle_observation: observation };
831
880
  if (decision === 'below_threshold') patch.idle_below_threshold = observation;
@@ -993,9 +1042,9 @@ const getTaskNow = (board, id) => view(board, (db) => getTask(db, id));
993
1042
 
994
1043
  /** A transport error is not a negative launch receipt. The key remains owned by its run. */
995
1044
  function launchReceipt(board, session) {
996
- 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},
997
1046
  { timeout: 30_000, as: boardAddress(board.label) });
998
- 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)}`);
999
1048
  if (r.stdout.trim() === 'null') return null;
1000
1049
  const receipt = lastJson(r.stdout);
1001
1050
  if (!receipt || receipt.key !== session.launch_key) throw unavailable(`invalid launch receipt for ${session.launch_key} on ${session.machine}`);
@@ -1003,7 +1052,7 @@ function launchReceipt(board, session) {
1003
1052
  }
1004
1053
 
1005
1054
  function cancelUnissuedLaunch(board, session) {
1006
- 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},
1007
1056
  { timeout: 30_000, as: boardAddress(board.label) });
1008
1057
  if (result.code !== 0) throw unavailable(`launch cancellation unavailable on ${session.machine}; retain ${session.launch_key}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
1009
1058
  const receipt = lastJson(result.stdout);
@@ -1031,7 +1080,8 @@ function retireUnissuedLaunch(board, row, session, receipt, { entry = null, effe
1031
1080
  forgotten: saved.mode === 'fresh' ? true : Boolean(saved.forgotten) });
1032
1081
  event(db, row.task_id, 'session_launch_cancelled', { key: session.launch_key, machine: session.machine, receipt }, row.id);
1033
1082
  if (!current.ended_at) {
1034
- 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});
1035
1085
  collected.push(...moved.effects);
1036
1086
  }
1037
1087
  event(db, row.task_id, 'session_closed', { key: session.launch_key, machine: session.machine, reason: 'launch not issued', receipt }, row.id);
@@ -1062,6 +1112,7 @@ function bindLaunch(board, row, session, pane, sid) {
1062
1112
 
1063
1113
  /** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
1064
1114
  export function reconcileCallerLaunch(board, sid) {
1115
+ eventFleetRoot = board.root;
1065
1116
  if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
1066
1117
  WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
1067
1118
  const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
@@ -1086,7 +1137,63 @@ export function reconcileCallerLaunch(board, sid) {
1086
1137
  }
1087
1138
 
1088
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
+ }
1089
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
1090
1197
  for(const {id,started_at} of deferred) {
1091
1198
  if (now() - started_at >= settings.launchWindow) continue;
1092
1199
  try {
@@ -1098,7 +1205,7 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1098
1205
  if(session)report.started.push([id,session.address??session.session_id,session.mode]);
1099
1206
  }catch(error){
1100
1207
  if(error.launchPending){report.queued.push([id,error.message]);continue;}
1101
- 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);
1102
1209
  report.failed.push([id,error.message]);
1103
1210
  }
1104
1211
  }
@@ -1127,7 +1234,9 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1127
1234
  const moved = tx(board, (db) => {
1128
1235
  if (getTask(db, row.task_id)?.current_run_id !== row.id) return null;
1129
1236
  setRunSession(db, row.id, { launch_pending: false });
1130
- 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 });
1131
1240
  });
1132
1241
  if (moved) effects.push(...moved.effects);
1133
1242
  }
@@ -1150,11 +1259,13 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1150
1259
  task = { ...task, workspace_path: made };
1151
1260
  }
1152
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};
1153
1263
  const tail = model ? ['--model', model] : [];
1154
1264
  if (lane.harness === 'codex' && lane.reasoning_effort) tail.push('-c', `model_reasoning_effort=${JSON.stringify(lane.reasoning_effort)}`);
1155
1265
  const harness = HARNESSES[lane.harness] ? lane.harness : 'claude-code';
1156
1266
  const how = HARNESSES[harness];
1157
- 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}`])];
1158
1269
  // the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
1159
1270
  // a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
1160
1271
  // session starts (never two processes on one session)
@@ -1168,7 +1279,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1168
1279
  if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
1169
1280
  const fresh = observeFleet();
1170
1281
  if (!fresh.ok) throw unavailable(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
1171
- const r = scOn(machine, ['teams', 'panes', 'ls'], { timeout: 60_000 });
1282
+ const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
1172
1283
  const listed = r.code === 0 ? json(r.stdout.trim(), null) : null;
1173
1284
  if (!Array.isArray(listed)) throw unavailable(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1174
1285
  const pane = continuationPane(fresh, prev, new Set(listed.filter(p => !p.dead).map((p) => p.pane)));
@@ -1183,6 +1294,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1183
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'}`);
1184
1295
  }
1185
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`);
1186
1300
  launchReceipt(board, { machine, launch_key: key }); // Prove the exact receipt door before starting a process.
1187
1301
  tx(board, (db) => setRunSession(db, runId, { ...meta, surface: 'pane', machine, harness, slot,
1188
1302
  session_id: sid, mode, launch_key: key, launch_pending: true, launched_at: now() }));
@@ -1233,7 +1347,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1233
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`);
1234
1348
  if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
1235
1349
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
1236
- 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;
1237
1351
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
1238
1352
  // a kept session is told what this run is for (its card came back to it, with what happened since)
1239
1353
  tx(board, (db) => setRunSession(db, runId, { ...session, ...meta }));
@@ -1249,6 +1363,15 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1249
1363
  }
1250
1364
  }
1251
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`);
1252
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
1253
1376
  // its next round gets that round's prompt, its arc's context with it), not only that it was resumed
1254
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.`;
@@ -1301,7 +1424,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1301
1424
 
1302
1425
  /** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
1303
1426
  function panePresent(machine, pane) {
1304
- const r = scOn(machine, ['teams', 'panes', 'ls'], { timeout: 60_000 });
1427
+ const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
1305
1428
  const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
1306
1429
  if (!Array.isArray(panes)) throw unavailable(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1307
1430
  return panes.some((p) => p.pane === pane && !p.dead);
@@ -1316,6 +1439,7 @@ function paneSurvives(machine, pane) {
1316
1439
  /** Hermes's own worker argv and task environment, run on this machine; watched by its pid. */
1317
1440
  function launchHeadless(board, task, runId, lane, settings, { prompt = null, skills: runSkills = [], meta = {} } = {}) {
1318
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};
1319
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));
1320
1444
  // the card's own skills and the ones its status's run names (a reviewer's, for one)
1321
1445
  const skills = [...new Set([...task.skills, ...runSkills])];
@@ -1334,7 +1458,7 @@ function launchHeadless(board, task, runId, lane, settings, { prompt = null, ski
1334
1458
  env: {
1335
1459
  SUPERCODE_BOARD_EXIT_FILE: exitFile,
1336
1460
  ...(task.goal_mode ? { HERMES_KANBAN_GOAL_MODE: '1', ...(task.goal_max_turns ? { HERMES_KANBAN_GOAL_MAX_TURNS: String(task.goal_max_turns) } : {}) } : {}),
1337
- ...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,
1338
1462
  HERMES_KANBAN_RUN_ID: String(runId), HERMES_KANBAN_DB: board.path, HERMES_KANBAN_BOARD: board.slug, HERMES_SESSION_SOURCE: 'kanban',
1339
1463
  HERMES_KANBAN_CLAIM_LOCK: claimer(), ...(prompt ? { SUPERCODE_BOARD_PROMPT: prompt } : {}), ...(task.tenant ? { HERMES_TENANT: task.tenant } : {}),
1340
1464
  ...(task.branch_name ? { HERMES_KANBAN_BRANCH: task.branch_name } : {}),
@@ -1393,14 +1517,14 @@ export function requestAdoption(board, id, who, { workspace = null, assignee = n
1393
1517
  }
1394
1518
 
1395
1519
  /** Adoptions the manager requested and no tick has answered yet, completed from this tick's fleet reading. */
1396
- export function completeAdoptions(board, fleet, settings, report) {
1520
+ export function completeAdoptions(board, fleet, settings, report, lanes = null) {
1397
1521
  const pending = view(board, db => db.prepare(`SELECT e.task_id, e.payload FROM task_events e WHERE e.kind = 'dispatch_held'
1398
1522
  AND json_extract(e.payload, '$.reason') = 'adoption requested'
1399
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());
1400
1524
  for (const row of pending) {
1401
1525
  const asked = json(row.payload, {}) ?? {};
1402
1526
  try {
1403
- 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});
1404
1528
  (report.adopted ??= []).push([row.task_id, got.address]);
1405
1529
  } catch (error) {
1406
1530
  const message = String(error.message).slice(-400);
@@ -1410,7 +1534,7 @@ export function completeAdoptions(board, fleet, settings, report) {
1410
1534
  }
1411
1535
  }
1412
1536
 
1413
- 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 } = {}) {
1414
1538
  fleet ??= observeFleet();
1415
1539
  if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
1416
1540
  const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who);
@@ -1427,14 +1551,24 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
1427
1551
  // the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
1428
1552
  let dir = workspace ?? native?.row?.cwd;
1429
1553
  if (!dir) {
1430
- const r = sc(['discover', '--on', host, '--harness', harness, '--limit', '2000'], { timeout: 120_000 });
1431
- dir = (json(r.stdout.trim(), {})?.sessions ?? []).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
1554
+ dir = nativeSessions(host, harness).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
1432
1555
  }
1433
1556
  if (!dir && found.pane) {
1434
- 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 });
1435
1558
  dir = (json(r.stdout.trim(), []) ?? []).find((p) => p.pane === found.pane)?.cwd ?? null;
1436
1559
  }
1437
- 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) => {
1438
1572
  const task = getTask(db, id);
1439
1573
  if (!task) throw new Error(`no card ${id}`);
1440
1574
  if ((task.current_run_id ?? null) !== (beforeRun?.id ?? null)) throw new Error(`${id} changed runs during adoption; retry against its current run`);
@@ -1457,16 +1591,68 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
1457
1591
  if (implementer?.session_id === sessionId) throw new Error('a reviewer must be independent of the implementer session');
1458
1592
  }
1459
1593
  event(db, id, 'dispatch_released', { reason: 'verified adoption', session: found.address });
1460
- if (!beforeRun) fire(db, board, entry, id, entry.workflow.run ? entry.workflow.dispatch.start : 'adopt', { roles: ['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() } }); }
1461
1598
  const runId = getTask(db, id).current_run_id;
1462
1599
  if (!runId) throw new Error(`${id} waits on parents that are not done`);
1463
- 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 } } } : {}) };
1464
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 });
1465
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);
1466
1605
  subscribeIn(db, id, found.address, 'worker');
1467
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');
1468
- return { ...session, workspace: dir };
1607
+ return { ...session, run: runId, workspace: dir };
1469
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
+ }
1470
1656
  }
1471
1657
 
1472
1658
  // ------------------------------------------------------------------ notify
@@ -1597,12 +1783,6 @@ async function notify(board, fleet, report, entry) {
1597
1783
  // a worker whose run has ended is not woken by a notice (a second session would start working the card beside
1598
1784
  // the one the next run gets): the notice is filed in its mailbox, read when it next works
1599
1785
  const working = role !== 'worker' || view(board, (db) => addressOf(currentRun(db, sub.task_id)?.metadata?.supercode) === sub.chat_id);
1600
- // An idle Claude session cannot hold a notice in its mailbox (it starts a turn for every message it is given), so
1601
- // a notice that must not wake it waits, unsent, for the session's next work.
1602
- if (!working && fleet.ok && fleet.sessions.some((s) => s.address === sub.chat_id && s.harness === 'claude-code' && s.status === 'idle')) {
1603
- report.notices_waiting = (report.notices_waiting ?? 0) + 1;
1604
- return;
1605
- }
1606
1786
  let cursor = Number(sub.last_event_id);
1607
1787
  let failed = null;
1608
1788
  try {
@@ -1621,7 +1801,7 @@ async function notify(board, fleet, report, entry) {
1621
1801
  } else text = noticeOf(entry, role, ev.kind, task, p);
1622
1802
  if (text) {
1623
1803
  const body = ['worker', 'dependency'].includes(role) ? identifyNotice(ev, text) : text;
1624
- 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}` });
1625
1805
  // A notice the recipient's mailbox took is filed: the machine delivers it at the session's next turn, so the
1626
1806
  // board does not send it again.
1627
1807
  if (!receipt.delivered && !receipt.queued) throw new Error(receipt.reason ?? 'notice was not filed');
@@ -1871,6 +2051,7 @@ const CORRUPT = /malformed|not a database|corrupt|disk image/i;
1871
2051
 
1872
2052
  /** What a tick would start on each board of the home, planned through the workflow's `dispatch`, writing nothing. */
1873
2053
  export async function plan({ root, lanes, settings, slugs = null, params = {} }) {
2054
+ eventFleetRoot = root;
1874
2055
  const loaded = await loadWorkflow(root, { fresh: true });
1875
2056
  const entry = { ...loaded, params: { ...loaded.params, ...params } };
1876
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' };
@@ -1899,8 +2080,8 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
1899
2080
  function noCodeReviewDue(board, id) {
1900
2081
  return view(board, (db) => {
1901
2082
  if (!db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id)) return false;
1902
- 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);
1903
- 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);
1904
2085
  });
1905
2086
  }
1906
2087
 
@@ -1976,6 +2157,7 @@ function capMax(cap, group, entry, lanes) {
1976
2157
  * version-checked card claims; no dispatcher file lock. Answers one report per board.
1977
2158
  */
1978
2159
  export async function tick({ root, lanes, settings, only = null, slugs = null, dispatch = true, params = {}, deploymentReceipt = false }) {
2160
+ eventFleetRoot = root;
1979
2161
  // a verb's flags (dispatch --max, --failure-limit) are params of this tick, read by the workflow as its own
1980
2162
  const deployment = deploymentReceipt ? workflowRevision(root) : null;
1981
2163
  const loaded = await loadWorkflow(root, { fresh: true });
@@ -2006,16 +2188,14 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2006
2188
  // where the tick's time goes, phase by phase (ms), printed by `serve --verbose` and in `--json`
2007
2189
  let mark = Date.now();
2008
2190
  const phase = (name) => { const t = Date.now(); report.ms[name] = t - mark; mark = t; };
2009
- if (!fleet.machineEvents?.subscriptions?.includes(boardAddress(board.label))) {
2010
- const subscription = sc(['teams','machines','subscribe',boardAddress(board.label),'--json']);
2011
- if (subscription.code !== 0) report.failed.push(['machine_events',(subscription.stderr || subscription.stdout).trim().slice(-200)]);
2012
- }
2191
+
2013
2192
  readMailbox(board, boardAddress(board.label), report); phase('mailbox');
2014
2193
  await refreshRelationAnswers(board); phase('answers');
2015
2194
  const effects = [];
2016
- 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');
2017
2196
  report.unsettled = [];
2018
2197
  report.promoted = recomputeReady(board, effects, report.unsettled);
2198
+ tx(board,db=>auditEndedRuns(db,board,entry));
2019
2199
  if (dispatch) await activatePullRequestReviews(board, entry, report, effects, { only });
2020
2200
  await sendBoardMessages(board, report);
2021
2201
  if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });