@sideboard-ai/core 0.1.279 → 0.1.281

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 (52) hide show
  1. package/dist/{abletime-VCVPSLSU.js → abletime-WLDBJJUI.js} +3 -3
  2. package/dist/{abletime-VRATUMDO.js → abletime-X64E3GUJ.js} +3 -3
  3. package/dist/{abletime-oauth-4K7MICAD.js → abletime-oauth-AXR676W5.js} +2 -2
  4. package/dist/{abletime-oauth-4C4G2J3R.js → abletime-oauth-UWOCEICP.js} +2 -2
  5. package/dist/{abletime-rest-PIBCNHF3.js → abletime-rest-LPUDJUEP.js} +2 -2
  6. package/dist/{abletime-rest-6N63ZVQ3.js → abletime-rest-S4XWEXV2.js} +2 -2
  7. package/dist/{agents-DYCFLDNL.js → agents-2IE4HKHA.js} +5 -5
  8. package/dist/{agents-RXP5NZBC.js → agents-XA2E64AW.js} +5 -5
  9. package/dist/{chunk-V5DMHB6Y.js → chunk-2KNO2GJQ.js} +781 -159
  10. package/dist/{chunk-TZ5JHIYK.js → chunk-2SJXQ4UX.js} +68 -21
  11. package/dist/{chunk-K7MWJLR7.js → chunk-5MGLNVNK.js} +6 -906
  12. package/dist/{chunk-RK5PIWCW.js → chunk-ADXVTKLC.js} +12 -4
  13. package/dist/{chunk-Y7JPL4WA.js → chunk-BDAUPNHL.js} +3 -4
  14. package/dist/{chunk-R7HLXD4I.js → chunk-BS3QLVPL.js} +93 -57
  15. package/dist/{chunk-CPGJ5ICZ.js → chunk-BYZ7F2BQ.js} +766 -148
  16. package/dist/{chunk-2J34G3WJ.js → chunk-DGPROCE5.js} +93 -57
  17. package/dist/{chunk-HQXBBUQX.js → chunk-DQZPF6WL.js} +12 -11
  18. package/dist/{chunk-YDXNCBA2.js → chunk-EIEAR62M.js} +12 -11
  19. package/dist/{chunk-XKZAVGTT.js → chunk-I6HSGKMZ.js} +1 -1
  20. package/dist/chunk-JPH7PYSK.js +1241 -0
  21. package/dist/{chunk-IPPY3FG5.js → chunk-K5XSTHHP.js} +6 -906
  22. package/dist/chunk-N5UGX3JL.js +1239 -0
  23. package/dist/{chunk-HNNS25SG.js → chunk-OCUIZD4Y.js} +14 -4
  24. package/dist/{chunk-IJDQKC5A.js → chunk-PY4UCMRI.js} +4 -4
  25. package/dist/{chunk-NQ7JMVSU.js → chunk-U7HAVAGJ.js} +1 -1
  26. package/dist/{chunk-PZURG3XN.js → chunk-VA55JG2I.js} +4 -4
  27. package/dist/{chunk-CPA43FBK.js → chunk-W7HIK2A6.js} +3 -4
  28. package/dist/{chunk-SKSOCIJT.js → chunk-WY2IAB7J.js} +68 -21
  29. package/dist/{chunk-QINJKNLR.js → chunk-XTPQEZZA.js} +2 -2
  30. package/dist/{conductor-J6WDOXWM.js → conductor-CNSCOC54.js} +5 -5
  31. package/dist/{conductor-OW6NAK3N.js → conductor-U776JSXQ.js} +5 -5
  32. package/dist/{coordinator-prompt-Z2D32HWK.js → coordinator-prompt-4DJIHSE6.js} +3 -3
  33. package/dist/{coordinator-prompt-ERTHREGJ.js → coordinator-prompt-SVVGU6AZ.js} +3 -3
  34. package/dist/{global-workspace-5PJXO45Z.js → global-workspace-UPUDWIS6.js} +3 -3
  35. package/dist/{global-workspace-AK5PSPQ2.js → global-workspace-XON4YZXE.js} +3 -3
  36. package/dist/index.cjs +3819 -2995
  37. package/dist/index.d.cts +103 -30
  38. package/dist/index.d.ts +103 -30
  39. package/dist/index.js +1013 -1001
  40. package/dist/mcp/run-stdio.cjs +2075 -1313
  41. package/dist/mcp/run-stdio.js +743 -785
  42. package/dist/{orchestrator-KY2KBBE2.js → orchestrator-FPGVKEID.js} +7 -7
  43. package/dist/{orchestrator-E5WK57DA.js → orchestrator-HWJHK5NL.js} +7 -7
  44. package/dist/{thread-store-B3ZM4QEZ.js → thread-store-7XBOUQ5K.js} +1 -1
  45. package/dist/{thread-store-SHCUBWB3.js → thread-store-ZQZDREVH.js} +1 -1
  46. package/dist/{workspaces-UP6DQ3WL.js → workspaces-7AJ5RZ3P.js} +7 -3
  47. package/dist/{workspaces-WU2RNLDV.js → workspaces-F76BWCSI.js} +7 -3
  48. package/dist/{worktree-L57T3KH7.js → worktree-NKLNVNXX.js} +20 -15
  49. package/dist/{worktree-Q6UJHG25.js → worktree-YTZC3UQN.js} +14 -9
  50. package/package.json +1 -1
  51. package/dist/chunk-FWNL62VA.js +0 -308
  52. package/dist/chunk-JANCPF7Z.js +0 -310
@@ -8,7 +8,7 @@ import {
8
8
  parseSessionQuotaResetAt,
9
9
  planModeTurnInstruction,
10
10
  resolveQuotaFallbackAgent
11
- } from "./chunk-HNNS25SG.js";
11
+ } from "./chunk-OCUIZD4Y.js";
12
12
  import {
13
13
  REVIEW_TAB_TITLE,
14
14
  applyWorktreePkgCacheEnv,
@@ -47,7 +47,7 @@ import {
47
47
  worktreeDevProcessKey,
48
48
  worktreeRunProcessKey,
49
49
  worktreeSetupProcessKey
50
- } from "./chunk-HQXBBUQX.js";
50
+ } from "./chunk-DQZPF6WL.js";
51
51
  import {
52
52
  applyAgentEvent,
53
53
  clipAgentEventForPaint,
@@ -62,7 +62,7 @@ import {
62
62
  partsToAssistantText,
63
63
  stripBrightsyNdjsonNoise,
64
64
  toolDescription
65
- } from "./chunk-Y7JPL4WA.js";
65
+ } from "./chunk-BDAUPNHL.js";
66
66
  import {
67
67
  titleFromPrompt
68
68
  } from "./chunk-7H655NEW.js";
@@ -86,16 +86,18 @@ import {
86
86
  isGlobalRepoPath,
87
87
  isGlobalThread,
88
88
  isOrchestratorThread,
89
+ isRemovedWorkspace,
90
+ listWorkspaces,
89
91
  orchestratorSessionPoisonedByBuiltins,
90
92
  persistPendingFileAttachments,
91
93
  removeWorkspace,
94
+ sameWorkspacePath,
92
95
  stageAbsolutePathsAsAttachments,
93
96
  stageBuffersAsAttachments,
94
97
  syncWorkspacesFromThreads
95
- } from "./chunk-SKSOCIJT.js";
98
+ } from "./chunk-WY2IAB7J.js";
96
99
  import {
97
100
  addPrStackLayer,
98
- agentNicknameFromWorktreePath,
99
101
  allocateTeamSlug,
100
102
  canonicalizeRepoPath,
101
103
  ciChecksFailed,
@@ -120,14 +122,11 @@ import {
120
122
  getPrStack,
121
123
  initPrStack,
122
124
  isDirty,
123
- isPlaceholderBranch,
124
125
  isSideboardScratchPath,
125
- isWorktreeIdentityTitle,
126
126
  listWorktrees,
127
127
  markPrReady,
128
128
  mergeAgentGitAuthEnv,
129
129
  mergePr,
130
- normalizeWorktreePath,
131
130
  pushBranch,
132
131
  removeWorktree,
133
132
  resolveAgentGitAuthEnv,
@@ -137,26 +136,32 @@ import {
137
136
  resolvePrSelector,
138
137
  resolveRepoRoot,
139
138
  switchWorktreeBranch,
140
- teamSlugFromName,
141
- ticketSlugForBranch,
142
- withRepoGitLock,
143
- worktreeDisplayLabelForGroup,
144
- worktreeIdentityTitles,
145
- worktreeNameFromPath,
146
- worktreeSlugForTicket
147
- } from "./chunk-IPPY3FG5.js";
139
+ withRepoGitLock
140
+ } from "./chunk-K5XSTHHP.js";
148
141
  import {
142
+ agentNicknameFromWorktreePath,
149
143
  appendMessage,
150
144
  createEmptyThread,
151
145
  deleteThreadRecord,
152
146
  findThreadByRef,
147
+ isPlaceholderBranch,
148
+ isWorktreeIdentityTitle,
153
149
  listThreads,
150
+ normalizeWorkspaceTags,
151
+ normalizeWorktreePath,
154
152
  readThread,
155
153
  setStatus,
154
+ teamSlugFromName,
155
+ ticketSlugForBranch,
156
156
  updateThread,
157
157
  withThreadLock,
158
+ workspaceTagsFromGroup,
159
+ worktreeDisplayLabelForGroup,
160
+ worktreeIdentityTitles,
161
+ worktreeNameFromPath,
162
+ worktreeSlugForTicket,
158
163
  writeThread
159
- } from "./chunk-FWNL62VA.js";
164
+ } from "./chunk-N5UGX3JL.js";
160
165
  import {
161
166
  httpFetch
162
167
  } from "./chunk-N7WZ6NIN.js";
