frizz 0.3.0 → 0.5.0

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 (98) hide show
  1. package/README.md +26 -4
  2. package/dist/claude-agent-broker.js +9 -4
  3. package/dist/dev-child.js +1868 -529
  4. package/dist/frizz.js +814 -234
  5. package/package.json +5 -3
  6. package/runtime/cc-worker/DECISIONS.md +17 -0
  7. package/runtime/cc-worker/bin/frizz-mcp.mjs +159 -174
  8. package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
  9. package/web-dist/assets/{TerminalPane-CTdetDJJ.js → TerminalPane-WcpgmCA-.js} +1 -1
  10. package/web-dist/assets/{abnfDiagram-VRR7QNED-BDKrjCMs.js → abnfDiagram-VRR7QNED-Caecklbz.js} +1 -1
  11. package/web-dist/assets/architecture-TIHT7OUA-BTy2nvYL.js +1 -0
  12. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-BJebpUUM.js → architectureDiagram-ZJ3FMSHR-BEk0fg8C.js} +1 -1
  13. package/web-dist/assets/{blockDiagram-677ZJIJ3-BNbuk25k.js → blockDiagram-677ZJIJ3-B4yq3xiy.js} +1 -1
  14. package/web-dist/assets/{c4Diagram-LMCZKHZV-lCfyotdU.js → c4Diagram-LMCZKHZV-DaA5NBkz.js} +1 -1
  15. package/web-dist/assets/channel-WghlT4VS.js +1 -0
  16. package/web-dist/assets/{chunk-32BRIVSS-DP72SEkr.js → chunk-32BRIVSS-2XsSs8F_.js} +1 -1
  17. package/web-dist/assets/{chunk-52WLFC77-DxO3gN-p.js → chunk-52WLFC77-DWJfECP3.js} +1 -1
  18. package/web-dist/assets/{chunk-C7G6YPKG-BXFx9Vlr.js → chunk-C7G6YPKG-Bz07eaGt.js} +1 -1
  19. package/web-dist/assets/{chunk-EX3LRPZG-CR3sHpPf.js → chunk-EX3LRPZG-D78kZEMM.js} +1 -1
  20. package/web-dist/assets/{chunk-FWX5IMBZ-CJ8L__oG.js → chunk-FWX5IMBZ-CeCa17CW.js} +2 -2
  21. package/web-dist/assets/{chunk-HOUHSVGY-BCl3JSWr.js → chunk-HOUHSVGY-9H4GFuBa.js} +1 -1
  22. package/web-dist/assets/{chunk-ICXQ74PX-CZxFMci1.js → chunk-ICXQ74PX--Aa17r4X.js} +1 -1
  23. package/web-dist/assets/{chunk-MOJQB5TN-Ck3_47dB.js → chunk-MOJQB5TN-C3vXhsPV.js} +1 -1
  24. package/web-dist/assets/{chunk-OGEWGWER-CVXjES-F.js → chunk-OGEWGWER-G4kZRBkX.js} +1 -1
  25. package/web-dist/assets/{chunk-PUDLZKDR-JjrVgaR7.js → chunk-PUDLZKDR-DWd5kvyM.js} +1 -1
  26. package/web-dist/assets/{chunk-Q4XR5HBZ-C5YMRAQ0.js → chunk-Q4XR5HBZ-u55p39Up.js} +1 -1
  27. package/web-dist/assets/{chunk-V7JOEXUC-DK0mfUHU.js → chunk-V7JOEXUC-BU4YBmV4.js} +1 -1
  28. package/web-dist/assets/{chunk-VAUOI2AC-BKBntpid.js → chunk-VAUOI2AC-VXcmT20q.js} +1 -1
  29. package/web-dist/assets/{chunk-VR4S4FIN-zgE1dG6U.js → chunk-VR4S4FIN-CSjEXLOC.js} +1 -1
  30. package/web-dist/assets/{chunk-WYO6CB5R-CZhh1IBq.js → chunk-WYO6CB5R-4WGk2dpL.js} +1 -1
  31. package/web-dist/assets/{chunk-ZGVPDNZ5-CrNjIEem.js → chunk-ZGVPDNZ5-D5Xl8ZV6.js} +1 -1
  32. package/web-dist/assets/classDiagram-OUVF2IWQ-D21-Dy4r.js +1 -0
  33. package/web-dist/assets/classDiagram-v2-EOCWNBFH-D21-Dy4r.js +1 -0
  34. package/web-dist/assets/{cynefin-VYW2F7L2-Ca_BPfTG.js → cynefin-VYW2F7L2-O6wB49wK.js} +1 -1
  35. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-CsINQf6g.js → cynefinDiagram-TSTJHNR4-pd6ZogNC.js} +1 -1
  36. package/web-dist/assets/{dagre-VKFMJZFB-CE0DYMFy.js → dagre-VKFMJZFB-D8W1S6fw.js} +1 -1
  37. package/web-dist/assets/{diagram-FQU43EPY-BqTduEYE.js → diagram-FQU43EPY-Bm1NOkHd.js} +1 -1
  38. package/web-dist/assets/{diagram-G47NLZAW-Cho718s6.js → diagram-G47NLZAW-CjwnXTIc.js} +1 -1
  39. package/web-dist/assets/{diagram-NH7WQ7WH-D0Z8t9UC.js → diagram-NH7WQ7WH-DH1jZ_mu.js} +1 -1
  40. package/web-dist/assets/{diagram-OA4YK3LP-CBacyeDx.js → diagram-OA4YK3LP-B-s969Q_.js} +1 -1
  41. package/web-dist/assets/{diagram-WEI45ONY-CozpXa4j.js → diagram-WEI45ONY-BU3lv6cL.js} +1 -1
  42. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-ba4NbQQo.js → ebnfDiagram-CCIWWBDH-FsAeyqcC.js} +1 -1
  43. package/web-dist/assets/{erDiagram-Q63AITRT-B3BciOYa.js → erDiagram-Q63AITRT-BRvg5Y9d.js} +1 -1
  44. package/web-dist/assets/eventmodeling-45OFAUF4-RhX2WA7g.js +1 -0
  45. package/web-dist/assets/flowDiagram-23GEKE2U-C7N14tdV.js +1 -0
  46. package/web-dist/assets/{ganttDiagram-NO4QXBWP-lPmrRit4.js → ganttDiagram-NO4QXBWP-eATKNh9U.js} +1 -1
  47. package/web-dist/assets/{gitGraph-TEB2WS4Q-BjyclOq0.js → gitGraph-TEB2WS4Q-CNYqsua8.js} +1 -1
  48. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-pzeb4Yrw.js → gitGraphDiagram-IHSO6WYX-BUxgluf8.js} +1 -1
  49. package/web-dist/assets/index-BFuReKr0.js +368 -0
  50. package/web-dist/assets/index-BH9U3Zlg.css +1 -0
  51. package/web-dist/assets/{info-DKCQHKI2-Bwycegvf.js → info-DKCQHKI2-HsVLwo1c.js} +1 -1
  52. package/web-dist/assets/{infoDiagram-FWYZ7A6U-BCvRGj_5.js → infoDiagram-FWYZ7A6U-D7U65xDp.js} +1 -1
  53. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js → ishikawaDiagram-FXEZZL3T-BsfIYXjX.js} +1 -1
  54. package/web-dist/assets/{journeyDiagram-5HDEW3XC-C5ROwFio.js → journeyDiagram-5HDEW3XC-CpkREKol.js} +1 -1
  55. package/web-dist/assets/{kanban-definition-HUTT4EX6-YLPLkpeT.js → kanban-definition-HUTT4EX6-2M3KW6Gw.js} +1 -1
  56. package/web-dist/assets/{line-KtkNqRgI.js → line-Cs7zWs24.js} +1 -1
  57. package/web-dist/assets/{mermaid-parser.core-D_FfqBe7.js → mermaid-parser.core-Dw-IECV5.js} +3 -3
  58. package/web-dist/assets/{mermaid.core-Ffv8anVf.js → mermaid.core-C1lrddjT.js} +3 -3
  59. package/web-dist/assets/{mindmap-definition-LN4V7U3C-B2jj4vfm.js → mindmap-definition-LN4V7U3C-gVS5On1g.js} +1 -1
  60. package/web-dist/assets/{packet-7NZHBO7P-BjmWHwra.js → packet-7NZHBO7P-Tti1-UWA.js} +1 -1
  61. package/web-dist/assets/{pegDiagram-2B236MQR-Dq3iJDyq.js → pegDiagram-2B236MQR-DE5in93G.js} +1 -1
  62. package/web-dist/assets/{pie-RZYD4A2V-jBbH1lv9.js → pie-RZYD4A2V-DfLNkvHl.js} +1 -1
  63. package/web-dist/assets/{pieDiagram-ENE6RG2P-C3ETW9lq.js → pieDiagram-ENE6RG2P-DcqSqJqb.js} +1 -1
  64. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DItSmme7.js → quadrantDiagram-ABIIQ3AL-DR-nRPgN.js} +1 -1
  65. package/web-dist/assets/{radar-I7S5WNFK-6ey6crgP.js → radar-I7S5WNFK-DGLDBfPd.js} +1 -1
  66. package/web-dist/assets/{railroad-3IZDKUUU-Cii-Mn0E.js → railroad-3IZDKUUU-BaiGEHlV.js} +1 -1
  67. package/web-dist/assets/railroad-abnf-AHOZXSZD-BCeoIOWA.js +1 -0
  68. package/web-dist/assets/railroad-ebnf-EBAXGLYW-lZpXJLD8.js +1 -0
  69. package/web-dist/assets/railroad-peg-LSFZ7HO6-CaUBrHTL.js +1 -0
  70. package/web-dist/assets/{railroadDiagram-RFXS5EU6-M363ils_.js → railroadDiagram-RFXS5EU6-82aEev5_.js} +1 -1
  71. package/web-dist/assets/{requirementDiagram-TGXJPOKE-CTs2_V6T.js → requirementDiagram-TGXJPOKE-Bs8opBRj.js} +1 -1
  72. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-QTLLcDPD.js → sankeyDiagram-HTMAVEWB-gOb3FIr8.js} +1 -1
  73. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js → sequenceDiagram-DBY2YBRQ-6DvjtRHR.js} +1 -1
  74. package/web-dist/assets/{stateDiagram-2N3HPSRC-BecB6roG.js → stateDiagram-2N3HPSRC-Cg2LWdUf.js} +1 -1
  75. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-Digf9CWZ.js +1 -0
  76. package/web-dist/assets/{swimlanes-5IMT3BWC-COiYgS0w.js → swimlanes-5IMT3BWC-BDpPdYEl.js} +1 -1
  77. package/web-dist/assets/swimlanesDiagram-G3AALYLV-DEZRDWHA.js +8 -0
  78. package/web-dist/assets/{timeline-definition-FHXFAJF6-Ci22coeH.js → timeline-definition-FHXFAJF6-BSjfeutv.js} +1 -1
  79. package/web-dist/assets/{treeView-QDETBFTQ-4DW35czh.js → treeView-QDETBFTQ-DwgPy6V3.js} +1 -1
  80. package/web-dist/assets/{treemap-6X3UGDF4-Dq_-Z6-Y.js → treemap-6X3UGDF4-7eiifTKW.js} +1 -1
  81. package/web-dist/assets/{vennDiagram-L72KCM5P-CHQSIEYq.js → vennDiagram-L72KCM5P-9u4AhcnV.js} +1 -1
  82. package/web-dist/assets/{wardley-OPB4EBWU-Bwlh7HCY.js → wardley-OPB4EBWU-DqNb5x9U.js} +1 -1
  83. package/web-dist/assets/{wardleyDiagram-EHGQE667-DctjuPYn.js → wardleyDiagram-EHGQE667-DlJ7h3vg.js} +1 -1
  84. package/web-dist/assets/{xychartDiagram-FW5EYKEG-ZoIGosv8.js → xychartDiagram-FW5EYKEG-B_9ukg2S.js} +1 -1
  85. package/web-dist/index.html +2 -2
  86. package/web-dist/assets/architecture-TIHT7OUA-ChUMo004.js +0 -1
  87. package/web-dist/assets/channel-pr7r6raB.js +0 -1
  88. package/web-dist/assets/classDiagram-OUVF2IWQ-CvGbPMn_.js +0 -1
  89. package/web-dist/assets/classDiagram-v2-EOCWNBFH-CvGbPMn_.js +0 -1
  90. package/web-dist/assets/eventmodeling-45OFAUF4-DKmyo-jd.js +0 -1
  91. package/web-dist/assets/flowDiagram-23GEKE2U-DluCBvT4.js +0 -1
  92. package/web-dist/assets/index-BQtjYMpV.css +0 -1
  93. package/web-dist/assets/index-CLW1Q49U.js +0 -360
  94. package/web-dist/assets/railroad-abnf-AHOZXSZD-U_vb4BrX.js +0 -1
  95. package/web-dist/assets/railroad-ebnf-EBAXGLYW-BIHG7gNU.js +0 -1
  96. package/web-dist/assets/railroad-peg-LSFZ7HO6-Cpd9r-tB.js +0 -1
  97. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-JXw9T96l.js +0 -1
  98. package/web-dist/assets/swimlanesDiagram-G3AALYLV--1Wv7FQQ.js +0 -8
