agent-board-runner 0.1.19 → 0.1.21

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.
Files changed (3) hide show
  1. package/dist/cli.js +133 -41
  2. package/dist/index.js +133 -41
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2666,30 +2666,14 @@ import { randomUUID as randomUUID7 } from "node:crypto";
2666
2666
  import { randomUUID as randomUUID6 } from "node:crypto";
2667
2667
  import { spawn as spawn3 } from "node-pty";
2668
2668
 
2669
- // src/sessionName.ts
2670
- var MAX_LENGTH = 60;
2671
- var FALLBACK_NAME = "card";
2672
- function toSessionName(title) {
2673
- const slug = title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2674
- if (slug.length === 0) return FALLBACK_NAME;
2675
- if (slug.length <= MAX_LENGTH) return slug;
2676
- const truncated = slug.slice(0, MAX_LENGTH);
2677
- const lastHyphen = truncated.lastIndexOf("-");
2678
- return (lastHyphen > 0 ? truncated.slice(0, lastHyphen) : truncated).replace(/-+$/, "");
2679
- }
2680
-
2681
- // src/agentSession.ts
2669
+ // src/ptyMessaging.ts
2682
2670
  var TUI_IDLE_MS = 1500;
2683
2671
  var TUI_READY_TIMEOUT_MS = 6e4;
2684
2672
  var PASTE_SETTLE_MS = 200;
2685
2673
  var PASTE_SETTLE_TIMEOUT_MS = 5e3;
2686
2674
  var PASTE_START = "\x1B[200~";
2687
2675
  var PASTE_END = "\x1B[201~";