@@ -210,6 +215,7 @@ import {
210
215
  orchestrationQuotaFallbackAgent,
211
216
  planHistoryAgePurge,
212
217
  planHistoryRetention,
218
+ readViewerContext,
213
219
  resolveEffectiveIssueSource,
214
220
  resolveNewThreadOptions,
215
221
  resolveOrchestratorDefaults,
@@ -217,8 +223,10 @@ import {
217
223
  saveAppSettings,
218
224
  sumUsageList,
219
225
  updateAdvancedSettings,
226
+ updateAppEnvironment,
220
227
  usageOnLimit,
221
- withTurnScopedCost
228
+ withTurnScopedCost,
229
+ writeViewerContext
222
230
  } from "./chunk-YNTXWFXT.js";
223
231
  import {
224
232
  examplePrefixedBranch,
@@ -251,11 +259,11 @@ import {
251
259
  import { EventEmitter } from "events";
252
260
 
253
261
  // src/threads/injected-notices.ts
254
- function isSlackExternalReplyText(text) {
255
- return text.startsWith("Slack reply from ") && text.includes("not a command");
262
+ function isSlackExternalReplyText(text3) {
263
+ return text3.startsWith("Slack reply from ") && text3.includes("not a command");
256
264
  }
257
- function isInjectedNoticeText(text) {
258
- return isSlackExternalReplyText(text);
265
+ function isInjectedNoticeText(text3) {
266
+ return isSlackExternalReplyText(text3);
259
267
  }
260
268
  function pendingInjectedNotices(messages, match = isInjectedNoticeText) {
261
269
  let i = messages.length - 1;
@@ -289,7 +297,7 @@ function formatInjectedNoticesForTurn(notices) {
289
297
  }
290
298
 
291
299
  // src/orchestrator/orchestrator.ts
292
- import { randomUUID as randomUUID3 } from "crypto";
300
+ import { randomUUID as randomUUID4 } from "crypto";
293
301
  import { existsSync as existsSync14 } from "fs";
294
302
 
295
303
  // src/agents/spawn.ts
@@ -336,8 +344,8 @@ function textSoFar(parts, parentId) {
336
344
  }
337
345
  return out;
338
346
  }
339
- function replaySized(text) {
340
- return text.trim().length >= REPLAY_MIN_CHARS;
347
+ function replaySized(text3) {
348
+ return text3.trim().length >= REPLAY_MIN_CHARS;
341
349
  }
342
350
  function tailAfterAccumulated(accumulated, incoming) {
343
351
  if (incoming.startsWith(accumulated)) return incoming.slice(accumulated.length);
@@ -441,9 +449,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
441
449
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
442
450
  );
443
451
  }
444
- const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-AK5PSPQ2.js");
452
+ const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-XON4YZXE.js");
445
453
  if (isGlobalThread2(thread)) {
446
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-Z2D32HWK.js");
454
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-4DJIHSE6.js");
447
455
  ensureGlobalCoordinatorCwd2(
448
456
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
449
457
  );
@@ -585,10 +593,10 @@ async function spawnAgentTurn(thread, input, onEvent) {
585
593
  outbound.flush();
586
594
  const finalized = finalizeParts(parts);
587
595
  const rawText = assistantText.trim() || partsToAssistantText(finalized);
588
- const text = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
589
- const exitCode = thread.agent === "brightsy" ? adjustBrightsyTurnExit(result.exitCode ?? null, text) : result.exitCode ?? null;
596
+ const text3 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
597
+ const exitCode = thread.agent === "brightsy" ? adjustBrightsyTurnExit(result.exitCode ?? null, text3) : result.exitCode ?? null;
590
598
  onEvent({ type: "exit", data: exitCode });
591
- return { exitCode, sessionId, assistantText: text, parts: finalized, usage };
599
+ return { exitCode, sessionId, assistantText: text3, parts: finalized, usage };
592
600
  });
593
601
  return {
594
602
  pid: child.pid,
@@ -1026,8 +1034,8 @@ function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
1026
1034
  if (outputChars <= max) return { output, truncated: false, outputChars };
1027
1035
  const cut = output.slice(outputChars - max);
1028
1036
  const nl = cut.indexOf("\n");
1029
- const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
1030
- return { output: text, truncated: true, outputChars };
1037
+ const text3 = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
1038
+ return { output: text3, truncated: true, outputChars };
1031
1039
  }
1032
1040
  var PERSIST_DEBOUNCE_MS2 = 500;
1033
1041
  function persistNow2(key) {
@@ -1397,7 +1405,7 @@ async function cloneRepoIntoSideboard(opts) {
1397
1405
  const dest = join5(sideboardReposDir(), name);
1398
1406
  if (existsSync4(dest)) {
1399
1407
  const repoPath2 = await resolveRepoRoot(dest);
1400
- const workspace2 = await ensureWorkspace(repoPath2);
1408
+ const workspace2 = await addWorkspace(repoPath2);
1401
1409
  return { repoPath: repoPath2, workspace: workspace2 };
1402
1410
  }
1403
1411
  const result = await execa3("git", ["clone", url, dest], { reject: false });
@@ -1407,7 +1415,7 @@ async function cloneRepoIntoSideboard(opts) {
1407
1415
  );
1408
1416
  }
1409
1417
  const repoPath = await resolveRepoRoot(dest);
1410
- const workspace = await ensureWorkspace(repoPath);
1418
+ const workspace = await addWorkspace(repoPath);
1411
1419
  return { repoPath, workspace };
1412
1420
  }
1413
1421
 
@@ -1469,9 +1477,9 @@ function thisProcessShouldDrainAgentQueues() {
1469
1477
  var MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS = 1500;
1470
1478
  var inputRequiredNotified = /* @__PURE__ */ new Set();
1471
1479
  function clampMessage(raw) {
1472
- const text = raw.replace(/\s+/g, " ").trim();
1473
- if (text.length <= MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS) return text;
1474
- return `${text.slice(0, MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS - 1)}\u2026`;
1480
+ const text3 = raw.replace(/\s+/g, " ").trim();
1481
+ if (text3.length <= MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS) return text3;
1482
+ return `${text3.slice(0, MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS - 1)}\u2026`;
1475
1483
  }
1476
1484
  function formatAskUserNotifyMessage(questions) {
1477
1485
  const qs = questions.map((q) => q.question?.trim() ?? "").filter(Boolean);
@@ -1563,10 +1571,10 @@ async function notifyOrchestrator(input) {
1563
1571
  // src/orchestrator/agent-block.ts
1564
1572
  var MAX_AGENT_BLOCK_REASON = 240;
1565
1573
  function clampBlockReason(raw) {
1566
- const text = raw.replace(/\s+/g, " ").trim();
1567
- if (!text) return "Waiting on a decision";
1568
- if (text.length <= MAX_AGENT_BLOCK_REASON) return text;
1569
- return `${text.slice(0, MAX_AGENT_BLOCK_REASON - 1)}\u2026`;
1574
+ const text3 = raw.replace(/\s+/g, " ").trim();
1575
+ if (!text3) return "Waiting on a decision";
1576
+ if (text3.length <= MAX_AGENT_BLOCK_REASON) return text3;
1577
+ return `${text3.slice(0, MAX_AGENT_BLOCK_REASON - 1)}\u2026`;
1570
1578
  }
1571
1579
  function makeAgentBlock(source, reason, at = (/* @__PURE__ */ new Date()).toISOString()) {
1572
1580
  return { source, reason: clampBlockReason(reason), at };
@@ -1708,6 +1716,45 @@ function notifyParentOfChildHalt(child, status, send) {
1708
1716
  return true;
1709
1717
  }
1710
1718
 
1719
+ // src/orchestrator/stop-reason.ts
1720
+ var STOP_REASON_MAX = 500;
1721
+ function resolveStopReason(opts) {
1722
+ const explicit = opts?.reason?.trim();
1723
+ if (explicit) return explicit.slice(0, STOP_REASON_MAX);
1724
+ if (opts?.continueQueue) return "Stopped to send the next prompt";
1725
+ if (opts?.clearQueue === false) return "Stopped by the user";
1726
+ return "Stopped (queue cleared)";
1727
+ }
1728
+ function reasonToKeepOnStop(existing, incoming) {
1729
+ const next = incoming?.trim();
1730
+ if (next) return next.slice(0, STOP_REASON_MAX);
1731
+ const prior = existing?.trim();
1732
+ if (prior) return prior.slice(0, STOP_REASON_MAX);
1733
+ return "Stopped";
1734
+ }
1735
+ function writeStoppedStatus(threadId, reason) {
1736
+ const latest = readThread(threadId);
1737
+ if (!latest || latest.status === "archived") return latest;
1738
+ return setStatus(threadId, "stopped", reasonToKeepOnStop(latest.lastError, reason));
1739
+ }
1740
+
1741
+ // src/orchestrator/steer-request.ts
1742
+ import { randomUUID } from "crypto";
1743
+ function isUnclaimedSteerRequest(req) {
1744
+ return Boolean(req?.requestId && !req.claimedAt);
1745
+ }
1746
+ function nextSteerRequest(existing) {
1747
+ if (existing?.requestId && !existing.claimedAt) return existing;
1748
+ return { requestId: randomUUID(), requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
1749
+ }
1750
+ function claimSteerRequest(threadId) {
1751
+ const pending = readThread(threadId)?.steerRequest;
1752
+ if (!pending?.requestId || pending.claimedAt) return;
1753
+ updateThread(threadId, {
1754
+ steerRequest: { ...pending, claimedAt: (/* @__PURE__ */ new Date()).toISOString() }
1755
+ });
1756
+ }
1757
+
1711
1758
  // src/threads/agent-purpose-title.ts
1712
1759
  function agentPurposeStamp(input) {
1713
1760
  if (input.autoContinue) return null;
@@ -2238,7 +2285,8 @@ function threadSearchText(thread, workspaceName = "") {
2238
2285
  thread.branchName,
2239
2286
  thread.prUrl ?? "",
2240
2287
  workspaceName,
2241
- thread.repoPath
2288
+ thread.repoPath,
2289
+ ...thread.tags ?? []
2242
2290
  ].join(" ");
2243
2291
  }
2244
2292
  function inWorkspace(repoPath, filterRepoPath) {
@@ -2252,8 +2300,8 @@ function visiblePage(items, shown) {
2252
2300
  hidden: Math.max(0, items.length - n)
2253
2301
  };
2254
2302
  }
2255
- function markdownPreviewSource(text, max = 480) {
2256
- const trimmed = text.trim();
2303
+ function markdownPreviewSource(text3, max = 480) {
2304
+ const trimmed = text3.trim();
2257
2305
  if (trimmed.length <= max) return trimmed;
2258
2306
  const slice = trimmed.slice(0, max);
2259
2307
  const nl = slice.lastIndexOf("\n");
@@ -2270,13 +2318,13 @@ function latestVisibleMessageText(messages, liveParts) {
2270
2318
  if (!m) continue;
2271
2319
  if (m.role === "agent") {
2272
2320
  const fromParts = lastAssistantMessageText(m.parts);
2273
- const text = (fromParts || m.text || "").trim();
2274
- if (text && !isInternalAgentStatusText(text)) return text;
2321
+ const text3 = (fromParts || m.text || "").trim();
2322
+ if (text3 && !isInternalAgentStatusText(text3)) return text3;
2275
2323
  continue;
2276
2324
  }
2277
2325
  if (m.role === "user") {
2278
- const text = m.text?.trim();
2279
- if (text) return text;
2326
+ const text3 = m.text?.trim();
2327
+ if (text3) return text3;
2280
2328
  }
2281
2329
  }
2282
2330
  return "";
@@ -2326,6 +2374,7 @@ function findBoardPr(prs, ref, repoPath = "") {
2326
2374
  function toThreadCard(group) {
2327
2375
  const thread = group[0];
2328
2376
  const withPr = group.find((t) => t.prUrl?.trim()) ?? thread;
2377
+ const tags = workspaceTagsFromGroup(group);
2329
2378
  return {
2330
2379
  kind: "thread",
2331
2380
  id: thread.id,
@@ -2338,7 +2387,8 @@ function toThreadCard(group) {
2338
2387
  prUrl: withPr.prUrl,
2339
2388
  link: `sideboard://chat/${thread.id}`,
2340
2389
  ...group.length > 1 ? { chatCount: group.length } : {},
2341
- ...threadCardBlockFields(group) ?? {}
2390
+ ...threadCardBlockFields(group) ?? {},
2391
+ ...tags.length > 0 ? { tags } : {}
2342
2392
  };
2343
2393
  }
2344
2394
  function threadMatchesKind(thread, kind) {
@@ -2414,6 +2464,51 @@ function assembleHomeBoard(input) {
2414
2464
  }
2415
2465
  var HOME_BOARD_AGENT_HINT = "Home is a Kanban of workspaces (one card per checkout; sibling chats on a card are separate agents). Do not create a second workspace for a ticket, PR, or named branch that already has a live checkout \u2014 create_workspace / start_board_card return that checkout (alreadyStarted) and its chats. Add another agent with fork_chat. Creating from the default branch still opens a new isolated workspace. Columns are the path to merge: New (no PR) \u2192 Draft (draft PR) \u2192 Review (open PR) \u2192 Merged. ownership=mine is PRs you authored (or WIP with no PR); ownership=reviewing is someone else's PR (or a review-requested checkout). Viewer login comes from gh, not a setting. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. blockedReason means an agent on that card is waiting on a person (the question, plan approval, or a reported block) \u2014 read it and do not send_to_chat a check-in. Orchestration chats stay in the sidebar. Do not invent status.";
2416
2466
 
2467
+ // src/threads/workspace-tags.ts
2468
+ function workspaceTagTargetRef(opts) {
2469
+ const requested = opts.requestedRef?.trim() || "";
2470
+ if (!opts.ownWorktreeOnly) {
2471
+ if (!requested) throw new Error("Pass a chat id on the workspace to label.");
2472
+ return requested;
2473
+ }
2474
+ const caller = opts.caller;
2475
+ if (!caller || isGlobalThread(caller) || caller.sourceType === "orchestration") {
2476
+ throw new Error("Tags belong on a project workspace. This chat is not one.");
2477
+ }
2478
+ if (!requested || requested === caller.id) return caller.id;
2479
+ const other = opts.find(requested);
2480
+ if (!other || normalizeWorktreePath(other.worktreePath) !== normalizeWorktreePath(caller.worktreePath)) {
2481
+ throw new Error("You can only change tags on this workspace.");
2482
+ }
2483
+ return other.id;
2484
+ }
2485
+ function applyWorkspaceTags(threadRef, tags, mode = "replace") {
2486
+ const thread = findThreadByRef(threadRef);
2487
+ if (!thread) throw new Error(`Thread not found: ${threadRef}`);
2488
+ if (isGlobalThread(thread) || thread.sourceType === "orchestration") {
2489
+ throw new Error("Tags belong on a project workspace. Pass a worktree chat id.");
2490
+ }
2491
+ const incoming = normalizeWorkspaceTags(tags);
2492
+ const siblings = threadsSharingWorktree(thread.worktreePath);
2493
+ const current = workspaceTagsFromGroup(siblings.length > 0 ? siblings : [thread]);
2494
+ let next;
2495
+ if (mode === "add") {
2496
+ next = normalizeWorkspaceTags([...current, ...incoming]);
2497
+ } else if (mode === "remove") {
2498
+ const drop = new Set(incoming.map((tag) => tag.toLowerCase()));
2499
+ next = current.filter((tag) => !drop.has(tag.toLowerCase()));
2500
+ } else {
2501
+ next = incoming;
2502
+ }
2503
+ const targets = siblings.length > 0 ? siblings : [thread];
2504
+ let updated = thread;
2505
+ for (const row of targets) {
2506
+ const written = updateThread(row.id, { tags: next });
2507
+ if (written.id === thread.id) updated = written;
2508
+ }
2509
+ return updated;
2510
+ }
2511
+
2417
2512
  // src/threads/create.ts
2418
2513
  function persistCreateAttachments(worktreePath, attachments) {
2419
2514
  return persistPendingFileAttachments(worktreePath, attachments ?? []);
@@ -2439,20 +2534,26 @@ function reuseLiveThread(input, repoPath, match) {
2439
2534
  liveThreadsForCreate()
2440
2535
  );
2441
2536
  if (!existing) return void 0;
2442
- const thread = readThread(existing.id) ?? existing;
2443
- if (!input.attachments?.length) return thread;
2444
- return updateThread(thread.id, {
2445
- attachments: persistCreateAttachments(thread.worktreePath, [
2446
- ...thread.attachments,
2447
- ...input.attachments
2448
- ])
2449
- });
2537
+ let thread = readThread(existing.id) ?? existing;
2538
+ if (input.attachments?.length) {
2539
+ thread = updateThread(thread.id, {
2540
+ attachments: persistCreateAttachments(thread.worktreePath, [
2541
+ ...thread.attachments,
2542
+ ...input.attachments
2543
+ ])
2544
+ });
2545
+ }
2546
+ if (normalizeWorkspaceTags(input.tags).length > 0) {
2547
+ thread = applyWorkspaceTags(thread.id, input.tags, "add");
2548
+ }
2549
+ return thread;
2450
2550
  }
2451
2551
  async function createThread(input, _onSetupLine) {
2452
2552
  const repoPath = await resolveRepoRoot(input.repoPath);
2453
2553
  if (!existsSync5(repoPath)) {
2454
2554
  throw new Error(`Repo not found: ${repoPath}`);
2455
2555
  }
2556
+ if (isRemovedWorkspace(repoPath)) throw new Error(`Project was removed: ${repoPath}`);
2456
2557
  const reused = reuseLiveThread(input, repoPath, {
2457
2558
  sourceType: input.sourceType,
2458
2559
  sourceRef: input.sourceRef,
@@ -2511,7 +2612,8 @@ async function createThread(input, _onSetupLine) {
2511
2612
  sourceIsFork: false,
2512
2613
  parentThreadId: input.parentThreadId ?? null,
2513
2614
  status: "idle",
2514
- cowboy: true
2615
+ cowboy: true,
2616
+ tags: input.tags
2515
2617
  });
2516
2618
  writeThread(thread2);
2517
2619
  await ensureWorkspace(repoPath);
@@ -2525,7 +2627,7 @@ async function createThread(input, _onSetupLine) {
2525
2627
  let sourceIsFork = false;
2526
2628
  if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
2527
2629
  if (isAbleTimeConnected() && getIssueSource() === "abletime") {
2528
- const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-VRATUMDO.js");
2630
+ const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-X64E3GUJ.js");
2529
2631
  const task = await ensureAbleTimeTask({
2530
2632
  title: title?.trim() || "Untitled work",
2531
2633
  description: input.prompt?.trim() || void 0
@@ -2628,14 +2730,15 @@ async function createThread(input, _onSetupLine) {
2628
2730
  prTitle,
2629
2731
  prAuthorLogin: prAuthorLogin2,
2630
2732
  prLabels,
2631
- prIsDraft
2733
+ prIsDraft,
2734
+ tags: input.tags
2632
2735
  });
2633
2736
  writeThread(thread);
2634
2737
  await ensureWorkspace(repoPath);
2635
2738
  return readThread(thread.id) ?? thread;
2636
2739
  }
2637
2740
  async function listLinearIssues(agent, repoPath) {
2638
- const { getAdapter: getAdapter2 } = await import("./agents-DYCFLDNL.js");
2741
+ const { getAdapter: getAdapter2 } = await import("./agents-2IE4HKHA.js");
2639
2742
  await requireAgent(agent, { requireLinear: true });
2640
2743
  const adapter = getAdapter2(agent);
2641
2744
  if (!adapter.listLinearIssues) {
@@ -2724,8 +2827,8 @@ function summarizeTurnLive(parts) {
2724
2827
  const interesting = [...tools].reverse().find((t) => !isPollWrapperToolName(t.name)) ?? lastTool;
2725
2828
  const lastToolLabel = interesting ? interesting.description || toolDescription(interesting.name, interesting.input) || interesting.name : void 0;
2726
2829
  const thinking = lastText(parts, "thinking");
2727
- const text = lastText(parts, "text");
2728
- const excerptRaw = thinking || text;
2830
+ const text3 = lastText(parts, "text");
2831
+ const excerptRaw = thinking || text3;
2729
2832
  const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
2730
2833
  const summary = liveActivitySummary(parts);
2731
2834
  return {
@@ -2786,6 +2889,9 @@ function writeTurnLive(threadId, progress) {
2786
2889
  }
2787
2890
  }
2788
2891
  }
2892
+ function readTurnLiveParts(threadId) {
2893
+ return buffers.get(threadId)?.parts.slice() ?? [];
2894
+ }
2789
2895
  function readTurnLive(threadId) {
2790
2896
  const path = threadLivePath(threadId);
2791
2897
  if (!existsSync6(path)) return null;
@@ -2874,7 +2980,8 @@ async function forkThreadWorktree(input, onSetupLine) {
2874
2980
  title: input.title?.trim() || void 0,
2875
2981
  parentThreadId: from.id,
2876
2982
  attachments: [attachment],
2877
- reuseExisting: false
2983
+ reuseExisting: false,
2984
+ tags: from.tags
2878
2985
  },
2879
2986
  onSetupLine
2880
2987
  );
@@ -2882,7 +2989,7 @@ async function forkThreadWorktree(input, onSetupLine) {
2882
2989
  }
2883
2990
 
2884
2991
  // src/orchestrator/quota-failover.ts
2885
- import { randomUUID } from "crypto";
2992
+ import { randomUUID as randomUUID2 } from "crypto";
2886
2993
  function managedKindForAgent(agent) {
2887
2994
  return agent === "claude" || agent === "codex" ? agent : null;
2888
2995
  }
@@ -2968,8 +3075,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
2968
3075
  );
2969
3076
  const recent = from.messages.slice(-8).map((m) => {
2970
3077
  const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
2971
- const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
2972
- return text ? `- ${role}: ${text}` : null;
3078
+ const text3 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
3079
+ return text3 ? `- ${role}: ${text3}` : null;
2973
3080
  }).filter(Boolean);
2974
3081
  const orch = isOrchestratorThread(from);
2975
3082
  const instructions = orch ? [
@@ -3002,7 +3109,7 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
3002
3109
  ...instructions
3003
3110
  ].join("\n");
3004
3111
  return {
3005
- id: randomUUID(),
3112
+ id: randomUUID2(),
3006
3113
  name: orch ? "Orchestration quota handoff.md" : "Quota handoff.md",
3007
3114
  kind: "transcript",
3008
3115
  content: body
@@ -3097,13 +3204,13 @@ function resolveConductorCursorAgentId(workspacePath) {
3097
3204
  for (const hash of hashes) {
3098
3205
  const agentsFile = join7(CURSOR_SDK_STORE, hash, "agents.ndjson");
3099
3206
  if (!existsSync7(agentsFile)) continue;
3100
- let text;
3207
+ let text3;
3101
3208
  try {
3102
- text = readFileSync5(agentsFile, "utf8");
3209
+ text3 = readFileSync5(agentsFile, "utf8");
3103
3210
  } catch {
3104
3211
  continue;
3105
3212
  }
3106
- for (const line of text.split("\n")) {
3213
+ for (const line of text3.split("\n")) {
3107
3214
  const trimmed = line.trim();
3108
3215
  if (!trimmed) continue;
3109
3216
  try {
@@ -3142,8 +3249,8 @@ async function adoptThread(input) {
3142
3249
  messages: input.messages ?? []
3143
3250
  });
3144
3251
  writeThread(thread);
3145
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-UP6DQ3WL.js");
3146
- await ensureWorkspace2(repoPath);
3252
+ const { addWorkspace: addWorkspace2 } = await import("./workspaces-7AJ5RZ3P.js");
3253
+ await addWorkspace2(repoPath);
3147
3254
  const { ensureWorktreeSideboardIgnored } = await import("./worktree-exclude-RXZPTCNH.js");
3148
3255
  await ensureWorktreeSideboardIgnored(input.worktreePath);
3149
3256
  return thread;
@@ -3882,13 +3989,13 @@ function formatStat(files) {
3882
3989
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
3883
3990
  }
3884
3991
  function emptyScopeStats() {
3885
- const z = emptyStat();
3992
+ const z4 = emptyStat();
3886
3993
  return {
3887
- commits: z,
3888
- uncommitted: z,
3889
- staged: z,
3890
- unstaged: z,
3891
- last_turn: z
3994
+ commits: z4,
3995
+ uncommitted: z4,
3996
+ staged: z4,
3997
+ unstaged: z4,
3998
+ last_turn: z4
3892
3999
  };
3893
4000
  }
3894
4001
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -4589,10 +4696,49 @@ async function confirmLand(thread, opts) {
4589
4696
  }
4590
4697
  }
4591
4698
 
4699
+ // src/diff/read-worktree-tail.ts
4700
+ import { closeSync, fstatSync, openSync, readSync } from "fs";
4701
+ import { join as join9 } from "path";
4702
+ var WORKTREE_TAIL_BYTES = 2e5;
4703
+ function readWorktreeFileTail(worktreePath, relativePath, maxBytes = WORKTREE_TAIL_BYTES) {
4704
+ if (!relativePath || relativePath.includes("..") || relativePath.startsWith("/")) {
4705
+ throw new Error("Invalid path");
4706
+ }
4707
+ const abs = join9(worktreePath, relativePath);
4708
+ const fd = openSync(abs, "r");
4709
+ try {
4710
+ const st = fstatSync(fd);
4711
+ if (!st.isFile()) throw new Error(`Not a file: ${relativePath}`);
4712
+ const size = st.size;
4713
+ const start = Math.max(0, size - maxBytes);
4714
+ const length = size - start;
4715
+ const buf = Buffer.alloc(length);
4716
+ if (length > 0) readSync(fd, buf, 0, length, start);
4717
+ if (buf.subarray(0, Math.min(buf.length, 8e3)).includes(0)) {
4718
+ return {
4719
+ path: relativePath,
4720
+ content: `(binary file, ${size} bytes)`,
4721
+ truncated: false,
4722
+ binary: true,
4723
+ encoding: "utf8"
4724
+ };
4725
+ }
4726
+ let content = buf.toString("utf8");
4727
+ const truncated = start > 0;
4728
+ if (truncated) {
4729
+ const nl = content.indexOf("\n");
4730
+ if (nl >= 0) content = content.slice(nl + 1);
4731
+ }
4732
+ return { path: relativePath, content, truncated, binary: false, encoding: "utf8" };
4733
+ } finally {
4734
+ closeSync(fd);
4735
+ }
4736
+ }
4737
+
4592
4738
  // src/skills/discover.ts
4593
4739
  import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
4594
4740
  import { homedir } from "os";
4595
- import { join as join9 } from "path";
4741
+ import { join as join10 } from "path";
4596
4742
 
4597
4743
  // src/skills/bundled/long-running.ts
4598
4744
  var LONG_RUNNING_SKILL_COMMAND = "long-running";
@@ -4761,7 +4907,7 @@ function scanSkillsDir(dir, source, out) {
4761
4907
  }
4762
4908
  for (const entry of entries) {
4763
4909
  if (entry.startsWith(".")) continue;
4764
- const skillMd = join9(dir, entry, "SKILL.md");
4910
+ const skillMd = join10(dir, entry, "SKILL.md");
4765
4911
  if (!existsSync10(skillMd)) continue;
4766
4912
  try {
4767
4913
  if (!statSync3(skillMd).isFile()) continue;
@@ -4783,12 +4929,12 @@ function scanClaudePluginSkills(pluginsRoot, out) {
4783
4929
  return;
4784
4930
  }
4785
4931
  if (lookingForSkillsDir && entries.includes("SKILL.md")) {
4786
- const skill = readSkill(join9(dir, "SKILL.md"), "cli");
4932
+ const skill = readSkill(join10(dir, "SKILL.md"), "cli");
4787
4933
  if (skill) out.push(skill);
4788
4934
  }
4789
4935
  for (const entry of entries) {
4790
4936
  if (entry === "node_modules" || entry === ".git") continue;
4791
- const full = join9(dir, entry);
4937
+ const full = join10(dir, entry);
4792
4938
  try {
4793
4939
  if (!statSync3(full).isDirectory()) continue;
4794
4940
  } catch {
@@ -4808,17 +4954,17 @@ function discoverSkills(worktreePath) {
4808
4954
  const home = homedir();
4809
4955
  const collected = [];
4810
4956
  for (const rel of [".claude/skills", ".cursor/skills", ".sideboard/skills", ".brightsy/skills", "skills"]) {
4811
- scanSkillsDir(join9(worktreePath, rel), "workspace", collected);
4957
+ scanSkillsDir(join10(worktreePath, rel), "workspace", collected);
4812
4958
  }
4813
4959
  for (const abs of [
4814
- join9(home, ".claude/skills"),
4815
- join9(home, ".cursor/skills"),
4816
- join9(home, ".sideboard/skills"),
4817
- join9(home, ".brightsy/skills")
4960
+ join10(home, ".claude/skills"),
4961
+ join10(home, ".cursor/skills"),
4962
+ join10(home, ".sideboard/skills"),
4963
+ join10(home, ".brightsy/skills")
4818
4964
  ]) {
4819
4965
  scanSkillsDir(abs, "user", collected);
4820
4966
  }
4821
- scanClaudePluginSkills(join9(home, ".claude/plugins"), collected);
4967
+ scanClaudePluginSkills(join10(home, ".claude/plugins"), collected);
4822
4968
  collected.push(...bundledSkills());
4823
4969
  const rank = {
4824
4970
  workspace: 0,
@@ -4990,11 +5136,11 @@ function shiftQueuedItem(queue, queueAttachments) {
4990
5136
 
4991
5137
  // src/agents/instructions.ts
4992
5138
  import { existsSync as existsSync12 } from "fs";
4993
- import { join as join11 } from "path";
5139
+ import { join as join12 } from "path";
4994
5140
 
4995
5141
  // src/skills/detached-job-path.ts
4996
5142
  import { existsSync as existsSync11 } from "fs";
4997
- import { dirname as dirname2, join as join10 } from "path";
5143
+ import { dirname as dirname2, join as join11 } from "path";
4998
5144
  import { fileURLToPath } from "url";
4999
5145
  var DETACHED_JOB_FILENAMES = ["detached-job.cjs", "detached-job.js"];
5000
5146
  function compiledModuleDir() {
@@ -5011,7 +5157,7 @@ function compiledModuleDir() {
5011
5157
  }
5012
5158
  function detachedJobScriptIn(dir) {
5013
5159
  for (const name of DETACHED_JOB_FILENAMES) {
5014
- const script = join10(dir, "scripts", name);
5160
+ const script = join11(dir, "scripts", name);
5015
5161
  if (existsSync11(script)) return script;
5016
5162
  }
5017
5163
  return null;
@@ -5148,12 +5294,15 @@ function formatWorktreeDirective(thread, opts) {
5148
5294
  lines.push(
5149
5295
  "Schedules: `create_schedule` / `list_schedules` (`at` / `every` / `cron`). `chatId=self` continues this chat; omit chatId for a new Global chat. Fires only while Sideboard.app is running. A future job turns on Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled."
5150
5296
  );
5297
+ lines.push(
5298
+ "`set_workspace_tags` labels this checkout (omit ref; replace, add, or remove). Not another checkout."
5299
+ );
5151
5300
  lines.push("");
5152
5301
  lines.push(formatProcessGuideDirective({ worktreePath: thread.worktreePath }));
5153
5302
  return lines.join("\n");
5154
5303
  }
5155
5304
  function formatWorktreeReminder() {
5156
- return "Sideboard workspace: stay in this cwd for all file and git work. Push and open PRs against origin, never upstream. Do not edit the main repo checkout. If a goal is given (Greptile 5/5, CI green), watch-fix-push until it lands \u2014 do not watch after every push. Schedules: create_schedule (at / every / cron; chatId=self for this chat).";
5305
+ return "Sideboard workspace: stay in this cwd for all file and git work. Push and open PRs against origin, never upstream. Do not edit the main repo checkout. If a goal is given (Greptile 5/5, CI green), watch-fix-push until it lands \u2014 do not watch after every push. Schedules: create_schedule (at / every / cron; chatId=self for this chat). set_workspace_tags updates labels on this workspace (omit ref).";
5157
5306
  }
5158
5307
  var PR_GOAL_RE = /greptile|\bci\b.*\b(green|pass)|checks?\s+(pass|green)|until\s+.*\b(pass|green|merge|clean)|\bfix ci\b|watch[- ]fix[- ]push/i;
5159
5308
  function mentionsPrGoal(prompt) {
@@ -5186,17 +5335,17 @@ function formatIssueToolsDirective(opts) {
5186
5335
  ];
5187
5336
  if (opts.linear) {
5188
5337
  lines.push(
5189
- "- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments, attachments; pass `include=full` if crush.truncated), `linear_download_attachment` (saves to `.context/attachments/`), `linear_comment`, `linear_update_issue` (state, cycle, labels, project, parent, relations), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
5338
+ "- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments; rows include the project), `linear_list_projects` (id, name, slug \u2014 use on update), `linear_get_issue` (comments, project, attachments; pass `include=full` if crush.truncated), `linear_download_attachment` (saves to `.context/attachments/`), `linear_comment`, `linear_update_issue` (state, cycle, labels, project, parent, relations), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
5190
5339
  );
5191
5340
  }
5192
5341
  if (github) {
5193
5342
  lines.push(
5194
- "- GitHub: `github_search_issues` (assignee=me; `updatedSince` for inbox updates + comments), `github_get_issue` (comments, attachments; pass `include=full` if crush.truncated), `github_download_attachment` (saves to `.context/attachments/`), `github_comment`, `github_update_issue` (state open|closed, labels, project, parent, relations), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
5343
+ "- GitHub: `github_search_issues` (assignee=me; `updatedSince` for inbox updates + comments; rows include projects when `gh` can read them), `github_list_projects` (titles for update), `github_get_issue` (comments, projects, attachments; pass `include=full` if crush.truncated; `projectsError` means run `gh auth refresh -s read:project`), `github_download_attachment` (saves to `.context/attachments/`), `github_comment`, `github_update_issue` (state open|closed, labels, project, parent, relations), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
5195
5344
  );
5196
5345
  }
5197
5346
  if (opts.abletime) {
5198
5347
  lines.push(
5199
- "- AbleTime: `abletime_list_tasks` / `abletime_search_tasks` (`updatedSince` for inbox updates + comments), `abletime_get_task` (comments, attachments; pass `include=full` if crush.truncated), `abletime_download_attachment` (saves to `.context/attachments/`), `abletime_comment`, `abletime_update_task` (state, labels, project, parent, relations), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
5348
+ "- AbleTime: `abletime_list_tasks` / `abletime_search_tasks` (`updatedSince` for inbox updates + comments; rows include the project), `abletime_get_task` (comments, project, attachments; pass `include=full` if crush.truncated), `abletime_download_attachment` (saves to `.context/attachments/`), `abletime_comment`, `abletime_update_task` (state, labels, project, parent, relations), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
5200
5349
  );
5201
5350
  }
5202
5351
  lines.push(
@@ -5252,9 +5401,9 @@ function formatPrGateDirective() {
5252
5401
  }
5253
5402
  function formatProcessGuideDirective(opts) {
5254
5403
  const worktree = opts?.worktreePath?.trim();
5255
- const hasGraphSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/graph-engineering/SKILL.md"));
5256
- const hasReviewSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/review/SKILL.md"));
5257
- const hasInstallSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/worktree-install/SKILL.md"));
5404
+ const hasGraphSkill = !!worktree && existsSync12(join12(worktree, ".claude/skills/graph-engineering/SKILL.md"));
5405
+ const hasReviewSkill = !!worktree && existsSync12(join12(worktree, ".claude/skills/review/SKILL.md"));
5406
+ const hasInstallSkill = !!worktree && existsSync12(join12(worktree, ".claude/skills/worktree-install/SKILL.md"));
5258
5407
  return [
5259
5408
  "Process guides (recurring work only):",
5260
5409
  hasGraphSkill ? "- Migrations, ports, batch fixes, and other fan-out: follow `.claude/skills/graph-engineering/SKILL.md` (`/graph-engineering`). Judge first; state on disk; grow the rulebook; do not patch around it." : null,
@@ -5375,12 +5524,12 @@ function firstString(record, keys) {
5375
5524
  return "";
5376
5525
  }
5377
5526
  async function readJson(res) {
5378
- const text = await res.text();
5379
- if (!text.trim()) return null;
5527
+ const text3 = await res.text();
5528
+ if (!text3.trim()) return null;
5380
5529
  try {
5381
- return JSON.parse(text);
5530
+ return JSON.parse(text3);
5382
5531
  } catch {
5383
- return text;
5532
+ return text3;
5384
5533
  }
5385
5534
  }
5386
5535
  async function authorizedGet(url, token) {
@@ -5531,6 +5680,9 @@ function formatOptionalServicesDirective(integrations) {
5531
5680
  );
5532
5681
  }
5533
5682
  }
5683
+ lines.push(
5684
+ "If a connector MCP is listed but needsAuth or returns unauthorized, do not sign in to that MCP. Check the native CLI (or the HTTP API when there is no CLI) \u2014 the token is already in this process."
5685
+ );
5534
5686
  lines.push(
5535
5687
  "If a CLI is missing, say so or use the HTTP API with the same token. The user can Install CLI from Settings \u2192 Connectors. Do not ask the user to install a vendor MCP."
5536
5688
  );
@@ -5556,7 +5708,451 @@ function formatOptionalServicesDirective(integrations) {
5556
5708
  }
5557
5709
  function formatOptionalServicesReminder(integrations) {
5558
5710
  if (connectedOptionalServices(integrations).length === 0) return null;
5559
- return "Settings \u2192 Connectors: official CLIs / PostHog HTTP with injected env. Write output to `.context/cli/` (not `.context/attachments/`) and read a slice \u2014 never dump raw --json/--expand into the tool result (any agent). stop_job if a fetch hangs. Do not add vendor MCPs.";
5711
+ return "Settings \u2192 Connectors: official CLIs / PostHog HTTP with injected env. If a connector MCP needsAuth or is unauthorized, check the native CLI or HTTP API instead of signing in. Write output to `.context/cli/` (not `.context/attachments/`) and read a slice \u2014 never dump raw --json/--expand into the tool result (any agent). stop_job if a fetch hangs. Do not add vendor MCPs.";
5712
+ }
5713
+
5714
+ // src/mcp/viewer-context-tools.ts
5715
+ import { z as z3 } from "zod";
5716
+
5717
+ // src/mcp/env-tools.ts
5718
+ import { z as z2 } from "zod";
5719
+
5720
+ // src/mcp/issue-list.ts
5721
+ import { z } from "zod";
5722
+
5723
+ // src/integrations/issue-since.ts
5724
+ var ISSUE_SINCE_ERROR = "updatedSince must be an ISO datetime, YYYY-MM-DD, or relative time (yesterday, 2d, 3 hours ago)";
5725
+ var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
5726
+ var DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
5727
+ var RELATIVE_RE = /^(\d+)\s*(hours?|hrs?|h|days?|d|weeks?|wks?|w)(?:\s+ago)?$/i;
5728
+ function startOfLocalDay(date) {
5729
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
5730
+ }
5731
+ function addLocalDays(date, days) {
5732
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
5733
+ }
5734
+ function parseIssueSince(input, now = /* @__PURE__ */ new Date()) {
5735
+ const raw = input.trim();
5736
+ if (!raw) throw new Error(ISSUE_SINCE_ERROR);
5737
+ const dateOnly = raw.match(DATE_ONLY_RE);
5738
+ if (dateOnly) {
5739
+ const year = Number(dateOnly[1]);
5740
+ const month = Number(dateOnly[2]);
5741
+ const day = Number(dateOnly[3]);
5742
+ const local = new Date(year, month - 1, day);
5743
+ if (local.getFullYear() !== year || local.getMonth() !== month - 1 || local.getDate() !== day) {
5744
+ throw new Error(ISSUE_SINCE_ERROR);
5745
+ }
5746
+ return startOfLocalDay(local).toISOString();
5747
+ }
5748
+ if (DATE_TIME_RE.test(raw)) {
5749
+ const ms = Date.parse(raw);
5750
+ if (!Number.isFinite(ms)) throw new Error(ISSUE_SINCE_ERROR);
5751
+ return new Date(ms).toISOString();
5752
+ }
5753
+ const lower = raw.toLowerCase();
5754
+ if (lower === "today") return startOfLocalDay(now).toISOString();
5755
+ if (lower === "yesterday") return addLocalDays(startOfLocalDay(now), -1).toISOString();
5756
+ if (lower === "last week" || lower === "lastweek") {
5757
+ return addLocalDays(startOfLocalDay(now), -7).toISOString();
5758
+ }
5759
+ const relative = lower.match(RELATIVE_RE);
5760
+ if (relative) {
5761
+ const n = Number(relative[1]);
5762
+ if (!Number.isInteger(n) || n < 1) throw new Error(ISSUE_SINCE_ERROR);
5763
+ const unit = relative[2];
5764
+ if (unit.startsWith("h")) return new Date(now.getTime() - n * 36e5).toISOString();
5765
+ if (unit.startsWith("w")) return addLocalDays(startOfLocalDay(now), -7 * n).toISOString();
5766
+ return addLocalDays(startOfLocalDay(now), -n).toISOString();
5767
+ }
5768
+ throw new Error(ISSUE_SINCE_ERROR);
5769
+ }
5770
+ function formatGitHubSearchUpdatedSince(iso) {
5771
+ return iso.replace(/\.\d{3}Z$/, "Z");
5772
+ }
5773
+ function issueMatchesUpdatedSince(issue, sinceIso) {
5774
+ const ts = issue.updatedAt || issue.createdAt;
5775
+ if (!ts) return false;
5776
+ const value = Date.parse(ts);
5777
+ const since = Date.parse(sinceIso);
5778
+ return Number.isFinite(value) && Number.isFinite(since) && value >= since;
5779
+ }
5780
+ var ISSUE_COMMENT_PREVIEW_CHARS = 240;
5781
+ function previewIssueCommentBody(body) {
5782
+ const flat = body.replace(/\s+/g, " ").trim();
5783
+ if (flat.length <= ISSUE_COMMENT_PREVIEW_CHARS) return flat;
5784
+ return `${flat.slice(0, ISSUE_COMMENT_PREVIEW_CHARS - 1).trimEnd()}\u2026`;
5785
+ }
5786
+ function issueActivityKind(issue, sinceIso) {
5787
+ const created = issue.createdAt ? Date.parse(issue.createdAt) : Number.NaN;
5788
+ const since = Date.parse(sinceIso);
5789
+ if (Number.isFinite(created) && Number.isFinite(since) && created >= since) return "created";
5790
+ return "updated";
5791
+ }
5792
+
5793
+ // src/mcp/issue-list.ts
5794
+ var MCP_ISSUE_LIST_DEFAULT_LIMIT = 40;
5795
+ var MCP_ISSUE_LIST_MAX_LIMIT = 250;
5796
+ function clampMcpIssueLimit(limit) {
5797
+ if (limit == null || !Number.isFinite(limit)) return MCP_ISSUE_LIST_DEFAULT_LIMIT;
5798
+ return Math.max(1, Math.min(MCP_ISSUE_LIST_MAX_LIMIT, Math.floor(limit)));
5799
+ }
5800
+ function applyIssueListWindow(items, limit) {
5801
+ const truncated = items.length > limit;
5802
+ return {
5803
+ items: truncated ? items.slice(0, limit) : items,
5804
+ truncated
5805
+ };
5806
+ }
5807
+ var MCP_ISSUE_UPDATED_SINCE_DESCRIPTION = "ISO datetime, YYYY-MM-DD, or relative (yesterday, 2d, 3 hours ago). Tickets updated since then plus new comments. Use for \u201Cany updates since yesterday?\u201D";
5808
+ var mcpIssueUpdatedSinceSchema = z.string().min(1).optional().describe(MCP_ISSUE_UPDATED_SINCE_DESCRIPTION);
5809
+ function compactIssueRow(issue, opts) {
5810
+ const projectNames = (issue.projects ?? []).map((project) => (project.name || project.id || "").trim()).filter(Boolean);
5811
+ return {
5812
+ identifier: issue.identifier,
5813
+ title: issue.title,
5814
+ ...issue.assignee ? { assignee: issue.assignee } : {},
5815
+ ...issue.labels.length > 0 ? { labels: issue.labels } : {},
5816
+ ...issue.teamKey ? { team: issue.teamKey } : {},
5817
+ ...issue.cycle?.name ? { cycle: issue.cycle.name } : {},
5818
+ ...projectNames.length ? { projects: projectNames } : {},
5819
+ ...issue.createdAt ? { createdAt: issue.createdAt } : {},
5820
+ ...issue.updatedAt ? { updatedAt: issue.updatedAt } : {},
5821
+ ...opts?.since ? { kind: issueActivityKind(issue, opts.since) } : {}
5822
+ };
5823
+ }
5824
+ function formatMcpIssueList(input) {
5825
+ const truncated = input.truncated || Boolean(input.commentsTruncated);
5826
+ const comments = input.comments ?? [];
5827
+ return {
5828
+ ...input.source ? { source: input.source } : {},
5829
+ ...input.viewer ? { viewer: input.viewer } : {},
5830
+ limit: input.limit,
5831
+ returned: input.issues.length,
5832
+ truncated,
5833
+ ...input.since ? { since: input.since } : {},
5834
+ ...truncated ? {
5835
+ hint: input.since ? "More issues or comments match. Pass a higher limit (max 250) or a tighter query." : "More issues match. Pass a higher limit (max 250) or a tighter query."
5836
+ } : {},
5837
+ issues: input.issues.map((issue) => compactIssueRow(issue, { since: input.since })),
5838
+ ...input.since || comments.length > 0 ? { comments } : {}
5839
+ };
5840
+ }
5841
+ function formatListedIssuesForMcp(result, page) {
5842
+ const windowed = applyIssueListWindow(result.issues, page);
5843
+ const comments = result.comments ?? [];
5844
+ const commentWindow = applyIssueListWindow(comments, page);
5845
+ return formatMcpIssueList({
5846
+ source: result.source,
5847
+ viewer: result.viewer?.name || result.viewer?.login,
5848
+ limit: page,
5849
+ issues: windowed.items,
5850
+ truncated: windowed.truncated,
5851
+ since: result.since,
5852
+ comments: commentWindow.items,
5853
+ commentsTruncated: commentWindow.truncated
5854
+ });
5855
+ }
5856
+ function mcpJson(payload, isError = false) {
5857
+ return {
5858
+ content: [{ type: "text", text: JSON.stringify(payload) }],
5859
+ ...isError ? { isError: true } : {}
5860
+ };
5861
+ }
5862
+
5863
+ // src/mcp/env-tools.ts
5864
+ function text(payload, isError = false) {
5865
+ return mcpJson(payload, isError);
5866
+ }
5867
+ var PROJECT_PATH_HINT = "Pass repoPath from list_projects. Orchestration cwd is synthetic \u2014 do not omit repoPath for scope=project. Worktree turns may omit it (uses cwd).";
5868
+ async function projectFor(repoPath) {
5869
+ const hint = repoPath?.trim() || process.cwd();
5870
+ const name = projectNameForRepoPath(hint);
5871
+ if (!name) return null;
5872
+ return { name, prefix: projectEnvPrefix(name) };
5873
+ }
5874
+ function registerEnvTools(server) {
5875
+ server.tool(
5876
+ "list_env",
5877
+ "List Settings \u2192 Environment variable names (not values). scope=project keys use the project prefix (project name, uppercased, plus _). scope=account keys are shared by every project. Pass repoPath from list_projects when calling from orchestration so the reply includes this project prefix.",
5878
+ {
5879
+ repoPath: z2.string().optional().describe(
5880
+ "Registered workspace or worktree path. Required from orchestration. Omit on a worktree turn (uses cwd)."
5881
+ )
5882
+ },
5883
+ async ({ repoPath }) => {
5884
+ try {
5885
+ const project = await projectFor(repoPath);
5886
+ return text({
5887
+ project: project?.name ?? null,
5888
+ prefix: project?.prefix ?? null,
5889
+ keys: listEnvKeys()
5890
+ });
5891
+ } catch (err) {
5892
+ return text({ error: err instanceof Error ? err.message : String(err) }, true);
5893
+ }
5894
+ }
5895
+ );
5896
+ server.tool(
5897
+ "set_env",
5898
+ "Set a Settings \u2192 Environment variable. scope=project prefixes the key with the project name (APPLE_ID on project sideboard is stored as SIDEBOARD_APPLE_ID). scope=account stores the key unchanged and is shared by every project. Values are injected into later agent turns. list_env does not return values.",
5899
+ {
5900
+ key: z2.string().describe("Env name. For scope=project, pass the bare name or the already-prefixed name."),
5901
+ value: z2.string().describe("Value to store. Empty deletes the key."),
5902
+ scope: z2.enum(["account", "project"]).describe("project = prefixed for one repo; account = shared by every project"),
5903
+ repoPath: z2.string().optional().describe(
5904
+ "Required for scope=project from orchestration (path from list_projects). Worktree turns may omit it (cwd)."
5905
+ )
5906
+ },
5907
+ async ({ key, value, scope, repoPath }) => {
5908
+ try {
5909
+ const envScope = scope;
5910
+ if (envScope === "project") {
5911
+ const project = await projectFor(repoPath);
5912
+ if (!project) return text({ ok: false, error: "repoPath required", message: PROJECT_PATH_HINT }, true);
5913
+ const saved2 = setEnvVar({
5914
+ key,
5915
+ value,
5916
+ scope: "project",
5917
+ projectName: project.name
5918
+ });
5919
+ return text({ ok: true, ...saved2 });
5920
+ }
5921
+ const saved = setEnvVar({ key, value, scope: "account" });
5922
+ return text({ ok: true, ...saved });
5923
+ } catch (err) {
5924
+ return text({ error: err instanceof Error ? err.message : String(err) }, true);
5925
+ }
5926
+ }
5927
+ );
5928
+ server.tool(
5929
+ "delete_env",
5930
+ "Delete a Settings \u2192 Environment variable. scope=project uses the project prefix (same rule as set_env). Does not print the stored value.",
5931
+ {
5932
+ key: z2.string().describe("Env name. For scope=project, pass the bare name or the already-prefixed name."),
5933
+ scope: z2.enum(["account", "project"]),
5934
+ repoPath: z2.string().optional().describe(
5935
+ "Required for scope=project from orchestration (path from list_projects). Worktree turns may omit it (cwd)."
5936
+ )
5937
+ },
5938
+ async ({ key, scope, repoPath }) => {
5939
+ try {
5940
+ const envScope = scope;
5941
+ if (envScope === "project") {
5942
+ const project = await projectFor(repoPath);
5943
+ if (!project) return text({ ok: false, error: "repoPath required", message: PROJECT_PATH_HINT }, true);
5944
+ const removed2 = deleteEnvVar({
5945
+ key,
5946
+ scope: "project",
5947
+ projectName: project.name
5948
+ });
5949
+ return text({ ok: true, ...removed2 });
5950
+ }
5951
+ const removed = deleteEnvVar({ key, scope: "account" });
5952
+ return text({ ok: true, ...removed });
5953
+ } catch (err) {
5954
+ return text({ error: err instanceof Error ? err.message : String(err) }, true);
5955
+ }
5956
+ }
5957
+ );
5958
+ }
5959
+
5960
+ // src/mcp/viewer-context-tools.ts
5961
+ function text2(payload, isError = false) {
5962
+ return mcpJson(payload, isError);
5963
+ }
5964
+ function matchRegisteredWorkspace(repoPath) {
5965
+ const hint = (repoPath ?? "").trim().replace(/\/+$/, "");
5966
+ if (!hint) return null;
5967
+ let best = null;
5968
+ for (const w of listWorkspaces()) {
5969
+ const stored = w.path.replace(/\/+$/, "");
5970
+ if (stored === hint || hint.startsWith(`${stored}/`)) {
5971
+ if (!best || stored.length > best.length) best = stored;
5972
+ }
5973
+ }
5974
+ return best;
5975
+ }
5976
+ async function resolveProjectRepoPath(repoPath) {
5977
+ const hint = repoPath?.trim() || process.cwd();
5978
+ let root = hint;
5979
+ try {
5980
+ root = await resolveRepoRoot(hint);
5981
+ } catch {
5982
+ root = hint.replace(/\/+$/, "");
5983
+ }
5984
+ return matchRegisteredWorkspace(root) ?? matchRegisteredWorkspace(hint);
5985
+ }
5986
+ var PROJECT_PATH_HINT2 = "Pass repoPath from list_projects. Orchestration cwd is synthetic and empty \u2014 do not omit repoPath for project context.";
5987
+ function registerViewerContextTools(server) {
5988
+ server.tool(
5989
+ "get_viewer_context",
5990
+ "Read account and project user context (Settings \u2192 Agents / Projects): roles, tickets, and review queues as freeform text. Orchestration must pass repoPath from list_projects for project context. Worktree turns may omit repoPath (uses cwd).",
5991
+ {
5992
+ repoPath: z3.string().optional().describe(
5993
+ "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
5994
+ )
5995
+ },
5996
+ async ({ repoPath }) => {
5997
+ try {
5998
+ const path = await resolveProjectRepoPath(repoPath);
5999
+ return text2(readViewerContext(path ?? void 0));
6000
+ } catch (err) {
6001
+ return text2(
6002
+ { error: err instanceof Error ? err.message : String(err) },
6003
+ true
6004
+ );
6005
+ }
6006
+ }
6007
+ );
6008
+ server.tool(
6009
+ "update_viewer_context",
6010
+ "Replace account or project user context (Settings \u2192 Agents / Projects). Never call until the user confirmed. Show the proposed text, ask_user (Save this context / Do not save), wait, then pass confirmed=true. Empty context clears that field.",
6011
+ {
6012
+ scope: z3.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
6013
+ context: z3.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
6014
+ confirmed: z3.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
6015
+ repoPath: z3.string().optional().describe(
6016
+ "Required for scope=project from orchestration (path from list_projects). Worktree turns may omit it (cwd)."
6017
+ )
6018
+ },
6019
+ async ({ scope, context, confirmed, repoPath }) => {
6020
+ try {
6021
+ if (scope === "project") {
6022
+ const path = await resolveProjectRepoPath(repoPath);
6023
+ if (!path) {
6024
+ return text2(
6025
+ {
6026
+ ok: false,
6027
+ error: "repoPath required",
6028
+ message: PROJECT_PATH_HINT2
6029
+ },
6030
+ true
6031
+ );
6032
+ }
6033
+ const result2 = writeViewerContext({
6034
+ scope,
6035
+ context,
6036
+ repoPath: path,
6037
+ confirmed: confirmed === true
6038
+ });
6039
+ return text2(result2, !result2.ok);
6040
+ }
6041
+ const result = writeViewerContext({
6042
+ scope,
6043
+ context,
6044
+ confirmed: confirmed === true
6045
+ });
6046
+ return text2(result, !result.ok);
6047
+ } catch (err) {
6048
+ return text2(
6049
+ { error: err instanceof Error ? err.message : String(err) },
6050
+ true
6051
+ );
6052
+ }
6053
+ }
6054
+ );
6055
+ registerEnvTools(server);
6056
+ }
6057
+
6058
+ // src/store/env-vars.ts
6059
+ var ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
6060
+ function projectEnvPrefix(projectName) {
6061
+ const slug = projectName.normalize("NFKD").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").toUpperCase().slice(0, 40);
6062
+ return `${slug || "PROJECT"}_`;
6063
+ }
6064
+ function assertEnvKey(key) {
6065
+ const k = key.trim();
6066
+ if (!ENV_KEY.test(k)) {
6067
+ throw new Error(
6068
+ `Invalid env key "${key}". Use letters, digits, and underscores, starting with a letter or underscore.`
6069
+ );
6070
+ }
6071
+ return k;
6072
+ }
6073
+ function qualifyProjectEnvKey(prefix, key) {
6074
+ const k = assertEnvKey(key);
6075
+ if (k.toUpperCase().startsWith(prefix)) return prefix + k.slice(prefix.length);
6076
+ return prefix + k;
6077
+ }
6078
+ function projectNameForRepoPath(repoPath) {
6079
+ const matched = matchRegisteredWorkspace(repoPath);
6080
+ if (!matched) return null;
6081
+ const found = listWorkspaces().find((w) => w.path.replace(/\/+$/, "") === matched);
6082
+ return found?.name?.trim() || null;
6083
+ }
6084
+ function projectPrefixes() {
6085
+ return listWorkspaces().map((w) => ({ name: w.name.trim(), prefix: projectEnvPrefix(w.name) })).filter((p) => p.name && p.prefix !== "PROJECT_").sort((a, b) => b.prefix.length - a.prefix.length);
6086
+ }
6087
+ function classifyEnvKey(key) {
6088
+ const hit = projectPrefixes().find((p) => key.startsWith(p.prefix));
6089
+ if (!hit) return { scope: "account" };
6090
+ return { scope: "project", project: hit.name, prefix: hit.prefix };
6091
+ }
6092
+ function listEnvKeys() {
6093
+ return Object.keys(loadAppSettings().environment).sort((a, b) => a.localeCompare(b)).map((key) => ({ key, ...classifyEnvKey(key) }));
6094
+ }
6095
+ function setEnvVar(input) {
6096
+ const value = input.value;
6097
+ if (input.scope === "account") {
6098
+ const key2 = assertEnvKey(input.key);
6099
+ if (value.trim() === "") {
6100
+ updateAppEnvironment({ [key2]: null });
6101
+ return { key: key2, scope: "account" };
6102
+ }
6103
+ updateAppEnvironment({ [key2]: value });
6104
+ return { key: key2, scope: "account" };
6105
+ }
6106
+ const project = input.projectName?.trim();
6107
+ if (!project) throw new Error("Project name required for scope=project.");
6108
+ const prefix = projectEnvPrefix(project);
6109
+ const key = qualifyProjectEnvKey(prefix, input.key);
6110
+ if (value.trim() === "") {
6111
+ updateAppEnvironment({ [key]: null });
6112
+ } else {
6113
+ updateAppEnvironment({ [key]: value });
6114
+ }
6115
+ return { key, scope: "project", prefix, project };
6116
+ }
6117
+ function deleteEnvVar(input) {
6118
+ const current = loadAppSettings().environment;
6119
+ if (input.scope === "account") {
6120
+ const key2 = assertEnvKey(input.key);
6121
+ const deleted2 = Object.prototype.hasOwnProperty.call(current, key2);
6122
+ updateAppEnvironment({ [key2]: null });
6123
+ return { key: key2, scope: "account", deleted: deleted2 };
6124
+ }
6125
+ const project = input.projectName?.trim();
6126
+ if (!project) throw new Error("Project name required for scope=project.");
6127
+ const prefix = projectEnvPrefix(project);
6128
+ const key = qualifyProjectEnvKey(prefix, input.key);
6129
+ const deleted = Object.prototype.hasOwnProperty.call(current, key);
6130
+ updateAppEnvironment({ [key]: null });
6131
+ return { key, scope: "project", deleted, prefix, project };
6132
+ }
6133
+ function formatEnvDirective(projectName) {
6134
+ if (!projectName) {
6135
+ return [
6136
+ "Sideboard environment (Settings \u2192 Environment): list_env, set_env, delete_env.",
6137
+ "scope=project prefixes the key with the registered project name (uppercased, then _). APPLE_ID for a project named sideboard is stored as SIDEBOARD_APPLE_ID.",
6138
+ "scope=account is shared by every project and is not prefixed.",
6139
+ "list_env returns names only. A value is injected into later agent turns."
6140
+ ].join(" ");
6141
+ }
6142
+ const prefix = projectEnvPrefix(projectName);
6143
+ return [
6144
+ "Sideboard environment (Settings \u2192 Environment): list_env, set_env, delete_env.",
6145
+ `This project's prefix is ${prefix}. Project secrets must use that prefix \u2014 set_env scope=project applies it (APPLE_ID becomes ${prefix}APPLE_ID).`,
6146
+ "scope=account is only for variables shared by every project.",
6147
+ "list_env returns names only. A value set here is injected into later agent turns."
6148
+ ].join(" ");
6149
+ }
6150
+ function formatEnvReminder(projectName) {
6151
+ if (!projectName) {
6152
+ return "Sideboard env: list_env / set_env / delete_env. scope=project prefixes the key with the project name (SIDEBOARD_APPLE_ID). scope=account is shared by every project.";
6153
+ }
6154
+ const prefix = projectEnvPrefix(projectName);
6155
+ return `Sideboard env: list_env / set_env / delete_env. This project's prefix is ${prefix}. scope=project stores ${prefix}<NAME>. scope=account is shared by every project.`;
5560
6156
  }
5561
6157
 
5562
6158
  // src/threads/sync-branch.ts
@@ -5580,9 +6176,9 @@ async function syncThreadBranchFromGit(threadId) {
5580
6176
  }
5581
6177
 
5582
6178
  // src/store/schedules.ts
5583
- import { randomUUID as randomUUID2 } from "crypto";
6179
+ import { randomUUID as randomUUID3 } from "crypto";
5584
6180
  import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
5585
- import { join as join12 } from "path";
6181
+ import { join as join13 } from "path";
5586
6182
  import { Cron } from "croner";
5587
6183
  var DURATION_RE = /^(\d+)(s|m|h|d)$/i;
5588
6184
  var UNIT_MS = {
@@ -5592,7 +6188,7 @@ var UNIT_MS = {
5592
6188
  d: 864e5
5593
6189
  };
5594
6190
  function schedulesPath() {
5595
- return join12(appDataDir(), "schedules.json");
6191
+ return join13(appDataDir(), "schedules.json");
5596
6192
  }
5597
6193
  function parseDurationMs(every) {
5598
6194
  const m = every.trim().match(DURATION_RE);
@@ -5725,7 +6321,7 @@ function buildTask(input, now) {
5725
6321
  const ts = nowIso(now);
5726
6322
  const agent = input.agent ? coerceOrchestratorAgent(input.agent) : null;
5727
6323
  return {
5728
- id: randomUUID2(),
6324
+ id: randomUUID3(),
5729
6325
  name: input.name?.trim() || defaultScheduleName(prompt),
5730
6326
  prompt,
5731
6327
  enabled: input.enabled !== false,
@@ -5839,7 +6435,7 @@ function formatScheduledPrompt(name, prompt) {
5839
6435
  ${prompt}`;
5840
6436
  }
5841
6437
  async function defaultDeps() {
5842
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-KY2KBBE2.js");
6438
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-FPGVKEID.js");
5843
6439
  const orch = getOrchestrator2();
5844
6440
  return {
5845
6441
  findThread: (id) => findThreadByRef(id),
@@ -6187,6 +6783,10 @@ var Orchestrator = class {
6187
6783
  }
6188
6784
  continue;
6189
6785
  }
6786
+ if (thisProcessShouldDrainAgentQueues() && isUnclaimedSteerRequest(thread.steerRequest) && thread.queue.length > 0) {
6787
+ this.finishSteer(thread.id, false);
6788
+ continue;
6789
+ }
6190
6790
  if (thread.queue.length > 0) {
6191
6791
  this.haltDrain.delete(thread.id);
6192
6792
  this.armDrain(thread.id);
@@ -6571,8 +7171,12 @@ var Orchestrator = class {
6571
7171
  async addWorkspace(repoPath) {
6572
7172
  return addWorkspace(repoPath);
6573
7173
  }
6574
- removeWorkspace(repoPath) {
7174
+ async removeWorkspace(repoPath) {
6575
7175
  removeWorkspace(repoPath);
7176
+ for (const thread of listThreads()) {
7177
+ if (!sameWorkspacePath(thread.repoPath, repoPath)) continue;
7178
+ await this.archive(thread.id).catch(() => void 0);
7179
+ }
6576
7180
  }
6577
7181
  async adopt(input) {
6578
7182
  const thread = await adoptThread(input);
@@ -6645,11 +7249,11 @@ var Orchestrator = class {
6645
7249
  return results;
6646
7250
  }
6647
7251
  /** Edit the text of a not-yet-started queued message. */
6648
- async editQueuedMessage(threadRef, index, text) {
7252
+ async editQueuedMessage(threadRef, index, text3) {
6649
7253
  const thread = this.requireThread(threadRef);
6650
7254
  return withThreadLock(thread.id, async () => {
6651
7255
  const current = this.requireThread(thread.id);
6652
- const trimmed = text.trim();
7256
+ const trimmed = text3.trim();
6653
7257
  if (!trimmed || index < 0 || index >= current.queue.length) {
6654
7258
  return current;
6655
7259
  }
@@ -6683,33 +7287,37 @@ var Orchestrator = class {
6683
7287
  return next;
6684
7288
  });
6685
7289
  }
6686
- /**
6687
- * Promote a queued message to run next, interrupting the in-flight turn (if any).
6688
- * The current turn is stopped without clearing the rest of the queue — drainQueue
6689
- * picks the promoted message up as soon as the interrupted turn unwinds.
6690
- */
7290
+ /** Promote a queued prompt to run next, interrupting the in-flight turn. */
6691
7291
  async sendQueuedMessageNow(threadRef, index) {
6692
7292
  const thread = this.requireThread(threadRef);
7293
+ const handoff = !thisProcessShouldDrainAgentQueues();
6693
7294
  const promoted = await withThreadLock(thread.id, async () => {
6694
- const current2 = this.requireThread(thread.id);
6695
- if (index < 0 || index >= current2.queue.length) return false;
6696
- const next = moveQueuedItemToFront(current2.queue, current2.queueAttachments, index);
7295
+ const current = this.requireThread(thread.id);
7296
+ if (index < 0 || index >= current.queue.length) return false;
7297
+ const next = moveQueuedItemToFront(current.queue, current.queueAttachments, index);
6697
7298
  this.haltDrain.delete(thread.id);
6698
- updateThread(thread.id, next);
7299
+ updateThread(thread.id, {
7300
+ ...next,
7301
+ ...handoff ? { steerRequest: nextSteerRequest(current.steerRequest) } : {}
7302
+ });
6699
7303
  this.emit({ type: "queue_changed", threadId: thread.id, queue: next.queue });
6700
7304
  return true;
6701
7305
  });
6702
- if (!promoted) return this.requireThread(thread.id);
6703
- if (!thisProcessShouldDrainAgentQueues()) return this.requireThread(thread.id);
6704
- const current = this.requireThread(thread.id);
6705
- const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
6706
- const livePid = current.agentPid;
6707
- const foreignLive = !inFlight && typeof livePid === "number" && livePid > 0 && isPidAlive(livePid);
6708
- if (inFlight || foreignLive) {
6709
- this.stop(thread.id, { clearQueue: false, continueQueue: true });
7306
+ if (!promoted || handoff) return this.requireThread(thread.id);
7307
+ return this.finishSteer(thread.id);
7308
+ }
7309
+ /** Desktop host: interrupt the live turn and start the front-of-queue prompt. */
7310
+ finishSteer(threadId, notifyParent = true) {
7311
+ claimSteerRequest(threadId);
7312
+ this.haltDrain.delete(threadId);
7313
+ const current = this.requireThread(threadId);
7314
+ const inFlight = this.activeTurns.has(threadId) || this.startingTurns.has(threadId);
7315
+ const pid = current.agentPid;
7316
+ if (inFlight || typeof pid === "number" && pid > 0 && isPidAlive(pid)) {
7317
+ this.stop(threadId, { clearQueue: false, continueQueue: true, notifyParent });
6710
7318
  }
6711
- this.armDrain(thread.id);
6712
- return this.requireThread(thread.id);
7319
+ this.armDrain(threadId);
7320
+ return this.requireThread(threadId);
6713
7321
  }
6714
7322
  armDrain(threadId) {
6715
7323
  void this.drainQueue(threadId).catch((err) => {
@@ -6840,7 +7448,7 @@ var Orchestrator = class {
6840
7448
  orch: isOrchestratorThread(thread)
6841
7449
  });
6842
7450
  if (this.stoppedTurns.has(threadId)) {
6843
- const stopped = writeLiveStatus(threadId, "stopped");
7451
+ const stopped = writeStoppedStatus(threadId);
6844
7452
  if (stopped?.status === "stopped") {
6845
7453
  this.emit({ type: "status_changed", threadId, status: "stopped" });
6846
7454
  }
@@ -6886,7 +7494,9 @@ var Orchestrator = class {
6886
7494
  ticketId: issueTicket?.id,
6887
7495
  ticketProvider: issueTicket?.provider
6888
7496
  }) : null;
6889
- const viewerContextReminder = thread.agent !== "brightsy" && !isOrchestratorThread(thread) ? formatViewerContextReminder() : null;
7497
+ const viewerContextReminder = thread.agent !== "brightsy" && !isOrchestratorThread(thread) ? `${formatViewerContextReminder()}
7498
+
7499
+ ${formatEnvReminder(projectNameForRepoPath(thread.repoPath || thread.worktreePath))}` : null;
6890
7500
  const reviewWriteGateReminder = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && isReviewWriteGatedThread(
6891
7501
  thread,
6892
7502
  thread.worktreePath ? threadsSharingWorktree(thread.worktreePath) : []
@@ -6960,16 +7570,15 @@ var Orchestrator = class {
6960
7570
  fresh.worktreePath ? threadsSharingWorktree(fresh.worktreePath) : []
6961
7571
  ) ? null : formatReviewWriteGateDirective();
6962
7572
  let viewerContextDirective = null;
7573
+ let envDirective = null;
6963
7574
  if (!isBrightsy && !isOrchestration) {
7575
+ const repo = fresh.repoPath || fresh.worktreePath;
6964
7576
  try {
6965
- viewerContextDirective = formatViewerContextDirective(
6966
- resolveViewerProfileForRepo(fresh.repoPath || fresh.worktreePath)
6967
- );
7577
+ viewerContextDirective = formatViewerContextDirective(resolveViewerProfileForRepo(repo));
6968
7578
  } catch {
6969
- viewerContextDirective = formatViewerContextDirective(
6970
- resolveViewerProfileForRepo()
6971
- );
7579
+ viewerContextDirective = formatViewerContextDirective(resolveViewerProfileForRepo());
6972
7580
  }
7581
+ envDirective = formatEnvDirective(projectNameForRepoPath(repo));
6973
7582
  }
6974
7583
  const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
6975
7584
  let renameBranchDirective = null;
@@ -7011,6 +7620,7 @@ var Orchestrator = class {
7011
7620
  issueToolsDirective,
7012
7621
  reviewWriteGateDirective,
7013
7622
  viewerContextDirective,
7623
+ envDirective,
7014
7624
  artifactDirective,
7015
7625
  longRunningDirective,
7016
7626
  renameBranchDirective,
@@ -7140,6 +7750,7 @@ var Orchestrator = class {
7140
7750
  issueToolsDirective,
7141
7751
  reviewWriteGateDirective,
7142
7752
  viewerContextDirective,
7753
+ envDirective,
7143
7754
  artifactDirective,
7144
7755
  longRunningDirective,
7145
7756
  renameBranchDirective,
@@ -7225,7 +7836,7 @@ var Orchestrator = class {
7225
7836
  }
7226
7837
  settleAgentBlock(threadId, parts, exitCode !== 0 && !this.stoppedTurns.has(threadId));
7227
7838
  if (this.stoppedTurns.has(threadId)) {
7228
- const stopped = writeLiveStatus(threadId, "stopped");
7839
+ const stopped = writeStoppedStatus(threadId);
7229
7840
  if (stopped?.status === "stopped") {
7230
7841
  this.emit({ type: "status_changed", threadId, status: "stopped" });
7231
7842
  }
@@ -7276,7 +7887,7 @@ var Orchestrator = class {
7276
7887
  await syncThreadBranchFromGit(threadId).catch(() => void 0);
7277
7888
  settleAgentBlock(threadId, void 0, !this.stoppedTurns.has(threadId));
7278
7889
  if (this.stoppedTurns.has(threadId)) {
7279
- const stopped = writeLiveStatus(threadId, "stopped");
7890
+ const stopped = writeStoppedStatus(threadId);
7280
7891
  if (stopped?.status === "stopped") {
7281
7892
  this.emit({ type: "status_changed", threadId, status: "stopped" });
7282
7893
  }
@@ -7357,7 +7968,7 @@ var Orchestrator = class {
7357
7968
  } catch {
7358
7969
  }
7359
7970
  }
7360
- const stopped = writeLiveStatus(thread.id, "stopped") ?? readThread(thread.id) ?? thread;
7971
+ const stopped = writeStoppedStatus(thread.id, resolveStopReason(opts)) ?? readThread(thread.id) ?? thread;
7361
7972
  if (stopped.status === "stopped") {
7362
7973
  this.emit({ type: "status_changed", threadId: thread.id, status: "stopped" });
7363
7974
  if (inFlight && opts?.notifyParent !== false) {
@@ -7536,7 +8147,7 @@ var Orchestrator = class {
7536
8147
  const request = sameIntent ? pending : {
7537
8148
  op,
7538
8149
  scriptName: scriptName ?? null,
7539
- requestId: randomUUID3(),
8150
+ requestId: randomUUID4(),
7540
8151
  requestedAt: (/* @__PURE__ */ new Date()).toISOString()
7541
8152
  };
7542
8153
  if (!sameIntent) {
@@ -7981,7 +8592,7 @@ var Orchestrator = class {
7981
8592
  const stillRunning = this.threadLooksLive(thread);
7982
8593
  const rawText = (lastAgent?.text ?? "").trim();
7983
8594
  const finishedText = (rawText && !isInternalAgentStatusText(rawText) ? rawText : "") || (thread.status === "error" || thread.status === "stopped" || thread.status === "broken" ? lastError ?? "" : "");
7984
- const text = stillRunning ? "" : finishedText;
8595
+ const text3 = stillRunning ? "" : finishedText;
7985
8596
  const live = stillRunning ? readTurnLive(thread.id) : null;
7986
8597
  const liveSummary = live?.summary && !isInternalAgentStatusText(live.summary) ? live.summary : null;
7987
8598
  const queuedHint = stillRunning && thread.status === "queued" && !liveSummary ? "Queued \u2014 waiting for a concurrency slot" : null;
@@ -7992,7 +8603,7 @@ var Orchestrator = class {
7992
8603
  });
7993
8604
  const block = thread.agentBlock?.reason?.trim() ? thread.agentBlock : null;
7994
8605
  return {
7995
- text,
8606
+ text: text3,
7996
8607
  status: thread.status,
7997
8608
  taskState,
7998
8609
  sessionId: thread.sessionId,
@@ -8067,12 +8678,12 @@ var Orchestrator = class {
8067
8678
  return "missing";
8068
8679
  }
8069
8680
  }
8070
- async readFile(threadRef, relativePath) {
8681
+ async readFile(threadRef, relativePath, tail) {
8071
8682
  const thread = this.requireThread(threadRef);
8072
8683
  if (relativePath.includes("..") || relativePath.startsWith("/")) {
8073
8684
  throw new Error("Invalid path");
8074
8685
  }
8075
- return readWorktreeFile(thread.worktreePath, relativePath);
8686
+ return (tail ? readWorktreeFileTail : readWorktreeFile)(thread.worktreePath, relativePath);
8076
8687
  }
8077
8688
  async readFileForUpload(threadRef, relativePath) {
8078
8689
  const thread = this.requireThread(threadRef);
@@ -8367,7 +8978,7 @@ var Orchestrator = class {
8367
8978
  return tab;
8368
8979
  }
8369
8980
  /**
8370
- * Desktop git buttons + MCP `ask_git`. Always queues the worktree agent
8981
+ * Desktop git buttons + MCP `ask_git`. Always steers the worktree agent
8371
8982
  * with the action prompt (same path as Resolve). Repository `[prompts]`
8372
8983
  * overrides apply when set.
8373
8984
  */
@@ -8451,6 +9062,9 @@ var Orchestrator = class {
8451
9062
  if (!next) throw new Error("Title cannot be empty");
8452
9063
  return updateThread(thread.id, { title: next, userSetTitle: true });
8453
9064
  }
9065
+ setWorkspaceTags(threadRef, tags) {
9066
+ return applyWorkspaceTags(threadRef, tags, "replace");
9067
+ }
8454
9068
  async switchThreadBranch(threadRef, branchName) {
8455
9069
  const thread = this.requireThread(threadRef);
8456
9070
  this.assertNotGlobal(thread, "Switch branch");
@@ -8532,11 +9146,7 @@ var Orchestrator = class {
8532
9146
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
8533
9147
  this.enforceHistoryRetention();
8534
9148
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
8535
- try {
8536
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-UP6DQ3WL.js");
8537
- await ensureWorkspace2(thread.repoPath);
8538
- } catch {
8539
- }
9149
+ await ensureWorkspace(thread.repoPath).catch(() => void 0);
8540
9150
  }
8541
9151
  return archived;
8542
9152
  }
@@ -8587,16 +9197,12 @@ var Orchestrator = class {
8587
9197
  `Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
8588
9198
  );
8589
9199
  }
8590
- const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-Q6UJHG25.js");
8591
- const slug = thread.worktreePath.split("/").pop();
8592
9200
  const dest = thread.worktreePath;
8593
9201
  await withRepoGitLock(thread.repoPath, async () => {
8594
9202
  await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
8595
9203
  });
8596
9204
  const { ensureWorktreeSideboardIgnored } = await import("./worktree-exclude-RXZPTCNH.js");
8597
9205
  await ensureWorktreeSideboardIgnored(dest);
8598
- void createThreadWorktree2;
8599
- void slug;
8600
9206
  }
8601
9207
  const restorePatch = {};
8602
9208
  let alreadyMerged = normalizePrState(thread.prState) === "MERGED";
@@ -8620,6 +9226,9 @@ var Orchestrator = class {
8620
9226
  if (Object.keys(restorePatch).length > 0) {
8621
9227
  updateThread(thread.id, restorePatch);
8622
9228
  }
9229
+ if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
9230
+ await addWorkspace(thread.repoPath).catch(() => void 0);
9231
+ }
8623
9232
  const restored = setStatus(thread.id, "idle");
8624
9233
  this.emit({ type: "status_changed", threadId: restored.id, status: restored.status });
8625
9234
  return restored;
@@ -8697,7 +9306,7 @@ async function startOrchestration(opts) {
8697
9306
  sourceRef: "default",
8698
9307
  ...createOpts
8699
9308
  }).catch(async () => {
8700
- const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-Q6UJHG25.js");
9309
+ const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-YTZC3UQN.js");
8701
9310
  const repo = await resolveRepoRoot2(repoPath);
8702
9311
  const def = await resolveDefaultBranch2(repo);
8703
9312
  return orch.createThread({
@@ -8707,7 +9316,7 @@ async function startOrchestration(opts) {
8707
9316
  repoPath: repo
8708
9317
  });
8709
9318
  });
8710
- const { updateThread: upd } = await import("./thread-store-SHCUBWB3.js");
9319
+ const { updateThread: upd } = await import("./thread-store-ZQZDREVH.js");
8711
9320
  const updated = upd(thread.id, {
8712
9321
  sourceType: "orchestration",
8713
9322
  sourceRef: goal
@@ -8805,6 +9414,8 @@ export {
8805
9414
  stampNotifyBlock,
8806
9415
  detectAgents,
8807
9416
  requireAgent,
9417
+ workspaceTagTargetRef,
9418
+ applyWorkspaceTags,
8808
9419
  createThread,
8809
9420
  listLinearIssues,
8810
9421
  IMPLIED_TICKET_RESOLVE_PROMPT,
@@ -8812,6 +9423,7 @@ export {
8812
9423
  isCowboyThread,
8813
9424
  isPrimaryCheckoutThread,
8814
9425
  shouldRemoveWorktreeOnTeardown,
9426
+ readTurnLiveParts,
8815
9427
  readTurnLive,
8816
9428
  shouldStampSetupLastError,
8817
9429
  isSetupLastError,
@@ -8892,6 +9504,16 @@ export {
8892
9504
  connectedOptionalServices,
8893
9505
  formatOptionalServicesDirective,
8894
9506
  formatOptionalServicesReminder,
9507
+ parseIssueSince,
9508
+ formatGitHubSearchUpdatedSince,
9509
+ issueMatchesUpdatedSince,
9510
+ previewIssueCommentBody,
9511
+ clampMcpIssueLimit,
9512
+ applyIssueListWindow,
9513
+ mcpIssueUpdatedSinceSchema,
9514
+ formatListedIssuesForMcp,
9515
+ mcpJson,
9516
+ registerViewerContextTools,
8895
9517
  LIVE_TURN_SPAWN_GRACE_MS,
8896
9518
  RUN_SCRIPT_DESKTOP_ADOPT_MS,
8897
9519
  isPidAlive,