agent-board-runner 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/dist/cli.js +168 -65
  2. package/dist/index.js +173 -70
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -188,7 +188,7 @@ import { execFile as execFile2 } from "node:child_process";
188
188
  import { randomUUID as randomUUID5 } from "node:crypto";
189
189
  import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
190
190
  import { homedir as homedir4 } from "node:os";
191
- import { join as join8, resolve as resolve2, sep } from "node:path";
191
+ import { join as join9, resolve as resolve2, sep } from "node:path";
192
192
  import { parse } from "node:url";
193
193
  import { promisify as promisify2 } from "node:util";
194
194
 
@@ -412,7 +412,10 @@ async function savePastedImage(sessionId, extCode, bytes) {
412
412
  return path;
413
413
  }
414
414
  function registerPtySession(sessionId, pty, ticket) {
415
- const handle = { pty, ticket, outputBuffer: [], sockets: /* @__PURE__ */ new Set(), readOnly: false };
415
+ const handle = { pty, ticket, outputBuffer: [], sockets: /* @__PURE__ */ new Set(), readOnly: false, exited: false };
416
+ pty.onExit(() => {
417
+ handle.exited = true;
418
+ });
416
419
  pty.onData((data) => {
417
420
  handle.outputBuffer.push(data);
418
421
  if (handle.outputBuffer.length > MAX_BUFFERED_CHUNKS) handle.outputBuffer.shift();
@@ -431,7 +434,7 @@ function setPtySessionReadOnly(sessionId, readOnly) {
431
434
  }
432
435
  function resizePtySession(sessionId, cols, rows) {
433
436
  const handle = registry.get(sessionId);
434
- if (!handle) return;
437
+ if (!handle || handle.exited) return;
435
438
  handle.pty.resize(cols, rows);
436
439
  }
437
440
  function stopPtySession(sessionId) {
@@ -735,10 +738,12 @@ async function logActivity(supabase, params) {
735
738
  }
736
739
 
737
740
  // src/sessions.ts
738
- async function countRunningSessions(supabase, projectId, ownerUserId) {
739
- const { count, error } = await supabase.from("sessions").select("id, cards!card_id!inner(project_id, assigned_user_id)", { count: "exact", head: true }).eq("cards.project_id", projectId).eq("cards.assigned_user_id", ownerUserId).eq("status", "running");
741
+ async function getRunningSessionForProject(supabase, projectId, ownerUserId) {
742
+ const { data, error } = await supabase.from("sessions").select("id, card_id, agent_state, cards!card_id!inner(project_id, assigned_user_id)").eq("cards.project_id", projectId).eq("cards.assigned_user_id", ownerUserId).eq("status", "running").limit(1).maybeSingle();
740
743
  if (error) throw error;
741
- return count ?? 0;
744
+ if (!data) return null;
745
+ const row = data;
746
+ return { id: row.id, cardId: row.card_id, agentState: row.agent_state };
742
747
  }
743
748
  async function listQueuedSessionsForRunner(supabase, ownerUserId) {
744
749
  const { data, error } = await supabase.from("sessions").select(
@@ -812,7 +817,8 @@ async function updateCard(supabase, cardId, changes) {
812
817
  ...changes.deployError !== void 0 ? { deploy_error: changes.deployError } : {},
813
818
  ...changes.customActionStatus !== void 0 ? { custom_action_status: changes.customActionStatus } : {},
814
819
  ...changes.customActionOutput !== void 0 ? { custom_action_output: changes.customActionOutput } : {},
815
- ...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {}
820
+ ...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {},
821
+ ...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {}
816
822
  }).eq("id", cardId);
817
823
  if (error) throw error;
818
824
  }
@@ -828,7 +834,7 @@ async function getProjectRepos(supabase, projectId) {
828
834
  }
829
835
  async function listCardsInColumn(supabase, projectId, column) {
830
836
  const { data, error } = await supabase.from("cards").select(
831
- "id, project_id, title, instructions, column_name, assigned_user_id, deploy_error, file_references, preview_urls, custom_action_status, requeue_requested_at"
837
+ "id, project_id, title, instructions, column_name, assigned_user_id, deploy_error, file_references, preview_urls, custom_action_status, requeue_requested_at, restart_requested_at"
832
838
  ).eq("project_id", projectId).eq("column_name", column);
833
839
  if (error) throw error;
834
840
  return data;
@@ -911,9 +917,9 @@ async function upsertClaudeUsage(supabase, userId, changes) {
911
917
 
912
918
  // src/usageMonitor.ts
913
919
  import { randomUUID as randomUUID4 } from "node:crypto";
914
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
915
- import { homedir as homedir3, tmpdir as tmpdir5 } from "node:os";
916
- import { join as join7 } from "node:path";
920
+ import { mkdirSync as mkdirSync3 } from "node:fs";
921
+ import { tmpdir as tmpdir5 } from "node:os";
922
+ import { join as join8 } from "node:path";
917
923
  import { spawn as spawn2 } from "node-pty";
918
924
 
919
925
  // src/agentMomentHookScript.ts
@@ -1163,11 +1169,16 @@ function formatPercent(value) {
1163
1169
  }
1164
1170
 
1165
1171
  function buildDisplayText(data) {
1166
- const rateLimits = data && data.rate_limits;
1167
- if (!rateLimits) return "";
1168
1172
  const parts = [];
1169
- if (rateLimits.five_hour) parts.push(\`5h \${formatPercent(rateLimits.five_hour.used_percentage)}\`);
1170
- if (rateLimits.seven_day) parts.push(\`7d \${formatPercent(rateLimits.seven_day.used_percentage)}\`);
1173
+ const contextWindow = data && data.context_window;
1174
+ if (contextWindow && typeof contextWindow.used_percentage === "number") {
1175
+ parts.push(\`ctx \${formatPercent(contextWindow.used_percentage)}\`);
1176
+ }
1177
+ const rateLimits = data && data.rate_limits;
1178
+ if (rateLimits) {
1179
+ if (rateLimits.five_hour) parts.push(\`5h \${formatPercent(rateLimits.five_hour.used_percentage)}\`);
1180
+ if (rateLimits.seven_day) parts.push(\`7d \${formatPercent(rateLimits.seven_day.used_percentage)}\`);
1181
+ }
1171
1182
  return parts.join(" \xB7 ");
1172
1183
  }
1173
1184
 
@@ -1263,38 +1274,79 @@ function claudeCliSettingsArgs() {
1263
1274
  ];
1264
1275
  }
1265
1276
 
1266
- // src/usageMonitor.ts
1267
- var SESSION_ID = "usage-monitor";
1268
- var TICKET = randomUUID4();
1269
- var SCRATCH_DIR = join7(tmpdir5(), "agent-board-usage-monitor");
1270
- var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
1271
- var IDLE_MS = 2e3;
1272
- var HARD_TIMEOUT_MS = 6e4;
1273
- function usageMonitorSessionId() {
1274
- return SESSION_ID;
1275
- }
1276
- function usageMonitorTicket() {
1277
- return TICKET;
1278
- }
1279
- function ensureScratchDirTrusted() {
1277
+ // src/claudeTrust.ts
1278
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
1279
+ import { homedir as homedir3 } from "node:os";
1280
+ import { join as join7 } from "node:path";
1281
+ function ensureDirTrusted(dirPath) {
1280
1282
  const claudeJsonPath = join7(homedir3(), ".claude.json");
1281
1283
  if (!existsSync3(claudeJsonPath)) return;
1282
1284
  try {
1283
1285
  const raw = readFileSync3(claudeJsonPath, "utf8");
1284
1286
  const data = JSON.parse(raw);
1285
1287
  data.projects ??= {};
1286
- const keys = [SCRATCH_DIR, SCRATCH_DIR.replace(/\\/g, "/")];
1288
+ const keys = [dirPath, dirPath.replace(/\\/g, "/")];
1287
1289
  const alreadyTrusted = keys.every((key) => data.projects[key]?.hasTrustDialogAccepted === true);
1288
1290
  if (alreadyTrusted) return;
1289
1291
  for (const key of keys) {
1290
1292
  data.projects[key] = { ...data.projects[key], hasTrustDialogAccepted: true };
1291
1293
  }
1292
1294
  writeFileSync6(claudeJsonPath, JSON.stringify(data, null, 2));
1293
- logger.runner.info("USAGE_MONITOR", `Pre-trusted scratch directory ${SCRATCH_DIR} in ~/.claude.json.`);
1295
+ logger.runner.info("TRUST", `Pre-trusted ${dirPath} in ~/.claude.json.`);
1296
+ } catch (err) {
1297
+ logger.runner.error("TRUST", `Failed to pre-trust ${dirPath}:`, err);
1298
+ }
1299
+ }
1300
+ function ensureMcpServersTrusted(dirPath) {
1301
+ const mcpJsonPath = join7(dirPath, ".mcp.json");
1302
+ if (!existsSync3(mcpJsonPath)) return;
1303
+ let serverNames;
1304
+ try {
1305
+ const mcpConfig = JSON.parse(readFileSync3(mcpJsonPath, "utf8"));
1306
+ serverNames = Object.keys(mcpConfig.mcpServers ?? {});
1307
+ } catch (err) {
1308
+ logger.runner.error("TRUST", `Failed to read ${mcpJsonPath}:`, err);
1309
+ return;
1310
+ }
1311
+ if (serverNames.length === 0) return;
1312
+ const claudeJsonPath = join7(homedir3(), ".claude.json");
1313
+ if (!existsSync3(claudeJsonPath)) return;
1314
+ try {
1315
+ const raw = readFileSync3(claudeJsonPath, "utf8");
1316
+ const data = JSON.parse(raw);
1317
+ data.projects ??= {};
1318
+ const keys = [dirPath, dirPath.replace(/\\/g, "/")];
1319
+ const alreadyApproved = keys.every(
1320
+ (key) => serverNames.every((name) => data.projects[key]?.enabledMcpjsonServers?.includes(name))
1321
+ );
1322
+ if (alreadyApproved) return;
1323
+ for (const key of keys) {
1324
+ const project = data.projects[key] ??= {};
1325
+ project.enabledMcpjsonServers = [.../* @__PURE__ */ new Set([...project.enabledMcpjsonServers ?? [], ...serverNames])];
1326
+ project.disabledMcpjsonServers = (project.disabledMcpjsonServers ?? []).filter(
1327
+ (name) => !serverNames.includes(name)
1328
+ );
1329
+ }
1330
+ writeFileSync6(claudeJsonPath, JSON.stringify(data, null, 2));
1331
+ logger.runner.info("TRUST", `Pre-approved MCP servers [${serverNames.join(", ")}] for ${dirPath}.`);
1294
1332
  } catch (err) {
1295
- logger.runner.error("USAGE_MONITOR", "Failed to pre-trust the usage-monitor scratch directory:", err);
1333
+ logger.runner.error("TRUST", `Failed to pre-approve MCP servers for ${dirPath}:`, err);
1296
1334
  }
1297
1335
  }
1336
+
1337
+ // src/usageMonitor.ts
1338
+ var SESSION_ID = "usage-monitor";
1339
+ var TICKET = randomUUID4();
1340
+ var SCRATCH_DIR = join8(tmpdir5(), "agent-board-usage-monitor");
1341
+ var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
1342
+ var IDLE_MS = 2e3;
1343
+ var HARD_TIMEOUT_MS = 6e4;
1344
+ function usageMonitorSessionId() {
1345
+ return SESSION_ID;
1346
+ }
1347
+ function usageMonitorTicket() {
1348
+ return TICKET;
1349
+ }
1298
1350
  function pollUsageOnce() {
1299
1351
  let pty;
1300
1352
  try {
@@ -1341,7 +1393,7 @@ function pollUsageOnce() {
1341
1393
  }
1342
1394
  function startUsageMonitor() {
1343
1395
  mkdirSync3(SCRATCH_DIR, { recursive: true });
1344
- ensureScratchDirTrusted();
1396
+ ensureDirTrusted(SCRATCH_DIR);
1345
1397
  pollUsageOnce();
1346
1398
  setInterval(pollUsageOnce, POLL_INTERVAL_MS);
1347
1399
  }
@@ -1357,7 +1409,7 @@ var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modu
1357
1409
  var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
1358
1410
  function ensureSkillsDirExists(claudeDir) {
1359
1411
  try {
1360
- mkdirSync4(join8(claudeDir, "skills"), { recursive: true });
1412
+ mkdirSync4(join9(claudeDir, "skills"), { recursive: true });
1361
1413
  } catch (err) {
1362
1414
  logger.runner.warn("FILES", "Could not create .claude/skills:", err);
1363
1415
  }
@@ -1484,7 +1536,7 @@ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
1484
1536
  }
1485
1537
  const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
1486
1538
  if (isRepoRootLevel) {
1487
- ensureSkillsDirExists(join8(baseDir, relPath, ".claude"));
1539
+ ensureSkillsDirExists(join9(baseDir, relPath, ".claude"));
1488
1540
  const entries2 = listDirectory(baseDir, relPath, { includeDotClaude: true });
1489
1541
  return entries2.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
1490
1542
  }
@@ -1515,7 +1567,7 @@ function assertWritableAgentPath(relPath, atClaudeRoot) {
1515
1567
  function listDirectoryForBrowse(rootPath, relPath) {
1516
1568
  return listDirectory(rootPath, relPath).map((entry) => ({
1517
1569
  ...entry,
1518
- hasGit: entry.type === "dir" && existsSync4(join8(rootPath, entry.path, ".git"))
1570
+ hasGit: entry.type === "dir" && existsSync4(join9(rootPath, entry.path, ".git"))
1519
1571
  }));
1520
1572
  }
1521
1573
  var SAFE_CLONE_URL_PATTERN = /^(https?:\/\/|git:\/\/|ssh:\/\/|git@[\w.-]+:)\S+$/i;
@@ -1553,7 +1605,7 @@ async function handleCloneRequest(req, res, query) {
1553
1605
  sendJson(res, 400, { error: "N\xE3o foi poss\xEDvel determinar o nome da pasta a partir da URL." });
1554
1606
  return;
1555
1607
  }
1556
- const destPath = join8(setupEntry.rootPath, folderName);
1608
+ const destPath = join9(setupEntry.rootPath, folderName);
1557
1609
  if (existsSync4(destPath)) {
1558
1610
  sendJson(res, 409, { error: `J\xE1 existe uma pasta "${folderName}" na raiz permitida.` });
1559
1611
  return;
@@ -1621,7 +1673,7 @@ function findBrandIconPath(repoRoot) {
1621
1673
  if (entry.isSymbolicLink() || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
1622
1674
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
1623
1675
  if (entry.isDirectory()) {
1624
- walk(join8(dir, entry.name), relPath, depth + 1);
1676
+ walk(join9(dir, entry.name), relPath, depth + 1);
1625
1677
  } else if (entry.isFile()) {
1626
1678
  const match = patterns.find(({ pattern }) => pattern.test(entry.name));
1627
1679
  if (match) candidatesByBaseName.get(match.baseName).push({ relPath, depth });
@@ -1786,7 +1838,7 @@ async function getRepoDiffForTarget(label, cwd) {
1786
1838
  async function handleDiffRequest(entry, res, projectId) {
1787
1839
  try {
1788
1840
  const children = statuslineSupabase && projectId ? await getProjectRepos(statuslineSupabase, projectId) : [];
1789
- const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join8(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
1841
+ const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join9(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
1790
1842
  const repos = await Promise.all(targets.map((target) => getRepoDiffForTarget(target.label, target.cwd)));
1791
1843
  sendJson(res, 200, { repos });
1792
1844
  } catch (err) {
@@ -2094,7 +2146,7 @@ function attachAgentEventsServer() {
2094
2146
 
2095
2147
  // src/columnActions.ts
2096
2148
  import { exec } from "node:child_process";
2097
- import { join as join9 } from "node:path";
2149
+ import { join as join10 } from "node:path";
2098
2150
  import { promisify as promisify3 } from "node:util";
2099
2151
  var execAsync = promisify3(exec);
2100
2152
  var OUTPUT_TAIL_CHARS = 4e3;
@@ -2104,7 +2156,7 @@ function tail(text) {
2104
2156
  function resolveTargets(project, children, repoRoot, column, timing) {
2105
2157
  if (children.length > 0) {
2106
2158
  return children.flatMap(
2107
- (child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join9(repoRoot, child2.subpath), command: action.command }))
2159
+ (child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join10(repoRoot, child2.subpath), command: action.command }))
2108
2160
  );
2109
2161
  }
2110
2162
  return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
@@ -2210,7 +2262,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
2210
2262
  }
2211
2263
 
2212
2264
  // src/deploy.ts
2213
- import { join as join12 } from "node:path";
2265
+ import { join as join13 } from "node:path";
2214
2266
 
2215
2267
  // src/devServer.ts
2216
2268
  import { spawn as spawn4 } from "node:child_process";
@@ -2219,7 +2271,7 @@ import { spawn as spawn4 } from "node:child_process";
2219
2271
  import { spawn as spawn3 } from "node:child_process";
2220
2272
  import { accessSync, constants } from "node:fs";
2221
2273
  import { homedir as homedir5 } from "node:os";
2222
- import { delimiter, join as join10 } from "node:path";
2274
+ import { delimiter, join as join11 } from "node:path";
2223
2275
  import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
2224
2276
  var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
2225
2277
  var STARTUP_TIMEOUT_MS = 2e4;
@@ -2230,15 +2282,15 @@ function wellKnownDirectories() {
2230
2282
  const home = homedir5();
2231
2283
  if (IS_WINDOWS2) {
2232
2284
  const candidates = [
2233
- process.env.ProgramFiles && join10(process.env.ProgramFiles, "cloudflared"),
2234
- process.env["ProgramFiles(x86)"] && join10(process.env["ProgramFiles(x86)"], "cloudflared"),
2235
- process.env.LOCALAPPDATA && join10(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
2236
- join10(home, "scoop", "shims"),
2237
- process.env.ChocolateyInstall && join10(process.env.ChocolateyInstall, "bin")
2285
+ process.env.ProgramFiles && join11(process.env.ProgramFiles, "cloudflared"),
2286
+ process.env["ProgramFiles(x86)"] && join11(process.env["ProgramFiles(x86)"], "cloudflared"),
2287
+ process.env.LOCALAPPDATA && join11(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
2288
+ join11(home, "scoop", "shims"),
2289
+ process.env.ChocolateyInstall && join11(process.env.ChocolateyInstall, "bin")
2238
2290
  ];
2239
2291
  return candidates.filter((dir) => Boolean(dir));
2240
2292
  }
2241
- return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join10(home, ".local", "bin")];
2293
+ return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join11(home, ".local", "bin")];
2242
2294
  }
2243
2295
  function pathDirectories() {
2244
2296
  return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
@@ -2254,7 +2306,7 @@ function isExecutableFile(path) {
2254
2306
  function findInstalledBinary() {
2255
2307
  const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
2256
2308
  const directories = [...pathDirectories(), ...wellKnownDirectories()];
2257
- const candidates = [...override ? [override] : [], ...directories.map((dir) => join10(dir, BINARY_NAME))];
2309
+ const candidates = [...override ? [override] : [], ...directories.map((dir) => join11(dir, BINARY_NAME))];
2258
2310
  return candidates.find(isExecutableFile) ?? null;
2259
2311
  }
2260
2312
  async function locateCloudflared() {
@@ -2485,6 +2537,8 @@ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2485
2537
  var SHIFT_TAB_DELAY_MS = 400;
2486
2538
  var PLAN_MODE_INDICATOR = /plan mode on/i;
2487
2539
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2540
+ ensureDirTrusted(repoPath);
2541
+ ensureMcpServersTrusted(repoPath);
2488
2542
  const claudeSessionId = randomUUID6();
2489
2543
  const pty = spawn5(
2490
2544
  CLAUDE_BINARY,
@@ -2581,6 +2635,8 @@ function sendContinue(pty, cardId) {
2581
2635
  }
2582
2636
  }
2583
2637
  function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
2638
+ ensureDirTrusted(repoPath);
2639
+ ensureMcpServersTrusted(repoPath);
2584
2640
  return spawn5(
2585
2641
  CLAUDE_BINARY,
2586
2642
  [
@@ -2604,8 +2660,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
2604
2660
  // src/cardAttachments.ts
2605
2661
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
2606
2662
  import { tmpdir as tmpdir6 } from "node:os";
2607
- import { extname, join as join11 } from "node:path";
2608
- var ATTACHMENTS_ROOT = join11(tmpdir6(), "agent-board", "card-attachments");
2663
+ import { extname, join as join12 } from "node:path";
2664
+ var ATTACHMENTS_ROOT = join12(tmpdir6(), "agent-board", "card-attachments");
2609
2665
  function sanitizeFileName(fileName) {
2610
2666
  return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
2611
2667
  }
@@ -2617,13 +2673,13 @@ async function downloadCardAttachments(supabase, cardId) {
2617
2673
  }
2618
2674
  const rows = data ?? [];
2619
2675
  if (rows.length === 0) return [];
2620
- const targetDir = join11(ATTACHMENTS_ROOT, cardId);
2676
+ const targetDir = join12(ATTACHMENTS_ROOT, cardId);
2621
2677
  await mkdir2(targetDir, { recursive: true });
2622
2678
  const paths = [];
2623
2679
  for (const row of rows) {
2624
2680
  const baseName = sanitizeFileName(row.file_name);
2625
2681
  const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
2626
- const localPath = join11(targetDir, `${row.id}-${fileName}`);
2682
+ const localPath = join12(targetDir, `${row.id}-${fileName}`);
2627
2683
  try {
2628
2684
  const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
2629
2685
  if (downloadError) throw downloadError;
@@ -2697,6 +2753,7 @@ function cancelUsageResume(sessionId) {
2697
2753
 
2698
2754
  // src/orchestrator.ts
2699
2755
  var TERMINAL_TICKET_TTL_MS = 60 * 60 * 1e3;
2756
+ var RESUME_CONVERSATION_MISSING_PATTERN = /no conversation found with session id/i;
2700
2757
  var inFlightCardIds = /* @__PURE__ */ new Set();
2701
2758
  var activeSessionByCard = /* @__PURE__ */ new Map();
2702
2759
  var intentionallyStoppedSessionIds = /* @__PURE__ */ new Set();
@@ -2770,11 +2827,17 @@ async function dispatchRunnerQueue(supabase, config) {
2770
2827
  try {
2771
2828
  const queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
2772
2829
  if (queued.length === 0) return;
2830
+ const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
2773
2831
  const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
2774
2832
  const runningProjectIds = /* @__PURE__ */ new Set();
2775
2833
  for (const project of config.registeredProjects) {
2776
- const runningCount = await countRunningSessions(supabase, project.id, config.ownerUserId);
2777
- if (runningCount > 0) runningProjectIds.add(project.id);
2834
+ const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
2835
+ if (!running) continue;
2836
+ if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
2837
+ await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
2838
+ continue;
2839
+ }
2840
+ runningProjectIds.add(project.id);
2778
2841
  }
2779
2842
  let freeProjectSlots = maxConcurrentProjects - runningProjectIds.size;
2780
2843
  if (freeProjectSlots <= 0) return;
@@ -2828,9 +2891,13 @@ async function spawnSessionPty(supabase, config, entry) {
2828
2891
  const ticket = randomUUID7();
2829
2892
  let pty;
2830
2893
  let newClaudeSessionId = null;
2894
+ let resumeConversationMissing = false;
2831
2895
  try {
2832
2896
  if (claudeSessionId) {
2833
2897
  pty = resumeAgentPty(registered.repoPath, claudeSessionId, sessionId, ticket);
2898
+ pty.onData((chunk) => {
2899
+ if (RESUME_CONVERSATION_MISSING_PATTERN.test(normalizeTerminalOutput(chunk))) resumeConversationMissing = true;
2900
+ });
2834
2901
  } else {
2835
2902
  const attachmentPaths = await downloadCardAttachments(supabase, cardId);
2836
2903
  const instructions = buildInstructionsWithMcpContext(
@@ -2878,11 +2945,24 @@ async function spawnSessionPty(supabase, config, entry) {
2878
2945
  );
2879
2946
  pty.onExit(({ exitCode }) => {
2880
2947
  const wasIntentional = intentionallyStoppedSessionIds.delete(sessionId);
2881
- logger.card.warn(cardId, `Session exited (code ${exitCode}) \u2014 this only happens if the process crashed or was killed, the card doesn't auto-advance.`);
2882
2948
  activityWatcher.stop();
2883
2949
  unregisterAgentActivityWatcher(sessionId);
2884
2950
  cancelUsageResume(sessionId);
2885
2951
  activeSessionByCard.delete(cardId);
2952
+ if (resumeConversationMissing) {
2953
+ logger.card.warn(
2954
+ cardId,
2955
+ `Session ${sessionId}: --resume ${claudeSessionId} found no such conversation \u2014 clearing it so the next attempt starts a fresh one instead of repeating the same failure.`
2956
+ );
2957
+ void updateSession(
2958
+ supabase,
2959
+ sessionId,
2960
+ { status: "failed", endedAt: (/* @__PURE__ */ new Date()).toISOString(), claudeSessionId: null },
2961
+ { projectId: card.project_id, cardId, userId: config.ownerUserId }
2962
+ );
2963
+ return;
2964
+ }
2965
+ logger.card.warn(cardId, `Session exited (code ${exitCode}) \u2014 this only happens if the process crashed or was killed, the card doesn't auto-advance.`);
2886
2966
  if (!wasIntentional && exitCode !== 0) {
2887
2967
  publishAgentState(supabase, card.project_id, cardId, sessionId, "error");
2888
2968
  }
@@ -2903,7 +2983,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
2903
2983
  } else {
2904
2984
  cancelUsageResume(sessionId);
2905
2985
  }
2906
- if (state === "done") void pauseFinishedSession(supabase, config, projectId, cardId, sessionId);
2986
+ if (state === "done") void dispatchRunnerQueue(supabase, config);
2907
2987
  });
2908
2988
  pty.onData((chunk) => watcher.push(chunk));
2909
2989
  registerAgentActivityWatcher(sessionId, watcher);
@@ -2913,8 +2993,7 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
2913
2993
  const paused = await pauseRunningSession(supabase, sessionId, { projectId, cardId, userId: config.ownerUserId });
2914
2994
  if (!paused) return;
2915
2995
  setPtySessionReadOnly(sessionId, true);
2916
- logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done") \u2014 WIP slot freed, pty stays viewable read-only.`);
2917
- await dispatchRunnerQueue(supabase, config);
2996
+ logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done", another session needs this project's WIP slot).`);
2918
2997
  }
2919
2998
  async function requeuePausedCard(supabase, config, card) {
2920
2999
  const active = await getActiveSessionForCard(supabase, card.id);
@@ -2992,6 +3071,24 @@ async function stopCardSession(supabase, config, card) {
2992
3071
  logger.card.info(card.id, `Session ${sessionId} stopped.`);
2993
3072
  await dispatchRunnerQueue(supabase, config);
2994
3073
  }
3074
+ async function restartCardSession(supabase, config, card) {
3075
+ const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
3076
+ if (sessionId) {
3077
+ intentionallyStoppedSessionIds.add(sessionId);
3078
+ stopPtySession(sessionId);
3079
+ cancelUsageResume(sessionId);
3080
+ activeSessionByCard.delete(card.id);
3081
+ await updateSession(
3082
+ supabase,
3083
+ sessionId,
3084
+ { status: "failed", claudeSessionId: null, endedAt: (/* @__PURE__ */ new Date()).toISOString() },
3085
+ { projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
3086
+ );
3087
+ logger.card.info(card.id, `Session ${sessionId} abandoned by "Reiniciar sess\xE3o"; starting a fresh one.`);
3088
+ }
3089
+ await updateCard(supabase, card.id, { restartRequestedAt: null });
3090
+ await enqueueCard(supabase, config, card);
3091
+ }
2995
3092
 
2996
3093
  // src/deploy.ts
2997
3094
  async function handleDeployRequested(supabase, config, project) {
@@ -3009,7 +3106,7 @@ async function handleDeployRequested(supabase, config, project) {
3009
3106
  }
3010
3107
  const children = await getProjectRepos(supabase, project.id);
3011
3108
  const repoRoot = resolveRepoPath(config, project.repo_path);
3012
- const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join12(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
3109
+ const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join13(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
3013
3110
  const failures = [];
3014
3111
  for (const target of targets) {
3015
3112
  const result = await pushBranchToMain(target.path);
@@ -3143,7 +3240,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
3143
3240
  }
3144
3241
 
3145
3242
  // src/review.ts
3146
- import { join as join13 } from "node:path";
3243
+ import { join as join14 } from "node:path";
3147
3244
  var inFlightCardIds2 = /* @__PURE__ */ new Set();
3148
3245
  async function handleCardEnteredRevisao(supabase, config, card) {
3149
3246
  if (inFlightCardIds2.has(card.id)) return;
@@ -3152,7 +3249,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
3152
3249
  const project = await getProject(supabase, card.project_id);
3153
3250
  const children = await getProjectRepos(supabase, project.id);
3154
3251
  const repoRoot = resolveRepoPath(config, project.repo_path);
3155
- const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd: join13(repoRoot, child2.subpath), command: child2.dev_command })) : project.dev_command ? [{ label: "", cwd: repoRoot, command: project.dev_command }] : [];
3252
+ const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd: join14(repoRoot, child2.subpath), command: child2.dev_command })) : project.dev_command ? [{ label: "", cwd: repoRoot, command: project.dev_command }] : [];
3156
3253
  if (targets.length === 0) {
3157
3254
  logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
3158
3255
  return;
@@ -3238,7 +3335,7 @@ function attachTerminalServer() {
3238
3335
  const buffer = data;
3239
3336
  const frameType = buffer[0];
3240
3337
  if (frameType === FRAME_TYPE_IMAGE) {
3241
- if (handle.readOnly) return;
3338
+ if (handle.readOnly || handle.exited) return;
3242
3339
  const extCode = buffer[1];
3243
3340
  const bytes = buffer.subarray(2);
3244
3341
  savePastedImage(sessionId, extCode, bytes).then((path) => handle.pty.write(`${path} `)).catch((err) => logger.runner.warn("TERMINAL", `Terminal WS: failed to save pasted image for session ${sessionId}:`, err));
@@ -3256,7 +3353,7 @@ function attachTerminalServer() {
3256
3353
  logger.runner.warn("TERMINAL", `Terminal WS: ignoring binary frame with unknown type ${frameType} for session ${sessionId}.`);
3257
3354
  return;
3258
3355
  }
3259
- if (handle.readOnly) return;
3356
+ if (handle.readOnly || handle.exited) return;
3260
3357
  handle.pty.write(data.toString());
3261
3358
  });
3262
3359
  socket.on("close", () => {
@@ -3378,6 +3475,7 @@ async function startRunner() {
3378
3475
  const registeredPaths = new Set(config.registeredProjects.map((project) => project.id));
3379
3476
  const lastKnownColumn = /* @__PURE__ */ new Map();
3380
3477
  const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
3478
+ const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
3381
3479
  await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
3382
3480
  logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
3383
3481
  for (const project of config.registeredProjects) {
@@ -3473,6 +3571,11 @@ async function startRunner() {
3473
3571
  if (card.requeue_requested_at && card.requeue_requested_at !== previousRequeueRequestedAt) {
3474
3572
  void requeuePausedCard(supabase, config, card);
3475
3573
  }
3574
+ const previousRestartRequestedAt = lastKnownRestartRequestedAt.get(card.id);
3575
+ lastKnownRestartRequestedAt.set(card.id, card.restart_requested_at);
3576
+ if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
3577
+ void restartCardSession(supabase, config, card);
3578
+ }
3476
3579
  if (previousColumn !== void 0 && previousColumn !== card.column_name) {
3477
3580
  void clearColumnActionStatus(supabase, card);
3478
3581
  void handleColumnEntered(supabase, config, card);
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import { execFile as execFile2 } from "node:child_process";
26
26
  import { randomUUID as randomUUID4 } from "node:crypto";
27
27
  import { existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
28
28
  import { homedir as homedir3 } from "node:os";
29
- import { join as join6, resolve, sep } from "node:path";
29
+ import { join as join7, resolve, sep } from "node:path";
30
30
  import { parse } from "node:url";
31
31
  import { promisify as promisify2 } from "node:util";
32
32
 
@@ -250,7 +250,10 @@ async function savePastedImage(sessionId, extCode, bytes) {
250
250
  return path;
251
251
  }
252
252
  function registerPtySession(sessionId, pty, ticket) {
253
- const handle = { pty, ticket, outputBuffer: [], sockets: /* @__PURE__ */ new Set(), readOnly: false };
253
+ const handle = { pty, ticket, outputBuffer: [], sockets: /* @__PURE__ */ new Set(), readOnly: false, exited: false };
254
+ pty.onExit(() => {
255
+ handle.exited = true;
256
+ });
254
257
  pty.onData((data) => {
255
258
  handle.outputBuffer.push(data);
256
259
  if (handle.outputBuffer.length > MAX_BUFFERED_CHUNKS) handle.outputBuffer.shift();
@@ -269,7 +272,7 @@ function setPtySessionReadOnly(sessionId, readOnly) {
269
272
  }
270
273
  function resizePtySession(sessionId, cols, rows) {
271
274
  const handle = registry.get(sessionId);
272
- if (!handle) return;
275
+ if (!handle || handle.exited) return;
273
276
  handle.pty.resize(cols, rows);
274
277
  }
275
278
  function stopPtySession(sessionId) {
@@ -573,10 +576,12 @@ async function logActivity(supabase, params) {
573
576
  }
574
577
 
575
578
  // src/sessions.ts
576
- async function countRunningSessions(supabase, projectId, ownerUserId) {
577
- const { count, error } = await supabase.from("sessions").select("id, cards!card_id!inner(project_id, assigned_user_id)", { count: "exact", head: true }).eq("cards.project_id", projectId).eq("cards.assigned_user_id", ownerUserId).eq("status", "running");
579
+ async function getRunningSessionForProject(supabase, projectId, ownerUserId) {
580
+ const { data, error } = await supabase.from("sessions").select("id, card_id, agent_state, cards!card_id!inner(project_id, assigned_user_id)").eq("cards.project_id", projectId).eq("cards.assigned_user_id", ownerUserId).eq("status", "running").limit(1).maybeSingle();
578
581
  if (error) throw error;
579
- return count ?? 0;
582
+ if (!data) return null;
583
+ const row = data;
584
+ return { id: row.id, cardId: row.card_id, agentState: row.agent_state };
580
585
  }
581
586
  async function listQueuedSessionsForRunner(supabase, ownerUserId) {
582
587
  const { data, error } = await supabase.from("sessions").select(
@@ -650,7 +655,8 @@ async function updateCard(supabase, cardId, changes) {
650
655
  ...changes.deployError !== void 0 ? { deploy_error: changes.deployError } : {},
651
656
  ...changes.customActionStatus !== void 0 ? { custom_action_status: changes.customActionStatus } : {},
652
657
  ...changes.customActionOutput !== void 0 ? { custom_action_output: changes.customActionOutput } : {},
653
- ...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {}
658
+ ...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {},
659
+ ...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {}
654
660
  }).eq("id", cardId);
655
661
  if (error) throw error;
656
662
  }
@@ -666,7 +672,7 @@ async function getProjectRepos(supabase, projectId) {
666
672
  }
667
673
  async function listCardsInColumn(supabase, projectId, column) {
668
674
  const { data, error } = await supabase.from("cards").select(
669
- "id, project_id, title, instructions, column_name, assigned_user_id, deploy_error, file_references, preview_urls, custom_action_status, requeue_requested_at"
675
+ "id, project_id, title, instructions, column_name, assigned_user_id, deploy_error, file_references, preview_urls, custom_action_status, requeue_requested_at, restart_requested_at"
670
676
  ).eq("project_id", projectId).eq("column_name", column);
671
677
  if (error) throw error;
672
678
  return data;
@@ -749,9 +755,9 @@ async function upsertClaudeUsage(supabase, userId, changes) {
749
755
 
750
756
  // src/usageMonitor.ts
751
757
  import { randomUUID as randomUUID3 } from "node:crypto";
752
- import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync4 } from "node:fs";
753
- import { homedir as homedir2, tmpdir as tmpdir5 } from "node:os";
754
- import { join as join5 } from "node:path";
758
+ import { mkdirSync } from "node:fs";
759
+ import { tmpdir as tmpdir5 } from "node:os";
760
+ import { join as join6 } from "node:path";
755
761
  import { spawn as spawn2 } from "node-pty";
756
762
 
757
763
  // src/agentMomentHookScript.ts
@@ -1001,11 +1007,16 @@ function formatPercent(value) {
1001
1007
  }
1002
1008
 
1003
1009
  function buildDisplayText(data) {
1004
- const rateLimits = data && data.rate_limits;
1005
- if (!rateLimits) return "";
1006
1010
  const parts = [];
1007
- if (rateLimits.five_hour) parts.push(\`5h \${formatPercent(rateLimits.five_hour.used_percentage)}\`);
1008
- if (rateLimits.seven_day) parts.push(\`7d \${formatPercent(rateLimits.seven_day.used_percentage)}\`);
1011
+ const contextWindow = data && data.context_window;
1012
+ if (contextWindow && typeof contextWindow.used_percentage === "number") {
1013
+ parts.push(\`ctx \${formatPercent(contextWindow.used_percentage)}\`);
1014
+ }
1015
+ const rateLimits = data && data.rate_limits;
1016
+ if (rateLimits) {
1017
+ if (rateLimits.five_hour) parts.push(\`5h \${formatPercent(rateLimits.five_hour.used_percentage)}\`);
1018
+ if (rateLimits.seven_day) parts.push(\`7d \${formatPercent(rateLimits.seven_day.used_percentage)}\`);
1019
+ }
1009
1020
  return parts.join(" \xB7 ");
1010
1021
  }
1011
1022
 
@@ -1101,38 +1112,79 @@ function claudeCliSettingsArgs() {
1101
1112
  ];
1102
1113
  }
1103
1114
 
1104
- // src/usageMonitor.ts
1105
- var SESSION_ID = "usage-monitor";
1106
- var TICKET = randomUUID3();
1107
- var SCRATCH_DIR = join5(tmpdir5(), "agent-board-usage-monitor");
1108
- var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
1109
- var IDLE_MS = 2e3;
1110
- var HARD_TIMEOUT_MS = 6e4;
1111
- function usageMonitorSessionId() {
1112
- return SESSION_ID;
1113
- }
1114
- function usageMonitorTicket() {
1115
- return TICKET;
1116
- }
1117
- function ensureScratchDirTrusted() {
1115
+ // src/claudeTrust.ts
1116
+ import { existsSync, readFileSync, writeFileSync as writeFileSync4 } from "node:fs";
1117
+ import { homedir as homedir2 } from "node:os";
1118
+ import { join as join5 } from "node:path";
1119
+ function ensureDirTrusted(dirPath) {
1118
1120
  const claudeJsonPath = join5(homedir2(), ".claude.json");
1119
1121
  if (!existsSync(claudeJsonPath)) return;
1120
1122
  try {
1121
1123
  const raw = readFileSync(claudeJsonPath, "utf8");
1122
1124
  const data = JSON.parse(raw);
1123
1125
  data.projects ??= {};
1124
- const keys = [SCRATCH_DIR, SCRATCH_DIR.replace(/\\/g, "/")];
1126
+ const keys = [dirPath, dirPath.replace(/\\/g, "/")];
1125
1127
  const alreadyTrusted = keys.every((key) => data.projects[key]?.hasTrustDialogAccepted === true);
1126
1128
  if (alreadyTrusted) return;
1127
1129
  for (const key of keys) {
1128
1130
  data.projects[key] = { ...data.projects[key], hasTrustDialogAccepted: true };
1129
1131
  }
1130
1132
  writeFileSync4(claudeJsonPath, JSON.stringify(data, null, 2));
1131
- logger.runner.info("USAGE_MONITOR", `Pre-trusted scratch directory ${SCRATCH_DIR} in ~/.claude.json.`);
1133
+ logger.runner.info("TRUST", `Pre-trusted ${dirPath} in ~/.claude.json.`);
1134
+ } catch (err) {
1135
+ logger.runner.error("TRUST", `Failed to pre-trust ${dirPath}:`, err);
1136
+ }
1137
+ }
1138
+ function ensureMcpServersTrusted(dirPath) {
1139
+ const mcpJsonPath = join5(dirPath, ".mcp.json");
1140
+ if (!existsSync(mcpJsonPath)) return;
1141
+ let serverNames;
1142
+ try {
1143
+ const mcpConfig = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
1144
+ serverNames = Object.keys(mcpConfig.mcpServers ?? {});
1145
+ } catch (err) {
1146
+ logger.runner.error("TRUST", `Failed to read ${mcpJsonPath}:`, err);
1147
+ return;
1148
+ }
1149
+ if (serverNames.length === 0) return;
1150
+ const claudeJsonPath = join5(homedir2(), ".claude.json");
1151
+ if (!existsSync(claudeJsonPath)) return;
1152
+ try {
1153
+ const raw = readFileSync(claudeJsonPath, "utf8");
1154
+ const data = JSON.parse(raw);
1155
+ data.projects ??= {};
1156
+ const keys = [dirPath, dirPath.replace(/\\/g, "/")];
1157
+ const alreadyApproved = keys.every(
1158
+ (key) => serverNames.every((name) => data.projects[key]?.enabledMcpjsonServers?.includes(name))
1159
+ );
1160
+ if (alreadyApproved) return;
1161
+ for (const key of keys) {
1162
+ const project = data.projects[key] ??= {};
1163
+ project.enabledMcpjsonServers = [.../* @__PURE__ */ new Set([...project.enabledMcpjsonServers ?? [], ...serverNames])];
1164
+ project.disabledMcpjsonServers = (project.disabledMcpjsonServers ?? []).filter(
1165
+ (name) => !serverNames.includes(name)
1166
+ );
1167
+ }
1168
+ writeFileSync4(claudeJsonPath, JSON.stringify(data, null, 2));
1169
+ logger.runner.info("TRUST", `Pre-approved MCP servers [${serverNames.join(", ")}] for ${dirPath}.`);
1132
1170
  } catch (err) {
1133
- logger.runner.error("USAGE_MONITOR", "Failed to pre-trust the usage-monitor scratch directory:", err);
1171
+ logger.runner.error("TRUST", `Failed to pre-approve MCP servers for ${dirPath}:`, err);
1134
1172
  }
1135
1173
  }
1174
+
1175
+ // src/usageMonitor.ts
1176
+ var SESSION_ID = "usage-monitor";
1177
+ var TICKET = randomUUID3();
1178
+ var SCRATCH_DIR = join6(tmpdir5(), "agent-board-usage-monitor");
1179
+ var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
1180
+ var IDLE_MS = 2e3;
1181
+ var HARD_TIMEOUT_MS = 6e4;
1182
+ function usageMonitorSessionId() {
1183
+ return SESSION_ID;
1184
+ }
1185
+ function usageMonitorTicket() {
1186
+ return TICKET;
1187
+ }
1136
1188
  function pollUsageOnce() {
1137
1189
  let pty;
1138
1190
  try {
@@ -1179,7 +1231,7 @@ function pollUsageOnce() {
1179
1231
  }
1180
1232
  function startUsageMonitor() {
1181
1233
  mkdirSync(SCRATCH_DIR, { recursive: true });
1182
- ensureScratchDirTrusted();
1234
+ ensureDirTrusted(SCRATCH_DIR);
1183
1235
  pollUsageOnce();
1184
1236
  setInterval(pollUsageOnce, POLL_INTERVAL_MS);
1185
1237
  }
@@ -1195,7 +1247,7 @@ var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modu
1195
1247
  var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
1196
1248
  function ensureSkillsDirExists(claudeDir) {
1197
1249
  try {
1198
- mkdirSync2(join6(claudeDir, "skills"), { recursive: true });
1250
+ mkdirSync2(join7(claudeDir, "skills"), { recursive: true });
1199
1251
  } catch (err) {
1200
1252
  logger.runner.warn("FILES", "Could not create .claude/skills:", err);
1201
1253
  }
@@ -1322,7 +1374,7 @@ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
1322
1374
  }
1323
1375
  const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
1324
1376
  if (isRepoRootLevel) {
1325
- ensureSkillsDirExists(join6(baseDir, relPath, ".claude"));
1377
+ ensureSkillsDirExists(join7(baseDir, relPath, ".claude"));
1326
1378
  const entries2 = listDirectory(baseDir, relPath, { includeDotClaude: true });
1327
1379
  return entries2.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
1328
1380
  }
@@ -1353,7 +1405,7 @@ function assertWritableAgentPath(relPath, atClaudeRoot) {
1353
1405
  function listDirectoryForBrowse(rootPath, relPath) {
1354
1406
  return listDirectory(rootPath, relPath).map((entry) => ({
1355
1407
  ...entry,
1356
- hasGit: entry.type === "dir" && existsSync2(join6(rootPath, entry.path, ".git"))
1408
+ hasGit: entry.type === "dir" && existsSync2(join7(rootPath, entry.path, ".git"))
1357
1409
  }));
1358
1410
  }
1359
1411
  var SAFE_CLONE_URL_PATTERN = /^(https?:\/\/|git:\/\/|ssh:\/\/|git@[\w.-]+:)\S+$/i;
@@ -1391,7 +1443,7 @@ async function handleCloneRequest(req, res, query) {
1391
1443
  sendJson(res, 400, { error: "N\xE3o foi poss\xEDvel determinar o nome da pasta a partir da URL." });
1392
1444
  return;
1393
1445
  }
1394
- const destPath = join6(setupEntry.rootPath, folderName);
1446
+ const destPath = join7(setupEntry.rootPath, folderName);
1395
1447
  if (existsSync2(destPath)) {
1396
1448
  sendJson(res, 409, { error: `J\xE1 existe uma pasta "${folderName}" na raiz permitida.` });
1397
1449
  return;
@@ -1459,7 +1511,7 @@ function findBrandIconPath(repoRoot) {
1459
1511
  if (entry.isSymbolicLink() || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
1460
1512
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
1461
1513
  if (entry.isDirectory()) {
1462
- walk(join6(dir, entry.name), relPath, depth + 1);
1514
+ walk(join7(dir, entry.name), relPath, depth + 1);
1463
1515
  } else if (entry.isFile()) {
1464
1516
  const match = patterns.find(({ pattern }) => pattern.test(entry.name));
1465
1517
  if (match) candidatesByBaseName.get(match.baseName).push({ relPath, depth });
@@ -1624,7 +1676,7 @@ async function getRepoDiffForTarget(label, cwd) {
1624
1676
  async function handleDiffRequest(entry, res, projectId) {
1625
1677
  try {
1626
1678
  const children = statuslineSupabase && projectId ? await getProjectRepos(statuslineSupabase, projectId) : [];
1627
- const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join6(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
1679
+ const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join7(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
1628
1680
  const repos = await Promise.all(targets.map((target) => getRepoDiffForTarget(target.label, target.cwd)));
1629
1681
  sendJson(res, 200, { repos });
1630
1682
  } catch (err) {
@@ -1933,15 +1985,15 @@ function attachAgentEventsServer() {
1933
1985
  // src/auth.ts
1934
1986
  import { createClient } from "@supabase/supabase-js";
1935
1987
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
1936
- import { join as join8 } from "node:path";
1988
+ import { join as join9 } from "node:path";
1937
1989
  import open from "open";
1938
1990
 
1939
1991
  // src/config.ts
1940
1992
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
1941
1993
  import { hostname, homedir as homedir4 } from "node:os";
1942
- import { join as join7, resolve as resolve2 } from "node:path";
1943
- var CONFIG_DIR = join7(homedir4(), ".agent-board");
1944
- var CONFIG_FILE = join7(CONFIG_DIR, "credentials.json");
1994
+ import { join as join8, resolve as resolve2 } from "node:path";
1995
+ var CONFIG_DIR = join8(homedir4(), ".agent-board");
1996
+ var CONFIG_FILE = join8(CONFIG_DIR, "credentials.json");
1945
1997
  function loadConfig() {
1946
1998
  if (!existsSync3(CONFIG_FILE)) return null;
1947
1999
  return JSON.parse(readFileSync3(CONFIG_FILE, "utf-8"));
@@ -1974,7 +2026,7 @@ function resolveRepoPath(config, relativeRepoPath) {
1974
2026
  }
1975
2027
 
1976
2028
  // src/auth.ts
1977
- var SESSION_FILE = join8(CONFIG_DIR, "session.json");
2029
+ var SESSION_FILE = join9(CONFIG_DIR, "session.json");
1978
2030
  var DEFAULT_SUPABASE_URL = "https://brpxshvnmxgkbffgdfcc.supabase.co";
1979
2031
  var DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJycHhzaHZubXhna2JmZmdkZmNjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODc1OTQwMTEsImV4cCI6MjEwMzE3MDAxMX0.ErsAGZysLFgNvijetR7w1YvIziXC-1D2sx04Hm2UW9c";
1980
2032
  function resolveSupabaseUrl() {
@@ -2033,7 +2085,7 @@ async function requireSession(supabase) {
2033
2085
 
2034
2086
  // src/columnActions.ts
2035
2087
  import { exec } from "node:child_process";
2036
- import { join as join9 } from "node:path";
2088
+ import { join as join10 } from "node:path";
2037
2089
  import { promisify as promisify3 } from "node:util";
2038
2090
  var execAsync = promisify3(exec);
2039
2091
  var OUTPUT_TAIL_CHARS = 4e3;
@@ -2043,7 +2095,7 @@ function tail(text) {
2043
2095
  function resolveTargets(project, children, repoRoot, column, timing) {
2044
2096
  if (children.length > 0) {
2045
2097
  return children.flatMap(
2046
- (child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join9(repoRoot, child2.subpath), command: action.command }))
2098
+ (child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join10(repoRoot, child2.subpath), command: action.command }))
2047
2099
  );
2048
2100
  }
2049
2101
  return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
@@ -2149,7 +2201,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
2149
2201
  }
2150
2202
 
2151
2203
  // src/deploy.ts
2152
- import { join as join12 } from "node:path";
2204
+ import { join as join13 } from "node:path";
2153
2205
 
2154
2206
  // src/devServer.ts
2155
2207
  import { spawn as spawn4 } from "node:child_process";
@@ -2158,7 +2210,7 @@ import { spawn as spawn4 } from "node:child_process";
2158
2210
  import { spawn as spawn3 } from "node:child_process";
2159
2211
  import { accessSync, constants } from "node:fs";
2160
2212
  import { homedir as homedir5 } from "node:os";
2161
- import { delimiter, join as join10 } from "node:path";
2213
+ import { delimiter, join as join11 } from "node:path";
2162
2214
  import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
2163
2215
  var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
2164
2216
  var STARTUP_TIMEOUT_MS = 2e4;
@@ -2169,15 +2221,15 @@ function wellKnownDirectories() {
2169
2221
  const home = homedir5();
2170
2222
  if (IS_WINDOWS2) {
2171
2223
  const candidates = [
2172
- process.env.ProgramFiles && join10(process.env.ProgramFiles, "cloudflared"),
2173
- process.env["ProgramFiles(x86)"] && join10(process.env["ProgramFiles(x86)"], "cloudflared"),
2174
- process.env.LOCALAPPDATA && join10(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
2175
- join10(home, "scoop", "shims"),
2176
- process.env.ChocolateyInstall && join10(process.env.ChocolateyInstall, "bin")
2224
+ process.env.ProgramFiles && join11(process.env.ProgramFiles, "cloudflared"),
2225
+ process.env["ProgramFiles(x86)"] && join11(process.env["ProgramFiles(x86)"], "cloudflared"),
2226
+ process.env.LOCALAPPDATA && join11(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
2227
+ join11(home, "scoop", "shims"),
2228
+ process.env.ChocolateyInstall && join11(process.env.ChocolateyInstall, "bin")
2177
2229
  ];
2178
2230
  return candidates.filter((dir) => Boolean(dir));
2179
2231
  }
2180
- return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join10(home, ".local", "bin")];
2232
+ return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join11(home, ".local", "bin")];
2181
2233
  }
2182
2234
  function pathDirectories() {
2183
2235
  return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
@@ -2193,7 +2245,7 @@ function isExecutableFile(path) {
2193
2245
  function findInstalledBinary() {
2194
2246
  const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
2195
2247
  const directories = [...pathDirectories(), ...wellKnownDirectories()];
2196
- const candidates = [...override ? [override] : [], ...directories.map((dir) => join10(dir, BINARY_NAME))];
2248
+ const candidates = [...override ? [override] : [], ...directories.map((dir) => join11(dir, BINARY_NAME))];
2197
2249
  return candidates.find(isExecutableFile) ?? null;
2198
2250
  }
2199
2251
  async function locateCloudflared() {
@@ -2424,6 +2476,8 @@ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2424
2476
  var SHIFT_TAB_DELAY_MS = 400;
2425
2477
  var PLAN_MODE_INDICATOR = /plan mode on/i;
2426
2478
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2479
+ ensureDirTrusted(repoPath);
2480
+ ensureMcpServersTrusted(repoPath);
2427
2481
  const claudeSessionId = randomUUID5();
2428
2482
  const pty = spawn5(
2429
2483
  CLAUDE_BINARY,
@@ -2520,6 +2574,8 @@ function sendContinue(pty, cardId) {
2520
2574
  }
2521
2575
  }
2522
2576
  function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
2577
+ ensureDirTrusted(repoPath);
2578
+ ensureMcpServersTrusted(repoPath);
2523
2579
  return spawn5(
2524
2580
  CLAUDE_BINARY,
2525
2581
  [
@@ -2543,8 +2599,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
2543
2599
  // src/cardAttachments.ts
2544
2600
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
2545
2601
  import { tmpdir as tmpdir6 } from "node:os";
2546
- import { extname, join as join11 } from "node:path";
2547
- var ATTACHMENTS_ROOT = join11(tmpdir6(), "agent-board", "card-attachments");
2602
+ import { extname, join as join12 } from "node:path";
2603
+ var ATTACHMENTS_ROOT = join12(tmpdir6(), "agent-board", "card-attachments");
2548
2604
  function sanitizeFileName(fileName) {
2549
2605
  return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
2550
2606
  }
@@ -2556,13 +2612,13 @@ async function downloadCardAttachments(supabase, cardId) {
2556
2612
  }
2557
2613
  const rows = data ?? [];
2558
2614
  if (rows.length === 0) return [];
2559
- const targetDir = join11(ATTACHMENTS_ROOT, cardId);
2615
+ const targetDir = join12(ATTACHMENTS_ROOT, cardId);
2560
2616
  await mkdir2(targetDir, { recursive: true });
2561
2617
  const paths = [];
2562
2618
  for (const row of rows) {
2563
2619
  const baseName = sanitizeFileName(row.file_name);
2564
2620
  const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
2565
- const localPath = join11(targetDir, `${row.id}-${fileName}`);
2621
+ const localPath = join12(targetDir, `${row.id}-${fileName}`);
2566
2622
  try {
2567
2623
  const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
2568
2624
  if (downloadError) throw downloadError;
@@ -2636,6 +2692,7 @@ function cancelUsageResume(sessionId) {
2636
2692
 
2637
2693
  // src/orchestrator.ts
2638
2694
  var TERMINAL_TICKET_TTL_MS = 60 * 60 * 1e3;
2695
+ var RESUME_CONVERSATION_MISSING_PATTERN = /no conversation found with session id/i;
2639
2696
  var inFlightCardIds = /* @__PURE__ */ new Set();
2640
2697
  var activeSessionByCard = /* @__PURE__ */ new Map();
2641
2698
  var intentionallyStoppedSessionIds = /* @__PURE__ */ new Set();
@@ -2709,11 +2766,17 @@ async function dispatchRunnerQueue(supabase, config) {
2709
2766
  try {
2710
2767
  const queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
2711
2768
  if (queued.length === 0) return;
2769
+ const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
2712
2770
  const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
2713
2771
  const runningProjectIds = /* @__PURE__ */ new Set();
2714
2772
  for (const project of config.registeredProjects) {
2715
- const runningCount = await countRunningSessions(supabase, project.id, config.ownerUserId);
2716
- if (runningCount > 0) runningProjectIds.add(project.id);
2773
+ const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
2774
+ if (!running) continue;
2775
+ if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
2776
+ await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
2777
+ continue;
2778
+ }
2779
+ runningProjectIds.add(project.id);
2717
2780
  }
2718
2781
  let freeProjectSlots = maxConcurrentProjects - runningProjectIds.size;
2719
2782
  if (freeProjectSlots <= 0) return;
@@ -2767,9 +2830,13 @@ async function spawnSessionPty(supabase, config, entry) {
2767
2830
  const ticket = randomUUID6();
2768
2831
  let pty;
2769
2832
  let newClaudeSessionId = null;
2833
+ let resumeConversationMissing = false;
2770
2834
  try {
2771
2835
  if (claudeSessionId) {
2772
2836
  pty = resumeAgentPty(registered.repoPath, claudeSessionId, sessionId, ticket);
2837
+ pty.onData((chunk) => {
2838
+ if (RESUME_CONVERSATION_MISSING_PATTERN.test(normalizeTerminalOutput(chunk))) resumeConversationMissing = true;
2839
+ });
2773
2840
  } else {
2774
2841
  const attachmentPaths = await downloadCardAttachments(supabase, cardId);
2775
2842
  const instructions = buildInstructionsWithMcpContext(
@@ -2817,11 +2884,24 @@ async function spawnSessionPty(supabase, config, entry) {
2817
2884
  );
2818
2885
  pty.onExit(({ exitCode }) => {
2819
2886
  const wasIntentional = intentionallyStoppedSessionIds.delete(sessionId);
2820
- logger.card.warn(cardId, `Session exited (code ${exitCode}) \u2014 this only happens if the process crashed or was killed, the card doesn't auto-advance.`);
2821
2887
  activityWatcher.stop();
2822
2888
  unregisterAgentActivityWatcher(sessionId);
2823
2889
  cancelUsageResume(sessionId);
2824
2890
  activeSessionByCard.delete(cardId);
2891
+ if (resumeConversationMissing) {
2892
+ logger.card.warn(
2893
+ cardId,
2894
+ `Session ${sessionId}: --resume ${claudeSessionId} found no such conversation \u2014 clearing it so the next attempt starts a fresh one instead of repeating the same failure.`
2895
+ );
2896
+ void updateSession(
2897
+ supabase,
2898
+ sessionId,
2899
+ { status: "failed", endedAt: (/* @__PURE__ */ new Date()).toISOString(), claudeSessionId: null },
2900
+ { projectId: card.project_id, cardId, userId: config.ownerUserId }
2901
+ );
2902
+ return;
2903
+ }
2904
+ logger.card.warn(cardId, `Session exited (code ${exitCode}) \u2014 this only happens if the process crashed or was killed, the card doesn't auto-advance.`);
2825
2905
  if (!wasIntentional && exitCode !== 0) {
2826
2906
  publishAgentState(supabase, card.project_id, cardId, sessionId, "error");
2827
2907
  }
@@ -2842,7 +2922,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
2842
2922
  } else {
2843
2923
  cancelUsageResume(sessionId);
2844
2924
  }
2845
- if (state === "done") void pauseFinishedSession(supabase, config, projectId, cardId, sessionId);
2925
+ if (state === "done") void dispatchRunnerQueue(supabase, config);
2846
2926
  });
2847
2927
  pty.onData((chunk) => watcher.push(chunk));
2848
2928
  registerAgentActivityWatcher(sessionId, watcher);
@@ -2852,8 +2932,7 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
2852
2932
  const paused = await pauseRunningSession(supabase, sessionId, { projectId, cardId, userId: config.ownerUserId });
2853
2933
  if (!paused) return;
2854
2934
  setPtySessionReadOnly(sessionId, true);
2855
- logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done") \u2014 WIP slot freed, pty stays viewable read-only.`);
2856
- await dispatchRunnerQueue(supabase, config);
2935
+ logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done", another session needs this project's WIP slot).`);
2857
2936
  }
2858
2937
  async function requeuePausedCard(supabase, config, card) {
2859
2938
  const active = await getActiveSessionForCard(supabase, card.id);
@@ -2931,6 +3010,24 @@ async function stopCardSession(supabase, config, card) {
2931
3010
  logger.card.info(card.id, `Session ${sessionId} stopped.`);
2932
3011
  await dispatchRunnerQueue(supabase, config);
2933
3012
  }
3013
+ async function restartCardSession(supabase, config, card) {
3014
+ const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
3015
+ if (sessionId) {
3016
+ intentionallyStoppedSessionIds.add(sessionId);
3017
+ stopPtySession(sessionId);
3018
+ cancelUsageResume(sessionId);
3019
+ activeSessionByCard.delete(card.id);
3020
+ await updateSession(
3021
+ supabase,
3022
+ sessionId,
3023
+ { status: "failed", claudeSessionId: null, endedAt: (/* @__PURE__ */ new Date()).toISOString() },
3024
+ { projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
3025
+ );
3026
+ logger.card.info(card.id, `Session ${sessionId} abandoned by "Reiniciar sess\xE3o"; starting a fresh one.`);
3027
+ }
3028
+ await updateCard(supabase, card.id, { restartRequestedAt: null });
3029
+ await enqueueCard(supabase, config, card);
3030
+ }
2934
3031
 
2935
3032
  // src/deploy.ts
2936
3033
  async function handleDeployRequested(supabase, config, project) {
@@ -2948,7 +3045,7 @@ async function handleDeployRequested(supabase, config, project) {
2948
3045
  }
2949
3046
  const children = await getProjectRepos(supabase, project.id);
2950
3047
  const repoRoot = resolveRepoPath(config, project.repo_path);
2951
- const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join12(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
3048
+ const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join13(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
2952
3049
  const failures = [];
2953
3050
  for (const target of targets) {
2954
3051
  const result = await pushBranchToMain(target.path);
@@ -3082,7 +3179,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
3082
3179
  }
3083
3180
 
3084
3181
  // src/review.ts
3085
- import { join as join13 } from "node:path";
3182
+ import { join as join14 } from "node:path";
3086
3183
  var inFlightCardIds2 = /* @__PURE__ */ new Set();
3087
3184
  async function handleCardEnteredRevisao(supabase, config, card) {
3088
3185
  if (inFlightCardIds2.has(card.id)) return;
@@ -3091,7 +3188,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
3091
3188
  const project = await getProject(supabase, card.project_id);
3092
3189
  const children = await getProjectRepos(supabase, project.id);
3093
3190
  const repoRoot = resolveRepoPath(config, project.repo_path);
3094
- const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd: join13(repoRoot, child2.subpath), command: child2.dev_command })) : project.dev_command ? [{ label: "", cwd: repoRoot, command: project.dev_command }] : [];
3191
+ const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd: join14(repoRoot, child2.subpath), command: child2.dev_command })) : project.dev_command ? [{ label: "", cwd: repoRoot, command: project.dev_command }] : [];
3095
3192
  if (targets.length === 0) {
3096
3193
  logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
3097
3194
  return;
@@ -3177,7 +3274,7 @@ function attachTerminalServer() {
3177
3274
  const buffer = data;
3178
3275
  const frameType = buffer[0];
3179
3276
  if (frameType === FRAME_TYPE_IMAGE) {
3180
- if (handle.readOnly) return;
3277
+ if (handle.readOnly || handle.exited) return;
3181
3278
  const extCode = buffer[1];
3182
3279
  const bytes = buffer.subarray(2);
3183
3280
  savePastedImage(sessionId, extCode, bytes).then((path) => handle.pty.write(`${path} `)).catch((err) => logger.runner.warn("TERMINAL", `Terminal WS: failed to save pasted image for session ${sessionId}:`, err));
@@ -3195,7 +3292,7 @@ function attachTerminalServer() {
3195
3292
  logger.runner.warn("TERMINAL", `Terminal WS: ignoring binary frame with unknown type ${frameType} for session ${sessionId}.`);
3196
3293
  return;
3197
3294
  }
3198
- if (handle.readOnly) return;
3295
+ if (handle.readOnly || handle.exited) return;
3199
3296
  handle.pty.write(data.toString());
3200
3297
  });
3201
3298
  socket.on("close", () => {
@@ -3317,6 +3414,7 @@ async function startRunner() {
3317
3414
  const registeredPaths = new Set(config.registeredProjects.map((project) => project.id));
3318
3415
  const lastKnownColumn = /* @__PURE__ */ new Map();
3319
3416
  const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
3417
+ const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
3320
3418
  await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
3321
3419
  logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
3322
3420
  for (const project of config.registeredProjects) {
@@ -3412,6 +3510,11 @@ async function startRunner() {
3412
3510
  if (card.requeue_requested_at && card.requeue_requested_at !== previousRequeueRequestedAt) {
3413
3511
  void requeuePausedCard(supabase, config, card);
3414
3512
  }
3513
+ const previousRestartRequestedAt = lastKnownRestartRequestedAt.get(card.id);
3514
+ lastKnownRestartRequestedAt.set(card.id, card.restart_requested_at);
3515
+ if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
3516
+ void restartCardSession(supabase, config, card);
3517
+ }
3415
3518
  if (previousColumn !== void 0 && previousColumn !== card.column_name) {
3416
3519
  void clearColumnActionStatus(supabase, card);
3417
3520
  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.2",
3
+ "version": "0.1.4",
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": {