agent-board-runner 0.1.16 → 0.1.18
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 +194 -25
- package/dist/index.js +194 -25
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -897,12 +897,37 @@ async function getRunningSessionForProject(supabase, projectId, runnerId) {
|
|
|
897
897
|
const row = data;
|
|
898
898
|
return { id: row.id, cardId: row.card_id, agentState: row.agent_state };
|
|
899
899
|
}
|
|
900
|
+
function compareQueueOrder(a, b) {
|
|
901
|
+
const orderA = a.cards.dev_queue_order;
|
|
902
|
+
const orderB = b.cards.dev_queue_order;
|
|
903
|
+
if (orderA !== null && orderB !== null) return orderA - orderB;
|
|
904
|
+
if (orderA !== null) return -1;
|
|
905
|
+
if (orderB !== null) return 1;
|
|
906
|
+
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
907
|
+
}
|
|
908
|
+
function applyDevQueueOrder(queued) {
|
|
909
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
910
|
+
for (const entry of queued) {
|
|
911
|
+
const list = byProject.get(entry.cards.project_id) ?? [];
|
|
912
|
+
list.push(entry);
|
|
913
|
+
byProject.set(entry.cards.project_id, list);
|
|
914
|
+
}
|
|
915
|
+
for (const entries of byProject.values()) entries.sort(compareQueueOrder);
|
|
916
|
+
const seenProjects = /* @__PURE__ */ new Set();
|
|
917
|
+
const result = [];
|
|
918
|
+
for (const entry of queued) {
|
|
919
|
+
if (seenProjects.has(entry.cards.project_id)) continue;
|
|
920
|
+
seenProjects.add(entry.cards.project_id);
|
|
921
|
+
result.push(...byProject.get(entry.cards.project_id));
|
|
922
|
+
}
|
|
923
|
+
return result;
|
|
924
|
+
}
|
|
900
925
|
async function listQueuedSessionsForRunner(supabase, runnerId) {
|
|
901
926
|
const { data, error } = await supabase.from("sessions").select(
|
|
902
|
-
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved)"
|
|
927
|
+
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved, dev_queue_order)"
|
|
903
928
|
).eq("runner_id", runnerId).eq("status", "queued").order("created_at", { ascending: true });
|
|
904
929
|
if (error) throw error;
|
|
905
|
-
return data;
|
|
930
|
+
return applyDevQueueOrder(data);
|
|
906
931
|
}
|
|
907
932
|
async function listPausedSessionsForRunner(supabase, runnerId) {
|
|
908
933
|
const { data, error } = await supabase.from("sessions").select("card_id, cards!card_id!inner(project_id)").eq("runner_id", runnerId).eq("status", "paused");
|
|
@@ -1010,7 +1035,8 @@ async function updateCard(supabase, cardId, changes) {
|
|
|
1010
1035
|
...changes.customActionStatus !== void 0 ? { custom_action_status: changes.customActionStatus } : {},
|
|
1011
1036
|
...changes.customActionOutput !== void 0 ? { custom_action_output: changes.customActionOutput } : {},
|
|
1012
1037
|
...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {},
|
|
1013
|
-
...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {}
|
|
1038
|
+
...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {},
|
|
1039
|
+
...changes.returnToBacklogRequestedAt !== void 0 ? { return_to_backlog_requested_at: changes.returnToBacklogRequestedAt } : {}
|
|
1014
1040
|
}).eq("id", cardId);
|
|
1015
1041
|
if (error) throw error;
|
|
1016
1042
|
}
|
|
@@ -2077,7 +2103,22 @@ function parseGitDiffNameStatus(output) {
|
|
|
2077
2103
|
const columns = line.split(" ");
|
|
2078
2104
|
const status = columns[0].slice(0, 1);
|
|
2079
2105
|
const path = columns[columns.length - 1];
|
|
2080
|
-
return { path, status };
|
|
2106
|
+
return { path, status, additions: null, deletions: null, binary: false };
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
function parseGitDiffNumstat(output) {
|
|
2110
|
+
return output.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
2111
|
+
const [added, deleted, path] = line.split(" ");
|
|
2112
|
+
const binary = added === "-" && deleted === "-";
|
|
2113
|
+
return { path, additions: binary ? null : Number(added), deletions: binary ? null : Number(deleted), binary };
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
function mergeNumstat(files, numstat) {
|
|
2117
|
+
const byPath = new Map(numstat.map((entry) => [entry.path, entry]));
|
|
2118
|
+
return files.map((file) => {
|
|
2119
|
+
const stat = byPath.get(file.path);
|
|
2120
|
+
if (!stat) return file;
|
|
2121
|
+
return { ...file, additions: stat.additions, deletions: stat.deletions, binary: stat.binary };
|
|
2081
2122
|
});
|
|
2082
2123
|
}
|
|
2083
2124
|
async function getRepoDiffForTarget(label, cwd) {
|
|
@@ -2111,14 +2152,84 @@ async function getRepoDiffForTarget(label, cwd) {
|
|
|
2111
2152
|
files = parseGitDiffNameStatus(stdout);
|
|
2112
2153
|
} catch (err) {
|
|
2113
2154
|
const message = err instanceof Error ? err.message : String(err);
|
|
2114
|
-
return { label, files: [], diff: "", error: message };
|
|
2155
|
+
return { label, files: [], diff: "", branch, error: message };
|
|
2156
|
+
}
|
|
2157
|
+
if (files.length === 0) return { label, files, diff: "", branch };
|
|
2158
|
+
try {
|
|
2159
|
+
const { stdout: numstatOut } = await execFileAsync2("git", ["diff", "--numstat", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
2160
|
+
files = mergeNumstat(files, parseGitDiffNumstat(numstatOut));
|
|
2161
|
+
} catch {
|
|
2115
2162
|
}
|
|
2116
|
-
if (files.length === 0) return { label, files, diff: "" };
|
|
2117
2163
|
try {
|
|
2118
2164
|
const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
2119
|
-
return { label, files, diff: stdout };
|
|
2165
|
+
return { label, files, diff: stdout, branch };
|
|
2120
2166
|
} catch {
|
|
2121
|
-
return { label, files, diff: "" };
|
|
2167
|
+
return { label, files, diff: "", branch };
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
async function resolveDiffTargetCwd(entry, projectId, label) {
|
|
2171
|
+
if (!label) return entry.repoRoot;
|
|
2172
|
+
if (!statuslineSupabase) throw new HttpError(400, "Reposit\xF3rio filho n\xE3o dispon\xEDvel.");
|
|
2173
|
+
const children = await getProjectRepos(statuslineSupabase, projectId);
|
|
2174
|
+
const child2 = children.find((candidate) => candidate.subpath === label);
|
|
2175
|
+
if (!child2) throw new HttpError(400, "Reposit\xF3rio filho desconhecido.");
|
|
2176
|
+
return join9(entry.repoRoot, child2.subpath);
|
|
2177
|
+
}
|
|
2178
|
+
async function readFileAtRevision(cwd, ref, relPath) {
|
|
2179
|
+
try {
|
|
2180
|
+
const { stdout } = await execFileAsync2("git", ["show", `${ref}:${relPath}`], {
|
|
2181
|
+
cwd,
|
|
2182
|
+
maxBuffer: DIFF_EXEC_OPTIONS.maxBuffer,
|
|
2183
|
+
encoding: "buffer"
|
|
2184
|
+
});
|
|
2185
|
+
return stdout;
|
|
2186
|
+
} catch {
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
function toFileRevisionJson(buffer, imageContentType) {
|
|
2191
|
+
if (buffer === null) return { exists: false, content: null, binary: false };
|
|
2192
|
+
if (looksBinary(buffer)) {
|
|
2193
|
+
return {
|
|
2194
|
+
exists: true,
|
|
2195
|
+
content: null,
|
|
2196
|
+
binary: true,
|
|
2197
|
+
...imageContentType ? { imageBase64: buffer.toString("base64"), imageContentType } : {}
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
return { exists: true, content: buffer.toString("utf8"), binary: false };
|
|
2201
|
+
}
|
|
2202
|
+
async function handleDiffFileRequest(entry, res, projectId, relPath, label) {
|
|
2203
|
+
if (!relPath) {
|
|
2204
|
+
sendJson(res, 400, { error: "Par\xE2metro path \xE9 obrigat\xF3rio." });
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
try {
|
|
2208
|
+
const cwd = await resolveDiffTargetCwd(entry, projectId, label);
|
|
2209
|
+
resolveWithinRepo(cwd, relPath);
|
|
2210
|
+
const { stdout: branchOut } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
|
|
2211
|
+
const branch = branchOut.trim();
|
|
2212
|
+
if (!branch) {
|
|
2213
|
+
sendJson(res, 400, { error: "HEAD destacado: sem branch local para comparar com a origin." });
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const [originalBuffer, modifiedBuffer] = await Promise.all([
|
|
2217
|
+
readFileAtRevision(cwd, `origin/${branch}`, relPath),
|
|
2218
|
+
readFileAtRevision(cwd, "HEAD", relPath)
|
|
2219
|
+
]);
|
|
2220
|
+
const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
|
|
2221
|
+
const imageContentType = MIME_TYPES_BY_EXT[ext];
|
|
2222
|
+
sendJson(res, 200, {
|
|
2223
|
+
original: toFileRevisionJson(originalBuffer, imageContentType),
|
|
2224
|
+
modified: toFileRevisionJson(modifiedBuffer, imageContentType)
|
|
2225
|
+
});
|
|
2226
|
+
} catch (err) {
|
|
2227
|
+
if (err instanceof HttpError) {
|
|
2228
|
+
sendJson(res, err.status, { error: err.message });
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
logger.runner.error("FILES", "Unexpected error handling a diff-file request:", err);
|
|
2232
|
+
sendJson(res, 500, { error: "Erro interno do runner." });
|
|
2122
2233
|
}
|
|
2123
2234
|
}
|
|
2124
2235
|
async function handleDiffRequest(entry, res, projectId) {
|
|
@@ -2303,6 +2414,12 @@ function handleRequest(req, res) {
|
|
|
2303
2414
|
void handleDiffRequest(entry, res, projectId);
|
|
2304
2415
|
return;
|
|
2305
2416
|
}
|
|
2417
|
+
if (pathname === "/diff-file") {
|
|
2418
|
+
const projectId = typeof query.projectId === "string" ? query.projectId : "";
|
|
2419
|
+
const label = typeof query.label === "string" ? query.label : null;
|
|
2420
|
+
void handleDiffFileRequest(entry, res, projectId, relPath, label);
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2306
2423
|
try {
|
|
2307
2424
|
if (pathname === "/tree") {
|
|
2308
2425
|
sendJson(res, 200, { entries: listDirectory(entry.repoRoot, relPath) });
|
|
@@ -2551,13 +2668,49 @@ function toSessionName(title) {
|
|
|
2551
2668
|
// src/agentSession.ts
|
|
2552
2669
|
var TUI_IDLE_MS = 1500;
|
|
2553
2670
|
var TUI_READY_TIMEOUT_MS = 6e4;
|
|
2554
|
-
var
|
|
2671
|
+
var PASTE_SETTLE_MS = 200;
|
|
2672
|
+
var PASTE_SETTLE_TIMEOUT_MS = 5e3;
|
|
2555
2673
|
var PASTE_START = "\x1B[200~";
|
|
2556
2674
|
var PASTE_END = "\x1B[201~";
|
|
2557
2675
|
var SHIFT_TAB = "\x1B[Z";
|
|
2558
2676
|
var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
|
|
2559
2677
|
var SHIFT_TAB_DELAY_MS = 400;
|
|
2560
2678
|
var PLAN_MODE_INDICATOR = /plan mode on/i;
|
|
2679
|
+
function writePasteAndSubmit(pty, cardId, message, context) {
|
|
2680
|
+
pty.write(`${PASTE_START}${message}${PASTE_END}`);
|
|
2681
|
+
let settleTimer;
|
|
2682
|
+
let ceilingTimer;
|
|
2683
|
+
let dataSubscription;
|
|
2684
|
+
let exitSubscription;
|
|
2685
|
+
let submitted = false;
|
|
2686
|
+
const submit = () => {
|
|
2687
|
+
if (submitted) return;
|
|
2688
|
+
submitted = true;
|
|
2689
|
+
clearTimeout(settleTimer);
|
|
2690
|
+
clearTimeout(ceilingTimer);
|
|
2691
|
+
dataSubscription?.dispose();
|
|
2692
|
+
exitSubscription?.dispose();
|
|
2693
|
+
try {
|
|
2694
|
+
pty.write("\r");
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
logger.card.error(cardId, `Failed to submit ${context} to the agent:`, err);
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
const cancel = () => {
|
|
2700
|
+
submitted = true;
|
|
2701
|
+
clearTimeout(settleTimer);
|
|
2702
|
+
clearTimeout(ceilingTimer);
|
|
2703
|
+
dataSubscription?.dispose();
|
|
2704
|
+
exitSubscription?.dispose();
|
|
2705
|
+
};
|
|
2706
|
+
dataSubscription = pty.onData(() => {
|
|
2707
|
+
clearTimeout(settleTimer);
|
|
2708
|
+
settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
|
|
2709
|
+
});
|
|
2710
|
+
settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
|
|
2711
|
+
ceilingTimer = setTimeout(submit, PASTE_SETTLE_TIMEOUT_MS);
|
|
2712
|
+
exitSubscription = pty.onExit(cancel);
|
|
2713
|
+
}
|
|
2561
2714
|
function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
|
|
2562
2715
|
ensureDirTrusted(repoPath);
|
|
2563
2716
|
ensureMcpServersTrusted(repoPath);
|
|
@@ -2601,14 +2754,7 @@ function sendMessageWhenIdle(pty, cardId, message, planFirst) {
|
|
|
2601
2754
|
if (!stopWaiting()) return;
|
|
2602
2755
|
if (planFirst) await enterPlanMode(pty, cardId);
|
|
2603
2756
|
try {
|
|
2604
|
-
pty
|
|
2605
|
-
setTimeout(() => {
|
|
2606
|
-
try {
|
|
2607
|
-
pty.write("\r");
|
|
2608
|
-
} catch (err) {
|
|
2609
|
-
logger.card.error(cardId, "Failed to submit the card instructions to the agent:", err);
|
|
2610
|
-
}
|
|
2611
|
-
}, SUBMIT_DELAY_MS);
|
|
2757
|
+
writePasteAndSubmit(pty, cardId, message, "the card instructions");
|
|
2612
2758
|
} catch (err) {
|
|
2613
2759
|
logger.card.error(cardId, "Failed to send the card instructions to the agent:", err);
|
|
2614
2760
|
}
|
|
@@ -2644,14 +2790,7 @@ async function enterPlanMode(pty, cardId) {
|
|
|
2644
2790
|
}
|
|
2645
2791
|
function sendMessageToIdleSession(pty, cardId, message) {
|
|
2646
2792
|
try {
|
|
2647
|
-
pty
|
|
2648
|
-
setTimeout(() => {
|
|
2649
|
-
try {
|
|
2650
|
-
pty.write("\r");
|
|
2651
|
-
} catch (err) {
|
|
2652
|
-
logger.card.error(cardId, "Failed to submit a message to the idle session:", err);
|
|
2653
|
-
}
|
|
2654
|
-
}, SUBMIT_DELAY_MS);
|
|
2793
|
+
writePasteAndSubmit(pty, cardId, message, "a message");
|
|
2655
2794
|
} catch (err) {
|
|
2656
2795
|
logger.card.error(cardId, "Failed to send a message to the idle session:", err);
|
|
2657
2796
|
}
|
|
@@ -3413,6 +3552,24 @@ async function restartCardSession(supabase, config, card) {
|
|
|
3413
3552
|
await updateCard(supabase, card.id, { restartRequestedAt: null });
|
|
3414
3553
|
await enqueueCard(supabase, config, card);
|
|
3415
3554
|
}
|
|
3555
|
+
async function returnCardToBacklog(supabase, config, card) {
|
|
3556
|
+
const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
|
|
3557
|
+
if (sessionId) {
|
|
3558
|
+
intentionallyStoppedSessionIds.add(sessionId);
|
|
3559
|
+
stopPtySession(sessionId);
|
|
3560
|
+
cancelUsageResume(sessionId);
|
|
3561
|
+
activeSessionByCard.delete(card.id);
|
|
3562
|
+
await updateSession(
|
|
3563
|
+
supabase,
|
|
3564
|
+
sessionId,
|
|
3565
|
+
{ status: "done", endedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3566
|
+
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3567
|
+
);
|
|
3568
|
+
logger.card.info(card.id, `Session ${sessionId} stopped by "voltar ao backlog".`);
|
|
3569
|
+
}
|
|
3570
|
+
await updateCard(supabase, card.id, { column: "backlog", returnToBacklogRequestedAt: null });
|
|
3571
|
+
await dispatchRunnerQueue(supabase, config);
|
|
3572
|
+
}
|
|
3416
3573
|
|
|
3417
3574
|
// src/columnActions.ts
|
|
3418
3575
|
var execAsync = promisify5(exec);
|
|
@@ -4206,6 +4363,8 @@ async function startRunner() {
|
|
|
4206
4363
|
const lastKnownColumn = /* @__PURE__ */ new Map();
|
|
4207
4364
|
const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
|
|
4208
4365
|
const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
|
|
4366
|
+
const lastKnownDevQueueOrder = /* @__PURE__ */ new Map();
|
|
4367
|
+
const lastKnownReturnToBacklogRequestedAt = /* @__PURE__ */ new Map();
|
|
4209
4368
|
await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
|
|
4210
4369
|
logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
|
|
4211
4370
|
for (const project of config.registeredProjects) {
|
|
@@ -4311,6 +4470,16 @@ async function startRunner() {
|
|
|
4311
4470
|
if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
|
|
4312
4471
|
void restartCardSession(supabase, config, card);
|
|
4313
4472
|
}
|
|
4473
|
+
const previousReturnToBacklogRequestedAt = lastKnownReturnToBacklogRequestedAt.get(card.id);
|
|
4474
|
+
lastKnownReturnToBacklogRequestedAt.set(card.id, card.return_to_backlog_requested_at);
|
|
4475
|
+
if (card.return_to_backlog_requested_at && card.return_to_backlog_requested_at !== previousReturnToBacklogRequestedAt) {
|
|
4476
|
+
void returnCardToBacklog(supabase, config, card);
|
|
4477
|
+
}
|
|
4478
|
+
const previousDevQueueOrder = lastKnownDevQueueOrder.get(card.id);
|
|
4479
|
+
lastKnownDevQueueOrder.set(card.id, card.dev_queue_order);
|
|
4480
|
+
if (card.column_name === "desenvolvimento" && card.dev_queue_order !== previousDevQueueOrder) {
|
|
4481
|
+
void dispatchRunnerQueue(supabase, config);
|
|
4482
|
+
}
|
|
4314
4483
|
if (previousColumn !== void 0 && previousColumn !== card.column_name) {
|
|
4315
4484
|
void clearColumnActionStatus(supabase, card);
|
|
4316
4485
|
void handleColumnEntered(supabase, config, card);
|
package/dist/index.js
CHANGED
|
@@ -617,12 +617,37 @@ async function getRunningSessionForProject(supabase, projectId, runnerId) {
|
|
|
617
617
|
const row = data;
|
|
618
618
|
return { id: row.id, cardId: row.card_id, agentState: row.agent_state };
|
|
619
619
|
}
|
|
620
|
+
function compareQueueOrder(a, b) {
|
|
621
|
+
const orderA = a.cards.dev_queue_order;
|
|
622
|
+
const orderB = b.cards.dev_queue_order;
|
|
623
|
+
if (orderA !== null && orderB !== null) return orderA - orderB;
|
|
624
|
+
if (orderA !== null) return -1;
|
|
625
|
+
if (orderB !== null) return 1;
|
|
626
|
+
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
627
|
+
}
|
|
628
|
+
function applyDevQueueOrder(queued) {
|
|
629
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
630
|
+
for (const entry of queued) {
|
|
631
|
+
const list = byProject.get(entry.cards.project_id) ?? [];
|
|
632
|
+
list.push(entry);
|
|
633
|
+
byProject.set(entry.cards.project_id, list);
|
|
634
|
+
}
|
|
635
|
+
for (const entries of byProject.values()) entries.sort(compareQueueOrder);
|
|
636
|
+
const seenProjects = /* @__PURE__ */ new Set();
|
|
637
|
+
const result = [];
|
|
638
|
+
for (const entry of queued) {
|
|
639
|
+
if (seenProjects.has(entry.cards.project_id)) continue;
|
|
640
|
+
seenProjects.add(entry.cards.project_id);
|
|
641
|
+
result.push(...byProject.get(entry.cards.project_id));
|
|
642
|
+
}
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
620
645
|
async function listQueuedSessionsForRunner(supabase, runnerId) {
|
|
621
646
|
const { data, error } = await supabase.from("sessions").select(
|
|
622
|
-
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved)"
|
|
647
|
+
"id, card_id, claude_session_id, agent_state, created_at, cards!card_id!inner(project_id, title, instructions, file_references, plan_first, prerequisite_card_id, prototype_first, prototype_url, prototype_approved, dev_queue_order)"
|
|
623
648
|
).eq("runner_id", runnerId).eq("status", "queued").order("created_at", { ascending: true });
|
|
624
649
|
if (error) throw error;
|
|
625
|
-
return data;
|
|
650
|
+
return applyDevQueueOrder(data);
|
|
626
651
|
}
|
|
627
652
|
async function listPausedSessionsForRunner(supabase, runnerId) {
|
|
628
653
|
const { data, error } = await supabase.from("sessions").select("card_id, cards!card_id!inner(project_id)").eq("runner_id", runnerId).eq("status", "paused");
|
|
@@ -730,7 +755,8 @@ async function updateCard(supabase, cardId, changes) {
|
|
|
730
755
|
...changes.customActionStatus !== void 0 ? { custom_action_status: changes.customActionStatus } : {},
|
|
731
756
|
...changes.customActionOutput !== void 0 ? { custom_action_output: changes.customActionOutput } : {},
|
|
732
757
|
...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {},
|
|
733
|
-
...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {}
|
|
758
|
+
...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {},
|
|
759
|
+
...changes.returnToBacklogRequestedAt !== void 0 ? { return_to_backlog_requested_at: changes.returnToBacklogRequestedAt } : {}
|
|
734
760
|
}).eq("id", cardId);
|
|
735
761
|
if (error) throw error;
|
|
736
762
|
}
|
|
@@ -1797,7 +1823,22 @@ function parseGitDiffNameStatus(output) {
|
|
|
1797
1823
|
const columns = line.split(" ");
|
|
1798
1824
|
const status = columns[0].slice(0, 1);
|
|
1799
1825
|
const path = columns[columns.length - 1];
|
|
1800
|
-
return { path, status };
|
|
1826
|
+
return { path, status, additions: null, deletions: null, binary: false };
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
function parseGitDiffNumstat(output) {
|
|
1830
|
+
return output.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
1831
|
+
const [added, deleted, path] = line.split(" ");
|
|
1832
|
+
const binary = added === "-" && deleted === "-";
|
|
1833
|
+
return { path, additions: binary ? null : Number(added), deletions: binary ? null : Number(deleted), binary };
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
function mergeNumstat(files, numstat) {
|
|
1837
|
+
const byPath = new Map(numstat.map((entry) => [entry.path, entry]));
|
|
1838
|
+
return files.map((file) => {
|
|
1839
|
+
const stat = byPath.get(file.path);
|
|
1840
|
+
if (!stat) return file;
|
|
1841
|
+
return { ...file, additions: stat.additions, deletions: stat.deletions, binary: stat.binary };
|
|
1801
1842
|
});
|
|
1802
1843
|
}
|
|
1803
1844
|
async function getRepoDiffForTarget(label, cwd) {
|
|
@@ -1831,14 +1872,84 @@ async function getRepoDiffForTarget(label, cwd) {
|
|
|
1831
1872
|
files = parseGitDiffNameStatus(stdout);
|
|
1832
1873
|
} catch (err) {
|
|
1833
1874
|
const message = err instanceof Error ? err.message : String(err);
|
|
1834
|
-
return { label, files: [], diff: "", error: message };
|
|
1875
|
+
return { label, files: [], diff: "", branch, error: message };
|
|
1876
|
+
}
|
|
1877
|
+
if (files.length === 0) return { label, files, diff: "", branch };
|
|
1878
|
+
try {
|
|
1879
|
+
const { stdout: numstatOut } = await execFileAsync2("git", ["diff", "--numstat", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
1880
|
+
files = mergeNumstat(files, parseGitDiffNumstat(numstatOut));
|
|
1881
|
+
} catch {
|
|
1835
1882
|
}
|
|
1836
|
-
if (files.length === 0) return { label, files, diff: "" };
|
|
1837
1883
|
try {
|
|
1838
1884
|
const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
|
|
1839
|
-
return { label, files, diff: stdout };
|
|
1885
|
+
return { label, files, diff: stdout, branch };
|
|
1840
1886
|
} catch {
|
|
1841
|
-
return { label, files, diff: "" };
|
|
1887
|
+
return { label, files, diff: "", branch };
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
async function resolveDiffTargetCwd(entry, projectId, label) {
|
|
1891
|
+
if (!label) return entry.repoRoot;
|
|
1892
|
+
if (!statuslineSupabase) throw new HttpError(400, "Reposit\xF3rio filho n\xE3o dispon\xEDvel.");
|
|
1893
|
+
const children = await getProjectRepos(statuslineSupabase, projectId);
|
|
1894
|
+
const child2 = children.find((candidate) => candidate.subpath === label);
|
|
1895
|
+
if (!child2) throw new HttpError(400, "Reposit\xF3rio filho desconhecido.");
|
|
1896
|
+
return join7(entry.repoRoot, child2.subpath);
|
|
1897
|
+
}
|
|
1898
|
+
async function readFileAtRevision(cwd, ref, relPath) {
|
|
1899
|
+
try {
|
|
1900
|
+
const { stdout } = await execFileAsync2("git", ["show", `${ref}:${relPath}`], {
|
|
1901
|
+
cwd,
|
|
1902
|
+
maxBuffer: DIFF_EXEC_OPTIONS.maxBuffer,
|
|
1903
|
+
encoding: "buffer"
|
|
1904
|
+
});
|
|
1905
|
+
return stdout;
|
|
1906
|
+
} catch {
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
function toFileRevisionJson(buffer, imageContentType) {
|
|
1911
|
+
if (buffer === null) return { exists: false, content: null, binary: false };
|
|
1912
|
+
if (looksBinary(buffer)) {
|
|
1913
|
+
return {
|
|
1914
|
+
exists: true,
|
|
1915
|
+
content: null,
|
|
1916
|
+
binary: true,
|
|
1917
|
+
...imageContentType ? { imageBase64: buffer.toString("base64"), imageContentType } : {}
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
return { exists: true, content: buffer.toString("utf8"), binary: false };
|
|
1921
|
+
}
|
|
1922
|
+
async function handleDiffFileRequest(entry, res, projectId, relPath, label) {
|
|
1923
|
+
if (!relPath) {
|
|
1924
|
+
sendJson(res, 400, { error: "Par\xE2metro path \xE9 obrigat\xF3rio." });
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
try {
|
|
1928
|
+
const cwd = await resolveDiffTargetCwd(entry, projectId, label);
|
|
1929
|
+
resolveWithinRepo(cwd, relPath);
|
|
1930
|
+
const { stdout: branchOut } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
|
|
1931
|
+
const branch = branchOut.trim();
|
|
1932
|
+
if (!branch) {
|
|
1933
|
+
sendJson(res, 400, { error: "HEAD destacado: sem branch local para comparar com a origin." });
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const [originalBuffer, modifiedBuffer] = await Promise.all([
|
|
1937
|
+
readFileAtRevision(cwd, `origin/${branch}`, relPath),
|
|
1938
|
+
readFileAtRevision(cwd, "HEAD", relPath)
|
|
1939
|
+
]);
|
|
1940
|
+
const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
|
|
1941
|
+
const imageContentType = MIME_TYPES_BY_EXT[ext];
|
|
1942
|
+
sendJson(res, 200, {
|
|
1943
|
+
original: toFileRevisionJson(originalBuffer, imageContentType),
|
|
1944
|
+
modified: toFileRevisionJson(modifiedBuffer, imageContentType)
|
|
1945
|
+
});
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
if (err instanceof HttpError) {
|
|
1948
|
+
sendJson(res, err.status, { error: err.message });
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
logger.runner.error("FILES", "Unexpected error handling a diff-file request:", err);
|
|
1952
|
+
sendJson(res, 500, { error: "Erro interno do runner." });
|
|
1842
1953
|
}
|
|
1843
1954
|
}
|
|
1844
1955
|
async function handleDiffRequest(entry, res, projectId) {
|
|
@@ -2023,6 +2134,12 @@ function handleRequest(req, res) {
|
|
|
2023
2134
|
void handleDiffRequest(entry, res, projectId);
|
|
2024
2135
|
return;
|
|
2025
2136
|
}
|
|
2137
|
+
if (pathname === "/diff-file") {
|
|
2138
|
+
const projectId = typeof query.projectId === "string" ? query.projectId : "";
|
|
2139
|
+
const label = typeof query.label === "string" ? query.label : null;
|
|
2140
|
+
void handleDiffFileRequest(entry, res, projectId, relPath, label);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2026
2143
|
try {
|
|
2027
2144
|
if (pathname === "/tree") {
|
|
2028
2145
|
sendJson(res, 200, { entries: listDirectory(entry.repoRoot, relPath) });
|
|
@@ -2439,13 +2556,49 @@ function toSessionName(title) {
|
|
|
2439
2556
|
// src/agentSession.ts
|
|
2440
2557
|
var TUI_IDLE_MS = 1500;
|
|
2441
2558
|
var TUI_READY_TIMEOUT_MS = 6e4;
|
|
2442
|
-
var
|
|
2559
|
+
var PASTE_SETTLE_MS = 200;
|
|
2560
|
+
var PASTE_SETTLE_TIMEOUT_MS = 5e3;
|
|
2443
2561
|
var PASTE_START = "\x1B[200~";
|
|
2444
2562
|
var PASTE_END = "\x1B[201~";
|
|
2445
2563
|
var SHIFT_TAB = "\x1B[Z";
|
|
2446
2564
|
var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
|
|
2447
2565
|
var SHIFT_TAB_DELAY_MS = 400;
|
|
2448
2566
|
var PLAN_MODE_INDICATOR = /plan mode on/i;
|
|
2567
|
+
function writePasteAndSubmit(pty, cardId, message, context) {
|
|
2568
|
+
pty.write(`${PASTE_START}${message}${PASTE_END}`);
|
|
2569
|
+
let settleTimer;
|
|
2570
|
+
let ceilingTimer;
|
|
2571
|
+
let dataSubscription;
|
|
2572
|
+
let exitSubscription;
|
|
2573
|
+
let submitted = false;
|
|
2574
|
+
const submit = () => {
|
|
2575
|
+
if (submitted) return;
|
|
2576
|
+
submitted = true;
|
|
2577
|
+
clearTimeout(settleTimer);
|
|
2578
|
+
clearTimeout(ceilingTimer);
|
|
2579
|
+
dataSubscription?.dispose();
|
|
2580
|
+
exitSubscription?.dispose();
|
|
2581
|
+
try {
|
|
2582
|
+
pty.write("\r");
|
|
2583
|
+
} catch (err) {
|
|
2584
|
+
logger.card.error(cardId, `Failed to submit ${context} to the agent:`, err);
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
const cancel = () => {
|
|
2588
|
+
submitted = true;
|
|
2589
|
+
clearTimeout(settleTimer);
|
|
2590
|
+
clearTimeout(ceilingTimer);
|
|
2591
|
+
dataSubscription?.dispose();
|
|
2592
|
+
exitSubscription?.dispose();
|
|
2593
|
+
};
|
|
2594
|
+
dataSubscription = pty.onData(() => {
|
|
2595
|
+
clearTimeout(settleTimer);
|
|
2596
|
+
settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
|
|
2597
|
+
});
|
|
2598
|
+
settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
|
|
2599
|
+
ceilingTimer = setTimeout(submit, PASTE_SETTLE_TIMEOUT_MS);
|
|
2600
|
+
exitSubscription = pty.onExit(cancel);
|
|
2601
|
+
}
|
|
2449
2602
|
function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
|
|
2450
2603
|
ensureDirTrusted(repoPath);
|
|
2451
2604
|
ensureMcpServersTrusted(repoPath);
|
|
@@ -2489,14 +2642,7 @@ function sendMessageWhenIdle(pty, cardId, message, planFirst) {
|
|
|
2489
2642
|
if (!stopWaiting()) return;
|
|
2490
2643
|
if (planFirst) await enterPlanMode(pty, cardId);
|
|
2491
2644
|
try {
|
|
2492
|
-
pty
|
|
2493
|
-
setTimeout(() => {
|
|
2494
|
-
try {
|
|
2495
|
-
pty.write("\r");
|
|
2496
|
-
} catch (err) {
|
|
2497
|
-
logger.card.error(cardId, "Failed to submit the card instructions to the agent:", err);
|
|
2498
|
-
}
|
|
2499
|
-
}, SUBMIT_DELAY_MS);
|
|
2645
|
+
writePasteAndSubmit(pty, cardId, message, "the card instructions");
|
|
2500
2646
|
} catch (err) {
|
|
2501
2647
|
logger.card.error(cardId, "Failed to send the card instructions to the agent:", err);
|
|
2502
2648
|
}
|
|
@@ -2532,14 +2678,7 @@ async function enterPlanMode(pty, cardId) {
|
|
|
2532
2678
|
}
|
|
2533
2679
|
function sendMessageToIdleSession(pty, cardId, message) {
|
|
2534
2680
|
try {
|
|
2535
|
-
pty
|
|
2536
|
-
setTimeout(() => {
|
|
2537
|
-
try {
|
|
2538
|
-
pty.write("\r");
|
|
2539
|
-
} catch (err) {
|
|
2540
|
-
logger.card.error(cardId, "Failed to submit a message to the idle session:", err);
|
|
2541
|
-
}
|
|
2542
|
-
}, SUBMIT_DELAY_MS);
|
|
2681
|
+
writePasteAndSubmit(pty, cardId, message, "a message");
|
|
2543
2682
|
} catch (err) {
|
|
2544
2683
|
logger.card.error(cardId, "Failed to send a message to the idle session:", err);
|
|
2545
2684
|
}
|
|
@@ -3301,6 +3440,24 @@ async function restartCardSession(supabase, config, card) {
|
|
|
3301
3440
|
await updateCard(supabase, card.id, { restartRequestedAt: null });
|
|
3302
3441
|
await enqueueCard(supabase, config, card);
|
|
3303
3442
|
}
|
|
3443
|
+
async function returnCardToBacklog(supabase, config, card) {
|
|
3444
|
+
const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
|
|
3445
|
+
if (sessionId) {
|
|
3446
|
+
intentionallyStoppedSessionIds.add(sessionId);
|
|
3447
|
+
stopPtySession(sessionId);
|
|
3448
|
+
cancelUsageResume(sessionId);
|
|
3449
|
+
activeSessionByCard.delete(card.id);
|
|
3450
|
+
await updateSession(
|
|
3451
|
+
supabase,
|
|
3452
|
+
sessionId,
|
|
3453
|
+
{ status: "done", endedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3454
|
+
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3455
|
+
);
|
|
3456
|
+
logger.card.info(card.id, `Session ${sessionId} stopped by "voltar ao backlog".`);
|
|
3457
|
+
}
|
|
3458
|
+
await updateCard(supabase, card.id, { column: "backlog", returnToBacklogRequestedAt: null });
|
|
3459
|
+
await dispatchRunnerQueue(supabase, config);
|
|
3460
|
+
}
|
|
3304
3461
|
|
|
3305
3462
|
// src/columnActions.ts
|
|
3306
3463
|
var execAsync = promisify5(exec);
|
|
@@ -4094,6 +4251,8 @@ async function startRunner() {
|
|
|
4094
4251
|
const lastKnownColumn = /* @__PURE__ */ new Map();
|
|
4095
4252
|
const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
|
|
4096
4253
|
const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
|
|
4254
|
+
const lastKnownDevQueueOrder = /* @__PURE__ */ new Map();
|
|
4255
|
+
const lastKnownReturnToBacklogRequestedAt = /* @__PURE__ */ new Map();
|
|
4097
4256
|
await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
|
|
4098
4257
|
logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
|
|
4099
4258
|
for (const project of config.registeredProjects) {
|
|
@@ -4199,6 +4358,16 @@ async function startRunner() {
|
|
|
4199
4358
|
if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
|
|
4200
4359
|
void restartCardSession(supabase, config, card);
|
|
4201
4360
|
}
|
|
4361
|
+
const previousReturnToBacklogRequestedAt = lastKnownReturnToBacklogRequestedAt.get(card.id);
|
|
4362
|
+
lastKnownReturnToBacklogRequestedAt.set(card.id, card.return_to_backlog_requested_at);
|
|
4363
|
+
if (card.return_to_backlog_requested_at && card.return_to_backlog_requested_at !== previousReturnToBacklogRequestedAt) {
|
|
4364
|
+
void returnCardToBacklog(supabase, config, card);
|
|
4365
|
+
}
|
|
4366
|
+
const previousDevQueueOrder = lastKnownDevQueueOrder.get(card.id);
|
|
4367
|
+
lastKnownDevQueueOrder.set(card.id, card.dev_queue_order);
|
|
4368
|
+
if (card.column_name === "desenvolvimento" && card.dev_queue_order !== previousDevQueueOrder) {
|
|
4369
|
+
void dispatchRunnerQueue(supabase, config);
|
|
4370
|
+
}
|
|
4202
4371
|
if (previousColumn !== void 0 && previousColumn !== card.column_name) {
|
|
4203
4372
|
void clearColumnActionStatus(supabase, card);
|
|
4204
4373
|
void handleColumnEntered(supabase, config, card);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-board-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Local runner for Agent Board — signs in with your own Supabase account (no shared secrets) and drives Claude Code against repos under your local repos root.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|