@sideboard-ai/core 0.1.100 → 0.1.107

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 (54) hide show
  1. package/dist/agents/cursor-runner.js +1 -1
  2. package/dist/{agents-OXZDMPSE.js → agents-4XQANGFO.js} +8 -8
  3. package/dist/{agents-CLP5BG4O.js → agents-T4RHL5UX.js} +8 -8
  4. package/dist/{app-settings-ZXPAB3QN.js → app-settings-2WH6DVOW.js} +4 -2
  5. package/dist/{app-settings-BRF7VKTQ.js → app-settings-6AXBWM3K.js} +4 -2
  6. package/dist/{caffeinate-hold-U3QJBV67.js → caffeinate-hold-NBRUWYXO.js} +2 -2
  7. package/dist/{caffeinate-hold-I7SG6UQR.js → caffeinate-hold-X3JNPR42.js} +2 -2
  8. package/dist/{chunk-QSLE4VEM.js → chunk-2GO3YKBA.js} +4 -4
  9. package/dist/chunk-3SIFRHJ3.js +7309 -0
  10. package/dist/{chunk-IFHKPZHA.js → chunk-5XUKY2JY.js} +14 -13
  11. package/dist/{chunk-OXC3MEFI.js → chunk-6HRR4T4P.js} +8 -8
  12. package/dist/{chunk-R7BQBSDT.js → chunk-6JJTMI7G.js} +55 -28
  13. package/dist/{chunk-DKHGWYWR.js → chunk-7YKXHBIW.js} +1 -1
  14. package/dist/{chunk-FT2SQOL4.js → chunk-7ZTQHC2Q.js} +11 -1
  15. package/dist/{chunk-JL4SJ6V7.js → chunk-BD2ICC4D.js} +4 -0
  16. package/dist/{chunk-NZPGMNFC.js → chunk-DCOZABNT.js} +1 -1
  17. package/dist/{chunk-6LFV4VFI.js → chunk-EX4NZN3O.js} +11 -1
  18. package/dist/{chunk-HYKZEP5A.js → chunk-GYFNFU62.js} +14 -13
  19. package/dist/{chunk-3WA37BRI.js → chunk-KGSVLZV6.js} +11 -4
  20. package/dist/{chunk-7KDBDMNC.js → chunk-KLBOWJ74.js} +1 -1
  21. package/dist/{chunk-MRBKGFTL.js → chunk-LRO7YVLJ.js} +3 -3
  22. package/dist/{chunk-JOF3XIEM.js → chunk-P3BQ5DOL.js} +11 -4
  23. package/dist/{chunk-CIRXAYWS.js → chunk-PZVA2VDF.js} +55 -28
  24. package/dist/{chunk-B4R2VB2A.js → chunk-QZQEXME2.js} +4 -0
  25. package/dist/{chunk-GR4JKWR4.js → chunk-SMWVSSE5.js} +9 -9
  26. package/dist/{chunk-WQTTUC4N.js → chunk-THXSFETX.js} +5 -5
  27. package/dist/{chunk-MHJV4WS3.js → chunk-VTTTV2LM.js} +3 -3
  28. package/dist/chunk-XEFHG6VG.js +6946 -0
  29. package/dist/{chunk-EQPQTLW6.js → chunk-XOYH3OMI.js} +6 -6
  30. package/dist/{connected-teams-SWHJWK2A.js → connected-teams-J4I5AKMR.js} +2 -2
  31. package/dist/{connected-teams-TLVKZPJP.js → connected-teams-M5XNXRI5.js} +2 -2
  32. package/dist/{coordinator-prompt-HHRKQWCX.js → coordinator-prompt-4TUQUEHY.js} +5 -5
  33. package/dist/{coordinator-prompt-OOBSQCWQ.js → coordinator-prompt-YQWRXCPX.js} +5 -5
  34. package/dist/{cursor-recover-S5ORPW34.js → cursor-recover-JBTIDARH.js} +1 -1
  35. package/dist/{cursor-recover-G34BYKL2.js → cursor-recover-ZUJG6XRQ.js} +1 -1
  36. package/dist/{global-workspace-YZWYHLQY.js → global-workspace-H37LFFXI.js} +6 -6
  37. package/dist/{global-workspace-KYEBFWKB.js → global-workspace-V6TMECDU.js} +6 -6
  38. package/dist/index.cjs +12180 -10835
  39. package/dist/index.d.cts +134 -16
  40. package/dist/index.d.ts +134 -16
  41. package/dist/index.js +899 -6769
  42. package/dist/mcp/run-stdio.cjs +7658 -6525
  43. package/dist/mcp/run-stdio.js +373 -6479
  44. package/dist/orchestrator-2P7DX44Q.js +31 -0
  45. package/dist/orchestrator-DEHSKERK.js +32 -0
  46. package/dist/{paths-TZX4L5X7.js → paths-QCI3MTO5.js} +3 -1
  47. package/dist/{paths-JQM2XRAD.js → paths-UMEDHOS5.js} +3 -1
  48. package/dist/{thread-store-XAKWCV2E.js → thread-store-4OUEQ2DB.js} +2 -2
  49. package/dist/{thread-store-V7NOOXKE.js → thread-store-DQJGDKIJ.js} +2 -2
  50. package/dist/{workspaces-ENVUDK6C.js → workspaces-JU3P6FHB.js} +7 -7
  51. package/dist/{workspaces-NNPD7QVV.js → workspaces-UPEVANU4.js} +7 -7
  52. package/dist/{worktree-EO5QAGJU.js → worktree-6MFKE465.js} +4 -4
  53. package/dist/{worktree-DA4BOV7G.js → worktree-NR5YJG2E.js} +4 -4
  54. package/package.json +2 -1
