frizz 0.1.0 → 0.3.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 (115) hide show
  1. package/README.md +119 -61
  2. package/dist/claude-agent-broker.js +63 -23
  3. package/dist/dev-child.js +6492 -3945
  4. package/dist/frizz.js +745 -495
  5. package/package.json +6 -2
  6. package/runtime/cc-worker/.claude-plugin/plugin.json +1 -1
  7. package/runtime/cc-worker/DECISIONS.md +64 -4
  8. package/runtime/cc-worker/bin/frizz-mcp.mjs +494 -57
  9. package/runtime/cc-worker/hooks/agent-dispatch.mjs +1 -1
  10. package/runtime/cc-worker/hooks/deny-plan.mjs +1 -1
  11. package/runtime/cc-worker/hooks/hooks.json +0 -7
  12. package/runtime/cc-worker/hooks/precompact-instructions.mjs +39 -28
  13. package/runtime/cc-worker/hooks/scratchpad.mjs +142 -169
  14. package/runtime/cc-worker/hooks/session-seed.mjs +16 -15
  15. package/runtime/cc-worker/skills/waits/SKILL.md +2 -2
  16. package/web-dist/apple-touch-icon.png +0 -0
  17. package/web-dist/assets/{TerminalPane-ROKHp1ib.js → TerminalPane-CTdetDJJ.js} +2 -2
  18. package/web-dist/assets/{abnfDiagram-VRR7QNED-DcpdhBs3.js → abnfDiagram-VRR7QNED-BDKrjCMs.js} +1 -1
  19. package/web-dist/assets/architecture-TIHT7OUA-ChUMo004.js +1 -0
  20. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CUAKf0mn.js → architectureDiagram-ZJ3FMSHR-BJebpUUM.js} +1 -1
  21. package/web-dist/assets/{blockDiagram-677ZJIJ3-BPwpJIzx.js → blockDiagram-677ZJIJ3-BNbuk25k.js} +1 -1
  22. package/web-dist/assets/{c4Diagram-LMCZKHZV-1lptuHzZ.js → c4Diagram-LMCZKHZV-lCfyotdU.js} +1 -1
  23. package/web-dist/assets/channel-pr7r6raB.js +1 -0
  24. package/web-dist/assets/{chunk-32BRIVSS-CFR9AKjY.js → chunk-32BRIVSS-DP72SEkr.js} +1 -1
  25. package/web-dist/assets/{chunk-52WLFC77-CM9uct7m.js → chunk-52WLFC77-DxO3gN-p.js} +1 -1
  26. package/web-dist/assets/{chunk-C7G6YPKG-DiveJARw.js → chunk-C7G6YPKG-BXFx9Vlr.js} +1 -1
  27. package/web-dist/assets/{chunk-EX3LRPZG-BE1CBw8F.js → chunk-EX3LRPZG-CR3sHpPf.js} +1 -1
  28. package/web-dist/assets/{chunk-FWX5IMBZ-DL42uXiO.js → chunk-FWX5IMBZ-CJ8L__oG.js} +2 -2
  29. package/web-dist/assets/{chunk-HOUHSVGY-DPhJWgDw.js → chunk-HOUHSVGY-BCl3JSWr.js} +1 -1
  30. package/web-dist/assets/{chunk-ICXQ74PX-CwYy-6AP.js → chunk-ICXQ74PX-CZxFMci1.js} +1 -1
  31. package/web-dist/assets/{chunk-MOJQB5TN-Ds5I9wxq.js → chunk-MOJQB5TN-Ck3_47dB.js} +1 -1
  32. package/web-dist/assets/{chunk-OGEWGWER-DHiZJwQD.js → chunk-OGEWGWER-CVXjES-F.js} +1 -1
  33. package/web-dist/assets/{chunk-PUDLZKDR-B-eyQTsF.js → chunk-PUDLZKDR-JjrVgaR7.js} +1 -1
  34. package/web-dist/assets/{chunk-Q4XR5HBZ-DK7dB3Ti.js → chunk-Q4XR5HBZ-C5YMRAQ0.js} +1 -1
  35. package/web-dist/assets/{chunk-V7JOEXUC-Dn59m74L.js → chunk-V7JOEXUC-DK0mfUHU.js} +1 -1
  36. package/web-dist/assets/{chunk-VAUOI2AC-DK7x36hd.js → chunk-VAUOI2AC-BKBntpid.js} +1 -1
  37. package/web-dist/assets/{chunk-VR4S4FIN-D7-CI3Yl.js → chunk-VR4S4FIN-zgE1dG6U.js} +1 -1
  38. package/web-dist/assets/{chunk-WYO6CB5R-B3l-mLCs.js → chunk-WYO6CB5R-CZhh1IBq.js} +1 -1
  39. package/web-dist/assets/{chunk-ZGVPDNZ5-Dobxlxie.js → chunk-ZGVPDNZ5-CrNjIEem.js} +1 -1
  40. package/web-dist/assets/classDiagram-OUVF2IWQ-CvGbPMn_.js +1 -0
  41. package/web-dist/assets/classDiagram-v2-EOCWNBFH-CvGbPMn_.js +1 -0
  42. package/web-dist/assets/{cynefin-VYW2F7L2-Dh7RuEUJ.js → cynefin-VYW2F7L2-Ca_BPfTG.js} +1 -1
  43. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-ClPi2mZZ.js → cynefinDiagram-TSTJHNR4-CsINQf6g.js} +1 -1
  44. package/web-dist/assets/{dagre-VKFMJZFB-52_WP1QV.js → dagre-VKFMJZFB-CE0DYMFy.js} +1 -1
  45. package/web-dist/assets/{diagram-FQU43EPY-D_1zVsTL.js → diagram-FQU43EPY-BqTduEYE.js} +1 -1
  46. package/web-dist/assets/{diagram-G47NLZAW-CdZxuGUy.js → diagram-G47NLZAW-Cho718s6.js} +1 -1
  47. package/web-dist/assets/{diagram-NH7WQ7WH-C8pSFu0P.js → diagram-NH7WQ7WH-D0Z8t9UC.js} +1 -1
  48. package/web-dist/assets/{diagram-OA4YK3LP-C5bjZLre.js → diagram-OA4YK3LP-CBacyeDx.js} +1 -1
  49. package/web-dist/assets/{diagram-WEI45ONY-Bxzhiuzn.js → diagram-WEI45ONY-CozpXa4j.js} +1 -1
  50. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-g-Z0J2wP.js → ebnfDiagram-CCIWWBDH-ba4NbQQo.js} +1 -1
  51. package/web-dist/assets/{erDiagram-Q63AITRT-DVNkgIHp.js → erDiagram-Q63AITRT-B3BciOYa.js} +1 -1
  52. package/web-dist/assets/eventmodeling-45OFAUF4-DKmyo-jd.js +1 -0
  53. package/web-dist/assets/flowDiagram-23GEKE2U-DluCBvT4.js +1 -0
  54. package/web-dist/assets/{ganttDiagram-NO4QXBWP-_71pQYEK.js → ganttDiagram-NO4QXBWP-lPmrRit4.js} +1 -1
  55. package/web-dist/assets/{gitGraph-TEB2WS4Q-ChIZiGZS.js → gitGraph-TEB2WS4Q-BjyclOq0.js} +1 -1
  56. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-DCHAFI0l.js → gitGraphDiagram-IHSO6WYX-pzeb4Yrw.js} +1 -1
  57. package/web-dist/assets/index-BQtjYMpV.css +1 -0
  58. package/web-dist/assets/index-CLW1Q49U.js +360 -0
  59. package/web-dist/assets/{info-DKCQHKI2-BW-n_T1j.js → info-DKCQHKI2-Bwycegvf.js} +1 -1
  60. package/web-dist/assets/{infoDiagram-FWYZ7A6U-CgDYsKi9.js → infoDiagram-FWYZ7A6U-BCvRGj_5.js} +1 -1
  61. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-ClzGNt9N.js → ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js} +1 -1
  62. package/web-dist/assets/{journeyDiagram-5HDEW3XC-DSCQxkHC.js → journeyDiagram-5HDEW3XC-C5ROwFio.js} +1 -1
  63. package/web-dist/assets/{kanban-definition-HUTT4EX6-CdrdX9N8.js → kanban-definition-HUTT4EX6-YLPLkpeT.js} +1 -1
  64. package/web-dist/assets/{line-ha38Dc-1.js → line-KtkNqRgI.js} +1 -1
  65. package/web-dist/assets/{mermaid-parser.core-Z4uMcpip.js → mermaid-parser.core-D_FfqBe7.js} +3 -3
  66. package/web-dist/assets/{mermaid.core-iZRq3hbu.js → mermaid.core-Ffv8anVf.js} +3 -3
  67. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DmhInJO_.js → mindmap-definition-LN4V7U3C-B2jj4vfm.js} +1 -1
  68. package/web-dist/assets/{packet-7NZHBO7P-DBPB36Kl.js → packet-7NZHBO7P-BjmWHwra.js} +1 -1
  69. package/web-dist/assets/{pegDiagram-2B236MQR-CAH3ljfj.js → pegDiagram-2B236MQR-Dq3iJDyq.js} +1 -1
  70. package/web-dist/assets/{pie-RZYD4A2V-_h_eX4Ca.js → pie-RZYD4A2V-jBbH1lv9.js} +1 -1
  71. package/web-dist/assets/{pieDiagram-ENE6RG2P-DFBPus8j.js → pieDiagram-ENE6RG2P-C3ETW9lq.js} +1 -1
  72. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DMvOCjt8.js → quadrantDiagram-ABIIQ3AL-DItSmme7.js} +1 -1
  73. package/web-dist/assets/{radar-I7S5WNFK-2EzoPHEZ.js → radar-I7S5WNFK-6ey6crgP.js} +1 -1
  74. package/web-dist/assets/{railroad-3IZDKUUU-BPJnn-hm.js → railroad-3IZDKUUU-Cii-Mn0E.js} +1 -1
  75. package/web-dist/assets/railroad-abnf-AHOZXSZD-U_vb4BrX.js +1 -0
  76. package/web-dist/assets/railroad-ebnf-EBAXGLYW-BIHG7gNU.js +1 -0
  77. package/web-dist/assets/railroad-peg-LSFZ7HO6-Cpd9r-tB.js +1 -0
  78. package/web-dist/assets/{railroadDiagram-RFXS5EU6-DKq5FagA.js → railroadDiagram-RFXS5EU6-M363ils_.js} +1 -1
  79. package/web-dist/assets/{requirementDiagram-TGXJPOKE-BJ5tGazp.js → requirementDiagram-TGXJPOKE-CTs2_V6T.js} +1 -1
  80. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-XSJjcBhX.js → sankeyDiagram-HTMAVEWB-QTLLcDPD.js} +1 -1
  81. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-CBb8emSe.js → sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js} +1 -1
  82. package/web-dist/assets/{stateDiagram-2N3HPSRC-DDfRW94V.js → stateDiagram-2N3HPSRC-BecB6roG.js} +1 -1
  83. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-JXw9T96l.js +1 -0
  84. package/web-dist/assets/{swimlanes-5IMT3BWC-DvRYbkZi.js → swimlanes-5IMT3BWC-COiYgS0w.js} +1 -1
  85. package/web-dist/assets/swimlanesDiagram-G3AALYLV--1Wv7FQQ.js +8 -0
  86. package/web-dist/assets/{timeline-definition-FHXFAJF6-BNUa_DwI.js → timeline-definition-FHXFAJF6-Ci22coeH.js} +1 -1
  87. package/web-dist/assets/{treeView-QDETBFTQ-I6-IW6nJ.js → treeView-QDETBFTQ-4DW35czh.js} +1 -1
  88. package/web-dist/assets/{treemap-6X3UGDF4-CWWmEUYJ.js → treemap-6X3UGDF4-Dq_-Z6-Y.js} +1 -1
  89. package/web-dist/assets/{vennDiagram-L72KCM5P-DTDrPGLk.js → vennDiagram-L72KCM5P-CHQSIEYq.js} +1 -1
  90. package/web-dist/assets/{wardley-OPB4EBWU-CNsdgXXA.js → wardley-OPB4EBWU-Bwlh7HCY.js} +1 -1
  91. package/web-dist/assets/{wardleyDiagram-EHGQE667-YE0tq3Kh.js → wardleyDiagram-EHGQE667-DctjuPYn.js} +1 -1
  92. package/web-dist/assets/{xychartDiagram-FW5EYKEG-D0ofMX8C.js → xychartDiagram-FW5EYKEG-ZoIGosv8.js} +1 -1
  93. package/web-dist/favicon-16.png +0 -0
  94. package/web-dist/favicon-32.png +0 -0
  95. package/web-dist/favicon.svg +17 -61
  96. package/web-dist/icon-192.png +0 -0
  97. package/web-dist/icon-512.png +0 -0
  98. package/web-dist/icon-maskable-512.png +0 -0
  99. package/web-dist/index.html +19 -8
  100. package/web-dist/manifest.webmanifest +3 -3
  101. package/runtime/cc-worker/hooks/scratchpad-stop.mjs +0 -125
  102. package/runtime/cc-worker/skills/handoff/SKILL.md +0 -209
  103. package/web-dist/assets/architecture-TIHT7OUA-CAviNivx.js +0 -1
  104. package/web-dist/assets/channel-CqKDIFQF.js +0 -1
  105. package/web-dist/assets/classDiagram-OUVF2IWQ-B_-6iXYY.js +0 -1
  106. package/web-dist/assets/classDiagram-v2-EOCWNBFH-B_-6iXYY.js +0 -1
  107. package/web-dist/assets/eventmodeling-45OFAUF4-MpmeH5YZ.js +0 -1
  108. package/web-dist/assets/flowDiagram-23GEKE2U-D-QgjjhF.js +0 -1
  109. package/web-dist/assets/index-w4v-GZEc.js +0 -358
  110. package/web-dist/assets/index-zyi22LPz.css +0 -1
  111. package/web-dist/assets/railroad-abnf-AHOZXSZD-YeUoiySk.js +0 -1
  112. package/web-dist/assets/railroad-ebnf-EBAXGLYW-Ddw1SuGG.js +0 -1
  113. package/web-dist/assets/railroad-peg-LSFZ7HO6-Dd8BOGeW.js +0 -1
  114. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-hc41W5Lx.js +0 -1
  115. package/web-dist/assets/swimlanesDiagram-G3AALYLV-BZyGdgSG.js +0 -8
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <h1 align="center">🧵<br/>Frizz</h1>
2
+ <h1 align="center"><img src="assets/logo-concepts/final/fff-tile.png" alt="" width="104" height="104"><br/>Frizz</h1>
3
3
  <p align="center">A local web UI for running many coding agents at once.
