agent-board-runner 0.1.20 → 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.
- package/dist/cli.js +133 -41
- package/dist/index.js +133 -41
- 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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|