2688
- var SHIFT_TAB = "\x1B[Z";
2689
- var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2690
- var SHIFT_TAB_DELAY_MS = 400;
2691
- var PLAN_MODE_INDICATOR = /plan mode on/i;
2692
- function writePasteAndSubmit(pty, cardId, message, context) {
2676
+ function writePasteAndSubmit(pty, logId, message, context, log) {
2693
2677
  pty.write(`${PASTE_START}${message}${PASTE_END}`);
2694
2678
  let settleTimer;
2695
2679
  let ceilingTimer;
@@ -2706,7 +2690,7 @@ function writePasteAndSubmit(pty, cardId, message, context) {
2706
2690
  try {
2707
2691
  pty.write("\r");
2708
2692
  } catch (err) {
2709
- logger.card.error(cardId, `Failed to submit ${context} to the agent:`, err);
2693
+ log.error(logId, `Failed to submit ${context} to the agent:`, err);
2710
2694
  }
2711
2695
  };
2712
2696
  const cancel = () => {
@@ -2724,6 +2708,48 @@ function writePasteAndSubmit(pty, cardId, message, context) {
2724
2708
  ceilingTimer = setTimeout(submit, PASTE_SETTLE_TIMEOUT_MS);
2725
2709
  exitSubscription = pty.onExit(cancel);
2726
2710
  }
2711
+ function waitForIdleThenRun(pty, run) {
2712
+ let idleTimer;
2713
+ let deadlineTimer;
2714
+ let dataSubscription;
2715
+ let settled = false;
2716
+ const stopWaiting = () => {
2717
+ if (settled) return false;
2718
+ settled = true;
2719
+ clearTimeout(idleTimer);
2720
+ clearTimeout(deadlineTimer);
2721
+ dataSubscription?.dispose();
2722
+ return true;
2723
+ };
2724
+ const fire = () => {
2725
+ if (!stopWaiting()) return;
2726
+ void run();
2727
+ };
2728
+ deadlineTimer = setTimeout(fire, TUI_READY_TIMEOUT_MS);
2729
+ dataSubscription = pty.onData(() => {
2730
+ clearTimeout(idleTimer);
2731
+ idleTimer = setTimeout(fire, TUI_IDLE_MS);
2732
+ });
2733
+ pty.onExit(() => stopWaiting());
2734
+ }
2735
+
2736
+ // src/sessionName.ts
2737
+ var MAX_LENGTH = 60;
2738
+ var FALLBACK_NAME = "card";
2739
+ function toSessionName(title) {
2740
+ const slug = title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2741
+ if (slug.length === 0) return FALLBACK_NAME;
2742
+ if (slug.length <= MAX_LENGTH) return slug;
2743
+ const truncated = slug.slice(0, MAX_LENGTH);
2744
+ const lastHyphen = truncated.lastIndexOf("-");
2745
+ return (lastHyphen > 0 ? truncated.slice(0, lastHyphen) : truncated).replace(/-+$/, "");
2746
+ }
2747
+
2748
+ // src/agentSession.ts
2749
+ var SHIFT_TAB = "\x1B[Z";
2750
+ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2751
+ var SHIFT_TAB_DELAY_MS = 400;
2752
+ var PLAN_MODE_INDICATOR = /plan mode on/i;
2727
2753
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2728
2754
  ensureDirTrusted(repoPath);
2729
2755
  ensureMcpServersTrusted(repoPath);
@@ -2751,33 +2777,14 @@ function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, tic
2751
2777
  return { pty, claudeSessionId };
2752
2778
  }
2753
2779
  function sendMessageWhenIdle(pty, cardId, message, planFirst) {
2754
- let idleTimer;
2755
- let deadlineTimer;
2756
- let dataSubscription;
2757
- let settled = false;
2758
- const stopWaiting = () => {
2759
- if (settled) return false;
2760
- settled = true;
2761
- clearTimeout(idleTimer);
2762
- clearTimeout(deadlineTimer);
2763
- dataSubscription?.dispose();
2764
- return true;
2765
- };
2766
- const send = async () => {
2767
- if (!stopWaiting()) return;
2780
+ waitForIdleThenRun(pty, async () => {
2768
2781
  if (planFirst) await enterPlanMode(pty, cardId);
2769
2782
  try {
2770
- writePasteAndSubmit(pty, cardId, message, "the card instructions");
2783
+ writePasteAndSubmit(pty, cardId, message, "the card instructions", logger.card);
2771
2784
  } catch (err) {
2772
2785
  logger.card.error(cardId, "Failed to send the card instructions to the agent:", err);
2773
2786
  }
2774
- };
2775
- deadlineTimer = setTimeout(send, TUI_READY_TIMEOUT_MS);
2776
- dataSubscription = pty.onData(() => {
2777
- clearTimeout(idleTimer);
2778
- idleTimer = setTimeout(send, TUI_IDLE_MS);
2779
2787
  });
2780
- pty.onExit(() => stopWaiting());
2781
2788
  }
2782
2789
  async function enterPlanMode(pty, cardId) {
2783
2790
  let recentOutput = "";
@@ -2803,7 +2810,7 @@ async function enterPlanMode(pty, cardId) {
2803
2810
  }
2804
2811
  function sendMessageToIdleSession(pty, cardId, message) {
2805
2812
  try {
2806
- writePasteAndSubmit(pty, cardId, message, "a message");
2813
+ writePasteAndSubmit(pty, cardId, message, "a message", logger.card);
2807
2814
  } catch (err) {
2808
2815
  logger.card.error(cardId, "Failed to send a message to the idle session:", err);
2809
2816
  }
@@ -4327,6 +4334,87 @@ async function stopTerminalTunnel(supabase, config) {
4327
4334
  await publishHostname(supabase, config, null);
4328
4335
  }
4329
4336
 
4337
+ // src/workflowShell.ts
4338
+ import { randomUUID as randomUUID8 } from "node:crypto";
4339
+ import { spawn as spawn6 } from "node-pty";
4340
+ function registerWorkflowShellRpcHandler(supabase, config) {
4341
+ registerRunnerRpcHandler("open_workflow_shell", (payload) => openWorkflowShell(supabase, config, payload));
4342
+ }
4343
+ function isOpenWorkflowShellPayload(value) {
4344
+ if (!value || typeof value !== "object") return false;
4345
+ const candidate = value;
4346
+ return typeof candidate.projectId === "string" && typeof candidate.executionId === "string" && typeof candidate.instructions === "string" && candidate.instructions.length > 0;
4347
+ }
4348
+ async function openWorkflowShell(supabase, config, payload) {
4349
+ if (!isOpenWorkflowShellPayload(payload)) throw new Error("Malformed open_workflow_shell payload.");
4350
+ const { projectId, executionId, instructions } = payload;
4351
+ const registered = config.registeredProjects.find((project) => project.id === projectId);
4352
+ if (!registered) throw new Error("This runner does not have that project registered.");
4353
+ ensureDirTrusted(registered.repoPath);
4354
+ ensureMcpServersTrusted(registered.repoPath);
4355
+ const claudeSessionId = randomUUID8();
4356
+ const shellTicket = randomUUID8();
4357
+ const pty = spawn6(
4358
+ CLAUDE_BINARY,
4359
+ [
4360
+ "--dangerously-skip-permissions",
4361
+ "--model",
4362
+ "claude-sonnet-5",
4363
+ "--session-id",
4364
+ claudeSessionId,
4365
+ ...claudeCliSettingsArgs()
4366
+ ],
4367
+ {
4368
+ name: "xterm-color",
4369
+ cols: 120,
4370
+ rows: 32,
4371
+ cwd: registered.repoPath,
4372
+ env: statuslineEnv(executionId, shellTicket)
4373
+ }
4374
+ );
4375
+ registerPtySession(executionId, pty, shellTicket);
4376
+ pty.onExit(({ exitCode }) => void handleWorkflowShellExit(supabase, executionId, exitCode));
4377
+ waitForIdleThenRun(pty, () => {
4378
+ try {
4379
+ writePasteAndSubmit(pty, executionId, instructions, "the workflow runbook", logger.runner);
4380
+ } catch (err) {
4381
+ logger.runner.error(executionId, "Failed to send the workflow runbook to the agent:", err);
4382
+ }
4383
+ });
4384
+ return { claudeSessionId, shellTicket };
4385
+ }
4386
+ async function handleWorkflowShellExit(supabase, executionId, exitCode) {
4387
+ const status = exitCode === 0 ? "done" : "failed";
4388
+ const { error } = await supabase.rpc("complete_workflow_execution", { p_execution_id: executionId, p_status: status });
4389
+ if (error) {
4390
+ logger.runner.error("WORKFLOW_SHELL", `Failed to record exit status for workflow execution ${executionId}:`, error);
4391
+ }
4392
+ }
4393
+ async function reconcileOrphanedWorkflowExecutions(supabase, config) {
4394
+ const { data, error } = await supabase.from("workflow_executions").select("id, project_id, claude_session_id, shell_ticket").eq("runner_id", config.runnerId).in("status", ["running", "paused_on_question"]);
4395
+ if (error) {
4396
+ logger.runner.error("WORKFLOW_SHELL", "Failed to scan orphaned workflow executions:", error.message);
4397
+ return;
4398
+ }
4399
+ for (const execution of data ?? []) {
4400
+ const { id: executionId, project_id: projectId, claude_session_id: claudeSessionId, shell_ticket: shellTicket } = execution;
4401
+ if (getPtySession(executionId)) continue;
4402
+ if (!claudeSessionId || !shellTicket) {
4403
+ logger.runner.warn("WORKFLOW_SHELL", `Workflow execution ${executionId} is missing claude_session_id/shell_ticket \u2014 cannot resume it.`);
4404
+ continue;
4405
+ }
4406
+ const registered = config.registeredProjects.find((project) => project.id === projectId);
4407
+ if (!registered) {
4408
+ logger.runner.warn("WORKFLOW_SHELL", `Workflow execution ${executionId}: project ${projectId} is not registered on this runner \u2014 skipping.`);
4409
+ continue;
4410
+ }
4411
+ const pty = resumeAgentPty(registered.repoPath, claudeSessionId, executionId, shellTicket);
4412
+ registerPtySession(executionId, pty, shellTicket);
4413
+ pty.onExit(({ exitCode }) => void handleWorkflowShellExit(supabase, executionId, exitCode));
4414
+ logger.runner.info("WORKFLOW_SHELL", `Re-spawned orphaned workflow execution ${executionId} (runner restarted while it was still active).`);
4415
+ }
4416
+ }
4417
+
4330
4418
  // src/runner.ts
4331
4419
  async function startRunner() {
4332
4420
  const loadedConfig = loadConfig();
@@ -4403,6 +4491,9 @@ async function startRunner() {
4403
4491
  await reconcileStaleSessions(supabase, config, project.id).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileStaleSessions failed:", err));
4404
4492
  }
4405
4493
  await reconcileOrphanedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileOrphanedSessions failed:", err));
4494
+ await reconcileOrphanedWorkflowExecutions(supabase, config).catch(
4495
+ (err) => logger.runner.error("WORKFLOW_SHELL", "reconcileOrphanedWorkflowExecutions failed:", err)
4496
+ );
4406
4497
  await reconcileUsageLimitedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileUsageLimitedSessions failed:", err));
4407
4498
  const terminalPort = Number(process.env.AGENT_BOARD_TERMINAL_PORT ?? 8787);
4408
4499
  ensureStatuslineScriptWritten();
@@ -4413,6 +4504,7 @@ async function startRunner() {
4413
4504
  const agentEventsWss = attachAgentEventsServer();
4414
4505
  const runnerRpcWss = attachRunnerRpcServer();
4415
4506
  registerProjectCommandRpcHandler(supabase, config);
4507
+ registerWorkflowShellRpcHandler(supabase, config);
4416
4508
  attachFilesServer(httpServer, supabase, config);
4417
4509
  httpServer.on("upgrade", (request, socket, head) => {
4418
4510
  const pathname = (request.url ?? "").split("?")[0];
package/dist/index.js CHANGED
@@ -2554,30 +2554,14 @@ import { randomUUID as randomUUID7 } from "node:crypto";
2554
2554
  import { randomUUID as randomUUID6 } from "node:crypto";
2555
2555
  import { spawn as spawn3 } from "node-pty";
2556
2556
 
2557
- // src/sessionName.ts
2558
- var MAX_LENGTH = 60;
2559
- var FALLBACK_NAME = "card";
2560
- function toSessionName(title) {
2561
- const slug = title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2562
- if (slug.length === 0) return FALLBACK_NAME;
2563
- if (slug.length <= MAX_LENGTH) return slug;
2564
- const truncated = slug.slice(0, MAX_LENGTH);
2565
- const lastHyphen = truncated.lastIndexOf("-");
2566
- return (lastHyphen > 0 ? truncated.slice(0, lastHyphen) : truncated).replace(/-+$/, "");
2567
- }
2568
-
2569
- // src/agentSession.ts
2557
+ // src/ptyMessaging.ts
2570
2558
  var TUI_IDLE_MS = 1500;
2571
2559
  var TUI_READY_TIMEOUT_MS = 6e4;
2572
2560
  var PASTE_SETTLE_MS = 200;
2573
2561
  var PASTE_SETTLE_TIMEOUT_MS = 5e3;
2574
2562
  var PASTE_START = "\x1B[200~";
2575
2563
  var PASTE_END = "\x1B[201~";
2576
- var SHIFT_TAB = "\x1B[Z";
2577
- var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2578
- var SHIFT_TAB_DELAY_MS = 400;
2579
- var PLAN_MODE_INDICATOR = /plan mode on/i;
2580
- function writePasteAndSubmit(pty, cardId, message, context) {
2564
+ function writePasteAndSubmit(pty, logId, message, context, log) {
2581
2565
  pty.write(`${PASTE_START}${message}${PASTE_END}`);
2582
2566
  let settleTimer;
2583
2567
  let ceilingTimer;
@@ -2594,7 +2578,7 @@ function writePasteAndSubmit(pty, cardId, message, context) {
2594
2578
  try {
2595
2579
  pty.write("\r");
2596
2580
  } catch (err) {
2597
- logger.card.error(cardId, `Failed to submit ${context} to the agent:`, err);
2581
+ log.error(logId, `Failed to submit ${context} to the agent:`, err);
2598
2582
  }
2599
2583
  };
2600
2584
  const cancel = () => {
@@ -2612,6 +2596,48 @@ function writePasteAndSubmit(pty, cardId, message, context) {
2612
2596
  ceilingTimer = setTimeout(submit, PASTE_SETTLE_TIMEOUT_MS);
2613
2597
  exitSubscription = pty.onExit(cancel);
2614
2598
  }
2599
+ function waitForIdleThenRun(pty, run) {
2600
+ let idleTimer;
2601
+ let deadlineTimer;
2602
+ let dataSubscription;
2603
+ let settled = false;
2604
+ const stopWaiting = () => {
2605
+ if (settled) return false;
2606
+ settled = true;
2607
+ clearTimeout(idleTimer);
2608
+ clearTimeout(deadlineTimer);
2609
+ dataSubscription?.dispose();
2610
+ return true;
2611
+ };
2612
+ const fire = () => {
2613
+ if (!stopWaiting()) return;
2614
+ void run();
2615
+ };
2616
+ deadlineTimer = setTimeout(fire, TUI_READY_TIMEOUT_MS);
2617
+ dataSubscription = pty.onData(() => {
2618
+ clearTimeout(idleTimer);
2619
+ idleTimer = setTimeout(fire, TUI_IDLE_MS);
2620
+ });
2621
+ pty.onExit(() => stopWaiting());
2622
+ }
2623
+
2624
+ // src/sessionName.ts
2625
+ var MAX_LENGTH = 60;
2626
+ var FALLBACK_NAME = "card";
2627
+ function toSessionName(title) {
2628
+ const slug = title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2629
+ if (slug.length === 0) return FALLBACK_NAME;
2630
+ if (slug.length <= MAX_LENGTH) return slug;
2631
+ const truncated = slug.slice(0, MAX_LENGTH);
2632
+ const lastHyphen = truncated.lastIndexOf("-");
2633
+ return (lastHyphen > 0 ? truncated.slice(0, lastHyphen) : truncated).replace(/-+$/, "");
2634
+ }
2635
+
2636
+ // src/agentSession.ts
2637
+ var SHIFT_TAB = "\x1B[Z";
2638
+ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2639
+ var SHIFT_TAB_DELAY_MS = 400;
2640
+ var PLAN_MODE_INDICATOR = /plan mode on/i;
2615
2641
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2616
2642
  ensureDirTrusted(repoPath);
2617
2643
  ensureMcpServersTrusted(repoPath);
@@ -2639,33 +2665,14 @@ function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, tic
2639
2665
  return { pty, claudeSessionId };
2640
2666
  }
2641
2667
  function sendMessageWhenIdle(pty, cardId, message, planFirst) {
2642
- let idleTimer;
2643
- let deadlineTimer;
2644
- let dataSubscription;
2645
- let settled = false;
2646
- const stopWaiting = () => {
2647
- if (settled) return false;
2648
- settled = true;
2649
- clearTimeout(idleTimer);
2650
- clearTimeout(deadlineTimer);
2651
- dataSubscription?.dispose();
2652
- return true;
2653
- };
2654
- const send = async () => {
2655
- if (!stopWaiting()) return;
2668
+ waitForIdleThenRun(pty, async () => {
2656
2669
  if (planFirst) await enterPlanMode(pty, cardId);
2657
2670
  try {
2658
- writePasteAndSubmit(pty, cardId, message, "the card instructions");
2671
+ writePasteAndSubmit(pty, cardId, message, "the card instructions", logger.card);
2659
2672
  } catch (err) {
2660
2673
  logger.card.error(cardId, "Failed to send the card instructions to the agent:", err);
2661
2674
  }
2662
- };
2663
- deadlineTimer = setTimeout(send, TUI_READY_TIMEOUT_MS);
2664
- dataSubscription = pty.onData(() => {
2665
- clearTimeout(idleTimer);
2666
- idleTimer = setTimeout(send, TUI_IDLE_MS);
2667
2675
  });
2668
- pty.onExit(() => stopWaiting());
2669
2676
  }
2670
2677
  async function enterPlanMode(pty, cardId) {
2671
2678
  let recentOutput = "";
@@ -2691,7 +2698,7 @@ async function enterPlanMode(pty, cardId) {
2691
2698
  }
2692
2699
  function sendMessageToIdleSession(pty, cardId, message) {
2693
2700
  try {
2694
- writePasteAndSubmit(pty, cardId, message, "a message");
2701
+ writePasteAndSubmit(pty, cardId, message, "a message", logger.card);
2695
2702
  } catch (err) {
2696
2703
  logger.card.error(cardId, "Failed to send a message to the idle session:", err);
2697
2704
  }
@@ -4215,6 +4222,87 @@ async function stopTerminalTunnel(supabase, config) {
4215
4222
  await publishHostname(supabase, config, null);
4216
4223
  }
4217
4224
 
4225
+ // src/workflowShell.ts
4226
+ import { randomUUID as randomUUID8 } from "node:crypto";
4227
+ import { spawn as spawn6 } from "node-pty";
4228
+ function registerWorkflowShellRpcHandler(supabase, config) {
4229
+ registerRunnerRpcHandler("open_workflow_shell", (payload) => openWorkflowShell(supabase, config, payload));
4230
+ }
4231
+ function isOpenWorkflowShellPayload(value) {
4232
+ if (!value || typeof value !== "object") return false;
4233
+ const candidate = value;
4234
+ return typeof candidate.projectId === "string" && typeof candidate.executionId === "string" && typeof candidate.instructions === "string" && candidate.instructions.length > 0;
4235
+ }
4236
+ async function openWorkflowShell(supabase, config, payload) {
4237
+ if (!isOpenWorkflowShellPayload(payload)) throw new Error("Malformed open_workflow_shell payload.");
4238
+ const { projectId, executionId, instructions } = payload;
4239
+ const registered = config.registeredProjects.find((project) => project.id === projectId);
4240
+ if (!registered) throw new Error("This runner does not have that project registered.");
4241
+ ensureDirTrusted(registered.repoPath);
4242
+ ensureMcpServersTrusted(registered.repoPath);
4243
+ const claudeSessionId = randomUUID8();
4244
+ const shellTicket = randomUUID8();
4245
+ const pty = spawn6(
4246
+ CLAUDE_BINARY,
4247
+ [
4248
+ "--dangerously-skip-permissions",
4249
+ "--model",
4250
+ "claude-sonnet-5",
4251
+ "--session-id",
4252
+ claudeSessionId,
4253
+ ...claudeCliSettingsArgs()
4254
+ ],
4255
+ {
4256
+ name: "xterm-color",
4257
+ cols: 120,
4258
+ rows: 32,
4259
+ cwd: registered.repoPath,
4260
+ env: statuslineEnv(executionId, shellTicket)
4261
+ }
4262
+ );
4263
+ registerPtySession(executionId, pty, shellTicket);
4264
+ pty.onExit(({ exitCode }) => void handleWorkflowShellExit(supabase, executionId, exitCode));
4265
+ waitForIdleThenRun(pty, () => {
4266
+ try {
4267
+ writePasteAndSubmit(pty, executionId, instructions, "the workflow runbook", logger.runner);
4268
+ } catch (err) {
4269
+ logger.runner.error(executionId, "Failed to send the workflow runbook to the agent:", err);
4270
+ }
4271
+ });
4272
+ return { claudeSessionId, shellTicket };
4273
+ }
4274
+ async function handleWorkflowShellExit(supabase, executionId, exitCode) {
4275
+ const status = exitCode === 0 ? "done" : "failed";
4276
+ const { error } = await supabase.rpc("complete_workflow_execution", { p_execution_id: executionId, p_status: status });
4277
+ if (error) {
4278
+ logger.runner.error("WORKFLOW_SHELL", `Failed to record exit status for workflow execution ${executionId}:`, error);
4279
+ }
4280
+ }
4281
+ async function reconcileOrphanedWorkflowExecutions(supabase, config) {
4282
+ const { data, error } = await supabase.from("workflow_executions").select("id, project_id, claude_session_id, shell_ticket").eq("runner_id", config.runnerId).in("status", ["running", "paused_on_question"]);
4283
+ if (error) {
4284
+ logger.runner.error("WORKFLOW_SHELL", "Failed to scan orphaned workflow executions:", error.message);
4285
+ return;
4286
+ }
4287
+ for (const execution of data ?? []) {
4288
+ const { id: executionId, project_id: projectId, claude_session_id: claudeSessionId, shell_ticket: shellTicket } = execution;
4289
+ if (getPtySession(executionId)) continue;
4290
+ if (!claudeSessionId || !shellTicket) {
4291
+ logger.runner.warn("WORKFLOW_SHELL", `Workflow execution ${executionId} is missing claude_session_id/shell_ticket \u2014 cannot resume it.`);
4292
+ continue;
4293
+ }
4294
+ const registered = config.registeredProjects.find((project) => project.id === projectId);
4295
+ if (!registered) {
4296
+ logger.runner.warn("WORKFLOW_SHELL", `Workflow execution ${executionId}: project ${projectId} is not registered on this runner \u2014 skipping.`);
4297
+ continue;
4298
+ }
4299
+ const pty = resumeAgentPty(registered.repoPath, claudeSessionId, executionId, shellTicket);
4300
+ registerPtySession(executionId, pty, shellTicket);
4301
+ pty.onExit(({ exitCode }) => void handleWorkflowShellExit(supabase, executionId, exitCode));
4302
+ logger.runner.info("WORKFLOW_SHELL", `Re-spawned orphaned workflow execution ${executionId} (runner restarted while it was still active).`);
4303
+ }
4304
+ }
4305
+
4218
4306
  // src/runner.ts
4219
4307
  async function startRunner() {
4220
4308
  const loadedConfig = loadConfig();
@@ -4291,6 +4379,9 @@ async function startRunner() {
4291
4379
  await reconcileStaleSessions(supabase, config, project.id).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileStaleSessions failed:", err));
4292
4380
  }
4293
4381
  await reconcileOrphanedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileOrphanedSessions failed:", err));
4382
+ await reconcileOrphanedWorkflowExecutions(supabase, config).catch(
4383
+ (err) => logger.runner.error("WORKFLOW_SHELL", "reconcileOrphanedWorkflowExecutions failed:", err)
4384
+ );
4294
4385
  await reconcileUsageLimitedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileUsageLimitedSessions failed:", err));
4295
4386
  const terminalPort = Number(process.env.AGENT_BOARD_TERMINAL_PORT ?? 8787);
4296
4387
  ensureStatuslineScriptWritten();
@@ -4301,6 +4392,7 @@ async function startRunner() {
4301
4392
  const agentEventsWss = attachAgentEventsServer();
4302
4393
  const runnerRpcWss = attachRunnerRpcServer();
4303
4394
  registerProjectCommandRpcHandler(supabase, config);
4395
+ registerWorkflowShellRpcHandler(supabase, config);
4304
4396
  attachFilesServer(httpServer, supabase, config);
4305
4397
  httpServer.on("upgrade", (request, socket, head) => {
4306
4398
  const pathname = (request.url ?? "").split("?")[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-board-runner",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Local runner for Agent Board — signs in with your own Supabase account (no shared secrets) and drives Claude Code against repos under your local repos root.",
5
5
  "type": "module",
6
6
  "bin": {