agent-board-runner 0.1.3 → 0.1.5
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 +334 -79
- package/dist/index.js +334 -79
- 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,
|
|
@@ -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
|
}
|
|
@@ -1169,11 +1285,16 @@ function formatPercent(value) {
|
|
|
1169
1285
|
}
|
|
1170
1286
|
|
|
1171
1287
|
function buildDisplayText(data) {
|
|
1172
|
-
const rateLimits = data && data.rate_limits;
|
|
1173
|
-
if (!rateLimits) return "";
|
|
1174
1288
|
const parts = [];
|
|
1175
|
-
|
|
1176
|
-
if (
|
|
1289
|
+
const contextWindow = data && data.context_window;
|
|
1290
|
+
if (contextWindow && typeof contextWindow.used_percentage === "number") {
|
|
1291
|
+
parts.push(\`ctx \${formatPercent(contextWindow.used_percentage)}\`);
|
|
1292
|
+
}
|
|
1293
|
+
const rateLimits = data && data.rate_limits;
|
|
1294
|
+
if (rateLimits) {
|
|
1295
|
+
if (rateLimits.five_hour) parts.push(\`5h \${formatPercent(rateLimits.five_hour.used_percentage)}\`);
|
|
1296
|
+
if (rateLimits.seven_day) parts.push(\`7d \${formatPercent(rateLimits.seven_day.used_percentage)}\`);
|
|
1297
|
+
}
|
|
1177
1298
|
return parts.join(" \xB7 ");
|
|
1178
1299
|
}
|
|
1179
1300
|
|
|
@@ -1784,47 +1905,51 @@ async function handleAgentHookReport(req, res) {
|
|
|
1784
1905
|
res.writeHead(204, { "Access-Control-Allow-Origin": "*" });
|
|
1785
1906
|
res.end();
|
|
1786
1907
|
}
|
|
1787
|
-
async function handleBranchRequest(entry, res, relPath) {
|
|
1788
|
-
let cwd;
|
|
1789
|
-
try {
|
|
1790
|
-
cwd = relPath ? resolveWithinRepo(entry.repoRoot, relPath) : entry.repoRoot;
|
|
1791
|
-
} catch (err) {
|
|
1792
|
-
if (err instanceof HttpError) {
|
|
1793
|
-
sendJson(res, err.status, { error: err.message });
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
throw err;
|
|
1797
|
-
}
|
|
1798
|
-
try {
|
|
1799
|
-
const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
|
|
1800
|
-
sendJson(res, 200, { branch: stdout.trim() });
|
|
1801
|
-
} catch (err) {
|
|
1802
|
-
logger.runner.error("FILES", "Unexpected error handling a branch request:", err);
|
|
1803
|
-
sendJson(res, 500, { error: "Erro interno do runner." });
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
1908
|
var DIFF_EXEC_OPTIONS = { maxBuffer: 10 * 1024 * 1024 };
|
|
1807
|
-
function
|
|
1909
|
+
function parseGitDiffNameStatus(output) {
|
|
1808
1910
|
return output.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
1809
|
-
const
|
|
1810
|
-
const
|
|
1811
|
-
const
|
|
1812
|
-
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];
|
|
1813
1914
|
return { path, status };
|
|
1814
1915
|
});
|
|
1815
1916
|
}
|
|
1816
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}`;
|
|
1817
1942
|
let files;
|
|
1818
1943
|
try {
|
|
1819
|
-
const { stdout } = await execFileAsync2("git", ["
|
|
1820
|
-
files =
|
|
1944
|
+
const { stdout } = await execFileAsync2("git", ["diff", "--name-status", remoteRef, "HEAD"], { cwd });
|
|
1945
|
+
files = parseGitDiffNameStatus(stdout);
|
|
1821
1946
|
} catch (err) {
|
|
1822
1947
|
const message = err instanceof Error ? err.message : String(err);
|
|
1823
1948
|
return { label, files: [], diff: "", error: message };
|
|
1824
1949
|
}
|
|
1825
1950
|
if (files.length === 0) return { label, files, diff: "" };
|
|
1826
1951
|
try {
|
|
1827
|
-
const { stdout } = await execFileAsync2("git", ["diff", "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
1952
|
+
const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
1828
1953
|
return { label, files, diff: stdout };
|
|
1829
1954
|
} catch {
|
|
1830
1955
|
return { label, files, diff: "" };
|
|
@@ -2007,10 +2132,6 @@ function handleRequest(req, res) {
|
|
|
2007
2132
|
sendJson(res, 401, { error: "Ticket inv\xE1lido, expirado, ou projeto desconhecido." });
|
|
2008
2133
|
return;
|
|
2009
2134
|
}
|
|
2010
|
-
if (pathname === "/branch") {
|
|
2011
|
-
void handleBranchRequest(entry, res, relPath);
|
|
2012
|
-
return;
|
|
2013
|
-
}
|
|
2014
2135
|
if (pathname === "/diff") {
|
|
2015
2136
|
const projectId = typeof query.projectId === "string" ? query.projectId : "";
|
|
2016
2137
|
void handleDiffRequest(entry, res, projectId);
|
|
@@ -2093,11 +2214,18 @@ function attachFilesServer(httpServer, supabase, config) {
|
|
|
2093
2214
|
}
|
|
2094
2215
|
|
|
2095
2216
|
// src/agentEventsServer.ts
|
|
2217
|
+
var EMPTY_QUEUE = { queuedCardIds: [], pausedCardIds: [] };
|
|
2096
2218
|
var stateByProject = /* @__PURE__ */ new Map();
|
|
2219
|
+
var queueByProject = /* @__PURE__ */ new Map();
|
|
2220
|
+
var branchesByProject = /* @__PURE__ */ new Map();
|
|
2097
2221
|
var socketsByProject = /* @__PURE__ */ new Map();
|
|
2098
2222
|
function sendSnapshot(socket, projectId) {
|
|
2099
2223
|
const states = Array.from(stateByProject.get(projectId)?.values() ?? []);
|
|
2100
|
-
|
|
2224
|
+
const queue = queueByProject.get(projectId) ?? EMPTY_QUEUE;
|
|
2225
|
+
const branches = Array.from(branchesByProject.get(projectId)?.entries() ?? []).map(
|
|
2226
|
+
([label, branch]) => ({ label: label || null, branch })
|
|
2227
|
+
);
|
|
2228
|
+
socket.send(JSON.stringify({ type: "snapshot", states, queue, branches }));
|
|
2101
2229
|
}
|
|
2102
2230
|
function publishAgentStateEvent(projectId, cardId, sessionId, agentState) {
|
|
2103
2231
|
let projectState = stateByProject.get(projectId);
|
|
@@ -2113,6 +2241,29 @@ function publishAgentStateEvent(projectId, cardId, sessionId, agentState) {
|
|
|
2113
2241
|
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
2114
2242
|
}
|
|
2115
2243
|
}
|
|
2244
|
+
function publishQueueEvent(projectId, queue) {
|
|
2245
|
+
queueByProject.set(projectId, queue);
|
|
2246
|
+
const sockets = socketsByProject.get(projectId);
|
|
2247
|
+
if (!sockets || sockets.size === 0) return;
|
|
2248
|
+
const payload = JSON.stringify({ type: "queue", ...queue });
|
|
2249
|
+
for (const socket of sockets) {
|
|
2250
|
+
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function publishBranchEvent(projectId, label, branch) {
|
|
2254
|
+
let projectBranches = branchesByProject.get(projectId);
|
|
2255
|
+
if (!projectBranches) {
|
|
2256
|
+
projectBranches = /* @__PURE__ */ new Map();
|
|
2257
|
+
branchesByProject.set(projectId, projectBranches);
|
|
2258
|
+
}
|
|
2259
|
+
projectBranches.set(label ?? "", branch);
|
|
2260
|
+
const sockets = socketsByProject.get(projectId);
|
|
2261
|
+
if (!sockets || sockets.size === 0) return;
|
|
2262
|
+
const payload = JSON.stringify({ type: "branch", label, branch });
|
|
2263
|
+
for (const socket of sockets) {
|
|
2264
|
+
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2116
2267
|
function attachAgentEventsServer() {
|
|
2117
2268
|
const wss = new WebSocketServer({ noServer: true });
|
|
2118
2269
|
wss.on("connection", (socket, request) => {
|
|
@@ -2139,11 +2290,77 @@ function attachAgentEventsServer() {
|
|
|
2139
2290
|
return wss;
|
|
2140
2291
|
}
|
|
2141
2292
|
|
|
2142
|
-
// src/
|
|
2143
|
-
import {
|
|
2293
|
+
// src/branchWatcher.ts
|
|
2294
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
2295
|
+
import { watch } from "node:fs";
|
|
2144
2296
|
import { join as join10 } from "node:path";
|
|
2145
2297
|
import { promisify as promisify3 } from "node:util";
|
|
2146
|
-
var
|
|
2298
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2299
|
+
var DEBOUNCE_MS = 300;
|
|
2300
|
+
var watchedProjects = /* @__PURE__ */ new Map();
|
|
2301
|
+
async function readCurrentBranch(cwd) {
|
|
2302
|
+
try {
|
|
2303
|
+
const { stdout } = await execFileAsync3("git", ["branch", "--show-current"], { cwd });
|
|
2304
|
+
return stdout.trim();
|
|
2305
|
+
} catch {
|
|
2306
|
+
return "";
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
function watchTarget(projectId, label, cwd, state) {
|
|
2310
|
+
let debounceTimer = null;
|
|
2311
|
+
function check() {
|
|
2312
|
+
void readCurrentBranch(cwd).then((branch) => {
|
|
2313
|
+
if (!branch || state.lastBranch.get(label) === branch) return;
|
|
2314
|
+
state.lastBranch.set(label, branch);
|
|
2315
|
+
publishBranchEvent(projectId, label || null, branch);
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
let watcher;
|
|
2319
|
+
try {
|
|
2320
|
+
watcher = watch(join10(cwd, ".git", "HEAD"), () => {
|
|
2321
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
2322
|
+
debounceTimer = setTimeout(check, DEBOUNCE_MS);
|
|
2323
|
+
});
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
logger.runner.warn("BRANCH_WATCH", `Could not watch .git/HEAD under ${cwd}:`, err instanceof Error ? err.message : err);
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
state.watchers.push(watcher);
|
|
2329
|
+
check();
|
|
2330
|
+
}
|
|
2331
|
+
async function ensureBranchWatchers(supabase, config) {
|
|
2332
|
+
for (const project of config.registeredProjects) {
|
|
2333
|
+
if (watchedProjects.has(project.id)) continue;
|
|
2334
|
+
const state = { watchers: [], lastBranch: /* @__PURE__ */ new Map() };
|
|
2335
|
+
watchedProjects.set(project.id, state);
|
|
2336
|
+
let children;
|
|
2337
|
+
try {
|
|
2338
|
+
children = (await getProjectRepos(supabase, project.id)).map((repo) => repo.subpath);
|
|
2339
|
+
} catch (err) {
|
|
2340
|
+
logger.runner.error("BRANCH_WATCH", `Failed to list child repos for project ${project.id}:`, err);
|
|
2341
|
+
children = [];
|
|
2342
|
+
}
|
|
2343
|
+
if (children.length === 0) {
|
|
2344
|
+
watchTarget(project.id, "", project.repoPath, state);
|
|
2345
|
+
} else {
|
|
2346
|
+
for (const subpath of children) {
|
|
2347
|
+
watchTarget(project.id, subpath, join10(project.repoPath, subpath), state);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const stillRegistered = new Set(config.registeredProjects.map((project) => project.id));
|
|
2352
|
+
for (const [projectId, state] of watchedProjects) {
|
|
2353
|
+
if (stillRegistered.has(projectId)) continue;
|
|
2354
|
+
for (const watcher of state.watchers) watcher.close();
|
|
2355
|
+
watchedProjects.delete(projectId);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/columnActions.ts
|
|
2360
|
+
import { exec } from "node:child_process";
|
|
2361
|
+
import { join as join11 } from "node:path";
|
|
2362
|
+
import { promisify as promisify4 } from "node:util";
|
|
2363
|
+
var execAsync = promisify4(exec);
|
|
2147
2364
|
var OUTPUT_TAIL_CHARS = 4e3;
|
|
2148
2365
|
function tail(text) {
|
|
2149
2366
|
return text.length > OUTPUT_TAIL_CHARS ? text.slice(text.length - OUTPUT_TAIL_CHARS) : text;
|
|
@@ -2151,7 +2368,7 @@ function tail(text) {
|
|
|
2151
2368
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
2152
2369
|
if (children.length > 0) {
|
|
2153
2370
|
return children.flatMap(
|
|
2154
|
-
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd:
|
|
2371
|
+
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join11(repoRoot, child2.subpath), command: action.command }))
|
|
2155
2372
|
);
|
|
2156
2373
|
}
|
|
2157
2374
|
return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
|
|
@@ -2257,7 +2474,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
|
|
|
2257
2474
|
}
|
|
2258
2475
|
|
|
2259
2476
|
// src/deploy.ts
|
|
2260
|
-
import { join as
|
|
2477
|
+
import { join as join14 } from "node:path";
|
|
2261
2478
|
|
|
2262
2479
|
// src/devServer.ts
|
|
2263
2480
|
import { spawn as spawn4 } from "node:child_process";
|
|
@@ -2266,7 +2483,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
2266
2483
|
import { spawn as spawn3 } from "node:child_process";
|
|
2267
2484
|
import { accessSync, constants } from "node:fs";
|
|
2268
2485
|
import { homedir as homedir5 } from "node:os";
|
|
2269
|
-
import { delimiter, join as
|
|
2486
|
+
import { delimiter, join as join12 } from "node:path";
|
|
2270
2487
|
import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
|
|
2271
2488
|
var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
2272
2489
|
var STARTUP_TIMEOUT_MS = 2e4;
|
|
@@ -2277,15 +2494,15 @@ function wellKnownDirectories() {
|
|
|
2277
2494
|
const home = homedir5();
|
|
2278
2495
|
if (IS_WINDOWS2) {
|
|
2279
2496
|
const candidates = [
|
|
2280
|
-
process.env.ProgramFiles &&
|
|
2281
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2282
|
-
process.env.LOCALAPPDATA &&
|
|
2283
|
-
|
|
2284
|
-
process.env.ChocolateyInstall &&
|
|
2497
|
+
process.env.ProgramFiles && join12(process.env.ProgramFiles, "cloudflared"),
|
|
2498
|
+
process.env["ProgramFiles(x86)"] && join12(process.env["ProgramFiles(x86)"], "cloudflared"),
|
|
2499
|
+
process.env.LOCALAPPDATA && join12(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
|
|
2500
|
+
join12(home, "scoop", "shims"),
|
|
2501
|
+
process.env.ChocolateyInstall && join12(process.env.ChocolateyInstall, "bin")
|
|
2285
2502
|
];
|
|
2286
2503
|
return candidates.filter((dir) => Boolean(dir));
|
|
2287
2504
|
}
|
|
2288
|
-
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
|
|
2505
|
+
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join12(home, ".local", "bin")];
|
|
2289
2506
|
}
|
|
2290
2507
|
function pathDirectories() {
|
|
2291
2508
|
return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
@@ -2301,7 +2518,7 @@ function isExecutableFile(path) {
|
|
|
2301
2518
|
function findInstalledBinary() {
|
|
2302
2519
|
const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
|
|
2303
2520
|
const directories = [...pathDirectories(), ...wellKnownDirectories()];
|
|
2304
|
-
const candidates = [...override ? [override] : [], ...directories.map((dir) =>
|
|
2521
|
+
const candidates = [...override ? [override] : [], ...directories.map((dir) => join12(dir, BINARY_NAME))];
|
|
2305
2522
|
return candidates.find(isExecutableFile) ?? null;
|
|
2306
2523
|
}
|
|
2307
2524
|
async function locateCloudflared() {
|
|
@@ -2488,12 +2705,12 @@ function stopAll() {
|
|
|
2488
2705
|
}
|
|
2489
2706
|
|
|
2490
2707
|
// src/git.ts
|
|
2491
|
-
import { execFile as
|
|
2492
|
-
import { promisify as
|
|
2493
|
-
var
|
|
2708
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
2709
|
+
import { promisify as promisify5 } from "node:util";
|
|
2710
|
+
var execFileAsync4 = promisify5(execFile4);
|
|
2494
2711
|
async function pushBranchToMain(repoPath) {
|
|
2495
2712
|
try {
|
|
2496
|
-
await
|
|
2713
|
+
await execFileAsync4("git", ["push", "origin", "HEAD:main"], { cwd: repoPath });
|
|
2497
2714
|
return { ok: true };
|
|
2498
2715
|
} catch (err) {
|
|
2499
2716
|
const stderr = err.stderr;
|
|
@@ -2655,8 +2872,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
|
2655
2872
|
// src/cardAttachments.ts
|
|
2656
2873
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2657
2874
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
2658
|
-
import { extname, join as
|
|
2659
|
-
var ATTACHMENTS_ROOT =
|
|
2875
|
+
import { extname, join as join13 } from "node:path";
|
|
2876
|
+
var ATTACHMENTS_ROOT = join13(tmpdir6(), "agent-board", "card-attachments");
|
|
2660
2877
|
function sanitizeFileName(fileName) {
|
|
2661
2878
|
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2662
2879
|
}
|
|
@@ -2668,13 +2885,13 @@ async function downloadCardAttachments(supabase, cardId) {
|
|
|
2668
2885
|
}
|
|
2669
2886
|
const rows = data ?? [];
|
|
2670
2887
|
if (rows.length === 0) return [];
|
|
2671
|
-
const targetDir =
|
|
2888
|
+
const targetDir = join13(ATTACHMENTS_ROOT, cardId);
|
|
2672
2889
|
await mkdir2(targetDir, { recursive: true });
|
|
2673
2890
|
const paths = [];
|
|
2674
2891
|
for (const row of rows) {
|
|
2675
2892
|
const baseName = sanitizeFileName(row.file_name);
|
|
2676
2893
|
const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
|
|
2677
|
-
const localPath =
|
|
2894
|
+
const localPath = join13(targetDir, `${row.id}-${fileName}`);
|
|
2678
2895
|
try {
|
|
2679
2896
|
const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
|
|
2680
2897
|
if (downloadError) throw downloadError;
|
|
@@ -2709,7 +2926,7 @@ ${list}`;
|
|
|
2709
2926
|
function buildInstructionsWithMcpContext(instructions, cardId, projectId) {
|
|
2710
2927
|
return `${instructions}
|
|
2711
2928
|
|
|
2712
|
-
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.`;
|
|
2929
|
+
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.`;
|
|
2713
2930
|
}
|
|
2714
2931
|
|
|
2715
2932
|
// src/usageResume.ts
|
|
@@ -2819,8 +3036,9 @@ async function reconcileStaleSessions(supabase, config, projectId) {
|
|
|
2819
3036
|
async function dispatchRunnerQueue(supabase, config) {
|
|
2820
3037
|
if (dispatchingRunner) return;
|
|
2821
3038
|
dispatchingRunner = true;
|
|
3039
|
+
let queued = [];
|
|
2822
3040
|
try {
|
|
2823
|
-
|
|
3041
|
+
queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
2824
3042
|
if (queued.length === 0) return;
|
|
2825
3043
|
const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
|
|
2826
3044
|
const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
|
|
@@ -2841,6 +3059,17 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2841
3059
|
const projectId = entry.cards.project_id;
|
|
2842
3060
|
if (runningProjectIds.has(projectId) || promotedThisPass.has(projectId)) continue;
|
|
2843
3061
|
if (freeProjectSlots <= 0) break;
|
|
3062
|
+
const prerequisiteCardId = entry.cards.prerequisite_card_id;
|
|
3063
|
+
if (prerequisiteCardId) {
|
|
3064
|
+
const prerequisiteAgentState = await getLatestAgentStateForCard(supabase, prerequisiteCardId);
|
|
3065
|
+
if (prerequisiteAgentState !== "done") {
|
|
3066
|
+
logger.card.info(
|
|
3067
|
+
entry.card_id,
|
|
3068
|
+
`Waiting on prerequisite card ${prerequisiteCardId} to reach agent state "done" (currently "${prerequisiteAgentState ?? "none"}") before starting \u2014 leaving queued.`
|
|
3069
|
+
);
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
2844
3073
|
try {
|
|
2845
3074
|
await promoteQueuedSession(supabase, config, entry);
|
|
2846
3075
|
promotedThisPass.add(projectId);
|
|
@@ -2850,9 +3079,33 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2850
3079
|
}
|
|
2851
3080
|
}
|
|
2852
3081
|
} finally {
|
|
3082
|
+
await publishQueueSnapshots(supabase, config, queued).catch(
|
|
3083
|
+
(err) => logger.runner.error("ORCHESTRATOR", "Failed to publish queue snapshots:", err)
|
|
3084
|
+
);
|
|
2853
3085
|
dispatchingRunner = false;
|
|
2854
3086
|
}
|
|
2855
3087
|
}
|
|
3088
|
+
async function publishQueueSnapshots(supabase, config, queued) {
|
|
3089
|
+
const paused = await listPausedSessionsForRunner(supabase, config.ownerUserId);
|
|
3090
|
+
const queuedByProject = /* @__PURE__ */ new Map();
|
|
3091
|
+
for (const entry of queued) {
|
|
3092
|
+
const list = queuedByProject.get(entry.cards.project_id) ?? [];
|
|
3093
|
+
list.push(entry.card_id);
|
|
3094
|
+
queuedByProject.set(entry.cards.project_id, list);
|
|
3095
|
+
}
|
|
3096
|
+
const pausedByProject = /* @__PURE__ */ new Map();
|
|
3097
|
+
for (const { card_id: cardId, project_id: projectId } of paused) {
|
|
3098
|
+
const list = pausedByProject.get(projectId) ?? [];
|
|
3099
|
+
list.push(cardId);
|
|
3100
|
+
pausedByProject.set(projectId, list);
|
|
3101
|
+
}
|
|
3102
|
+
for (const project of config.registeredProjects) {
|
|
3103
|
+
publishQueueEvent(project.id, {
|
|
3104
|
+
queuedCardIds: queuedByProject.get(project.id) ?? [],
|
|
3105
|
+
pausedCardIds: pausedByProject.get(project.id) ?? []
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
2856
3109
|
async function promoteQueuedSession(supabase, config, entry) {
|
|
2857
3110
|
const existingHandle = getPtySession(entry.id);
|
|
2858
3111
|
if (existingHandle) {
|
|
@@ -3101,7 +3354,7 @@ async function handleDeployRequested(supabase, config, project) {
|
|
|
3101
3354
|
}
|
|
3102
3355
|
const children = await getProjectRepos(supabase, project.id);
|
|
3103
3356
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3104
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path:
|
|
3357
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join14(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
|
|
3105
3358
|
const failures = [];
|
|
3106
3359
|
for (const target of targets) {
|
|
3107
3360
|
const result = await pushBranchToMain(target.path);
|
|
@@ -3235,7 +3488,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
|
|
|
3235
3488
|
}
|
|
3236
3489
|
|
|
3237
3490
|
// src/review.ts
|
|
3238
|
-
import { join as
|
|
3491
|
+
import { join as join15 } from "node:path";
|
|
3239
3492
|
var inFlightCardIds2 = /* @__PURE__ */ new Set();
|
|
3240
3493
|
async function handleCardEnteredRevisao(supabase, config, card) {
|
|
3241
3494
|
if (inFlightCardIds2.has(card.id)) return;
|
|
@@ -3244,7 +3497,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
|
|
|
3244
3497
|
const project = await getProject(supabase, card.project_id);
|
|
3245
3498
|
const children = await getProjectRepos(supabase, project.id);
|
|
3246
3499
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3247
|
-
const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd:
|
|
3500
|
+
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 }] : [];
|
|
3248
3501
|
if (targets.length === 0) {
|
|
3249
3502
|
logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
|
|
3250
3503
|
return;
|
|
@@ -3513,6 +3766,7 @@ async function startRunner() {
|
|
|
3513
3766
|
void ensureFilesTickets(supabase, config).catch((err) => logger.runner.error("FILES", "ensureFilesTickets failed:", err));
|
|
3514
3767
|
void ensureSetupTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureSetupTicket failed:", err));
|
|
3515
3768
|
void ensureAgentTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureAgentTicket failed:", err));
|
|
3769
|
+
void ensureBranchWatchers(supabase, config).catch((err) => logger.runner.error("BRANCH_WATCH", "ensureBranchWatchers failed:", err));
|
|
3516
3770
|
const presenceChannel = supabase.channel(`runner-presence:${config.runnerId}`);
|
|
3517
3771
|
presenceChannel.subscribe((status) => {
|
|
3518
3772
|
if (status === "SUBSCRIBED") {
|
|
@@ -3609,6 +3863,7 @@ async function startRunner() {
|
|
|
3609
3863
|
await ensureFilesTickets(supabase, config);
|
|
3610
3864
|
await ensureSetupTicket(supabase, config);
|
|
3611
3865
|
await ensureAgentTicket(supabase, config);
|
|
3866
|
+
await ensureBranchWatchers(supabase, config).catch((err) => logger.runner.error("BRANCH_WATCH", "ensureBranchWatchers failed:", err));
|
|
3612
3867
|
await scanForWork(supabase, config);
|
|
3613
3868
|
await reconcileUsageLimitedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileUsageLimitedSessions failed:", err));
|
|
3614
3869
|
await checkPendingDeploys(supabase, config).catch((err) => logger.runner.error("DEPLOY", "checkPendingDeploys failed:", err));
|