package/dist/dev-child.js CHANGED
@@ -5541,13 +5541,27 @@ function isAllowedAttachmentName(name) {
5541
5541
  function isDirectSubAgent(agent) {
5542
5542
  return (agent.depth ?? 1) === 1;
5543
5543
  }
5544
- function isValidAwaitingTimer(value) {
5545
- const s = value.trim();
5546
- return AWAITING_TIMER_RE.test(s) && Number.isFinite(Date.parse(s));
5544
+ function retiredAwaitingKindsIn(body) {
5545
+ if (!body || !RETIRED_LINE_RE.test(body)) return [];
5546
+ const found = [];
5547
+ for (const line of body.split("\n")) {
5548
+ const m = /^\s*([a-z-]+):\s*\S/i.exec(line);
5549
+ const kind = m?.[1].toLowerCase();
5550
+ if (!kind) continue;
5551
+ const hit = RETIRED_AWAITING_KINDS.find((k) => k === kind);
5552
+ if (hit && !found.includes(hit)) found.push(hit);
5553
+ }
5554
+ return found;
5547
5555
  }
5548
- function canonicalSnoozeInstant(value) {
5549
- if (!isValidAwaitingTimer(value)) return null;
5550
- return new Date(Date.parse(value.trim())).toISOString();
5556
+ function isAwaitingItemKind(kind) {
5557
+ return AWAITING_ITEM_KINDS.includes(kind);
5558
+ }
5559
+ function parseAwaitingDuration(value) {
5560
+ const m = AWAITING_DURATION_RE.exec(value.trim());
5561
+ if (!m) return null;
5562
+ const ms = Number(m[1]) * DURATION_UNIT_MS[m[2]];
5563
+ if (!Number.isFinite(ms) || ms <= 0) return null;
5564
+ return Math.min(ms, AWAITING_FOR_MAX_MS);
5551
5565
  }
5552
5566
  function saysAllDone(text) {
5553
5567
  if (typeof text !== "string") return false;
@@ -5557,10 +5571,11 @@ function saysAllDone(text) {
5557
5571
  }
5558
5572
  return false;
5559
5573
  }
5560
- function restPromptMessage(prompt) {
5574
+ function restPromptMessage(prompt, opts = {}) {
5575
+ const note = opts.overQuestion ? `${OVER_QUESTION_NOTE} ${OPT_OUT_NOTE}` : OPT_OUT_NOTE;
5561
5576
  return `${prompt.trim()}
5562
5577
 
5563
- (Goal \u2014 sent each time you come to rest. ${OPT_OUT_NOTE})`;
5578
+ (Goal \u2014 sent each time you come to rest. ${note})`;
5564
5579
  }
5565
5580
  function compactionPromptMessage(prompt) {
5566
5581
  return `${prompt.trim()}
@@ -5580,11 +5595,55 @@ function formatIntervalLabel(seconds) {
5580
5595
  const hours = minutes / 60;
5581
5596
  return `${Number.isInteger(hours) ? hours : hours.toFixed(1)} hr`;
5582
5597
  }
5583
- function watchWakeMessage(kind, target, detail) {
5584
- const what = kind === "shell" ? `your background shell \`${target}\`` : `${target}`;
5585
- return `\u23F0 ${detail} (${what}).
5598
+ function prWatchWakeMessage(input) {
5599
+ const lines = [];
5600
+ if (input.merged || input.closed) {
5601
+ lines.push(`\u23F0 ${input.target} was ${input.merged ? "MERGED" : "CLOSED"}.`, "");
5602
+ lines.push("(This watcher is spent \u2014 there is nothing further to report on a finished PR.)");
5603
+ return lines.join("\n");
5604
+ }
5605
+ if (input.checks) {
5606
+ const c = input.checks;
5607
+ lines.push(
5608
+ c.verdict === "passing" ? `\u2705 CI PASSED on ${input.target} \u2014 ${c.passed} check${c.passed === 1 ? "" : "s"} green.` : `\u274C CI FAILED on ${input.target}${c.failing.length ? `: ${c.failing.join(", ")}` : ""}.`
5609
+ );
5610
+ }
5611
+ if (input.review) {
5612
+ if (lines.length) lines.push("");
5613
+ lines.push(input.review);
5614
+ }
5615
+ lines.push("", "(Registered PR watcher \u2014 STILL ARMED. It reports again on the next CI change, review or comment. Drop it with `mcp__frizz__watch_pr` when it stops mattering.)");
5616
+ return lines.join("\n");
5617
+ }
5618
+ function shellDoneMessage(shell) {
5619
+ const what = shell.taskId ? `\`${shell.taskId}\` \u2014 ${shell.label}` : shell.label;
5620
+ const verb = shell.status === "failed" ? "FAILED" : shell.status === "killed" ? "was STOPPED" : "finished";
5621
+ return `\u23F0 Your background shell ${verb}: ${what}.
5586
5622
 
5587
- (Registered watcher \u2014 it has fired and is no longer armed. Register another with \`mcp__frizz__watch\` if you need to keep waiting.)`;
5623
+ (Frizz sends this because it finished after you came to rest, where your runtime's own completion notification does not reach you. Read its output if you still need it.)`;
5624
+ }
5625
+ function liveOpsLines(ops) {
5626
+ const lines = [];
5627
+ const section = (heading, kind, items) => {
5628
+ if (!items.length) return;
5629
+ lines.push("", heading);
5630
+ for (const i of items) lines.push(`- \`${kind}: ${i.id ?? "?"}\` \u2014 ${i.label}`);
5631
+ };
5632
+ section("Background shells still running:", "shell", ops?.shells ?? []);
5633
+ section("Sub-agents still running (they re-invoke you on their own, so parking on one is optional):", "agent", ops?.subAgents ?? []);
5634
+ section("Timers you have armed:", "timer", ops?.timers ?? []);
5635
+ section("Pull requests you registered:", "pr", ops?.prs ?? []);
5636
+ return lines;
5637
+ }
5638
+ function signoffNudgeMessage(ops) {
5639
+ const lines = liveOpsLines(ops);
5640
+ if (lines.length) {
5641
+ lines.push("", "An ```awaiting fence takes one such line per thing you are ACTUALLY waiting on, plus a");
5642
+ lines.push("required `for:` duration (`30s`/`15m`/`2h`/`3d`) and a one-line `reason:`. Frizz checks every");
5643
+ lines.push("id: name something that is not running and you are bumped rather than parked.");
5644
+ }
5645
+ return lines.length === 0 ? SIGNOFF_NUDGE_MESSAGE : `${SIGNOFF_NUDGE_MESSAGE}
5646
+ ${lines.join("\n")}`;
5588
5647
  }
5589
5648
  function timerPromptMessage(prompt, fireAt) {
5590
5649
  return `${prompt.trim()}
@@ -5692,7 +5751,7 @@ function parseGithubWakeSteer(text) {
5692
5751
  function frizzRoute(path) {
5693
5752
  return `${FRIZZ_ROUTE_PREFIX}${path.startsWith("/") ? path : `/${path}`}`;
5694
5753
  }
5695
- var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, AWAITING_TIMER_RE, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, DEFAULT_GOAL_TRIGGERS, DEFAULT_RECURRING_PROMPT, RecurringIntervalSeconds, ThreadRecurringPrompt, ALLDONE_SENTINEL, OPT_OUT_NOTE, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TIMER_MAX_ARMED, WATCH_MAX_ARMED, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, ThreadWatchKind, RegisterableThreadWatchKind, ThreadWatchTarget, ThreadWatchView, ThreadTimerView, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, AddOwnThreadWatchInput, PromoteOwnThreadWatchInput, PromoteOwnThreadWatchResult, DropOwnThreadWatchInput, ListOwnThreadWatchesInput, OwnThreadWatchesResult, AddOwnThreadWatchResult, DropOwnThreadWatchResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_DELIVERY_TOKEN_TAIL, WAKE_DELIVERY_TOKEN_LINE, AGENT_MESSAGE_WRAPPER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_BACKLOG_TAIL, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, WAKE_ITEM, WAKE_MORE, GithubStatus, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, ProjectCard, PROJECT_ICON_EXTENSIONS, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT, threadIdentityName;
5754
+ var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, RETIRED_AWAITING_REPLACEMENT, AWAITING_ITEM_KINDS, AWAITING_DURATION_RE, DURATION_UNIT_MS, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, ALLDONE_SENTINEL, OPT_OUT_NOTE, OVER_QUESTION_NOTE, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TIMER_MAX_ARMED, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, ThreadWatchView, PR_WATCH_DEFAULT_FOR_MS, PR_WATCH_MAX_ARMED, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_DELIVERY_TOKEN_TAIL, WAKE_DELIVERY_TOKEN_LINE, AGENT_MESSAGE_WRAPPER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_BACKLOG_TAIL, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, WAKE_ITEM, WAKE_MORE, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, ProjectCard, PROJECT_ICON_EXTENSIONS, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT, threadIdentityName;
5696
5755
  var init_src = __esm({
5697
5756
  "packages/shared/src/index.ts"() {
5698
5757
  "use strict";
@@ -5904,7 +5963,15 @@ var init_src = __esm({
5904
5963
  command: external_exports.string().optional(),
5905
5964
  // ISO8601 of the shell output file's last write — "last active 6 min ago" for a quiet-but-live
5906
5965
  // watcher. Optional (see SubAgentView.lastActivityAt).
5907
- lastActivityAt: external_exports.string().optional()
5966
+ lastActivityAt: external_exports.string().optional(),
5967
+ // The PROVIDER's session-wide background-task handle (`bzvtnt3ig`), as distinct from `id`, which is
5968
+ // the launch tool_use id. Both name the same shell and neither is a substitute for the other:
5969
+ // `id` is what the two copies of a row reconcile on, and this is the handle the runtime hands the
5970
+ // MODEL — "Command running in background with ID: bzvtnt3ig" is the only id a worker ever sees, so
5971
+ // it is the one it registers a `shell` watcher against. Matching on `id`/`label` alone meant every
5972
+ // such watcher was unfireable (scheduler.evalWatchers, 2026-08-14). Absent for a CODEX row, whose
5973
+ // single `processId` IS its `id`, and for a Claude row between its tool_use and its launch ack.
5974
+ taskId: external_exports.string().optional()
5908
5975
  });
5909
5976
  CompletionHoldOp = external_exports.object({
5910
5977
  label: external_exports.string(),
@@ -5941,10 +6008,22 @@ var init_src = __esm({
5941
6008
  title: external_exports.string().max(120)
5942
6009
  });
5943
6010
  AwaitingHint = external_exports.object({
5944
- kind: external_exports.enum(["pr-watch", "human", "timer", "pr", "ci", "session"]),
6011
+ kind: external_exports.enum(["shell", "agent", "timer", "pr", "for", "reason"]),
5945
6012
  value: external_exports.string()
5946
6013
  });
5947
- AWAITING_TIMER_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/;
6014
+ RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session"];
6015
+ RETIRED_LINE_RE = new RegExp(`^\\s*(${RETIRED_AWAITING_KINDS.join("|")}):\\s*\\S`, "im");
6016
+ RETIRED_AWAITING_REPLACEMENT = {
6017
+ "watch": "`shell: <the id your runtime gave you>` (or `agent: <id>`) \u2014 the same id, on the current line kind",
6018
+ "pr-watch": "register the PR with `mcp__frizz__watch_pr`, then name it `pr: owner/repo#123`",
6019
+ "human": "there is no human gate any more \u2014 if you need a person, ask a ```question instead of parking",
6020
+ "ci": "CI is not a wait of its own: register the PR with `mcp__frizz__watch_pr` and you are woken when its checks settle",
6021
+ "session": "there is no cross-session wait \u2014 name the sub-agent you dispatched with `agent: <id>`"
6022
+ };
6023
+ AWAITING_ITEM_KINDS = ["shell", "agent", "timer", "pr"];
6024
+ AWAITING_DURATION_RE = /^(\d{1,5})(s|m|h|d)$/;
6025
+ DURATION_UNIT_MS = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
6026
+ AWAITING_FOR_MAX_MS = 24 * 60 * 60 * 1e3;
5948
6027
  SnoozeUntil = external_exports.string().regex(
5949
6028
  /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
5950
6029
  "Snooze time must be an ISO-8601 UTC instant"
@@ -5958,13 +6037,6 @@ var init_src = __esm({
5958
6037
  RECURRING_MIN_INTERVAL_SECONDS = 60;
5959
6038
  RECURRING_MAX_INTERVAL_SECONDS = 24 * 60 * 60;
5960
6039
  RecurringPromptText = external_exports.string().trim().min(1).max(RECURRING_PROMPT_MAX);
5961
- DEFAULT_GOAL_TRIGGERS = {
5962
- stopHook: true,
5963
- heartbeat: false,
5964
- postCompaction: false,
5965
- pauseOnQuestions: true
5966
- };
5967
- DEFAULT_RECURRING_PROMPT = "If further work towards the original task/goal remains, keep going. If there are open questions that require human input, ask them with question fences.";
5968
6040
  RecurringIntervalSeconds = external_exports.number().int().min(RECURRING_MIN_INTERVAL_SECONDS).max(RECURRING_MAX_INTERVAL_SECONDS);
5969
6041
  ThreadRecurringPrompt = external_exports.object({
5970
6042
  prompt: external_exports.string(),
@@ -5974,12 +6046,6 @@ var init_src = __esm({
5974
6046
  stopHook: external_exports.boolean(),
5975
6047
  heartbeat: external_exports.boolean(),
5976
6048
  postCompaction: external_exports.boolean(),
5977
- /** NOT a fourth trigger — a HOLD over all three. While it is on, nothing is sent for as long as the
5978
- * thread is blocked on the human: an unanswered ```question fence, a native ask, or a permission
5979
- * prompt. The footer panel seeds it ON for a fresh arming, with the stop hook, because they are one
5980
- * intent — see scheduler.ts, "WHAT A PENDING QUESTION DOES TO ALL THREE TRIGGERS". The stored column
5981
- * still defaults OFF, so an existing row and an older caller both keep the behaviour they had. */
5982
- pauseOnQuestions: external_exports.boolean(),
5983
6049
  intervalSeconds: external_exports.number().int().positive().optional(),
5984
6050
  armedAt: external_exports.string(),
5985
6051
  /** Last delivery per trigger; stamped separately so each reads its own clock. */
@@ -5989,40 +6055,164 @@ var init_src = __esm({
5989
6055
  }).strict();
5990
6056
  ALLDONE_SENTINEL = "ALLDONE";
5991
6057
  OPT_OUT_NOTE = "To stop these, sign off with a ```done fence \u2014 but ONLY when the work is genuinely finished: it files this thread away, and nothing but new work from the human reopens it.";
6058
+ OVER_QUESTION_NOTE = "Your ```question is still unanswered, and a Goal armed at rest means the operator is not waiting to answer it: decide it yourself, say in one line which way you went and what would reverse it, and carry on. Do NOT re-ask it.";
5992
6059
  TIMER_PROMPT_MAX = SNOOZE_PROMPT_MAX;
5993
6060
  TIMER_MAX_DELAY_SECONDS = 30 * 24 * 60 * 60;
5994
6061
  TIMER_MAX_ARMED = 64;
5995
- WATCH_MAX_ARMED = 32;
5996
6062
  TimerPromptText = external_exports.string().trim().min(1).max(TIMER_PROMPT_MAX);
5997
6063
  SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
5998
6064
  SIGNOFF_NUDGE_MESSAGE = [
5999
- `${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and this is not a request to do more work.`,
6065
+ `${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and no new work is being asked of you.`,
6066
+ "",
6067
+ "You rested without a fence, so this thread cannot be triaged.",
6068
+ "",
6069
+ "**IF THE TASK STILL HAS PARTS LEFT, THE FENCE IS NOT WHAT YOU OWE \u2014 THE WORK IS.** If ANY part of the",
6070
+ "original task is unfinished, unverified, or deferred, resume it NOW, in THIS turn, and sign off once it",
6071
+ "is genuinely finished. A milestone, a green test run and a long turn are none of them endings, and",
6072
+ "neither is naming the next step or writing it into a scratch file.",
6000
6073
  "",
6001
- "You rested without a fence, so this thread cannot be triaged. Add one at the END of your next message:",
6074
+ "**AND THAT TASK IS ALSO THE CEILING \u2014 finish it, and nothing else.** Work you notice on the way is a",
6075
+ "FINDING TO REPORT in your sign-off, never work to take on: the bug beside the one you were sent for,",
6076
+ "the refactor the code obviously wants, the second issue the first one touches. Widening the job is not",
6077
+ "thoroughness \u2014 it is a different job nobody asked for, and it buries the answer they did ask for under",
6078
+ "changes they now have to review. If it should be done, name it in one line and let the human dispatch",
6079
+ "it.",
6080
+ "",
6081
+ "**IF WHAT YOU WERE ASKED FOR IS A DOCUMENT, THE DOCUMENT IS THE ENDING.** A triage, a review, an",
6082
+ "investigation, a recommendation, a plan \u2014 when that is the deliverable, the finished write-up IS the",
6083
+ "work. Implementing what it proposes is the NEXT job, and not yours unless you were asked. Sign off with",
6084
+ "the write-up.",
6085
+ "",
6086
+ "**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
6087
+ "right now: ask that one in a question fence. Every other open choice INSIDE the task \u2014 a name, a",
6088
+ "default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
6089
+ "what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
6090
+ "unanswered question is not permission to go build the answer.",
6091
+ "",
6092
+ "Otherwise, add a fence at the END of your next message:",
6002
6093
  "",
6003
6094
  "- `` ```question `` \u2014 you need the human. One question per fence, lettered options, one recommended.",
6004
6095
  "- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
6005
6096
  " if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
6006
6097
  " **bolded verb phrase**.",
6007
- "- Blocked on a PR, CI, or your own background shell? Register it with `mcp__frizz__watch` and rest.",
6098
+ "- `` ```awaiting `` \u2014 you are WAITING on work that is actually running. It is PURE STRUCTURE: one line",
6099
+ " per thing you are waiting on, a REQUIRED `for:` duration, and one line of `reason:` prose.",
6100
+ "",
6101
+ " ```awaiting",
6102
+ " shell: <the id your runtime gave you>",
6103
+ " pr: owner/repo#123",
6104
+ " for: 2h",
6105
+ " reason: what you are waiting for, in one line",
6106
+ " ```",
6107
+ "",
6108
+ " Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
6109
+ " A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
6110
+ " awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
6111
+ " `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
6112
+ "",
6113
+ "**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
6114
+ "draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
6115
+ "the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
6116
+ "or you DO it first \u2014 a sub-agent's result comes BACK to you, so it lands on your card; a new card via",
6117
+ "`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
6118
+ "None of them is a `done`. And what is not",
6119
+ 'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
6120
+ "in the handoff.",
6008
6121
  "",
6009
6122
  "**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
6010
6123
  "fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
6124
+ "The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
6125
+ "ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
6011
6126
  "",
6012
6127
  "Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
6013
6128
  "seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
6014
6129
  "frizz \u2014 so anything you assumed they had followed, they have not."
6015
6130
  ].join("\n");
6016
6131
  ThreadWatchKind = external_exports.enum(["shell", "github"]);
6017
- RegisterableThreadWatchKind = external_exports.enum(["shell"]);
6018
- ThreadWatchTarget = external_exports.string().trim().min(1).max(200);
6132
+ GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
6133
+ GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
6134
+ GithubWatchStatus = external_exports.object({
6135
+ checks: GithubChecksState,
6136
+ /** The counts behind the verdict, so the card can say "3 running, 12 passed" the way GitHub does
6137
+ * rather than only "checks are running". */
6138
+ running: external_exports.number().int().nonnegative(),
6139
+ passed: external_exports.number().int().nonnegative(),
6140
+ failed: external_exports.number().int().nonnegative(),
6141
+ /** The failing job NAMES, capped — what a human actually needs to decide whether to look. */
6142
+ failing: external_exports.array(external_exports.string()).max(8).default([]),
6143
+ merge: GithubMergeState,
6144
+ /** OPEN | CLOSED | MERGED, lowercased. A merged or closed PR ends the wait outright. */
6145
+ state: external_exports.enum(["open", "closed", "merged"]),
6146
+ /** When frizz last heard from GitHub. A poll can fail or be rate-limited, and a stale reading stated
6147
+ * as current is worse than no reading. */
6148
+ polledAt: external_exports.string(),
6149
+ /** The head commit this verdict was reached on. A PR watcher reports a CI verdict when it is NEWS, and
6150
+ * "red again on a new commit" is news that the bare word `failing` cannot express. Optional because a
6151
+ * reading taken before 2026-08-17 carries none. */
6152
+ head: external_exports.string().optional(),
6153
+ /** A digest of WHICH jobs are failing right now. The head commit alone cannot express a re-run of the
6154
+ * same job, or a slower job going red after the first — both of which are a second failure the worker
6155
+ * must hear about. Empty when nothing is failing. */
6156
+ failureSig: external_exports.string().optional()
6157
+ }).strict();
6019
6158
  ThreadWatchView = external_exports.object({
6020
6159
  id: external_exports.string(),
6021
6160
  kind: ThreadWatchKind,
6022
6161
  target: external_exports.string(),
6023
6162
  state: external_exports.enum(["armed", "fired", "dropped"]),
6024
- createdAt: external_exports.string()
6163
+ createdAt: external_exports.string(),
6164
+ /** `github` rows only, and absent until the first successful poll. */
6165
+ github: GithubWatchStatus.optional()
6166
+ }).strict();
6167
+ PR_WATCH_DEFAULT_FOR_MS = 6 * 60 * 60 * 1e3;
6168
+ PR_WATCH_MAX_ARMED = 32;
6169
+ PrWatchView = external_exports.object({
6170
+ id: external_exports.string(),
6171
+ /** `owner/repo#N`, normalized — the same string the board's row and the status book are keyed by. */
6172
+ target: external_exports.string(),
6173
+ state: external_exports.enum(["armed", "dropped", "settled"]),
6174
+ createdAt: external_exports.string(),
6175
+ /** The PR's checks/mergeability as the poller last saw it. Absent until the first successful poll. */
6176
+ github: GithubWatchStatus.optional()
6177
+ }).strict();
6178
+ AddOwnPrWatchInput = external_exports.object({
6179
+ slug: ThreadSlug,
6180
+ /** `owner/repo#123` or a PR URL. Parsed server-side; an unparseable ref is refused rather than stored,
6181
+ * because a watcher that can never fire is worse than none — the worker rests believing it is covered. */
6182
+ target: external_exports.string().trim().min(1).max(200),
6183
+ /** How long to watch, as a DURATION (`30m`, `2h`, `3d` — parseAwaitingDuration).
6184
+ *
6185
+ * A PR nobody ever reviews would otherwise be polled forever, and a thread parked on it would wait
6186
+ * forever with it — the same unbounded wait the awaiting fence's `for:` closes, one level down. A
6187
+ * duration rather than an instant for the same reason it is one there: it cannot be written in the
6188
+ * past (maintainer 2026-08-15, asking for it explicitly).
6189
+ *
6190
+ * REQUIRED BY THE TOOL, OPTIONAL ON THE WIRE, and the asymmetry is deliberate. A worker's MCP server
6191
+ * outlives every frizz restart, so a session dispatched before this existed still holds a binary that
6192
+ * cannot send it — and making the RPC reject that would break `watch_pr` outright for every thread
6193
+ * already running, with no recourse from inside those threads. Absent ⇒ PR_WATCH_DEFAULT_FOR, which
6194
+ * still BOUNDS the poll; the point of the field is that a worker chooses, and one that cannot choose
6195
+ * is better bounded than broken. */
6196
+ for: external_exports.string().trim().min(1).max(16).optional()
6197
+ }).strict();
6198
+ AddOwnPrWatchResult = external_exports.object({
6199
+ id: external_exports.string(),
6200
+ target: external_exports.string(),
6201
+ /** True when this exact PR was ALREADY watched by this thread, so the call registered nothing new.
6202
+ * Re-registering after a compaction is the common case, and a duplicate would double every wake. */
6203
+ alreadyArmed: external_exports.boolean(),
6204
+ watches: external_exports.array(PrWatchView)
6205
+ }).strict();
6206
+ DropOwnPrWatchInput = external_exports.object({
6207
+ slug: ThreadSlug,
6208
+ id: external_exports.string().min(1).max(64)
6025
6209
  }).strict();
6210
+ DropOwnPrWatchResult = external_exports.object({
6211
+ dropped: external_exports.boolean(),
6212
+ watches: external_exports.array(PrWatchView)
6213
+ }).strict();
6214
+ ListOwnPrWatchesInput = external_exports.object({ slug: ThreadSlug }).strict();
6215
+ OwnPrWatchesResult = external_exports.object({ watches: external_exports.array(PrWatchView) }).strict();
6026
6216
  ThreadTimerView = external_exports.object({
6027
6217
  id: external_exports.string(),
6028
6218
  prompt: external_exports.string(),
@@ -6180,6 +6370,16 @@ var init_src = __esm({
6180
6370
  // AUTO-bump (the scheduler resumes the agent with exactly this text) rather than a reminder, which is
6181
6371
  // the distinction the held row's tooltip renders. Absent ⇒ the card merely re-surfaces.
6182
6372
  snoozePrompt: external_exports.string().optional(),
6373
+ /** The EVENT snooze on the resting card is armed for this exact rest — the human has said "hide this
6374
+ * until something reports". Distinct from `snoozedUntil`, which is a wall-clock park on the whole
6375
+ * thread: this one has no deadline and clears itself when the thread comes to a NEW rest.
6376
+ *
6377
+ * It travels because the chat has to honour it too (2026-08-14). Until then only the QUEUE did, on
6378
+ * the reasoning that the card states a FACT the drawer must keep showing or it blanks at rest and
6379
+ * reads as "the agent died". That reasoning holds for a thread nobody has parked; once the human has
6380
+ * explicitly parked THIS rest, showing them the same card with the same button one surface over is
6381
+ * not information, and they said so. */
6382
+ bgSnoozed: external_exports.boolean().optional(),
6183
6383
  // Which Claude transport serves this thread: "broker" = a session-broker-owned Agent SDK session
6184
6384
  // (typed control channel), "tmux" = the interactive TUI in a pane. Only the broker can be asked to
6185
6385
  // reload its plugin closure in place, so the board needs it to decide whether to offer that verb at
@@ -6297,6 +6497,13 @@ var init_src = __esm({
6297
6497
  // Optional so a pre-restart server keeps working; absent means "fall back to `/`", i.e. the old
6298
6498
  // behaviour. Registry-derived, so it is the same slug every other surface links to.
6299
6499
  projectSlug: external_exports.string().optional(),
6500
+ // The server's home directory — the expansion of a `~` a worker wrote in prose. Agents reference
6501
+ // files that way constantly (`~/.claude/CLAUDE.md`), and the browser has no way to derive it, so a
6502
+ // `~`-anchored Markdown link had no absolute path to become and stayed a same-origin anchor that
6503
+ // navigated out of Frizz. The client only ever uses it to build a path it then hands BACK to the
6504
+ // server, which realpath-gates it exactly as it gates one the author typed in full. Optional so a
6505
+ // pre-restart server keeps working; absent means `~` links stay unresolved, i.e. the old behaviour.
6506
+ homeDir: external_exports.string().optional(),
6300
6507
  // (No `.frizz/ exists` bit here on purpose. Threads are session-first — the ui.db registry IS the
6301
6508
  // board — so `.frizz/` presence says nothing about whether this project has one. Its only consumer
6302
6509
  // was a shell gate that dead-ended `.frizz`-less repos; the server still probes the directory
@@ -6403,13 +6610,23 @@ var init_src = __esm({
6403
6610
  // Default action for a vetted non-image local path in agent markdown. Image clicks always use the
6404
6611
  // OS default viewer so screenshots retain their expected behavior.
6405
6612
  localFileOpener: LocalFileOpener.optional(),
6406
- // GitHub batch-dispatch prompt templates (the picker's per-item worker prompt). Optional: when
6407
- // unset OR blank the server falls back to its exported DEFAULT_ISSUE_PROMPT / DEFAULT_PR_PROMPT.
6408
- // Substitution tokens the server fills: {repo} {n} {title} {url} {labels} {body}. The leading
6409
- // `THREAD: <slug>` tag is prepended by the server (not part of the editable template) so a custom
6410
- // prompt can never break the thread↔.frizz-file binding. Optional so old settings blobs parse.
6411
- githubIssuePrompt: external_exports.string().optional(),
6412
- githubPrPrompt: external_exports.string().optional()
6613
+ // The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
6614
+ // unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
6615
+ // the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
6616
+ // prepended by the server (not part of the editable template) so a custom prompt can never break the
6617
+ // thread↔.frizz-file binding. Optional so old settings blobs parse.
6618
+ //
6619
+ // ONE field, not one per kind. Issue and PR each had their own template until 2026-08-15, and the two
6620
+ // said the same thing twice: read the whole thread, be skeptical, cite what you checked, post nothing.
6621
+ // A person tuning "be more dubious" had to make the same edit in two boxes and keep them in step. The
6622
+ // ONE thing that genuinely differs per kind — which `gh` command reads the item — is a line in the
6623
+ // metadata block, so the merged template just carries both, and the worker picks the one that applies.
6624
+ //
6625
+ // There is no migration off the two old keys, and that is deliberate (the maintainer's call): Settings
6626
+ // is a non-strict z.object, so `githubIssuePrompt`/`githubPrPrompt` are STRIPPED the moment an old
6627
+ // blob is parsed. Any existing override is dropped and the reader gets the new shipped default —
6628
+ // exactly the intended backfill. Merging two customized templates into one has no correct answer.
6629
+ githubPrompt: external_exports.string().optional()
6413
6630
  });
6414
6631
  DispatchProviderPreferences = external_exports.object({
6415
6632
  model: external_exports.string().trim().min(1).max(200).optional(),
@@ -6538,9 +6755,6 @@ var init_src = __esm({
6538
6755
  // so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
6539
6756
  // with the trigger off, which is the honest reading of a caller that has never heard of it.
6540
6757
  postCompaction: external_exports.boolean().default(false),
6541
- // The QUESTION HOLD (2026-08-11). Defaulted for the same reason as the trigger above: a caller that
6542
- // has never heard of it means "don't hold", which is also this option's own default.
6543
- pauseOnQuestions: external_exports.boolean().default(false),
6544
6758
  intervalSeconds: RecurringIntervalSeconds.optional()
6545
6759
  }).strict();
6546
6760
  SetOwnThreadRecurringPromptInput = external_exports.object({
@@ -6552,8 +6766,14 @@ var init_src = __esm({
6552
6766
  // so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
6553
6767
  // with the trigger off, which is the honest reading of a caller that has never heard of it.
6554
6768
  postCompaction: external_exports.boolean().default(false),
6555
- // The QUESTION HOLD (2026-08-11), defaulted exactly as the trigger above and for the same reason.
6556
- pauseOnQuestions: external_exports.boolean().default(false),
6769
+ /** ACCEPTED AND IGNORED. The question hold was deleted 2026-08-16 (see scheduler.ts) and no caller in
6770
+ * this repo sends it any more — but this object is `.strict()`, and the caller on the other end is a
6771
+ * DETACHED worker daemon holding the `frizz-mcp.mjs` it was spawned with. Those outlive a server
6772
+ * restart by design, so for as long as any pre-2026-08-16 worker is alive a `start` would otherwise
6773
+ * come back as a validation error on a field the model cannot see it is sending. Tolerated here rather
6774
+ * than in the router so the shape stays one declaration; delete it once no such worker can be running.
6775
+ * The BROWSER input above needs no such clause — a stale tab is one reload away. */
6776
+ pauseOnQuestions: external_exports.boolean().optional(),
6557
6777
  intervalSeconds: RecurringIntervalSeconds.optional()
6558
6778
  }).strict();
6559
6779
  SetOwnThreadRecurringPromptResult = external_exports.object({
@@ -6580,6 +6800,23 @@ var init_src = __esm({
6580
6800
  OwnThreadTimersResult = external_exports.object({
6581
6801
  timers: external_exports.array(ThreadTimerView)
6582
6802
  }).strict();
6803
+ ThreadActivityItem = external_exports.object({
6804
+ kind: external_exports.enum(["shell", "agent", "timer", "pr"]),
6805
+ /** The string a `<kind>:` fence line must carry. For a shell that is the runtime task id the worker
6806
+ * was shown; for a PR, `owner/repo#N`; for a timer, its `tmr_…` row id. */
6807
+ id: external_exports.string(),
6808
+ label: external_exports.string(),
6809
+ /** ISO8601 of when it started or was armed — absent when frizz has no instant for it. */
6810
+ since: external_exports.string().optional(),
6811
+ /** A timer's fire instant, or a PR's expiry. Absent for shells and sub-agents. */
6812
+ until: external_exports.string().optional()
6813
+ }).strict();
6814
+ ListOwnThreadActivityInput = external_exports.object({
6815
+ slug: ThreadSlug
6816
+ }).strict();
6817
+ OwnThreadActivityResult = external_exports.object({
6818
+ activity: external_exports.array(ThreadActivityItem)
6819
+ }).strict();
6583
6820
  SetOwnThreadTimerResult = external_exports.object({
6584
6821
  id: external_exports.string(),
6585
6822
  fireAt: external_exports.string(),
@@ -6589,49 +6826,6 @@ var init_src = __esm({
6589
6826
  cancelled: external_exports.boolean(),
6590
6827
  timers: external_exports.array(ThreadTimerView)
6591
6828
  }).strict();
6592
- AddOwnThreadWatchInput = external_exports.object({
6593
- slug: ThreadSlug,
6594
- // REGISTERABLE kinds only, which is `shell` and nothing else — deliberately narrower than
6595
- // `ThreadWatchKind`, whose `github` member is a board-derived VIEW row with no registration behind it.
6596
- // The MCP tool already refuses a PR here in words ("a pr wait does not belong here — use an ```awaiting
6597
- // fence"); this is the same refusal at the wire, where it cannot be talked past.
6598
- kind: RegisterableThreadWatchKind,
6599
- target: ThreadWatchTarget,
6600
- /** The worker is BLOCKING on this one inside its tool call, so the scheduler settles it silently
6601
- * instead of delivering a wake — see the column comment in storage.ts. Defaulted, so a caller that
6602
- * has never heard of it registers an ordinary background watch. */
6603
- foreground: external_exports.boolean().default(false)
6604
- }).strict();
6605
- PromoteOwnThreadWatchInput = external_exports.object({
6606
- slug: ThreadSlug,
6607
- id: external_exports.string().min(1).max(64)
6608
- }).strict();
6609
- PromoteOwnThreadWatchResult = external_exports.object({
6610
- promoted: external_exports.boolean(),
6611
- watches: external_exports.array(ThreadWatchView)
6612
- }).strict();
6613
- DropOwnThreadWatchInput = external_exports.object({
6614
- slug: ThreadSlug,
6615
- id: external_exports.string().min(1).max(64)
6616
- }).strict();
6617
- ListOwnThreadWatchesInput = external_exports.object({
6618
- slug: ThreadSlug
6619
- }).strict();
6620
- OwnThreadWatchesResult = external_exports.object({
6621
- watches: external_exports.array(ThreadWatchView)
6622
- }).strict();
6623
- AddOwnThreadWatchResult = external_exports.object({
6624
- id: external_exports.string(),
6625
- /** True when this exact (kind, target) was ALREADY armed on the thread, so the call registered
6626
- * nothing new. A worker re-registering after a compaction is the common case, and silently minting a
6627
- * duplicate would mean two wakes for one event. */
6628
- alreadyArmed: external_exports.boolean(),
6629
- watches: external_exports.array(ThreadWatchView)
6630
- }).strict();
6631
- DropOwnThreadWatchResult = external_exports.object({
6632
- dropped: external_exports.boolean(),
6633
- watches: external_exports.array(ThreadWatchView)
6634
- }).strict();
6635
6829
  SetOwnThreadStopHookInput = external_exports.object({
6636
6830
  slug: ThreadSlug,
6637
6831
  prompt: RecurringPromptText.nullable(),
@@ -6736,12 +6930,52 @@ ${DISPATCH_TASK_BANNER}
6736
6930
  );
6737
6931
  WAKE_ITEM = /^- (👤|🤖) (.+?) from @(\S+?)(?: at (\S+?))?(?:: (\S+))?$/;
6738
6932
  WAKE_MORE = /^- …and (\d+) more not listed — check .+ for the rest$/;
6933
+ PR_WATCH_FINISHED = new RegExp(String.raw`^⏰ (${WAKE_REF}) was (MERGED|CLOSED)\.$`);
6934
+ PR_WATCH_CI_PASSED = new RegExp(String.raw`^✅ CI PASSED on (${WAKE_REF}) — (\d+) checks? green\.$`);
6935
+ PR_WATCH_CI_FAILED = new RegExp(String.raw`^❌ CI FAILED on (${WAKE_REF})(?:: (.+?))?\.$`);
6739
6936
  GithubStatus = external_exports.object({
6740
6937
  installed: external_exports.boolean(),
6741
6938
  inRepo: external_exports.boolean(),
6742
6939
  nameWithOwner: external_exports.string().nullable(),
6743
6940
  authed: external_exports.boolean()
6744
6941
  });
6942
+ GithubRefCard = external_exports.object({
6943
+ ref: external_exports.string(),
6944
+ kind: external_exports.enum(["issue", "pr", "commit"]),
6945
+ repo: external_exports.string(),
6946
+ // owner/repo, for the card's header line
6947
+ url: external_exports.string(),
6948
+ title: external_exports.string(),
6949
+ body: external_exports.string(),
6950
+ // already truncated server-side to the card's excerpt budget
6951
+ state: external_exports.string(),
6952
+ // OPEN | CLOSED | MERGED | DRAFT — empty for a commit, which has no state
6953
+ stateReason: external_exports.string().optional(),
6954
+ // COMPLETED | NOT_PLANNED | REOPENED — GitHub's closed-issue nuance
6955
+ at: external_exports.string().optional(),
6956
+ // ISO: opened-at for an issue/PR, committed-at for a commit
6957
+ authorLogin: external_exports.string().optional(),
6958
+ authorName: external_exports.string().optional(),
6959
+ // commits carry a git author name with no GitHub account behind it
6960
+ authorAvatar: external_exports.string().optional(),
6961
+ labels: external_exports.array(external_exports.object({ name: external_exports.string(), color: external_exports.string() })).default([]),
6962
+ additions: external_exports.number().int().nonnegative().optional(),
6963
+ deletions: external_exports.number().int().nonnegative().optional(),
6964
+ changedFiles: external_exports.number().int().nonnegative().optional(),
6965
+ comments: external_exports.number().int().nonnegative().optional(),
6966
+ // Epoch ms of the fetch this card came from. The CLIENT owns the freshness decision (render the
6967
+ // cached card instantly, then revalidate if it is old), so it has to be able to see the age.
6968
+ fetchedAt: external_exports.number().int().nonnegative()
6969
+ });
6970
+ GithubRefPreviewInput = external_exports.object({
6971
+ refs: external_exports.array(external_exports.string().min(3).max(120)).min(1).max(100),
6972
+ refresh: external_exports.boolean().default(false)
6973
+ });
6974
+ GithubRefPreviewResult = external_exports.object({
6975
+ cards: external_exports.array(GithubRefCard),
6976
+ missing: external_exports.array(external_exports.string()),
6977
+ error: external_exports.string().optional()
6978
+ });
6745
6979
  GithubItem = external_exports.object({
6746
6980
  kind: external_exports.enum(["issue", "pr"]),
6747
6981
  number: external_exports.number().int().positive(),
@@ -7798,11 +8032,11 @@ ${GITHUB_DISPATCH_UI_BOUNDARY}
7798
8032
 
7799
8033
  ${filled}`;
7800
8034
  }
7801
- function effectiveTemplate(kind, custom2) {
8035
+ function effectiveTemplate(custom2) {
7802
8036
  if (custom2 && custom2.trim().length > 0) return custom2;
7803
- return kind === "issue" ? DEFAULT_ISSUE_PROMPT : DEFAULT_PR_PROMPT;
8037
+ return DEFAULT_GITHUB_PROMPT;
7804
8038
  }
7805
- var pexec, GH_TIMEOUT, GH_MAXBUF, SEARCH_SORT, SEARCH_RESULT_WINDOW, LINKED_PR_CHUNK, GITHUB_PAGE_SIZE, BODY_CAP, DEFAULT_ISSUE_PROMPT, DEFAULT_PR_PROMPT;
8039
+ var pexec, GH_TIMEOUT, GH_MAXBUF, SEARCH_SORT, SEARCH_RESULT_WINDOW, LINKED_PR_CHUNK, GITHUB_PAGE_SIZE, BODY_CAP, DEFAULT_GITHUB_PROMPT;
7806
8040
  var init_github = __esm({
7807
8041
  "packages/server/src/github.ts"() {
7808
8042
  "use strict";
@@ -7818,26 +8052,30 @@ var init_github = __esm({
7818
8052
  LINKED_PR_CHUNK = 25;
7819
8053
  GITHUB_PAGE_SIZE = 30;
7820
8054
  BODY_CAP = 8 * 1024;
7821
- DEFAULT_ISSUE_PROMPT = `Triage this GitHub issue and recommend what to do about it. This is a RESEARCH thread: the deliverable is FINDINGS and a recommendation, NOT a landed fix. Read the full thread first \u2014 body, labels, and the whole discussion \u2014 then classify the report as a BUG or a FEATURE request and say which it is and why. If it is a BUG: establish whether the reported behavior actually happens on the current tree (capture the exact steps, command and output if it reproduces; say what you saw instead if it does not), trace the cause to concrete code, and state the smallest correct fix \u2014 or the top 2 options with the tradeoff of each \u2014 plus the files it touches and the risk. If it is a FEATURE: restate the request precisely (the use-case behind it and any ambiguity a maintainer must resolve), map where it lands in the code and what public API / UX surface it changes, and sketch a concrete plan for the smallest viable version with its risks, open design questions and a rough size estimate. Cite exact file:line for every load-bearing claim \u2014 an uncited claim is a LEAD, so flag it. Do NOT implement anything unasked, and post NOTHING to GitHub (no comments, no labels, no close) unless the human explicitly asks \u2014 read-only. Put your findings and recommendation in your FINAL MESSAGE: this one is headed for a fix, so it is NOT \`\`\`done\`\`\` \u2014 close with a \`\`\`question\`\`\` when there's a fix to choose (recommendation marked, so one reply rolls into implementation), or bare-rest.
8055
+ DEFAULT_GITHUB_PROMPT = `Triage this GitHub issue/PR and make recommendations. Be thoughtful, thorough, and dubious. Think through whether this is truly a good idea or not. Many ideas are bad.
7822
8056
 
7823
- ---
8057
+ Read the full thread first \u2014 body, labels, and the whole discussion \u2014 then classify the report as a BUG or a FEATURE request and say which it is and why.
7824
8058
 
7825
- Issue #{n}: {title}
7826
- Repository: {repo}
7827
- URL: {url}
7828
- Labels: {labels}
7829
- Body + full thread: \`gh issue view {n} -R {repo} --comments\``;
7830
- DEFAULT_PR_PROMPT = `Review this open pull request. This is an AUDIT thread: adversarially verify the change is correct, safe and complete before recommending merge. Read the PR description and discussion first, then read the full diff \u2014 the changed files in context, not just the hunks (pipe large output through toon). For each substantive change ask: is it correct? does it handle edges? does it break existing behavior or the public API? are there tests, and do they actually cover the change? Check CI too: pending automation is evidence to report, not a reason to park this audit; if later scope explicitly includes shepherding the PR, keep CI/bot/merge progression active with the backend wait primitive from the worker contract. Then recommend approve / request-changes / needs-discussion with a concise findings list \u2014 each concern citing exact file:line in the diff, blocking issues distinguished from nits. Post NOTHING to GitHub (no review, no comment, no approve/merge) unless the human explicitly asks \u2014 read-only. Put your review in your FINAL MESSAGE and close with a \`\`\`done\`\`\` fence listing the completed audit/evidence, or a two-option \`\`\`question\`\`\` if you want a go/no-go on posting the review to GitHub.
8059
+ If it is a BUG REPORT: establish whether the reported behavior actually happens on the current tree (capture the exact steps, command and output if it reproduces; say what you saw instead if it does not), trace the cause to concrete code, and state the smallest correct fix \u2014 or the top 2 options with the tradeoff of each \u2014 plus the files it touches and the risk. Be sure you have high confidence that the bug is real. There should be a clean and elegant solution. If there isn't an elegant way to do something, then it often means that it's not a good idea or that we are in need of a larger codebase refactor. The issue: you should stay in the codebase for similar categories of bugs. We are constantly seeking to proactively self-improve the codebase.
8060
+
8061
+ If it is a FEATURE REQUEST: restate the request precisely (the use-case behind it and any ambiguity a maintainer must resolve), map where it lands in the code and what public API / UX surface it changes, and sketch a concrete plan for the smallest viable version with its risks, open design questions and a rough size estimate.
8062
+
8063
+ If it is a PR: review it. Read the PR description, comments, and changes. You should be dubious. Do not assume that the PR is high quality, many are not. For each substantive change ask: is there a better approach? Is it correct? Does it handle edges? Is it elegant? Does it break existing behavior or the public API? Are there tests, and do they actually cover the change? Trace the impact of the changes thoughtfully. Check CI too. Recommend either closing, making changes (explain them), or merging. Test the changes yourself if necessary/possible.
8064
+
8065
+ If it is a DOCS CHANGE: make sure that it is well written, minimal, no walls of text, well structured, consistent tonally with others docs/prose, and not AI slop.
8066
+
8067
+ Bias always towards proactivity and taking immediate action. If you encounter issues, feel free to add fixes for those in the same PR. Do not worry about perfect git hygiene.
7831
8068
 
7832
8069
  ---
7833
8070
 
7834
- PR #{n}: {title}
8071
+ Issue/PR #{n}: {title}
7835
8072
  Repository: {repo}
7836
8073
  URL: {url}
7837
8074
  Labels: {labels}
7838
- Description + discussion: \`gh pr view {n} -R {repo} --comments\`
7839
- Diff: \`gh pr diff {n} -R {repo}\`
7840
- CI: \`gh pr checks {n} -R {repo}\``;
8075
+ Body + thread (issues): \`gh issue view {n} -R {repo} --comments\`
8076
+ Body + thread (PRs): \`gh pr view {n} -R {repo} --comments\`
8077
+ Diff (PRs): \`gh pr diff {n} -R {repo}\`
8078
+ CI (PRs): \`gh pr checks {n} -R {repo}\``;
7841
8079
  }
7842
8080
  });
7843
8081
 
@@ -10440,41 +10678,39 @@ function createStorage(dbPath) {
10440
10678
  ON thread_timer(state, fire_at);
10441
10679
  CREATE INDEX IF NOT EXISTS thread_timer_slug
10442
10680
  ON thread_timer(thread_slug, state, fire_at);
10443
- -- A worker's registered WATCHERS (2026-08-12). One row per thing a thread is waiting on, so a wait
10444
- -- has an IDENTITY: it can be listed after a compaction and DROPPED when it stops mattering.
10445
- --
10446
- -- This is the table the awaiting FENCE could never be. That fence is derived state \u2014 recomputed
10447
- -- from the final assistant message on every fold, and cleared by any newer record \u2014 so a park was
10448
- -- lost the moment the worker said one more sentence, there was no id to cancel, and nothing for a
10449
- -- Snooze button to be about. Maintainer 2026-08-11: "seems a little weird to register these watchers
10450
- -- with no identifer and no elegant mechanism for dismissing the watcher. they just live forever in
10451
- -- markdown\u2026i think that's why we dropped awaiting before."
10681
+ -- A worker's registered PR WATCHERS (2026-08-14). Registered by tool call, never by a fence: the
10682
+ -- fence states what a thread is waiting on, and watching is a separate, orthogonal thing that
10683
+ -- happens whether or not anything is written down (maintainer: "We should have a tool for this. The
10684
+ -- agent should have a tool to register a PR watcher").
10452
10685
  --
10453
- -- Shaped on thread_timer deliberately, which is the same lesson already learned once: a thread may
10454
- -- hold MANY, so the record of intent is a table rather than a column on the session row.
10455
- CREATE TABLE IF NOT EXISTS thread_watch (
10686
+ -- REPEATING, not one-shot. It reports CI turning green or red and every later review or comment, so
10687
+ -- it stays armed and carries a cursor; only a merge or a close settles it.
10688
+ CREATE TABLE IF NOT EXISTS pr_watch (
10456
10689
  id TEXT PRIMARY KEY,
10457
10690
  thread_slug TEXT NOT NULL,
10458
- kind TEXT NOT NULL CHECK (kind IN ('pr', 'ci', 'shell')),
10459
- target TEXT NOT NULL,
10460
- state TEXT NOT NULL CHECK (state IN ('armed', 'fired', 'dropped')),
10691
+ owner TEXT NOT NULL,
10692
+ repo TEXT NOT NULL,
10693
+ number INTEGER NOT NULL,
10694
+ state TEXT NOT NULL CHECK (state IN ('armed', 'dropped', 'settled')),
10461
10695
  created_at INTEGER NOT NULL,
10462
10696
  settled_at INTEGER,
10463
- -- Opaque per-kind progress marker: for a pr watcher, the activity baseline that decides what
10464
- -- counts as NEW. Written by the scheduler, never read by anything else, so its grammar can change
10465
- -- without a migration.
10466
10697
  cursor TEXT,
10467
- -- FOREGROUND (2026-08-12): the worker is BLOCKING on this one inside its own tool call, so the
10468
- -- scheduler must SETTLE it without delivering a wake. Waking a thread that is mid-turn waiting for
10469
- -- the very thing that woke it is noise at best; at worst the message lands while the tool is still
10470
- -- blocked and the worker reads its own answer twice. The tool polls the row and returns; if its
10471
- -- timeout expires first it PROMOTES the row (clears this), and the wake comes back.
10472
- foreground INTEGER NOT NULL DEFAULT 0
10698
+ -- When this watcher stops polling by itself. REQUIRED at registration (2026-08-15): a PR nobody
10699
+ -- ever touches would otherwise be polled forever, and the thread parked on it would wait forever
10700
+ -- with it. Nullable in the column only so an older row reads; the tool refuses to arm without one.
10701
+ expires_at INTEGER
10473
10702
  );
10474
- CREATE INDEX IF NOT EXISTS thread_watch_armed
10475
- ON thread_watch(state, kind);
10476
- CREATE INDEX IF NOT EXISTS thread_watch_slug
10477
- ON thread_watch(thread_slug, state, created_at);
10703
+ CREATE INDEX IF NOT EXISTS pr_watch_armed
10704
+ ON pr_watch(state);
10705
+ CREATE INDEX IF NOT EXISTS pr_watch_slug
10706
+ ON pr_watch(thread_slug, state, created_at);
10707
+ -- thread_watch WAS HERE and is retired (2026-08-14). A worker's wait is a 'watch:' line in its own
10708
+ -- awaiting fence again, which is BOTH the park and the wake -- the scheduler matches the name against
10709
+ -- the thread's live shells and its retired-shell ring, so nothing needs registering and nothing can
10710
+ -- outlive the fence that declares it. The registry existed because a fence had no identity to drop;
10711
+ -- the answer turned out to be that a park nobody has to drop needs none. An old DB keeps the orphan
10712
+ -- table, which costs nothing and is safer than a migration to remove it.
10713
+ -- (No backticks in this comment: the whole schema is a template literal.)
10478
10714
  `);
10479
10715
  for (const col of [
10480
10716
  "archived INTEGER NOT NULL DEFAULT 0",
@@ -10549,16 +10785,21 @@ function createStorage(dbPath) {
10549
10785
  // which is the correct default: an existing prompt described the triggers its operator chose.
10550
10786
  "recurring_on_compact INTEGER NOT NULL DEFAULT 0",
10551
10787
  "recurring_compact_fired_at TEXT",
10552
- // The question hold (2026-08-11). Its own ALTER for the same reason as the trigger above: an existing
10553
- // armed row picks it up OFF on the next boot, which is the honest reading of a row whose operator has
10554
- // never been shown the option.
10555
- "recurring_pause_on_questions INTEGER NOT NULL DEFAULT 0",
10788
+ // NO `recurring_pause_on_questions` HERE ANY MORE. It held every trigger while the thread was waiting
10789
+ // on the human and the footer showed it inverted as "Autonomous mode"; both were deleted 2026-08-16
10790
+ // (see scheduler.ts, "WHAT A PENDING QUESTION DOES TO THE THREE TRIGGERS"). A database created before
10791
+ // that release still carries the column — it is `NOT NULL DEFAULT 0`, so nothing needs to write it,
10792
+ // and dropping it would cost a table rebuild to reclaim one inert integer per row.
10556
10793
  // THE BUILT-IN SIGN-OFF NUDGE (scheduler SOURCE 9, 2026-08-12). How many times in a row frizz has
10557
10794
  // told this thread how to sign off without a fence appearing. Cleared ONLY when the thread signs
10558
10795
  // off — never by a user record, because frizz's own delivery is one. The second column holds the
10559
10796
  // last-nudged delivery id, for diagnosis.
10560
10797
  "signoff_nudges INTEGER NOT NULL DEFAULT 0",
10561
10798
  "signoff_nudge_anchor TEXT",
10799
+ // Cleared by `resetParkBumps` when a park is actually HONOURED — the one event that proves the
10800
+ // correction landed, and the one frizz cannot cause by correcting.
10801
+ "park_bumps INTEGER NOT NULL DEFAULT 0",
10802
+ "park_bump_anchor TEXT",
10562
10803
  // Title provenance for the CURRENT text (2026-08-07): 1 = the worker's own title signal wrote it,
10563
10804
  // 0 = the dispatch seeded it. DEFAULT 0 is the conservative direction — an existing row is assumed
10564
10805
  // to hold its dispatch chop until the repair below (or the next title signal) says otherwise.
@@ -10569,6 +10810,10 @@ function createStorage(dbPath) {
10569
10810
  } catch {
10570
10811
  }
10571
10812
  }
10813
+ try {
10814
+ db.exec("ALTER TABLE pr_watch ADD COLUMN expires_at INTEGER");
10815
+ } catch {
10816
+ }
10572
10817
  try {
10573
10818
  db.exec("UPDATE session SET tmux_name = 'frizz-' || substr(tmux_name, 6) WHERE tmux_name LIKE 'fray-%'");
10574
10819
  } catch {
@@ -10925,10 +11170,6 @@ function createStorage(dbPath) {
10925
11170
  recurring_on_rest = ?,
10926
11171
  recurring_on_schedule = ?,
10927
11172
  recurring_on_compact = ?,
10928
- -- The hold is deliberately absent from every CASE below it: it changes neither the WORDS nor the
10929
- -- cadence, so a delivery already queued still describes this row exactly. Flipping it must not
10930
- -- mint a generation or drop a "last sent" stamp.
10931
- recurring_pause_on_questions = ?,
10932
11173
  recurring_armed_at = CASE
10933
11174
  WHEN ? IS NULL THEN NULL
10934
11175
  WHEN recurring_armed_at IS NOT NULL AND recurring_prompt IS ? AND recurring_interval_ms IS ? THEN recurring_armed_at
@@ -10945,7 +11186,7 @@ function createStorage(dbPath) {
10945
11186
  WHEN ? IS NULL THEN NULL
10946
11187
  WHEN recurring_armed_at IS NOT NULL AND recurring_prompt IS ? THEN recurring_compact_fired_at
10947
11188
  ELSE NULL END`;
10948
- const recurringArgs = ({ prompt, stopHook, heartbeat, postCompaction, pauseOnQuestions, intervalMs, armedAt }) => {
11189
+ const recurringArgs = ({ prompt, stopHook, heartbeat, postCompaction, intervalMs, armedAt }) => {
10949
11190
  const ms = prompt === null ? null : intervalMs;
10950
11191
  const flag = (on) => prompt === null || !on ? 0 : 1;
10951
11192
  return [
@@ -10955,7 +11196,6 @@ function createStorage(dbPath) {
10955
11196
  flag(stopHook),
10956
11197
  flag(heartbeat),
10957
11198
  flag(postCompaction),
10958
- flag(pauseOnQuestions),
10959
11199
  prompt,
10960
11200
  prompt,
10961
11201
  ms,
@@ -11015,35 +11255,41 @@ function createStorage(dbPath) {
11015
11255
  UPDATE session SET signoff_nudges = 0, signoff_nudge_anchor = NULL
11016
11256
  WHERE slug = ? AND signoff_nudges > 0
11017
11257
  `);
11018
- const armWatchStmt = db.prepare(`
11019
- INSERT INTO thread_watch (id, thread_slug, kind, target, state, created_at, settled_at, cursor, foreground)
11020
- VALUES (@id, @slug, @kind, @target, 'armed', @createdAtMs, NULL, NULL, @foreground)
11258
+ const countParkBumpStmt = db.prepare(`
11259
+ UPDATE session SET park_bumps = park_bumps + 1, park_bump_anchor = ?
11260
+ WHERE slug = ?
11261
+ `);
11262
+ const resetParkBumpsStmt = db.prepare(`
11263
+ UPDATE session SET park_bumps = 0, park_bump_anchor = NULL
11264
+ WHERE slug = ? AND park_bumps > 0
11021
11265
  `);
11022
- const watchesBySlugStmt = db.prepare(
11023
- "SELECT * FROM thread_watch WHERE thread_slug = ? ORDER BY created_at, id"
11266
+ const armPrWatchStmt = db.prepare(`
11267
+ INSERT INTO pr_watch (id, thread_slug, owner, repo, number, state, created_at, settled_at, cursor, expires_at)
11268
+ VALUES (@id, @slug, @owner, @repo, @number, 'armed', @createdAtMs, NULL, NULL, @expiresAtMs)
11269
+ `);
11270
+ const expiredPrWatchesStmt = db.prepare(
11271
+ "SELECT * FROM pr_watch WHERE state = 'armed' AND expires_at IS NOT NULL AND expires_at <= ? ORDER BY expires_at, id"
11024
11272
  );
11025
- const armedWatchesBySlugStmt = db.prepare(
11026
- "SELECT * FROM thread_watch WHERE thread_slug = ? AND state = 'armed' ORDER BY created_at, id"
11273
+ const prWatchesBySlugStmt = db.prepare(
11274
+ "SELECT * FROM pr_watch WHERE thread_slug = ? ORDER BY created_at, id"
11027
11275
  );
11028
- const watchByIdStmt = db.prepare("SELECT * FROM thread_watch WHERE id = ?");
11029
- const armedWatchesStmt = db.prepare(
11030
- "SELECT * FROM thread_watch WHERE state = 'armed' ORDER BY created_at, id"
11276
+ const armedPrWatchesBySlugStmt = db.prepare(
11277
+ "SELECT * FROM pr_watch WHERE thread_slug = ? AND state = 'armed' ORDER BY created_at, id"
11031
11278
  );
11032
- const promoteWatchStmt = db.prepare(`
11033
- UPDATE thread_watch SET foreground = 0 WHERE id = ? AND thread_slug = ? AND state = 'armed'
11034
- `);
11035
- const dropWatchStmt = db.prepare(`
11036
- UPDATE thread_watch SET state = 'dropped', settled_at = ?
11279
+ const prWatchByIdStmt = db.prepare("SELECT * FROM pr_watch WHERE id = ?");
11280
+ const armedPrWatchesStmt = db.prepare(
11281
+ "SELECT * FROM pr_watch WHERE state = 'armed' ORDER BY created_at, id"
11282
+ );
11283
+ const dropPrWatchStmt = db.prepare(`
11284
+ UPDATE pr_watch SET state = 'dropped', settled_at = ?
11037
11285
  WHERE id = ? AND thread_slug = ? AND state = 'armed'
11038
11286
  `);
11039
- const fireWatchStmt = db.prepare(`
11040
- UPDATE thread_watch SET state = 'fired', settled_at = ?
11287
+ const settlePrWatchStmt = db.prepare(`
11288
+ UPDATE pr_watch SET state = 'settled', settled_at = ?
11041
11289
  WHERE id = ? AND state = 'armed'
11042
11290
  `);
11043
- const watchCursorStmt = db.prepare(`
11044
- UPDATE thread_watch SET cursor = ? WHERE id = ? AND state = 'armed'
11045
- `);
11046
- const delThreadWatches = db.prepare("DELETE FROM thread_watch WHERE thread_slug = ?");
11291
+ const prWatchCursorStmt = db.prepare("UPDATE pr_watch SET cursor = ? WHERE id = ? AND state = 'armed'");
11292
+ const delPrWatches = db.prepare("DELETE FROM pr_watch WHERE thread_slug = ?");
11047
11293
  const confirmAwaitingWaitStmt = db.prepare(`
11048
11294
  UPDATE session
11049
11295
  SET awaiting_fence_id = ?, awaiting_confirmed_at = ?, snoozed_until = ?
@@ -11106,7 +11352,7 @@ function createStorage(dbPath) {
11106
11352
  }
11107
11353
  delRetiredOps.run(existing.slug);
11108
11354
  delThreadTimers.run(existing.slug);
11109
- delThreadWatches.run(existing.slug);
11355
+ delPrWatches.run(existing.slug);
11110
11356
  delSession.run(existing.slug);
11111
11357
  return existing;
11112
11358
  };
@@ -11544,14 +11790,16 @@ function createStorage(dbPath) {
11544
11790
  setRecurringPromptBySlug: (slug, write) => recurringBySlugStmt.run(...recurringArgs(write), slug).changes === 1,
11545
11791
  countSignoffNudge: (slug, anchor) => void countNudgeStmt.run(anchor, slug),
11546
11792
  resetSignoffNudges: (slug) => void resetNudgesStmt.run(slug),
11547
- armThreadWatch: (watch) => void armWatchStmt.run({ ...watch, foreground: watch.foreground ? 1 : 0 }),
11548
- promoteThreadWatch: (slug, id) => promoteWatchStmt.run(id, slug).changes === 1,
11549
- listThreadWatches: (slug, opts) => (opts?.armedOnly ? armedWatchesBySlugStmt : watchesBySlugStmt).all(slug),
11550
- getThreadWatch: (id) => watchByIdStmt.get(id),
11551
- armedThreadWatches: () => armedWatchesStmt.all(),
11552
- dropThreadWatch: (slug, id, settledAtMs) => dropWatchStmt.run(settledAtMs, id, slug).changes === 1,
11553
- markThreadWatchFired: (id, settledAtMs) => fireWatchStmt.run(settledAtMs, id).changes === 1,
11554
- setThreadWatchCursor: (id, cursor) => watchCursorStmt.run(cursor, id).changes === 1,
11793
+ countParkBump: (slug, anchor) => void countParkBumpStmt.run(anchor, slug),
11794
+ resetParkBumps: (slug) => void resetParkBumpsStmt.run(slug),
11795
+ armPrWatch: (watch) => void armPrWatchStmt.run(watch),
11796
+ listPrWatches: (slug, opts) => (opts?.armedOnly ? armedPrWatchesBySlugStmt : prWatchesBySlugStmt).all(slug),
11797
+ getPrWatch: (id) => prWatchByIdStmt.get(id),
11798
+ armedPrWatches: () => armedPrWatchesStmt.all(),
11799
+ expiredPrWatches: (nowMs) => expiredPrWatchesStmt.all(nowMs),
11800
+ dropPrWatch: (slug, id, settledAtMs) => dropPrWatchStmt.run(settledAtMs, id, slug).changes === 1,
11801
+ settlePrWatch: (id, settledAtMs) => settlePrWatchStmt.run(settledAtMs, id).changes === 1,
11802
+ setPrWatchCursor: (id, cursor) => prWatchCursorStmt.run(cursor, id).changes === 1,
11555
11803
  armThreadTimer: (timer) => void armTimerStmt.run(timer),
11556
11804
  listThreadTimers: (slug, opts) => (opts?.armedOnly ? armedTimersBySlugStmt : timersBySlugStmt).all(slug),
11557
11805
  getThreadTimer: (id) => timerByIdStmt.get(id),
@@ -13334,15 +13582,25 @@ function matchComposedText(items, recordText, contemporaneous) {
13334
13582
  }
13335
13583
  return matched;
13336
13584
  }
13337
- function ageDeliveries(items, nowMs) {
13585
+ function ageDeliveries(items, nowMs, observedUserAt) {
13338
13586
  if (!items.length) return items;
13339
13587
  let changed = false;
13588
+ const userAtMs = Date.parse(observedUserAt ?? "");
13589
+ const supersededByUserTurn = (item) => {
13590
+ if (!Number.isFinite(userAtMs)) return false;
13591
+ const born = Date.parse(item.at);
13592
+ return Number.isFinite(born) && userAtMs >= born;
13593
+ };
13340
13594
  const next = [];
13341
13595
  for (const item of items) {
13342
13596
  if (item.state === "cancelled") {
13343
13597
  next.push(item);
13344
13598
  continue;
13345
13599
  }
13600
+ if (supersededByUserTurn(item)) {
13601
+ changed = true;
13602
+ continue;
13603
+ }
13346
13604
  const born = Date.parse(item.at);
13347
13605
  if (item.state === "pending" && Number.isFinite(born) && nowMs - born > PENDING_TIMEOUT_MS) {
13348
13606
  next.push({ ...item, state: "unconfirmed", updatedAt: new Date(nowMs).toISOString() });
@@ -14027,6 +14285,7 @@ function newTailState(slug, sessionId, path, foreign = false, nativeSessionId =
14027
14285
  paneDead: false,
14028
14286
  noTranscript: false,
14029
14287
  nextDiscoverMs: 0,
14288
+ discoverMisses: 0,
14030
14289
  stallLogged: false,
14031
14290
  customTitleRevision: 0
14032
14291
  };
@@ -14086,18 +14345,23 @@ function parseSignalFence(text) {
14086
14345
  if (!kind) return void 0;
14087
14346
  if (norm.slice(end).trim() !== "") return void 0;
14088
14347
  if (kind === "done") return { kind, body: capFenceBody(raw2.trim()), hints: [] };
14348
+ const lines = raw2.split("\n");
14349
+ const delimiter2 = lines.findIndex((l) => /^\s*---+\s*$/.test(l));
14350
+ const frontmatter = delimiter2 === -1 ? lines : lines.slice(0, delimiter2);
14351
+ const prose = delimiter2 === -1 ? [] : lines.slice(delimiter2 + 1);
14089
14352
  const hints = [];
14090
14353
  const rest = [];
14091
- for (const line of raw2.split("\n")) {
14354
+ for (const line of frontmatter) {
14092
14355
  const hm = line.match(AWAITING_HINT_RE);
14093
14356
  const k = hm?.[1].toLowerCase();
14094
- if (hm && (k === "pr-watch" || k === "human" || k === "timer" || k === "pr" || k === "ci" || k === "session")) {
14357
+ if (hm && (k === "shell" || k === "agent" || k === "timer" || k === "pr" || k === "for" || k === "reason")) {
14095
14358
  const value = hm[2].trim();
14096
14359
  hints.push({ kind: k, value: value.length > HINT_VALUE_MAX ? value.slice(0, HINT_VALUE_MAX) : value });
14097
14360
  } else {
14098
14361
  rest.push(line);
14099
14362
  }
14100
14363
  }
14364
+ rest.push(...prose);
14101
14365
  return { kind, body: capFenceBody(rest.join("\n").trim()), hints: hints.slice(0, HINT_MAX) };
14102
14366
  }
14103
14367
  function isRealUserMessage(content) {
@@ -14202,7 +14466,7 @@ function retireLive(state, entry, finishedAt, status) {
14202
14466
  state.subAgents.delete(entry.toolUseId);
14203
14467
  if (entry.kind === "shell") {
14204
14468
  state.retiredShells.delete(entry.toolUseId);
14205
- state.retiredShells.set(entry.toolUseId, { toolUseId: entry.toolUseId, command: entry.command, outputFile: entry.outputFile, status });
14469
+ state.retiredShells.set(entry.toolUseId, { toolUseId: entry.toolUseId, command: entry.command, outputFile: entry.outputFile, status, taskId: entry.taskId, label: entry.label, finishedAt });
14206
14470
  while (state.retiredShells.size > RETAINED_SHELLS_MAX) {
14207
14471
  const oldest = state.retiredShells.keys().next().value;
14208
14472
  if (oldest === void 0) break;
@@ -14762,6 +15026,10 @@ function defaultBrokerDaemonAlive(project, now) {
14762
15026
  return alive;
14763
15027
  };
14764
15028
  }
15029
+ function rowIsArchived(row) {
15030
+ if (row.state === "open" || row.state === "archived") return row.state === "archived";
15031
+ return row.archived === 1;
15032
+ }
14765
15033
  function createTailer(deps) {
14766
15034
  const now = deps.now ?? Date.now;
14767
15035
  const paneDead = deps.paneDead ?? (() => true);
@@ -14977,10 +15245,17 @@ function createTailer(deps) {
14977
15245
  for (const e of state.subAgents.values()) {
14978
15246
  if (e.kind !== "shell") continue;
14979
15247
  const lastActivityAt = entryLastActivity(e);
14980
- out.push({ label: e.label, startedAt: e.startedAt, state: "running", id: e.toolUseId, ...e.taskId ? { stoppable: true } : {}, ...lastActivityAt ? { lastActivityAt } : {} });
15248
+ const since = Date.parse(lastActivityAt ?? e.startedAt);
15249
+ const shellState = Number.isFinite(since) && now() - since > BGSHELL_STALE_MS ? "stale" : "running";
15250
+ out.push({ label: e.label, startedAt: e.startedAt, state: shellState, id: e.toolUseId, ...e.taskId ? { stoppable: true, taskId: e.taskId } : {}, ...lastActivityAt ? { lastActivityAt } : {} });
14981
15251
  }
14982
15252
  return out;
14983
15253
  }
15254
+ function retiredShellViews(state) {
15255
+ const out = [];
15256
+ for (const r of state.retiredShells.values()) out.push({ id: r.toolUseId, taskId: r.taskId, label: r.label, status: r.status, finishedAt: r.finishedAt });
15257
+ return out;
15258
+ }
14984
15259
  function codexBgShellViews(state) {
14985
15260
  if (!deps.codexBackgroundExecs || state.foreign || state.paneDead) return [];
14986
15261
  const execs = deps.codexBackgroundExecs(state.slug, state.sessionId);
@@ -15373,7 +15648,7 @@ ${ask}`;
15373
15648
  const next = items.filter((item) => item.state !== "pending" && item.state !== "enqueued");
15374
15649
  return next.length === items.length ? items : next;
15375
15650
  }
15376
- function ledgerFold(row, nowMs) {
15651
+ function ledgerFold(row, nowMs, state) {
15377
15652
  if (!row.delivery_ledger) {
15378
15653
  return { finish: () => ({ changed: false, value: null }) };
15379
15654
  }
@@ -15393,7 +15668,7 @@ ${ask}`;
15393
15668
  },
15394
15669
  finish: () => {
15395
15670
  items = retireDeliveriesLostWithTheDaemon(row, items);
15396
- items = ageDeliveries(items, nowMs);
15671
+ items = ageDeliveries(items, nowMs, state.lastUserAt);
15397
15672
  if (items === before) return { changed: false, value: null };
15398
15673
  const value = serializeDeliveryLedger(items);
15399
15674
  deps.storage.setDeliveryLedger(row.slug, value);
@@ -15562,6 +15837,7 @@ ${ask}`;
15562
15837
  }
15563
15838
  function captureStall(state, row) {
15564
15839
  if (state.stallLogged) return;
15840
+ if (row.exited && rowIsArchived(row)) return;
15565
15841
  state.stallLogged = true;
15566
15842
  let pane = "";
15567
15843
  try {
@@ -15571,11 +15847,11 @@ ${ask}`;
15571
15847
  }
15572
15848
  const authFailure = row.backend !== "codex" && isClaudeAuthErrorText(pane);
15573
15849
  if (authFailure) state.authFault = "authentication_rejected";
15574
- const evidence = isBrokerClaudeRow(row) && deps.project.stateDir ? `no tmux pane (broker runtime). Daemon diagnostics: ${claudeBrokerDiagnosticLogPath(deps.project.stateDir, row.session_id)}` : isHeadlessRow(row) ? `no tmux pane (headless ${row.backend === "codex" ? "codex app-server" : "claude broker"} runtime)` : "";
15575
- const detail = authFailure ? "(claude authentication failure \u2014 pane content redacted; sign in and retry)" : pane.trim() || evidence || "(pane empty / unavailable)";
15850
+ const evidence = isBrokerClaudeRow(row) && deps.project.stateDir ? `no tmux pane (broker runtime). Daemon diagnostics: ${claudeBrokerDiagnosticLogPath(deps.project.stateDir, row.session_id)}` : isHeadlessRow(row) ? `no tmux pane (headless ${row.backend === "codex" ? "codex app-server" : "claude broker"} runtime)` : "no pane captured (this build runs no tmux)";
15851
+ const detail = authFailure ? "(claude authentication failure \u2014 pane content redacted; sign in and retry)" : pane.trim() || evidence;
15576
15852
  log.error(
15577
15853
  "tailer",
15578
- `thread ${row.slug} (session ${row.session_id}): no transcript ${DISCOVERY_GRACE_MS / 1e3}s after dispatch \u2014 likely a boot failure. ${isHeadlessRow(row) && !pane.trim() && !authFailure ? "" : "Pane:\n"}${detail.slice(0, 4e3)}`
15854
+ `thread ${row.slug} (session ${row.session_id}): no transcript ${DISCOVERY_GRACE_MS / 1e3}s after dispatch \u2014 likely a boot failure. ${pane.trim() && !authFailure ? "Pane:\n" : ""}${detail.slice(0, 4e3)}`
15579
15855
  );
15580
15856
  try {
15581
15857
  mkdirSync10(stallLogDir, { recursive: true });
@@ -15598,6 +15874,7 @@ ${detail}
15598
15874
  if (size > 0) {
15599
15875
  state.noTranscript = false;
15600
15876
  state.stallLogged = false;
15877
+ state.discoverMisses = 0;
15601
15878
  return true;
15602
15879
  }
15603
15880
  const spawnedMs = Date.parse(row.spawned_at);
@@ -15612,6 +15889,7 @@ ${detail}
15612
15889
  state.primed = false;
15613
15890
  state.noTranscript = false;
15614
15891
  state.stallLogged = false;
15892
+ state.discoverMisses = 0;
15615
15893
  return true;
15616
15894
  }
15617
15895
  const found = discoverTranscriptId(logDir, row.session_id, { nowMs, exclude: claimedIds(row.slug) });
@@ -15634,8 +15912,14 @@ ${detail}
15634
15912
  state.primed = false;
15635
15913
  state.noTranscript = false;
15636
15914
  state.stallLogged = false;
15915
+ state.discoverMisses = 0;
15637
15916
  return true;
15638
15917
  }
15918
+ state.discoverMisses++;
15919
+ state.nextDiscoverMs = nowMs + Math.min(
15920
+ DISCOVER_RETRY_MS * 2 ** (state.discoverMisses - 1),
15921
+ DISCOVER_RETRY_MAX_MS
15922
+ );
15639
15923
  state.noTranscript = true;
15640
15924
  captureStall(state, row);
15641
15925
  return true;
@@ -15647,6 +15931,15 @@ ${detail}
15647
15931
  const nowMs = now();
15648
15932
  adoptionBindings = /* @__PURE__ */ new Map();
15649
15933
  const rows = deps.storage.allSessions();
15934
+ let coldVisibleRow = false;
15935
+ for (const row of rows) {
15936
+ if (rowIsArchived(row)) continue;
15937
+ const known = states.get(row.slug);
15938
+ if (!known || !known.primed) {
15939
+ coldVisibleRow = true;
15940
+ break;
15941
+ }
15942
+ }
15650
15943
  let primed = 0;
15651
15944
  let primedRows = 0;
15652
15945
  const tickStartedMs = performance.now();
@@ -15672,13 +15965,19 @@ ${detail}
15672
15965
  const prevNoTranscript = state.noTranscript;
15673
15966
  if (row.backend !== "codex" && !resolveTranscript(state, row, nowMs)) continue;
15674
15967
  if (!state.primed) {
15675
- if (primedRows >= MAX_PRIME_ROWS_PER_TICK || primedRows > 0 && performance.now() - tickStartedMs > PRIME_BUDGET_MS) {
15968
+ if (
15969
+ // Yield the slot to a still-cold visible row — but never at the cost of the forward-progress
15970
+ // guarantee below. `primedRows > 0` keeps the archive advancing by at least one row per tick
15971
+ // even if a visible row were somehow to stay cold indefinitely, so deferral can never become
15972
+ // starvation. The visible row still primes on this same tick; it just is not necessarily first.
15973
+ coldVisibleRow && primedRows > 0 && rowIsArchived(row) || primedRows >= MAX_PRIME_ROWS_PER_TICK || primedRows > 0 && performance.now() - tickStartedMs > PRIME_BUDGET_MS
15974
+ ) {
15676
15975
  primeIncomplete = true;
15677
15976
  continue;
15678
15977
  }
15679
15978
  primedRows++;
15680
15979
  const primeOffset = state.offset;
15681
- const primeLedger = ledgerFold(row, nowMs);
15980
+ const primeLedger = ledgerFold(row, nowMs, state);
15682
15981
  consume(state, backend, chainOnLine(primeLedger.onLine, codexSubAgentOnLine(row, state)));
15683
15982
  state.codexSubAgents?.poll(nowMs);
15684
15983
  const primedLedger = primeLedger.finish();
@@ -15734,7 +16033,7 @@ ${detail}
15734
16033
  const prevTurn = state.turn;
15735
16034
  const rowLedger = row.delivery_ledger ?? null;
15736
16035
  const ledgerDrifted = rowLedger !== (state.deliveryLedgerSeen ?? null);
15737
- const ledger = ledgerFold(row, nowMs);
16036
+ const ledger = ledgerFold(row, nowMs, state);
15738
16037
  consume(state, backend, chainOnLine(ledger.onLine, codexSubAgentOnLine(row, state)));
15739
16038
  state.codexSubAgents?.poll(nowMs);
15740
16039
  const ledgerResult = ledger.finish();
@@ -15949,7 +16248,7 @@ ${detail}
15949
16248
  if (!s) return void 0;
15950
16249
  const pendingQuestion = s.turn === "idle" && s.lastAssistantHasQuestion;
15951
16250
  const nowMs = now();
15952
- return { turn: s.turn, permPrompt: s.permPrompt, permPolicy: s.permPolicy, permDenies: s.permDenies, nativeInputRequired: s.nativeInputRequired, model: s.model, effort: s.effort, profileAt: s.profileAt, profileRevision: s.profileRevision, permissionMode: s.permissionMode, permissionModeAt: s.permissionModeAt, permissionModeRevision: s.permissionModeRevision, lastActivityAt: s.lastActivityAt, lastAssistantAt: s.lastAssistantAt, lastAssistant: s.lastAssistant, aiTitle: s.aiTitle, customTitle: s.customTitle, customTitleRevision: s.customTitleRevision, subAgents: subAgentViews(s, nowMs), droppedReports: [...s.queuedReports.values()], bgShells: [...bgShellViews(s), ...codexBgShellViews(s)], pendingAsk: s.pendingAsk, pendingQuestion, lastAssistantAllDone: s.lastAssistantAllDone, lastUserAt: s.lastUserAt, lastUserText: s.lastUserText, lastFence: s.lastFence, noTranscript: s.noTranscript, authFault: s.authFault, limitFault: s.limitFault, contextTokens: s.contextTokens, contextWindow: s.contextWindow, lastCompactionAt: s.lastCompactionAt };
16251
+ return { turn: s.turn, permPrompt: s.permPrompt, permPolicy: s.permPolicy, permDenies: s.permDenies, nativeInputRequired: s.nativeInputRequired, model: s.model, effort: s.effort, profileAt: s.profileAt, profileRevision: s.profileRevision, permissionMode: s.permissionMode, permissionModeAt: s.permissionModeAt, permissionModeRevision: s.permissionModeRevision, lastActivityAt: s.lastActivityAt, lastAssistantAt: s.lastAssistantAt, lastAssistant: s.lastAssistant, aiTitle: s.aiTitle, customTitle: s.customTitle, customTitleRevision: s.customTitleRevision, subAgents: subAgentViews(s, nowMs), droppedReports: [...s.queuedReports.values()], bgShells: [...bgShellViews(s), ...codexBgShellViews(s)], retiredShells: retiredShellViews(s), pendingAsk: s.pendingAsk, pendingQuestion, lastAssistantAllDone: s.lastAssistantAllDone, lastUserAt: s.lastUserAt, lastUserText: s.lastUserText, lastFence: s.lastFence, noTranscript: s.noTranscript, authFault: s.authFault, limitFault: s.limitFault, contextTokens: s.contextTokens, contextWindow: s.contextWindow, lastCompactionAt: s.lastCompactionAt };
15953
16252
  },
15954
16253
  // The CURRENT fresh foreign session ids (mtime within FOREIGN_FRESH_MS, capped), mtime-desc. Kept
15955
16254
  // as the last scan's result — recomputed at most every FOREIGN_SCAN_EVERY ticks.
@@ -16002,7 +16301,7 @@ function landsAfterRead(eventAt, lastReadAt) {
16002
16301
  if (!Number.isFinite(e) || !Number.isFinite(r)) return true;
16003
16302
  return e > r;
16004
16303
  }
16005
- var IDLE_BACKSTOP_MS, RUNTIME_CHASE_MAX, POLL_MS2, MAX_PRIME_ROWS_PER_TICK, PRIME_BUDGET_MS, TRANSCRIPT_READ_WINDOW, EMPTY_BUFFER, MAX_POLL_MS, SUBAGENT_STALE_MS, FOREIGN_FRESH_MS, FOREIGN_MAX, FOREIGN_SCAN_EVERY, CACHE_FLUSH_MS, PRIME_PROGRESS_EVERY, DISCOVER_RETRY_MS, PERM_YES_OPTION, PERM_QUESTION, PERM_FOOTER, PERM_COMPOSER_FOOTER, PERM_MODAL_TAIL_ROWS, PERM_COMPOSER_TAIL_ROWS, BOOT_APIKEY_HEADLINE, BOOT_APIKEY_QUESTION, BOOT_TRUST_QUESTION, BOOT_TRUST_OPTION, BOOT_MODAL_TAIL_ROWS, RETAINED_SUBAGENTS_MAX, RETAINED_SHELLS_MAX, DESCENDANT_TERMINALS_MAX, PENDING_RESUMES_MAX, PENDING_SHELLS_MAX, RESUME_REPLAY_SLACK_MS, QUESTION_BLOCK_RE, SIGNAL_FENCE_RE, AWAITING_HINT_RE, FENCE_BODY_MAX, HINT_MAX, HINT_VALUE_MAX, LAUNCH_ACK_RE, AUTO_BACKGROUND_ACK_RE, DESCENDANT_SIDECAR_MAX, DESCENDANT_NOTIFY_GRACE_MS, MAX_OWNED_TOOL_USE_IDS, BROKER_LIVENESS_TTL_MS, UNRESTORED_TAIL_FIELDS;
16304
+ var IDLE_BACKSTOP_MS, RUNTIME_CHASE_MAX, POLL_MS2, MAX_PRIME_ROWS_PER_TICK, PRIME_BUDGET_MS, TRANSCRIPT_READ_WINDOW, EMPTY_BUFFER, MAX_POLL_MS, SUBAGENT_STALE_MS, BGSHELL_STALE_MS, FOREIGN_FRESH_MS, FOREIGN_MAX, FOREIGN_SCAN_EVERY, CACHE_FLUSH_MS, PRIME_PROGRESS_EVERY, DISCOVER_RETRY_MS, DISCOVER_RETRY_MAX_MS, PERM_YES_OPTION, PERM_QUESTION, PERM_FOOTER, PERM_COMPOSER_FOOTER, PERM_MODAL_TAIL_ROWS, PERM_COMPOSER_TAIL_ROWS, BOOT_APIKEY_HEADLINE, BOOT_APIKEY_QUESTION, BOOT_TRUST_QUESTION, BOOT_TRUST_OPTION, BOOT_MODAL_TAIL_ROWS, RETAINED_SUBAGENTS_MAX, RETAINED_SHELLS_MAX, DESCENDANT_TERMINALS_MAX, PENDING_RESUMES_MAX, PENDING_SHELLS_MAX, RESUME_REPLAY_SLACK_MS, QUESTION_BLOCK_RE, SIGNAL_FENCE_RE, AWAITING_HINT_RE, FENCE_BODY_MAX, HINT_MAX, HINT_VALUE_MAX, LAUNCH_ACK_RE, AUTO_BACKGROUND_ACK_RE, DESCENDANT_SIDECAR_MAX, DESCENDANT_NOTIFY_GRACE_MS, MAX_OWNED_TOOL_USE_IDS, BROKER_LIVENESS_TTL_MS, UNRESTORED_TAIL_FIELDS;
16006
16305
  var init_tailer = __esm({
16007
16306
  "packages/server/src/tailer.ts"() {
16008
16307
  "use strict";
@@ -16031,12 +16330,14 @@ var init_tailer = __esm({
16031
16330
  EMPTY_BUFFER = Buffer.alloc(0);
16032
16331
  MAX_POLL_MS = 1e4;
16033
16332
  SUBAGENT_STALE_MS = 15 * 6e4;
16333
+ BGSHELL_STALE_MS = 12 * 60 * 6e4;
16034
16334
  FOREIGN_FRESH_MS = 24 * 60 * 6e4;
16035
16335
  FOREIGN_MAX = 20;
16036
16336
  FOREIGN_SCAN_EVERY = 5;
16037
16337
  CACHE_FLUSH_MS = 3e4;
16038
16338
  PRIME_PROGRESS_EVERY = 20;
16039
16339
  DISCOVER_RETRY_MS = 15e3;
16340
+ DISCOVER_RETRY_MAX_MS = 15 * 6e4;
16040
16341
  PERM_YES_OPTION = /(^|\n)\s*(❯\s*)?1\.\s+Yes\b/;
16041
16342
  PERM_QUESTION = /\b(?:Do you want|Would you like)\b/;
16042
16343
  PERM_FOOTER = /\bEsc to (cancel|reject)\b/;
@@ -16056,7 +16357,7 @@ var init_tailer = __esm({
16056
16357
  RESUME_REPLAY_SLACK_MS = 6e4;
16057
16358
  QUESTION_BLOCK_RE = /^```question(?:[ \t]+[A-Za-z][^\r\n]*?)?[ \t]*\r?\n[\s\S]*?\r?\n```[ \t]*$/gm;
16058
16359
  SIGNAL_FENCE_RE = /^```(done|awaiting)[ \t]*\n([\s\S]*?)\n```[ \t]*$/gm;
16059
- AWAITING_HINT_RE = /^(pr-watch|human|timer|pr|ci|session):\s*(\S.*)$/i;
16360
+ AWAITING_HINT_RE = /^(shell|agent|timer|pr|for|reason):\s*(\S.*)$/i;
16060
16361
  FENCE_BODY_MAX = 500;
16061
16362
  HINT_MAX = 8;
16062
16363
  HINT_VALUE_MAX = 200;
@@ -16089,6 +16390,7 @@ var init_tailer = __esm({
16089
16390
  "permDenies",
16090
16391
  "noTranscript",
16091
16392
  "nextDiscoverMs",
16393
+ "discoverMisses",
16092
16394
  "stallLogged",
16093
16395
  "deliveryLedgerSeen",
16094
16396
  // The chase bookkeeping is compared against an IN-MEMORY, per-process counter — the ingest's `live`
@@ -16451,8 +16753,8 @@ function parseCodexSubAgentLine(line, toolNameFor) {
16451
16753
  if (pt === "function_call" && p.name === "spawn_agent") {
16452
16754
  const args = parseToolArguments(p.arguments);
16453
16755
  const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
16454
- const str = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
16455
- return { kind: "spawn", at, callId, taskName: str(a.task_name), model: str(a.model), effort: str(a.reasoning_effort), agentType: str(a.agent_type) };
16756
+ const str2 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
16757
+ return { kind: "spawn", at, callId, taskName: str2(a.task_name), model: str2(a.model), effort: str2(a.reasoning_effort), agentType: str2(a.agent_type) };
16456
16758
  }
16457
16759
  if (pt !== "function_call_output") return void 0;
16458
16760
  const name = toolNameFor(callId);
@@ -18602,7 +18904,7 @@ var require_sender = __commonJS({
18602
18904
  * @param {Function} [cb] Callback
18603
18905
  * @private
18604
18906
  */
18605
- getBlobData(blob, compress, options, cb) {
18907
+ getBlobData(blob, compress2, options, cb) {
18606
18908
  this._bufferedBytes += options[kByteLength];
18607
18909
  this._state = GET_BLOB_DATA;
18608
18910
  blob.arrayBuffer().then((arrayBuffer) => {
@@ -18615,12 +18917,12 @@ var require_sender = __commonJS({
18615
18917
  }
18616
18918
  this._bufferedBytes -= options[kByteLength];
18617
18919
  const data = toBuffer(arrayBuffer);
18618
- if (!compress) {
18920
+ if (!compress2) {
18619
18921
  this._state = DEFAULT;
18620
18922
  this.sendFrame(_Sender.frame(data, options), cb);
18621
18923
  this.dequeue();
18622
18924
  } else {
18623
- this.dispatch(data, compress, options, cb);
18925
+ this.dispatch(data, compress2, options, cb);
18624
18926
  }
18625
18927
  }).catch((err) => {
18626
18928
  process.nextTick(onError, this, err, cb);
@@ -18649,8 +18951,8 @@ var require_sender = __commonJS({
18649
18951
  * @param {Function} [cb] Callback
18650
18952
  * @private
18651
18953
  */
18652
- dispatch(data, compress, options, cb) {
18653
- if (!compress) {
18954
+ dispatch(data, compress2, options, cb) {
18955
+ if (!compress2) {
18654
18956
  this.sendFrame(_Sender.frame(data, options), cb);
18655
18957
  return;
18656
18958
  }
@@ -24857,20 +25159,64 @@ function parsePrRef(value) {
24857
25159
  return { owner: m[1], repo: m[2].replace(/\.git$/, ""), number };
24858
25160
  }
24859
25161
  function isActionableAwaitingHint(hint) {
24860
- if (!hint) return false;
24861
- if (hint.kind === "timer") return isValidAwaitingTimer(hint.value);
24862
- if (hint.kind === "pr-watch") return parsePrRef(hint.value) !== void 0;
24863
- return false;
25162
+ return hint !== void 0 && false;
25163
+ }
25164
+ function readAwaitingPark(hints) {
25165
+ const items = [];
25166
+ let forMs = null;
25167
+ let reason = "";
25168
+ for (const h of hints) {
25169
+ const value = h.value.trim();
25170
+ if (isAwaitingItemKind(h.kind)) {
25171
+ if (value) items.push({ kind: h.kind, value });
25172
+ } else if (h.kind === "for") {
25173
+ forMs = parseAwaitingDuration(value);
25174
+ } else if (h.kind === "reason" && !reason) {
25175
+ reason = value;
25176
+ }
25177
+ }
25178
+ return { items, forMs, reason };
25179
+ }
25180
+ function unaccountedItems(items, live) {
25181
+ return items.filter((i) => !live[LIVE_SET[i.kind]].has(i.value));
25182
+ }
25183
+ function parkIsHonoured(park, live) {
25184
+ if (park.items.length === 0) return false;
25185
+ if (park.forMs === null) return false;
25186
+ return unaccountedItems(park.items, live).length === 0;
25187
+ }
25188
+ function parkExpiresAt(park, fenceAtMs) {
25189
+ if (park.forMs === null || !Number.isFinite(fenceAtMs)) return null;
25190
+ return fenceAtMs + Math.min(park.forMs, AWAITING_FOR_MAX_MS);
24864
25191
  }
24865
25192
  function awaitingFenceIdentity(hint, fenceAt) {
24866
25193
  return createHash11("sha256").update(fenceAt).update("\0").update(hint.kind).update("\0").update(hint.value).digest("hex");
24867
25194
  }
24868
- var PR_REF_RE;
25195
+ function readGithubStatusBook(raw2) {
25196
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return {};
25197
+ const out = {};
25198
+ for (const [key, value] of Object.entries(raw2)) {
25199
+ const parsed = GithubWatchStatus.safeParse(value);
25200
+ if (parsed.success) out[key] = parsed.data;
25201
+ }
25202
+ return out;
25203
+ }
25204
+ function githubStatusKey(ref) {
25205
+ return `${ref.owner}/${ref.repo}#${ref.number}`;
25206
+ }
25207
+ var PR_REF_RE, LIVE_SET, GITHUB_STATUS_SETTING;
24869
25208
  var init_awaiting = __esm({
24870
25209
  "packages/server/src/awaiting.ts"() {
24871
25210
  "use strict";
24872
25211
  init_src();
24873
25212
  PR_REF_RE = /(?:https?:\/\/github\.com\/)?([A-Za-z0-9][\w.-]*)\/([A-Za-z0-9][\w.-]*?)(?:\/pull\/|\/pulls\/|#)(\d+)/;
25213
+ LIVE_SET = {
25214
+ shell: "shells",
25215
+ agent: "agents",
25216
+ timer: "timers",
25217
+ pr: "prs"
25218
+ };
25219
+ GITHUB_STATUS_SETTING = "waker.github.status.v1";
24874
25220
  }
24875
25221
  });
24876
25222
 
@@ -24971,11 +25317,12 @@ triage: it says neither "answer me" nor "this is finished", so it sits in the qu
24971
25317
  Frizz corrects it, twice, and then stops: rest without a fence and you will be told to pick one, which
24972
25318
  costs you a turn each time.
24973
25319
 
24974
- Pick \`question\` when you need the human, \`done\` when the work is genuinely finished, and REGISTER A
24975
- WAIT (\`mcp__frizz__watch\`) when you are blocked on a PR, CI, or your own background shell \u2014 a
24976
- registered wait is durable, survives compaction and restarts, and can be dropped when it stops
24977
- mattering. You stay OUT of the queue only while you own a live sub-agent or a valid \`awaiting\` fence;
24978
- a detached background process does NOT hold you out.
25320
+ Pick \`question\` when you need the human, \`done\` when the work is genuinely finished, and \`awaiting\`
25321
+ when you are WAITING \u2014 on a PR, or on your own background work \u2014 naming what you wait on, one line each.
25322
+
25323
+ \`awaiting\` REGISTERS NOTHING. It is how you come to rest without being bumped for a handoff, and how
25324
+ the human sees what you are waiting on. Your background shells and sub-agents are watched AUTOMATICALLY,
25325
+ fence or no fence: frizz wakes you when one finishes, every time.
24979
25326
 
24980
25327
  **WRITE IT TO BE READ COLD.** The human reads your final message in a queue, hours later, with none of
24981
25328
  your context \u2014 and they have NOT seen anything since their own last message. Every prompt you received
@@ -24986,6 +25333,16 @@ you would do next \u2014 rather than only the last thing you touched. Do not ass
24986
25333
  Be concise, but do not omit. Use whatever markup makes it readable: headings, bullets, tables, code.
24987
25334
  The one place brevity is a RULE is a \`done\` body \u2014 see its entry below.
24988
25335
 
25336
+ **NAME THE PULL REQUEST, EVERY TIME YOU REST.** If your effort has a PR \u2014 one you opened, one you are
25337
+ reviewing, one you are fixing \u2014 write its number in EVERY resting message, not just the one where it
25338
+ first appeared. The human reads these as a queue of cards from a dozen threads at once, and a card that
25339
+ says "pushed the fix, CI is green" without a number is a card they cannot place: they have to open the
25340
+ thread to learn which PR it was about (maintainer 2026-08-16: "I keep being unclear what PR is being
25341
+ implemented in a given chat"). One \`#123\` in the first line or two costs you nothing and answers it.
25342
+
25343
+ Several PRs in play \u21D2 name them all. No PR yet \u21D2 say what the work is against instead (a branch, an
25344
+ issue, a file) \u2014 the point is that the card identifies its own subject without being opened.
25345
+
24989
25346
  Frizz turns \`#123\`, \`owner/repo#123\` and bare commit hashes into github.com links itself, in every
24990
25347
  surface it draws your prose, for any project whose origin remote is github.com. Write the bare form;
24991
25348
  wrapping one in a \`[link](url)\` yourself is redundant. Everything else you want clickable \u2014 a file, a
@@ -24995,8 +25352,14 @@ Use at most ONE fenced signal block, at the very END of the final message. The f
24995
25352
  state; the body is the card the human reads. A \` \`\`\`question \` block and real permission prompts are
24996
25353
  higher-priority asks than either fence below.
24997
25354
 
25355
+ **THE FENCE AND THE PROSE ABOVE IT ARE TWO SURFACES, NOT ONE MESSAGE WRITTEN TWICE.** The card is the
25356
+ LEDGER: one bullet per deliverable, what shipped and where. The prose is only what a ledger cannot hold
25357
+ \u2014 the reasoning, the caveat, the thing the human has to do. If a sentence would read the same in either,
25358
+ it belongs in exactly ONE of them. Writing the summary and then restating it as bullets costs the human
25359
+ a second read for nothing, and it is the commonest thing wrong with an otherwise good handoff.
25360
+
24998
25361
  - \` \`\`\`done \` \u2014 you COMPLETED the effort's real work: code LANDED on the project's mainline, a plan or
24999
- doc written, or a commissioned research/audit report finished. Body: ONE TO THREE SENTENCES, then a
25362
+ doc written, or a commissioned research/audit report finished INTO A FILE. Body: ONE TO THREE SENTENCES, then a
25000
25363
  BULLET LIST, one \`- \` item per task, each opening with a **bolded verb phrase** and naming what
25001
25364
  shipped and where. This is the ONE place the short shape is a rule \u2014 a dismissal card is scanned, not
25002
25365
  read, so it is a ledger. Everywhere else, say as much as the reader needs. The card renders inline markdown, so backtick every path,
@@ -25011,62 +25374,151 @@ higher-priority asks than either fence below.
25011
25374
  \`\`\`
25012
25375
 
25013
25376
  **\`done\` is a DISMISSAL, not a summary** \u2014 its card files the thread away where nobody looks again,
25014
- and anything living only in the conversation goes with it. If the thread points at future work AT ALL,
25015
- bare-rest instead; uncertain is not done. What blocks \`done\` is work you still OWE, not a process that
25016
- happens to still be RUNNING: a watcher, dev server, or poller you have already moved on from does not
25017
- hold it back \u2014 name it in the body and fence anyway. A \`done\` fence REPLACES the awaiting-background
25018
- card with your own completion card, which is what turns a finished thread with a live watcher into a
25019
- one-click dismissal instead of a card the human has to keep deciding about. Code written but not LANDED
25020
- is not done (a commit, a pushed branch, an open PR is work still ahead of the merge \u2014 where the project
25021
- uses PRs, \`done\` waits for the MERGE, so park the PR on \` \`\`\`awaiting \` with \`pr-watch:\`). An
25022
- investigation headed for a fix is NOT \`done\`; the fix is still owed. Two cases DO earn \`done\` on
25023
- something other than landed code: a commissioned research or audit EFFORT, whose finished report is the
25024
- deliverable, earns \`done\`; and so does a PLANNING session whose plan file is FULLY written and
25025
- PERSISTED (\`.frizz/plans/<topic>.md\`), because that artifact already lives outside the thread, so
25026
- dismissing the thread loses nothing.
25027
-
25028
- - \` \`\`\`awaiting \` \u2014 a durable wait frizz's scheduler manages. Lead the body with one or more
25029
- \`kind: value\` hint lines, then concise prose naming the exact wake condition. New waits use only:
25030
-
25031
- - \`pr-watch: owner/repo#NUMBER\` \u2014 frizz polls the PR and resumes you on ANY new review, approval, or
25032
- comment, from a HUMAN OR A BOT alike. Your thread STAYS IN THE QUEUE as a visible "watching it"
25033
- handoff. This is the default for a PR you opened. ONE line watches ONE PR, and a fence may carry
25034
- SEVERAL \u2014 repeat the line per PR, across any mix of repos, and activity on ANY of them wakes you.
25035
- Never downgrade to a periodic \`timer:\` sweep just because you are tracking more than one PR. The
25036
- body keeps its first 8 hint lines and silently DROPS the rest, so past 8 watch the 8 that matter
25037
- and cover the tail with a \`timer:\`.
25038
-
25039
- **READ THE PR BEFORE YOU PARK ON IT.** After your first park on a given PR the baseline is the
25040
- INSTANT YOU REST \u2014 only what arrives AFTER can wake you again. Frizz covers the first park by
25041
- replaying whatever was already sitting there, once, so review you never read cannot be lost; but
25042
- that replay costs you a whole turn to answer something you could have read before resting. Run
25043
- \`gh pr view N --comments\` first, deal with what is there, and only then write the fence.
25044
- "Waiting on review" is a claim that review has not arrived; go and check that it is true.
25045
- - \`human: <actor + exact review/approval>\` \u2014 a third party whose action cannot be supplied in this
25046
- frizz conversation. PARKS you in the dimmed Held band. A bot, CI gate, or merge queue is NOT a human
25047
- wait. Pair with \`pr-watch:\` when a GitHub PR exists, or \`timer:\` when none does.
25048
- - \`timer: <ISO-8601 instant>\` \u2014 the durable scheduler resumes you then, across restarts
25049
- (\`{{FRIZZ_RESUME_CMD}}\`).
25377
+ and anything living only in the conversation goes with it. So the test is never "have I stopped
25378
+ working": it is **WHAT IS LOST IF NOBODY EVER OPENS THIS THREAD AGAIN?** Name one thing and it is not
25379
+ done. If the thread points at future work AT ALL, you are not done \u2014 see **When the work is finished
25380
+ but the thread found more** for the ways out of that, none of which is stretching \`done\`. Uncertain
25381
+ is not done.
25382
+
25383
+ **A RECOMMENDATION IS NOT A CONCLUSION, AND AN UNSENT DRAFT IS NOT A DELIVERABLE.** When the verdict
25384
+ is that SOMEONE SHOULD NOW DO SOMETHING \u2014 merge it, decline it, post this comment, pick one of these
25385
+ two designs, press the button you are not allowed to press \u2014 that someone is the human, so the effort
25386
+ ends on a \` \`\`\`question \` carrying your recommendation as option A. Same for anything you WROTE but
25387
+ did not SEND: a drafted comment, reply, issue body or release note you were right not to publish
25388
+ unilaterally is one click short of delivered, and \`done\` files that draft away with the thread.
25389
+
25390
+ **A \`done\` MESSAGE SAYS WHAT HAPPENED. DELETE EVERY DANGLING "WORTH DOING LATER".** "One thing to
25391
+ carry forward\u2026", "a follow-up could\u2026", "someone should add a changelog line before this ships" \u2014 a
25392
+ forward-reference parked in a dismissal card is clutter: too weak for anyone to act on, too present to
25393
+ ignore, and archived unread. Each one resolves four ways and there is no fifth \u2014 DO it, SPAWN it onto
25394
+ its own card, ASK about it, or DROP it. Something the human must do NOW ("re-pull before you restart")
25395
+ is not a dangling idea; that is the handoff, and it belongs in the prose.
25396
+
25397
+ What blocks \`done\` is work still OWED \u2014 by you, or by the human because of what you found \u2014 not a
25398
+ BACKGROUND PROCESS that happens to still be running: a watcher, dev server, or poller you have already
25399
+ moved on from does not hold it back \u2014 name it in the body and fence anyway. **Read that carve-out
25400
+ narrowly: its subject is a running process, and nothing else.** Follow-up work you DISCOVERED blocks
25401
+ \`done\` just as hard as work you were assigned, even when it is someone else's to do \u2014 "not mine" is
25402
+ not "not owed", because the card takes the finding with it either way. A \`done\` fence REPLACES the
25403
+ awaiting-background card with your own completion card, which is what turns a finished thread with a
25404
+ live watcher into a one-click dismissal instead of a card the human has to keep deciding about. Code
25405
+ written but not LANDED is not done (a commit, a pushed branch, an open PR is work still ahead of the
25406
+ merge \u2014 where the project uses PRs, \`done\` waits for the MERGE, so park the PR on \` \`\`\`awaiting \`
25407
+ with \`pr:\`). An investigation headed for a fix is NOT \`done\`; the fix is still owed. Two cases
25408
+ DO earn \`done\` on something other than landed code, and BOTH earn it the same way \u2014 by passing the
25409
+ test above, because the deliverable is a FILE the human opens without this thread: a commissioned
25410
+ research or audit EFFORT, whose finished report is written to that file, earns \`done\`; and so does a
25411
+ PLANNING session whose plan file is FULLY written and PERSISTED (\`.frizz/plans/<topic>.md\`), because
25412
+ that artifact already lives outside the thread, so dismissing the thread loses nothing. Neither
25413
+ exception stretches to a report that ENDS IN A DECISION the human has yet to make: write the file AND
25414
+ ask the question.
25415
+
25416
+ - \` \`\`\`awaiting \` \u2014 you have STOPPED, and you are waiting on work that is actually running. The
25417
+ fence is PURE STRUCTURE: a list of things frizz can look up, how long the park may stand, and one line
25418
+ of prose. Nothing else belongs in it.
25050
25419
 
25051
25420
  \`\`\`awaiting
25052
- pr-watch: acme/app#391
25053
- PR is open and CI is green. Watching for review \u2014 I'll address comments or merge on approval.
25421
+ shell: bzvtnt3ig
25422
+ agent: <the runtime agent id>
25423
+ timer: tmr_a1b2c3d4e5f6
25424
+ pr: acme/app#391
25425
+ for: 2h
25426
+ ---
25427
+ Waiting on the three-platform run before porting the v2 drivers.
25428
+
25429
+ - the macOS leg is the one that has been flaky
25430
+ - if it goes red I will bisect rather than re-run
25054
25431
  \`\`\`
25055
25432
 
25056
- \`pr:\` / \`ci:\` / \`session:\` remain parser compatibility for existing transcripts only; never emit
25057
- them. The operator's own answer or approval is a \` \`\`\`question \`, not \`awaiting\`.
25433
+ **FRONTMATTER, THEN MARKDOWN.** The structural lines come first; a \`---\` line ends them and everything
25434
+ after it is ORDINARY MARKDOWN \u2014 as much as you want, with paragraphs, lists and links. That prose is what
25435
+ the human reads on your resting card, so write it as a handoff, not as a label.
25436
+
25437
+ Omit the \`---\` and the whole fence is structure. A single \`reason: <one line>\` line also still works, but
25438
+ prefer the delimiter: a handoff is prose, and one line is not enough room for one.
25439
+
25058
25440
 
25059
- **CI, releases, deploys and merge progression are automatable \u2014 never \`awaiting\` them.** They stay
25060
- ACTIVE: dispatch a sub-agent to own the wait (its liveness keeps you Active and its return re-invokes
25061
- you), or use a timer when the next check genuinely belongs at a later wall-clock time.
25441
+ - \`shell:\` / \`agent:\` \u2014 the runtime id you were handed when you launched it ("Command running in
25442
+ background with ID: bzvtnt3ig").
25443
+
25444
+ **LOST AN ID? \`mcp__frizz__activity\` PRINTS THEM ALL** \u2014 every shell, sub-agent, timer and PR you have
25445
+ out, each with the exact line your fence needs. Call it rather than guessing: a guessed id names
25446
+ nothing, so frizz refuses the park and bumps you.
25447
+ - \`timer:\` \u2014 a timer id from \`mcp__frizz__timer\`. NOT an instant; the tool creates the timer.
25448
+ - \`pr:\` \u2014 a pull request you REGISTERED with \`mcp__frizz__watch_pr\`, written \`owner/repo#N\`. Register
25449
+ FIRST: this line states the wait, it does not create one. That tool wakes you on CI turning green or red,
25450
+ and on every later review, approval or comment, human or bot, for as long as the PR lives.
25451
+ - \`for:\` \u2014 **REQUIRED**, and a DURATION: \`30s\`, \`15m\`, \`2h\`, \`3d\`. Never an instant. When it runs
25452
+ out frizz brings you back to re-check everything; re-parking is fine and uncapped.
25453
+ - \`reason:\` \u2014 a one-line alternative to the \`---\` block above, kept for fences already written
25454
+ that way. Prefer the delimiter.
25455
+
25456
+ **REGISTERING IS NOT PARKING, AND PARKING IS NOT REGISTERING.** Launching a shell, dispatching a
25457
+ sub-agent, setting a timer, registering a PR watcher \u2014 those just happen, and frizz watches them
25458
+ whether or not you ever write a fence. This fence is only how you declare that you have STOPPED, and
25459
+ name which of them you stopped for.
25460
+
25461
+ **A FENCE THAT NAMES NOTHING IS NOT A PARK.** \`for:\` and \`reason:\` describe a wait; they are not one.
25462
+ If you are waiting on CI, REGISTER the PR and name it \u2014 do not write "waiting on the checks" and a
25463
+ duration, because nothing will wake you and your thread just sits in the queue. And if you are not
25464
+ actually waiting on anything, you are not awaiting: end with \`\`\`done, or ask a \`\`\`question.
25465
+
25466
+ **FRIZZ CHECKS EVERY NAME THE MOMENT YOU REST.** All of them live \u21D2 your thread parks in the dimmed
25467
+ Held band. Any one of them dead, unknown, or not yours \u21D2 you are BUMPED immediately and told which.
25468
+ A fence naming nothing at all is not a park either \u2014 it is a claim to be waiting with no way to be
25469
+ woken, which is the exact failure this grammar exists to make impossible.
25470
+
25471
+ **WAITING ON A PERSON IS A \` \`\`\`question \`.** There is no human gate and there is no prose park.
25472
+ If you need a human, ask them something answerable.
25473
+
25474
+ **CI, RELEASES, DEPLOYS AND MERGE PROGRESSION ARE AUTOMATABLE \u2014 never \` \`\`\`awaiting \` them
25475
+ BLINDLY.** For a PULL REQUEST that is what \`mcp__frizz__watch_pr\` is for: register it and you are told
25476
+ when CI settles, so the wait is real rather than a hope. For anything else, stay ACTIVE \u2014 dispatch a
25477
+ sub-agent to own the wait (its return re-invokes you), or set a timer and name it here.
25478
+
25479
+ **THE PARK SURVIVES A RESTART.** The registration and the fence are both durable, so frizz brings you
25480
+ back (\`{{FRIZZ_RESUME_CMD}}\`) even if its own server or your daemon is replaced while you wait.
25062
25481
 
25063
25482
  **A follow-up clears the previous fence.** If the human says "back to awaiting", never answer that it
25064
- is already parked and never rely on the old fence: re-check the blocker, then re-emit a fresh
25065
- \` \`\`\`awaiting \` with a current \`human:\` / \`timer:\` / \`pr-watch:\` hint. If it turns out to be
25066
- automatable, arm the active wait instead and do not fence.
25483
+ is already parked and never rely on the old fence: re-check what you are waiting on, confirm it is
25484
+ still running, and emit a FRESH fence naming it. If it turns out to be automatable, arm the active
25485
+ wait instead and do not fence.
25067
25486
 
25068
25487
  - \` \`\`\`question \` \u2014 you need the human's input; see **Questions for the human**.
25069
25488
 
25489
+ ### When the work is finished but the thread found more
25490
+
25491
+ Your mandate is complete and you turned up something else: a bug you were not sent for, a fix nobody
25492
+ landed, a follow-up the change implies. \`done\` is barred \u2014 the finding dies with the card. \`awaiting\`
25493
+ is barred \u2014 nothing is running. \`question\` looks barred \u2014 the human asked nothing. **That deadlock is
25494
+ not real, and stretching \`done\` is not how you break it.** Take the first exit that fits:
25495
+
25496
+ 1. **DO IT \u2014 the default, and it covers far more than "small".** Anything inside your change's own blast
25497
+ radius is exit 1: the fix your change implies, the test it needs, the breakage it revealed, the caller
25498
+ it left stale. **Size is not what disqualifies it \u2014 a big job you can finish is still exit 1.** If it
25499
+ is more than one sitting, dispatch an in-session SUB-AGENT: its result comes BACK to you, so the
25500
+ follow-up lands on YOUR card, in your diff, under one review. One thread that finished two things
25501
+ beats two threads that each finished one and never learned of the other. Then \`done\` is honest with
25502
+ nothing left over.
25503
+ 2. **ASK** \u2014 the human should choose whether or how it happens. An ordinary \` \`\`\`question \`; "nothing
25504
+ was pending" is not an objection. This is the exit for work you cannot bound, cannot verify, or that
25505
+ is genuinely the human's call \u2014 and it is where a separable effort goes, as an OPTION they pick,
25506
+ rather than a card you create for them.
25507
+ 3. **HAND IT OFF TO ITS OWN CARD \u2014 the LAST resort, not the tidy one.** \`mcp__frizz__spawn_thread\` puts
25508
+ a separable effort on the board as its own thread; put the returned \`[title](/thread/<slug>)\` link in
25509
+ the body. **A spawned thread is FIRE-AND-FORGET: it reports to the HUMAN, never to you, so nothing it
25510
+ learns can ever reach this effort or its siblings.** That is the whole cost, and it is not
25511
+ theoretical \u2014 one thread here spawned four, three of those spawned more, and three separate
25512
+ descendants independently rediscovered the SAME root-cause commit over twenty hours because none of
25513
+ them could tell the others. Reach for it only when the work genuinely cannot ride on your card: a
25514
+ different repo, a different long-lived runtime, an effort that must outlive yours. If a sub-agent
25515
+ could do it, exit 1. If the human should choose, exit 2. Never spawn merely to clear your own \`done\`.
25516
+ 4. **DROP IT** \u2014 it is not worth an effort, a card, or a question. Then it is not worth a SENTENCE
25517
+ either: delete it. Do not park it in the handoff as "one thing to carry forward".
25518
+
25519
+ A bare rest is the residual, not a plan \u2014 legitimate only when nothing above fits, and frizz will ask
25520
+ you twice for a fence before it gives up.
25521
+
25070
25522
  A mid-conversation turn carries NO fence. Nor is a turn on a thread that still points at future work \u2014
25071
25523
  a live code-change discussion above all \u2014 ever \`done\`.`;
25072
25524
  AGENT_COMPLETION = `## Agent completion invariant
@@ -25165,6 +25617,23 @@ Three specific traps, each of which has ended a turn that should have continued:
25165
25617
  - **Pre-emptively worrying about context budget.** You do not get to stop early because the turn feels
25166
25618
  long. If context genuinely runs out, the harness summarizes and you continue.
25167
25619
 
25620
+ **AND THE INSTRUCTION IS ALSO THE CEILING.** Everything above pushes you not to stop early; none of it
25621
+ is a licence to grow the job. "Keep going" has no upper bound of its own \u2014 there is always more to do
25622
+ in any repository \u2014 so a worker that may not stop until nothing is left can only ever stop by widening
25623
+ what it was asked. Finish the mandate you were given, and nothing else.
25624
+
25625
+ - **Work you notice on the way is a FINDING, not a task.** The bug beside the one you were sent for,
25626
+ the refactor the code obviously wants, the second issue the first one touches: name each in one line
25627
+ in your handoff and let the human dispatch it. Widening is not thoroughness \u2014 it buries the answer
25628
+ they asked for under changes they now have to review, and it spends hours they did not agree to.
25629
+ - **If the deliverable is a DOCUMENT, the document is the ending.** A triage, a review, an
25630
+ investigation, a recommendation, a plan \u2014 when that is what you were asked for, the finished
25631
+ write-up IS the work. Implementing what it proposes is the NEXT job, and it is not yours unless you
25632
+ were asked.
25633
+ - **An unanswered question is not permission to build the answer.** If you asked and nobody has
25634
+ replied, decide what the task itself needs and carry on INSIDE it. Do not treat the silence as
25635
+ authorization for the work the answer would have unlocked.
25636
+
25168
25637
  **Your default is to DECIDE.** A reversible call costs minutes to redo; a round-trip to the human costs
25169
25638
  hours \u2014 they may not read your question for hours, and the whole effort sits idle until they do. If it
25170
25639
  is derivable from the code, the conventions, or ordinary engineering judgment, it is yours: make it,
@@ -25173,12 +25642,20 @@ say which way you went, and keep moving.
25173
25642
  **The test that catches almost every bad question: if you are about to mark one option
25174
25643
  \`(recommended)\`, you already know the answer \u2014 so implement it instead of asking.** Writing a
25175
25644
  confident rationale for your own recommendation is proof the decision was yours to make. The same goes
25176
- for "want me to fix it?" on a bug you were dispatched to fix or noticed on the way, "should I also
25645
+ for "want me to fix it?" on a bug you were dispatched to fix, "should I also
25177
25646
  handle X?" where X is obviously in scope, and any question about a name, a default value, a file
25178
25647
  location, an error message, a flag spelling, or which of two equivalent designs to use. Those are
25179
- granular implementation calls. Make them, note the notable ones in your handoff, and move on \u2014 a
25648
+ granular implementation calls. (A bug you merely NOTICED is not a question either \u2014 by the ceiling
25649
+ above it is a line in your handoff, not work to adopt and not permission to seek.) Make them, note the
25650
+ notable ones in your handoff, and move on \u2014 a
25180
25651
  decision the human can see and reverse in a line of code is not worth an hours-long stall.
25181
25652
 
25653
+ **That test inverts when knowing the answer and being ABLE TO ACT ON IT come apart.** Where the act is
25654
+ one you are not permitted or not able to perform \u2014 a read-only boundary, a comment that goes out under
25655
+ the human's name, a merge, a close, a publish, a spend \u2014 your recommendation is not a decision you take
25656
+ silently. It becomes the QUESTION, with the recommendation as option A and the act spelled out concretely
25657
+ enough to approve in one word. Never resolve that fork by fencing \` \`\`\`done \` on the investigation.
25658
+
25182
25659
  Stop only when a wrong guess would be BOTH costly AND hard to undo:
25183
25660
 
25184
25661
  - destructive or irreversible actions (history rewrite, data loss, force-merge, a published release);
@@ -25471,8 +25948,9 @@ When delegation is explicitly authorized:
25471
25948
  Keep automatable waits inside the active turn through the selected persistent \`exec_command\` /
25472
25949
  \`write_stdin\` monitor session until it reaches a terminal condition. Then diagnose/fix/retry/merge as
25473
25950
  authorized. Do not emit \`awaiting\` for CI,
25474
- automated review, release, or merge progression. Those tool sessions are process-bound; use a durable
25475
- \`timer:\` awaiting fence only when the next check belongs at a named wall-clock instant. A partial
25951
+ automated review, release, or merge progression. Those tool sessions are process-bound; set a durable
25952
+ timer with \`mcp__frizz__timer\` and name it in your fence only when the next check belongs at a later
25953
+ wall-clock time. A partial
25476
25954
  \`gh pr checks\` rollup is not a CI-green verdict: inspect workflow runs for the exact PR head too, and
25477
25955
  treat \`ACTION_REQUIRED\` fork gates as pending. When no valid project monitor is declared, run the
25478
25956
  bundled \`ci-watch.mjs\` / \`review-watch.mjs\` named above instead of inventing a loop of your own.
@@ -25497,7 +25975,7 @@ So when a wait is longer than a single yield budget:
25497
25975
  3. Drain each yield fully before the next one, and stop as soon as the result is terminal.
25498
25976
 
25499
25977
  If you find yourself on the third identical poll with no new output, you are in this anti-pattern:
25500
- switch to a blocking monitor, or emit a \`timer:\` awaiting fence for a genuinely distant check.
25978
+ switch to a blocking monitor, or set a timer with \`mcp__frizz__timer\` for a genuinely distant check.
25501
25979
 
25502
25980
  When you genuinely need to work alongside a disposable local process (a dev server or long gate), use
25503
25981
  the managed unified-exec handoff: create the \`tools.exec_command(...)\` promise, call
@@ -25512,8 +25990,9 @@ the cell can wake a rested turn.
25512
25990
 
25513
25991
  Never implement a recurring wake as a shell \`sleep\` loop. Shell output cannot create a new Codex turn,
25514
25992
  so an unattended loop is not a wake mechanism. When a later wall-clock check genuinely belongs after a
25515
- rest, emit an \`awaiting\` fence with \`timer: <ISO-8601 instant>\`; Frizz's durable scheduler starts the
25516
- new turn at that instant. The timer is one-shot; do not try to make it recur.
25993
+ rest, set one with \`mcp__frizz__timer\` and name the id it returns on a \`timer:\` line in your
25994
+ \`\`\`awaiting fence; Frizz's durable scheduler starts the new turn then. Each timer is one-shot \u2014 set
25995
+ several if you need several, and do not try to make one recur.
25517
25996
 
25518
25997
  ## Your model and reasoning effort
25519
25998
 
@@ -25538,7 +26017,15 @@ effort that deserves its own card and whose output you do not need.
25538
26017
 
25539
26018
  Give it a self-contained \`prompt\` and choose \`model\` + \`effort\` by the new task's complexity (both
25540
26019
  required). It returns a \`[title](/thread/<slug>)\` link \u2014 put that in your handoff so the human can open
25541
- it.`;
26020
+ it.
26021
+
26022
+ **It is the LAST resort among the exits, never the tidy one.** A finding you turned up is not by itself a
26023
+ reason to spawn: the ordered exits put DOING it (with a sub-agent, whose result comes back to you) first
26024
+ and ASKING second, precisely because both keep the work reachable. A spawned thread does not \u2014 it reports
26025
+ to the HUMAN and nothing it learns returns to you or to its siblings, so a chain of them re-derives the
26026
+ same facts in parallel and nobody notices. Spawn only when the work genuinely cannot ride on your card,
26027
+ and never merely to clear your own \` \`\`\`done \`. See **When the work is finished but the thread found
26028
+ more**.`;
25542
26029
  THREAD_EXECUTION = {
25543
26030
  claude: `## Thread types
25544
26031
 
@@ -26967,17 +27454,6 @@ function buildClaudeResumeCommand(opts) {
26967
27454
  if (opts.message) argv.push(opts.message);
26968
27455
  return argv;
26969
27456
  }
26970
- function armDefaultGoal(storage, slug) {
26971
- try {
26972
- storage.setRecurringPromptBySlug(slug, {
26973
- prompt: DEFAULT_RECURRING_PROMPT,
26974
- ...DEFAULT_GOAL_TRIGGERS,
26975
- intervalMs: null,
26976
- armedAt: (/* @__PURE__ */ new Date()).toISOString()
26977
- });
26978
- } catch {
26979
- }
26980
- }
26981
27457
  function createDispatcher(deps) {
26982
27458
  const readBoardSource = deps.readBoard ?? readBoard;
26983
27459
  const frizzDir = join23(deps.project.dir, ".frizz");
@@ -27063,7 +27539,6 @@ function createDispatcher(deps) {
27063
27539
  effort: effort ?? null,
27064
27540
  permission_mode: permissionMode
27065
27541
  });
27066
- armDefaultGoal(deps.storage, slug);
27067
27542
  deps.storage.setBackend(slug, "codex");
27068
27543
  deps.storage.setAgentSession(slug, spawned.binding.codexSessionId);
27069
27544
  deps.storage.setCodexRuntime(slug, "app-server");
@@ -27124,7 +27599,6 @@ function createDispatcher(deps) {
27124
27599
  effort: effort ?? null,
27125
27600
  permission_mode: permissionMode
27126
27601
  });
27127
- armDefaultGoal(deps.storage, slug);
27128
27602
  deps.storage.setBackend(slug, "claude");
27129
27603
  deps.storage.setClaudeRuntime(slug, "broker");
27130
27604
  void deps.board.rebuild().catch(() => {
@@ -27501,6 +27975,7 @@ var init_plan_files = __esm({
27501
27975
  import {
27502
27976
  watch as fsWatch
27503
27977
  } from "node:fs";
27978
+ import { homedir as homedir12 } from "node:os";
27504
27979
  import { join as join25 } from "node:path";
27505
27980
  import watcher from "@parcel/watcher";
27506
27981
  function appServerTurnStalled(liveness, lastActivityAt, nowMs) {
@@ -27564,6 +28039,53 @@ function stampStoppableShells(shells, row) {
27564
28039
  if (isHeadlessRow(row)) return shells;
27565
28040
  return shells.map((shell) => shell.stoppable ? { ...shell, stoppable: false } : shell);
27566
28041
  }
28042
+ function declaredWaitIds(tele) {
28043
+ if (tele?.lastFence?.kind !== "awaiting") return [];
28044
+ return readAwaitingPark(tele.lastFence.hints).items.filter((i) => i.kind === "shell" || i.kind === "agent").map((i) => i.value);
28045
+ }
28046
+ function liveWaitHandles(tele) {
28047
+ const handles = /* @__PURE__ */ new Set();
28048
+ for (const shell of tele?.bgShells ?? []) {
28049
+ if (shell.state !== "running") continue;
28050
+ if (shell.id) handles.add(shell.id);
28051
+ if (shell.taskId) handles.add(shell.taskId);
28052
+ if (shell.label) handles.add(shell.label);
28053
+ }
28054
+ for (const agent of tele?.subAgents ?? []) {
28055
+ if (!isDirectSubAgent(agent) || agent.state !== "running") continue;
28056
+ handles.add(agent.id);
28057
+ if (agent.label) handles.add(agent.label);
28058
+ }
28059
+ return handles;
28060
+ }
28061
+ function hasDeclaredBackgroundPark(tele, nowMs) {
28062
+ const named = declaredWaitIds(tele);
28063
+ if (named.length === 0) return false;
28064
+ const live = liveWaitHandles(tele);
28065
+ if (!named.every((id) => live.has(id))) return false;
28066
+ const restedMs = Date.parse(tele?.lastAssistantAt ?? "");
28067
+ if (Number.isFinite(restedMs) && nowMs - restedMs > DECLARED_PARK_MAX_MS) return false;
28068
+ return true;
28069
+ }
28070
+ function hasDeclaredWait(tele, nowMs) {
28071
+ if (hasDeclaredBackgroundPark(tele, nowMs)) return true;
28072
+ if (tele?.lastFence?.kind === "awaiting" && hasParkedPrWatch(tele)) return true;
28073
+ return (tele?.subAgents ?? []).some((a) => isDirectSubAgent(a) && a.state === "running");
28074
+ }
28075
+ function heldByRunningChecks(tele, github, registered) {
28076
+ if (tele?.lastFence?.kind !== "awaiting") return false;
28077
+ const refs = [];
28078
+ for (const hint of tele.lastFence.hints) {
28079
+ if (hint.kind !== "pr") continue;
28080
+ const ref = parsePrRef(hint.value);
28081
+ if (ref && registered.has(githubStatusKey(ref))) refs.push(githubStatusKey(ref));
28082
+ }
28083
+ if (refs.length === 0) return false;
28084
+ return refs.every((key) => {
28085
+ const status = github[key];
28086
+ return status?.checks === "running" && status.state === "open";
28087
+ });
28088
+ }
27567
28089
  function hasLiveOwnWork(tele) {
27568
28090
  return Boolean(
27569
28091
  tele?.subAgents?.some((agent) => isDirectSubAgent(agent) && agent.state === "running") || tele?.bgShells?.some((shell) => shell.state === "running") || // A PARKED PR WATCHER COUNTS (2026-08-13). It is the same shape of thing as a detached shell: the
@@ -27577,46 +28099,47 @@ function hasLiveOwnWork(tele) {
27577
28099
  }
27578
28100
  function hasParkedPrWatch(tele) {
27579
28101
  if (tele?.lastFence?.kind !== "awaiting") return false;
27580
- return tele.lastFence.hints.some((hint) => hint.kind === "pr-watch" && parsePrRef(hint.value) !== void 0);
28102
+ return tele.lastFence.hints.some((hint) => hint.kind === "pr" && parsePrRef(hint.value) !== void 0);
27581
28103
  }
27582
28104
  function bgSnoozeArmed(row) {
27583
28105
  return row.bg_snooze_rested_at != null && row.rested_at != null && row.bg_snooze_rested_at === row.rested_at;
27584
28106
  }
27585
- function hasParkedExternalWait(tele, nowMs) {
27586
- if (tele?.lastFence?.kind !== "awaiting") return false;
27587
- return tele.lastFence.hints.some(
27588
- (hint) => hint.kind === "human" || hint.kind === "timer" && isValidAwaitingTimer(hint.value) && Date.parse(hint.value) > nowMs
27589
- );
28107
+ function hasParkedExternalWait(tele, _nowMs) {
28108
+ return false;
27590
28109
  }
27591
- function githubWatchViews(slug, tele, fenceAt) {
27592
- if (tele?.lastFence?.kind !== "awaiting") return [];
28110
+ function fenceWatchViews(slug, tele, fenceAt, github = {}, registered = []) {
27593
28111
  const seen = /* @__PURE__ */ new Set();
27594
28112
  const out = [];
27595
- for (const hint of tele.lastFence.hints) {
27596
- if (hint.kind !== "pr-watch") continue;
27597
- const ref = parsePrRef(hint.value);
27598
- if (!ref) continue;
27599
- const target = `${ref.owner}/${ref.repo}#${ref.number}`;
27600
- if (seen.has(target)) continue;
27601
- seen.add(target);
28113
+ for (const w of registered) {
28114
+ if (seen.has(`github:${w.target}`)) continue;
28115
+ seen.add(`github:${w.target}`);
27602
28116
  out.push({
27603
- id: `github:${slug}:${target}`,
28117
+ id: `github:${slug}:${w.target}`,
27604
28118
  kind: "github",
27605
- target,
28119
+ target: w.target,
27606
28120
  state: "armed",
27607
- // When the worker PARKED — which is what the strip's duration should count from, and what the
27608
- // watcher's own activity baseline is keyed on. Falls back to the thread's last activity when the
27609
- // fold has no instant for the fence.
27610
- createdAt: fenceAt ?? tele.lastAssistantAt ?? (/* @__PURE__ */ new Date()).toISOString()
28121
+ createdAt: w.createdAt,
28122
+ // Absent until the poller's first successful read. The card says "checking…" then, rather than
28123
+ // inventing a verdict — an unpolled PR and a PR with no CI are different facts.
28124
+ ...github[w.target] ? { github: github[w.target] } : {}
27611
28125
  });
27612
28126
  }
28127
+ if (tele?.lastFence?.kind !== "awaiting") return out;
28128
+ const createdAt = fenceAt ?? tele.lastAssistantAt ?? (/* @__PURE__ */ new Date()).toISOString();
28129
+ for (const hint of tele.lastFence.hints) {
28130
+ if (hint.kind !== "shell" && hint.kind !== "agent") continue;
28131
+ const target = hint.value.trim();
28132
+ if (!target || seen.has(`shell:${target}`) || !liveWaitHandles(tele).has(target)) continue;
28133
+ seen.add(`shell:${target}`);
28134
+ out.push({ id: `shell:${slug}:${target}`, kind: "shell", target, state: "armed", createdAt });
28135
+ }
27613
28136
  return out;
27614
28137
  }
27615
28138
  function hasFreshDelivery(row, processGone) {
27616
28139
  if (processGone) return false;
27617
28140
  return parseDeliveryLedger(row.delivery_ledger).some((d) => d.state === "pending" || d.state === "enqueued");
27618
28141
  }
27619
- function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, excuseLiveOwnWork = true, deliveryProcessGone = false) {
28142
+ function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, excuseLiveOwnWork = true, deliveryProcessGone = false, github = {}, registeredPrWatches = /* @__PURE__ */ new Set()) {
27620
28143
  if (futureSnooze(row, nowMs)) return false;
27621
28144
  if (hasActionableInteraction) return true;
27622
28145
  if (runtime === "perm-prompt") return true;
@@ -27630,15 +28153,17 @@ function deriveNeedsYou(row, tele, runtime, hasActionableInteraction = false, no
27630
28153
  if (hasParkedExternalWait(tele, nowMs)) return false;
27631
28154
  if (limitPause?.autoResume) return false;
27632
28155
  if (excuseLiveOwnWork && runtime !== "exited" && hasLiveBackgroundWork(tele) && !tele?.lastFence) return false;
28156
+ if (excuseLiveOwnWork && runtime !== "exited" && hasDeclaredBackgroundPark(tele, nowMs)) return false;
28157
+ if (excuseLiveOwnWork && runtime !== "exited" && heldByRunningChecks(tele, github, registeredPrWatches)) return false;
27633
28158
  if (runtime !== "exited" && hasLiveOwnWork(tele) && tele?.lastFence?.kind !== "done") return !bgSnoozeArmed(row);
27634
28159
  if (tele?.lastFence?.kind === "done") return true;
27635
28160
  return true;
27636
28161
  }
27637
- function deriveAwaitingBackground(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, deliveryProcessGone = false) {
27638
- if (runtime !== "turn-idle" || !hasLiveOwnWork(tele)) return false;
28162
+ function deriveAwaitingBackground(row, tele, runtime, hasActionableInteraction = false, nowMs = Date.now(), limitPause = void 0, deliveryProcessGone = false, github = {}, registeredPrWatches = /* @__PURE__ */ new Set()) {
28163
+ if (runtime !== "turn-idle" || !hasDeclaredWait(tele, nowMs)) return false;
27639
28164
  if (hasActionableInteraction || tele?.pendingAsk || tele?.nativeInputRequired || tele?.pendingQuestion) return false;
27640
28165
  if (tele?.lastFence?.kind === "done") return false;
27641
- if (tele?.lastFence?.kind === "awaiting" && !hasParkedPrWatch(tele)) return false;
28166
+ if (tele?.lastFence?.kind === "awaiting" && !hasParkedPrWatch(tele) && !hasDeclaredBackgroundPark(tele, nowMs)) return false;
27642
28167
  return deriveNeedsYou({ ...row, bg_snooze_rested_at: null }, tele, runtime, hasActionableInteraction, nowMs, limitPause, false, deliveryProcessGone);
27643
28168
  }
27644
28169
  function resolveSessionProfile(row, tele) {
@@ -27683,7 +28208,6 @@ function resolveRecurringPrompt(row) {
27683
28208
  stopHook: row.recurring_on_rest === 1,
27684
28209
  heartbeat: row.recurring_on_schedule === 1,
27685
28210
  postCompaction: row.recurring_on_compact === 1,
27686
- pauseOnQuestions: row.recurring_pause_on_questions === 1,
27687
28211
  // Carried whenever a cadence has ever been chosen, INCLUDING while the heartbeat is off — the
27688
28212
  // minutes field has to read back what switching it on again would use.
27689
28213
  intervalSeconds: row.recurring_interval_ms ? Math.round(row.recurring_interval_ms / 1e3) : void 0,
@@ -27717,7 +28241,12 @@ function resolveSessionTitle(row, tele) {
27717
28241
  aiTitle: locked ? void 0 : tele?.aiTitle ?? persisted
27718
28242
  };
27719
28243
  }
27720
- function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTerminal, interactionPresence, nowMs, codexTurnLiveness, claudeBrokerDaemonAlive) {
28244
+ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTerminal, interactionPresence, nowMs, codexTurnLiveness, claudeBrokerDaemonAlive, github = {}) {
28245
+ const armedPrWatches = storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({
28246
+ target: `${w.owner}/${w.repo}#${w.number}`,
28247
+ createdAt: new Date(w.created_at).toISOString()
28248
+ }));
28249
+ const registeredPrWatches = new Set(armedPrWatches.map((w) => w.target));
27721
28250
  const headlessStalled = row.codex_runtime === "app-server" ? appServerTurnStalled(codexTurnLiveness(row.slug, row.session_id), tele?.lastActivityAt, nowMs) : isBrokerClaudeRow(row) ? !claudeBrokerDaemonAlive(row.session_id) : false;
27722
28251
  const headlessLostWork = isBrokerClaudeRow(row) && headlessStalled && hasUnretiredOwnAgents(tele);
27723
28252
  const deliveryProcessGone = isBrokerClaudeRow(row) && headlessStalled;
@@ -27728,8 +28257,8 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
27728
28257
  const state = effectiveSessionState(row, registeredLegacyTerminal);
27729
28258
  const archived = state === "archived";
27730
28259
  const limitPause = resolveLimitPause(row, tele, nowMs);
27731
- const needsYou = archived ? false : deriveNeedsYou(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, true, deliveryProcessGone);
27732
- const awaitingBackground = archived ? false : deriveAwaitingBackground(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, deliveryProcessGone);
28260
+ const needsYou = archived ? false : deriveNeedsYou(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, true, deliveryProcessGone, github, registeredPrWatches);
28261
+ const awaitingBackground = archived ? false : deriveAwaitingBackground(row, tele, runtime, interactionPresence.needsUser, nowMs, limitPause, deliveryProcessGone, github, registeredPrWatches);
27733
28262
  const crashed = runtime === "exited" && (tele?.turn === "in-flight" || hasLiveBackgroundWork(tele) || headlessLostWork);
27734
28263
  const snoozedUntil = futureSnooze(row, nowMs);
27735
28264
  const profile = resolveSessionProfile(row, tele);
@@ -27761,20 +28290,11 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
27761
28290
  lastAssistantAt: tele?.lastAssistantAt,
27762
28291
  subAgents: stampStoppable(tele?.subAgents ?? [], row),
27763
28292
  bgShells: stampStoppableShells(tele?.bgShells ?? [], row),
27764
- // TWO SOURCES, one list. A `shell` watch is a REGISTRATION the worker made by tool call, read from
27765
- // the registry — the whole point of moving that kind of wait out of the fence. A `github` watch has
27766
- // no row and never will (`f366e2d`); it is derived from the standing fence, so that the strip lists
27767
- // every wait the thread actually has out rather than only the ones with a table behind them.
27768
- watches: [
27769
- ...storage.listThreadWatches(row.slug, { armedOnly: true }).map((w) => ({
27770
- id: w.id,
27771
- kind: w.kind,
27772
- target: w.target,
27773
- state: w.state,
27774
- createdAt: new Date(w.created_at).toISOString()
27775
- })),
27776
- ...githubWatchViews(row.slug, tele, tele?.lastAssistantAt)
27777
- ],
28293
+ // ONE SOURCE: the FENCE. Both kinds are derived from what the worker wrote `pr-watch:` lines
28294
+ // become the github rows, `watch:` lines the shell rows so this strip lists exactly what will
28295
+ // actually wake the thread, and the two cannot drift into claiming different things. There is no
28296
+ // registry behind either any more (`thread_watch`, retired 2026-08-14).
28297
+ watches: fenceWatchViews(row.slug, tele, tele?.lastAssistantAt, github, armedPrWatches),
27778
28298
  pendingAsk: tele?.pendingAsk ? { questions: tele.pendingAsk.questions } : void 0,
27779
28299
  nativeInputRequired: tele?.nativeInputRequired,
27780
28300
  pendingQuestion: tele?.pendingQuestion ?? false,
@@ -27795,6 +28315,10 @@ function sessionThreadView(projectDir, storage, row, tele, registeredLegacyTermi
27795
28315
  // Only meaningful while the snooze is still pending — a prompt without a live deadline is an
27796
28316
  // already-delivered (or superseded) bump the row has not been swept clean of yet.
27797
28317
  snoozePrompt: snoozedUntil ? row.snooze_prompt ?? void 0 : void 0,
28318
+ // The event-snooze, as a fact the CHAT can read. `awaitingBackground` still ignores it — that flag
28319
+ // states whether the thread is waiting, which the snooze does not change — so the suppression is a
28320
+ // presentation rule the client applies, not a second opinion about the thread's state.
28321
+ bgSnoozed: bgSnoozeArmed(row) || void 0,
27798
28322
  claudeRuntime: row.claude_runtime === "broker" ? "broker" : row.claude_runtime === "tmux" ? "tmux" : void 0,
27799
28323
  // The recurring prompt — the same projection the worker's own `action: "get"` reads back.
27800
28324
  recurringPrompt: resolveRecurringPrompt(row),
@@ -27881,6 +28405,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
27881
28405
  let plansCache = [];
27882
28406
  function buildSessionThreads(nowMs) {
27883
28407
  const rows = storage.allSessions().filter((row) => ThreadSlug.safeParse(row.slug).success);
28408
+ const github = readGithubStatusBook(storage.getSetting(GITHUB_STATUS_SETTING));
27884
28409
  const currentInteractionKeys = /* @__PURE__ */ new Set();
27885
28410
  const out = [];
27886
28411
  for (const row of rows) {
@@ -27916,7 +28441,8 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
27916
28441
  interactionPresence,
27917
28442
  nowMs,
27918
28443
  codexTurnLiveness,
27919
- claudeBrokerDaemonAlive
28444
+ claudeBrokerDaemonAlive,
28445
+ github
27920
28446
  ));
27921
28447
  }
27922
28448
  for (const key of pendingInteractionCache.keys()) {
@@ -27932,7 +28458,9 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
27932
28458
  projectName: project.name,
27933
28459
  projectLabel: project.label,
27934
28460
  ...project.githubRepo ? { githubRepo: project.githubRepo } : {},
27935
- projectSlug: findByPath(project.dir)?.slug
28461
+ projectSlug: findByPath(project.dir)?.slug,
28462
+ // So the client can expand a `~` a worker wrote in prose (see BoardSnapshot.homeDir).
28463
+ homeDir: homedir12()
27936
28464
  };
27937
28465
  const sessionThreads = buildSessionThreads(assembledAtMs);
27938
28466
  armSnoozeWake(sessionThreads, assembledAtMs);
@@ -28113,7 +28641,7 @@ function createBoard(project, storage, bus, tailer, bootId, deps = {}) {
28113
28641
  }
28114
28642
  };
28115
28643
  }
28116
- var DEBOUNCE_MS, RECONCILE_MS, STALL_GRACE_MS, LINE_CAP;
28644
+ var DEBOUNCE_MS, RECONCILE_MS, STALL_GRACE_MS, LINE_CAP, DECLARED_PARK_MAX_MS;
28117
28645
  var init_board = __esm({
28118
28646
  "packages/server/src/board.ts"() {
28119
28647
  "use strict";
@@ -28134,6 +28662,7 @@ var init_board = __esm({
28134
28662
  RECONCILE_MS = 15e3;
28135
28663
  STALL_GRACE_MS = 3e4;
28136
28664
  LINE_CAP = 240;
28665
+ DECLARED_PARK_MAX_MS = 24 * 60 * 60 * 1e3;
28137
28666
  }
28138
28667
  });
28139
28668
 
@@ -28641,6 +29170,17 @@ var init_github_review = __esm({
28641
29170
  import { execFile as execFile6 } from "node:child_process";
28642
29171
  import { promisify as promisify6 } from "node:util";
28643
29172
  import { createHash as createHash14, randomUUID as randomUUID14 } from "node:crypto";
29173
+ function failureSignature(rollup) {
29174
+ const ids = [];
29175
+ for (const c of Array.isArray(rollup) ? rollup : []) {
29176
+ if (!c || typeof c !== "object") continue;
29177
+ if (!rollupEntryFailed(typeof c.conclusion === "string" ? c.conclusion : void 0, typeof c.state === "string" ? c.state : void 0)) continue;
29178
+ const label = c.name ?? c.context ?? c.workflowName ?? "?";
29179
+ ids.push(`${label}@${c.detailsUrl ?? c.targetUrl ?? c.completedAt ?? ""}`);
29180
+ }
29181
+ if (ids.length === 0) return "";
29182
+ return createHash14("sha1").update(ids.sort().join("\n")).digest("hex").slice(0, 12);
29183
+ }
28644
29184
  function parsePrRef2(value) {
28645
29185
  const m = value.trim().match(PR_REF_RE2);
28646
29186
  if (!m) return void 0;
@@ -28652,7 +29192,7 @@ function refKey2(r) {
28652
29192
  return `${r.owner}/${r.repo}#${r.number}`;
28653
29193
  }
28654
29194
  function ghPrViewArgs(ref) {
28655
- return ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "state,mergedAt,statusCheckRollup,headRefOid"];
29195
+ return ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "state,mergedAt,statusCheckRollup,headRefOid,mergeable,reviewDecision"];
28656
29196
  }
28657
29197
  function latestWorkflowRuns(runs) {
28658
29198
  const latest = /* @__PURE__ */ new Map();
@@ -28708,12 +29248,40 @@ function failedCheckNames(rollup, runs = [], cap = 8) {
28708
29248
  }
28709
29249
  return { names: names.slice(0, cap), omitted: Math.max(0, names.length - cap) };
28710
29250
  }
28711
- function isPrWatchHint(kind) {
28712
- return kind === "pr-watch";
29251
+ function githubWatchStatus(pr, polledAt) {
29252
+ const entries = Array.isArray(pr.rollup) ? pr.rollup.filter((c) => c && typeof c === "object") : [];
29253
+ let running = 0;
29254
+ let passed = 0;
29255
+ let failed = 0;
29256
+ for (const c of entries) {
29257
+ const status = typeof c.status === "string" ? c.status : void 0;
29258
+ const state2 = typeof c.state === "string" ? c.state : void 0;
29259
+ const conclusion = typeof c.conclusion === "string" ? c.conclusion : void 0;
29260
+ const terminal = status ? status === "COMPLETED" : state2 ? state2 !== "PENDING" && state2 !== "EXPECTED" : false;
29261
+ if (!terminal) running++;
29262
+ else if (rollupEntryFailed(conclusion, state2)) failed++;
29263
+ else passed++;
29264
+ }
29265
+ const state = pr.state === "MERGED" ? "merged" : pr.state === "CLOSED" ? "closed" : "open";
29266
+ const checks = entries.length === 0 ? "none" : failed > 0 ? "failing" : running > 0 ? "running" : "passing";
29267
+ const merge = pr.mergeable === "CONFLICTING" ? "conflicting" : pr.mergeable !== "MERGEABLE" ? "unknown" : checks === "failing" || pr.reviewDecision === "CHANGES_REQUESTED" || pr.reviewDecision === "REVIEW_REQUIRED" ? "blocked" : "mergeable";
29268
+ return {
29269
+ checks,
29270
+ running,
29271
+ passed,
29272
+ failed,
29273
+ failing: failedCheckNames(entries, pr.workflowRuns ?? []).names,
29274
+ merge,
29275
+ state,
29276
+ polledAt,
29277
+ head: pr.head,
29278
+ failureSig: failureSignature(entries)
29279
+ };
29280
+ }
29281
+ function isPrWatchHint(_kind) {
29282
+ return false;
28713
29283
  }
28714
- function isActionable(hint) {
28715
- if (hint.kind === "timer") return isValidAwaitingTimer(hint.value);
28716
- if (isPrWatchHint(hint.kind) || hint.kind === "pr" || hint.kind === "ci") return parsePrRef2(hint.value) !== void 0;
29284
+ function isActionable(_hint) {
28717
29285
  return false;
28718
29286
  }
28719
29287
  function fenceIdentity(hints, fenceAt) {
@@ -28763,20 +29331,28 @@ function isHeartbeatFenceId(fenceId) {
28763
29331
  function saidDone(tele) {
28764
29332
  return tele.lastFence?.kind === "done" || tele.lastAssistantAllDone === true;
28765
29333
  }
28766
- function restMessageIsSignedOff(tele) {
28767
- return tele.pendingQuestion === true || saidDone(tele) || parkedOnAWaitItCannotAdvance(tele);
29334
+ function armedTimerIdsOf(storage, slug) {
29335
+ return new Set(storage.listThreadTimers(slug, { armedOnly: true }).map((t) => t.id));
28768
29336
  }
28769
- function parkedOnAWaitItCannotAdvance(tele) {
28770
- const fence = tele.lastFence;
28771
- if (fence?.kind !== "awaiting") return false;
28772
- return fence.hints.some((h) => isActionable(h) || h.kind === "human");
29337
+ function restMessageIsSignedOff(tele, _registeredPrWatches = /* @__PURE__ */ new Set(), _armedTimerIds = /* @__PURE__ */ new Set()) {
29338
+ if (tele.lastFence?.kind === "awaiting") return true;
29339
+ return saidDone(tele);
28773
29340
  }
28774
- function blockedOnHuman(tele) {
28775
- return tele.pendingQuestion === true || tele.pendingAsk !== void 0 || tele.permPrompt === true || tele.nativeInputRequired !== void 0;
29341
+ function liveActivityOf(tele, registeredPrWatches, armedTimerIds) {
29342
+ const shells = /* @__PURE__ */ new Set();
29343
+ for (const sh of tele.bgShells ?? []) {
29344
+ if (sh.state !== "running") continue;
29345
+ for (const h of [sh.taskId, sh.id, sh.label]) if (h) shells.add(h);
29346
+ }
29347
+ const agents = /* @__PURE__ */ new Set();
29348
+ for (const a of tele.subAgents ?? []) {
29349
+ if (a.state !== "running") continue;
29350
+ for (const h of [a.id, a.label]) if (h) agents.add(h);
29351
+ }
29352
+ return { shells, agents, timers: armedTimerIds, prs: registeredPrWatches };
28776
29353
  }
28777
- function heldByQuestion(row, tele) {
28778
- if (row.recurring_pause_on_questions !== 1) return false;
28779
- return tele !== void 0 && blockedOnHuman(tele);
29354
+ function registeredPrWatchesOf(storage, slug) {
29355
+ return new Set(storage.listPrWatches(slug, { armedOnly: true }).map((w) => `${w.owner}/${w.repo}#${w.number}`));
28780
29356
  }
28781
29357
  function armedSchedule(row) {
28782
29358
  const prompt = row.recurring_prompt?.trim();
@@ -28814,20 +29390,36 @@ function armedCompact(row) {
28814
29390
  if (row.recurring_on_compact !== 1) return void 0;
28815
29391
  return { prompt, armedAt };
28816
29392
  }
29393
+ function shellFenceId(toolUseId) {
29394
+ return `${SHELL_FENCE_PREFIX}:${toolUseId}`;
29395
+ }
29396
+ function isShellFenceId(fenceId) {
29397
+ return fenceId.startsWith(`${SHELL_FENCE_PREFIX}:`);
29398
+ }
29399
+ function prWatchFenceId(watchId, report) {
29400
+ return `${PR_WATCH_FENCE_PREFIX}:${watchId}:${report}`;
29401
+ }
29402
+ function prWatchExpiredFenceId(watchId) {
29403
+ return `${PR_WATCH_EXPIRED_FENCE_PREFIX}:${watchId}`;
29404
+ }
29405
+ function isPrWatchFenceId(fenceId) {
29406
+ return fenceId.startsWith(`${PR_WATCH_FENCE_PREFIX}:`) || fenceId.startsWith(`${PR_WATCH_EXPIRED_FENCE_PREFIX}:`);
29407
+ }
28817
29408
  function signoffFenceId(restedAt) {
28818
29409
  return `${SIGNOFF_FENCE_PREFIX}:${restedAt}`;
28819
29410
  }
28820
29411
  function isSignoffFenceId(fenceId) {
28821
29412
  return fenceId.startsWith(`${SIGNOFF_FENCE_PREFIX}:`);
28822
29413
  }
28823
- function watchFenceId(watchId) {
28824
- return `${WATCH_FENCE_PREFIX}:${watchId}`;
29414
+ function parkFenceId(cause, spokeAt) {
29415
+ return `${PARK_FENCE_PREFIX}:${cause}:${spokeAt}`;
28825
29416
  }
28826
- function isWatchFenceId(fenceId) {
28827
- return fenceId.startsWith(`${WATCH_FENCE_PREFIX}:`);
29417
+ function isParkFenceId(fenceId) {
29418
+ return fenceId.startsWith(`${PARK_FENCE_PREFIX}:`);
28828
29419
  }
28829
- function watchIdOf(fenceId) {
28830
- return fenceId.slice(WATCH_FENCE_PREFIX.length + 1);
29420
+ function parkFenceRestOf(fenceId) {
29421
+ const cut = fenceId.indexOf(":", PARK_FENCE_PREFIX.length + 1);
29422
+ return cut < 0 ? "" : fenceId.slice(cut + 1);
28831
29423
  }
28832
29424
  function timerFenceId(timerId) {
28833
29425
  return `${TIMER_FENCE_PREFIX}:${timerId}`;
@@ -28839,12 +29431,6 @@ function timerIdOf(fenceId) {
28839
29431
  return fenceId.slice(TIMER_FENCE_PREFIX.length + 1);
28840
29432
  }
28841
29433
  function evalHint(hint, nowMs, prCache, fenceBody) {
28842
- if (hint.kind === "timer") {
28843
- const target = Date.parse(hint.value);
28844
- if (!isValidAwaitingTimer(hint.value) || !Number.isFinite(target)) return void 0;
28845
- const desc = fenceBody.trim().replace(/\s+/g, " ").slice(0, 200);
28846
- return { met: nowMs >= target, steer: `\u23F0 Your timer fired${desc ? `: ${desc}` : ""}. Continue.`, reason: `timer ${hint.value}` };
28847
- }
28848
29434
  if (isPrWatchHint(hint.kind)) return void 0;
28849
29435
  const ref = parsePrRef2(hint.value);
28850
29436
  if (!ref) return void 0;
@@ -28895,6 +29481,9 @@ async function defaultFetchPr(ref) {
28895
29481
  state: j.state,
28896
29482
  mergedAt: typeof j.mergedAt === "string" ? j.mergedAt : null,
28897
29483
  rollup: Array.isArray(j.statusCheckRollup) ? j.statusCheckRollup : [],
29484
+ head: j.headRefOid,
29485
+ mergeable: typeof j.mergeable === "string" ? j.mergeable : void 0,
29486
+ reviewDecision: typeof j.reviewDecision === "string" ? j.reviewDecision : void 0,
28898
29487
  workflowRuns
28899
29488
  };
28900
29489
  } catch {
@@ -28937,6 +29526,14 @@ function createScheduler(deps) {
28937
29526
  let timer = null;
28938
29527
  let activeTick = null;
28939
29528
  let stopped = false;
29529
+ function publishGithubStatus(key, pr, nowMs) {
29530
+ const raw2 = deps.storage.getSetting(GITHUB_STATUS_SETTING);
29531
+ const book = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? { ...raw2 } : {};
29532
+ book[key] = githubWatchStatus(pr, new Date(nowMs).toISOString());
29533
+ const keys = Object.keys(book);
29534
+ for (const stale of keys.slice(0, Math.max(0, keys.length - GITHUB_STATUS_CAP))) delete book[stale];
29535
+ deps.storage.setSetting(GITHUB_STATUS_SETTING, book);
29536
+ }
28940
29537
  function loadFired() {
28941
29538
  const raw2 = deps.storage.getSetting("waker.fired");
28942
29539
  return Array.isArray(raw2) ? raw2.filter((x) => typeof x === "string") : [];
@@ -29067,6 +29664,12 @@ function createScheduler(deps) {
29067
29664
  if (!fault || limitFenceId(fault) !== item.fenceId) return "superseded";
29068
29665
  return tele.turn === "idle" ? "current-idle" : "current-busy";
29069
29666
  }
29667
+ if (isShellFenceId(item.fenceId)) {
29668
+ return tele.turn === "idle" ? "current-idle" : "current-busy";
29669
+ }
29670
+ if (isPrWatchFenceId(item.fenceId)) {
29671
+ return tele.turn === "idle" ? "current-idle" : "current-busy";
29672
+ }
29070
29673
  if (isSnoozeFenceId(item.fenceId)) {
29071
29674
  if (armedSnooze(row)?.fenceId !== item.fenceId) return "superseded";
29072
29675
  return tele.turn === "idle" ? "current-idle" : "current-busy";
@@ -29079,7 +29682,7 @@ function createScheduler(deps) {
29079
29682
  if (isStopHookFenceId(item.fenceId)) {
29080
29683
  const armed = armedRest(row);
29081
29684
  if (!armed || item.fenceId !== stopHookFenceId(armed.armedAt, tele.lastAssistantAt ?? "")) return "superseded";
29082
- if (restMessageIsSignedOff(tele)) return "superseded";
29685
+ if (restMessageIsSignedOff(tele, registeredPrWatchesOf(deps.storage, item.slug), armedTimerIdsOf(deps.storage, item.slug))) return "superseded";
29083
29686
  return tele.turn === "idle" ? "current-idle" : "current-busy";
29084
29687
  }
29085
29688
  if (isCompactFenceId(item.fenceId)) {
@@ -29096,15 +29699,19 @@ function createScheduler(deps) {
29096
29699
  if (tele.lastFence || tele.pendingQuestion) return "superseded";
29097
29700
  return tele.turn === "idle" ? "current-idle" : "current-busy";
29098
29701
  }
29099
- if (isWatchFenceId(item.fenceId)) {
29100
- if (deps.storage.getThreadWatch(watchIdOf(item.fenceId))?.state !== "armed") return "superseded";
29101
- return tele.turn === "idle" ? "current-idle" : "current-busy";
29102
- }
29103
29702
  if (isReportFenceId(item.fenceId)) {
29104
29703
  const stillMissing = tele.droppedReports?.some((r) => reportFenceId(r.taskId) === item.fenceId);
29105
29704
  if (!stillMissing) return "superseded";
29106
29705
  return tele.turn === "idle" ? "current-idle" : "current-busy";
29107
29706
  }
29707
+ if (isParkFenceId(item.fenceId)) {
29708
+ if (parkFenceRestOf(item.fenceId) !== (tele.lastAssistantAt ?? "")) return "superseded";
29709
+ return tele.turn === "idle" ? "current-idle" : "current-busy";
29710
+ }
29711
+ if (!item.fenceId.includes("")) {
29712
+ log3(`waker: UNROUTED wake prefix ${item.fenceId.split(":")[0]} for ${item.slug} \u2014 no deliveryContext branch, superseding`);
29713
+ return "superseded";
29714
+ }
29108
29715
  const fence = tele.lastFence;
29109
29716
  if (!fence || fence.kind !== "awaiting" || !fence.hints.some(isActionable)) return "superseded";
29110
29717
  if (fenceIdentity(fence.hints, tele.lastActivityAt) !== item.fenceId) return "superseded";
@@ -29246,26 +29853,30 @@ function createScheduler(deps) {
29246
29853
  const deliveryId = wakeDeliveryId(slug, sessionId, fenceId);
29247
29854
  if (outbox.get(deliveryId)) st.fired = true;
29248
29855
  if (st.fired) return;
29249
- const needsPr = actionable.some((h) => h.kind === "pr" || h.kind === "ci");
29856
+ const needsPr = false;
29250
29857
  const needsReview = actionable.some((h) => isPrWatchHint(h.kind));
29251
29858
  if ((needsPr || needsReview) && (st.lastPollAt === 0 || nowMs - st.lastPollAt >= pollMs)) {
29252
29859
  st.lastPollAt = nowMs;
29253
29860
  const refs = /* @__PURE__ */ new Map();
29254
29861
  const reviewRefs = /* @__PURE__ */ new Map();
29862
+ const verdictRefs = /* @__PURE__ */ new Set();
29255
29863
  for (const h of actionable) {
29256
29864
  const ref = parsePrRef2(h.value);
29257
29865
  if (!ref) continue;
29258
- if (h.kind === "pr" || h.kind === "ci") refs.set(refKey2(ref), ref);
29259
29866
  if (isPrWatchHint(h.kind)) reviewRefs.set(refKey2(ref), ref);
29260
29867
  }
29261
29868
  await Promise.all([
29262
29869
  ...[...refs].map(async ([k, ref]) => {
29263
29870
  try {
29264
29871
  const s = await fetchPr(ref);
29265
- if (s) st.prCache.set(k, s);
29266
- else log3(`waker: gh check skipped for ${k} (${slug}) \u2014 gh unavailable / not authed / rate-limited`);
29872
+ if (s) {
29873
+ st.prCache.set(k, s);
29874
+ publishGithubStatus(k, s, nowMs);
29875
+ } else if (verdictRefs.has(k)) {
29876
+ log3(`waker: gh check skipped for ${k} (${slug}) \u2014 gh unavailable / not authed / rate-limited`);
29877
+ }
29267
29878
  } catch (err) {
29268
- log3(`waker: gh check errored for ${k} (${slug}): ${err instanceof Error ? err.message : String(err)}`);
29879
+ if (verdictRefs.has(k)) log3(`waker: gh check errored for ${k} (${slug}): ${err instanceof Error ? err.message : String(err)}`);
29269
29880
  }
29270
29881
  }),
29271
29882
  ...[...reviewRefs].map(async ([k, ref]) => {
@@ -29451,7 +30062,19 @@ function createScheduler(deps) {
29451
30062
  sessionId: row.session_id,
29452
30063
  fenceId,
29453
30064
  hintKey: SIGNOFF_HINT_KEY,
29454
- message: SIGNOFF_NUDGE_MESSAGE,
30065
+ // The reminder plus THIS THREAD's live ops and their ids, so the agent can write a correct
30066
+ // `watch:` line without going looking for an id it cannot see. Shells are named by `taskId` —
30067
+ // the handle the runtime actually showed the worker — because that is the string it will
30068
+ // naturally reach for, and the one the fence's own integrity check matches on.
30069
+ message: signoffNudgeMessage({
30070
+ shells: (tele.bgShells ?? []).filter((sh) => sh.state === "running").map((sh) => ({ id: sh.taskId ?? sh.id, label: sh.label })),
30071
+ subAgents: (tele.subAgents ?? []).filter((a) => a.state === "running").map((a) => ({ id: a.id, label: a.label })),
30072
+ // The other two registries, so the nudge lists EVERY kind an awaiting fence can name rather
30073
+ // than the two the fold happens to know about — a worker told about half its work writes half
30074
+ // a fence, and the half it left out is not what gets it bumped.
30075
+ timers: deps.storage.listThreadTimers(row.slug, { armedOnly: true }).map((t) => ({ id: t.id, label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 80) })),
30076
+ prs: deps.storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({ id: `${w.owner}/${w.repo}#${w.number}`, label: `${w.owner}/${w.repo}#${w.number}` }))
30077
+ }),
29455
30078
  reason: "rested without signing off"
29456
30079
  }, nowMs).delivery;
29457
30080
  log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
@@ -29462,27 +30085,99 @@ function createScheduler(deps) {
29462
30085
  if (!isSignoffFenceId(item.fenceId)) return;
29463
30086
  deps.storage.countSignoffNudge(item.slug, item.fenceId);
29464
30087
  }
29465
- function evalWatchers(nowMs) {
29466
- for (const watch of deps.storage.armedThreadWatches()) {
29467
- if (watch.kind !== "shell") continue;
29468
- const row = deps.storage.getSession(watch.thread_slug);
29469
- if (!row || row.state === "archived" || row.archived === 1) continue;
30088
+ function evalParkIntegrity(nowMs) {
30089
+ for (const row of deps.storage.allSessions()) {
30090
+ if (row.state === "archived" || row.archived === 1) continue;
29470
30091
  const tele = deps.tailer.get(row.slug);
29471
- if (!tele) continue;
29472
- const live = tele.bgShells.some(
29473
- (sh) => sh.state === "running" && (sh.id === watch.target || sh.label === watch.target)
30092
+ if (!tele || tele.turn !== "idle") continue;
30093
+ if (tele.lastFence?.kind !== "awaiting") continue;
30094
+ const spokeAt = tele.lastAssistantAt;
30095
+ if (!spokeAt) continue;
30096
+ if (tele.lastUserAt && Date.parse(tele.lastUserAt) >= Date.parse(spokeAt)) continue;
30097
+ if (tele.authFault) continue;
30098
+ const park = readAwaitingPark(tele.lastFence.hints);
30099
+ const live = liveActivityOf(
30100
+ tele,
30101
+ registeredPrWatchesOf(deps.storage, row.slug),
30102
+ armedTimerIdsOf(deps.storage, row.slug)
29474
30103
  );
29475
- if (live) {
29476
- if (watch.cursor !== SHELL_SEEN) deps.storage.setThreadWatchCursor(watch.id, SHELL_SEEN);
29477
- continue;
29478
- }
29479
- if (watch.cursor !== SHELL_SEEN) continue;
29480
- if (watch.foreground === 1) {
29481
- deps.storage.markThreadWatchFired(watch.id, nowMs);
29482
- log3(`waker: settled ${row.slug} \u2014 foreground watcher ${watch.id} resolved (shell ${watch.target})`);
29483
- continue;
29484
- }
29485
- const fenceId = watchFenceId(watch.id);
30104
+ const dead = unaccountedItems(park.items, live);
30105
+ const expiresAt = parkExpiresAt(park, Date.parse(spokeAt));
30106
+ const expired = expiresAt !== null && nowMs >= expiresAt;
30107
+ const nameless = park.items.length === 0;
30108
+ if (parkIsHonoured(park, live) && (row.park_bumps ?? 0) > 0) deps.storage.resetParkBumps(row.slug);
30109
+ if (dead.length === 0 && !expired && !nameless) continue;
30110
+ if (park.forMs === null && !nameless) continue;
30111
+ const status = park.items.map((i) => {
30112
+ const gone = dead.some((d) => d.kind === i.kind && d.value === i.value);
30113
+ return `- \`${i.kind}: ${i.value}\` \u2014 ${gone ? "NOT RUNNING" : "still running"}`;
30114
+ });
30115
+ const retired = retiredAwaitingKindsIn(tele.lastFence.body ?? "");
30116
+ const head = retired.length > 0 ? [
30117
+ `\u26D4 Your \`\`\`awaiting fence uses ${retired.length === 1 ? "a line kind" : "line kinds"} that no longer exist, so frizz ignored ${retired.length === 1 ? "it" : "them"} \u2014 the`,
30118
+ "fence named nothing and your thread stayed in the queue.",
30119
+ "",
30120
+ ...retired.map((k) => `- \`${k}:\` is GONE \u2192 ${RETIRED_AWAITING_REPLACEMENT[k]}`),
30121
+ "",
30122
+ "THE ONLY LINE KINDS NOW SUPPORTED, and every one is checked against something frizz can see:",
30123
+ "",
30124
+ " shell: <runtime task id> a background shell you launched",
30125
+ " agent: <runtime agent id> a sub-agent you dispatched",
30126
+ " timer: tmr_\u2026 a timer from `mcp__frizz__timer`",
30127
+ " pr: owner/repo#123 a PR registered with `mcp__frizz__watch_pr`",
30128
+ " for: 2h REQUIRED \u2014 a DURATION, never an instant",
30129
+ " reason: one line the only prose",
30130
+ "",
30131
+ "`mcp__frizz__activity` lists everything you have running with the exact id each line needs.",
30132
+ "If you are not waiting on anything, you are not awaiting \u2014 end with ```done, or ask a ```question."
30133
+ ].join("\n") : nameless ? [
30134
+ "\u26A0\uFE0F Your ```awaiting fence names nothing to wait on, so it is not a park \u2014 your thread is still",
30135
+ "in the queue, and nothing will wake you.",
30136
+ "",
30137
+ "A wait has to be something frizz can SEE. Register one, then name it:",
30138
+ "",
30139
+ "- a pull request \u2192 `mcp__frizz__watch_pr` (it wakes you on CI going green or red, and on every",
30140
+ " later review and comment) \u2192 `pr: owner/repo#123`",
30141
+ "- a wall-clock check \u2192 `mcp__frizz__timer` \u2192 `timer: tmr_\u2026`",
30142
+ "- work you already launched \u2192 `shell: <the id your runtime gave you>` or `agent: <id>`",
30143
+ "",
30144
+ "Use `mcp__frizz__activity` to read back everything you have running, with the exact id each",
30145
+ "line needs.",
30146
+ "",
30147
+ "AND IF YOU ARE NOT WAITING ON ANYTHING, you are not awaiting \u2014 you are done. End with ```done,",
30148
+ "or ask a ```question if you need the human."
30149
+ ].join("\n") : expired ? [
30150
+ "\u23F0 Your wait expired, nothing resolved. Check back in on everything.",
30151
+ "",
30152
+ ...status,
30153
+ "",
30154
+ "Re-park if they are genuinely still going \u2014 there is no limit on that, and a long job is not a",
30155
+ "failure. If something is finished, read its result. If nothing is left, end in ```done or ask a",
30156
+ "```question."
30157
+ ].join("\n") : [
30158
+ `\u26A0\uFE0F Your \`\`\`awaiting fence names ${dead.length === 1 ? "something that is" : "things that are"} not running, so it is not a park and your thread stayed in the queue.`,
30159
+ "",
30160
+ ...status,
30161
+ "",
30162
+ "Use `mcp__frizz__activity` to read back what you actually have out, with the exact id each line",
30163
+ "needs, then re-fence naming only those \u2014 or end in ```done or a ```question if there is nothing",
30164
+ "left to wait for."
30165
+ ].join("\n");
30166
+ const cause = retired.length > 0 ? "retired" : nameless ? "nameless" : expired ? "expired" : "dead";
30167
+ const ops = cause === "expired" ? [] : liveOpsLines({
30168
+ shells: (tele.bgShells ?? []).filter((sh) => sh.state === "running").map((sh) => ({ id: sh.taskId ?? sh.id, label: sh.label })),
30169
+ subAgents: (tele.subAgents ?? []).filter((a) => a.state === "running").map((a) => ({ id: a.id, label: a.label })),
30170
+ timers: deps.storage.listThreadTimers(row.slug, { armedOnly: true }).map((t) => ({ id: t.id, label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 80) })),
30171
+ prs: deps.storage.listPrWatches(row.slug, { armedOnly: true }).map((w) => ({ id: `${w.owner}/${w.repo}#${w.number}`, label: `${w.owner}/${w.repo}#${w.number}` }))
30172
+ });
30173
+ const message = ops.length > 0 ? `${head}
30174
+ ${ops.join("\n")}` : cause === "expired" ? head : `${head}
30175
+
30176
+ You have NOTHING running right now \u2014 no shell, no sub-agent, no timer, no registered
30177
+ pull request. There is nothing that could wake you, so this thread is not awaiting: finish in
30178
+ \`\`\`done, or ask a \`\`\`question.`;
30179
+ if (cause !== "expired" && (row.park_bumps ?? 0) >= PARK_BUMP_MAX) continue;
30180
+ const fenceId = parkFenceId(cause, spokeAt);
29486
30181
  const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
29487
30182
  if (outbox.get(deliveryId)) continue;
29488
30183
  const item = outbox.enqueue({
@@ -29490,17 +30185,199 @@ function createScheduler(deps) {
29490
30185
  slug: row.slug,
29491
30186
  sessionId: row.session_id,
29492
30187
  fenceId,
29493
- hintKey: `${WATCH_HINT_PREFIX}${watch.id}`,
29494
- message: watchWakeMessage("shell", watch.target, "The background shell you were waiting on has finished"),
29495
- reason: `watcher ${watch.id} resolved (shell ${watch.target})`
30188
+ hintKey: fenceId,
30189
+ message,
30190
+ reason: retired.length > 0 ? `awaiting fence uses retired kind(s): ${retired.join(", ")}` : nameless ? "awaiting park names nothing" : expired ? "awaiting park expired" : `awaiting park named ${dead.length} dead item(s)`
29496
30191
  }, nowMs).delivery;
30192
+ if (cause !== "expired") deps.storage.countParkBump(row.slug, fenceId);
29497
30193
  log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
29498
30194
  checkpoint("after-enqueue", item);
29499
30195
  }
29500
30196
  }
29501
- function settleWatcher(item) {
29502
- if (!isWatchFenceId(item.fenceId)) return;
29503
- deps.storage.markThreadWatchFired(watchIdOf(item.fenceId), now());
30197
+ const prWatchPolledAt = /* @__PURE__ */ new Map();
30198
+ function readPrWatchCursor(raw2) {
30199
+ try {
30200
+ const parsed = raw2 ? JSON.parse(raw2) : null;
30201
+ if (!parsed || typeof parsed !== "object") return { seen: [] };
30202
+ const seen = Array.isArray(parsed.seen) ? parsed.seen.filter((x) => typeof x === "string") : [];
30203
+ return { seen, checks: typeof parsed.checks === "string" ? parsed.checks : void 0, report: Number(parsed.report) || 0 };
30204
+ } catch {
30205
+ return { seen: [] };
30206
+ }
30207
+ }
30208
+ async function evalPrWatches(nowMs) {
30209
+ for (const w of deps.storage.expiredPrWatches(nowMs)) {
30210
+ const row = deps.storage.getSession(w.thread_slug);
30211
+ deps.storage.settlePrWatch(w.id, nowMs);
30212
+ const ref = `${w.owner}/${w.repo}#${w.number}`;
30213
+ log3(`waker: settled ${w.thread_slug} \u2014 pr watcher ${w.id} expired (${ref})`);
30214
+ if (!row || row.state === "archived" || row.archived === 1) continue;
30215
+ const fenceId = prWatchExpiredFenceId(w.id);
30216
+ const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
30217
+ if (outbox.get(deliveryId)) continue;
30218
+ const item = outbox.enqueue({
30219
+ id: deliveryId,
30220
+ slug: row.slug,
30221
+ sessionId: row.session_id,
30222
+ fenceId,
30223
+ hintKey: fenceId,
30224
+ message: `\u23F0 Your watcher on ${ref} has expired and is no longer armed \u2014 nothing on that PR will wake you now.
30225
+
30226
+ If you still care about it, register it again with \`mcp__frizz__watch_pr\` and a fresh \`for:\`. If you do not, and it was the only thing you were waiting on, end in a proper terminal state instead of parking on it again.`,
30227
+ reason: `pr watcher ${w.id} expired (${ref})`
30228
+ }, nowMs).delivery;
30229
+ log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
30230
+ checkpoint("after-enqueue", item);
30231
+ }
30232
+ const armed = deps.storage.armedPrWatches();
30233
+ if (armed.length === 0) return;
30234
+ const refs = /* @__PURE__ */ new Map();
30235
+ for (const w of armed) {
30236
+ const key = `${w.owner}/${w.repo}#${w.number}`;
30237
+ const last = prWatchPolledAt.get(key) ?? 0;
30238
+ if (nowMs - last < PR_WATCH_POLL_MS) continue;
30239
+ refs.set(key, { owner: w.owner, repo: w.repo, number: w.number });
30240
+ }
30241
+ const status = /* @__PURE__ */ new Map();
30242
+ const activity = /* @__PURE__ */ new Map();
30243
+ await Promise.all([...refs].map(async ([key, ref]) => {
30244
+ prWatchPolledAt.set(key, nowMs);
30245
+ try {
30246
+ const pr = await fetchPr(ref);
30247
+ if (pr) {
30248
+ status.set(key, githubWatchStatus(pr, new Date(nowMs).toISOString()));
30249
+ publishGithubStatus(key, pr, nowMs);
30250
+ }
30251
+ } catch (err) {
30252
+ log3(`waker: pr-watch status fetch failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
30253
+ }
30254
+ try {
30255
+ const result = normalizeReviewResult(await fetchGithubReview(ref));
30256
+ if (result.status === "ok") {
30257
+ activity.set(key, result.activity);
30258
+ recordReviewSuccess(key, "pr-watch registry");
30259
+ } else if (result.status === "error") recordReviewFailure(key, "pr-watch registry", result, nowMs);
30260
+ } catch (err) {
30261
+ recordReviewFailure(key, "pr-watch registry", {
30262
+ status: "error",
30263
+ failure: { kind: "network", message: err instanceof Error ? err.message : String(err) }
30264
+ }, nowMs);
30265
+ }
30266
+ }));
30267
+ for (const w of armed) {
30268
+ const row = deps.storage.getSession(w.thread_slug);
30269
+ if (!row || row.state === "archived" || row.archived === 1) continue;
30270
+ const key = `${w.owner}/${w.repo}#${w.number}`;
30271
+ const st = status.get(key);
30272
+ const acts = activity.get(key);
30273
+ if (!st && !acts) continue;
30274
+ const cursor = readPrWatchCursor(w.cursor);
30275
+ if (st && st.state !== "open") {
30276
+ deps.storage.settlePrWatch(w.id, nowMs);
30277
+ enqueuePrWatchWake(row, w.id, nextReport(cursor), prWatchWakeMessage({
30278
+ target: key,
30279
+ merged: st.state === "merged",
30280
+ closed: st.state === "closed"
30281
+ }), `pr-watch ${key} ${st.state}`, nowMs);
30282
+ continue;
30283
+ }
30284
+ const terminal = st && (st.checks === "passing" || st.checks === "failing") ? st.checks : void 0;
30285
+ const stamp = terminal !== void 0 ? `${st?.head ?? "?"}:${terminal}:${st?.failureSig ?? ""}` : void 0;
30286
+ const checksChanged = stamp !== void 0 && cursor.checks !== stamp;
30287
+ const seen = new Set(cursor.seen);
30288
+ const firstPoll = w.cursor === null;
30289
+ const newestFirst = [...acts ?? []].sort((a, b) => {
30290
+ const at = Date.parse(b.at ?? "") - Date.parse(a.at ?? "");
30291
+ return Number.isFinite(at) && at !== 0 ? at : b.id.localeCompare(a.id);
30292
+ });
30293
+ const fresh = newestFirst.filter((a) => {
30294
+ if (seen.has(a.id)) return false;
30295
+ if (!firstPoll) return true;
30296
+ const landed = Date.parse(a.at ?? "");
30297
+ return Number.isFinite(landed) && landed > w.created_at;
30298
+ });
30299
+ const named = fresh.slice(0, REVIEW_STEER_CAP);
30300
+ const reviewSteer = fresh.length > 0 ? formatGithubWakeSteer({
30301
+ ref: key,
30302
+ omitted: fresh.length - named.length,
30303
+ items: [...named].reverse().map((a) => ({
30304
+ label: activityLabel(a),
30305
+ actor: a.actor,
30306
+ bot: isBotGithubActor(a),
30307
+ ...a.at ? { at: a.at } : {},
30308
+ ...a.url ? { url: a.url } : {}
30309
+ }))
30310
+ }) : void 0;
30311
+ const nextCursor = {
30312
+ seen: acts ? [.../* @__PURE__ */ new Set([...cursor.seen, ...acts.map((a) => a.id)])].slice(-REVIEW_SEEN_CAP) : cursor.seen,
30313
+ // The STAMP, so the next poll compares against the commit as well as the colour.
30314
+ checks: stamp ?? cursor.checks,
30315
+ report: cursor.report ?? 0
30316
+ };
30317
+ if (!checksChanged && !reviewSteer) {
30318
+ if (JSON.stringify(nextCursor) !== JSON.stringify(cursor)) {
30319
+ deps.storage.setPrWatchCursor(w.id, JSON.stringify(nextCursor));
30320
+ }
30321
+ continue;
30322
+ }
30323
+ deps.storage.setSnoozedUntil(row.slug, null);
30324
+ const report = nextReport(cursor);
30325
+ enqueuePrWatchWake(row, w.id, report, prWatchWakeMessage({
30326
+ target: key,
30327
+ ...checksChanged && terminal ? { checks: { verdict: terminal, passed: st.passed, failed: st.failed, failing: st.failing } } : {},
30328
+ ...reviewSteer ? { review: reviewSteer } : {}
30329
+ }), `pr-watch ${key}${checksChanged ? ` CI ${terminal}` : ""}${reviewSteer ? " review" : ""}`, nowMs);
30330
+ deps.storage.setPrWatchCursor(w.id, JSON.stringify({ ...nextCursor, report }));
30331
+ }
30332
+ }
30333
+ function nextReport(cursor) {
30334
+ return (cursor.report ?? 0) + 1;
30335
+ }
30336
+ function enqueuePrWatchWake(row, watchId, report, message, reason, nowMs) {
30337
+ const fenceId = prWatchFenceId(watchId, report);
30338
+ const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
30339
+ if (outbox.get(deliveryId)) return;
30340
+ const item = outbox.enqueue({
30341
+ id: deliveryId,
30342
+ slug: row.slug,
30343
+ sessionId: row.session_id,
30344
+ fenceId,
30345
+ hintKey: `${PR_WATCH_FENCE_PREFIX}:${watchId}`,
30346
+ message,
30347
+ reason
30348
+ }, nowMs).delivery;
30349
+ log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
30350
+ checkpoint("after-enqueue", item);
30351
+ }
30352
+ function evalShellCompletions(nowMs) {
30353
+ for (const row of deps.storage.allSessions()) {
30354
+ if (row.state === "archived" || row.archived === 1) continue;
30355
+ const tele = deps.tailer.get(row.slug);
30356
+ if (!tele) continue;
30357
+ if (tele.turn !== "idle") continue;
30358
+ if (tele.authFault) continue;
30359
+ const restedAt = Date.parse(tele.lastAssistantAt ?? "");
30360
+ if (!Number.isFinite(restedAt)) continue;
30361
+ for (const shell of tele.retiredShells ?? []) {
30362
+ const finishedAt = Date.parse(shell.finishedAt ?? "");
30363
+ if (!Number.isFinite(finishedAt) || finishedAt <= restedAt) continue;
30364
+ const fenceId = shellFenceId(shell.id);
30365
+ const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
30366
+ if (outbox.get(deliveryId)) continue;
30367
+ const item = outbox.enqueue({
30368
+ id: deliveryId,
30369
+ slug: row.slug,
30370
+ sessionId: row.session_id,
30371
+ fenceId,
30372
+ hintKey: `${SHELL_FENCE_PREFIX}:${shell.id}`,
30373
+ message: shellDoneMessage(shell),
30374
+ reason: `background shell finished (${shell.taskId ?? shell.label})`
30375
+ }, nowMs).delivery;
30376
+ log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
30377
+ checkpoint("after-enqueue", item);
30378
+ return;
30379
+ }
30380
+ }
29504
30381
  }
29505
30382
  function evalTimers(nowMs) {
29506
30383
  for (const timer2 of deps.storage.dueThreadTimers(nowMs)) {
@@ -29534,7 +30411,6 @@ function createScheduler(deps) {
29534
30411
  if (!armed || armed.dueAtMs > nowMs) continue;
29535
30412
  const beatTele = deps.tailer.get(row.slug);
29536
30413
  if (beatTele && saidDone(beatTele)) continue;
29537
- if (heldByQuestion(row, beatTele)) continue;
29538
30414
  if (openSchedulePrompt(row.slug, row.session_id)) continue;
29539
30415
  const beat = beatIndex(row.recurring_schedule_fired_at ?? null, armed);
29540
30416
  const fenceId = heartbeatFenceId(armed.armedAt, beat);
@@ -29583,8 +30459,7 @@ function createScheduler(deps) {
29583
30459
  if (!restedAt) continue;
29584
30460
  if (tele.lastUserAt && Date.parse(tele.lastUserAt) >= Date.parse(restedAt)) continue;
29585
30461
  if (tele.authFault) continue;
29586
- if (restMessageIsSignedOff(tele)) continue;
29587
- if (heldByQuestion(row, tele)) continue;
30462
+ if (restMessageIsSignedOff(tele, registeredPrWatchesOf(deps.storage, row.slug), armedTimerIdsOf(deps.storage, row.slug))) continue;
29588
30463
  const fenceId = stopHookFenceId(armed.armedAt, restedAt);
29589
30464
  const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
29590
30465
  if (outbox.get(deliveryId)) continue;
@@ -29594,7 +30469,7 @@ function createScheduler(deps) {
29594
30469
  sessionId: row.session_id,
29595
30470
  fenceId,
29596
30471
  hintKey: STOP_HOOK_HINT_KEY,
29597
- message: restPromptMessage(armed.prompt),
30472
+ message: restPromptMessage(armed.prompt, { overQuestion: tele.pendingQuestion === true }),
29598
30473
  reason: "recurring prompt at rest"
29599
30474
  }, nowMs).delivery;
29600
30475
  log3(`waker: queued ${row.slug} \u2014 ${item.reason}`);
@@ -29609,7 +30484,6 @@ function createScheduler(deps) {
29609
30484
  const tele = deps.tailer.get(row.slug);
29610
30485
  if (!tele?.lastCompactionAt) continue;
29611
30486
  if (tele.lastCompactionAt <= armed.armedAt) continue;
29612
- if (heldByQuestion(row, tele)) continue;
29613
30487
  const fenceId = compactFenceId(armed.armedAt, tele.lastCompactionAt);
29614
30488
  const deliveryId = wakeDeliveryId(row.slug, row.session_id, fenceId);
29615
30489
  if (outbox.get(deliveryId)) continue;
@@ -29659,7 +30533,6 @@ function createScheduler(deps) {
29659
30533
  settleRestPrompt(item);
29660
30534
  settleCompactPrompt(item);
29661
30535
  settleTimer(item);
29662
- settleWatcher(item);
29663
30536
  continue;
29664
30537
  }
29665
30538
  if (context === "superseded") {
@@ -29679,7 +30552,6 @@ function createScheduler(deps) {
29679
30552
  if (recovered?.state === "exhausted") {
29680
30553
  settleSnooze(item);
29681
30554
  settleTimer(item);
29682
- settleWatcher(item);
29683
30555
  log3(`waker: delivery EXHAUSTED for ${item.slug} after ${recovered.attempts} attempts \u2014 ${recovered.lastError ?? "unknown error"}`);
29684
30556
  }
29685
30557
  }
@@ -29698,7 +30570,6 @@ function createScheduler(deps) {
29698
30570
  settleRestPrompt(item);
29699
30571
  settleCompactPrompt(item);
29700
30572
  settleTimer(item);
29701
- settleWatcher(item);
29702
30573
  continue;
29703
30574
  }
29704
30575
  if (context === "superseded") {
@@ -29726,7 +30597,6 @@ function createScheduler(deps) {
29726
30597
  outbox.supersede(item.id, failedAt, message);
29727
30598
  settleSnooze(item);
29728
30599
  settleTimer(item);
29729
- settleWatcher(item);
29730
30600
  log3(`waker: delivery ABANDONED for ${item.slug} (terminal, no retry): ${message}`);
29731
30601
  continue;
29732
30602
  }
@@ -29752,7 +30622,6 @@ function createScheduler(deps) {
29752
30622
  settleRestPrompt(item);
29753
30623
  settleCompactPrompt(item);
29754
30624
  settleTimer(item);
29755
- settleWatcher(item);
29756
30625
  settleSignoffNudge(item);
29757
30626
  const acknowledged = outbox.get(item.id);
29758
30627
  if (acknowledged) checkpoint("after-ack", acknowledged);
@@ -29821,15 +30690,22 @@ function createScheduler(deps) {
29821
30690
  }
29822
30691
  try {
29823
30692
  evalSignoffNudges(now());
30693
+ evalParkIntegrity(now());
29824
30694
  } catch (err) {
29825
30695
  if (err instanceof InjectedSchedulerCrash) throw err;
29826
30696
  log3(`waker: sign-off nudge pass failed: ${err instanceof Error ? err.message : String(err)}`);
29827
30697
  }
29828
30698
  try {
29829
- evalWatchers(now());
30699
+ await evalPrWatches(now());
30700
+ } catch (err) {
30701
+ if (err instanceof InjectedSchedulerCrash) throw err;
30702
+ log3(`waker: pr-watch registry pass failed: ${err instanceof Error ? err.message : String(err)}`);
30703
+ }
30704
+ try {
30705
+ evalShellCompletions(now());
29830
30706
  } catch (err) {
29831
30707
  if (err instanceof InjectedSchedulerCrash) throw err;
29832
- log3(`waker: watcher pass failed: ${err instanceof Error ? err.message : String(err)}`);
30708
+ log3(`waker: shell-completion pass failed: ${err instanceof Error ? err.message : String(err)}`);
29833
30709
  }
29834
30710
  try {
29835
30711
  evalTimers(now());
@@ -29881,11 +30757,12 @@ function createScheduler(deps) {
29881
30757
  tick
29882
30758
  };
29883
30759
  }
29884
- var execFileAsync4, PR_REF_RE2, LIMIT_FENCE_PREFIX, LIMIT_HINT_PREFIX, LIMIT_RESUME_GRACE_MS, LIMIT_RESUME_HEADROOM_PERCENT, LIMIT_HEADROOM_MIN_FAULT_AGE_MS, REPORT_FENCE_PREFIX, REPORT_HINT_PREFIX, SNOOZE_FENCE_PREFIX, SNOOZE_HINT_PREFIX, HEARTBEAT_FENCE_PREFIX, HEARTBEAT_HINT_PREFIX, STOP_HOOK_FENCE_PREFIX, STOP_HOOK_HINT_KEY, COMPACT_FENCE_PREFIX, COMPACT_HINT_KEY, TIMER_FENCE_PREFIX, TIMER_HINT_PREFIX, SIGNOFF_FENCE_PREFIX, SIGNOFF_HINT_KEY, SIGNOFF_NUDGE_MAX, SIGNOFF_NUDGE_SETTING, WATCH_FENCE_PREFIX, WATCH_HINT_PREFIX, SHELL_SEEN, FIRED_CAP, REGISTRATION_CAP, REVIEW_SEEN_CAP, INTRODUCED_CAP, REVIEW_STEER_CAP;
30760
+ var execFileAsync4, PR_REF_RE2, LIMIT_FENCE_PREFIX, LIMIT_HINT_PREFIX, LIMIT_RESUME_GRACE_MS, LIMIT_RESUME_HEADROOM_PERCENT, LIMIT_HEADROOM_MIN_FAULT_AGE_MS, REPORT_FENCE_PREFIX, REPORT_HINT_PREFIX, SNOOZE_FENCE_PREFIX, SNOOZE_HINT_PREFIX, HEARTBEAT_FENCE_PREFIX, HEARTBEAT_HINT_PREFIX, STOP_HOOK_FENCE_PREFIX, STOP_HOOK_HINT_KEY, COMPACT_FENCE_PREFIX, COMPACT_HINT_KEY, TIMER_FENCE_PREFIX, TIMER_HINT_PREFIX, SHELL_FENCE_PREFIX, PR_WATCH_FENCE_PREFIX, PR_WATCH_EXPIRED_FENCE_PREFIX, PR_WATCH_POLL_MS, SIGNOFF_FENCE_PREFIX, SIGNOFF_HINT_KEY, SIGNOFF_NUDGE_MAX, PARK_BUMP_MAX, SIGNOFF_NUDGE_SETTING, PARK_FENCE_PREFIX, FIRED_CAP, GITHUB_STATUS_CAP, REGISTRATION_CAP, REVIEW_SEEN_CAP, INTRODUCED_CAP, REVIEW_STEER_CAP;
29885
30761
  var init_scheduler = __esm({
29886
30762
  "packages/server/src/scheduler.ts"() {
29887
30763
  "use strict";
29888
30764
  init_src();
30765
+ init_awaiting();
29889
30766
  init_usage_limit();
29890
30767
  init_wake_store();
29891
30768
  init_shutdown();
@@ -29912,14 +30789,18 @@ var init_scheduler = __esm({
29912
30789
  COMPACT_HINT_KEY = "recurring:compaction";
29913
30790
  TIMER_FENCE_PREFIX = "timer";
29914
30791
  TIMER_HINT_PREFIX = "timer:";
30792
+ SHELL_FENCE_PREFIX = "shell";
30793
+ PR_WATCH_FENCE_PREFIX = "prwatch";
30794
+ PR_WATCH_EXPIRED_FENCE_PREFIX = "prwatch-expired";
30795
+ PR_WATCH_POLL_MS = 6e4;
29915
30796
  SIGNOFF_FENCE_PREFIX = "signoff";
29916
30797
  SIGNOFF_HINT_KEY = "signoff:rest";
29917
30798
  SIGNOFF_NUDGE_MAX = 2;
30799
+ PARK_BUMP_MAX = 3;
29918
30800
  SIGNOFF_NUDGE_SETTING = "signoffNudge";
29919
- WATCH_FENCE_PREFIX = "watch";
29920
- WATCH_HINT_PREFIX = "watch:";
29921
- SHELL_SEEN = "seen";
30801
+ PARK_FENCE_PREFIX = "park";
29922
30802
  FIRED_CAP = 500;
30803
+ GITHUB_STATUS_CAP = 200;
29923
30804
  REGISTRATION_CAP = 500;
29924
30805
  REVIEW_SEEN_CAP = 300;
29925
30806
  INTRODUCED_CAP = 500;
@@ -30628,7 +31509,7 @@ var init_orphan_reaper = __esm({
30628
31509
  // packages/server/src/context.ts
30629
31510
  import { join as join29 } from "node:path";
30630
31511
  import { randomUUID as randomUUID15 } from "node:crypto";
30631
- import { homedir as homedir12 } from "node:os";
31512
+ import { homedir as homedir13 } from "node:os";
30632
31513
  function reconcileSessions(storage) {
30633
31514
  for (const row of storage.allSessions()) {
30634
31515
  if (isHeadlessRow(row)) continue;
@@ -30782,7 +31663,7 @@ async function createContext(opts = {}) {
30782
31663
  }
30783
31664
  }
30784
31665
  function createContextUnchecked(opts, resources) {
30785
- const home = opts.home ?? homedir12();
31666
+ const home = opts.home ?? homedir13();
30786
31667
  const project = opts.project ?? resolveProject();
30787
31668
  const dbPath = join29(project.stateDir, "ui.db");
30788
31669
  const storage = createStorage(dbPath);
@@ -31356,11 +32237,11 @@ var init_url = __esm({
31356
32237
  }
31357
32238
  return null;
31358
32239
  };
31359
- tryDecode = (str, decoder) => {
32240
+ tryDecode = (str2, decoder) => {
31360
32241
  try {
31361
- return decoder(str);
32242
+ return decoder(str2);
31362
32243
  } catch {
31363
- return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
32244
+ return str2.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
31364
32245
  try {
31365
32246
  return decoder(match2);
31366
32247
  } catch {
@@ -31369,7 +32250,7 @@ var init_url = __esm({
31369
32250
  });
31370
32251
  }
31371
32252
  };
31372
- tryDecodeURI = (str) => tryDecode(str, decodeURI);
32253
+ tryDecodeURI = (str2) => tryDecode(str2, decodeURI);
31373
32254
  getPath = (request) => {
31374
32255
  const url = request.url;
31375
32256
  const start = url.indexOf("/", url.indexOf(":") + 4);
@@ -31517,7 +32398,7 @@ var init_request = __esm({
31517
32398
  init_constants();
31518
32399
  init_body();
31519
32400
  init_url();
31520
- tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
32401
+ tryDecodeURIComponent = (str2) => tryDecode(str2, decodeURIComponent_);
31521
32402
  HonoRequest = class {
31522
32403
  /**
31523
32404
  * `.raw` can get the raw Request object.
@@ -31816,27 +32697,27 @@ var init_html = __esm({
31816
32697
  escapedString.callbacks = callbacks;
31817
32698
  return escapedString;
31818
32699
  };
31819
- resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
31820
- if (typeof str === "object" && !(str instanceof String)) {
31821
- if (!(str instanceof Promise)) {
31822
- str = str.toString();
32700
+ resolveCallback = async (str2, phase, preserveCallbacks, context, buffer) => {
32701
+ if (typeof str2 === "object" && !(str2 instanceof String)) {
32702
+ if (!(str2 instanceof Promise)) {
32703
+ str2 = str2.toString();
31823
32704
  }
31824
- if (str instanceof Promise) {
31825
- str = await str;
32705
+ if (str2 instanceof Promise) {
32706
+ str2 = await str2;
31826
32707
  }
31827
32708
  }
31828
- const callbacks = str.callbacks;
32709
+ const callbacks = str2.callbacks;
31829
32710
  if (!callbacks?.length) {
31830
- return Promise.resolve(str);
32711
+ return Promise.resolve(str2);
31831
32712
  }
31832
32713
  if (buffer) {
31833
- buffer[0] += str;
32714
+ buffer[0] += str2;
31834
32715
  } else {
31835
- buffer = [str];
32716
+ buffer = [str2];
31836
32717
  }
31837
32718
  const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then(
31838
32719
  (res) => Promise.all(
31839
- res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context, buffer))
32720
+ res.filter(Boolean).map((str22) => resolveCallback(str22, phase, false, context, buffer))
31840
32721
  ).then(() => buffer[0])
31841
32722
  );
31842
32723
  if (preserveCallbacks) {
@@ -34006,11 +34887,14 @@ import { closeSync as closeSync13, existsSync as existsSync11, fstatSync as fsta
34006
34887
  import { createHash as createHash15 } from "node:crypto";
34007
34888
  import { StringDecoder as StringDecoder4 } from "node:string_decoder";
34008
34889
  import { join as join30 } from "node:path";
34009
- import { homedir as homedir13 } from "node:os";
34890
+ import { homedir as homedir14 } from "node:os";
34010
34891
  function isInjectedNoise(text) {
34011
34892
  const t = text.trimStart();
34012
34893
  return NOISE_PREFIXES.some((p) => t.startsWith(p));
34013
34894
  }
34895
+ function isInterruptMarker(text) {
34896
+ return INTERRUPT_MARKERS.includes(text.trim());
34897
+ }
34014
34898
  function formatTokens(n) {
34015
34899
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
34016
34900
  if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
@@ -34141,6 +35025,7 @@ function createTranscriptFold(identityPrefix = "claude") {
34141
35025
  const pendingTools = /* @__PURE__ */ new Map();
34142
35026
  const agentDispatches = /* @__PURE__ */ new Map();
34143
35027
  const backgroundShells = /* @__PURE__ */ new Map();
35028
+ const consumedShells = /* @__PURE__ */ new Map();
34144
35029
  const backgroundTaskIds = /* @__PURE__ */ new Map();
34145
35030
  const relayedWakesDrawn = /* @__PURE__ */ new Set();
34146
35031
  const childDispatchIds = /* @__PURE__ */ new Map();
@@ -34148,9 +35033,11 @@ function createTranscriptFold(identityPrefix = "claude") {
34148
35033
  const queuedPending = [];
34149
35034
  let deliveredDedupe = null;
34150
35035
  let pendingRest = null;
35036
+ const restEpoch = { n: 0 };
34151
35037
  function flushRest() {
34152
35038
  if (!pendingRest) return;
34153
35039
  out.push(restMessage(pendingRest.sourceId, pendingRest.at));
35040
+ restEpoch.n++;
34154
35041
  pendingRest = null;
34155
35042
  lastAssistantId = null;
34156
35043
  }
@@ -34194,7 +35081,7 @@ function createTranscriptFold(identityPrefix = "claude") {
34194
35081
  if (pendingRest && !(rec.type === "assistant" && typeof rec.message?.id === "string" && rec.message.id === pendingRest.msgId)) {
34195
35082
  flushRest();
34196
35083
  }
34197
- const evs = completionEvents(rec, agentDispatches, backgroundShells, backgroundTaskIds, relayedWakesDrawn);
35084
+ const evs = completionEvents(rec, agentDispatches, backgroundShells, backgroundTaskIds, relayedWakesDrawn, consumedShells, restEpoch);
34198
35085
  let drewEvents = false;
34199
35086
  if (evs.length > 0) {
34200
35087
  attachToolResults(rec, pendingTools, backgroundShells, backgroundTaskIds, childDispatchIds);
@@ -34246,8 +35133,8 @@ function createTranscriptFold(identityPrefix = "claude") {
34246
35133
  }
34247
35134
  const peerOrigin = att.origin?.kind === "peer" ? att.origin : void 0;
34248
35135
  if (peerOrigin && att.commandMode === "prompt") {
34249
- const str = (v) => typeof v === "string" ? v.trim() : "";
34250
- const senderTaskId = str(peerOrigin.senderTaskId);
35136
+ const str2 = (v) => typeof v === "string" ? v.trim() : "";
35137
+ const senderTaskId = str2(peerOrigin.senderTaskId);
34251
35138
  const dispatchId = senderTaskId ? childDispatchIds.get(senderTaskId) : void 0;
34252
35139
  const entry = prompt.trim() ? findQueued(prompt) : void 0;
34253
35140
  if (entry) {
@@ -34263,7 +35150,7 @@ function createTranscriptFold(identityPrefix = "claude") {
34263
35150
  } else {
34264
35151
  const parsed = prompt.trim() ? parseAgentMessage(prompt) : void 0;
34265
35152
  const describedHere = dispatchId ? dispatchLabels.get(dispatchId) : void 0;
34266
- const from = describedHere || str(peerOrigin.from) || str(peerOrigin.name) || parsed?.from || "";
35153
+ const from = describedHere || str2(peerOrigin.from) || str2(peerOrigin.name) || parsed?.from || "";
34267
35154
  const body = parsed?.body ?? (typeof peerOrigin.body === "string" ? peerOrigin.body : "");
34268
35155
  if (from && body.trim()) {
34269
35156
  out.push({
@@ -34307,6 +35194,7 @@ function createTranscriptFold(identityPrefix = "claude") {
34307
35194
  }
34308
35195
  let text = userText(rec);
34309
35196
  if (text && isAllInjectedNoise(text)) return;
35197
+ if (text && isInterruptMarker(text)) return;
34310
35198
  if (text) {
34311
35199
  const queued = findQueued(text)?.message;
34312
35200
  if (queued) {
@@ -34442,9 +35330,14 @@ ${block.text}` : block.text;
34442
35330
  return {
34443
35331
  ingest,
34444
35332
  finalize,
35333
+ // The SAME window the paged reader returns (latestWindowStart), because this is the OTHER producer:
35334
+ // the /ws push renders from here while the RPC page renders from there, and a push whose head sat
35335
+ // further forward than the page's would splice the human's ask straight back out of a card that had
35336
+ // just anchored on it (reconcileLiveMessages replaces the window wholesale).
34445
35337
  messages: () => {
34446
35338
  const all = projected();
34447
- return all.length > MAX_MESSAGES ? all.slice(-MAX_MESSAGES) : all;
35339
+ const start = latestWindowStart(all);
35340
+ return start === 0 ? all : all.slice(start);
34448
35341
  },
34449
35342
  allMessages: projected,
34450
35343
  consumedBytes: () => offset + Buffer.byteLength(buffer)
@@ -34874,7 +35767,7 @@ function notificationCarrierText(rec) {
34874
35767
  }
34875
35768
  return void 0;
34876
35769
  }
34877
- function completionEvents(rec, dispatches, backgroundShells, backgroundTaskIds, relayedWakesDrawn) {
35770
+ function completionEvents(rec, dispatches, backgroundShells, backgroundTaskIds, relayedWakesDrawn, consumedShells = /* @__PURE__ */ new Map(), restEpoch = { n: 0 }) {
34878
35771
  const carrier = notificationCarrierText(rec);
34879
35772
  const raw2 = carrier === void 0 ? void 0 : splitWakeDeliveries(carrier).map((s) => relayNotificationBlock(s) ?? s).join("\n");
34880
35773
  if (!raw2 || !raw2.includes("<task-notification>")) return [];
@@ -34908,10 +35801,17 @@ function completionEvents(rec, dispatches, backgroundShells, backgroundTaskIds,
34908
35801
  if (!d) {
34909
35802
  const shell = backgroundShells.get(id);
34910
35803
  if (!shell) {
35804
+ const drawn = consumedShells.get(id);
35805
+ if (drawn && restEpoch.n > drawn.epoch) {
35806
+ consumedShells.set(id, { epoch: restEpoch.n, call: drawn.call });
35807
+ out.push({ role: "assistant", kind: "event", boundary: "wake", text: backgroundWakeLabel(drawn.call, status, block), tools: [], parts: [], at });
35808
+ continue;
35809
+ }
34911
35810
  relayedFallback();
34912
35811
  continue;
34913
35812
  }
34914
35813
  backgroundShells.delete(id);
35814
+ consumedShells.set(id, { epoch: restEpoch.n, call: shell.call });
34915
35815
  const elapsedMs2 = elapsedBetween(shell.at, rec.timestamp);
34916
35816
  shell.call.status = status === "completed" ? "completed" : status === "killed" ? "cancelled" : "failed";
34917
35817
  if (elapsedMs2 !== void 0) shell.call.durationMs = elapsedMs2;
@@ -35015,7 +35915,7 @@ function verifyIncrementalParse(path, identityPrefix, incremental) {
35015
35915
  }
35016
35916
  }
35017
35917
  function logDirOf(project) {
35018
- return join30(homedir13(), ".claude", "projects", project.cwdSlug);
35918
+ return join30(homedir14(), ".claude", "projects", project.cwdSlug);
35019
35919
  }
35020
35920
  function resolveTranscriptPath(project, sessionId) {
35021
35921
  const path = join30(logDirOf(project), `${sessionId}.jsonl`);
@@ -35267,8 +36167,30 @@ function parseCodexTranscript(raw2, identityPrefix = "codex") {
35267
36167
  const out = projectCodexTranscript(raw2, identityPrefix);
35268
36168
  return latestTranscriptWindow(out);
35269
36169
  }
36170
+ function latestWindowStart(messages) {
36171
+ const tail = Math.max(0, messages.length - MAX_MESSAGES);
36172
+ if (tail === 0) return 0;
36173
+ let boundary = -1;
36174
+ for (let i = tail - 1; i >= 0; i--) {
36175
+ const m = messages[i];
36176
+ if (m.role === "user" && !m.wake && !m.queued) {
36177
+ boundary = i;
36178
+ break;
36179
+ }
36180
+ }
36181
+ if (boundary < 0) return tail;
36182
+ let start = tail;
36183
+ let bytes = 0;
36184
+ while (start > boundary && tail - start < LATEST_WINDOW_ASK_REACH_ITEMS) {
36185
+ const nextBytes = messageBytes(messages[start - 1]);
36186
+ if (bytes + nextBytes > LATEST_WINDOW_ASK_REACH_BYTES) break;
36187
+ start--;
36188
+ bytes += nextBytes;
36189
+ }
36190
+ return start === boundary ? start : tail;
36191
+ }
35270
36192
  function latestTranscriptWindow(messages) {
35271
- const start = Math.max(0, messages.length - MAX_MESSAGES);
36193
+ const start = latestWindowStart(messages);
35272
36194
  if (start === 0) return [...messages];
35273
36195
  const pinned = [];
35274
36196
  for (let i = 0; i < start; i++) {
@@ -36161,7 +37083,7 @@ function readLatestThreadTranscriptPage(project, storage, slug, backendFor) {
36161
37083
  }
36162
37084
  }
36163
37085
  const latest = latestTranscriptWindow(projected);
36164
- const canonicalStart = Math.max(0, projected.length - MAX_MESSAGES);
37086
+ const canonicalStart = latestWindowStart(projected);
36165
37087
  const row = storage.getSession(slug);
36166
37088
  const pageMessages = row ? projectDeliveryLedger(latest, parseDeliveryLedger(row.delivery_ledger)) : latest;
36167
37089
  return {
@@ -36302,7 +37224,7 @@ function readTranscriptFile(absPath) {
36302
37224
  return [];
36303
37225
  }
36304
37226
  }
36305
- var MAX_MESSAGES, NOISE_PREFIXES, REST_LABEL, LEGACY_TASK_MARKER, LEGACY_BANNER_PREAMBLE, COMMAND_NAME_RE, COMMAND_ARGS_RE, PEER_SESSION_PREAMBLE, QUEUED_STALE_MS, EDIT_CAP, TRUNC_MARKER, ENCRYPTED_PAYLOAD, AGENT_PROMPT_CAP, SEND_BODY_CAP, COMMAND_CAP, TOOL_INPUT_CAP, READ_LINE_CAP, READ_BYTE_CAP, IMAGE_MEDIA_EXT, SCREENSHOT_CACHE_DIR, SCREENSHOT_CACHE_MAX, SCREENSHOT_MAX_BASE64, screenshotTmpSeq, SENT_IMAGE_MAX_BYTES, SENT_FILES_MAX, TODO_ROWS_MAX, TODO_TEXT_CAP, TRANSCRIPT_CACHE_CAP, transcriptCache, PARSE_VERIFY_SAMPLE, TRANSCRIPT_DECODE_WINDOW, FOREIGN_SESSION_ID_RE, MAX_PINNED_BACKGROUND_OPERATIONS, CAPTION_MAX, _defaultCodexBackend, TRANSCRIPT_EARLIER_MAX_ITEMS, TRANSCRIPT_EARLIER_MAX_BYTES, fullDigestCache, FULL_DIGEST_CACHE_CAP, EMPTY_RETIRED_OPS;
37227
+ var MAX_MESSAGES, LATEST_WINDOW_ASK_REACH_ITEMS, LATEST_WINDOW_ASK_REACH_BYTES, NOISE_PREFIXES, INTERRUPT_MARKERS, REST_LABEL, LEGACY_TASK_MARKER, LEGACY_BANNER_PREAMBLE, COMMAND_NAME_RE, COMMAND_ARGS_RE, PEER_SESSION_PREAMBLE, QUEUED_STALE_MS, EDIT_CAP, TRUNC_MARKER, ENCRYPTED_PAYLOAD, AGENT_PROMPT_CAP, SEND_BODY_CAP, COMMAND_CAP, TOOL_INPUT_CAP, READ_LINE_CAP, READ_BYTE_CAP, IMAGE_MEDIA_EXT, SCREENSHOT_CACHE_DIR, SCREENSHOT_CACHE_MAX, SCREENSHOT_MAX_BASE64, screenshotTmpSeq, SENT_IMAGE_MAX_BYTES, SENT_FILES_MAX, TODO_ROWS_MAX, TODO_TEXT_CAP, TRANSCRIPT_CACHE_CAP, transcriptCache, PARSE_VERIFY_SAMPLE, TRANSCRIPT_DECODE_WINDOW, FOREIGN_SESSION_ID_RE, MAX_PINNED_BACKGROUND_OPERATIONS, CAPTION_MAX, _defaultCodexBackend, TRANSCRIPT_EARLIER_MAX_ITEMS, TRANSCRIPT_EARLIER_MAX_BYTES, fullDigestCache, FULL_DIGEST_CACHE_CAP, EMPTY_RETIRED_OPS;
36306
37228
  var init_transcript = __esm({
36307
37229
  "packages/server/src/transcript.ts"() {
36308
37230
  "use strict";
@@ -36317,7 +37239,10 @@ var init_transcript = __esm({
36317
37239
  init_bash_background();
36318
37240
  init_frizz_paths();
36319
37241
  MAX_MESSAGES = 300;
37242
+ LATEST_WINDOW_ASK_REACH_ITEMS = 100;
37243
+ LATEST_WINDOW_ASK_REACH_BYTES = 512 * 1024;
36320
37244
  NOISE_PREFIXES = ["<task-notification>", "[SYSTEM NOTIFICATION", "<system-reminder>", "<frizz-", "[frizz]"];
37245
+ INTERRUPT_MARKERS = ["[Request interrupted by user]", "[Request interrupted by user for tool use]"];
36321
37246
  REST_LABEL = "Agent rested";
36322
37247
  LEGACY_TASK_MARKER = "\nTASK:\n";
36323
37248
  LEGACY_BANNER_PREAMBLE = "Everything ABOVE this line is frizz system orientation. Everything BELOW the `TASK:` marker is the human operator's own prompt, verbatim.";
@@ -36394,7 +37319,7 @@ var init_open_external = __esm({
36394
37319
  // packages/server/src/local-file.ts
36395
37320
  import { spawn as spawn6 } from "node:child_process";
36396
37321
  import { readFileSync as readFileSync20, realpathSync as realpathSync8, statSync as statSync15 } from "node:fs";
36397
- import { homedir as homedir14 } from "node:os";
37322
+ import { homedir as homedir15 } from "node:os";
36398
37323
  import { isAbsolute as isAbsolute3, join as join31, resolve as resolve7, sep } from "node:path";
36399
37324
  function isUnder(real, root) {
36400
37325
  let rootReal;
@@ -36422,7 +37347,7 @@ function resolveLocalFile(rawPath, roots) {
36422
37347
  }
36423
37348
  return real;
36424
37349
  }
36425
- function resolveOpenableFile(raw2, projectDir, roots, home = homedir14()) {
37350
+ function resolveOpenableFile(raw2, projectDir, roots, home = homedir15()) {
36426
37351
  const trimmed = raw2.trim().replace(/:\d+(?::\d+)?$/, "");
36427
37352
  if (!trimmed) return null;
36428
37353
  const abs = trimmed === "~" ? home : trimmed.startsWith("~/") ? join31(home, trimmed.slice(2)) : isAbsolute3(trimmed) ? trimmed : resolve7(projectDir, trimmed);
@@ -36462,6 +37387,286 @@ var init_local_file = __esm({
36462
37387
  }
36463
37388
  });
36464
37389
 
37390
+ // packages/server/src/github-hovercard.ts
37391
+ function parseRef(ref) {
37392
+ const match2 = REF_PATTERN.exec(ref);
37393
+ if (!match2) return null;
37394
+ const [, owner, name, number, sha] = match2;
37395
+ if (number) {
37396
+ const n = Number(number);
37397
+ if (!Number.isSafeInteger(n) || n <= 0 || n > 2147483647) return null;
37398
+ return { key: ref, owner, name, number: n };
37399
+ }
37400
+ return { key: ref, owner, name, sha };
37401
+ }
37402
+ function isImmutableRef(ref) {
37403
+ return ref.sha !== void 0;
37404
+ }
37405
+ function buildRefQuery(refs) {
37406
+ const variables = {};
37407
+ const declarations = [];
37408
+ const selections = [];
37409
+ refs.forEach((ref, index) => {
37410
+ declarations.push(`$owner${index}: String!`, `$name${index}: String!`);
37411
+ variables[`owner${index}`] = ref.owner;
37412
+ variables[`name${index}`] = ref.name;
37413
+ if (ref.number !== void 0) {
37414
+ declarations.push(`$number${index}: Int!`);
37415
+ variables[`number${index}`] = ref.number;
37416
+ selections.push(` a${index}: repository(owner: $owner${index}, name: $name${index}) {
37417
+ target: issueOrPullRequest(number: $number${index}) {${ISSUE_FIELDS}
37418
+ }
37419
+ }`);
37420
+ } else {
37421
+ declarations.push(`$rev${index}: String!`);
37422
+ variables[`rev${index}`] = ref.sha;
37423
+ selections.push(` a${index}: repository(owner: $owner${index}, name: $name${index}) {
37424
+ target: object(expression: $rev${index}) {${COMMIT_FIELDS}
37425
+ }
37426
+ }`);
37427
+ }
37428
+ });
37429
+ return { query: `query(${declarations.join(", ")}) {
37430
+ ${selections.join("\n")}
37431
+ }`, variables };
37432
+ }
37433
+ function str(v) {
37434
+ return typeof v === "string" && v ? v : void 0;
37435
+ }
37436
+ function int(v) {
37437
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.trunc(v) : void 0;
37438
+ }
37439
+ function excerptBody(body) {
37440
+ if (!body) return "";
37441
+ const cleaned = body.replace(/<!--[\s\S]*?-->/g, "").trim();
37442
+ if (cleaned.length <= BODY_BUDGET) return cleaned;
37443
+ return `${cleaned.slice(0, BODY_BUDGET).trimEnd()}\u2026`;
37444
+ }
37445
+ function labelsOf(v) {
37446
+ const nodes = v?.nodes;
37447
+ if (!Array.isArray(nodes)) return [];
37448
+ const out = [];
37449
+ for (const node of nodes) {
37450
+ const name = str(node?.name);
37451
+ if (name) out.push({ name, color: str(node?.color) ?? "" });
37452
+ }
37453
+ return out;
37454
+ }
37455
+ function cardFromNode(ref, node, fetchedAt) {
37456
+ if (!node || typeof node !== "object") return null;
37457
+ const n = node;
37458
+ const repo = `${ref.owner}/${ref.name}`;
37459
+ const typename = str(n.__typename);
37460
+ if (typename === "Commit") {
37461
+ const headline = str(n.messageHeadline) ?? "";
37462
+ if (!headline) return null;
37463
+ const author2 = n.author ?? {};
37464
+ const user = author2.user ?? {};
37465
+ return {
37466
+ ref: ref.key,
37467
+ kind: "commit",
37468
+ repo,
37469
+ url: str(n.url) ?? `https://github.com/${repo}/commit/${str(n.oid) ?? ref.sha}`,
37470
+ title: headline,
37471
+ body: excerptBody(str(n.messageBody)),
37472
+ state: "",
37473
+ at: str(n.committedDate),
37474
+ ...str(user.login) ? { authorLogin: str(user.login) } : {},
37475
+ ...str(author2.name) ? { authorName: str(author2.name) } : {},
37476
+ // The LINKED ACCOUNT's avatar first. A commit's `GitActor.avatarUrl` is derived from the
37477
+ // committer email, so for an author with no matching account it is a Gravatar behind GitHub's
37478
+ // camo image proxy — a different host, and a generic silhouette rather than the person.
37479
+ ...str(user.avatarUrl) || str(author2.avatarUrl) ? { authorAvatar: str(user.avatarUrl) ?? str(author2.avatarUrl) } : {},
37480
+ labels: [],
37481
+ ...int(n.additions) !== void 0 ? { additions: int(n.additions) } : {},
37482
+ ...int(n.deletions) !== void 0 ? { deletions: int(n.deletions) } : {},
37483
+ ...int(n.changedFilesIfAvailable) !== void 0 ? { changedFiles: int(n.changedFilesIfAvailable) } : {},
37484
+ fetchedAt
37485
+ };
37486
+ }
37487
+ if (typename !== "Issue" && typename !== "PullRequest") return null;
37488
+ const title = str(n.title);
37489
+ if (!title) return null;
37490
+ const author = n.author ?? {};
37491
+ const state = n.isDraft === true && str(n.state)?.toUpperCase() === "OPEN" ? "DRAFT" : str(n.state)?.toUpperCase() ?? "";
37492
+ return {
37493
+ ref: ref.key,
37494
+ kind: typename === "Issue" ? "issue" : "pr",
37495
+ repo,
37496
+ url: str(n.url) ?? `https://github.com/${repo}/issues/${ref.number}`,
37497
+ title,
37498
+ body: excerptBody(str(n.body)),
37499
+ state,
37500
+ ...str(n.stateReason) ? { stateReason: str(n.stateReason).toUpperCase() } : {},
37501
+ at: str(n.createdAt),
37502
+ ...str(author.login) ? { authorLogin: str(author.login) } : {},
37503
+ ...str(author.avatarUrl) ? { authorAvatar: str(author.avatarUrl) } : {},
37504
+ labels: labelsOf(n.labels),
37505
+ ...int(n.additions) !== void 0 ? { additions: int(n.additions) } : {},
37506
+ ...int(n.deletions) !== void 0 ? { deletions: int(n.deletions) } : {},
37507
+ ...int(n.changedFiles) !== void 0 ? { changedFiles: int(n.changedFiles) } : {},
37508
+ ...int(n.comments?.totalCount) !== void 0 ? { comments: int(n.comments.totalCount) } : {},
37509
+ fetchedAt
37510
+ };
37511
+ }
37512
+ function parseRefResponse(raw2, refs, fetchedAt) {
37513
+ const data = raw2?.data;
37514
+ const cards = [];
37515
+ const missing = [];
37516
+ refs.forEach((ref, index) => {
37517
+ const alias = data?.[`a${index}`] ?? null;
37518
+ const card = cardFromNode(ref, alias?.target, fetchedAt);
37519
+ if (card) cards.push(card);
37520
+ else missing.push(ref.key);
37521
+ });
37522
+ return { cards, missing };
37523
+ }
37524
+ function conciseError2(error) {
37525
+ const raw2 = error instanceof Error ? error.message : String(error);
37526
+ return raw2.trim().split(/\r?\n/, 1)[0]?.slice(0, 300) || "unknown error";
37527
+ }
37528
+ function createGithubHovercardService(deps = {}) {
37529
+ const getToken = deps.getToken ?? defaultGetToken;
37530
+ const request = deps.request ?? globalThis.fetch;
37531
+ const now = deps.now ?? Date.now;
37532
+ const cache = /* @__PURE__ */ new Map();
37533
+ const inflight = /* @__PURE__ */ new Map();
37534
+ let tokenPromise;
37535
+ const token = async () => {
37536
+ if (!tokenPromise) {
37537
+ tokenPromise = getToken();
37538
+ tokenPromise.catch(() => {
37539
+ tokenPromise = void 0;
37540
+ });
37541
+ }
37542
+ return tokenPromise;
37543
+ };
37544
+ const isFresh = (ref, entry, refresh) => {
37545
+ if (isImmutableRef(ref) && entry.card) return true;
37546
+ if (refresh) return false;
37547
+ return now() - entry.at < MUTABLE_TTL_MS;
37548
+ };
37549
+ const fetchChunk = async (refs) => {
37550
+ const authToken = await token();
37551
+ const { query: query2, variables } = buildRefQuery(refs);
37552
+ const controller = new AbortController();
37553
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
37554
+ timeout.unref?.();
37555
+ try {
37556
+ const response = await request("https://api.github.com/graphql", {
37557
+ method: "POST",
37558
+ headers: {
37559
+ accept: "application/vnd.github+json",
37560
+ authorization: `Bearer ${authToken}`,
37561
+ "content-type": "application/json",
37562
+ "user-agent": "frizz-hovercard",
37563
+ "x-github-api-version": "2022-11-28"
37564
+ },
37565
+ body: JSON.stringify({ query: query2, variables }),
37566
+ signal: controller.signal
37567
+ });
37568
+ const text = await response.text();
37569
+ if (!response.ok) {
37570
+ if (response.status === 401 || response.status === 403) tokenPromise = void 0;
37571
+ throw new Error(`GitHub GraphQL returned HTTP ${response.status}`);
37572
+ }
37573
+ let body;
37574
+ try {
37575
+ body = JSON.parse(text);
37576
+ } catch {
37577
+ throw new Error("GitHub GraphQL returned malformed JSON");
37578
+ }
37579
+ const at = now();
37580
+ const { cards, missing } = parseRefResponse(body, refs, at);
37581
+ for (const card of cards) cache.set(card.ref, { card, at });
37582
+ for (const key of missing) cache.set(key, { missing: true, at });
37583
+ } finally {
37584
+ clearTimeout(timeout);
37585
+ }
37586
+ };
37587
+ return {
37588
+ size: () => cache.size,
37589
+ async preview(refs, opts = {}) {
37590
+ const refresh = opts.refresh === true;
37591
+ const parsed = /* @__PURE__ */ new Map();
37592
+ const missing = [];
37593
+ for (const raw2 of refs) {
37594
+ if (parsed.has(raw2)) continue;
37595
+ const ref = parseRef(raw2);
37596
+ if (ref) parsed.set(raw2, ref);
37597
+ else missing.push(raw2);
37598
+ }
37599
+ const wanted = [];
37600
+ const joins = [];
37601
+ for (const ref of parsed.values()) {
37602
+ const entry = cache.get(ref.key);
37603
+ if (entry && isFresh(ref, entry, refresh)) continue;
37604
+ const pending = inflight.get(ref.key);
37605
+ if (pending && !refresh) joins.push(pending);
37606
+ else wanted.push(ref);
37607
+ }
37608
+ let error;
37609
+ if (wanted.length > 0) {
37610
+ const chunks = [];
37611
+ for (let i = 0; i < wanted.length; i += MAX_REFS_PER_QUERY) chunks.push(wanted.slice(i, i + MAX_REFS_PER_QUERY));
37612
+ const runs = chunks.map((chunk) => {
37613
+ const run3 = fetchChunk(chunk);
37614
+ for (const ref of chunk) inflight.set(ref.key, run3);
37615
+ return run3.finally(() => {
37616
+ for (const ref of chunk) if (inflight.get(ref.key) === run3) inflight.delete(ref.key);
37617
+ });
37618
+ });
37619
+ const settled = await Promise.allSettled([...runs, ...joins]);
37620
+ const failure = settled.find((r) => r.status === "rejected");
37621
+ if (failure && failure.status === "rejected") error = conciseError2(failure.reason);
37622
+ } else if (joins.length > 0) {
37623
+ const settled = await Promise.allSettled(joins);
37624
+ const failure = settled.find((r) => r.status === "rejected");
37625
+ if (failure && failure.status === "rejected") error = conciseError2(failure.reason);
37626
+ }
37627
+ const cards = [];
37628
+ for (const ref of parsed.values()) {
37629
+ const entry = cache.get(ref.key);
37630
+ if (entry?.card) cards.push(entry.card);
37631
+ else if (entry?.missing) missing.push(ref.key);
37632
+ }
37633
+ return { cards, missing, ...error ? { error } : {} };
37634
+ }
37635
+ };
37636
+ }
37637
+ var REQUEST_TIMEOUT_MS2, MAX_REFS_PER_QUERY, BODY_BUDGET, MUTABLE_TTL_MS, REF_SEG, REF_PATTERN, ISSUE_FIELDS, COMMIT_FIELDS;
37638
+ var init_github_hovercard = __esm({
37639
+ "packages/server/src/github-hovercard.ts"() {
37640
+ "use strict";
37641
+ init_github_review();
37642
+ REQUEST_TIMEOUT_MS2 = 15e3;
37643
+ MAX_REFS_PER_QUERY = 25;
37644
+ BODY_BUDGET = 400;
37645
+ MUTABLE_TTL_MS = 5 * 6e4;
37646
+ REF_SEG = "[A-Za-z0-9][A-Za-z0-9._-]*";
37647
+ REF_PATTERN = new RegExp(`^(${REF_SEG})/(${REF_SEG})(?:#([1-9]\\d{0,9})|@([0-9a-f]{7,40}))$`);
37648
+ ISSUE_FIELDS = `
37649
+ ... on Issue {
37650
+ __typename number title body state stateReason url createdAt
37651
+ author { login avatarUrl }
37652
+ labels(first: 6) { nodes { name color } }
37653
+ comments { totalCount }
37654
+ }
37655
+ ... on PullRequest {
37656
+ __typename number title body state isDraft url createdAt additions deletions changedFiles
37657
+ author { login avatarUrl }
37658
+ labels(first: 6) { nodes { name color } }
37659
+ comments { totalCount }
37660
+ }`;
37661
+ COMMIT_FIELDS = `
37662
+ ... on Commit {
37663
+ __typename oid abbreviatedOid url messageHeadline messageBody committedDate
37664
+ additions deletions changedFilesIfAvailable
37665
+ author { name avatarUrl user { login avatarUrl } }
37666
+ }`;
37667
+ }
37668
+ });
37669
+
36465
37670
  // packages/server/src/backend/account-actions.ts
36466
37671
  import { execFile as execFile8 } from "node:child_process";
36467
37672
  import { promisify as promisify7 } from "node:util";
@@ -36897,7 +38102,7 @@ import { join as join32, resolve as resolve8 } from "node:path";
36897
38102
  import { randomUUID as randomUUID16 } from "node:crypto";
36898
38103
  import { basename as basename10, dirname as dirname14 } from "node:path";
36899
38104
  import { existsSync as existsSync12, mkdirSync as mkdirSync16, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
36900
- import { homedir as homedir15 } from "node:os";
38105
+ import { homedir as homedir16 } from "node:os";
36901
38106
  function validateGithubDispatchProfile(input) {
36902
38107
  validateThreadProfile(input.backend, input.model, input.effort);
36903
38108
  }
@@ -37056,7 +38261,7 @@ function projectCard(entry, stale) {
37056
38261
  function addProjectAtPath(input) {
37057
38262
  const typed = input.trim();
37058
38263
  if (!typed) throw new Error("Enter a folder path.");
37059
- const expanded = typed === "~" || typed.startsWith("~/") ? join32(homedir15(), typed.slice(1)) : typed;
38264
+ const expanded = typed === "~" || typed.startsWith("~/") ? join32(homedir16(), typed.slice(1)) : typed;
37060
38265
  const absolute = resolve8(expanded);
37061
38266
  let stats;
37062
38267
  try {
@@ -37066,7 +38271,7 @@ function addProjectAtPath(input) {
37066
38271
  }
37067
38272
  if (!stats.isDirectory()) throw new Error(`That is a file, not a folder: ${absolute}`);
37068
38273
  const root = discoverProjectRoot(absolute);
37069
- const id = ensureProjectIdFile(root, homedir15(), existingProjectId(root));
38274
+ const id = ensureProjectIdFile(root, homedir16(), existingProjectId(root));
37070
38275
  const remoteOwner = resolveProjectLabel(root)?.split("/")[0];
37071
38276
  let registered = registerProject({ dir: root, id, remoteOwner });
37072
38277
  if (registered.action === "duplicate") {
@@ -37139,14 +38344,18 @@ function createRouter(ctx) {
37139
38344
  if (input.prompt === null || input.intervalSeconds === void 0) return null;
37140
38345
  return input.intervalSeconds * 1e3;
37141
38346
  }
37142
- function armedWatchViews(slug) {
37143
- return ctx.storage.listThreadWatches(slug, { armedOnly: true }).map((w) => ({
37144
- id: w.id,
37145
- kind: w.kind,
37146
- target: w.target,
37147
- state: w.state,
37148
- createdAt: new Date(w.created_at).toISOString()
37149
- }));
38347
+ function armedPrWatchViews(slug) {
38348
+ const github = readGithubStatusBook(ctx.storage.getSetting(GITHUB_STATUS_SETTING));
38349
+ return ctx.storage.listPrWatches(slug, { armedOnly: true }).map((w) => {
38350
+ const target = `${w.owner}/${w.repo}#${w.number}`;
38351
+ return {
38352
+ id: w.id,
38353
+ target,
38354
+ state: w.state,
38355
+ createdAt: new Date(w.created_at).toISOString(),
38356
+ ...github[target] ? { github: github[target] } : {}
38357
+ };
38358
+ });
37150
38359
  }
37151
38360
  function armedTimerViews(slug) {
37152
38361
  return ctx.storage.listThreadTimers(slug, { armedOnly: true }).map((t) => ({
@@ -37162,16 +38371,14 @@ function createRouter(ctx) {
37162
38371
  if (!row) throw new Error(`thread ${slug} is not registered`);
37163
38372
  const otherOn = trigger === "rest" ? row.recurring_on_schedule === 1 : row.recurring_on_rest === 1;
37164
38373
  const compactOn = row.recurring_on_compact === 1;
37165
- const holdOn = row.recurring_pause_on_questions === 1;
37166
38374
  const storedSeconds = row.recurring_interval_ms ? Math.round(row.recurring_interval_ms / 1e3) : void 0;
37167
38375
  const on = write.enabled && write.prompt !== null;
37168
38376
  const prompt = on ? write.prompt : otherOn || compactOn ? row.recurring_prompt ?? null : null;
37169
- const next = prompt === null ? { prompt: null, stopHook: false, heartbeat: false, postCompaction: false, pauseOnQuestions: false } : {
38377
+ const next = prompt === null ? { prompt: null, stopHook: false, heartbeat: false, postCompaction: false } : {
37170
38378
  prompt,
37171
38379
  stopHook: trigger === "rest" ? on : otherOn,
37172
38380
  heartbeat: trigger === "schedule" ? on : otherOn,
37173
38381
  postCompaction: compactOn,
37174
- pauseOnQuestions: holdOn,
37175
38382
  intervalSeconds: trigger === "schedule" && on ? write.intervalSeconds : storedSeconds
37176
38383
  };
37177
38384
  assertRecurringPromptArmable(next, row);
@@ -37180,7 +38387,6 @@ function createRouter(ctx) {
37180
38387
  stopHook: next.stopHook,
37181
38388
  heartbeat: next.heartbeat,
37182
38389
  postCompaction: next.postCompaction,
37183
- pauseOnQuestions: next.pauseOnQuestions,
37184
38390
  intervalMs: recurringIntervalMs(next),
37185
38391
  armedAt: (/* @__PURE__ */ new Date()).toISOString()
37186
38392
  })) {
@@ -38004,7 +39210,6 @@ function createRouter(ctx) {
38004
39210
  stopHook: input.stopHook,
38005
39211
  heartbeat: input.heartbeat,
38006
39212
  postCompaction: input.postCompaction,
38007
- pauseOnQuestions: input.pauseOnQuestions,
38008
39213
  intervalMs: recurringIntervalMs(input),
38009
39214
  armedAt: (/* @__PURE__ */ new Date()).toISOString()
38010
39215
  })) {
@@ -38034,7 +39239,6 @@ function createRouter(ctx) {
38034
39239
  stopHook: input.stopHook,
38035
39240
  heartbeat: input.heartbeat,
38036
39241
  postCompaction: input.postCompaction,
38037
- pauseOnQuestions: input.pauseOnQuestions,
38038
39242
  intervalMs: recurringIntervalMs(input),
38039
39243
  armedAt: (/* @__PURE__ */ new Date()).toISOString()
38040
39244
  })) {
@@ -38111,56 +39315,106 @@ function createRouter(ctx) {
38111
39315
  output: OwnThreadTimersResult,
38112
39316
  handler: async ({ input }) => ({ timers: armedTimerViews(input.slug) })
38113
39317
  }),
38114
- // ---- THE WATCHER REGISTRY (add / drop / list) ------------------------------------------------
38115
- // The worker's own waits, from `mcp__frizz__watch`. Same caller and therefore the same rules as the
38116
- // timers above: slug-only (the MCP server outlives the session ids underneath it), and no thread
38117
- // parameter a model could aim elsewhere.
38118
- addOwnThreadWatch: mutation({
38119
- input: AddOwnThreadWatchInput,
38120
- output: AddOwnThreadWatchResult,
39318
+ // EVERY kind of background work this thread has out, with the id its awaiting fence names it by.
39319
+ //
39320
+ // The fence is structural it references live things by id so a worker that no longer has those
39321
+ // ids to hand cannot write a correct fence at all, and the failure is silent: it names something
39322
+ // wrong, frizz refuses the park, and the thread queues. That is the exact stall the grammar exists
39323
+ // to prevent, so the ids have to be RETRIEVABLE rather than remembered. Same list the sign-off nudge
39324
+ // prints, from the same source, so the two can never tell a worker different things.
39325
+ //
39326
+ // A shell is keyed by its RUNTIME task id ("Command running in background with ID: bzvtnt3ig"),
39327
+ // never its launch tool_use id: that is the string the worker was actually shown and the one it will
39328
+ // reach for. Falls back to the launch id only when the ack has not landed yet.
39329
+ listOwnThreadActivity: mutation({
39330
+ input: ListOwnThreadActivityInput,
39331
+ output: OwnThreadActivityResult,
39332
+ handler: async ({ input }) => {
39333
+ const tele = ctx.tailer.get(input.slug);
39334
+ const activity = [];
39335
+ for (const sh of tele?.bgShells ?? []) {
39336
+ if (sh.state !== "running") continue;
39337
+ const id = sh.taskId ?? sh.id;
39338
+ if (id) activity.push({ kind: "shell", id, label: sh.label, since: sh.startedAt });
39339
+ }
39340
+ for (const a of tele?.subAgents ?? []) {
39341
+ if (a.state !== "running") continue;
39342
+ if (a.id) activity.push({ kind: "agent", id: a.id, label: a.label, since: a.startedAt });
39343
+ }
39344
+ for (const t of ctx.storage.listThreadTimers(input.slug, { armedOnly: true })) {
39345
+ activity.push({
39346
+ kind: "timer",
39347
+ id: t.id,
39348
+ label: t.prompt.trim().replace(/\s+/g, " ").slice(0, 120),
39349
+ since: new Date(t.created_at).toISOString(),
39350
+ until: new Date(t.fire_at).toISOString()
39351
+ });
39352
+ }
39353
+ for (const w of ctx.storage.listPrWatches(input.slug, { armedOnly: true })) {
39354
+ activity.push({
39355
+ kind: "pr",
39356
+ id: `${w.owner}/${w.repo}#${w.number}`,
39357
+ label: `${w.owner}/${w.repo}#${w.number}`,
39358
+ since: new Date(w.created_at).toISOString()
39359
+ });
39360
+ }
39361
+ return { activity };
39362
+ }
39363
+ }),
39364
+ // THE WATCHER REGISTRY IS GONE (2026-08-14). `mcp__frizz__watch` and its four procedures are
39365
+ // deleted rather than shimmed: a wait is now a `watch:` line in the worker's own ```awaiting fence,
39366
+ // which is BOTH the park and the wake (scheduler `watchVerdict`), so there is nothing to register and
39367
+ // nothing an alias could usefully do. A session dispatched before the change still holds the old MCP
39368
+ // binary and will get a 404 from it — which is the honest answer, and the tool it should not be
39369
+ // calling is the only casualty.
39370
+ // ---- REGISTERED PR WATCHERS (add / drop / list) ---------------------------------------------
39371
+ // The worker's own PR watchers, from `mcp__frizz__watch_pr`. Same caller and therefore the same rules
39372
+ // as the timers above: slug-only (the MCP server outlives the session ids underneath it), and no
39373
+ // thread parameter a model could aim elsewhere.
39374
+ addOwnPrWatch: mutation({
39375
+ input: AddOwnPrWatchInput,
39376
+ output: AddOwnPrWatchResult,
38121
39377
  handler: async ({ input }) => {
38122
39378
  const row = ctx.storage.getSession(input.slug);
38123
39379
  if (!row) throw new Error(`thread ${input.slug} is not registered`);
38124
39380
  if (row.state === "archived" || row.archived === 1) {
38125
39381
  throw new Error("Reopen this thread before registering a watcher on it");
38126
39382
  }
38127
- const armed = ctx.storage.listThreadWatches(input.slug, { armedOnly: true });
38128
- const existing = armed.find((w) => w.kind === input.kind && w.target === input.target);
38129
- if (existing) return { id: existing.id, alreadyArmed: true, watches: armedWatchViews(input.slug) };
38130
- if (armed.length >= WATCH_MAX_ARMED) {
38131
- throw new Error(`this thread already has ${armed.length} armed watchers (the limit is ${WATCH_MAX_ARMED}) \u2014 drop one first`);
39383
+ const ref = parsePrRef(input.target);
39384
+ if (!ref) {
39385
+ throw new Error(`\`${input.target}\` is not a pull request I can watch \u2014 give owner/repo#123 or a PR URL`);
39386
+ }
39387
+ const armed = ctx.storage.listPrWatches(input.slug, { armedOnly: true });
39388
+ const existing = armed.find((w) => w.owner === ref.owner && w.repo === ref.repo && w.number === ref.number);
39389
+ const target = `${ref.owner}/${ref.repo}#${ref.number}`;
39390
+ if (existing) return { id: existing.id, target, alreadyArmed: true, watches: armedPrWatchViews(input.slug) };
39391
+ if (armed.length >= PR_WATCH_MAX_ARMED) {
39392
+ throw new Error(`this thread already watches ${armed.length} pull requests (the limit is ${PR_WATCH_MAX_ARMED}) \u2014 drop one first`);
39393
+ }
39394
+ const forMs = input.for === void 0 ? PR_WATCH_DEFAULT_FOR_MS : parseAwaitingDuration(input.for);
39395
+ if (forMs === null) {
39396
+ throw new Error(`\`for: ${input.for}\` is not a duration \u2014 give one like \`30m\`, \`2h\` or \`3d\` (max 24h)`);
38132
39397
  }
38133
- const id = `wch_${randomUUID16().replace(/-/g, "").slice(0, 12)}`;
38134
- ctx.storage.armThreadWatch({ id, slug: input.slug, kind: input.kind, target: input.target, createdAtMs: Date.now(), foreground: input.foreground });
39398
+ const now = Date.now();
39399
+ const id = `prw_${randomUUID16().replace(/-/g, "").slice(0, 12)}`;
39400
+ ctx.storage.armPrWatch({ id, slug: input.slug, owner: ref.owner, repo: ref.repo, number: ref.number, createdAtMs: now, expiresAtMs: now + forMs });
38135
39401
  ctx.board.refresh();
38136
- return { id, alreadyArmed: false, watches: armedWatchViews(input.slug) };
39402
+ return { id, target, alreadyArmed: false, watches: armedPrWatchViews(input.slug) };
38137
39403
  }
38138
39404
  }),
38139
- dropOwnThreadWatch: mutation({
38140
- input: DropOwnThreadWatchInput,
38141
- output: DropOwnThreadWatchResult,
39405
+ dropOwnPrWatch: mutation({
39406
+ input: DropOwnPrWatchInput,
39407
+ output: DropOwnPrWatchResult,
38142
39408
  handler: async ({ input }) => {
38143
- const dropped = ctx.storage.dropThreadWatch(input.slug, input.id, Date.now());
39409
+ const dropped = ctx.storage.dropPrWatch(input.slug, input.id, Date.now());
38144
39410
  if (dropped) ctx.board.refresh();
38145
- return { dropped, watches: armedWatchViews(input.slug) };
39411
+ return { dropped, watches: armedPrWatchViews(input.slug) };
38146
39412
  }
38147
39413
  }),
38148
- // Hand a foreground watch back to the scheduler. Called by the blocking tool call when its timeout
38149
- // expires, so a wait the worker started degrades into a durable one rather than being lost — which
38150
- // is the property that makes choosing the blocking mode safe.
38151
- promoteOwnThreadWatch: mutation({
38152
- input: PromoteOwnThreadWatchInput,
38153
- output: PromoteOwnThreadWatchResult,
38154
- handler: async ({ input }) => {
38155
- const promoted = ctx.storage.promoteThreadWatch(input.slug, input.id);
38156
- if (promoted) ctx.board.refresh();
38157
- return { promoted, watches: armedWatchViews(input.slug) };
38158
- }
38159
- }),
38160
- listOwnThreadWatches: mutation({
38161
- input: ListOwnThreadWatchesInput,
38162
- output: OwnThreadWatchesResult,
38163
- handler: async ({ input }) => ({ watches: armedWatchViews(input.slug) })
39414
+ listOwnPrWatches: mutation({
39415
+ input: ListOwnPrWatchesInput,
39416
+ output: OwnPrWatchesResult,
39417
+ handler: async ({ input }) => ({ watches: armedPrWatchViews(input.slug) })
38164
39418
  }),
38165
39419
  // The SUPERSEDED worker procedures, aliased onto the row above — see SetOwnThreadStopHookInput for
38166
39420
  // why they cannot simply be deleted. A worker's MCP server outlives every server restart, so these
@@ -38238,10 +39492,7 @@ function createRouter(ctx) {
38238
39492
  if (!fenceAt || !Number.isFinite(Date.parse(fenceAt)) || fenceAt !== input.fenceAt) {
38239
39493
  throw new Error("This awaiting proposal changed before it could be confirmed");
38240
39494
  }
38241
- const snoozedUntil = hint.kind === "timer" ? canonicalSnoozeInstant(hint.value) : null;
38242
- if (hint.kind === "timer" && !snoozedUntil) {
38243
- throw new Error("This awaiting proposal changed before it could be confirmed");
38244
- }
39495
+ const snoozedUntil = null;
38245
39496
  if (snoozedUntil && Date.parse(snoozedUntil) <= Date.now()) {
38246
39497
  throw new Error("This scheduled time has already passed");
38247
39498
  }
@@ -38738,12 +39989,12 @@ function createRouter(ctx) {
38738
39989
  output: DispatchPreferences,
38739
39990
  handler: async ({ input }) => setDispatchPreference(ctx.storage, ctx.getSettings(), input, readCodexModels())
38740
39991
  }),
38741
- // The shipped GitHub batch-dispatch prompt templates (single source of truth: server/github.ts).
38742
- // The Settings UI reads these to prefill the editors for editing and to power "reset to default";
38743
- // an empty/unset githubIssuePrompt/githubPrPrompt setting means the server uses exactly these.
39992
+ // The shipped GitHub batch-dispatch prompt template (single source of truth: server/github.ts).
39993
+ // The Settings UI reads it to prefill the editor and to power "reset to default"; an empty/unset
39994
+ // githubPrompt setting means the server uses exactly this. One template serves issues AND PRs.
38744
39995
  githubPromptDefaults: query({
38745
- output: external_exports.object({ issue: external_exports.string(), pr: external_exports.string() }),
38746
- handler: async () => ({ issue: DEFAULT_ISSUE_PROMPT, pr: DEFAULT_PR_PROMPT })
39996
+ output: external_exports.object({ prompt: external_exports.string() }),
39997
+ handler: async () => ({ prompt: DEFAULT_GITHUB_PROMPT })
38747
39998
  }),
38748
39999
  // ---- GitHub-first batch dispatch ----
38749
40000
  // gh availability: installed (cache-warmed resolveInstalled) + inRepo/nameWithOwner (cache-warmed
@@ -38775,6 +40026,18 @@ function createRouter(ctx) {
38775
40026
  return await listItems(repo, input.kind, input.sort, input.page, input.perPage);
38776
40027
  }
38777
40028
  }),
40029
+ // Hovercard data for every GitHub reference the client autolinked into the prose on screen —
40030
+ // ONE request for a whole page of refs, answered from a process-lifetime cache (see
40031
+ // github-hovercard.ts). The client asks as the prose renders, so the hover itself never waits on
40032
+ // the network; `refresh` is its revalidation of the handful it is actually pointing at.
40033
+ //
40034
+ // Never throws: a missing gh, an unauthenticated one, a rate limit and a network stall all come
40035
+ // back as `error` with whatever cards the cache already holds, and the anchor stays a plain link.
40036
+ githubRefPreview: query({
40037
+ input: GithubRefPreviewInput,
40038
+ output: GithubRefPreviewResult,
40039
+ handler: async ({ input }) => await githubHovercards.preview(input.refs, { refresh: input.refresh })
40040
+ }),
38778
40041
  // Spin up one frizz thread per checked item: hydrate each fresh from gh, template a server-side
38779
40042
  // prompt (single source of truth, unit-tested), then REUSE ctx.dispatcher.dispatch (no new spawn
38780
40043
  // logic). SEQUENTIAL — a burst of 20 concurrent tmux spawns would hammer the box (risk 5). A
@@ -38786,14 +40049,13 @@ function createRouter(ctx) {
38786
40049
  validateGithubDispatchProfile(input);
38787
40050
  const repo = await resolveRepo();
38788
40051
  if (!repo) throw new Error("not a GitHub repo");
38789
- const settings = ctx.getSettings();
40052
+ const template = effectiveTemplate(ctx.getSettings().githubPrompt);
38790
40053
  const dispatched = [];
38791
40054
  const failed = [];
38792
40055
  for (const it of input.items) {
38793
40056
  try {
38794
40057
  const title = `${it.kind === "issue" ? "Investigate" : "Review"} ${repo}#${it.number}`;
38795
40058
  const slug = resolveSlug(frizzDir, slugify(title), (s) => ctx.storage.getSession(s) !== void 0);
38796
- const template = effectiveTemplate(it.kind, it.kind === "issue" ? settings.githubIssuePrompt : settings.githubPrPrompt);
38797
40059
  const hydrated = it.kind === "issue" ? await hydrateIssue(repo, it.number) : await hydratePr(repo, it.number);
38798
40060
  const prompt = renderGithubPrompt(template, repo, hydrated, slug, it.kind);
38799
40061
  const request = githubDispatcherRequest(input, { prompt, title, slug });
@@ -38808,7 +40070,7 @@ function createRouter(ctx) {
38808
40070
  })
38809
40071
  };
38810
40072
  }
38811
- var SlugInput, cachedLivenessTerminator, HOLD_OPS_MAX, HOLD_LABEL_MAX;
40073
+ var SlugInput, cachedLivenessTerminator, HOLD_OPS_MAX, HOLD_LABEL_MAX, githubHovercards;
38812
40074
  var init_router5 = __esm({
38813
40075
  "packages/server/src/router.ts"() {
38814
40076
  "use strict";
@@ -38826,6 +40088,7 @@ var init_router5 = __esm({
38826
40088
  init_local_file();
38827
40089
  init_project();
38828
40090
  init_github();
40091
+ init_github_hovercard();
38829
40092
  init_dispatch();
38830
40093
  init_codex_models();
38831
40094
  init_codex();
@@ -38860,6 +40123,7 @@ var init_router5 = __esm({
38860
40123
  };
38861
40124
  HOLD_OPS_MAX = 8;
38862
40125
  HOLD_LABEL_MAX = 100;
40126
+ githubHovercards = createGithubHovercardService();
38863
40127
  }
38864
40128
  });
38865
40129
 
@@ -38999,6 +40263,52 @@ var init_local_origin = __esm({
38999
40263
  }
39000
40264
  });
39001
40265
 
40266
+ // packages/server/src/compression.ts
40267
+ import { brotliCompressSync, gzipSync, constants as constants3 } from "node:zlib";
40268
+ function negotiateEncoding(acceptEncoding) {
40269
+ if (!acceptEncoding) return null;
40270
+ const offers = /* @__PURE__ */ new Map();
40271
+ for (const part of acceptEncoding.split(",")) {
40272
+ const [name, ...params] = part.trim().split(";");
40273
+ if (!name) continue;
40274
+ const q = params.map((p) => /^\s*q=([0-9.]+)\s*$/i.exec(p)).find(Boolean);
40275
+ offers.set(name.trim().toLowerCase(), q ? Number(q[1]) : 1);
40276
+ }
40277
+ const wildcard = offers.get("*");
40278
+ const accepted = (name) => {
40279
+ const explicit = offers.get(name);
40280
+ if (explicit !== void 0) return explicit > 0;
40281
+ return wildcard !== void 0 && wildcard > 0;
40282
+ };
40283
+ if (accepted("br")) return "br";
40284
+ if (accepted("gzip")) return "gzip";
40285
+ return null;
40286
+ }
40287
+ function compress(body, encoding) {
40288
+ const out = encoding === "br" ? brotliCompressSync(body, {
40289
+ params: {
40290
+ [constants3.BROTLI_PARAM_QUALITY]: 4,
40291
+ // Telling brotli the input size lets it size its window instead of guessing.
40292
+ [constants3.BROTLI_PARAM_SIZE_HINT]: body.byteLength
40293
+ }
40294
+ }) : gzipSync(body, { level: 6 });
40295
+ return Uint8Array.from(out);
40296
+ }
40297
+ function shouldCompress(headers, byteLength2) {
40298
+ if (byteLength2 < MIN_COMPRESS_BYTES) return false;
40299
+ if (headers.has("content-encoding")) return false;
40300
+ const type = headers.get("content-type") ?? "";
40301
+ if (type.includes("text/event-stream")) return false;
40302
+ return /json|text\/|javascript|xml|svg/i.test(type);
40303
+ }
40304
+ var MIN_COMPRESS_BYTES;
40305
+ var init_compression = __esm({
40306
+ "packages/server/src/compression.ts"() {
40307
+ "use strict";
40308
+ MIN_COMPRESS_BYTES = 1024;
40309
+ }
40310
+ });
40311
+
39002
40312
  // packages/server/src/local-image.ts
39003
40313
  import { readFileSync as readFileSync22, realpathSync as realpathSync9, statSync as statSync18 } from "node:fs";
39004
40314
  import { extname as extname2, isAbsolute as isAbsolute4 } from "node:path";
@@ -39035,7 +40345,7 @@ var init_local_image = __esm({
39035
40345
 
39036
40346
  // packages/server/src/project-icon.ts
39037
40347
  import { readFileSync as readFileSync23, readdirSync as readdirSync11, realpathSync as realpathSync10, statSync as statSync19 } from "node:fs";
39038
- import { homedir as homedir16 } from "node:os";
40348
+ import { homedir as homedir17 } from "node:os";
39039
40349
  import { basename as basename11, dirname as dirname15, extname as extname3, join as join33, relative, sep as sep2 } from "node:path";
39040
40350
  function normalizeStem(stem) {
39041
40351
  const sized = stem.toLowerCase().replace(/[@-]\d+(\.\d+)?x(\d+)?$/u, "").replace(/[-_]\d{2,4}$/u, "");
@@ -39174,7 +40484,7 @@ function iconMediaType(path) {
39174
40484
  }
39175
40485
  function resolveProjectIconResponse(id, options = {}) {
39176
40486
  if (!id) return { status: 400 };
39177
- const home = options.home ?? homedir16();
40487
+ const home = options.home ?? homedir17();
39178
40488
  const entry = readRegistry(home).projects.find((project) => project.id === id);
39179
40489
  if (!entry) return { status: 404 };
39180
40490
  const path = resolveProjectIcon(id, (root) => detectProjectIcon(root)?.path, { home });
@@ -39502,6 +40812,21 @@ function createApp(ctx, options = {}) {
39502
40812
  c.header("x-frizz-boot", ctx.bootId);
39503
40813
  await next();
39504
40814
  });
40815
+ app.use(`${frizzRoute("/rpc")}/*`, async (c, next) => {
40816
+ await next();
40817
+ const encoding = negotiateEncoding(c.req.header("accept-encoding"));
40818
+ if (!encoding || !c.res.body) return;
40819
+ const body = new Uint8Array(await c.res.arrayBuffer());
40820
+ if (!shouldCompress(c.res.headers, body.byteLength)) {
40821
+ c.res = new Response(body, { status: c.res.status, headers: c.res.headers });
40822
+ return;
40823
+ }
40824
+ const headers = new Headers(c.res.headers);
40825
+ headers.set("content-encoding", encoding);
40826
+ headers.set("vary", "Accept-Encoding");
40827
+ headers.delete("content-length");
40828
+ c.res = new Response(compress(body, encoding), { status: c.res.status, headers });
40829
+ });
39505
40830
  app.get(frizzRoute("/health"), (c) => c.json({
39506
40831
  ok: true,
39507
40832
  projectId: ctx.project.id,
@@ -39608,6 +40933,7 @@ var init_app = __esm({
39608
40933
  init_src();
39609
40934
  init_router5();
39610
40935
  init_local_origin();
40936
+ init_compression();
39611
40937
  init_local_image();
39612
40938
  init_project_icon();
39613
40939
  init_local_visualization();
@@ -40071,6 +41397,11 @@ function createAppSocketServer(deps) {
40071
41397
  const bufferedAmount = deps.bufferedAmount ?? ((ws) => ws.bufferedAmount);
40072
41398
  const now = deps.now ?? Date.now;
40073
41399
  const serializeMessage = deps.serializeMessage ?? JSON.stringify;
41400
+ const scheduleSubscriptionFlush = deps.scheduleSubscriptionFlush ?? ((flush) => {
41401
+ const immediate = setImmediate(flush);
41402
+ immediate.unref?.();
41403
+ return () => clearImmediate(immediate);
41404
+ });
40074
41405
  const terminationTimers = /* @__PURE__ */ new WeakMap();
40075
41406
  const keyframes = /* @__PURE__ */ new Set();
40076
41407
  let closing = false;
@@ -40307,7 +41638,33 @@ function createAppSocketServer(deps) {
40307
41638
  scheduleTranscriptFlush(BACKPRESSURE_RETRY_MS);
40308
41639
  }
40309
41640
  const pendingSubscriptionPushes = /* @__PURE__ */ new Map();
40310
- let subscriptionFlushImmediate = null;
41641
+ let subscriptionFlushArmed = false;
41642
+ let cancelSubscriptionFlush = null;
41643
+ function flushSubscriptionPushes() {
41644
+ subscriptionFlushArmed = false;
41645
+ cancelSubscriptionFlush = null;
41646
+ if (closing) {
41647
+ pendingSubscriptionPushes.clear();
41648
+ return;
41649
+ }
41650
+ const batch = [...pendingSubscriptionPushes];
41651
+ pendingSubscriptionPushes.clear();
41652
+ const batchResults = /* @__PURE__ */ new Map();
41653
+ for (const [candidate, candidateSlugs] of batch) {
41654
+ if (candidate.readyState !== candidate.OPEN) continue;
41655
+ for (const candidateSlug of candidateSlugs) {
41656
+ if (!registry.isSubscribed(candidate, candidateSlug)) continue;
41657
+ let result = batchResults.get(candidateSlug);
41658
+ if (!result || result.kind === "limited" && result.scope === "origin") {
41659
+ result = snapshotFor(candidateSlug, socketOrigins.get(candidate));
41660
+ if (!(result.kind === "limited" && result.scope === "origin")) {
41661
+ batchResults.set(candidateSlug, result);
41662
+ }
41663
+ }
41664
+ pushSnapshotResult(candidate, candidateSlug, result);
41665
+ }
41666
+ }
41667
+ }
40311
41668
  function enqueueSubscriptionPush(ws, slug) {
40312
41669
  deleteTranscriptSnapshot(slug);
40313
41670
  let slugs = pendingSubscriptionPushes.get(ws);
@@ -40316,32 +41673,10 @@ function createAppSocketServer(deps) {
40316
41673
  pendingSubscriptionPushes.set(ws, slugs);
40317
41674
  }
40318
41675
  slugs.add(slug);
40319
- if (subscriptionFlushImmediate) return;
40320
- subscriptionFlushImmediate = setImmediate(() => {
40321
- subscriptionFlushImmediate = null;
40322
- if (closing) {
40323
- pendingSubscriptionPushes.clear();
40324
- return;
40325
- }
40326
- const batch = [...pendingSubscriptionPushes];
40327
- pendingSubscriptionPushes.clear();
40328
- const batchResults = /* @__PURE__ */ new Map();
40329
- for (const [candidate, candidateSlugs] of batch) {
40330
- if (candidate.readyState !== candidate.OPEN) continue;
40331
- for (const candidateSlug of candidateSlugs) {
40332
- if (!registry.isSubscribed(candidate, candidateSlug)) continue;
40333
- let result = batchResults.get(candidateSlug);
40334
- if (!result || result.kind === "limited" && result.scope === "origin") {
40335
- result = snapshotFor(candidateSlug, socketOrigins.get(candidate));
40336
- if (!(result.kind === "limited" && result.scope === "origin")) {
40337
- batchResults.set(candidateSlug, result);
40338
- }
40339
- }
40340
- pushSnapshotResult(candidate, candidateSlug, result);
40341
- }
40342
- }
40343
- });
40344
- subscriptionFlushImmediate.unref?.();
41676
+ if (subscriptionFlushArmed) return;
41677
+ subscriptionFlushArmed = true;
41678
+ const cancel = scheduleSubscriptionFlush(flushSubscriptionPushes);
41679
+ if (subscriptionFlushArmed) cancelSubscriptionFlush = cancel ?? null;
40345
41680
  }
40346
41681
  function cancelPendingSubscriptionPush(ws, slug) {
40347
41682
  if (slug === void 0) {
@@ -40643,8 +41978,12 @@ function createAppSocketServer(deps) {
40643
41978
  offTranscript();
40644
41979
  } catch {
40645
41980
  }
40646
- if (subscriptionFlushImmediate) clearImmediate(subscriptionFlushImmediate);
40647
- subscriptionFlushImmediate = null;
41981
+ try {
41982
+ cancelSubscriptionFlush?.();
41983
+ } catch {
41984
+ }
41985
+ cancelSubscriptionFlush = null;
41986
+ subscriptionFlushArmed = false;
40648
41987
  pendingSubscriptionPushes.clear();
40649
41988
  if (transcriptRefreshTimer) clearTimeout(transcriptRefreshTimer);
40650
41989
  transcriptRefreshTimer = null;