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/index.js
CHANGED
|
@@ -38,8 +38,9 @@ var EVAL_INTERVAL_MS = 2e3;
|
|
|
38
38
|
var ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
39
39
|
var BOX_DRAWING_PATTERN = /[─-╿]/g;
|
|
40
40
|
var WORKING_PATTERN = /esc to interrupt/i;
|
|
41
|
+
var BACKGROUND_SHELL_PATTERN = /\b[1-9]\d*\s+shells?\b/i;
|
|
41
42
|
function isAgentHookMoment(value) {
|
|
42
|
-
return value === "implementing" || value === "waiting_plan_approval" || value === "commit_detected" || value === "turn_stopped" || value === "notification";
|
|
43
|
+
return value === "implementing" || value === "tool_active" || value === "waiting_plan_approval" || value === "commit_detected" || value === "turn_stopped" || value === "notification";
|
|
43
44
|
}
|
|
44
45
|
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;
|
|
45
46
|
var PLAN_APPROVAL_PATTERN = /would you like to proceed\??/i;
|
|
@@ -63,7 +64,10 @@ function classifyAgentOutput(recentOutput, options) {
|
|
|
63
64
|
if (options.planApprovalPending) return "waiting_plan_approval";
|
|
64
65
|
if (USER_PROMPT_PATTERNS.some((pattern) => pattern.test(recentOutput))) return "waiting_user";
|
|
65
66
|
const idleThreshold = options.toolCallPending ? IDLE_AFTER_MS_TOOL_PENDING : IDLE_AFTER_MS;
|
|
66
|
-
if (options.silentForMs >= idleThreshold)
|
|
67
|
+
if (options.silentForMs >= idleThreshold) {
|
|
68
|
+
if (BACKGROUND_SHELL_PATTERN.test(recentOutput)) return "waiting_background";
|
|
69
|
+
return options.commitDetected ? "done" : "waiting_user";
|
|
70
|
+
}
|
|
67
71
|
return options.previous;
|
|
68
72
|
}
|
|
69
73
|
var AgentActivityWatcher = class {
|
|
@@ -124,6 +128,10 @@ var AgentActivityWatcher = class {
|
|
|
124
128
|
this.commitDetected = false;
|
|
125
129
|
this.setState("implementing");
|
|
126
130
|
break;
|
|
131
|
+
case "tool_active":
|
|
132
|
+
if (this.usageLimited) break;
|
|
133
|
+
this.setState("implementing");
|
|
134
|
+
break;
|
|
127
135
|
case "waiting_plan_approval":
|
|
128
136
|
this.planApprovalPending = true;
|
|
129
137
|
this.setState("waiting_plan_approval");
|
|
@@ -133,17 +141,29 @@ var AgentActivityWatcher = class {
|
|
|
133
141
|
break;
|
|
134
142
|
case "turn_stopped":
|
|
135
143
|
if (this.usageLimited) break;
|
|
144
|
+
if (BACKGROUND_SHELL_PATTERN.test(this.recentOutput())) {
|
|
145
|
+
this.setState("waiting_background");
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
136
148
|
this.setState(this.commitDetected ? "done" : "waiting_user");
|
|
137
149
|
break;
|
|
138
150
|
case "notification":
|
|
139
151
|
if (this.usageLimited || this.planApprovalPending) break;
|
|
140
|
-
if (this.state === "implementing" || this.state === "starting")
|
|
152
|
+
if (this.state === "implementing" || this.state === "starting") {
|
|
153
|
+
this.setState(BACKGROUND_SHELL_PATTERN.test(this.recentOutput()) ? "waiting_background" : "waiting_user");
|
|
154
|
+
}
|
|
141
155
|
break;
|
|
142
156
|
}
|
|
143
157
|
}
|
|
144
|
-
|
|
158
|
+
/** Text still inside RECENT_WINDOW_MS as of `now`, pruning anything older. Shared by evaluate()
|
|
159
|
+
* (the text/idle-timer path) and reportHookMoment() (the hook path), which both need to know
|
|
160
|
+
* whether the background-shell indicator is currently on screen. */
|
|
161
|
+
recentOutput(now = Date.now()) {
|
|
145
162
|
while (this.chunks.length > 0 && now - this.chunks[0].at > RECENT_WINDOW_MS) this.chunks.shift();
|
|
146
|
-
|
|
163
|
+
return this.chunks.map((chunk) => chunk.text).join("");
|
|
164
|
+
}
|
|
165
|
+
evaluate(now = Date.now()) {
|
|
166
|
+
const next = classifyAgentOutput(this.recentOutput(now), {
|
|
147
167
|
silentForMs: now - this.lastOutputAt,
|
|
148
168
|
usageLimited: this.usageLimited,
|
|
149
169
|
planApprovalPending: this.planApprovalPending,
|
|
@@ -289,7 +309,7 @@ function stopPtySession(sessionId) {
|
|
|
289
309
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
290
310
|
import { spawn } from "node-pty";
|
|
291
311
|
var SHELL_COMMAND = process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "bash";
|
|
292
|
-
function spawnShellPty(repoPath, sessionIdPrefix) {
|
|
312
|
+
function spawnShellPty(repoPath, sessionIdPrefix, initialCommand) {
|
|
293
313
|
const sessionId = `${sessionIdPrefix}:${randomUUID2()}`;
|
|
294
314
|
const ticket = randomUUID2();
|
|
295
315
|
const pty = spawn(SHELL_COMMAND, [], {
|
|
@@ -299,14 +319,15 @@ function spawnShellPty(repoPath, sessionIdPrefix) {
|
|
|
299
319
|
cwd: repoPath
|
|
300
320
|
});
|
|
301
321
|
registerPtySession(sessionId, pty, ticket);
|
|
322
|
+
if (initialCommand) pty.write(`${initialCommand}\r`);
|
|
302
323
|
pty.onExit(({ exitCode }) => {
|
|
303
324
|
logger.runner.info("SHELL", `Shell ${sessionId} exited (code ${exitCode}).`);
|
|
304
325
|
stopPtySession(sessionId);
|
|
305
326
|
});
|
|
306
327
|
return { sessionId, ticket };
|
|
307
328
|
}
|
|
308
|
-
function openShellSession(repoPath) {
|
|
309
|
-
return spawnShellPty(repoPath, "shell");
|
|
329
|
+
function openShellSession(repoPath, initialCommand) {
|
|
330
|
+
return spawnShellPty(repoPath, "shell", initialCommand);
|
|
310
331
|
}
|
|
311
332
|
function closeShellSession(sessionId) {
|
|
312
333
|
stopPtySession(sessionId);
|
|
@@ -315,9 +336,8 @@ var columnShells = /* @__PURE__ */ new Map();
|
|
|
315
336
|
function openOrReuseColumnShell(key, repoPath, initialCommand) {
|
|
316
337
|
const existing = columnShells.get(key);
|
|
317
338
|
if (existing && getPtySession(existing.sessionId)) return existing;
|
|
318
|
-
const shell = spawnShellPty(repoPath, "column-shell");
|
|
339
|
+
const shell = spawnShellPty(repoPath, "column-shell", initialCommand);
|
|
319
340
|
columnShells.set(key, shell);
|
|
320
|
-
if (initialCommand) getPtySession(shell.sessionId)?.pty.write(`${initialCommand}\r`);
|
|
321
341
|
return shell;
|
|
322
342
|
}
|
|
323
343
|
function closeColumnShell(key) {
|
|
@@ -585,11 +605,19 @@ async function getRunningSessionForProject(supabase, projectId, ownerUserId) {
|
|
|
585
605
|
}
|
|
586
606
|
async function listQueuedSessionsForRunner(supabase, ownerUserId) {
|
|
587
607
|
const { data, error } = await supabase.from("sessions").select(
|
|
588
|
-
"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)"
|
|
608
|
+
"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)"
|
|
589
609
|
).eq("cards.assigned_user_id", ownerUserId).eq("status", "queued").order("created_at", { ascending: true });
|
|
590
610
|
if (error) throw error;
|
|
591
611
|
return data;
|
|
592
612
|
}
|
|
613
|
+
async function listPausedSessionsForRunner(supabase, ownerUserId) {
|
|
614
|
+
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");
|
|
615
|
+
if (error) throw error;
|
|
616
|
+
return data.map((row) => ({
|
|
617
|
+
card_id: row.card_id,
|
|
618
|
+
project_id: row.cards.project_id
|
|
619
|
+
}));
|
|
620
|
+
}
|
|
593
621
|
async function createSession(supabase, userId, cardId, resumeClaudeSessionId) {
|
|
594
622
|
const { data, error } = await supabase.from("sessions").insert({
|
|
595
623
|
card_id: cardId,
|
|
@@ -648,6 +676,11 @@ async function getLastClaudeSessionIdForCard(supabase, cardId) {
|
|
|
648
676
|
if (error) throw error;
|
|
649
677
|
return data?.claude_session_id ?? null;
|
|
650
678
|
}
|
|
679
|
+
async function getLatestAgentStateForCard(supabase, cardId) {
|
|
680
|
+
const { data, error } = await supabase.from("sessions").select("agent_state").eq("card_id", cardId).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
681
|
+
if (error) throw error;
|
|
682
|
+
return data?.agent_state ?? null;
|
|
683
|
+
}
|
|
651
684
|
async function updateCard(supabase, cardId, changes) {
|
|
652
685
|
const { error } = await supabase.from("cards").update({
|
|
653
686
|
...changes.column !== void 0 ? { column_name: changes.column } : {},
|
|
@@ -768,6 +801,9 @@ var AGENT_MOMENT_HOOK_SCRIPT_PATH = join2(tmpdir2(), "agent-board-agent-moment-h
|
|
|
768
801
|
var SCRIPT_SOURCE = `#!/usr/bin/env node
|
|
769
802
|
"use strict";
|
|
770
803
|
const http = require("node:http");
|
|
804
|
+
const fs = require("node:fs");
|
|
805
|
+
const os = require("node:os");
|
|
806
|
+
const path = require("node:path");
|
|
771
807
|
|
|
772
808
|
function readStdin() {
|
|
773
809
|
return new Promise((resolve) => {
|
|
@@ -786,6 +822,58 @@ function readStdin() {
|
|
|
786
822
|
const COMMIT_SUCCESS_PATTERN = /\\[\\S+\\s+[0-9a-f]{7,40}\\]/;
|
|
787
823
|
const GIT_COMMIT_COMMAND_PATTERN = /\\bgit\\b[^\\n]*\\bcommit\\b/i;
|
|
788
824
|
|
|
825
|
+
// Catches the agent writing a tool invocation as plain prose (e.g. "Agent({ subagent_type:
|
|
826
|
+
// ... })") in what was meant to be its final message, instead of actually emitting a
|
|
827
|
+
// structured tool call \u2014 Claude Code then just ends the turn, having "announced" work it
|
|
828
|
+
// never did. Restricted to names that look like a real tool (PascalCase built-ins, or
|
|
829
|
+
// "mcp__server__tool") rather than any "Word({" in the text, so ordinary prose describing
|
|
830
|
+
// hypothetical code doesn't trip it.
|
|
831
|
+
const KNOWN_TOOL_NAMES = new Set([
|
|
832
|
+
"Agent", "AskUserQuestion", "Bash", "PowerShell", "Edit", "Write", "Read", "Glob", "Grep",
|
|
833
|
+
"NotebookEdit", "WebFetch", "WebSearch", "Skill", "ExitPlanMode", "EnterPlanMode",
|
|
834
|
+
"ToolSearch", "ListAgents", "SendMessage", "ScheduleWakeup", "SendFeedback", "ReportFindings",
|
|
835
|
+
"Artifact", "CronCreate", "CronDelete", "CronList", "DesignSync", "EndConversation",
|
|
836
|
+
"EnterWorktree", "ExitWorktree", "LSP", "ListMcpResourcesTool", "Monitor",
|
|
837
|
+
"PushNotification", "ReadMcpResourceDirTool", "ReadMcpResourceTool", "RemoteTrigger",
|
|
838
|
+
"TaskOutput", "TaskStop", "TodoWrite", "SlashCommand", "Task",
|
|
839
|
+
]);
|
|
840
|
+
const TOOL_CALL_LOOKALIKE_PATTERN = /\\b([A-Z][A-Za-z0-9]*|mcp__[A-Za-z0-9_]+)\\(\\s*\\{/g;
|
|
841
|
+
|
|
842
|
+
function findFakeToolCallName(text) {
|
|
843
|
+
if (!text) return null;
|
|
844
|
+
TOOL_CALL_LOOKALIKE_PATTERN.lastIndex = 0;
|
|
845
|
+
let match;
|
|
846
|
+
while ((match = TOOL_CALL_LOOKALIKE_PATTERN.exec(text))) {
|
|
847
|
+
const name = match[1];
|
|
848
|
+
if (name.startsWith("mcp__") || KNOWN_TOOL_NAMES.has(name)) return name;
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Bounds the auto-continue below to at most one nudge per session: if the model fakes a
|
|
854
|
+
// tool call again right after being told to actually call it, something stranger than this
|
|
855
|
+
// specific slip is going on, and blocking forever would risk an unbounded loop. One file,
|
|
856
|
+
// shared by every session on this machine, keyed by AGENT_BOARD_SESSION_ID \u2014 a miscount
|
|
857
|
+
// under concurrent sessions just means the cap is off by one, never a correctness issue.
|
|
858
|
+
const FAKE_CALL_STATE_PATH = path.join(os.tmpdir(), "agent-board-fake-tool-call-hook-state.json");
|
|
859
|
+
const MAX_AUTO_CONTINUES_PER_SESSION = 1;
|
|
860
|
+
|
|
861
|
+
function fakeCallAttemptsSoFar(sessionId) {
|
|
862
|
+
try {
|
|
863
|
+
const state = JSON.parse(fs.readFileSync(FAKE_CALL_STATE_PATH, "utf8"));
|
|
864
|
+
return state[sessionId] || 0;
|
|
865
|
+
} catch {
|
|
866
|
+
return 0;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function recordFakeCallAttempt(sessionId) {
|
|
871
|
+
let state = {};
|
|
872
|
+
try { state = JSON.parse(fs.readFileSync(FAKE_CALL_STATE_PATH, "utf8")); } catch {}
|
|
873
|
+
state[sessionId] = (state[sessionId] || 0) + 1;
|
|
874
|
+
try { fs.writeFileSync(FAKE_CALL_STATE_PATH, JSON.stringify(state)); } catch {}
|
|
875
|
+
}
|
|
876
|
+
|
|
789
877
|
// Which "moment" (if any) this hook invocation reports, from the single JSON payload Claude
|
|
790
878
|
// Code gives a hook on stdin. Returning null means "nothing conclusive" \u2014 the hook exits
|
|
791
879
|
// quietly and the runner's text-based fallback (agentActivity.ts) stays in charge.
|
|
@@ -801,6 +889,12 @@ function detectMoment(input) {
|
|
|
801
889
|
// "would you like to proceed?", before the CLI even renders the prompt.
|
|
802
890
|
if (eventName === "PreToolUse" && input.tool_name === "ExitPlanMode") return "waiting_plan_approval";
|
|
803
891
|
|
|
892
|
+
// Any other tool about to run is also a precise "the turn is active" signal \u2014 notably the
|
|
893
|
+
// one that resumes a permission prompt (y/n) or an AskUserQuestion answer: neither of those
|
|
894
|
+
// is a new UserPromptSubmit, so without this the badge had nothing but the text fallback to
|
|
895
|
+
// notice the agent is working again once the human replies.
|
|
896
|
+
if (eventName === "PreToolUse") return "tool_active";
|
|
897
|
+
|
|
804
898
|
// The plan prompt resolving one way or the other: approved, the turn resumes implementing
|
|
805
899
|
// right away with no new UserPromptSubmit to catch it; asked to revise, a fresh
|
|
806
900
|
// UserPromptSubmit follows almost immediately anyway. Either way "implementing" is correct
|
|
@@ -809,14 +903,22 @@ function detectMoment(input) {
|
|
|
809
903
|
|
|
810
904
|
// A commit that actually succeeded, read off the Bash tool's own structured result instead
|
|
811
905
|
// of guessing from scrollback text (exact, and immune to locale/color/format changes to
|
|
812
|
-
// git's own banner).
|
|
906
|
+
// git's own banner). Any other Bash completion still reports "tool_active" below, same as
|
|
907
|
+
// every other tool.
|
|
813
908
|
if (eventName === "PostToolUse" && input.tool_name === "Bash") {
|
|
814
909
|
const command = String((input.tool_input && input.tool_input.command) || "");
|
|
815
|
-
if (
|
|
816
|
-
|
|
817
|
-
|
|
910
|
+
if (GIT_COMMIT_COMMAND_PATTERN.test(command)) {
|
|
911
|
+
const response = JSON.stringify(input.tool_response || "");
|
|
912
|
+
if (COMMIT_SUCCESS_PATTERN.test(response)) return "commit_detected";
|
|
913
|
+
}
|
|
818
914
|
}
|
|
819
915
|
|
|
916
|
+
// Any other tool finishing is the same "still active" signal as PreToolUse above \u2014 this is
|
|
917
|
+
// what actually clears a yellow badge once a permission-gated or AskUserQuestion tool call
|
|
918
|
+
// resumes and completes (there's no hook fired at the exact instant permission is granted,
|
|
919
|
+
// only at the tool call's start and end).
|
|
920
|
+
if (eventName === "PostToolUse") return "tool_active";
|
|
921
|
+
|
|
820
922
|
// The turn actually ending \u2014 Claude Code back at its input box, no idle timer needed.
|
|
821
923
|
if (eventName === "Stop") return "turn_stopped";
|
|
822
924
|
|
|
@@ -858,6 +960,23 @@ function reportToRunner(sessionId, ticket, port, moment) {
|
|
|
858
960
|
(async () => {
|
|
859
961
|
try {
|
|
860
962
|
const input = JSON.parse(await readStdin());
|
|
963
|
+
if (input.hook_event_name === "Stop") {
|
|
964
|
+
const fakeCallName = findFakeToolCallName(input.lastAssistantMessage);
|
|
965
|
+
const sessionId = process.env.AGENT_BOARD_SESSION_ID || "unknown";
|
|
966
|
+
if (fakeCallName && fakeCallAttemptsSoFar(sessionId) < MAX_AUTO_CONTINUES_PER_SESSION) {
|
|
967
|
+
recordFakeCallAttempt(sessionId);
|
|
968
|
+
process.stdout.write(JSON.stringify({
|
|
969
|
+
decision: "block",
|
|
970
|
+
reason:
|
|
971
|
+
\`You wrote "\${fakeCallName}({...})" as plain text in your final message instead of \` +
|
|
972
|
+
"actually calling that tool \u2014 no such call happened, so nothing was delegated/executed. " +
|
|
973
|
+
"If you meant to use it, call it for real now as a structured tool call, don't just " +
|
|
974
|
+
"describe it. If you already finished without needing it, say so explicitly instead of " +
|
|
975
|
+
"writing pseudocode that looks like a call.",
|
|
976
|
+
}));
|
|
977
|
+
process.exit(0);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
861
980
|
const moment = detectMoment(input);
|
|
862
981
|
if (moment) {
|
|
863
982
|
const sessionId = process.env.AGENT_BOARD_SESSION_ID;
|
|
@@ -883,11 +1002,8 @@ function agentMomentHookSettingsFragment() {
|
|
|
883
1002
|
return {
|
|
884
1003
|
hooks: {
|
|
885
1004
|
UserPromptSubmit: [{ hooks: [hookEntry] }],
|
|
886
|
-
PreToolUse: [{ matcher: "
|
|
887
|
-
PostToolUse: [
|
|
888
|
-
{ matcher: "ExitPlanMode", hooks: [hookEntry] },
|
|
889
|
-
{ matcher: "Bash", hooks: [hookEntry] }
|
|
890
|
-
],
|
|
1005
|
+
PreToolUse: [{ matcher: "*", hooks: [hookEntry] }],
|
|
1006
|
+
PostToolUse: [{ matcher: "*", hooks: [hookEntry] }],
|
|
891
1007
|
Stop: [{ hooks: [hookEntry] }],
|
|
892
1008
|
Notification: [{ hooks: [hookEntry] }]
|
|
893
1009
|
}
|
|
@@ -1627,47 +1743,51 @@ async function handleAgentHookReport(req, res) {
|
|
|
1627
1743
|
res.writeHead(204, { "Access-Control-Allow-Origin": "*" });
|
|
1628
1744
|
res.end();
|
|
1629
1745
|
}
|
|
1630
|
-
async function handleBranchRequest(entry, res, relPath) {
|
|
1631
|
-
let cwd;
|
|
1632
|
-
try {
|
|
1633
|
-
cwd = relPath ? resolveWithinRepo(entry.repoRoot, relPath) : entry.repoRoot;
|
|
1634
|
-
} catch (err) {
|
|
1635
|
-
if (err instanceof HttpError) {
|
|
1636
|
-
sendJson(res, err.status, { error: err.message });
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
throw err;
|
|
1640
|
-
}
|
|
1641
|
-
try {
|
|
1642
|
-
const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
|
|
1643
|
-
sendJson(res, 200, { branch: stdout.trim() });
|
|
1644
|
-
} catch (err) {
|
|
1645
|
-
logger.runner.error("FILES", "Unexpected error handling a branch request:", err);
|
|
1646
|
-
sendJson(res, 500, { error: "Erro interno do runner." });
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
1746
|
var DIFF_EXEC_OPTIONS = { maxBuffer: 10 * 1024 * 1024 };
|
|
1650
|
-
function
|
|
1747
|
+
function parseGitDiffNameStatus(output) {
|
|
1651
1748
|
return output.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
1652
|
-
const
|
|
1653
|
-
const
|
|
1654
|
-
const
|
|
1655
|
-
const path = arrowIndex >= 0 ? rest.slice(arrowIndex + 4) : rest;
|
|
1749
|
+
const columns = line.split(" ");
|
|
1750
|
+
const status = columns[0].slice(0, 1);
|
|
1751
|
+
const path = columns[columns.length - 1];
|
|
1656
1752
|
return { path, status };
|
|
1657
1753
|
});
|
|
1658
1754
|
}
|
|
1659
1755
|
async function getRepoDiffForTarget(label, cwd) {
|
|
1756
|
+
let branch;
|
|
1757
|
+
try {
|
|
1758
|
+
const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
|
|
1759
|
+
branch = stdout.trim();
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1762
|
+
return { label, files: [], diff: "", error: message };
|
|
1763
|
+
}
|
|
1764
|
+
if (!branch) {
|
|
1765
|
+
return { label, files: [], diff: "", error: "HEAD destacado: sem branch local para comparar com a origin." };
|
|
1766
|
+
}
|
|
1767
|
+
try {
|
|
1768
|
+
await execFileAsync2("git", ["fetch", "origin", branch], { cwd });
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1771
|
+
const notOnRemote = /couldn't find remote ref/i.test(message);
|
|
1772
|
+
return {
|
|
1773
|
+
label,
|
|
1774
|
+
files: [],
|
|
1775
|
+
diff: "",
|
|
1776
|
+
error: notOnRemote ? `A branch "${branch}" ainda n\xE3o existe na origin.` : message
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
const remoteRef = `origin/${branch}`;
|
|
1660
1780
|
let files;
|
|
1661
1781
|
try {
|
|
1662
|
-
const { stdout } = await execFileAsync2("git", ["
|
|
1663
|
-
files =
|
|
1782
|
+
const { stdout } = await execFileAsync2("git", ["diff", "--name-status", remoteRef, "HEAD"], { cwd });
|
|
1783
|
+
files = parseGitDiffNameStatus(stdout);
|
|
1664
1784
|
} catch (err) {
|
|
1665
1785
|
const message = err instanceof Error ? err.message : String(err);
|
|
1666
1786
|
return { label, files: [], diff: "", error: message };
|
|
1667
1787
|
}
|
|
1668
1788
|
if (files.length === 0) return { label, files, diff: "" };
|
|
1669
1789
|
try {
|
|
1670
|
-
const { stdout } = await execFileAsync2("git", ["diff", "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
1790
|
+
const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
1671
1791
|
return { label, files, diff: stdout };
|
|
1672
1792
|
} catch {
|
|
1673
1793
|
return { label, files, diff: "" };
|
|
@@ -1850,10 +1970,6 @@ function handleRequest(req, res) {
|
|
|
1850
1970
|
sendJson(res, 401, { error: "Ticket inv\xE1lido, expirado, ou projeto desconhecido." });
|
|
1851
1971
|
return;
|
|
1852
1972
|
}
|
|
1853
|
-
if (pathname === "/branch") {
|
|
1854
|
-
void handleBranchRequest(entry, res, relPath);
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
1973
|
if (pathname === "/diff") {
|
|
1858
1974
|
const projectId = typeof query.projectId === "string" ? query.projectId : "";
|
|
1859
1975
|
void handleDiffRequest(entry, res, projectId);
|
|
@@ -1890,7 +2006,9 @@ function handleRequest(req, res) {
|
|
|
1890
2006
|
return;
|
|
1891
2007
|
}
|
|
1892
2008
|
if (pathname === "/shell/open") {
|
|
1893
|
-
|
|
2009
|
+
const kind = typeof query.kind === "string" ? query.kind : "shell";
|
|
2010
|
+
if (!COLUMN_SHELL_KINDS.includes(kind)) throw new HttpError(400, "Par\xE2metro kind inv\xE1lido.");
|
|
2011
|
+
sendJson(res, 200, openShellSession(entry.repoRoot, kind === "claude" ? CLAUDE_BYPASS_COMMAND : void 0));
|
|
1894
2012
|
return;
|
|
1895
2013
|
}
|
|
1896
2014
|
if (pathname === "/shell/close") {
|
|
@@ -1936,11 +2054,18 @@ function attachFilesServer(httpServer, supabase, config) {
|
|
|
1936
2054
|
}
|
|
1937
2055
|
|
|
1938
2056
|
// src/agentEventsServer.ts
|
|
2057
|
+
var EMPTY_QUEUE = { queuedCardIds: [], pausedCardIds: [] };
|
|
1939
2058
|
var stateByProject = /* @__PURE__ */ new Map();
|
|
2059
|
+
var queueByProject = /* @__PURE__ */ new Map();
|
|
2060
|
+
var branchesByProject = /* @__PURE__ */ new Map();
|
|
1940
2061
|
var socketsByProject = /* @__PURE__ */ new Map();
|
|
1941
2062
|
function sendSnapshot(socket, projectId) {
|
|
1942
2063
|
const states = Array.from(stateByProject.get(projectId)?.values() ?? []);
|
|
1943
|
-
|
|
2064
|
+
const queue = queueByProject.get(projectId) ?? EMPTY_QUEUE;
|
|
2065
|
+
const branches = Array.from(branchesByProject.get(projectId)?.entries() ?? []).map(
|
|
2066
|
+
([label, branch]) => ({ label: label || null, branch })
|
|
2067
|
+
);
|
|
2068
|
+
socket.send(JSON.stringify({ type: "snapshot", states, queue, branches }));
|
|
1944
2069
|
}
|
|
1945
2070
|
function publishAgentStateEvent(projectId, cardId, sessionId, agentState) {
|
|
1946
2071
|
let projectState = stateByProject.get(projectId);
|
|
@@ -1956,6 +2081,29 @@ function publishAgentStateEvent(projectId, cardId, sessionId, agentState) {
|
|
|
1956
2081
|
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
1957
2082
|
}
|
|
1958
2083
|
}
|
|
2084
|
+
function publishQueueEvent(projectId, queue) {
|
|
2085
|
+
queueByProject.set(projectId, queue);
|
|
2086
|
+
const sockets = socketsByProject.get(projectId);
|
|
2087
|
+
if (!sockets || sockets.size === 0) return;
|
|
2088
|
+
const payload = JSON.stringify({ type: "queue", ...queue });
|
|
2089
|
+
for (const socket of sockets) {
|
|
2090
|
+
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
function publishBranchEvent(projectId, label, branch) {
|
|
2094
|
+
let projectBranches = branchesByProject.get(projectId);
|
|
2095
|
+
if (!projectBranches) {
|
|
2096
|
+
projectBranches = /* @__PURE__ */ new Map();
|
|
2097
|
+
branchesByProject.set(projectId, projectBranches);
|
|
2098
|
+
}
|
|
2099
|
+
projectBranches.set(label ?? "", branch);
|
|
2100
|
+
const sockets = socketsByProject.get(projectId);
|
|
2101
|
+
if (!sockets || sockets.size === 0) return;
|
|
2102
|
+
const payload = JSON.stringify({ type: "branch", label, branch });
|
|
2103
|
+
for (const socket of sockets) {
|
|
2104
|
+
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
1959
2107
|
function attachAgentEventsServer() {
|
|
1960
2108
|
const wss = new WebSocketServer({ noServer: true });
|
|
1961
2109
|
wss.on("connection", (socket, request) => {
|
|
@@ -2027,8 +2175,8 @@ function resolveRepoPath(config, relativeRepoPath) {
|
|
|
2027
2175
|
|
|
2028
2176
|
// src/auth.ts
|
|
2029
2177
|
var SESSION_FILE = join9(CONFIG_DIR, "session.json");
|
|
2030
|
-
var DEFAULT_SUPABASE_URL = "https://
|
|
2031
|
-
var DEFAULT_SUPABASE_ANON_KEY = "
|
|
2178
|
+
var DEFAULT_SUPABASE_URL = "https://supabase.lealtek.com";
|
|
2179
|
+
var DEFAULT_SUPABASE_ANON_KEY = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc5MjAwNTE4MCwiZXhwIjo0OTQ3Njc4NzgwLCJyb2xlIjoiYW5vbiJ9.gxNuRa1STVA4lQPLghoGbuqV9_21lVOG1yXSu9WNmJ4";
|
|
2032
2180
|
function resolveSupabaseUrl() {
|
|
2033
2181
|
return process.env.AGENT_BOARD_SUPABASE_URL ?? DEFAULT_SUPABASE_URL;
|
|
2034
2182
|
}
|
|
@@ -2083,11 +2231,77 @@ async function requireSession(supabase) {
|
|
|
2083
2231
|
return session;
|
|
2084
2232
|
}
|
|
2085
2233
|
|
|
2086
|
-
// src/
|
|
2087
|
-
import {
|
|
2234
|
+
// src/branchWatcher.ts
|
|
2235
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
2236
|
+
import { watch } from "node:fs";
|
|
2088
2237
|
import { join as join10 } from "node:path";
|
|
2089
2238
|
import { promisify as promisify3 } from "node:util";
|
|
2090
|
-
var
|
|
2239
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2240
|
+
var DEBOUNCE_MS = 300;
|
|
2241
|
+
var watchedProjects = /* @__PURE__ */ new Map();
|
|
2242
|
+
async function readCurrentBranch(cwd) {
|
|
2243
|
+
try {
|
|
2244
|
+
const { stdout } = await execFileAsync3("git", ["branch", "--show-current"], { cwd });
|
|
2245
|
+
return stdout.trim();
|
|
2246
|
+
} catch {
|
|
2247
|
+
return "";
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
function watchTarget(projectId, label, cwd, state) {
|
|
2251
|
+
let debounceTimer = null;
|
|
2252
|
+
function check() {
|
|
2253
|
+
void readCurrentBranch(cwd).then((branch) => {
|
|
2254
|
+
if (!branch || state.lastBranch.get(label) === branch) return;
|
|
2255
|
+
state.lastBranch.set(label, branch);
|
|
2256
|
+
publishBranchEvent(projectId, label || null, branch);
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
let watcher;
|
|
2260
|
+
try {
|
|
2261
|
+
watcher = watch(join10(cwd, ".git", "HEAD"), () => {
|
|
2262
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
2263
|
+
debounceTimer = setTimeout(check, DEBOUNCE_MS);
|
|
2264
|
+
});
|
|
2265
|
+
} catch (err) {
|
|
2266
|
+
logger.runner.warn("BRANCH_WATCH", `Could not watch .git/HEAD under ${cwd}:`, err instanceof Error ? err.message : err);
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
state.watchers.push(watcher);
|
|
2270
|
+
check();
|
|
2271
|
+
}
|
|
2272
|
+
async function ensureBranchWatchers(supabase, config) {
|
|
2273
|
+
for (const project of config.registeredProjects) {
|
|
2274
|
+
if (watchedProjects.has(project.id)) continue;
|
|
2275
|
+
const state = { watchers: [], lastBranch: /* @__PURE__ */ new Map() };
|
|
2276
|
+
watchedProjects.set(project.id, state);
|
|
2277
|
+
let children;
|
|
2278
|
+
try {
|
|
2279
|
+
children = (await getProjectRepos(supabase, project.id)).map((repo) => repo.subpath);
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
logger.runner.error("BRANCH_WATCH", `Failed to list child repos for project ${project.id}:`, err);
|
|
2282
|
+
children = [];
|
|
2283
|
+
}
|
|
2284
|
+
if (children.length === 0) {
|
|
2285
|
+
watchTarget(project.id, "", project.repoPath, state);
|
|
2286
|
+
} else {
|
|
2287
|
+
for (const subpath of children) {
|
|
2288
|
+
watchTarget(project.id, subpath, join10(project.repoPath, subpath), state);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const stillRegistered = new Set(config.registeredProjects.map((project) => project.id));
|
|
2293
|
+
for (const [projectId, state] of watchedProjects) {
|
|
2294
|
+
if (stillRegistered.has(projectId)) continue;
|
|
2295
|
+
for (const watcher of state.watchers) watcher.close();
|
|
2296
|
+
watchedProjects.delete(projectId);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// src/columnActions.ts
|
|
2301
|
+
import { exec } from "node:child_process";
|
|
2302
|
+
import { join as join11 } from "node:path";
|
|
2303
|
+
import { promisify as promisify4 } from "node:util";
|
|
2304
|
+
var execAsync = promisify4(exec);
|
|
2091
2305
|
var OUTPUT_TAIL_CHARS = 4e3;
|
|
2092
2306
|
function tail(text) {
|
|
2093
2307
|
return text.length > OUTPUT_TAIL_CHARS ? text.slice(text.length - OUTPUT_TAIL_CHARS) : text;
|
|
@@ -2095,7 +2309,7 @@ function tail(text) {
|
|
|
2095
2309
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
2096
2310
|
if (children.length > 0) {
|
|
2097
2311
|
return children.flatMap(
|
|
2098
|
-
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd:
|
|
2312
|
+
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join11(repoRoot, child2.subpath), command: action.command }))
|
|
2099
2313
|
);
|
|
2100
2314
|
}
|
|
2101
2315
|
return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
|
|
@@ -2201,7 +2415,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
|
|
|
2201
2415
|
}
|
|
2202
2416
|
|
|
2203
2417
|
// src/deploy.ts
|
|
2204
|
-
import { join as
|
|
2418
|
+
import { join as join14 } from "node:path";
|
|
2205
2419
|
|
|
2206
2420
|
// src/devServer.ts
|
|
2207
2421
|
import { spawn as spawn4 } from "node:child_process";
|
|
@@ -2210,7 +2424,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
2210
2424
|
import { spawn as spawn3 } from "node:child_process";
|
|
2211
2425
|
import { accessSync, constants } from "node:fs";
|
|
2212
2426
|
import { homedir as homedir5 } from "node:os";
|
|
2213
|
-
import { delimiter, join as
|
|
2427
|
+
import { delimiter, join as join12 } from "node:path";
|
|
2214
2428
|
import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
|
|
2215
2429
|
var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
2216
2430
|
var STARTUP_TIMEOUT_MS = 2e4;
|
|
@@ -2221,15 +2435,15 @@ function wellKnownDirectories() {
|
|
|
2221
2435
|
const home = homedir5();
|
|
2222
2436
|
if (IS_WINDOWS2) {
|
|
2223
2437
|
const candidates = [
|
|
2224
|
-
process.env.ProgramFiles &&
|
|
2225
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2226
|
-
process.env.LOCALAPPDATA &&
|
|
2227
|
-
|
|
2228
|
-
process.env.ChocolateyInstall &&
|
|
2438
|
+
process.env.ProgramFiles && join12(process.env.ProgramFiles, "cloudflared"),
|
|
2439
|
+
process.env["ProgramFiles(x86)"] && join12(process.env["ProgramFiles(x86)"], "cloudflared"),
|
|
2440
|
+
process.env.LOCALAPPDATA && join12(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
|
|
2441
|
+
join12(home, "scoop", "shims"),
|
|
2442
|
+
process.env.ChocolateyInstall && join12(process.env.ChocolateyInstall, "bin")
|
|
2229
2443
|
];
|
|
2230
2444
|
return candidates.filter((dir) => Boolean(dir));
|
|
2231
2445
|
}
|
|
2232
|
-
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
|
|
2446
|
+
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join12(home, ".local", "bin")];
|
|
2233
2447
|
}
|
|
2234
2448
|
function pathDirectories() {
|
|
2235
2449
|
return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
@@ -2245,7 +2459,7 @@ function isExecutableFile(path) {
|
|
|
2245
2459
|
function findInstalledBinary() {
|
|
2246
2460
|
const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
|
|
2247
2461
|
const directories = [...pathDirectories(), ...wellKnownDirectories()];
|
|
2248
|
-
const candidates = [...override ? [override] : [], ...directories.map((dir) =>
|
|
2462
|
+
const candidates = [...override ? [override] : [], ...directories.map((dir) => join12(dir, BINARY_NAME))];
|
|
2249
2463
|
return candidates.find(isExecutableFile) ?? null;
|
|
2250
2464
|
}
|
|
2251
2465
|
async function locateCloudflared() {
|
|
@@ -2432,12 +2646,12 @@ function stopAll() {
|
|
|
2432
2646
|
}
|
|
2433
2647
|
|
|
2434
2648
|
// src/git.ts
|
|
2435
|
-
import { execFile as
|
|
2436
|
-
import { promisify as
|
|
2437
|
-
var
|
|
2649
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
2650
|
+
import { promisify as promisify5 } from "node:util";
|
|
2651
|
+
var execFileAsync4 = promisify5(execFile4);
|
|
2438
2652
|
async function pushBranchToMain(repoPath) {
|
|
2439
2653
|
try {
|
|
2440
|
-
await
|
|
2654
|
+
await execFileAsync4("git", ["push", "origin", "HEAD:main"], { cwd: repoPath });
|
|
2441
2655
|
return { ok: true };
|
|
2442
2656
|
} catch (err) {
|
|
2443
2657
|
const stderr = err.stderr;
|
|
@@ -2599,8 +2813,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
|
2599
2813
|
// src/cardAttachments.ts
|
|
2600
2814
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2601
2815
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
2602
|
-
import { extname, join as
|
|
2603
|
-
var ATTACHMENTS_ROOT =
|
|
2816
|
+
import { extname, join as join13 } from "node:path";
|
|
2817
|
+
var ATTACHMENTS_ROOT = join13(tmpdir6(), "agent-board", "card-attachments");
|
|
2604
2818
|
function sanitizeFileName(fileName) {
|
|
2605
2819
|
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2606
2820
|
}
|
|
@@ -2612,13 +2826,13 @@ async function downloadCardAttachments(supabase, cardId) {
|
|
|
2612
2826
|
}
|
|
2613
2827
|
const rows = data ?? [];
|
|
2614
2828
|
if (rows.length === 0) return [];
|
|
2615
|
-
const targetDir =
|
|
2829
|
+
const targetDir = join13(ATTACHMENTS_ROOT, cardId);
|
|
2616
2830
|
await mkdir2(targetDir, { recursive: true });
|
|
2617
2831
|
const paths = [];
|
|
2618
2832
|
for (const row of rows) {
|
|
2619
2833
|
const baseName = sanitizeFileName(row.file_name);
|
|
2620
2834
|
const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
|
|
2621
|
-
const localPath =
|
|
2835
|
+
const localPath = join13(targetDir, `${row.id}-${fileName}`);
|
|
2622
2836
|
try {
|
|
2623
2837
|
const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
|
|
2624
2838
|
if (downloadError) throw downloadError;
|
|
@@ -2653,7 +2867,7 @@ ${list}`;
|
|
|
2653
2867
|
function buildInstructionsWithMcpContext(instructions, cardId, projectId) {
|
|
2654
2868
|
return `${instructions}
|
|
2655
2869
|
|
|
2656
|
-
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.`;
|
|
2870
|
+
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.`;
|
|
2657
2871
|
}
|
|
2658
2872
|
|
|
2659
2873
|
// src/usageResume.ts
|
|
@@ -2763,8 +2977,9 @@ async function reconcileStaleSessions(supabase, config, projectId) {
|
|
|
2763
2977
|
async function dispatchRunnerQueue(supabase, config) {
|
|
2764
2978
|
if (dispatchingRunner) return;
|
|
2765
2979
|
dispatchingRunner = true;
|
|
2980
|
+
let queued = [];
|
|
2766
2981
|
try {
|
|
2767
|
-
|
|
2982
|
+
queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
2768
2983
|
if (queued.length === 0) return;
|
|
2769
2984
|
const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
|
|
2770
2985
|
const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
|
|
@@ -2785,6 +3000,17 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2785
3000
|
const projectId = entry.cards.project_id;
|
|
2786
3001
|
if (runningProjectIds.has(projectId) || promotedThisPass.has(projectId)) continue;
|
|
2787
3002
|
if (freeProjectSlots <= 0) break;
|
|
3003
|
+
const prerequisiteCardId = entry.cards.prerequisite_card_id;
|
|
3004
|
+
if (prerequisiteCardId) {
|
|
3005
|
+
const prerequisiteAgentState = await getLatestAgentStateForCard(supabase, prerequisiteCardId);
|
|
3006
|
+
if (prerequisiteAgentState !== "done") {
|
|
3007
|
+
logger.card.info(
|
|
3008
|
+
entry.card_id,
|
|
3009
|
+
`Waiting on prerequisite card ${prerequisiteCardId} to reach agent state "done" (currently "${prerequisiteAgentState ?? "none"}") before starting \u2014 leaving queued.`
|
|
3010
|
+
);
|
|
3011
|
+
continue;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
2788
3014
|
try {
|
|
2789
3015
|
await promoteQueuedSession(supabase, config, entry);
|
|
2790
3016
|
promotedThisPass.add(projectId);
|
|
@@ -2794,9 +3020,33 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2794
3020
|
}
|
|
2795
3021
|
}
|
|
2796
3022
|
} finally {
|
|
3023
|
+
await publishQueueSnapshots(supabase, config, queued).catch(
|
|
3024
|
+
(err) => logger.runner.error("ORCHESTRATOR", "Failed to publish queue snapshots:", err)
|
|
3025
|
+
);
|
|
2797
3026
|
dispatchingRunner = false;
|
|
2798
3027
|
}
|
|
2799
3028
|
}
|
|
3029
|
+
async function publishQueueSnapshots(supabase, config, queued) {
|
|
3030
|
+
const paused = await listPausedSessionsForRunner(supabase, config.ownerUserId);
|
|
3031
|
+
const queuedByProject = /* @__PURE__ */ new Map();
|
|
3032
|
+
for (const entry of queued) {
|
|
3033
|
+
const list = queuedByProject.get(entry.cards.project_id) ?? [];
|
|
3034
|
+
list.push(entry.card_id);
|
|
3035
|
+
queuedByProject.set(entry.cards.project_id, list);
|
|
3036
|
+
}
|
|
3037
|
+
const pausedByProject = /* @__PURE__ */ new Map();
|
|
3038
|
+
for (const { card_id: cardId, project_id: projectId } of paused) {
|
|
3039
|
+
const list = pausedByProject.get(projectId) ?? [];
|
|
3040
|
+
list.push(cardId);
|
|
3041
|
+
pausedByProject.set(projectId, list);
|
|
3042
|
+
}
|
|
3043
|
+
for (const project of config.registeredProjects) {
|
|
3044
|
+
publishQueueEvent(project.id, {
|
|
3045
|
+
queuedCardIds: queuedByProject.get(project.id) ?? [],
|
|
3046
|
+
pausedCardIds: pausedByProject.get(project.id) ?? []
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
2800
3050
|
async function promoteQueuedSession(supabase, config, entry) {
|
|
2801
3051
|
const existingHandle = getPtySession(entry.id);
|
|
2802
3052
|
if (existingHandle) {
|
|
@@ -3045,7 +3295,7 @@ async function handleDeployRequested(supabase, config, project) {
|
|
|
3045
3295
|
}
|
|
3046
3296
|
const children = await getProjectRepos(supabase, project.id);
|
|
3047
3297
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3048
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path:
|
|
3298
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join14(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
|
|
3049
3299
|
const failures = [];
|
|
3050
3300
|
for (const target of targets) {
|
|
3051
3301
|
const result = await pushBranchToMain(target.path);
|
|
@@ -3179,7 +3429,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
|
|
|
3179
3429
|
}
|
|
3180
3430
|
|
|
3181
3431
|
// src/review.ts
|
|
3182
|
-
import { join as
|
|
3432
|
+
import { join as join15 } from "node:path";
|
|
3183
3433
|
var inFlightCardIds2 = /* @__PURE__ */ new Set();
|
|
3184
3434
|
async function handleCardEnteredRevisao(supabase, config, card) {
|
|
3185
3435
|
if (inFlightCardIds2.has(card.id)) return;
|
|
@@ -3188,7 +3438,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
|
|
|
3188
3438
|
const project = await getProject(supabase, card.project_id);
|
|
3189
3439
|
const children = await getProjectRepos(supabase, project.id);
|
|
3190
3440
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3191
|
-
const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd:
|
|
3441
|
+
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 }] : [];
|
|
3192
3442
|
if (targets.length === 0) {
|
|
3193
3443
|
logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
|
|
3194
3444
|
return;
|
|
@@ -3457,6 +3707,7 @@ async function startRunner() {
|
|
|
3457
3707
|
void ensureFilesTickets(supabase, config).catch((err) => logger.runner.error("FILES", "ensureFilesTickets failed:", err));
|
|
3458
3708
|
void ensureSetupTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureSetupTicket failed:", err));
|
|
3459
3709
|
void ensureAgentTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureAgentTicket failed:", err));
|
|
3710
|
+
void ensureBranchWatchers(supabase, config).catch((err) => logger.runner.error("BRANCH_WATCH", "ensureBranchWatchers failed:", err));
|
|
3460
3711
|
const presenceChannel = supabase.channel(`runner-presence:${config.runnerId}`);
|
|
3461
3712
|
presenceChannel.subscribe((status) => {
|
|
3462
3713
|
if (status === "SUBSCRIBED") {
|
|
@@ -3553,6 +3804,7 @@ async function startRunner() {
|
|
|
3553
3804
|
await ensureFilesTickets(supabase, config);
|
|
3554
3805
|
await ensureSetupTicket(supabase, config);
|
|
3555
3806
|
await ensureAgentTicket(supabase, config);
|
|
3807
|
+
await ensureBranchWatchers(supabase, config).catch((err) => logger.runner.error("BRANCH_WATCH", "ensureBranchWatchers failed:", err));
|
|
3556
3808
|
await scanForWork(supabase, config);
|
|
3557
3809
|
await reconcileUsageLimitedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileUsageLimitedSessions failed:", err));
|
|
3558
3810
|
await checkPendingDeploys(supabase, config).catch((err) => logger.runner.error("DEPLOY", "checkPendingDeploys failed:", err));
|