@@ -2,14 +2,14 @@
2
2
 
3
3
  import {
4
4
  resolveGithubRepoSlug
5
- } from "./chunk-R7BQBSDT.js";
5
+ } from "./chunk-6JJTMI7G.js";
6
6
  import {
7
7
  resolveThreadDefaults
8
- } from "./chunk-6LFV4VFI.js";
8
+ } from "./chunk-EX4NZN3O.js";
9
9
  import {
10
10
  globalAgentCwd,
11
11
  sideboardReposDir
12
- } from "./chunk-JL4SJ6V7.js";
12
+ } from "./chunk-BD2ICC4D.js";
13
13
 
14
14
  // src/orchestrator/coordinator-prompt.ts
15
15
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -36,7 +36,7 @@ function coordinatorGreenfieldPlaybook(reposDir) {
36
36
  "- Examples:",
37
37
  ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
38
38
  ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
39
- "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
39
+ "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
40
40
  "- Always target the child worktree's **origin** (`github:` slug from list_workspaces / `git remote get-url origin` in that worktree). Never open PRs against `upstream`.",
41
41
  "- Do coding work in the child worktree thread, not by editing files in this home cwd."
42
42
  ].join("\n");
@@ -54,7 +54,8 @@ var COORDINATOR_TOOL_PLAYBOOK = [
54
54
  "- list_models \u2014 only when you need a specific model (rare); otherwise omit model so Account defaults apply",
55
55
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
56
56
  "- ask_user \u2014 composer multiple-choice only when blocked on a concrete choice (approach fork, which API). Never for hellos, check-ins, or invented \u201Cwhat should we do?\u201D menus \u2014 reply in chat. Explain options first, description on every option, then wait.",
57
- "- set_caffeinate \u2014 keep this Mac awake across turns (macOS caffeinate). Turn on for Slack / away-from-keyboard work. Turn OFF when the user says they are done, wrapping up, going to sleep, or no longer need the machine awake. Closing this chat also releases it.",
57
+ "- set_caffeinate \u2014 keep this Mac awake across turns (macOS caffeinate). Turn on for Slack / away-from-keyboard work, overnight schedules, or when the user will be away. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the machine awake. Closing this chat also releases it.",
58
+ "- list_schedules / create_schedule / update_schedule / delete_schedule / run_schedule \u2014 local jobs that send a prompt to an orchestration chat (threadId or self) or start a new Global chat (omit threadId). One-shot `at`, interval `every` (15m/1h/6h/1d), or 5-field `cron`. Recurring jobs without threadId open a new chat each run. Jobs fire only while Sideboard.app is running; sleep skips until wake. Overnight/unattended runs: ask the user to enable Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or call set_caffeinate.",
58
59
  "Workspaces:",
59
60
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
60
61
  "Worktree threads (chats):",
@@ -62,7 +63,7 @@ var COORDINATOR_TOOL_PLAYBOOK = [
62
63
  "- fork_worktree \u2014 fork a worktree chat into a NEW git worktree + chat (transcript attached); optional agent; leave model unset (Auto) unless you have a reason. Not for orchestration chats.",
63
64
  "- fork_chat \u2014 fork a worktree chat (same worktree tab) OR a Global orchestration chat (new orchestration tab); optional agent; leave model unset (Auto) unless you have a reason. Remote coordinators: use this to continue another orchestration chat on a different agent after session limits.",
64
65
  "- send_to_thread \u2014 queue a prompt (start/continue a chat turn); pass force_stop: true to interrupt mid-turn / clear stale queued prompts before replacing with a new request",
65
- "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
66
+ "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply. wait_for_turn returns within ~45s even if the child is still working (MCP clients kill longer tool calls). If stillRunning is true, progress is a live snapshot of tools/thinking \u2014 call wait_for_turn again. Do not send \u201Care you stuck?\u201D or assume a hang while lastActivityAt is recent. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
66
67
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
67
68
  "- archive_thread / restore_thread \u2014 archive (tears down worktree when last tab) or restore",
68
69
  "Setup / run:",
@@ -70,8 +71,8 @@ var COORDINATOR_TOOL_PLAYBOOK = [
70
71
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
71
72
  "Inspect / review / PRs:",
72
73
  "- get_diff \u2014 compact diff summary",
73
- '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn / get_turn_result on the returned id',
74
- "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn. Prefer this over paraphrasing.",
74
+ '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id',
75
+ "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn (loop while stillRunning). Prefer this over paraphrasing.",
75
76
  '- Merge (`ask_git` action=merge / send_to_thread "Merge PR.") only when the user explicitly asked to merge that PR. Do not merge because the work looks done, CI is green, or a typical flow includes it.',
76
77
  '- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
77
78
  "Process guides:",
@@ -107,10 +108,10 @@ function coordinatorTurnReminder(opts) {
107
108
  goal ? `- Goal / title: ${goal}` : null,
108
109
  accountDefaultsPlaybookLine(),
109
110
  `- For "what's going on": call list_threads (and list_workspaces if needed). Summarize fleet status \u2014 do not ls/git-status this synthetic home.`,
110
- "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. If status is error, lastError/text is the failure \u2014 adapt (switch agent, tell the user). Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
111
- "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 ask_git create-draft.",
111
+ "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. If stillRunning, the child is working (progress is tools/thinking) \u2014 wait_for_turn again; do not ping it. If status is error, lastError/text is the failure \u2014 adapt (switch agent, tell the user). Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
112
+ "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
112
113
  "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
113
- "- If they will wait on Slack or leave the Mac, call set_caffeinate enabled=true. When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false. Closing this chat also turns it off."
114
+ "- If they will wait on Slack, leave the Mac, or rely on overnight schedules, call set_caffeinate enabled=true (or they can enable Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled). When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false. Closing this chat also turns it off."
114
115
  ].filter(Boolean).join("\n");
115
116
  }
116
117
  function ensureGlobalCoordinatorCwd(opts) {
@@ -161,8 +162,8 @@ function ensureGlobalCoordinatorCwd(opts) {
161
162
  orchId ? `Pass parentThreadId="${orchId}" (or omit it). Never invent another parentThreadId.` : "Pass `parentThreadId` for children (this chat's id from the turn reminder).",
162
163
  "Omit `agent` / `model` on `create_thread` unless you have a reason to override Account defaults.",
163
164
  "Never pass `agent=codex` when you yourself are Codex \u2014 nested Codex deadlocks on shared ~/.codex locks. Omit agent (Account default) or use cursor/claude.",
164
- "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
165
- "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn \u2192 ask_git create-draft.",
165
+ "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
166
+ "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
166
167
  "Always ask worktree agents to commit, push, and open draft PRs (`ask_git` / `send_to_thread`). Tell them to merge only when the user explicitly asked. The worktree agent runs git/gh; never merge from this orchestration cwd."
167
168
  ].join("\n");
168
169
  try {
@@ -1,13 +1,13 @@
1
- import {
2
- isOrchestratorThread
3
- } from "./chunk-GR4JKWR4.js";
4
1
  import {
5
2
  applyConnectedTeamToCli,
6
3
  brightsyMcpServerName,
7
4
  ensureCliTeamTracked,
8
5
  ensureConnectedBrightsyTeamTokens,
9
6
  loadBrightsyConfig
10
- } from "./chunk-QSLE4VEM.js";
7
+ } from "./chunk-2GO3YKBA.js";
8
+ import {
9
+ isOrchestratorThread
10
+ } from "./chunk-SMWVSSE5.js";
11
11
  import {
12
12
  applyNodeLaunch,
13
13
  cursorRipgrepEnv,
@@ -25,20 +25,20 @@ import {
25
25
  mergeAgentGitAuthEnv,
26
26
  resolveAgentGitAuthEnv,
27
27
  resolveCodexGitWritableRoots
28
- } from "./chunk-CIRXAYWS.js";
28
+ } from "./chunk-PZVA2VDF.js";
29
29
  import {
30
30
  claudeChromeEnabled,
31
31
  loadAppSettings,
32
32
  resolveAgentExecutable,
33
33
  resolveClaudeExecutable
34
- } from "./chunk-FT2SQOL4.js";
34
+ } from "./chunk-7ZTQHC2Q.js";
35
35
  import {
36
36
  isElectronLikeCommand,
37
37
  unwrapStrippedElectronLaunch
38
38
  } from "./chunk-TLPJHLLM.js";
39
39
  import {
40
40
  appDataDir
41
- } from "./chunk-B4R2VB2A.js";
41
+ } from "./chunk-QZQEXME2.js";
42
42
  import {
43
43
  enrichPathWithNpmGlobalBin,
44
44
  isConductorBundledCli,
@@ -1019,7 +1019,7 @@ var claudeAdapter = {
1019
1019
  );
1020
1020
  }
1021
1021
  const mode = permissionMode(thread);
1022
- const { isOrchestratorThread: isOrchestratorThread2 } = await import("./global-workspace-KYEBFWKB.js");
1022
+ const { isOrchestratorThread: isOrchestratorThread2 } = await import("./global-workspace-V6TMECDU.js");
1023
1023
  const isOrchestrator = isOrchestratorThread2(thread);
1024
1024
  const injectedServers = await buildInjectedMcpServers({
1025
1025
  includeSideboard: true,
@@ -5,14 +5,14 @@ import {
5
5
  } from "./chunk-B3SJXYIJ.js";
6
6
  import {
7
7
  listThreads
8
- } from "./chunk-JOF3XIEM.js";
8
+ } from "./chunk-P3BQ5DOL.js";
9
9
  import {
10
10
  getGithubGitAuthMode,
11
11
  getGithubPat
12
- } from "./chunk-6LFV4VFI.js";
12
+ } from "./chunk-EX4NZN3O.js";
13
13
  import {
14
14
  worktreesRoot
15
- } from "./chunk-JL4SJ6V7.js";
15
+ } from "./chunk-BD2ICC4D.js";
16
16
  import {
17
17
  gh,
18
18
  git,
@@ -787,7 +787,7 @@ function allocateTeamName(taken, random = Math.random) {
787
787
 
788
788
  // src/git/worktree-labels.ts
789
789
  function normalizeWorktreePath(worktreePath) {
790
- const trimmed = worktreePath.replace(/\/+$/, "") || "/";
790
+ const trimmed = (worktreePath ?? "").replace(/\/+$/, "") || "/";
791
791
  const absolute = trimmed.startsWith("/");
792
792
  const parts = trimmed.split("/").filter((p) => p && p !== ".");
793
793
  const out = [];
@@ -1154,6 +1154,30 @@ function formatGitAuthModeDirective(mode) {
1154
1154
  }
1155
1155
  }
1156
1156
 
1157
+ // src/util/enqueue-by-key.ts
1158
+ var tails = /* @__PURE__ */ new Map();
1159
+ function enqueueByKey(key, fn) {
1160
+ const prev = tails.get(key) ?? Promise.resolve();
1161
+ const next = prev.then(fn, fn);
1162
+ tails.set(
1163
+ key,
1164
+ next.then(
1165
+ () => void 0,
1166
+ () => void 0
1167
+ )
1168
+ );
1169
+ return next;
1170
+ }
1171
+
1172
+ // src/git/repo-git-lock.ts
1173
+ function repoLockKey(repoPath) {
1174
+ const trimmed = repoPath.replace(/\/+$/, "");
1175
+ return `git-worktree:${trimmed || "/"}`;
1176
+ }
1177
+ function withRepoGitLock(repoPath, fn) {
1178
+ return enqueueByKey(repoLockKey(repoPath), fn);
1179
+ }
1180
+
1157
1181
  // src/git/pr-gates.ts
1158
1182
  function prIsInMergeQueue(gate) {
1159
1183
  if (gate.isInMergeQueue) return true;
@@ -2223,12 +2247,13 @@ async function createThreadWorktree(opts) {
2223
2247
  `Invalid git reference: ${opts.sourceRef} (and fetch did not resolve it)`
2224
2248
  );
2225
2249
  }
2226
- const add = await git(
2227
- ["worktree", "add", "-b", branchName, worktreePath, startPoint],
2228
- opts.repoPath,
2229
- { reject: false, timeoutMs: 6e4 }
2230
- );
2231
- if (add.exitCode !== 0) {
2250
+ const added = await withRepoGitLock(opts.repoPath, async () => {
2251
+ const add = await git(
2252
+ ["worktree", "add", "-b", branchName, worktreePath, startPoint],
2253
+ opts.repoPath,
2254
+ { reject: false, timeoutMs: 6e4 }
2255
+ );
2256
+ if (add.exitCode === 0) return branchName;
2232
2257
  const err = `${add.stderr}
2233
2258
  ${add.stdout}`;
2234
2259
  if (/already used by worktree|already exists|checked out/i.test(err)) {
@@ -2239,17 +2264,14 @@ ${add.stdout}`;
2239
2264
  opts.repoPath,
2240
2265
  { reject: false, timeoutMs: 6e4 }
2241
2266
  );
2242
- if (retry.exitCode === 0) {
2243
- branchName = alt;
2244
- await ensureGhPreferOrigin(worktreePath);
2245
- return { branchName, worktreePath };
2246
- }
2267
+ if (retry.exitCode === 0) return alt;
2247
2268
  }
2248
2269
  }
2249
2270
  throw new Error(
2250
2271
  `Failed to create worktree: ${add.stderr.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
2251
2272
  );
2252
- }
2273
+ });
2274
+ branchName = added;
2253
2275
  await ensureGhPreferOrigin(worktreePath);
2254
2276
  return { branchName, worktreePath };
2255
2277
  }
@@ -2273,12 +2295,13 @@ async function createExistingBranchWorktree(opts) {
2273
2295
  );
2274
2296
  }
2275
2297
  const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
2276
- const add = await git(
2277
- ["worktree", "add", worktreePath, startPoint],
2278
- opts.repoPath,
2279
- { reject: false }
2280
- );
2281
- if (add.exitCode !== 0) {
2298
+ await withRepoGitLock(opts.repoPath, async () => {
2299
+ const add = await git(
2300
+ ["worktree", "add", worktreePath, startPoint],
2301
+ opts.repoPath,
2302
+ { reject: false }
2303
+ );
2304
+ if (add.exitCode === 0) return;
2282
2305
  const retry = await git(
2283
2306
  ["worktree", "add", worktreePath, branchName],
2284
2307
  opts.repoPath,
@@ -2289,7 +2312,7 @@ async function createExistingBranchWorktree(opts) {
2289
2312
  `Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
2290
2313
  );
2291
2314
  }
2292
- }
2315
+ });
2293
2316
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
2294
2317
  reject: false
2295
2318
  });
@@ -2300,12 +2323,14 @@ async function createExistingBranchWorktree(opts) {
2300
2323
  return { branchName, worktreePath };
2301
2324
  }
2302
2325
  async function removeWorktree(repoPath, worktreePath, opts) {
2303
- await git(["worktree", "remove", "--force", worktreePath], repoPath, {
2304
- reject: false
2326
+ await withRepoGitLock(repoPath, async () => {
2327
+ await git(["worktree", "remove", "--force", worktreePath], repoPath, {
2328
+ reject: false
2329
+ });
2330
+ if (opts?.deleteBranch) {
2331
+ await git(["branch", "-D", opts.deleteBranch], repoPath, { reject: false });
2332
+ }
2305
2333
  });
2306
- if (opts?.deleteBranch) {
2307
- await git(["branch", "-D", opts.deleteBranch], repoPath, { reject: false });
2308
- }
2309
2334
  }
2310
2335
  async function listWorktrees(repoPath) {
2311
2336
  const { stdout } = await git(
@@ -2606,6 +2631,8 @@ export {
2606
2631
  resolveCodexGitWritableRoots,
2607
2632
  codexUnattendedGitConfigArgs,
2608
2633
  formatGitAuthModeDirective,
2634
+ enqueueByKey,
2635
+ withRepoGitLock,
2609
2636
  detectGhStack,
2610
2637
  getPrStack,
2611
2638
  initPrStack,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import {
4
4
  appDataDir
5
- } from "./chunk-JL4SJ6V7.js";
5
+ } from "./chunk-BD2ICC4D.js";
6
6
  import {
7
7
  run
8
8
  } from "./chunk-3KETJKYA.js";
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-77WWLBCI.js";
11
11
  import {
12
12
  appDataDir
13
- } from "./chunk-B4R2VB2A.js";
13
+ } from "./chunk-QZQEXME2.js";
14
14
 
15
15
  // src/store/app-settings.ts
16
16
  import { randomUUID } from "crypto";
@@ -424,6 +424,9 @@ function normalizeAdvanced(raw) {
424
424
  } else if (typeof source.caffeinateWhileCloudConnect === "boolean") {
425
425
  out.caffeinateWhileSlackListen = source.caffeinateWhileCloudConnect;
426
426
  }
427
+ if (typeof source.caffeinateWhileSchedules === "boolean") {
428
+ out.caffeinateWhileSchedules = source.caffeinateWhileSchedules;
429
+ }
427
430
  if (typeof source.deleteBranchOnPurge === "boolean") {
428
431
  out.deleteBranchOnPurge = source.deleteBranchOnPurge;
429
432
  }
@@ -955,6 +958,9 @@ function updateAdvancedSettings(patch) {
955
958
  advanced.caffeinateWhileSlackListen = patch.caffeinateWhileCloudConnect;
956
959
  delete advanced.caffeinateWhileCloudConnect;
957
960
  }
961
+ if (typeof patch.caffeinateWhileSchedules === "boolean") {
962
+ advanced.caffeinateWhileSchedules = patch.caffeinateWhileSchedules;
963
+ }
958
964
  if (typeof patch.deleteBranchOnPurge === "boolean") {
959
965
  advanced.deleteBranchOnPurge = patch.deleteBranchOnPurge;
960
966
  }
@@ -999,6 +1005,9 @@ function caffeinateWhileRunningEnabled(settings = loadAppSettings()) {
999
1005
  function caffeinateWhileSlackListenEnabled(settings = loadAppSettings()) {
1000
1006
  return Boolean(settings.advanced.caffeinateWhileSlackListen);
1001
1007
  }
1008
+ function caffeinateWhileSchedulesEnabled(settings = loadAppSettings()) {
1009
+ return Boolean(settings.advanced.caffeinateWhileSchedules);
1010
+ }
1002
1011
  function caffeinateWhileCloudConnectEnabled(settings = loadAppSettings()) {
1003
1012
  return caffeinateWhileSlackListenEnabled(settings);
1004
1013
  }
@@ -1141,6 +1150,7 @@ export {
1141
1150
  autoRunAfterSetupEnabled,
1142
1151
  caffeinateWhileRunningEnabled,
1143
1152
  caffeinateWhileSlackListenEnabled,
1153
+ caffeinateWhileSchedulesEnabled,
1144
1154
  caffeinateWhileCloudConnectEnabled,
1145
1155
  deleteBranchOnPurgeEnabled,
1146
1156
  autoArchiveOnMergeEnabled,
@@ -359,6 +359,9 @@ function worktreesRoot(repoPath) {
359
359
  function threadFilePath(id) {
360
360
  return join4(threadsDir(), `${id}.json`);
361
361
  }
362
+ function threadLivePath(id) {
363
+ return join4(threadsDir(), `${id}.live.json`);
364
+ }
362
365
  function threadLockPath(id) {
363
366
  return join4(locksDir(), `${id}.lock`);
364
367
  }
@@ -383,6 +386,7 @@ export {
383
386
  repoSlug,
384
387
  worktreesRoot,
385
388
  threadFilePath,
389
+ threadLivePath,
386
390
  threadLockPath,
387
391
  globalAgentCwd
388
392
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-JT7R45JB.js";
4
4
  import {
5
5
  appDataDir
6
- } from "./chunk-B4R2VB2A.js";
6
+ } from "./chunk-QZQEXME2.js";
7
7
 
8
8
  // src/store/caffeinate-hold.ts
9
9
  import { spawn } from "child_process";
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-KGZBYWZ3.js";
10
10
  import {
11
11
  appDataDir
12
- } from "./chunk-JL4SJ6V7.js";
12
+ } from "./chunk-BD2ICC4D.js";
13
13
 
14
14
  // src/store/app-settings.ts
15
15
  import { randomUUID } from "crypto";
@@ -452,6 +452,9 @@ function normalizeAdvanced(raw) {
452
452
  } else if (typeof source.caffeinateWhileCloudConnect === "boolean") {
453
453
  out.caffeinateWhileSlackListen = source.caffeinateWhileCloudConnect;
454
454
  }
455
+ if (typeof source.caffeinateWhileSchedules === "boolean") {
456
+ out.caffeinateWhileSchedules = source.caffeinateWhileSchedules;
457
+ }
455
458
  if (typeof source.deleteBranchOnPurge === "boolean") {
456
459
  out.deleteBranchOnPurge = source.deleteBranchOnPurge;
457
460
  }
@@ -983,6 +986,9 @@ function updateAdvancedSettings(patch) {
983
986
  advanced.caffeinateWhileSlackListen = patch.caffeinateWhileCloudConnect;
984
987
  delete advanced.caffeinateWhileCloudConnect;
985
988
  }
989
+ if (typeof patch.caffeinateWhileSchedules === "boolean") {
990
+ advanced.caffeinateWhileSchedules = patch.caffeinateWhileSchedules;
991
+ }
986
992
  if (typeof patch.deleteBranchOnPurge === "boolean") {
987
993
  advanced.deleteBranchOnPurge = patch.deleteBranchOnPurge;
988
994
  }
@@ -1027,6 +1033,9 @@ function caffeinateWhileRunningEnabled(settings = loadAppSettings()) {
1027
1033
  function caffeinateWhileSlackListenEnabled(settings = loadAppSettings()) {
1028
1034
  return Boolean(settings.advanced.caffeinateWhileSlackListen);
1029
1035
  }
1036
+ function caffeinateWhileSchedulesEnabled(settings = loadAppSettings()) {
1037
+ return Boolean(settings.advanced.caffeinateWhileSchedules);
1038
+ }
1030
1039
  function caffeinateWhileCloudConnectEnabled(settings = loadAppSettings()) {
1031
1040
  return caffeinateWhileSlackListenEnabled(settings);
1032
1041
  }
@@ -1170,6 +1179,7 @@ export {
1170
1179
  autoRunAfterSetupEnabled,
1171
1180
  caffeinateWhileRunningEnabled,
1172
1181
  caffeinateWhileSlackListenEnabled,
1182
+ caffeinateWhileSchedulesEnabled,
1173
1183
  caffeinateWhileCloudConnectEnabled,
1174
1184
  deleteBranchOnPurgeEnabled,
1175
1185
  autoArchiveOnMergeEnabled,
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  resolveGithubRepoSlug
3
- } from "./chunk-CIRXAYWS.js";
3
+ } from "./chunk-PZVA2VDF.js";
4
4
  import {
5
5
  resolveThreadDefaults
6
- } from "./chunk-FT2SQOL4.js";
6
+ } from "./chunk-7ZTQHC2Q.js";
7
7
  import {
8
8
  globalAgentCwd,
9
9
  sideboardReposDir
10
- } from "./chunk-B4R2VB2A.js";
10
+ } from "./chunk-QZQEXME2.js";
11
11
 
12
12
  // src/orchestrator/coordinator-prompt.ts
13
13
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -34,7 +34,7 @@ function coordinatorGreenfieldPlaybook(reposDir) {
34
34
  "- Examples:",
35
35
  ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
36
36
  ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
37
- "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
37
+ "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
38
38
  "- Always target the child worktree's **origin** (`github:` slug from list_workspaces / `git remote get-url origin` in that worktree). Never open PRs against `upstream`.",
39
39
  "- Do coding work in the child worktree thread, not by editing files in this home cwd."
40
40
  ].join("\n");
@@ -52,7 +52,8 @@ var COORDINATOR_TOOL_PLAYBOOK = [
52
52
  "- list_models \u2014 only when you need a specific model (rare); otherwise omit model so Account defaults apply",
53
53
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
54
54
  "- ask_user \u2014 composer multiple-choice only when blocked on a concrete choice (approach fork, which API). Never for hellos, check-ins, or invented \u201Cwhat should we do?\u201D menus \u2014 reply in chat. Explain options first, description on every option, then wait.",
55
- "- set_caffeinate \u2014 keep this Mac awake across turns (macOS caffeinate). Turn on for Slack / away-from-keyboard work. Turn OFF when the user says they are done, wrapping up, going to sleep, or no longer need the machine awake. Closing this chat also releases it.",
55
+ "- set_caffeinate \u2014 keep this Mac awake across turns (macOS caffeinate). Turn on for Slack / away-from-keyboard work, overnight schedules, or when the user will be away. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the machine awake. Closing this chat also releases it.",
56
+ "- list_schedules / create_schedule / update_schedule / delete_schedule / run_schedule \u2014 local jobs that send a prompt to an orchestration chat (threadId or self) or start a new Global chat (omit threadId). One-shot `at`, interval `every` (15m/1h/6h/1d), or 5-field `cron`. Recurring jobs without threadId open a new chat each run. Jobs fire only while Sideboard.app is running; sleep skips until wake. Overnight/unattended runs: ask the user to enable Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or call set_caffeinate.",
56
57
  "Workspaces:",
57
58
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
58
59
  "Worktree threads (chats):",
@@ -60,7 +61,7 @@ var COORDINATOR_TOOL_PLAYBOOK = [
60
61
  "- fork_worktree \u2014 fork a worktree chat into a NEW git worktree + chat (transcript attached); optional agent; leave model unset (Auto) unless you have a reason. Not for orchestration chats.",
61
62
  "- fork_chat \u2014 fork a worktree chat (same worktree tab) OR a Global orchestration chat (new orchestration tab); optional agent; leave model unset (Auto) unless you have a reason. Remote coordinators: use this to continue another orchestration chat on a different agent after session limits.",
62
63
  "- send_to_thread \u2014 queue a prompt (start/continue a chat turn); pass force_stop: true to interrupt mid-turn / clear stale queued prompts before replacing with a new request",
63
- "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
64
+ "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply. wait_for_turn returns within ~45s even if the child is still working (MCP clients kill longer tool calls). If stillRunning is true, progress is a live snapshot of tools/thinking \u2014 call wait_for_turn again. Do not send \u201Care you stuck?\u201D or assume a hang while lastActivityAt is recent. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
64
65
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
65
66
  "- archive_thread / restore_thread \u2014 archive (tears down worktree when last tab) or restore",
66
67
  "Setup / run:",
@@ -68,8 +69,8 @@ var COORDINATOR_TOOL_PLAYBOOK = [
68
69
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
69
70
  "Inspect / review / PRs:",
70
71
  "- get_diff \u2014 compact diff summary",
71
- '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn / get_turn_result on the returned id',
72
- "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn. Prefer this over paraphrasing.",
72
+ '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id',
73
+ "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn (loop while stillRunning). Prefer this over paraphrasing.",
73
74
  '- Merge (`ask_git` action=merge / send_to_thread "Merge PR.") only when the user explicitly asked to merge that PR. Do not merge because the work looks done, CI is green, or a typical flow includes it.',
74
75
  '- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
75
76
  "Process guides:",
@@ -105,10 +106,10 @@ function coordinatorTurnReminder(opts) {
105
106
  goal ? `- Goal / title: ${goal}` : null,
106
107
  accountDefaultsPlaybookLine(),
107
108
  `- For "what's going on": call list_threads (and list_workspaces if needed). Summarize fleet status \u2014 do not ls/git-status this synthetic home.`,
108
- "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. If status is error, lastError/text is the failure \u2014 adapt (switch agent, tell the user). Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
109
- "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 ask_git create-draft.",
109
+ "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. If stillRunning, the child is working (progress is tools/thinking) \u2014 wait_for_turn again; do not ping it. If status is error, lastError/text is the failure \u2014 adapt (switch agent, tell the user). Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
110
+ "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
110
111
  "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
111
- "- If they will wait on Slack or leave the Mac, call set_caffeinate enabled=true. When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false. Closing this chat also turns it off."
112
+ "- If they will wait on Slack, leave the Mac, or rely on overnight schedules, call set_caffeinate enabled=true (or they can enable Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled). When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false. Closing this chat also turns it off."
112
113
  ].filter(Boolean).join("\n");
113
114
  }
114
115
  function ensureGlobalCoordinatorCwd(opts) {
@@ -159,8 +160,8 @@ function ensureGlobalCoordinatorCwd(opts) {
159
160
  orchId ? `Pass parentThreadId="${orchId}" (or omit it). Never invent another parentThreadId.` : "Pass `parentThreadId` for children (this chat's id from the turn reminder).",
160
161
  "Omit `agent` / `model` on `create_thread` unless you have a reason to override Account defaults.",
161
162
  "Never pass `agent=codex` when you yourself are Codex \u2014 nested Codex deadlocks on shared ~/.codex locks. Omit agent (Account default) or use cursor/claude.",
162
- "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
163
- "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn \u2192 ask_git create-draft.",
163
+ "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
164
+ "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
164
165
  "Always ask worktree agents to commit, push, and open draft PRs (`ask_git` / `send_to_thread`). Tell them to merge only when the user explicitly asked. The worktree agent runs git/gh; never merge from this orchestration cwd."
165
166
  ].join("\n");
166
167
  try {
@@ -5,7 +5,7 @@ import {
5
5
  threadFilePath,
6
6
  threadLockPath,
7
7
  threadsDir
8
- } from "./chunk-B4R2VB2A.js";
8
+ } from "./chunk-QZQEXME2.js";
9
9
 
10
10
  // src/store/thread-store.ts
11
11
  import { randomUUID } from "crypto";
@@ -117,8 +117,11 @@ function writeThread(thread) {
117
117
  function idPath(id) {
118
118
  return id;
119
119
  }
120
+ function isThreadRecordFile(name) {
121
+ return name.endsWith(".json") && !name.endsWith(".live.json");
122
+ }
120
123
  function listThreads(opts) {
121
- const files = readdirSync(threadsDir()).filter((f) => f.endsWith(".json"));
124
+ const files = readdirSync(threadsDir()).filter(isThreadRecordFile);
122
125
  const threads = files.map((f) => {
123
126
  try {
124
127
  return normalizeThread(
@@ -127,7 +130,9 @@ function listThreads(opts) {
127
130
  } catch {
128
131
  return null;
129
132
  }
130
- }).filter((t) => t !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
133
+ }).filter(
134
+ (t) => t !== null && typeof t.id === "string" && t.id.length > 0
135
+ ).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
131
136
  if (opts?.includeArchived) return threads;
132
137
  return threads.filter((t) => t.status !== "archived");
133
138
  }
@@ -159,7 +164,9 @@ function setStatus(id, status, lastError) {
159
164
  }
160
165
  function findThreadByRef(ref) {
161
166
  const all = listThreads({ includeArchived: true });
162
- return all.find((t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref) ?? null;
167
+ return all.find(
168
+ (t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref
169
+ ) ?? null;
163
170
  }
164
171
 
165
172
  export {
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-NSTQ6QKD.js";
6
6
  import {
7
7
  appDataDir
8
- } from "./chunk-JL4SJ6V7.js";
8
+ } from "./chunk-BD2ICC4D.js";
9
9
 
10
10
  // src/store/caffeinate-hold.ts
11
11
  import { spawn } from "child_process";
@@ -2,14 +2,14 @@
2
2
 
3
3
  import {
4
4
  isGlobalRepoPath
5
- } from "./chunk-WQTTUC4N.js";
5
+ } from "./chunk-THXSFETX.js";
6
6
  import {
7
7
  ensureGhPreferOrigin,
8
8
  resolveRepoRoot
9
- } from "./chunk-R7BQBSDT.js";
9
+ } from "./chunk-6JJTMI7G.js";
10
10
  import {
11
11
  appDataDir
12
- } from "./chunk-JL4SJ6V7.js";
12
+ } from "./chunk-BD2ICC4D.js";
13
13
 
14
14
  // src/store/workspaces.ts
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -7,7 +7,7 @@ import {
7
7
  threadFilePath,
8
8
  threadLockPath,
9
9
  threadsDir
10
- } from "./chunk-JL4SJ6V7.js";
10
+ } from "./chunk-BD2ICC4D.js";
11
11
 
12
12
  // src/store/thread-store.ts
13
13
  import { randomUUID } from "crypto";
@@ -119,8 +119,11 @@ function writeThread(thread) {
119
119
  function idPath(id) {
120
120
  return id;
121
121
  }
122
+ function isThreadRecordFile(name) {
123
+ return name.endsWith(".json") && !name.endsWith(".live.json");
124
+ }
122
125
  function listThreads(opts) {
123
- const files = readdirSync(threadsDir()).filter((f) => f.endsWith(".json"));
126
+ const files = readdirSync(threadsDir()).filter(isThreadRecordFile);
124
127
  const threads = files.map((f) => {
125
128
  try {
126
129
  return normalizeThread(
@@ -129,7 +132,9 @@ function listThreads(opts) {
129
132
  } catch {
130
133
  return null;
131
134
  }
132
- }).filter((t) => t !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
135
+ }).filter(
136
+ (t) => t !== null && typeof t.id === "string" && t.id.length > 0
137
+ ).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
133
138
  if (opts?.includeArchived) return threads;
134
139
  return threads.filter((t) => t.status !== "archived");
135
140
  }
@@ -161,7 +166,9 @@ function setStatus(id, status, lastError) {
161
166
  }
162
167
  function findThreadByRef(ref) {
163
168
  const all = listThreads({ includeArchived: true });
164
- return all.find((t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref) ?? null;
169
+ return all.find(
170
+ (t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref
171
+ ) ?? null;
165
172
  }
166
173
 
167
174
  export {