agent-board-runner 0.1.2 → 0.1.3

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 +159 -61
  2. package/dist/index.js +164 -66
  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
@@ -1263,38 +1269,79 @@ function claudeCliSettingsArgs() {
1263
1269
  ];
1264
1270
  }
1265
1271
 
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() {
1272
+ // src/claudeTrust.ts
1273
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
1274
+ import { homedir as homedir3 } from "node:os";
1275
+ import { join as join7 } from "node:path";
1276
+ function ensureDirTrusted(dirPath) {
1280
1277
  const claudeJsonPath = join7(homedir3(), ".claude.json");
1281
1278
  if (!existsSync3(claudeJsonPath)) return;
1282
1279
  try {
1283
1280
  const raw = readFileSync3(claudeJsonPath, "utf8");
1284
1281
  const data = JSON.parse(raw);
1285
1282
  data.projects ??= {};
1286
- const keys = [SCRATCH_DIR, SCRATCH_DIR.replace(/\\/g, "/")];
1283
+ const keys = [dirPath, dirPath.replace(/\\/g, "/")];
1287
1284
  const alreadyTrusted = keys.every((key) => data.projects[key]?.hasTrustDialogAccepted === true);
1288
1285
  if (alreadyTrusted) return;
1289
1286
  for (const key of keys) {
1290
1287
  data.projects[key] = { ...data.projects[key], hasTrustDialogAccepted: true };
1291
1288
  }
1292
1289
  writeFileSync6(claudeJsonPath, JSON.stringify(data, null, 2));
1293
- logger.runner.info("USAGE_MONITOR", `Pre-trusted scratch directory ${SCRATCH_DIR} in ~/.claude.json.`);
1290
+ logger.runner.info("TRUST", `Pre-trusted ${dirPath} in ~/.claude.json.`);
1294
1291
  } catch (err) {
1295
- logger.runner.error("USAGE_MONITOR", "Failed to pre-trust the usage-monitor scratch directory:", err);
1292
+ logger.runner.error("TRUST", `Failed to pre-trust ${dirPath}:`, err);
1296
1293
  }
1297
1294
  }
1295
+ function ensureMcpServersTrusted(dirPath) {
1296
+ const mcpJsonPath = join7(dirPath, ".mcp.json");
1297
+ if (!existsSync3(mcpJsonPath)) return;
1298
+ let serverNames;
1299
+ try {
1300
+ const mcpConfig = JSON.parse(readFileSync3(mcpJsonPath, "utf8"));
1301
+ serverNames = Object.keys(mcpConfig.mcpServers ?? {});
1302
+ } catch (err) {
1303
+ logger.runner.error("TRUST", `Failed to read ${mcpJsonPath}:`, err);
1304
+ return;
1305
+ }
1306
+ if (serverNames.length === 0) return;
1307
+ const claudeJsonPath = join7(homedir3(), ".claude.json");
1308
+ if (!existsSync3(claudeJsonPath)) return;
1309
+ try {
1310
+ const raw = readFileSync3(claudeJsonPath, "utf8");
1311
+ const data = JSON.parse(raw);
1312
+ data.projects ??= {};
1313
+ const keys = [dirPath, dirPath.replace(/\\/g, "/")];
1314
+ const alreadyApproved = keys.every(
1315
+ (key) => serverNames.every((name) => data.projects[key]?.enabledMcpjsonServers?.includes(name))
1316
+ );
1317
+ if (alreadyApproved) return;
1318
+ for (const key of keys) {
1319
+ const project = data.projects[key] ??= {};
1320
+ project.enabledMcpjsonServers = [.../* @__PURE__ */ new Set([...project.enabledMcpjsonServers ?? [], ...serverNames])];
1321
+ project.disabledMcpjsonServers = (project.disabledMcpjsonServers ?? []).filter(
1322
+ (name) => !serverNames.includes(name)
1323
+ );
1324
+ }
1325
+ writeFileSync6(claudeJsonPath, JSON.stringify(data, null, 2));
1326
+ logger.runner.info("TRUST", `Pre-approved MCP servers [${serverNames.join(", ")}] for ${dirPath}.`);
1327
+ } catch (err) {
1328
+ logger.runner.error("TRUST", `Failed to pre-approve MCP servers for ${dirPath}:`, err);
1329
+ }
1330
+ }
1331
+
1332
+ // src/usageMonitor.ts
1333
+ var SESSION_ID = "usage-monitor";
1334
+ var TICKET = randomUUID4();
1335
+ var SCRATCH_DIR = join8(tmpdir5(), "agent-board-usage-monitor");
1336
+ var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
1337
+ var IDLE_MS = 2e3;
1338
+ var HARD_TIMEOUT_MS = 6e4;
1339
+ function usageMonitorSessionId() {
1340
+ return SESSION_ID;
1341
+ }
1342
+ function usageMonitorTicket() {
1343
+ return TICKET;
1344
+ }
1298
1345
  function pollUsageOnce() {
1299
1346
  let pty;
1300
1347
  try {
@@ -1341,7 +1388,7 @@ function pollUsageOnce() {
1341
1388
  }
1342
1389
  function startUsageMonitor() {
1343
1390
  mkdirSync3(SCRATCH_DIR, { recursive: true });
1344
- ensureScratchDirTrusted();
1391
+ ensureDirTrusted(SCRATCH_DIR);
1345
1392
  pollUsageOnce();
1346
1393
  setInterval(pollUsageOnce, POLL_INTERVAL_MS);
1347
1394
  }
@@ -1357,7 +1404,7 @@ var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modu
1357
1404
  var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
1358
1405
  function ensureSkillsDirExists(claudeDir) {
1359
1406
  try {
1360
- mkdirSync4(join8(claudeDir, "skills"), { recursive: true });
1407
+ mkdirSync4(join9(claudeDir, "skills"), { recursive: true });
1361
1408
  } catch (err) {
1362
1409
  logger.runner.warn("FILES", "Could not create .claude/skills:", err);
1363
1410
  }
@@ -1484,7 +1531,7 @@ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
1484
1531
  }
1485
1532
  const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
1486
1533
  if (isRepoRootLevel) {
1487
- ensureSkillsDirExists(join8(baseDir, relPath, ".claude"));
1534
+ ensureSkillsDirExists(join9(baseDir, relPath, ".claude"));
1488
1535
  const entries2 = listDirectory(baseDir, relPath, { includeDotClaude: true });
1489
1536
  return entries2.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
1490
1537
  }
@@ -1515,7 +1562,7 @@ function assertWritableAgentPath(relPath, atClaudeRoot) {
1515
1562
  function listDirectoryForBrowse(rootPath, relPath) {
1516
1563
  return listDirectory(rootPath, relPath).map((entry) => ({
1517
1564
  ...entry,
1518
- hasGit: entry.type === "dir" && existsSync4(join8(rootPath, entry.path, ".git"))
1565
+ hasGit: entry.type === "dir" && existsSync4(join9(rootPath, entry.path, ".git"))
1519
1566
  }));
1520
1567
  }
1521
1568
  var SAFE_CLONE_URL_PATTERN = /^(https?:\/\/|git:\/\/|ssh:\/\/|git@[\w.-]+:)\S+$/i;
@@ -1553,7 +1600,7 @@ async function handleCloneRequest(req, res, query) {
1553
1600
  sendJson(res, 400, { error: "N\xE3o foi poss\xEDvel determinar o nome da pasta a partir da URL." });
1554
1601
  return;
1555
1602
  }
1556
- const destPath = join8(setupEntry.rootPath, folderName);
1603
+ const destPath = join9(setupEntry.rootPath, folderName);
1557
1604
  if (existsSync4(destPath)) {
1558
1605
  sendJson(res, 409, { error: `J\xE1 existe uma pasta "${folderName}" na raiz permitida.` });
1559
1606
  return;
@@ -1621,7 +1668,7 @@ function findBrandIconPath(repoRoot) {
1621
1668
  if (entry.isSymbolicLink() || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
1622
1669
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
1623
1670
  if (entry.isDirectory()) {
1624
- walk(join8(dir, entry.name), relPath, depth + 1);
1671
+ walk(join9(dir, entry.name), relPath, depth + 1);
1625
1672
  } else if (entry.isFile()) {
1626
1673
  const match = patterns.find(({ pattern }) => pattern.test(entry.name));
1627
1674
  if (match) candidatesByBaseName.get(match.baseName).push({ relPath, depth });
@@ -1786,7 +1833,7 @@ async function getRepoDiffForTarget(label, cwd) {
1786
1833
  async function handleDiffRequest(entry, res, projectId) {
1787
1834
  try {
1788
1835
  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 }];
1836
+ const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join9(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
1790
1837
  const repos = await Promise.all(targets.map((target) => getRepoDiffForTarget(target.label, target.cwd)));
1791
1838
  sendJson(res, 200, { repos });
1792
1839
  } catch (err) {
@@ -2094,7 +2141,7 @@ function attachAgentEventsServer() {
2094
2141
 
2095
2142
  // src/columnActions.ts
2096
2143
  import { exec } from "node:child_process";
2097
- import { join as join9 } from "node:path";
2144
+ import { join as join10 } from "node:path";
2098
2145
  import { promisify as promisify3 } from "node:util";
2099
2146
  var execAsync = promisify3(exec);
2100
2147
  var OUTPUT_TAIL_CHARS = 4e3;
@@ -2104,7 +2151,7 @@ function tail(text) {
2104
2151
  function resolveTargets(project, children, repoRoot, column, timing) {
2105
2152
  if (children.length > 0) {
2106
2153
  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 }))
2154
+ (child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join10(repoRoot, child2.subpath), command: action.command }))
2108
2155
  );
2109
2156
  }
2110
2157
  return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
@@ -2210,7 +2257,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
2210
2257
  }
2211
2258
 
2212
2259
  // src/deploy.ts
2213
- import { join as join12 } from "node:path";
2260
+ import { join as join13 } from "node:path";
2214
2261
 
2215
2262
  // src/devServer.ts
2216
2263
  import { spawn as spawn4 } from "node:child_process";
@@ -2219,7 +2266,7 @@ import { spawn as spawn4 } from "node:child_process";
2219
2266
  import { spawn as spawn3 } from "node:child_process";
2220
2267
  import { accessSync, constants } from "node:fs";
2221
2268
  import { homedir as homedir5 } from "node:os";
2222
- import { delimiter, join as join10 } from "node:path";
2269
+ import { delimiter, join as join11 } from "node:path";
2223
2270
  import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
2224
2271
  var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
2225
2272
  var STARTUP_TIMEOUT_MS = 2e4;
@@ -2230,15 +2277,15 @@ function wellKnownDirectories() {
2230
2277
  const home = homedir5();
2231
2278
  if (IS_WINDOWS2) {
2232
2279
  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")
2280
+ process.env.ProgramFiles && join11(process.env.ProgramFiles, "cloudflared"),
2281
+ process.env["ProgramFiles(x86)"] && join11(process.env["ProgramFiles(x86)"], "cloudflared"),
2282
+ process.env.LOCALAPPDATA && join11(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
2283
+ join11(home, "scoop", "shims"),
2284
+ process.env.ChocolateyInstall && join11(process.env.ChocolateyInstall, "bin")
2238
2285
  ];
2239
2286
  return candidates.filter((dir) => Boolean(dir));
2240
2287
  }
2241
- return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join10(home, ".local", "bin")];
2288
+ return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join11(home, ".local", "bin")];
2242
2289
  }
2243
2290
  function pathDirectories() {
2244
2291
  return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
@@ -2254,7 +2301,7 @@ function isExecutableFile(path) {
2254
2301
  function findInstalledBinary() {
2255
2302
  const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
2256
2303
  const directories = [...pathDirectories(), ...wellKnownDirectories()];
2257
- const candidates = [...override ? [override] : [], ...directories.map((dir) => join10(dir, BINARY_NAME))];
2304
+ const candidates = [...override ? [override] : [], ...directories.map((dir) => join11(dir, BINARY_NAME))];
2258
2305
  return candidates.find(isExecutableFile) ?? null;
2259
2306
  }
2260
2307
  async function locateCloudflared() {
@@ -2485,6 +2532,8 @@ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2485
2532
  var SHIFT_TAB_DELAY_MS = 400;
2486
2533
  var PLAN_MODE_INDICATOR = /plan mode on/i;
2487
2534
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2535
+ ensureDirTrusted(repoPath);
2536
+ ensureMcpServersTrusted(repoPath);
2488
2537
  const claudeSessionId = randomUUID6();
2489
2538
  const pty = spawn5(
2490
2539
  CLAUDE_BINARY,
@@ -2581,6 +2630,8 @@ function sendContinue(pty, cardId) {
2581
2630
  }
2582
2631
  }
2583
2632
  function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
2633
+ ensureDirTrusted(repoPath);
2634
+ ensureMcpServersTrusted(repoPath);
2584
2635
  return spawn5(
2585
2636
  CLAUDE_BINARY,
2586
2637
  [
@@ -2604,8 +2655,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
2604
2655
  // src/cardAttachments.ts
2605
2656
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
2606
2657
  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");
2658
+ import { extname, join as join12 } from "node:path";
2659
+ var ATTACHMENTS_ROOT = join12(tmpdir6(), "agent-board", "card-attachments");
2609
2660
  function sanitizeFileName(fileName) {
2610
2661
  return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
2611
2662
  }
@@ -2617,13 +2668,13 @@ async function downloadCardAttachments(supabase, cardId) {
2617
2668
  }
2618
2669
  const rows = data ?? [];
2619
2670
  if (rows.length === 0) return [];
2620
- const targetDir = join11(ATTACHMENTS_ROOT, cardId);
2671
+ const targetDir = join12(ATTACHMENTS_ROOT, cardId);
2621
2672
  await mkdir2(targetDir, { recursive: true });
2622
2673
  const paths = [];
2623
2674
  for (const row of rows) {
2624
2675
  const baseName = sanitizeFileName(row.file_name);
2625
2676
  const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
2626
- const localPath = join11(targetDir, `${row.id}-${fileName}`);
2677
+ const localPath = join12(targetDir, `${row.id}-${fileName}`);
2627
2678
  try {
2628
2679
  const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
2629
2680
  if (downloadError) throw downloadError;
@@ -2697,6 +2748,7 @@ function cancelUsageResume(sessionId) {
2697
2748
 
2698
2749
  // src/orchestrator.ts
2699
2750
  var TERMINAL_TICKET_TTL_MS = 60 * 60 * 1e3;
2751
+ var RESUME_CONVERSATION_MISSING_PATTERN = /no conversation found with session id/i;
2700
2752
  var inFlightCardIds = /* @__PURE__ */ new Set();
2701
2753
  var activeSessionByCard = /* @__PURE__ */ new Map();
2702
2754
  var intentionallyStoppedSessionIds = /* @__PURE__ */ new Set();
@@ -2770,11 +2822,17 @@ async function dispatchRunnerQueue(supabase, config) {
2770
2822
  try {
2771
2823
  const queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
2772
2824
  if (queued.length === 0) return;
2825
+ const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
2773
2826
  const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
2774
2827
  const runningProjectIds = /* @__PURE__ */ new Set();
2775
2828
  for (const project of config.registeredProjects) {
2776
- const runningCount = await countRunningSessions(supabase, project.id, config.ownerUserId);
2777
- if (runningCount > 0) runningProjectIds.add(project.id);
2829
+ const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
2830
+ if (!running) continue;
2831
+ if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
2832
+ await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
2833
+ continue;
2834
+ }
2835
+ runningProjectIds.add(project.id);
2778
2836
  }
2779
2837
  let freeProjectSlots = maxConcurrentProjects - runningProjectIds.size;
2780
2838
  if (freeProjectSlots <= 0) return;
@@ -2828,9 +2886,13 @@ async function spawnSessionPty(supabase, config, entry) {
2828
2886
  const ticket = randomUUID7();
2829
2887
  let pty;
2830
2888
  let newClaudeSessionId = null;
2889
+ let resumeConversationMissing = false;
2831
2890
  try {
2832
2891
  if (claudeSessionId) {
2833
2892
  pty = resumeAgentPty(registered.repoPath, claudeSessionId, sessionId, ticket);
2893
+ pty.onData((chunk) => {
2894
+ if (RESUME_CONVERSATION_MISSING_PATTERN.test(normalizeTerminalOutput(chunk))) resumeConversationMissing = true;
2895
+ });
2834
2896
  } else {
2835
2897
  const attachmentPaths = await downloadCardAttachments(supabase, cardId);
2836
2898
  const instructions = buildInstructionsWithMcpContext(
@@ -2878,11 +2940,24 @@ async function spawnSessionPty(supabase, config, entry) {
2878
2940
  );
2879
2941
  pty.onExit(({ exitCode }) => {
2880
2942
  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
2943
  activityWatcher.stop();
2883
2944
  unregisterAgentActivityWatcher(sessionId);
2884
2945
  cancelUsageResume(sessionId);
2885
2946
  activeSessionByCard.delete(cardId);
2947
+ if (resumeConversationMissing) {
2948
+ logger.card.warn(
2949
+ cardId,
2950
+ `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.`
2951
+ );
2952
+ void updateSession(
2953
+ supabase,
2954
+ sessionId,
2955
+ { status: "failed", endedAt: (/* @__PURE__ */ new Date()).toISOString(), claudeSessionId: null },
2956
+ { projectId: card.project_id, cardId, userId: config.ownerUserId }
2957
+ );
2958
+ return;
2959
+ }
2960
+ 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
2961
  if (!wasIntentional && exitCode !== 0) {
2887
2962
  publishAgentState(supabase, card.project_id, cardId, sessionId, "error");
2888
2963
  }
@@ -2903,7 +2978,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
2903
2978
  } else {
2904
2979
  cancelUsageResume(sessionId);
2905
2980
  }
2906
- if (state === "done") void pauseFinishedSession(supabase, config, projectId, cardId, sessionId);
2981
+ if (state === "done") void dispatchRunnerQueue(supabase, config);
2907
2982
  });
2908
2983
  pty.onData((chunk) => watcher.push(chunk));
2909
2984
  registerAgentActivityWatcher(sessionId, watcher);
@@ -2913,8 +2988,7 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
2913
2988
  const paused = await pauseRunningSession(supabase, sessionId, { projectId, cardId, userId: config.ownerUserId });
2914
2989
  if (!paused) return;
2915
2990
  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);
2991
+ logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done", another session needs this project's WIP slot).`);
2918
2992
  }
2919
2993
  async function requeuePausedCard(supabase, config, card) {
2920
2994
  const active = await getActiveSessionForCard(supabase, card.id);
@@ -2992,6 +3066,24 @@ async function stopCardSession(supabase, config, card) {
2992
3066
  logger.card.info(card.id, `Session ${sessionId} stopped.`);
2993
3067
  await dispatchRunnerQueue(supabase, config);
2994
3068
  }
3069
+ async function restartCardSession(supabase, config, card) {
3070
+ const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
3071
+ if (sessionId) {
3072
+ intentionallyStoppedSessionIds.add(sessionId);
3073
+ stopPtySession(sessionId);
3074
+ cancelUsageResume(sessionId);
3075
+ activeSessionByCard.delete(card.id);
3076
+ await updateSession(
3077
+ supabase,
3078
+ sessionId,
3079
+ { status: "failed", claudeSessionId: null, endedAt: (/* @__PURE__ */ new Date()).toISOString() },
3080
+ { projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
3081
+ );
3082
+ logger.card.info(card.id, `Session ${sessionId} abandoned by "Reiniciar sess\xE3o"; starting a fresh one.`);
3083
+ }
3084
+ await updateCard(supabase, card.id, { restartRequestedAt: null });
3085
+ await enqueueCard(supabase, config, card);
3086
+ }
2995
3087
 
2996
3088
  // src/deploy.ts
2997
3089
  async function handleDeployRequested(supabase, config, project) {
@@ -3009,7 +3101,7 @@ async function handleDeployRequested(supabase, config, project) {
3009
3101
  }
3010
3102
  const children = await getProjectRepos(supabase, project.id);
3011
3103
  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 }];
3104
+ const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join13(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
3013
3105
  const failures = [];
3014
3106
  for (const target of targets) {
3015
3107
  const result = await pushBranchToMain(target.path);
@@ -3143,7 +3235,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
3143
3235
  }
3144
3236
 
3145
3237
  // src/review.ts
3146
- import { join as join13 } from "node:path";
3238
+ import { join as join14 } from "node:path";
3147
3239
  var inFlightCardIds2 = /* @__PURE__ */ new Set();
3148
3240
  async function handleCardEnteredRevisao(supabase, config, card) {
3149
3241
  if (inFlightCardIds2.has(card.id)) return;
@@ -3152,7 +3244,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
3152
3244
  const project = await getProject(supabase, card.project_id);
3153
3245
  const children = await getProjectRepos(supabase, project.id);
3154
3246
  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 }] : [];
3247
+ 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
3248
  if (targets.length === 0) {
3157
3249
  logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
3158
3250
  return;
@@ -3238,7 +3330,7 @@ function attachTerminalServer() {
3238
3330
  const buffer = data;
3239
3331
  const frameType = buffer[0];
3240
3332
  if (frameType === FRAME_TYPE_IMAGE) {
3241
- if (handle.readOnly) return;
3333
+ if (handle.readOnly || handle.exited) return;
3242
3334
  const extCode = buffer[1];
3243
3335
  const bytes = buffer.subarray(2);
3244
3336
  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 +3348,7 @@ function attachTerminalServer() {
3256
3348
  logger.runner.warn("TERMINAL", `Terminal WS: ignoring binary frame with unknown type ${frameType} for session ${sessionId}.`);
3257
3349
  return;
3258
3350
  }
3259
- if (handle.readOnly) return;
3351
+ if (handle.readOnly || handle.exited) return;
3260
3352
  handle.pty.write(data.toString());
3261
3353
  });
3262
3354
  socket.on("close", () => {
@@ -3378,6 +3470,7 @@ async function startRunner() {
3378
3470
  const registeredPaths = new Set(config.registeredProjects.map((project) => project.id));
3379
3471
  const lastKnownColumn = /* @__PURE__ */ new Map();
3380
3472
  const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
3473
+ const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
3381
3474
  await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
3382
3475
  logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
3383
3476
  for (const project of config.registeredProjects) {
@@ -3473,6 +3566,11 @@ async function startRunner() {
3473
3566
  if (card.requeue_requested_at && card.requeue_requested_at !== previousRequeueRequestedAt) {
3474
3567
  void requeuePausedCard(supabase, config, card);
3475
3568
  }
3569
+ const previousRestartRequestedAt = lastKnownRestartRequestedAt.get(card.id);
3570
+ lastKnownRestartRequestedAt.set(card.id, card.restart_requested_at);
3571
+ if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
3572
+ void restartCardSession(supabase, config, card);
3573
+ }
3476
3574
  if (previousColumn !== void 0 && previousColumn !== card.column_name) {
3477
3575
  void clearColumnActionStatus(supabase, card);
3478
3576
  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
@@ -1101,38 +1107,79 @@ function claudeCliSettingsArgs() {
1101
1107
  ];
1102
1108
  }
1103
1109
 
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() {
1110
+ // src/claudeTrust.ts
1111
+ import { existsSync, readFileSync, writeFileSync as writeFileSync4 } from "node:fs";
1112
+ import { homedir as homedir2 } from "node:os";
1113
+ import { join as join5 } from "node:path";
1114
+ function ensureDirTrusted(dirPath) {
1118
1115
  const claudeJsonPath = join5(homedir2(), ".claude.json");
1119
1116
  if (!existsSync(claudeJsonPath)) return;
1120
1117
  try {
1121
1118
  const raw = readFileSync(claudeJsonPath, "utf8");
1122
1119
  const data = JSON.parse(raw);
1123
1120
  data.projects ??= {};
1124
- const keys = [SCRATCH_DIR, SCRATCH_DIR.replace(/\\/g, "/")];
1121
+ const keys = [dirPath, dirPath.replace(/\\/g, "/")];
1125
1122
  const alreadyTrusted = keys.every((key) => data.projects[key]?.hasTrustDialogAccepted === true);
1126
1123
  if (alreadyTrusted) return;
1127
1124
  for (const key of keys) {
1128
1125
  data.projects[key] = { ...data.projects[key], hasTrustDialogAccepted: true };
1129
1126
  }
1130
1127
  writeFileSync4(claudeJsonPath, JSON.stringify(data, null, 2));
1131
- logger.runner.info("USAGE_MONITOR", `Pre-trusted scratch directory ${SCRATCH_DIR} in ~/.claude.json.`);
1128
+ logger.runner.info("TRUST", `Pre-trusted ${dirPath} in ~/.claude.json.`);
1129
+ } catch (err) {
1130
+ logger.runner.error("TRUST", `Failed to pre-trust ${dirPath}:`, err);
1131
+ }
1132
+ }
1133
+ function ensureMcpServersTrusted(dirPath) {
1134
+ const mcpJsonPath = join5(dirPath, ".mcp.json");
1135
+ if (!existsSync(mcpJsonPath)) return;
1136
+ let serverNames;
1137
+ try {
1138
+ const mcpConfig = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
1139
+ serverNames = Object.keys(mcpConfig.mcpServers ?? {});
1140
+ } catch (err) {
1141
+ logger.runner.error("TRUST", `Failed to read ${mcpJsonPath}:`, err);
1142
+ return;
1143
+ }
1144
+ if (serverNames.length === 0) return;
1145
+ const claudeJsonPath = join5(homedir2(), ".claude.json");
1146
+ if (!existsSync(claudeJsonPath)) return;
1147
+ try {
1148
+ const raw = readFileSync(claudeJsonPath, "utf8");
1149
+ const data = JSON.parse(raw);
1150
+ data.projects ??= {};
1151
+ const keys = [dirPath, dirPath.replace(/\\/g, "/")];
1152
+ const alreadyApproved = keys.every(
1153
+ (key) => serverNames.every((name) => data.projects[key]?.enabledMcpjsonServers?.includes(name))
1154
+ );
1155
+ if (alreadyApproved) return;
1156
+ for (const key of keys) {
1157
+ const project = data.projects[key] ??= {};
1158
+ project.enabledMcpjsonServers = [.../* @__PURE__ */ new Set([...project.enabledMcpjsonServers ?? [], ...serverNames])];
1159
+ project.disabledMcpjsonServers = (project.disabledMcpjsonServers ?? []).filter(
1160
+ (name) => !serverNames.includes(name)
1161
+ );
1162
+ }
1163
+ writeFileSync4(claudeJsonPath, JSON.stringify(data, null, 2));
1164
+ logger.runner.info("TRUST", `Pre-approved MCP servers [${serverNames.join(", ")}] for ${dirPath}.`);
1132
1165
  } catch (err) {
1133
- logger.runner.error("USAGE_MONITOR", "Failed to pre-trust the usage-monitor scratch directory:", err);
1166
+ logger.runner.error("TRUST", `Failed to pre-approve MCP servers for ${dirPath}:`, err);
1134
1167
  }
1135
1168
  }
1169
+
1170
+ // src/usageMonitor.ts
1171
+ var SESSION_ID = "usage-monitor";
1172
+ var TICKET = randomUUID3();
1173
+ var SCRATCH_DIR = join6(tmpdir5(), "agent-board-usage-monitor");
1174
+ var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
1175
+ var IDLE_MS = 2e3;
1176
+ var HARD_TIMEOUT_MS = 6e4;
1177
+ function usageMonitorSessionId() {
1178
+ return SESSION_ID;
1179
+ }
1180
+ function usageMonitorTicket() {
1181
+ return TICKET;
1182
+ }
1136
1183
  function pollUsageOnce() {
1137
1184
  let pty;
1138
1185
  try {
@@ -1179,7 +1226,7 @@ function pollUsageOnce() {
1179
1226
  }
1180
1227
  function startUsageMonitor() {
1181
1228
  mkdirSync(SCRATCH_DIR, { recursive: true });
1182
- ensureScratchDirTrusted();
1229
+ ensureDirTrusted(SCRATCH_DIR);
1183
1230
  pollUsageOnce();
1184
1231
  setInterval(pollUsageOnce, POLL_INTERVAL_MS);
1185
1232
  }
@@ -1195,7 +1242,7 @@ var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modu
1195
1242
  var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
1196
1243
  function ensureSkillsDirExists(claudeDir) {
1197
1244
  try {
1198
- mkdirSync2(join6(claudeDir, "skills"), { recursive: true });
1245
+ mkdirSync2(join7(claudeDir, "skills"), { recursive: true });
1199
1246
  } catch (err) {
1200
1247
  logger.runner.warn("FILES", "Could not create .claude/skills:", err);
1201
1248
  }
@@ -1322,7 +1369,7 @@ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
1322
1369
  }
1323
1370
  const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
1324
1371
  if (isRepoRootLevel) {
1325
- ensureSkillsDirExists(join6(baseDir, relPath, ".claude"));
1372
+ ensureSkillsDirExists(join7(baseDir, relPath, ".claude"));
1326
1373
  const entries2 = listDirectory(baseDir, relPath, { includeDotClaude: true });
1327
1374
  return entries2.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
1328
1375
  }
@@ -1353,7 +1400,7 @@ function assertWritableAgentPath(relPath, atClaudeRoot) {
1353
1400
  function listDirectoryForBrowse(rootPath, relPath) {
1354
1401
  return listDirectory(rootPath, relPath).map((entry) => ({
1355
1402
  ...entry,
1356
- hasGit: entry.type === "dir" && existsSync2(join6(rootPath, entry.path, ".git"))
1403
+ hasGit: entry.type === "dir" && existsSync2(join7(rootPath, entry.path, ".git"))
1357
1404
  }));
1358
1405
  }
1359
1406
  var SAFE_CLONE_URL_PATTERN = /^(https?:\/\/|git:\/\/|ssh:\/\/|git@[\w.-]+:)\S+$/i;
@@ -1391,7 +1438,7 @@ async function handleCloneRequest(req, res, query) {
1391
1438
  sendJson(res, 400, { error: "N\xE3o foi poss\xEDvel determinar o nome da pasta a partir da URL." });
1392
1439
  return;
1393
1440
  }
1394
- const destPath = join6(setupEntry.rootPath, folderName);
1441
+ const destPath = join7(setupEntry.rootPath, folderName);
1395
1442
  if (existsSync2(destPath)) {
1396
1443
  sendJson(res, 409, { error: `J\xE1 existe uma pasta "${folderName}" na raiz permitida.` });
1397
1444
  return;
@@ -1459,7 +1506,7 @@ function findBrandIconPath(repoRoot) {
1459
1506
  if (entry.isSymbolicLink() || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
1460
1507
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
1461
1508
  if (entry.isDirectory()) {
1462
- walk(join6(dir, entry.name), relPath, depth + 1);
1509
+ walk(join7(dir, entry.name), relPath, depth + 1);
1463
1510
  } else if (entry.isFile()) {
1464
1511
  const match = patterns.find(({ pattern }) => pattern.test(entry.name));
1465
1512
  if (match) candidatesByBaseName.get(match.baseName).push({ relPath, depth });
@@ -1624,7 +1671,7 @@ async function getRepoDiffForTarget(label, cwd) {
1624
1671
  async function handleDiffRequest(entry, res, projectId) {
1625
1672
  try {
1626
1673
  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 }];
1674
+ const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join7(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
1628
1675
  const repos = await Promise.all(targets.map((target) => getRepoDiffForTarget(target.label, target.cwd)));
1629
1676
  sendJson(res, 200, { repos });
1630
1677
  } catch (err) {
@@ -1933,15 +1980,15 @@ function attachAgentEventsServer() {
1933
1980
  // src/auth.ts
1934
1981
  import { createClient } from "@supabase/supabase-js";
1935
1982
  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";
1983
+ import { join as join9 } from "node:path";
1937
1984
  import open from "open";
1938
1985
 
1939
1986
  // src/config.ts
1940
1987
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
1941
1988
  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");
1989
+ import { join as join8, resolve as resolve2 } from "node:path";
1990
+ var CONFIG_DIR = join8(homedir4(), ".agent-board");
1991
+ var CONFIG_FILE = join8(CONFIG_DIR, "credentials.json");
1945
1992
  function loadConfig() {
1946
1993
  if (!existsSync3(CONFIG_FILE)) return null;
1947
1994
  return JSON.parse(readFileSync3(CONFIG_FILE, "utf-8"));
@@ -1974,7 +2021,7 @@ function resolveRepoPath(config, relativeRepoPath) {
1974
2021
  }
1975
2022
 
1976
2023
  // src/auth.ts
1977
- var SESSION_FILE = join8(CONFIG_DIR, "session.json");
2024
+ var SESSION_FILE = join9(CONFIG_DIR, "session.json");
1978
2025
  var DEFAULT_SUPABASE_URL = "https://brpxshvnmxgkbffgdfcc.supabase.co";
1979
2026
  var DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJycHhzaHZubXhna2JmZmdkZmNjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODc1OTQwMTEsImV4cCI6MjEwMzE3MDAxMX0.ErsAGZysLFgNvijetR7w1YvIziXC-1D2sx04Hm2UW9c";
1980
2027
  function resolveSupabaseUrl() {
@@ -2033,7 +2080,7 @@ async function requireSession(supabase) {
2033
2080
 
2034
2081
  // src/columnActions.ts
2035
2082
  import { exec } from "node:child_process";
2036
- import { join as join9 } from "node:path";
2083
+ import { join as join10 } from "node:path";
2037
2084
  import { promisify as promisify3 } from "node:util";
2038
2085
  var execAsync = promisify3(exec);
2039
2086
  var OUTPUT_TAIL_CHARS = 4e3;
@@ -2043,7 +2090,7 @@ function tail(text) {
2043
2090
  function resolveTargets(project, children, repoRoot, column, timing) {
2044
2091
  if (children.length > 0) {
2045
2092
  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 }))
2093
+ (child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join10(repoRoot, child2.subpath), command: action.command }))
2047
2094
  );
2048
2095
  }
2049
2096
  return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
@@ -2149,7 +2196,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
2149
2196
  }
2150
2197
 
2151
2198
  // src/deploy.ts
2152
- import { join as join12 } from "node:path";
2199
+ import { join as join13 } from "node:path";
2153
2200
 
2154
2201
  // src/devServer.ts
2155
2202
  import { spawn as spawn4 } from "node:child_process";
@@ -2158,7 +2205,7 @@ import { spawn as spawn4 } from "node:child_process";
2158
2205
  import { spawn as spawn3 } from "node:child_process";
2159
2206
  import { accessSync, constants } from "node:fs";
2160
2207
  import { homedir as homedir5 } from "node:os";
2161
- import { delimiter, join as join10 } from "node:path";
2208
+ import { delimiter, join as join11 } from "node:path";
2162
2209
  import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
2163
2210
  var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
2164
2211
  var STARTUP_TIMEOUT_MS = 2e4;
@@ -2169,15 +2216,15 @@ function wellKnownDirectories() {
2169
2216
  const home = homedir5();
2170
2217
  if (IS_WINDOWS2) {
2171
2218
  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")
2219
+ process.env.ProgramFiles && join11(process.env.ProgramFiles, "cloudflared"),
2220
+ process.env["ProgramFiles(x86)"] && join11(process.env["ProgramFiles(x86)"], "cloudflared"),
2221
+ process.env.LOCALAPPDATA && join11(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
2222
+ join11(home, "scoop", "shims"),
2223
+ process.env.ChocolateyInstall && join11(process.env.ChocolateyInstall, "bin")
2177
2224
  ];
2178
2225
  return candidates.filter((dir) => Boolean(dir));
2179
2226
  }
2180
- return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join10(home, ".local", "bin")];
2227
+ return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join11(home, ".local", "bin")];
2181
2228
  }
2182
2229
  function pathDirectories() {
2183
2230
  return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
@@ -2193,7 +2240,7 @@ function isExecutableFile(path) {
2193
2240
  function findInstalledBinary() {
2194
2241
  const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
2195
2242
  const directories = [...pathDirectories(), ...wellKnownDirectories()];
2196
- const candidates = [...override ? [override] : [], ...directories.map((dir) => join10(dir, BINARY_NAME))];
2243
+ const candidates = [...override ? [override] : [], ...directories.map((dir) => join11(dir, BINARY_NAME))];
2197
2244
  return candidates.find(isExecutableFile) ?? null;
2198
2245
  }
2199
2246
  async function locateCloudflared() {
@@ -2424,6 +2471,8 @@ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2424
2471
  var SHIFT_TAB_DELAY_MS = 400;
2425
2472
  var PLAN_MODE_INDICATOR = /plan mode on/i;
2426
2473
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2474
+ ensureDirTrusted(repoPath);
2475
+ ensureMcpServersTrusted(repoPath);
2427
2476
  const claudeSessionId = randomUUID5();
2428
2477
  const pty = spawn5(
2429
2478
  CLAUDE_BINARY,
@@ -2520,6 +2569,8 @@ function sendContinue(pty, cardId) {
2520
2569
  }
2521
2570
  }
2522
2571
  function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
2572
+ ensureDirTrusted(repoPath);
2573
+ ensureMcpServersTrusted(repoPath);
2523
2574
  return spawn5(
2524
2575
  CLAUDE_BINARY,
2525
2576
  [
@@ -2543,8 +2594,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
2543
2594
  // src/cardAttachments.ts
2544
2595
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
2545
2596
  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");
2597
+ import { extname, join as join12 } from "node:path";
2598
+ var ATTACHMENTS_ROOT = join12(tmpdir6(), "agent-board", "card-attachments");
2548
2599
  function sanitizeFileName(fileName) {
2549
2600
  return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
2550
2601
  }
@@ -2556,13 +2607,13 @@ async function downloadCardAttachments(supabase, cardId) {
2556
2607
  }
2557
2608
  const rows = data ?? [];
2558
2609
  if (rows.length === 0) return [];
2559
- const targetDir = join11(ATTACHMENTS_ROOT, cardId);
2610
+ const targetDir = join12(ATTACHMENTS_ROOT, cardId);
2560
2611
  await mkdir2(targetDir, { recursive: true });
2561
2612
  const paths = [];
2562
2613
  for (const row of rows) {
2563
2614
  const baseName = sanitizeFileName(row.file_name);
2564
2615
  const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
2565
- const localPath = join11(targetDir, `${row.id}-${fileName}`);
2616
+ const localPath = join12(targetDir, `${row.id}-${fileName}`);
2566
2617
  try {
2567
2618
  const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
2568
2619
  if (downloadError) throw downloadError;
@@ -2636,6 +2687,7 @@ function cancelUsageResume(sessionId) {
2636
2687
 
2637
2688
  // src/orchestrator.ts
2638
2689
  var TERMINAL_TICKET_TTL_MS = 60 * 60 * 1e3;
2690
+ var RESUME_CONVERSATION_MISSING_PATTERN = /no conversation found with session id/i;
2639
2691
  var inFlightCardIds = /* @__PURE__ */ new Set();
2640
2692
  var activeSessionByCard = /* @__PURE__ */ new Map();
2641
2693
  var intentionallyStoppedSessionIds = /* @__PURE__ */ new Set();
@@ -2709,11 +2761,17 @@ async function dispatchRunnerQueue(supabase, config) {
2709
2761
  try {
2710
2762
  const queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
2711
2763
  if (queued.length === 0) return;
2764
+ const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
2712
2765
  const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
2713
2766
  const runningProjectIds = /* @__PURE__ */ new Set();
2714
2767
  for (const project of config.registeredProjects) {
2715
- const runningCount = await countRunningSessions(supabase, project.id, config.ownerUserId);
2716
- if (runningCount > 0) runningProjectIds.add(project.id);
2768
+ const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
2769
+ if (!running) continue;
2770
+ if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
2771
+ await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
2772
+ continue;
2773
+ }
2774
+ runningProjectIds.add(project.id);
2717
2775
  }
2718
2776
  let freeProjectSlots = maxConcurrentProjects - runningProjectIds.size;
2719
2777
  if (freeProjectSlots <= 0) return;
@@ -2767,9 +2825,13 @@ async function spawnSessionPty(supabase, config, entry) {
2767
2825
  const ticket = randomUUID6();
2768
2826
  let pty;
2769
2827
  let newClaudeSessionId = null;
2828
+ let resumeConversationMissing = false;
2770
2829
  try {
2771
2830
  if (claudeSessionId) {
2772
2831
  pty = resumeAgentPty(registered.repoPath, claudeSessionId, sessionId, ticket);
2832
+ pty.onData((chunk) => {
2833
+ if (RESUME_CONVERSATION_MISSING_PATTERN.test(normalizeTerminalOutput(chunk))) resumeConversationMissing = true;
2834
+ });
2773
2835
  } else {
2774
2836
  const attachmentPaths = await downloadCardAttachments(supabase, cardId);
2775
2837
  const instructions = buildInstructionsWithMcpContext(
@@ -2817,11 +2879,24 @@ async function spawnSessionPty(supabase, config, entry) {
2817
2879
  );
2818
2880
  pty.onExit(({ exitCode }) => {
2819
2881
  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
2882
  activityWatcher.stop();
2822
2883
  unregisterAgentActivityWatcher(sessionId);
2823
2884
  cancelUsageResume(sessionId);
2824
2885
  activeSessionByCard.delete(cardId);
2886
+ if (resumeConversationMissing) {
2887
+ logger.card.warn(
2888
+ cardId,
2889
+ `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.`
2890
+ );
2891
+ void updateSession(
2892
+ supabase,
2893
+ sessionId,
2894
+ { status: "failed", endedAt: (/* @__PURE__ */ new Date()).toISOString(), claudeSessionId: null },
2895
+ { projectId: card.project_id, cardId, userId: config.ownerUserId }
2896
+ );
2897
+ return;
2898
+ }
2899
+ 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
2900
  if (!wasIntentional && exitCode !== 0) {
2826
2901
  publishAgentState(supabase, card.project_id, cardId, sessionId, "error");
2827
2902
  }
@@ -2842,7 +2917,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
2842
2917
  } else {
2843
2918
  cancelUsageResume(sessionId);
2844
2919
  }
2845
- if (state === "done") void pauseFinishedSession(supabase, config, projectId, cardId, sessionId);
2920
+ if (state === "done") void dispatchRunnerQueue(supabase, config);
2846
2921
  });
2847
2922
  pty.onData((chunk) => watcher.push(chunk));
2848
2923
  registerAgentActivityWatcher(sessionId, watcher);
@@ -2852,8 +2927,7 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
2852
2927
  const paused = await pauseRunningSession(supabase, sessionId, { projectId, cardId, userId: config.ownerUserId });
2853
2928
  if (!paused) return;
2854
2929
  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);
2930
+ logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done", another session needs this project's WIP slot).`);
2857
2931
  }
2858
2932
  async function requeuePausedCard(supabase, config, card) {
2859
2933
  const active = await getActiveSessionForCard(supabase, card.id);
@@ -2931,6 +3005,24 @@ async function stopCardSession(supabase, config, card) {
2931
3005
  logger.card.info(card.id, `Session ${sessionId} stopped.`);
2932
3006
  await dispatchRunnerQueue(supabase, config);
2933
3007
  }
3008
+ async function restartCardSession(supabase, config, card) {
3009
+ const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
3010
+ if (sessionId) {
3011
+ intentionallyStoppedSessionIds.add(sessionId);
3012
+ stopPtySession(sessionId);
3013
+ cancelUsageResume(sessionId);
3014
+ activeSessionByCard.delete(card.id);
3015
+ await updateSession(
3016
+ supabase,
3017
+ sessionId,
3018
+ { status: "failed", claudeSessionId: null, endedAt: (/* @__PURE__ */ new Date()).toISOString() },
3019
+ { projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
3020
+ );
3021
+ logger.card.info(card.id, `Session ${sessionId} abandoned by "Reiniciar sess\xE3o"; starting a fresh one.`);
3022
+ }
3023
+ await updateCard(supabase, card.id, { restartRequestedAt: null });
3024
+ await enqueueCard(supabase, config, card);
3025
+ }
2934
3026
 
2935
3027
  // src/deploy.ts
2936
3028
  async function handleDeployRequested(supabase, config, project) {
@@ -2948,7 +3040,7 @@ async function handleDeployRequested(supabase, config, project) {
2948
3040
  }
2949
3041
  const children = await getProjectRepos(supabase, project.id);
2950
3042
  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 }];
3043
+ const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join13(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
2952
3044
  const failures = [];
2953
3045
  for (const target of targets) {
2954
3046
  const result = await pushBranchToMain(target.path);
@@ -3082,7 +3174,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
3082
3174
  }
3083
3175
 
3084
3176
  // src/review.ts
3085
- import { join as join13 } from "node:path";
3177
+ import { join as join14 } from "node:path";
3086
3178
  var inFlightCardIds2 = /* @__PURE__ */ new Set();
3087
3179
  async function handleCardEnteredRevisao(supabase, config, card) {
3088
3180
  if (inFlightCardIds2.has(card.id)) return;
@@ -3091,7 +3183,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
3091
3183
  const project = await getProject(supabase, card.project_id);
3092
3184
  const children = await getProjectRepos(supabase, project.id);
3093
3185
  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 }] : [];
3186
+ 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
3187
  if (targets.length === 0) {
3096
3188
  logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
3097
3189
  return;
@@ -3177,7 +3269,7 @@ function attachTerminalServer() {
3177
3269
  const buffer = data;
3178
3270
  const frameType = buffer[0];
3179
3271
  if (frameType === FRAME_TYPE_IMAGE) {
3180
- if (handle.readOnly) return;
3272
+ if (handle.readOnly || handle.exited) return;
3181
3273
  const extCode = buffer[1];
3182
3274
  const bytes = buffer.subarray(2);
3183
3275
  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 +3287,7 @@ function attachTerminalServer() {
3195
3287
  logger.runner.warn("TERMINAL", `Terminal WS: ignoring binary frame with unknown type ${frameType} for session ${sessionId}.`);
3196
3288
  return;
3197
3289
  }
3198
- if (handle.readOnly) return;
3290
+ if (handle.readOnly || handle.exited) return;
3199
3291
  handle.pty.write(data.toString());
3200
3292
  });
3201
3293
  socket.on("close", () => {
@@ -3317,6 +3409,7 @@ async function startRunner() {
3317
3409
  const registeredPaths = new Set(config.registeredProjects.map((project) => project.id));
3318
3410
  const lastKnownColumn = /* @__PURE__ */ new Map();
3319
3411
  const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
3412
+ const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
3320
3413
  await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
3321
3414
  logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
3322
3415
  for (const project of config.registeredProjects) {
@@ -3412,6 +3505,11 @@ async function startRunner() {
3412
3505
  if (card.requeue_requested_at && card.requeue_requested_at !== previousRequeueRequestedAt) {
3413
3506
  void requeuePausedCard(supabase, config, card);
3414
3507
  }
3508
+ const previousRestartRequestedAt = lastKnownRestartRequestedAt.get(card.id);
3509
+ lastKnownRestartRequestedAt.set(card.id, card.restart_requested_at);
3510
+ if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
3511
+ void restartCardSession(supabase, config, card);
3512
+ }
3415
3513
  if (previousColumn !== void 0 && previousColumn !== card.column_name) {
3416
3514
  void clearColumnActionStatus(supabase, card);
3417
3515
  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.3",
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": {