4
4
  <br/>
5
5
  by <a href="https://x.com/colinhacks">@colinhacks</a>
@@ -13,61 +13,78 @@
13
13
  <a href="https://github.com/colinhacks/frizz" rel="nofollow"><img src="https://img.shields.io/github/stars/colinhacks/frizz" alt="stars"></a>
14
14
  </p>
15
15
 
16
+ <br/>
17
+
16
18
  Frizz is for you if you have any of these opinions:
17
19
 
18
- - **Terminal UIs are dated** and have fundamental limitations that are incompatible with good user experience.
19
- - **Orchestrator-style apps** feel overly complex.
20
- - **I'm tired of constantly switching between sessions** to check in on my agents' progress.
20
+ - Terminal UIs are dated and have fundamental limitations that are incompatible with good user experience.
21
+ - Orchestrator-style apps like Conductor feel overly complex.
22
+ - It's annoying to constantly switch between sessions to check in on my agents' progress.
23
+
24
+ <br/>
21
25
 
22
26
  <h2 align="center">Getting started</h2>
23
27
 
24
- 1. Node 22.13+, or 23.4+ on the Node 23 line
25
- 2. Git
26
- 3. The [Claude Code](https://claude.com/claude-code) or [Codex](https://developers.openai.com/codex) CLI, signed in
28
+ **Requirements.** Node 22.13+, and the [Claude Code](https://claude.com/claude-code) or [Codex](https://developers.openai.com/codex) CLI installed and signed in — Frizz drives the subscription you already pay for.
27
29
 
28
- Then run it inside any Git repo.
30
+ Then run it in any directory — a repo, a jj checkout, or a folder of scripts. Frizz has no opinion about version control and does not require Git.
29
31
 
30
32
  ```sh
31
- $ cd ~/taskly
33
+ $ cd path/to/acme
32
34
  $ npx frizz
33
35
 
34
- FRIZZ v0.1.0 ready in 4.0s
36
+ FRIZZ v0.3.0 ready in 4.0s
35
37
 
36
- ➜ Local: http://127.0.0.1:4922/
37
- ➜ Project: taskly~/taskly
38
- ➜ Logs: ~/.frizz/projects/979dae3c-fe15-4038-817e-11d0e7491959/logs/frizz-2026-08-01T13-44-43-16931.log
38
+ ➜ Local: http://127.0.0.1:9393/project/acme/
39
+ ➜ Project: acmepath/to/acme
40
+ ➜ Logs: ~/Library/Application Support/Frizz/projects/979dae3c-fe15-4038-817e-11d0e7491959/logs/frizz-2026-08-01T13-44-43-16931.log
39
41
 
40
42
  press ctrl-c to stop · run with --debug for the full event feed
41
43
  ```
42
44
 
43
- A browser tab opens on that address — a dedicated workspace for this repo. **One tab per repo!** Runs on macOS, Linux, and Windows.
45
+ A browser tab opens on localhost — a dedicated workspace for this directory. **One tab per project!** One Frizz serves every project on the machine, each at its own `/project/<name>` URL, so running it in a second directory joins the server already running rather than starting another. Runs on macOS, Linux, and Windows.
44
46
 
45
47
  <p align="center">
46
48
  <img src="assets/board.png" alt="Frizz running in a browser tab at 127.0.0.1:4921: a sidebar of threads on the left, and on the right a card where an agent is asking an answerable question with lettered options, above Snooze and Mark as done." width="100%">
47
49
  </p>
48
50
 
49
- <h2 align="center">A queue, not a sidebar</h2>
50
-
51
- A sidebar of sessions makes every agent something you have to remember to go check. Frizz gives you one queue instead.
52
-
53
- When an agent comes to rest needing you, a card is added to it. You can quickly evaluate what it has done since your last message and decide to answer its questions, steer it, snooze the card, or mark the session complete. You're continuously presented with a set of action items in one place, instead of constantly switching back and forth between sessions.
54
-
55
- The queue is strict about what earns a card, which is what keeps it a real todo list. A thread resting only because *its own* helpers are still working isn't waiting on you, so it stays quiet until they're back. Nothing shows up just to be dismissed.
51
+ <br/>
56
52
 
57
53
  <h2 align="center">Features</h2>
58
54
 
59
55
  Frizz is a browser tab, a queue, and the agent CLIs you already pay for. It brings no model of its own, automates none of your workflow, and keeps every opinion it does have in a text file you can edit.
60
56
 
61
- - 🗂️ **A unified queue, not a tab strip.** Every agent that comes to rest needing you becomes a card. Work the queue top to bottom instead of polling ten terminals.
62
- - 🔌 **Headless.** Every thread's agent runs in its own detached background process no terminal to babysit, no window that has to stay open for work to continue.
63
- - 🔁 **Resumable and quittable.** Close the tab, quit the browser, ctrl-c the server, reboot. Your threads are all still there when you come back, and Frizz reconnects to the ones still running rather than replaying them from disk.
64
- - 🤖 **Claude Code *and* Codex.** Pick the backend per thread and run both against the same repo at once. Frizz drives the CLIs you already have installed and signed in.
57
+ - 🗂️ **A task queue, not a sidebar.** Every agent that comes to rest needing you becomes a card. Work the queue top to bottom instead of polling ten terminals.
58
+ - 🔌 **Headless.** Every thread's agent runs in its own detached background process. Close the tab, quit the browser, ctrl-c the server, reboot your threads are all still there when you come back, and Frizz reconnects to the ones still running rather than replaying them from disk.
59
+ - 🤖 **Claude Code *and* Codex.** Pick the backend per thread and run both against the same repo at once. Frizz supports Claude Code and Codex subscriptions the CLIs you already have installed and signed in.
65
60
  - 😴 **Snooze.** Not everything needs an answer now. Park a card for an hour, until tomorrow morning, or until a date you pick — optionally with a follow-up prompt attached, so the thread wakes up already working on what you told it to do next.
66
- - 🔄 **Recurring prompts.** Give a thread a prompt that repeats — every time it comes to rest, on a clock you set in minutes, or both. Good for "keep going until CI is green" without you re-asking. A scheduled one reaches the agent even mid-turn, so it can nudge a thread that never stops. Switch it off whenever, or let the agent say it's finished.
61
+ - 🔄 **Heartbeats.** Give a thread a prompt that repeats — every time it comes to rest, on a clock you set in minutes, or both. Good for "keep going until CI is green" without you re-asking. A scheduled one reaches the agent even mid-turn, so it can nudge a thread that never stops. Switch it off whenever, or let the agent say it's finished.
67
62
  - 🐙 **GitHub integration.** Browse your repo's issues and pull requests without leaving the composer, and turn a selection of them into threads. Workers can read issues, diffs, and CI on their own.
68
63
  - 👀 **Built-in CI and PR watchers.** A worker waiting on a build or a review doesn't hand the thread back to you to be told "keep going." It watches, and picks the work back up when the run goes green or a review lands.
69
64
  - 📝 **No magic.** A thread behaves like a Claude Code session you started yourself. Frizz adds no worktrees, no branches, no dev server, no build integration, no workflow engine to fight with.
70
- - 🔒 **Local only.** No cloud, no account, no telemetry. The server binds `127.0.0.1` by default and its state lives in `~/.frizz/`, never in your checkout.
65
+ - 🔒 **Local only.** No cloud, no account, no telemetry. The server binds `127.0.0.1` by default and its state lives in your user directory, never in your checkout.
66
+
67
+ ### The queue
68
+
69
+ A sidebar of sessions makes every agent something you have to remember to go check. Frizz gives you one queue instead.
70
+
71
+ When an agent comes to rest needing you, a card is added to it. You can quickly evaluate what it has done since your last message and decide to answer its questions, steer it, snooze the card, or mark the session complete. You're continuously presented with a set of action items in one place, instead of constantly switching back and forth between sessions.
72
+
73
+ The queue is strict about what earns a card, which is what keeps it a real todo list. A thread resting only because *its own* helpers are still working isn't waiting on you, so it stays quiet until they're back. Nothing shows up just to be dismissed.
74
+
75
+ **Threads are built to run without you.** A worker keeps going until it reaches something only you can settle — a product call, a fork where guessing wrong is expensive to undo, an irreversible action — and then it hands back an answerable *question* rather than a wall of text for you to re-read and interpret.
76
+
77
+ <p align="center">
78
+ <img src="assets/question.png" alt="A question card titled Question: 'Should the settings store use SQLite or a JSON file?' with two lettered options, A tagged RECOMMENDED, and a third row for typing something else." width="100%">
79
+ </p>
80
+
81
+ Options are lettered and answered in one click, and a worker marks its own recommendation when it has one — so the common case is a single keystroke. There is always a row for writing something else instead.
82
+
83
+ When the answer isn't one thing, the same card takes several: check any combination and add a note.
84
+
85
+ <p align="center">
86
+ <img src="assets/question-multi.png" alt="A question card titled Select multiple: 'Which of these findings should I fix in this pass?' with three checkbox options, the first two ticked, and a field for adding a note." width="100%">
87
+ </p>
71
88
 
72
89
  ### GitHub
73
90
 
@@ -87,7 +104,7 @@ Park a card for an hour, until tomorrow morning, or until a date you pick. Attac
87
104
  <img src="assets/snooze.png" alt="The snooze menu open on a queue card, offering 1 hour, tomorrow at 9am, 1 day, 3 days, 1 week, and a custom time and prompt." width="100%">
88
105
  </p>
89
106
 
90
- ### Recurring prompts
107
+ ### Heartbeats
91
108
 
92
109
  Give a thread a prompt that repeats. Send it every time the agent comes to rest, on a clock you set in minutes, or both — a scheduled send reaches the agent even mid-turn, without cutting off work in progress.
93
110
 
@@ -95,6 +112,8 @@ Give a thread a prompt that repeats. Send it every time the agent comes to rest,
95
112
  <img src="assets/recurring.png" alt="The recurring prompt panel: one prompt saying to keep going until the test suite is green, with both triggers switched on — every time it stops, and every 30 minutes." width="100%">
96
113
  </p>
97
114
 
115
+ <br/>
116
+
98
117
  <h2 align="center">CLI</h2>
99
118
 
100
119
  ```sh
@@ -131,67 +150,101 @@ at all — Frizz stays on loopback and the tunnel dials it. Frizz still has no l
131
150
  authentication at the proxy: with Cloudflare Access, that is the whole of your access control.
132
151
  ```
133
152
 
153
+ <br/>
154
+
134
155
  <h2 align="center">FAQ</h2>
135
156
 
136
- **Does Frizz run its own agent or model?**
157
+ <details>
158
+ <summary><b>Does Frizz run its own agent or model?</b></summary>
137
159
 
138
- No. It drives the Claude Code or Codex CLI already installed and signed in on your machine. Your subscription, your rate limits, your settings.
160
+ > No. It drives the Claude Code or Codex CLI already installed and signed in on your machine. Your subscription, your rate limits, your settings.
139
161
 
140
- **Does anything leave my machine?**
162
+ </details>
141
163
 
142
- Nothing from Frizz. There's no account, no telemetry, and the server binds to `127.0.0.1` unless you ask for otherwise with `--host`. The agents themselves talk to their providers, and `gh` talks to GitHub, but Frizz is a local process looking at local files.
164
+ <details>
165
+ <summary><b>Does anything leave my machine?</b></summary>
143
166
 
144
- **What happens if I close the tab?**
167
+ > Nothing from Frizz. There's no account, no telemetry, and the server binds to `127.0.0.1` unless you ask for otherwise with `--host`. The agents themselves talk to their providers, and `gh` talks to GitHub, but Frizz is a local process looking at local files.
145
168
 
146
- Nothing. Each thread's agent runs in its own detached background process, independent of the browser *and* of Frizz itself — you can stop Frizz entirely and your agents keep working. Relaunch, and it reconnects to the sessions that are still running.
169
+ </details>
147
170
 
148
- **Does it put junk in my repo?**
171
+ <details>
172
+ <summary><b>What happens if I close the tab?</b></summary>
149
173
 
150
- Barely. Dispatching a thread writes no thread file into your repo the agent session *is* the thread. All Frizz adds to your working tree is a `.frizz/` directory holding a scratchpad per thread plus a couple of tiny hook state files. Everything durable lives outside your checkout in `~/.frizz/projects/<id>/`, so you can delete `.frizz/` and keep every thread and setting. Frizz does not touch your `.gitignore`, so add `.frizz/` yourself if you don't want it in `git status`.
174
+ > Nothing. Each thread's agent runs in its own detached background process, independent of the browser *and* of Frizz itself you can stop Frizz entirely and your agents keep working. Relaunch, and it reconnects to the sessions that are still running.
151
175
 
152
- **Do I have to use worktrees?**
176
+ </details>
153
177
 
154
- No. Frizz doesn't own your git workflow and won't create branches or worktrees behind your back. Tell your agents what you want in `FRIZZ.md`. If you do run Frizz inside a linked worktree, it isolates that worktree's state from its siblings automatically.
178
+ <details>
179
+ <summary><b>Does it put junk in my repo?</b></summary>
155
180
 
156
- **Can I run it on several repos at once?**
181
+ > Barely. Dispatching a thread writes no thread file into your repo — the agent session *is* the thread. All Frizz adds to your working tree is a `.frizz/` directory holding a scratch directory per thread (empty unless the agent writes something in it) plus a couple of tiny hook state files. Everything durable lives outside your checkout, under `~/.frizz/` if you already have one and otherwise in your platform's own data directory (`~/Library/Application Support/Frizz` on macOS, `$XDG_DATA_HOME/frizz` on Linux, LocalAppData on Windows), so you can delete `.frizz/` and keep every thread and setting. Frizz does not touch your `.gitignore`, so add `.frizz/` yourself if you don't want it in `git status`.
157
182
 
158
- Yes — one server and one tab per repo, each fully isolated. There is deliberately no cross-repo board.
183
+ </details>
159
184
 
160
- **Can I reach it from another machine?**
185
+ <details>
186
+ <summary><b>Do I have to use worktrees?</b></summary>
161
187
 
162
- Yes, with `--host` the flag every dev server has, and it means the same thing here:
188
+ > No. Frizz doesn't own your git workflow and won't create branches or worktrees behind your back. Tell your agents what you want in `FRIZZ.md`. If you do run Frizz inside a linked worktree, it isolates that worktree's state from its siblings automatically.
163
189
 
164
- ```sh
165
- npx frizz --host # every interface, i.e. 0.0.0.0
166
- npx frizz --host 192.168.1.5 # one interface
167
- ```
190
+ </details>
168
191
 
169
- Frizz prints the addresses to use and warns you as it starts. Reaching it by IP works as-is; reach it by name and you have to say so — `--host --allowed-host frizz.local` — because an unlisted name is how DNS rebinding gets a browser to treat an attacker's page as same-origin with your board. `FRIZZ_HOST` and `FRIZZ_ALLOWED_HOSTS` do the same thing when the launch command lives in an image or a unit file.
192
+ <details>
193
+ <summary><b>Can I run it on several repos at once?</b></summary>
170
194
 
171
- Understand what you're turning on. Frizz has no login: reaching the port *is* the authorization, and the board runs shell commands as you. Only do this on a network you trust, and prefer a tunnel (`ssh -L 4922:127.0.0.1:4922 you@box`, using the port Frizz printed, the same on both ends) if you just want your own board from your own laptop — that needs no flag at all.
195
+ > Yes one server and one tab per repo, each fully isolated. There is deliberately no cross-repo board.
172
196
 
173
- **Can I reach it from anywhere, not just my LAN?**
197
+ </details>
174
198
 
175
- Yes — put it behind a tunnel and tell Frizz the address the tunnel answers on, with `--public-origin`:
199
+ <details>
200
+ <summary><b>Can I reach it from another machine?</b></summary>
176
201
 
177
- ```sh
178
- npx frizz --public-origin https://frizz.example.com
179
- cloudflared tunnel --url http://127.0.0.1:4922 # the port Frizz printed
180
- ```
202
+ > Yes, with `--host` — the flag every dev server has, and it means the same thing here:
203
+ >
204
+ > ```sh
205
+ > npx frizz --host # every interface, i.e. 0.0.0.0
206
+ > npx frizz --host 192.168.1.5 # one interface
207
+ > ```
208
+ >
209
+ > Frizz prints the addresses to use and warns you as it starts. Reaching it by IP works as-is; reach it by name and you have to say so — `--host --allowed-host frizz.local` — because an unlisted name is how DNS rebinding gets a browser to treat an attacker's page as same-origin with your board. `FRIZZ_HOST` and `FRIZZ_ALLOWED_HOSTS` do the same thing when the launch command lives in an image or a unit file.
210
+ >
211
+ > Understand what you're turning on. Frizz has no login: reaching the port *is* the authorization, and the board runs shell commands as you. Only do this on a network you trust, and prefer a tunnel (`ssh -L 9393:127.0.0.1:9393 you@box`, using the port Frizz printed, the same on both ends) if you just want your own board from your own laptop — that needs no flag at all.
212
+
213
+ </details>
181
214
 
182
- Frizz stays bound to `127.0.0.1` — `--public-origin` is not `--host` and does not put anything on your LAN. The tunnel runs on the same machine and dials the loopback port, so the only way in is through the tunnel. That is also what makes this the *good* remote option rather than merely a working one: the tunnel terminates TLS, so the board is a real `https://` origin and therefore a secure context, which plain `--host` over a LAN IP is not. Copy buttons and desktop notifications work again, and it works on a phone.
215
+ <details>
216
+ <summary><b>Can I reach it from anywhere, not just my LAN?</b></summary>
183
217
 
184
- The address you pass must be the exact origin your browser shows — scheme and host, no path. Frizz accepts that one origin, and accepts `X-Forwarded-*` only on requests that actually arrived as it.
218
+ > Yes put it behind a tunnel and tell Frizz the address the tunnel answers on, with `--public-origin`:
219
+ >
220
+ > ```sh
221
+ > npx frizz --public-origin https://frizz.example.com
222
+ > cloudflared tunnel --url http://127.0.0.1:9393 # the port Frizz printed
223
+ > ```
224
+ >
225
+ > Frizz stays bound to `127.0.0.1` — `--public-origin` is not `--host` and does not put anything on your LAN. The tunnel runs on the same machine and dials the loopback port, so the only way in is through the tunnel. That is also what makes this the *good* remote option rather than merely a working one: the tunnel terminates TLS, so the board is a real `https://` origin and therefore a secure context, which plain `--host` over a LAN IP is not. Copy buttons and desktop notifications work again, and it works on a phone.
226
+ >
227
+ > The address you pass must be the exact origin your browser shows — scheme and host, no path. Frizz accepts that one origin, and accepts `X-Forwarded-*` only on requests that actually arrived as it.
228
+ >
229
+ > **This is the part that matters: Frizz has no login, so whatever you put in front of the tunnel *is* your access control.** A bare tunnel publishes a shell-capable board to the open internet for anyone who has the URL. Require authentication at the proxy — with Cloudflare, that means a [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) application over the hostname, with a policy allowing only your own email, created *before* the hostname resolves. Tailscale Serve is the same idea with device identity instead of SSO. Frizz prints this warning on every launch that names a public origin, and it is not boilerplate.
185
230
 
186
- **This is the part that matters: Frizz has no login, so whatever you put in front of the tunnel *is* your access control.** A bare tunnel publishes a shell-capable board to the open internet for anyone who has the URL. Require authentication at the proxy — with Cloudflare, that means a [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) application over the hostname, with a policy allowing only your own email, created *before* the hostname resolves. Tailscale Serve is the same idea with device identity instead of SSO. Frizz prints this warning on every launch that names a public origin, and it is not boilerplate.
231
+ </details>
187
232
 
188
- **What platforms does it run on?**
233
+ <details>
234
+ <summary><b>What platforms does it run on?</b></summary>
189
235
 
190
- macOS, Linux, and Windows. Windows support landed once the last dependency that had no native Windows build was removed.
236
+ > macOS, Linux, and Windows. Windows support landed once the last dependency that had no native Windows build was removed.
191
237
 
192
- **How is this different from the other orchestrator apps?**
238
+ </details>
193
239
 
194
- Those apps wrap your agents in their own workflow. Frizz doesn't: it's a viewer and a queue over the CLIs you already run, with every piece of orchestration judgment sitting in editable text instead of inside the binary.
240
+ <details>
241
+ <summary><b>How is this different from the other orchestrator apps?</b></summary>
242
+
243
+ > Those apps wrap your agents in their own workflow. Frizz doesn't: it's a viewer and a queue over the CLIs you already run, with every piece of orchestration judgment sitting in editable text instead of inside the binary.
244
+
245
+ </details>
246
+
247
+ <br/>
195
248
 
196
249
  <h2 align="center">Glossary</h2>
197
250
 
@@ -205,14 +258,19 @@ Frizz has its own small vocabulary. Most of it names a feature, so this doubles
205
258
  | **Rested** | An agent that has ended its turn and is waiting on a human. A rested thread isn't idle, it's *your move*. |
206
259
  | **The queue** | The single list of threads that need you. A thread only earns a card when it genuinely wants a human. |
207
260
  | **Snooze** | Hide a card until later — an hour, tomorrow morning, or a date you pick — optionally with a follow-up prompt attached. |
261
+ | **Heartbeat** | A prompt that repeats on its own — every time a thread rests, on a clock, or both — until you switch it off or the agent says it's done. |
208
262
  | **Scratchpad** | A thread's durable working memory, readable under its **Doc** tab. Where a worker keeps what a summary would otherwise lose: the approach, the alternatives it rejected, the decisions you made and reversed. |
209
263
  | **`FRIZZ.md`** | An optional file at your repo root whose contents are injected into every thread, for when you want agents to follow your repo's own norms. |
210
264
 
265
+ <br/>
266
+
211
267
  <h2 align="center">Docs</h2>
212
268
 
213
269
  - [`ARCHITECTURE.md`](ARCHITECTURE.md) — the invariants, layout, and design decisions. Read it before changing anything.
214
270
  - [`FRIZZ.md`](FRIZZ.md) — this repo's own worker norms, as a worked example of the optional per-repo prompt.
215
271
 
272
+ <br/>
273
+
216
274
  <h2 align="center">License</h2>
217
275
 
218
276
  MIT
@@ -19861,6 +19861,7 @@ var CLAUDE_BROKER_CAPABILITY_CANCEL_INPUT = "cancel-input-v1";
19861
19861
  var CLAUDE_BROKER_CAPABILITY_STOP_TASK = "stop-task-v1";
19862
19862
  var CLAUDE_BROKER_CAPABILITY_RELOAD_PLUGINS = "reload-plugins-v1";
19863
19863
  var CLAUDE_BROKER_CAPABILITY_RENAME = "rename-v1";
19864
+ var CLAUDE_INPUT_DROP_DIAGNOSTIC_PREFIX = "input dropped, the agent never received it";
19864
19865
  var ClaudeAgentSdkProtocolError = class extends Error {
19865
19866
  constructor(message) {
19866
19867
  super(message);
@@ -20114,6 +20115,7 @@ var MAX_PERMISSION_REQUESTS = 128;
20114
20115
  var MAX_ELICITATION_CALLBACKS = 128;
20115
20116
  var MAX_SESSION_TITLE_DESCRIPTION_BYTES = 64 * 1024;
20116
20117
  var MAX_SESSION_TITLE_BYTES = 2 * 1024;
20118
+ var UNECHOABLE_AFTER_RESULTS = 2;
20117
20119
  var NUB_NODE_SHIM_PATH_SEGMENT = /(?:^|[\\/])nub-node-shim-[^\\/]+$/;
20118
20120
  function createClaudeQueryFactory(options) {
20119
20121
  if (options.enabled !== true) {
@@ -20245,8 +20247,12 @@ var RealClaudeQueryHandle = class {
20245
20247
  input;
20246
20248
  diagnostic;
20247
20249
  lifecycleAbort;
20248
- outstandingInputs = /* @__PURE__ */ new Set();
20249
- outstandingInputOrder = [];
20250
+ // id the main-thread turn count at the moment it was sent. A Map rather than a Set plus a
20251
+ // parallel order array because insertion order IS send order, which is all the oldest-first
20252
+ // fallback release ever wanted, and the recorded turn count is what makes the bound reclaimable
20253
+ // (see pruneUnechoableInputs).
20254
+ outstandingInputs = /* @__PURE__ */ new Map();
20255
+ mainThreadResults = 0;
20250
20256
  providerProgressCovered = false;
20251
20257
  closePromise;
20252
20258
  constructor(sdkQuery, input, sessionId, lifecycleAbort, redactor, diagnostic) {
@@ -20272,11 +20278,11 @@ var RealClaudeQueryHandle = class {
20272
20278
  const parsed = validateInputMessage(message);
20273
20279
  if (!UUID_PATTERN.test(parsed.id)) throw new ClaudeAgentSdkProtocolError("input.id must be a UUID");
20274
20280
  if (this.outstandingInputs.has(parsed.id)) throw new ClaudeAgentSdkProtocolError("input UUID is already outstanding");
20281
+ if (this.outstandingInputs.size >= CLAUDE_AGENT_SDK_MAX_QUEUED_INPUTS) this.pruneUnechoableInputs();
20275
20282
  if (this.outstandingInputs.size >= CLAUDE_AGENT_SDK_MAX_QUEUED_INPUTS) {
20276
20283
  throw new ClaudeAgentSdkProtocolError("Claude outstanding input limit exceeded");
20277
20284
  }
20278
- this.outstandingInputs.add(parsed.id);
20279
- this.outstandingInputOrder.push(parsed.id);
20285
+ this.outstandingInputs.set(parsed.id, this.mainThreadResults);
20280
20286
  try {
20281
20287
  this.input.push({
20282
20288
  type: "user",
@@ -20288,8 +20294,6 @@ var RealClaudeQueryHandle = class {
20288
20294
  });
20289
20295
  } catch (error) {
20290
20296
  this.outstandingInputs.delete(parsed.id);
20291
- const orderIndex = this.outstandingInputOrder.indexOf(parsed.id);
20292
- if (orderIndex >= 0) this.outstandingInputOrder.splice(orderIndex, 1);
20293
20297
  throw error;
20294
20298
  }
20295
20299
  }
@@ -20383,11 +20387,7 @@ var RealClaudeQueryHandle = class {
20383
20387
  if (typeof answer !== "boolean") {
20384
20388
  throw new ClaudeAgentSdkProtocolError("Claude queued-input cancellation returned an unreadable answer");
20385
20389
  }
20386
- if (answer) {
20387
- this.outstandingInputs.delete(messageUuid);
20388
- const orderIndex = this.outstandingInputOrder.indexOf(messageUuid);
20389
- if (orderIndex >= 0) this.outstandingInputOrder.splice(orderIndex, 1);
20390
- }
20390
+ if (answer) this.outstandingInputs.delete(messageUuid);
20391
20391
  return answer;
20392
20392
  }
20393
20393
  async setPermissionMode(mode) {
@@ -20499,27 +20499,56 @@ var RealClaudeQueryHandle = class {
20499
20499
  }
20500
20500
  observeProviderProgress(event) {
20501
20501
  if (event.kind === "user" && !event.synthetic && event.toolResultIds.length === 0 && event.messageId && this.outstandingInputs.delete(event.messageId)) {
20502
- const orderIndex = this.outstandingInputOrder.indexOf(event.messageId);
20503
- if (orderIndex >= 0) this.outstandingInputOrder.splice(orderIndex, 1);
20504
20502
  this.providerProgressCovered = true;
20505
20503
  return;
20506
20504
  }
20507
20505
  const mainThreadProgress = event.kind === "assistant" && event.parentToolUseId === void 0 || event.kind === "result";
20508
20506
  if (mainThreadProgress) {
20509
20507
  if (!this.providerProgressCovered && this.releaseOldestOutstandingInput()) this.providerProgressCovered = true;
20510
- if (event.kind === "result") this.providerProgressCovered = false;
20508
+ if (event.kind === "result") {
20509
+ this.providerProgressCovered = false;
20510
+ this.mainThreadResults += 1;
20511
+ }
20511
20512
  }
20512
20513
  }
20513
20514
  releaseOldestOutstandingInput() {
20514
- while (this.outstandingInputOrder.length > 0) {
20515
- const id2 = this.outstandingInputOrder.shift();
20516
- if (this.outstandingInputs.delete(id2)) return true;
20517
- }
20518
- return false;
20515
+ const oldest = this.outstandingInputs.keys().next();
20516
+ if (oldest.done === true) return false;
20517
+ this.outstandingInputs.delete(oldest.value);
20518
+ return true;
20519
+ }
20520
+ /**
20521
+ * Reclaim the slots of inputs the provider can no longer echo.
20522
+ *
20523
+ * The CLI consumes a queued input at a TURN BOUNDARY: send it to an idle session and the echo comes
20524
+ * back on the turn that starts immediately; send it mid-turn and it is consumed when that turn ends.
20525
+ * So an input still unechoed after TWO main-thread `result` frames have passed under it was either
20526
+ * consumed without an echo frizz could match or lost outright — either way its echo is never coming,
20527
+ * and holding its slot for the life of the session is a pure leak. Measured on the session in the
20528
+ * `send` comment: 349 inputs accepted in one daemon generation, 36 of them never echoed.
20529
+ *
20530
+ * Deliberately NOT a "just evict the oldest to make room" eviction, which would also throw away the
20531
+ * genuinely-queued. A flood of 64 sends inside a single turn prunes nothing and still rejects, which
20532
+ * is the backpressure this bound exists for: `ClaudeInputQueue` cannot supply it, because the SDK
20533
+ * drains that queue eagerly and its buffer therefore sits near empty however hard the host sends.
20534
+ */
20535
+ pruneUnechoableInputs() {
20536
+ let reclaimed = 0;
20537
+ for (const [id2, resultsAtSend] of this.outstandingInputs) {
20538
+ if (this.mainThreadResults - resultsAtSend < UNECHOABLE_AFTER_RESULTS) continue;
20539
+ this.outstandingInputs.delete(id2);
20540
+ reclaimed += 1;
20541
+ }
20542
+ if (reclaimed === 0) return;
20543
+ this.diagnostic?.({
20544
+ kind: "stderr",
20545
+ message: `reclaimed ${reclaimed} outstanding input slot(s) the provider never echoed`,
20546
+ truncated: false
20547
+ });
20519
20548
  }
20520
20549
  clearOutstandingInputs() {
20521
20550
  this.outstandingInputs.clear();
20522
- this.outstandingInputOrder.splice(0);
20551
+ this.mainThreadResults = 0;
20523
20552
  this.providerProgressCovered = false;
20524
20553
  }
20525
20554
  async awaitOpenControl(operation) {
@@ -21408,7 +21437,10 @@ var EVENT_ERROR_TOLERANCE = 5;
21408
21437
  function runClaudeBroker(config) {
21409
21438
  const generation = config.generation ?? randomUUID();
21410
21439
  let client = null;
21440
+ const MAX_BACKLOG_FRAMES = 2e4;
21441
+ const MAX_BACKLOG_BYTES = 64 * 1024 * 1024;
21411
21442
  const eventBacklog = [];
21443
+ let eventBacklogBytes = 0;
21412
21444
  const pendingPermissions = /* @__PURE__ */ new Map();
21413
21445
  let permSeq = 0;
21414
21446
  let published = false;
@@ -21418,8 +21450,14 @@ function runClaudeBroker(config) {
21418
21450
  const emitEvent = (event) => {
21419
21451
  if (client) write(client, { t: "event", event });
21420
21452
  else {
21421
- eventBacklog.push(JSON.stringify({ t: "event", event }) + "\n");
21422
- if (eventBacklog.length > 2e4) eventBacklog.shift();
21453
+ const frame = JSON.stringify({ t: "event", event }) + "\n";
21454
+ eventBacklog.push(frame);
21455
+ eventBacklogBytes += Buffer.byteLength(frame);
21456
+ while (eventBacklog.length > MAX_BACKLOG_FRAMES || eventBacklogBytes > MAX_BACKLOG_BYTES) {
21457
+ const dropped = eventBacklog.shift();
21458
+ if (dropped === void 0) break;
21459
+ eventBacklogBytes -= Buffer.byteLength(dropped);
21460
+ }
21423
21461
  }
21424
21462
  };
21425
21463
  const writeDiagnostic = config.diagnosticLogPath ? createClaudeBrokerDiagnosticWriter(config.diagnosticLogPath, { daemonPid: process.pid, generation }) : void 0;
@@ -21514,6 +21552,7 @@ function runClaudeBroker(config) {
21514
21552
  write(sock, { t: "hello", sessionId: handle.sessionId, generation });
21515
21553
  for (const [requestId, { request }] of pendingPermissions) write(sock, { t: "permission-request", requestId, request });
21516
21554
  while (eventBacklog.length) sock.write(eventBacklog.shift());
21555
+ eventBacklogBytes = 0;
21517
21556
  let buf = "";
21518
21557
  sock.on("data", (chunk) => {
21519
21558
  buf += chunk;
@@ -21536,7 +21575,8 @@ function runClaudeBroker(config) {
21536
21575
  });
21537
21576
  void handle.send(message).catch((error) => {
21538
21577
  const detail = error instanceof Error ? error.message : String(error);
21539
- const diagnostic = { kind: "stderr", message: `input dropped, the agent never received it: ${detail}`, truncated: false };
21578
+ const droppedId = typeof message?.id === "string" ? ` id=${message.id}` : "";
21579
+ const diagnostic = { kind: "stderr", message: `${CLAUDE_INPUT_DROP_DIAGNOSTIC_PREFIX}:${droppedId} ${detail}`, truncated: false };
21540
21580
  writeDiagnostic?.(diagnostic);
21541
21581
  if (client) write(client, { t: "diagnostic", diagnostic });
21542
21582
  });