agent-board-runner 0.1.4 → 0.1.6
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 +333 -81
- package/dist/index.js +333 -81
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -55,8 +55,8 @@ function resolveRepoPath(config, relativeRepoPath) {
|
|
|
55
55
|
|
|
56
56
|
// src/auth.ts
|
|
57
57
|
var SESSION_FILE = join2(CONFIG_DIR, "session.json");
|
|
58
|
-
var DEFAULT_SUPABASE_URL = "https://
|
|
59
|
-
var DEFAULT_SUPABASE_ANON_KEY = "
|
|
58
|
+
var DEFAULT_SUPABASE_URL = "https://supabase.lealtek.com";
|
|
59
|
+
var DEFAULT_SUPABASE_ANON_KEY = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc5MjAwNTE4MCwiZXhwIjo0OTQ3Njc4NzgwLCJyb2xlIjoiYW5vbiJ9.gxNuRa1STVA4lQPLghoGbuqV9_21lVOG1yXSu9WNmJ4";
|
|
60
60
|
function resolveSupabaseUrl() {
|
|
61
61
|
return process.env.AGENT_BOARD_SUPABASE_URL ?? DEFAULT_SUPABASE_URL;
|
|
62
62
|
}
|
|
@@ -200,8 +200,9 @@ var EVAL_INTERVAL_MS = 2e3;
|
|
|
200
200
|
var ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
201
201
|
var BOX_DRAWING_PATTERN = /[─-╿]/g;
|
|
202
202
|
var WORKING_PATTERN = /esc to interrupt/i;
|
|
203
|
+
var BACKGROUND_SHELL_PATTERN = /\b[1-9]\d*\s+shells?\b/i;
|
|
203
204
|
function isAgentHookMoment(value) {
|
|
204
|
-
return value === "implementing" || value === "waiting_plan_approval" || value === "commit_detected" || value === "turn_stopped" || value === "notification";
|
|
205
|
+
return value === "implementing" || value === "tool_active" || value === "waiting_plan_approval" || value === "commit_detected" || value === "turn_stopped" || value === "notification";
|
|
205
206
|
}
|
|
206
207
|
var USAGE_LIMIT_PATTERN = /(usage limit reached|limit reached[^\n]{0,60}resets|reached your (?:usage|session) limit|out of (?:weekly|session) (?:usage|limit))/i;
|
|
207
208
|
var PLAN_APPROVAL_PATTERN = /would you like to proceed\??/i;
|
|
@@ -225,7 +226,10 @@ function classifyAgentOutput(recentOutput, options) {
|
|
|
225
226
|
if (options.planApprovalPending) return "waiting_plan_approval";
|
|
226
227
|
if (USER_PROMPT_PATTERNS.some((pattern) => pattern.test(recentOutput))) return "waiting_user";
|
|
227
228
|
const idleThreshold = options.toolCallPending ? IDLE_AFTER_MS_TOOL_PENDING : IDLE_AFTER_MS;
|
|
228
|
-
if (options.silentForMs >= idleThreshold)
|
|
229
|
+
if (options.silentForMs >= idleThreshold) {
|
|
230
|
+
if (BACKGROUND_SHELL_PATTERN.test(recentOutput)) return "waiting_background";
|
|
231
|
+
return options.commitDetected ? "done" : "waiting_user";
|
|
232
|
+
}
|
|
229
233
|
return options.previous;
|
|
230
234
|
}
|
|
231
235
|
var AgentActivityWatcher = class {
|
|
@@ -286,6 +290,10 @@ var AgentActivityWatcher = class {
|
|
|
286
290
|
this.commitDetected = false;
|
|
287
291
|
this.setState("implementing");
|
|
288
292
|
break;
|
|
293
|
+
case "tool_active":
|
|
294
|
+
if (this.usageLimited) break;
|
|
295
|
+
this.setState("implementing");
|
|
296
|
+
break;
|
|
289
297
|
case "waiting_plan_approval":
|
|
290
298
|
this.planApprovalPending = true;
|
|
291
299
|
this.setState("waiting_plan_approval");
|
|
@@ -295,17 +303,29 @@ var AgentActivityWatcher = class {
|
|
|
295
303
|
break;
|
|
296
304
|
case "turn_stopped":
|
|
297
305
|
if (this.usageLimited) break;
|
|
306
|
+
if (BACKGROUND_SHELL_PATTERN.test(this.recentOutput())) {
|
|
307
|
+
this.setState("waiting_background");
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
298
310
|
this.setState(this.commitDetected ? "done" : "waiting_user");
|
|
299
311
|
break;
|
|
300
312
|
case "notification":
|
|
301
313
|
if (this.usageLimited || this.planApprovalPending) break;
|
|
302
|
-
if (this.state === "implementing" || this.state === "starting")
|
|
314
|
+
if (this.state === "implementing" || this.state === "starting") {
|
|
315
|
+
this.setState(BACKGROUND_SHELL_PATTERN.test(this.recentOutput()) ? "waiting_background" : "waiting_user");
|
|
316
|
+
}
|
|
303
317
|
break;
|
|
304
318
|
}
|
|
305
319
|
}
|
|
306
|
-
|
|
320
|
+
/** Text still inside RECENT_WINDOW_MS as of `now`, pruning anything older. Shared by evaluate()
|
|
321
|
+
* (the text/idle-timer path) and reportHookMoment() (the hook path), which both need to know
|
|
322
|
+
* whether the background-shell indicator is currently on screen. */
|
|
323
|
+
recentOutput(now = Date.now()) {
|
|
307
324
|
while (this.chunks.length > 0 && now - this.chunks[0].at > RECENT_WINDOW_MS) this.chunks.shift();
|
|
308
|
-
|
|
325
|
+
return this.chunks.map((chunk) => chunk.text).join("");
|
|
326
|
+
}
|
|
327
|
+
evaluate(now = Date.now()) {
|
|
328
|
+
const next = classifyAgentOutput(this.recentOutput(now), {
|
|
309
329
|
silentForMs: now - this.lastOutputAt,
|
|
310
330
|
usageLimited: this.usageLimited,
|
|
311
331
|
planApprovalPending: this.planApprovalPending,
|
|
@@ -451,7 +471,7 @@ function stopPtySession(sessionId) {
|
|
|
451
471
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
452
472
|
import { spawn } from "node-pty";
|
|
453
473
|
var SHELL_COMMAND = process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "bash";
|
|
454
|
-
function spawnShellPty(repoPath, sessionIdPrefix) {
|
|
474
|
+
function spawnShellPty(repoPath, sessionIdPrefix, initialCommand) {
|
|
455
475
|
const sessionId = `${sessionIdPrefix}:${randomUUID3()}`;
|
|
456
476
|
const ticket = randomUUID3();
|
|
457
477
|
const pty = spawn(SHELL_COMMAND, [], {
|
|
@@ -461,14 +481,15 @@ function spawnShellPty(repoPath, sessionIdPrefix) {
|
|
|
461
481
|
cwd: repoPath
|
|
462
482
|
});
|
|
463
483
|
registerPtySession(sessionId, pty, ticket);
|
|
484
|
+
if (initialCommand) pty.write(`${initialCommand}\r`);
|
|
464
485
|
pty.onExit(({ exitCode }) => {
|
|
465
486
|
logger.runner.info("SHELL", `Shell ${sessionId} exited (code ${exitCode}).`);
|
|
466
487
|
stopPtySession(sessionId);
|
|
467
488
|
});
|
|
468
489
|
return { sessionId, ticket };
|
|
469
490
|
}
|
|
470
|
-
function openShellSession(repoPath) {
|
|
471
|
-
return spawnShellPty(repoPath, "shell");
|
|
491
|
+
function openShellSession(repoPath, initialCommand) {
|
|
492
|
+
return spawnShellPty(repoPath, "shell", initialCommand);
|
|
472
493
|
}
|
|
473
494
|
function closeShellSession(sessionId) {
|
|
474
495
|
stopPtySession(sessionId);
|
|
@@ -477,9 +498,8 @@ var columnShells = /* @__PURE__ */ new Map();
|
|
|
477
498
|
function openOrReuseColumnShell(key, repoPath, initialCommand) {
|
|
478
499
|
const existing = columnShells.get(key);
|
|
479
500
|
if (existing && getPtySession(existing.sessionId)) return existing;
|
|
480
|
-
const shell = spawnShellPty(repoPath, "column-shell");
|
|
501
|
+
const shell = spawnShellPty(repoPath, "column-shell", initialCommand);
|
|
481
502
|
columnShells.set(key, shell);
|
|
482
|
-
if (initialCommand) getPtySession(shell.sessionId)?.pty.write(`${initialCommand}\r`);
|
|
483
503
|
return shell;
|
|
484
504
|
}
|
|
485
505
|
function closeColumnShell(key) {
|
|
@@ -747,11 +767,19 @@ async function getRunningSessionForProject(supabase, projectId, ownerUserId) {
|
|
|
747
767
|
}
|
|
748
768
|
async function listQueuedSessionsForRunner(supabase, ownerUserId) {
|
|
749
769
|
const { data, error } = await supabase.from("sessions").select(
|
|
750
|
-
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, assigned_user_id, title, instructions, file_references, plan_first)"
|
|
770
|
+
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, assigned_user_id, title, instructions, file_references, plan_first, prerequisite_card_id)"
|
|
751
771
|
).eq("cards.assigned_user_id", ownerUserId).eq("status", "queued").order("created_at", { ascending: true });
|
|
752
772
|
if (error) throw error;
|
|
753
773
|
return data;
|
|
754
774
|
}
|
|
775
|
+
async function listPausedSessionsForRunner(supabase, ownerUserId) {
|
|
776
|
+
const { data, error } = await supabase.from("sessions").select("card_id, cards!card_id!inner(project_id, assigned_user_id)").eq("cards.assigned_user_id", ownerUserId).eq("status", "paused");
|
|
777
|
+
if (error) throw error;
|
|
778
|
+
return data.map((row) => ({
|
|
779
|
+
card_id: row.card_id,
|
|
780
|
+
project_id: row.cards.project_id
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
755
783
|
async function createSession(supabase, userId, cardId, resumeClaudeSessionId) {
|
|
756
784
|
const { data, error } = await supabase.from("sessions").insert({
|
|
757
785
|
card_id: cardId,
|
|
@@ -810,6 +838,11 @@ async function getLastClaudeSessionIdForCard(supabase, cardId) {
|
|
|
810
838
|
if (error) throw error;
|
|
811
839
|
return data?.claude_session_id ?? null;
|
|
812
840
|
}
|
|
841
|
+
async function getLatestAgentStateForCard(supabase, cardId) {
|
|
842
|
+
const { data, error } = await supabase.from("sessions").select("agent_state").eq("card_id", cardId).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
843
|
+
if (error) throw error;
|
|
844
|
+
return data?.agent_state ?? null;
|
|
845
|
+
}
|
|
813
846
|
async function updateCard(supabase, cardId, changes) {
|
|
814
847
|
const { error } = await supabase.from("cards").update({
|
|
815
848
|
...changes.column !== void 0 ? { column_name: changes.column } : {},
|
|
@@ -930,6 +963,9 @@ var AGENT_MOMENT_HOOK_SCRIPT_PATH = join4(tmpdir2(), "agent-board-agent-moment-h
|
|
|
930
963
|
var SCRIPT_SOURCE = `#!/usr/bin/env node
|
|
931
964
|
"use strict";
|
|
932
965
|
const http = require("node:http");
|
|
966
|
+
const fs = require("node:fs");
|
|
967
|
+
const os = require("node:os");
|
|
968
|
+
const path = require("node:path");
|
|
933
969
|
|
|
934
970
|
function readStdin() {
|
|
935
971
|
return new Promise((resolve) => {
|
|
@@ -948,6 +984,58 @@ function readStdin() {
|
|
|
948
984
|
const COMMIT_SUCCESS_PATTERN = /\\[\\S+\\s+[0-9a-f]{7,40}\\]/;
|
|
949
985
|
const GIT_COMMIT_COMMAND_PATTERN = /\\bgit\\b[^\\n]*\\bcommit\\b/i;
|
|
950
986
|
|
|
987
|
+
// Catches the agent writing a tool invocation as plain prose (e.g. "Agent({ subagent_type:
|
|
988
|
+
// ... })") in what was meant to be its final message, instead of actually emitting a
|
|
989
|
+
// structured tool call \u2014 Claude Code then just ends the turn, having "announced" work it
|
|
990
|
+
// never did. Restricted to names that look like a real tool (PascalCase built-ins, or
|
|
991
|
+
// "mcp__server__tool") rather than any "Word({" in the text, so ordinary prose describing
|
|
992
|
+
// hypothetical code doesn't trip it.
|
|
993
|
+
const KNOWN_TOOL_NAMES = new Set([
|
|
994
|
+
"Agent", "AskUserQuestion", "Bash", "PowerShell", "Edit", "Write", "Read", "Glob", "Grep",
|
|
995
|
+
"NotebookEdit", "WebFetch", "WebSearch", "Skill", "ExitPlanMode", "EnterPlanMode",
|
|
996
|
+
"ToolSearch", "ListAgents", "SendMessage", "ScheduleWakeup", "SendFeedback", "ReportFindings",
|
|
997
|
+
"Artifact", "CronCreate", "CronDelete", "CronList", "DesignSync", "EndConversation",
|
|
998
|
+
"EnterWorktree", "ExitWorktree", "LSP", "ListMcpResourcesTool", "Monitor",
|
|
999
|
+
"PushNotification", "ReadMcpResourceDirTool", "ReadMcpResourceTool", "RemoteTrigger",
|
|
1000
|
+
"TaskOutput", "TaskStop", "TodoWrite", "SlashCommand", "Task",
|
|
1001
|
+
]);
|
|
1002
|
+
const TOOL_CALL_LOOKALIKE_PATTERN = /\\b([A-Z][A-Za-z0-9]*|mcp__[A-Za-z0-9_]+)\\(\\s*\\{/g;
|
|
1003
|
+
|
|
1004
|
+
function findFakeToolCallName(text) {
|
|
1005
|
+
if (!text) return null;
|
|
1006
|
+
TOOL_CALL_LOOKALIKE_PATTERN.lastIndex = 0;
|
|
1007
|
+
let match;
|
|
1008
|
+
while ((match = TOOL_CALL_LOOKALIKE_PATTERN.exec(text))) {
|
|
1009
|
+
const name = match[1];
|
|
1010
|
+
if (name.startsWith("mcp__") || KNOWN_TOOL_NAMES.has(name)) return name;
|
|
1011
|
+
}
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Bounds the auto-continue below to at most one nudge per session: if the model fakes a
|
|
1016
|
+
// tool call again right after being told to actually call it, something stranger than this
|
|
1017
|
+
// specific slip is going on, and blocking forever would risk an unbounded loop. One file,
|
|
1018
|
+
// shared by every session on this machine, keyed by AGENT_BOARD_SESSION_ID \u2014 a miscount
|
|
1019
|
+
// under concurrent sessions just means the cap is off by one, never a correctness issue.
|
|
1020
|
+
const FAKE_CALL_STATE_PATH = path.join(os.tmpdir(), "agent-board-fake-tool-call-hook-state.json");
|
|
1021
|
+
const MAX_AUTO_CONTINUES_PER_SESSION = 1;
|
|
1022
|
+
|
|
1023
|
+
function fakeCallAttemptsSoFar(sessionId) {
|
|
1024
|
+
try {
|
|
1025
|
+
const state = JSON.parse(fs.readFileSync(FAKE_CALL_STATE_PATH, "utf8"));
|
|
1026
|
+
return state[sessionId] || 0;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return 0;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function recordFakeCallAttempt(sessionId) {
|
|
1033
|
+
let state = {};
|
|
1034
|
+
try { state = JSON.parse(fs.readFileSync(FAKE_CALL_STATE_PATH, "utf8")); } catch {}
|
|
1035
|
+
state[sessionId] = (state[sessionId] || 0) + 1;
|
|
1036
|
+
try { fs.writeFileSync(FAKE_CALL_STATE_PATH, JSON.stringify(state)); } catch {}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
951
1039
|
// Which "moment" (if any) this hook invocation reports, from the single JSON payload Claude
|
|
952
1040
|
// Code gives a hook on stdin. Returning null means "nothing conclusive" \u2014 the hook exits
|
|
953
1041
|
// quietly and the runner's text-based fallback (agentActivity.ts) stays in charge.
|
|
@@ -963,6 +1051,12 @@ function detectMoment(input) {
|
|
|
963
1051
|
// "would you like to proceed?", before the CLI even renders the prompt.
|
|
964
1052
|
if (eventName === "PreToolUse" && input.tool_name === "ExitPlanMode") return "waiting_plan_approval";
|
|
965
1053
|
|
|
1054
|
+
// Any other tool about to run is also a precise "the turn is active" signal \u2014 notably the
|
|
1055
|
+
// one that resumes a permission prompt (y/n) or an AskUserQuestion answer: neither of those
|
|
1056
|
+
// is a new UserPromptSubmit, so without this the badge had nothing but the text fallback to
|
|
1057
|
+
// notice the agent is working again once the human replies.
|
|
1058
|
+
if (eventName === "PreToolUse") return "tool_active";
|
|
1059
|
+
|
|
966
1060
|
// The plan prompt resolving one way or the other: approved, the turn resumes implementing
|
|
967
1061
|
// right away with no new UserPromptSubmit to catch it; asked to revise, a fresh
|
|
968
1062
|
// UserPromptSubmit follows almost immediately anyway. Either way "implementing" is correct
|
|
@@ -971,14 +1065,22 @@ function detectMoment(input) {
|
|
|
971
1065
|
|
|
972
1066
|
// A commit that actually succeeded, read off the Bash tool's own structured result instead
|
|
973
1067
|
// of guessing from scrollback text (exact, and immune to locale/color/format changes to
|
|
974
|
-
// git's own banner).
|
|
1068
|
+
// git's own banner). Any other Bash completion still reports "tool_active" below, same as
|
|
1069
|
+
// every other tool.
|
|
975
1070
|
if (eventName === "PostToolUse" && input.tool_name === "Bash") {
|
|
976
1071
|
const command = String((input.tool_input && input.tool_input.command) || "");
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
|
|
1072
|
+
if (GIT_COMMIT_COMMAND_PATTERN.test(command)) {
|
|
1073
|
+
const response = JSON.stringify(input.tool_response || "");
|
|
1074
|
+
if (COMMIT_SUCCESS_PATTERN.test(response)) return "commit_detected";
|
|
1075
|
+
}
|
|
980
1076
|
}
|
|
981
1077
|
|
|
1078
|
+
// Any other tool finishing is the same "still active" signal as PreToolUse above \u2014 this is
|
|
1079
|
+
// what actually clears a yellow badge once a permission-gated or AskUserQuestion tool call
|
|
1080
|
+
// resumes and completes (there's no hook fired at the exact instant permission is granted,
|
|
1081
|
+
// only at the tool call's start and end).
|
|
1082
|
+
if (eventName === "PostToolUse") return "tool_active";
|
|
1083
|
+
|
|
982
1084
|
// The turn actually ending \u2014 Claude Code back at its input box, no idle timer needed.
|
|
983
1085
|
if (eventName === "Stop") return "turn_stopped";
|
|
984
1086
|
|
|
@@ -1020,6 +1122,23 @@ function reportToRunner(sessionId, ticket, port, moment) {
|
|
|
1020
1122
|
(async () => {
|
|
1021
1123
|
try {
|
|
1022
1124
|
const input = JSON.parse(await readStdin());
|
|
1125
|
+
if (input.hook_event_name === "Stop") {
|
|
1126
|
+
const fakeCallName = findFakeToolCallName(input.lastAssistantMessage);
|
|
1127
|
+
const sessionId = process.env.AGENT_BOARD_SESSION_ID || "unknown";
|
|
1128
|
+
if (fakeCallName && fakeCallAttemptsSoFar(sessionId) < MAX_AUTO_CONTINUES_PER_SESSION) {
|
|
1129
|
+
recordFakeCallAttempt(sessionId);
|
|
1130
|
+
process.stdout.write(JSON.stringify({
|
|
1131
|
+
decision: "block",
|
|
1132
|
+
reason:
|
|
1133
|
+
\`You wrote "\${fakeCallName}({...})" as plain text in your final message instead of \` +
|
|
1134
|
+
"actually calling that tool \u2014 no such call happened, so nothing was delegated/executed. " +
|
|
1135
|
+
"If you meant to use it, call it for real now as a structured tool call, don't just " +
|
|
1136
|
+
"describe it. If you already finished without needing it, say so explicitly instead of " +
|
|
1137
|
+
"writing pseudocode that looks like a call.",
|
|
1138
|
+
}));
|
|
1139
|
+
process.exit(0);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1023
1142
|
const moment = detectMoment(input);
|
|
1024
1143
|
if (moment) {
|
|
1025
1144
|
const sessionId = process.env.AGENT_BOARD_SESSION_ID;
|
|
@@ -1045,11 +1164,8 @@ function agentMomentHookSettingsFragment() {
|
|
|
1045
1164
|
return {
|
|
1046
1165
|
hooks: {
|
|
1047
1166
|
UserPromptSubmit: [{ hooks: [hookEntry] }],
|
|
1048
|
-
PreToolUse: [{ matcher: "
|
|
1049
|
-
PostToolUse: [
|
|
1050
|
-
{ matcher: "ExitPlanMode", hooks: [hookEntry] },
|
|
1051
|
-
{ matcher: "Bash", hooks: [hookEntry] }
|
|
1052
|
-
],
|
|
1167
|
+
PreToolUse: [{ matcher: "*", hooks: [hookEntry] }],
|
|
1168
|
+
PostToolUse: [{ matcher: "*", hooks: [hookEntry] }],
|
|
1053
1169
|
Stop: [{ hooks: [hookEntry] }],
|
|
1054
1170
|
Notification: [{ hooks: [hookEntry] }]
|
|
1055
1171
|
}
|
|
@@ -1789,47 +1905,51 @@ async function handleAgentHookReport(req, res) {
|
|
|
1789
1905
|
res.writeHead(204, { "Access-Control-Allow-Origin": "*" });
|
|
1790
1906
|
res.end();
|
|
1791
1907
|
}
|
|
1792
|
-
async function handleBranchRequest(entry, res, relPath) {
|
|
1793
|
-
let cwd;
|
|
1794
|
-
try {
|
|
1795
|
-
cwd = relPath ? resolveWithinRepo(entry.repoRoot, relPath) : entry.repoRoot;
|
|
1796
|
-
} catch (err) {
|
|
1797
|
-
if (err instanceof HttpError) {
|
|
1798
|
-
sendJson(res, err.status, { error: err.message });
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
throw err;
|
|
1802
|
-
}
|
|
1803
|
-
try {
|
|
1804
|
-
const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
|
|
1805
|
-
sendJson(res, 200, { branch: stdout.trim() });
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
logger.runner.error("FILES", "Unexpected error handling a branch request:", err);
|
|
1808
|
-
sendJson(res, 500, { error: "Erro interno do runner." });
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
1908
|
var DIFF_EXEC_OPTIONS = { maxBuffer: 10 * 1024 * 1024 };
|
|
1812
|
-
function
|
|
1909
|
+
function parseGitDiffNameStatus(output) {
|
|
1813
1910
|
return output.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
1814
|
-
const
|
|
1815
|
-
const
|
|
1816
|
-
const
|
|
1817
|
-
const path = arrowIndex >= 0 ? rest2.slice(arrowIndex + 4) : rest2;
|
|
1911
|
+
const columns = line.split(" ");
|
|
1912
|
+
const status = columns[0].slice(0, 1);
|
|
1913
|
+
const path = columns[columns.length - 1];
|
|
1818
1914
|
return { path, status };
|
|
1819
1915
|
});
|
|
1820
1916
|
}
|
|
1821
1917
|
async function getRepoDiffForTarget(label, cwd) {
|
|
1918
|
+
let branch;
|
|
1919
|
+
try {
|
|
1920
|
+
const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
|
|
1921
|
+
branch = stdout.trim();
|
|
1922
|
+
} catch (err) {
|
|
1923
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1924
|
+
return { label, files: [], diff: "", error: message };
|
|
1925
|
+
}
|
|
1926
|
+
if (!branch) {
|
|
1927
|
+
return { label, files: [], diff: "", error: "HEAD destacado: sem branch local para comparar com a origin." };
|
|
1928
|
+
}
|
|
1929
|
+
try {
|
|
1930
|
+
await execFileAsync2("git", ["fetch", "origin", branch], { cwd });
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1933
|
+
const notOnRemote = /couldn't find remote ref/i.test(message);
|
|
1934
|
+
return {
|
|
1935
|
+
label,
|
|
1936
|
+
files: [],
|
|
1937
|
+
diff: "",
|
|
1938
|
+
error: notOnRemote ? `A branch "${branch}" ainda n\xE3o existe na origin.` : message
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
const remoteRef = `origin/${branch}`;
|
|
1822
1942
|
let files;
|
|
1823
1943
|
try {
|
|
1824
|
-
const { stdout } = await execFileAsync2("git", ["
|
|
1825
|
-
files =
|
|
1944
|
+
const { stdout } = await execFileAsync2("git", ["diff", "--name-status", remoteRef, "HEAD"], { cwd });
|
|
1945
|
+
files = parseGitDiffNameStatus(stdout);
|
|
1826
1946
|
} catch (err) {
|
|
1827
1947
|
const message = err instanceof Error ? err.message : String(err);
|
|
1828
1948
|
return { label, files: [], diff: "", error: message };
|
|
1829
1949
|
}
|
|
1830
1950
|
if (files.length === 0) return { label, files, diff: "" };
|
|
1831
1951
|
try {
|
|
1832
|
-
const { stdout } = await execFileAsync2("git", ["diff", "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
1952
|
+
const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
1833
1953
|
return { label, files, diff: stdout };
|
|
1834
1954
|
} catch {
|
|
1835
1955
|
return { label, files, diff: "" };
|
|
@@ -2012,10 +2132,6 @@ function handleRequest(req, res) {
|
|
|
2012
2132
|
sendJson(res, 401, { error: "Ticket inv\xE1lido, expirado, ou projeto desconhecido." });
|
|
2013
2133
|
return;
|
|
2014
2134
|
}
|
|
2015
|
-
if (pathname === "/branch") {
|
|
2016
|
-
void handleBranchRequest(entry, res, relPath);
|
|
2017
|
-
return;
|
|
2018
|
-
}
|
|
2019
2135
|
if (pathname === "/diff") {
|
|
2020
2136
|
const projectId = typeof query.projectId === "string" ? query.projectId : "";
|
|
2021
2137
|
void handleDiffRequest(entry, res, projectId);
|
|
@@ -2052,7 +2168,9 @@ function handleRequest(req, res) {
|
|
|
2052
2168
|
return;
|
|
2053
2169
|
}
|
|
2054
2170
|
if (pathname === "/shell/open") {
|
|
2055
|
-
|
|
2171
|
+
const kind = typeof query.kind === "string" ? query.kind : "shell";
|
|
2172
|
+
if (!COLUMN_SHELL_KINDS.includes(kind)) throw new HttpError(400, "Par\xE2metro kind inv\xE1lido.");
|
|
2173
|
+
sendJson(res, 200, openShellSession(entry.repoRoot, kind === "claude" ? CLAUDE_BYPASS_COMMAND : void 0));
|
|
2056
2174
|
return;
|
|
2057
2175
|
}
|
|
2058
2176
|
if (pathname === "/shell/close") {
|
|
@@ -2098,11 +2216,18 @@ function attachFilesServer(httpServer, supabase, config) {
|
|
|
2098
2216
|
}
|
|
2099
2217
|
|
|
2100
2218
|
// src/agentEventsServer.ts
|
|
2219
|
+
var EMPTY_QUEUE = { queuedCardIds: [], pausedCardIds: [] };
|
|
2101
2220
|
var stateByProject = /* @__PURE__ */ new Map();
|
|
2221
|
+
var queueByProject = /* @__PURE__ */ new Map();
|
|
2222
|
+
var branchesByProject = /* @__PURE__ */ new Map();
|
|
2102
2223
|
var socketsByProject = /* @__PURE__ */ new Map();
|
|
2103
2224
|
function sendSnapshot(socket, projectId) {
|
|
2104
2225
|
const states = Array.from(stateByProject.get(projectId)?.values() ?? []);
|
|
2105
|
-
|
|
2226
|
+
const queue = queueByProject.get(projectId) ?? EMPTY_QUEUE;
|
|
2227
|
+
const branches = Array.from(branchesByProject.get(projectId)?.entries() ?? []).map(
|
|
2228
|
+
([label, branch]) => ({ label: label || null, branch })
|
|
2229
|
+
);
|
|
2230
|
+
socket.send(JSON.stringify({ type: "snapshot", states, queue, branches }));
|
|
2106
2231
|
}
|
|
2107
2232
|
function publishAgentStateEvent(projectId, cardId, sessionId, agentState) {
|
|
2108
2233
|
let projectState = stateByProject.get(projectId);
|
|
@@ -2118,6 +2243,29 @@ function publishAgentStateEvent(projectId, cardId, sessionId, agentState) {
|
|
|
2118
2243
|
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
2119
2244
|
}
|
|
2120
2245
|
}
|
|
2246
|
+
function publishQueueEvent(projectId, queue) {
|
|
2247
|
+
queueByProject.set(projectId, queue);
|
|
2248
|
+
const sockets = socketsByProject.get(projectId);
|
|
2249
|
+
if (!sockets || sockets.size === 0) return;
|
|
2250
|
+
const payload = JSON.stringify({ type: "queue", ...queue });
|
|
2251
|
+
for (const socket of sockets) {
|
|
2252
|
+
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
function publishBranchEvent(projectId, label, branch) {
|
|
2256
|
+
let projectBranches = branchesByProject.get(projectId);
|
|
2257
|
+
if (!projectBranches) {
|
|
2258
|
+
projectBranches = /* @__PURE__ */ new Map();
|
|
2259
|
+
branchesByProject.set(projectId, projectBranches);
|
|
2260
|
+
}
|
|
2261
|
+
projectBranches.set(label ?? "", branch);
|
|
2262
|
+
const sockets = socketsByProject.get(projectId);
|
|
2263
|
+
if (!sockets || sockets.size === 0) return;
|
|
2264
|
+
const payload = JSON.stringify({ type: "branch", label, branch });
|
|
2265
|
+
for (const socket of sockets) {
|
|
2266
|
+
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2121
2269
|
function attachAgentEventsServer() {
|
|
2122
2270
|
const wss = new WebSocketServer({ noServer: true });
|
|
2123
2271
|
wss.on("connection", (socket, request) => {
|
|
@@ -2144,11 +2292,77 @@ function attachAgentEventsServer() {
|
|
|
2144
2292
|
return wss;
|
|
2145
2293
|
}
|
|
2146
2294
|
|
|
2147
|
-
// src/
|
|
2148
|
-
import {
|
|
2295
|
+
// src/branchWatcher.ts
|
|
2296
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
2297
|
+
import { watch } from "node:fs";
|
|
2149
2298
|
import { join as join10 } from "node:path";
|
|
2150
2299
|
import { promisify as promisify3 } from "node:util";
|
|
2151
|
-
var
|
|
2300
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2301
|
+
var DEBOUNCE_MS = 300;
|
|
2302
|
+
var watchedProjects = /* @__PURE__ */ new Map();
|
|
2303
|
+
async function readCurrentBranch(cwd) {
|
|
2304
|
+
try {
|
|
2305
|
+
const { stdout } = await execFileAsync3("git", ["branch", "--show-current"], { cwd });
|
|
2306
|
+
return stdout.trim();
|
|
2307
|
+
} catch {
|
|
2308
|
+
return "";
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
function watchTarget(projectId, label, cwd, state) {
|
|
2312
|
+
let debounceTimer = null;
|
|
2313
|
+
function check() {
|
|
2314
|
+
void readCurrentBranch(cwd).then((branch) => {
|
|
2315
|
+
if (!branch || state.lastBranch.get(label) === branch) return;
|
|
2316
|
+
state.lastBranch.set(label, branch);
|
|
2317
|
+
publishBranchEvent(projectId, label || null, branch);
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
let watcher;
|
|
2321
|
+
try {
|
|
2322
|
+
watcher = watch(join10(cwd, ".git", "HEAD"), () => {
|
|
2323
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
2324
|
+
debounceTimer = setTimeout(check, DEBOUNCE_MS);
|
|
2325
|
+
});
|
|
2326
|
+
} catch (err) {
|
|
2327
|
+
logger.runner.warn("BRANCH_WATCH", `Could not watch .git/HEAD under ${cwd}:`, err instanceof Error ? err.message : err);
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
state.watchers.push(watcher);
|
|
2331
|
+
check();
|
|
2332
|
+
}
|
|
2333
|
+
async function ensureBranchWatchers(supabase, config) {
|
|
2334
|
+
for (const project of config.registeredProjects) {
|
|
2335
|
+
if (watchedProjects.has(project.id)) continue;
|
|
2336
|
+
const state = { watchers: [], lastBranch: /* @__PURE__ */ new Map() };
|
|
2337
|
+
watchedProjects.set(project.id, state);
|
|
2338
|
+
let children;
|
|
2339
|
+
try {
|
|
2340
|
+
children = (await getProjectRepos(supabase, project.id)).map((repo) => repo.subpath);
|
|
2341
|
+
} catch (err) {
|
|
2342
|
+
logger.runner.error("BRANCH_WATCH", `Failed to list child repos for project ${project.id}:`, err);
|
|
2343
|
+
children = [];
|
|
2344
|
+
}
|
|
2345
|
+
if (children.length === 0) {
|
|
2346
|
+
watchTarget(project.id, "", project.repoPath, state);
|
|
2347
|
+
} else {
|
|
2348
|
+
for (const subpath of children) {
|
|
2349
|
+
watchTarget(project.id, subpath, join10(project.repoPath, subpath), state);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
const stillRegistered = new Set(config.registeredProjects.map((project) => project.id));
|
|
2354
|
+
for (const [projectId, state] of watchedProjects) {
|
|
2355
|
+
if (stillRegistered.has(projectId)) continue;
|
|
2356
|
+
for (const watcher of state.watchers) watcher.close();
|
|
2357
|
+
watchedProjects.delete(projectId);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// src/columnActions.ts
|
|
2362
|
+
import { exec } from "node:child_process";
|
|
2363
|
+
import { join as join11 } from "node:path";
|
|
2364
|
+
import { promisify as promisify4 } from "node:util";
|
|
2365
|
+
var execAsync = promisify4(exec);
|
|
2152
2366
|
var OUTPUT_TAIL_CHARS = 4e3;
|
|
2153
2367
|
function tail(text) {
|
|
2154
2368
|
return text.length > OUTPUT_TAIL_CHARS ? text.slice(text.length - OUTPUT_TAIL_CHARS) : text;
|
|
@@ -2156,7 +2370,7 @@ function tail(text) {
|
|
|
2156
2370
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
2157
2371
|
if (children.length > 0) {
|
|
2158
2372
|
return children.flatMap(
|
|
2159
|
-
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd:
|
|
2373
|
+
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join11(repoRoot, child2.subpath), command: action.command }))
|
|
2160
2374
|
);
|
|
2161
2375
|
}
|
|
2162
2376
|
return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
|
|
@@ -2262,7 +2476,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
|
|
|
2262
2476
|
}
|
|
2263
2477
|
|
|
2264
2478
|
// src/deploy.ts
|
|
2265
|
-
import { join as
|
|
2479
|
+
import { join as join14 } from "node:path";
|
|
2266
2480
|
|
|
2267
2481
|
// src/devServer.ts
|
|
2268
2482
|
import { spawn as spawn4 } from "node:child_process";
|
|
@@ -2271,7 +2485,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
2271
2485
|
import { spawn as spawn3 } from "node:child_process";
|
|
2272
2486
|
import { accessSync, constants } from "node:fs";
|
|
2273
2487
|
import { homedir as homedir5 } from "node:os";
|
|
2274
|
-
import { delimiter, join as
|
|
2488
|
+
import { delimiter, join as join12 } from "node:path";
|
|
2275
2489
|
import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
|
|
2276
2490
|
var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
2277
2491
|
var STARTUP_TIMEOUT_MS = 2e4;
|
|
@@ -2282,15 +2496,15 @@ function wellKnownDirectories() {
|
|
|
2282
2496
|
const home = homedir5();
|
|
2283
2497
|
if (IS_WINDOWS2) {
|
|
2284
2498
|
const candidates = [
|
|
2285
|
-
process.env.ProgramFiles &&
|
|
2286
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2287
|
-
process.env.LOCALAPPDATA &&
|
|
2288
|
-
|
|
2289
|
-
process.env.ChocolateyInstall &&
|
|
2499
|
+
process.env.ProgramFiles && join12(process.env.ProgramFiles, "cloudflared"),
|
|
2500
|
+
process.env["ProgramFiles(x86)"] && join12(process.env["ProgramFiles(x86)"], "cloudflared"),
|
|
2501
|
+
process.env.LOCALAPPDATA && join12(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
|
|
2502
|
+
join12(home, "scoop", "shims"),
|
|
2503
|
+
process.env.ChocolateyInstall && join12(process.env.ChocolateyInstall, "bin")
|
|
2290
2504
|
];
|
|
2291
2505
|
return candidates.filter((dir) => Boolean(dir));
|
|
2292
2506
|
}
|
|
2293
|
-
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
|
|
2507
|
+
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join12(home, ".local", "bin")];
|
|
2294
2508
|
}
|
|
2295
2509
|
function pathDirectories() {
|
|
2296
2510
|
return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
@@ -2306,7 +2520,7 @@ function isExecutableFile(path) {
|
|
|
2306
2520
|
function findInstalledBinary() {
|
|
2307
2521
|
const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
|
|
2308
2522
|
const directories = [...pathDirectories(), ...wellKnownDirectories()];
|
|
2309
|
-
const candidates = [...override ? [override] : [], ...directories.map((dir) =>
|
|
2523
|
+
const candidates = [...override ? [override] : [], ...directories.map((dir) => join12(dir, BINARY_NAME))];
|
|
2310
2524
|
return candidates.find(isExecutableFile) ?? null;
|
|
2311
2525
|
}
|
|
2312
2526
|
async function locateCloudflared() {
|
|
@@ -2493,12 +2707,12 @@ function stopAll() {
|
|
|
2493
2707
|
}
|
|
2494
2708
|
|
|
2495
2709
|
// src/git.ts
|
|
2496
|
-
import { execFile as
|
|
2497
|
-
import { promisify as
|
|
2498
|
-
var
|
|
2710
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
2711
|
+
import { promisify as promisify5 } from "node:util";
|
|
2712
|
+
var execFileAsync4 = promisify5(execFile4);
|
|
2499
2713
|
async function pushBranchToMain(repoPath) {
|
|
2500
2714
|
try {
|
|
2501
|
-
await
|
|
2715
|
+
await execFileAsync4("git", ["push", "origin", "HEAD:main"], { cwd: repoPath });
|
|
2502
2716
|
return { ok: true };
|
|
2503
2717
|
} catch (err) {
|
|
2504
2718
|
const stderr = err.stderr;
|
|
@@ -2660,8 +2874,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
|
2660
2874
|
// src/cardAttachments.ts
|
|
2661
2875
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2662
2876
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
2663
|
-
import { extname, join as
|
|
2664
|
-
var ATTACHMENTS_ROOT =
|
|
2877
|
+
import { extname, join as join13 } from "node:path";
|
|
2878
|
+
var ATTACHMENTS_ROOT = join13(tmpdir6(), "agent-board", "card-attachments");
|
|
2665
2879
|
function sanitizeFileName(fileName) {
|
|
2666
2880
|
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2667
2881
|
}
|
|
@@ -2673,13 +2887,13 @@ async function downloadCardAttachments(supabase, cardId) {
|
|
|
2673
2887
|
}
|
|
2674
2888
|
const rows = data ?? [];
|
|
2675
2889
|
if (rows.length === 0) return [];
|
|
2676
|
-
const targetDir =
|
|
2890
|
+
const targetDir = join13(ATTACHMENTS_ROOT, cardId);
|
|
2677
2891
|
await mkdir2(targetDir, { recursive: true });
|
|
2678
2892
|
const paths = [];
|
|
2679
2893
|
for (const row of rows) {
|
|
2680
2894
|
const baseName = sanitizeFileName(row.file_name);
|
|
2681
2895
|
const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
|
|
2682
|
-
const localPath =
|
|
2896
|
+
const localPath = join13(targetDir, `${row.id}-${fileName}`);
|
|
2683
2897
|
try {
|
|
2684
2898
|
const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
|
|
2685
2899
|
if (downloadError) throw downloadError;
|
|
@@ -2714,7 +2928,7 @@ ${list}`;
|
|
|
2714
2928
|
function buildInstructionsWithMcpContext(instructions, cardId, projectId) {
|
|
2715
2929
|
return `${instructions}
|
|
2716
2930
|
|
|
2717
|
-
Agent Board MCP: this card's id is \`${cardId}\` and its project id is \`${projectId}\` \u2014 pass these directly to board tools (\`update_card\`, \`create_card\`, \`add_card_comment\`, etc.), no lookup needed. If this task turns out too large for one session, use \`create_card\` to split it into subtask cards (chain them with \`prerequisiteCardId\` when one depends on another) instead of trying to fit everything here. Before finishing, post a short summary of what you did as a comment on this card via \`add_card_comment\` so the project's humans have visibility without opening this terminal.`;
|
|
2931
|
+
Agent Board MCP: this card's id is \`${cardId}\` and its project id is \`${projectId}\` \u2014 pass these directly to board tools (\`update_card\`, \`create_card\`, \`add_card_comment\`, etc.), no lookup needed. If this task turns out too large for one session, use \`create_card\` to split it into subtask cards (chain them with \`prerequisiteCardId\` when one depends on another) instead of trying to fit everything here. If part of the delivery needs a human to do something you can't do yourself (rotate a secret, approve something in a third-party dashboard, delete a resource manually, etc.), register each step with \`add_manual_actions\` instead of just mentioning it in your final message \u2014 that's the only place humans will reliably see it. Before finishing, post a short summary of what you did as a comment on this card via \`add_card_comment\` so the project's humans have visibility without opening this terminal.`;
|
|
2718
2932
|
}
|
|
2719
2933
|
|
|
2720
2934
|
// src/usageResume.ts
|
|
@@ -2824,8 +3038,9 @@ async function reconcileStaleSessions(supabase, config, projectId) {
|
|
|
2824
3038
|
async function dispatchRunnerQueue(supabase, config) {
|
|
2825
3039
|
if (dispatchingRunner) return;
|
|
2826
3040
|
dispatchingRunner = true;
|
|
3041
|
+
let queued = [];
|
|
2827
3042
|
try {
|
|
2828
|
-
|
|
3043
|
+
queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
2829
3044
|
if (queued.length === 0) return;
|
|
2830
3045
|
const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
|
|
2831
3046
|
const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
|
|
@@ -2846,6 +3061,17 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2846
3061
|
const projectId = entry.cards.project_id;
|
|
2847
3062
|
if (runningProjectIds.has(projectId) || promotedThisPass.has(projectId)) continue;
|
|
2848
3063
|
if (freeProjectSlots <= 0) break;
|
|
3064
|
+
const prerequisiteCardId = entry.cards.prerequisite_card_id;
|
|
3065
|
+
if (prerequisiteCardId) {
|
|
3066
|
+
const prerequisiteAgentState = await getLatestAgentStateForCard(supabase, prerequisiteCardId);
|
|
3067
|
+
if (prerequisiteAgentState !== "done") {
|
|
3068
|
+
logger.card.info(
|
|
3069
|
+
entry.card_id,
|
|
3070
|
+
`Waiting on prerequisite card ${prerequisiteCardId} to reach agent state "done" (currently "${prerequisiteAgentState ?? "none"}") before starting \u2014 leaving queued.`
|
|
3071
|
+
);
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
2849
3075
|
try {
|
|
2850
3076
|
await promoteQueuedSession(supabase, config, entry);
|
|
2851
3077
|
promotedThisPass.add(projectId);
|
|
@@ -2855,9 +3081,33 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2855
3081
|
}
|
|
2856
3082
|
}
|
|
2857
3083
|
} finally {
|
|
3084
|
+
await publishQueueSnapshots(supabase, config, queued).catch(
|
|
3085
|
+
(err) => logger.runner.error("ORCHESTRATOR", "Failed to publish queue snapshots:", err)
|
|
3086
|
+
);
|
|
2858
3087
|
dispatchingRunner = false;
|
|
2859
3088
|
}
|
|
2860
3089
|
}
|
|
3090
|
+
async function publishQueueSnapshots(supabase, config, queued) {
|
|
3091
|
+
const paused = await listPausedSessionsForRunner(supabase, config.ownerUserId);
|
|
3092
|
+
const queuedByProject = /* @__PURE__ */ new Map();
|
|
3093
|
+
for (const entry of queued) {
|
|
3094
|
+
const list = queuedByProject.get(entry.cards.project_id) ?? [];
|
|
3095
|
+
list.push(entry.card_id);
|
|
3096
|
+
queuedByProject.set(entry.cards.project_id, list);
|
|
3097
|
+
}
|
|
3098
|
+
const pausedByProject = /* @__PURE__ */ new Map();
|
|
3099
|
+
for (const { card_id: cardId, project_id: projectId } of paused) {
|
|
3100
|
+
const list = pausedByProject.get(projectId) ?? [];
|
|
3101
|
+
list.push(cardId);
|
|
3102
|
+
pausedByProject.set(projectId, list);
|
|
3103
|
+
}
|
|
3104
|
+
for (const project of config.registeredProjects) {
|
|
3105
|
+
publishQueueEvent(project.id, {
|
|
3106
|
+
queuedCardIds: queuedByProject.get(project.id) ?? [],
|
|
3107
|
+
pausedCardIds: pausedByProject.get(project.id) ?? []
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
2861
3111
|
async function promoteQueuedSession(supabase, config, entry) {
|
|
2862
3112
|
const existingHandle = getPtySession(entry.id);
|
|
2863
3113
|
if (existingHandle) {
|
|
@@ -3106,7 +3356,7 @@ async function handleDeployRequested(supabase, config, project) {
|
|
|
3106
3356
|
}
|
|
3107
3357
|
const children = await getProjectRepos(supabase, project.id);
|
|
3108
3358
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3109
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path:
|
|
3359
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join14(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
|
|
3110
3360
|
const failures = [];
|
|
3111
3361
|
for (const target of targets) {
|
|
3112
3362
|
const result = await pushBranchToMain(target.path);
|
|
@@ -3240,7 +3490,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
|
|
|
3240
3490
|
}
|
|
3241
3491
|
|
|
3242
3492
|
// src/review.ts
|
|
3243
|
-
import { join as
|
|
3493
|
+
import { join as join15 } from "node:path";
|
|
3244
3494
|
var inFlightCardIds2 = /* @__PURE__ */ new Set();
|
|
3245
3495
|
async function handleCardEnteredRevisao(supabase, config, card) {
|
|
3246
3496
|
if (inFlightCardIds2.has(card.id)) return;
|
|
@@ -3249,7 +3499,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
|
|
|
3249
3499
|
const project = await getProject(supabase, card.project_id);
|
|
3250
3500
|
const children = await getProjectRepos(supabase, project.id);
|
|
3251
3501
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3252
|
-
const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd:
|
|
3502
|
+
const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd: join15(repoRoot, child2.subpath), command: child2.dev_command })) : project.dev_command ? [{ label: "", cwd: repoRoot, command: project.dev_command }] : [];
|
|
3253
3503
|
if (targets.length === 0) {
|
|
3254
3504
|
logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
|
|
3255
3505
|
return;
|
|
@@ -3518,6 +3768,7 @@ async function startRunner() {
|
|
|
3518
3768
|
void ensureFilesTickets(supabase, config).catch((err) => logger.runner.error("FILES", "ensureFilesTickets failed:", err));
|
|
3519
3769
|
void ensureSetupTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureSetupTicket failed:", err));
|
|
3520
3770
|
void ensureAgentTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureAgentTicket failed:", err));
|
|
3771
|
+
void ensureBranchWatchers(supabase, config).catch((err) => logger.runner.error("BRANCH_WATCH", "ensureBranchWatchers failed:", err));
|
|
3521
3772
|
const presenceChannel = supabase.channel(`runner-presence:${config.runnerId}`);
|
|
3522
3773
|
presenceChannel.subscribe((status) => {
|
|
3523
3774
|
if (status === "SUBSCRIBED") {
|
|
@@ -3614,6 +3865,7 @@ async function startRunner() {
|
|
|
3614
3865
|
await ensureFilesTickets(supabase, config);
|
|
3615
3866
|
await ensureSetupTicket(supabase, config);
|
|
3616
3867
|
await ensureAgentTicket(supabase, config);
|
|
3868
|
+
await ensureBranchWatchers(supabase, config).catch((err) => logger.runner.error("BRANCH_WATCH", "ensureBranchWatchers failed:", err));
|
|
3617
3869
|
await scanForWork(supabase, config);
|
|
3618
3870
|
await reconcileUsageLimitedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileUsageLimitedSessions failed:", err));
|
|
3619
3871
|
await checkPendingDeploys(supabase, config).catch((err) => logger.runner.error("DEPLOY", "checkPendingDeploys failed:", err));
|