frizz 0.2.0 → 0.4.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 +25 -20
  2. package/dist/claude-agent-broker.js +72 -27
  3. package/dist/dev-child.js +8302 -4628
  4. package/dist/frizz.js +1302 -701
  5. package/package.json +10 -4
  6. package/runtime/cc-worker/.claude-plugin/plugin.json +1 -1
  7. package/runtime/cc-worker/DECISIONS.md +81 -4
  8. package/runtime/cc-worker/bin/frizz-mcp.mjs +472 -58
  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-sd3XXjx5.js} +2 -2
  18. package/web-dist/assets/{abnfDiagram-VRR7QNED-DcpdhBs3.js → abnfDiagram-VRR7QNED-Bry_FIqg.js} +1 -1
  19. package/web-dist/assets/architecture-TIHT7OUA-CoMJQ5Ya.js +1 -0
  20. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CUAKf0mn.js → architectureDiagram-ZJ3FMSHR-Baok4OeI.js} +1 -1
  21. package/web-dist/assets/{blockDiagram-677ZJIJ3-BPwpJIzx.js → blockDiagram-677ZJIJ3-BgHIO9lI.js} +1 -1
  22. package/web-dist/assets/{c4Diagram-LMCZKHZV-1lptuHzZ.js → c4Diagram-LMCZKHZV-rfj9Z17Z.js} +1 -1
  23. package/web-dist/assets/channel-D6uS1y7I.js +1 -0
  24. package/web-dist/assets/{chunk-32BRIVSS-CFR9AKjY.js → chunk-32BRIVSS-BwaHfK1Y.js} +1 -1
  25. package/web-dist/assets/{chunk-52WLFC77-CM9uct7m.js → chunk-52WLFC77-BmdQDZOA.js} +1 -1
  26. package/web-dist/assets/{chunk-C7G6YPKG-DiveJARw.js → chunk-C7G6YPKG-ByQFjoZe.js} +1 -1
  27. package/web-dist/assets/{chunk-EX3LRPZG-BE1CBw8F.js → chunk-EX3LRPZG-DQFudBKr.js} +1 -1
  28. package/web-dist/assets/{chunk-FWX5IMBZ-DL42uXiO.js → chunk-FWX5IMBZ-OW9qEPiq.js} +2 -2
  29. package/web-dist/assets/{chunk-HOUHSVGY-DPhJWgDw.js → chunk-HOUHSVGY-BdKLF8nO.js} +1 -1
  30. package/web-dist/assets/{chunk-ICXQ74PX-CwYy-6AP.js → chunk-ICXQ74PX-C1roqKR1.js} +1 -1
  31. package/web-dist/assets/{chunk-MOJQB5TN-Ds5I9wxq.js → chunk-MOJQB5TN-Cx0u1ZDK.js} +1 -1
  32. package/web-dist/assets/{chunk-OGEWGWER-DHiZJwQD.js → chunk-OGEWGWER-rxv9xtNR.js} +1 -1
  33. package/web-dist/assets/{chunk-PUDLZKDR-B-eyQTsF.js → chunk-PUDLZKDR-BXWPPPPi.js} +1 -1
  34. package/web-dist/assets/{chunk-Q4XR5HBZ-DK7dB3Ti.js → chunk-Q4XR5HBZ-CpqbD101.js} +1 -1
  35. package/web-dist/assets/{chunk-V7JOEXUC-Dn59m74L.js → chunk-V7JOEXUC-CP8_3qVt.js} +1 -1
  36. package/web-dist/assets/{chunk-VAUOI2AC-DK7x36hd.js → chunk-VAUOI2AC-Dl7xahpk.js} +1 -1
  37. package/web-dist/assets/{chunk-VR4S4FIN-D7-CI3Yl.js → chunk-VR4S4FIN-BKtMX64l.js} +1 -1
  38. package/web-dist/assets/{chunk-WYO6CB5R-B3l-mLCs.js → chunk-WYO6CB5R-DmLOdJcu.js} +1 -1
  39. package/web-dist/assets/{chunk-ZGVPDNZ5-Dobxlxie.js → chunk-ZGVPDNZ5-5wN7eXvS.js} +1 -1
  40. package/web-dist/assets/classDiagram-OUVF2IWQ-BW5YiBf4.js +1 -0
  41. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BW5YiBf4.js +1 -0
  42. package/web-dist/assets/{cynefin-VYW2F7L2-Dh7RuEUJ.js → cynefin-VYW2F7L2-DUYahBXb.js} +1 -1
  43. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-ClPi2mZZ.js → cynefinDiagram-TSTJHNR4-Rm0i30nE.js} +1 -1
  44. package/web-dist/assets/{dagre-VKFMJZFB-52_WP1QV.js → dagre-VKFMJZFB-3uhsB-W3.js} +1 -1
  45. package/web-dist/assets/{diagram-FQU43EPY-D_1zVsTL.js → diagram-FQU43EPY-BUvflNRQ.js} +1 -1
  46. package/web-dist/assets/{diagram-G47NLZAW-CdZxuGUy.js → diagram-G47NLZAW-DUluIZel.js} +1 -1
  47. package/web-dist/assets/{diagram-NH7WQ7WH-C8pSFu0P.js → diagram-NH7WQ7WH-BBrwsbed.js} +1 -1
  48. package/web-dist/assets/{diagram-OA4YK3LP-C5bjZLre.js → diagram-OA4YK3LP-DWnTQBw4.js} +1 -1
  49. package/web-dist/assets/{diagram-WEI45ONY-Bxzhiuzn.js → diagram-WEI45ONY-BjEavoC0.js} +1 -1
  50. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-g-Z0J2wP.js → ebnfDiagram-CCIWWBDH-CW4xZAZH.js} +1 -1
  51. package/web-dist/assets/{erDiagram-Q63AITRT-DVNkgIHp.js → erDiagram-Q63AITRT-BemQ1R4B.js} +1 -1
  52. package/web-dist/assets/eventmodeling-45OFAUF4-BF5KB7ja.js +1 -0
  53. package/web-dist/assets/flowDiagram-23GEKE2U-BYo4Eytx.js +1 -0
  54. package/web-dist/assets/{ganttDiagram-NO4QXBWP-_71pQYEK.js → ganttDiagram-NO4QXBWP-lDdG5I_z.js} +1 -1
  55. package/web-dist/assets/{gitGraph-TEB2WS4Q-ChIZiGZS.js → gitGraph-TEB2WS4Q-Bw87a_Se.js} +1 -1
  56. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-DCHAFI0l.js → gitGraphDiagram-IHSO6WYX-B762r0yi.js} +1 -1
  57. package/web-dist/assets/index-B3QArXu2.css +1 -0
  58. package/web-dist/assets/index-CAapfhMv.js +365 -0
  59. package/web-dist/assets/{info-DKCQHKI2-BW-n_T1j.js → info-DKCQHKI2-DGOv6Gx0.js} +1 -1
  60. package/web-dist/assets/{infoDiagram-FWYZ7A6U-CgDYsKi9.js → infoDiagram-FWYZ7A6U-DL92kAzf.js} +1 -1
  61. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-ClzGNt9N.js → ishikawaDiagram-FXEZZL3T-CFYd4OtU.js} +1 -1
  62. package/web-dist/assets/{journeyDiagram-5HDEW3XC-DSCQxkHC.js → journeyDiagram-5HDEW3XC-QHuYL5zq.js} +1 -1
  63. package/web-dist/assets/{kanban-definition-HUTT4EX6-CdrdX9N8.js → kanban-definition-HUTT4EX6-DwmMluQ8.js} +1 -1
  64. package/web-dist/assets/{line-ha38Dc-1.js → line-Je_PFFHm.js} +1 -1
  65. package/web-dist/assets/{mermaid-parser.core-Z4uMcpip.js → mermaid-parser.core-BeiGWsn3.js} +3 -3
  66. package/web-dist/assets/{mermaid.core-iZRq3hbu.js → mermaid.core-BP8JrW48.js} +3 -3
  67. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DmhInJO_.js → mindmap-definition-LN4V7U3C-BsmjCIrM.js} +1 -1
  68. package/web-dist/assets/{packet-7NZHBO7P-DBPB36Kl.js → packet-7NZHBO7P-D6ekYYGJ.js} +1 -1
  69. package/web-dist/assets/{pegDiagram-2B236MQR-CAH3ljfj.js → pegDiagram-2B236MQR-CQc2Kt0u.js} +1 -1
  70. package/web-dist/assets/{pie-RZYD4A2V-_h_eX4Ca.js → pie-RZYD4A2V-C_45fO8J.js} +1 -1
  71. package/web-dist/assets/{pieDiagram-ENE6RG2P-DFBPus8j.js → pieDiagram-ENE6RG2P-Dg5z6iWt.js} +1 -1
  72. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DMvOCjt8.js → quadrantDiagram-ABIIQ3AL-c7gZxuCX.js} +1 -1
  73. package/web-dist/assets/{radar-I7S5WNFK-2EzoPHEZ.js → radar-I7S5WNFK-D80XNJQq.js} +1 -1
  74. package/web-dist/assets/{railroad-3IZDKUUU-BPJnn-hm.js → railroad-3IZDKUUU-Brz7eZQ0.js} +1 -1
  75. package/web-dist/assets/railroad-abnf-AHOZXSZD-D44pcGhJ.js +1 -0
  76. package/web-dist/assets/railroad-ebnf-EBAXGLYW-CiWVH5SP.js +1 -0
  77. package/web-dist/assets/railroad-peg-LSFZ7HO6-Cy1B6-pN.js +1 -0
  78. package/web-dist/assets/{railroadDiagram-RFXS5EU6-DKq5FagA.js → railroadDiagram-RFXS5EU6-Db9K7bty.js} +1 -1
  79. package/web-dist/assets/{requirementDiagram-TGXJPOKE-BJ5tGazp.js → requirementDiagram-TGXJPOKE-m1ipMrDe.js} +1 -1
  80. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-XSJjcBhX.js → sankeyDiagram-HTMAVEWB-4Zz9ry0k.js} +1 -1
  81. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-CBb8emSe.js → sequenceDiagram-DBY2YBRQ-CZeGHw4X.js} +1 -1
  82. package/web-dist/assets/{stateDiagram-2N3HPSRC-DDfRW94V.js → stateDiagram-2N3HPSRC-rLUVs1Pg.js} +1 -1
  83. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-w7QDFEvx.js +1 -0
  84. package/web-dist/assets/{swimlanes-5IMT3BWC-DvRYbkZi.js → swimlanes-5IMT3BWC-BAeBdscz.js} +1 -1
  85. package/web-dist/assets/swimlanesDiagram-G3AALYLV-UhdgWpNZ.js +8 -0
  86. package/web-dist/assets/{timeline-definition-FHXFAJF6-BNUa_DwI.js → timeline-definition-FHXFAJF6-DL0eWMF-.js} +1 -1
  87. package/web-dist/assets/{treeView-QDETBFTQ-I6-IW6nJ.js → treeView-QDETBFTQ-gEtzLMA8.js} +1 -1
  88. package/web-dist/assets/{treemap-6X3UGDF4-CWWmEUYJ.js → treemap-6X3UGDF4-CCym1Gz6.js} +1 -1
  89. package/web-dist/assets/{vennDiagram-L72KCM5P-DTDrPGLk.js → vennDiagram-L72KCM5P-cF97-nb5.js} +1 -1
  90. package/web-dist/assets/{wardley-OPB4EBWU-CNsdgXXA.js → wardley-OPB4EBWU-DQJHCGbM.js} +1 -1
  91. package/web-dist/assets/{wardleyDiagram-EHGQE667-YE0tq3Kh.js → wardleyDiagram-EHGQE667-0CnHzXLX.js} +1 -1
  92. package/web-dist/assets/{xychartDiagram-FW5EYKEG-D0ofMX8C.js → xychartDiagram-FW5EYKEG-9YDOp4m5.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>
@@ -30,19 +30,19 @@ Frizz is for you if you have any of these opinions:
30
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.
31
31
 
32
32
  ```sh
33
- $ cd path/to/project
33
+ $ cd path/to/acme
34
34
  $ npx frizz
35
35
 
36
- FRIZZ v0.2.0 ready in 4.0s
36
+ FRIZZ v0.4.0 ready in 4.0s
37
37
 
38
- ➜ Local: http://127.0.0.1:4922/
39
- ➜ Project: project — path/to/project
40
- ➜ 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: acme — path/to/acme
40
+ ➜ Logs: ~/Library/Application Support/Frizz/projects/979dae3c-fe15-4038-817e-11d0e7491959/logs/frizz-2026-08-01T13-44-43-16931.log
41
41
 
42
42
  press ctrl-c to stop · run with --debug for the full event feed
43
43
  ```
44
44
 
45
- A browser tab opens on localhost — a dedicated workspace for this directory. **One tab per project!** 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.
46
46
 
47
47
  <p align="center">
48
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%">
@@ -62,7 +62,7 @@ Frizz is a browser tab, a queue, and the agent CLIs you already pay for. It brin
62
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.
63
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.
64
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.
65
- - 🔒 **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
66
 
67
67
  ### The queue
68
68
 
@@ -133,21 +133,24 @@ Options:
133
133
  --host [address] serve on a network address instead of loopback (bare --host means 0.0.0.0)
134
134
  --allowed-host <name> with --host, also accept this DNS name as the board's address (repeatable)
135
135
  --public-origin <url> serve behind a proxy/tunnel reachable at this exact origin
136
+ --link print a fresh single-use access link for the running board
136
137
  --debug stream the full event feed to the terminal instead of the compact readout
137
138
  -h, --help show this help
138
139
 
139
140
  Environment:
140
- FRIZZ_HOST same as --host
141
- FRIZZ_ALLOWED_HOSTS same as --allowed-host, comma separated
142
- FRIZZ_PUBLIC_ORIGIN same as --public-origin
141
+ FRIZZ_HOST same as --host
142
+ FRIZZ_ALLOWED_HOSTS same as --allowed-host, comma separated
143
+ FRIZZ_PUBLIC_ORIGIN same as --public-origin
144
+ FRIZZ_PUBLIC_TOKEN standing secret for HEADLESS boxes that cannot show a QR
143
145
 
144
- --host puts a board that can run shell commands as you on the network, and Frizz has no login:
145
- anyone who reaches the port controls it. Only do this on a network you trust. An IP address works
146
- as-is; to reach the board by DNS name you must list that name with --allowed-host ("*" allows any).
146
+ --host puts a board that can run shell commands as you on the network, and Frizz has no login: anyone
147
+ who reaches the port controls it. Only do this on a network you trust. An IP address works as-is; to
148
+ reach the board by DNS name you must list that name with --allowed-host ("*" allows any).
147
149
 
148
150
  --public-origin serves the board through a tunnel or reverse proxy without putting it on the LAN
149
- at all — Frizz stays on loopback and the tunnel dials it. Frizz still has no login, so require
150
- authentication at the proxy: with Cloudflare Access, that is the whole of your access control.
151
+ at all — Frizz stays on loopback and the tunnel dials it. It prints a SINGLE-USE access link, and
152
+ shows it as a QR so you can scan it from a phone; press L for a fresh one at any time. Scanning it
153
+ trades the code for a session cookie, so the link itself stops working the moment it is used.
151
154
  ```
152
155
 
153
156
  <br/>
@@ -178,7 +181,7 @@ authentication at the proxy: with Cloudflare Access, that is the whole of your a
178
181
  <details>
179
182
  <summary><b>Does it put junk in my repo?</b></summary>
180
183
 
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 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`.
184
+ > 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`.
182
185
 
183
186
  </details>
184
187
 
@@ -208,7 +211,7 @@ authentication at the proxy: with Cloudflare Access, that is the whole of your a
208
211
  >
209
212
  > 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
213
  >
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 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.
214
+ > 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
215
 
213
216
  </details>
214
217
 
@@ -219,14 +222,16 @@ authentication at the proxy: with Cloudflare Access, that is the whole of your a
219
222
  >
220
223
  > ```sh
221
224
  > npx frizz --public-origin https://frizz.example.com
222
- > cloudflared tunnel --url http://127.0.0.1:4922 # the port Frizz printed
225
+ > cloudflared tunnel --url http://127.0.0.1:9393 # the port Frizz printed
223
226
  > ```
224
227
  >
225
228
  > 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
229
  >
227
230
  > 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
231
  >
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.
232
+ > Frizz prints a **single-use** access link and renders it as a QR in your terminal, so you can scan it from a phone without typing forty characters. Scanning trades the code for a session cookie and the link immediately stops working; press **L** any time for a fresh one. A code expires in five minutes, and a spent one says so rather than failing silently.
233
+
234
+ **Even so: Frizz has no accounts, so that link — and anything 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.
230
235
 
231
236
  </details>
232
237
 
@@ -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) {
@@ -20594,13 +20623,18 @@ function startClaudeQuery(executablePath, options) {
20594
20623
  ...options.session.kind === "new" ? { sessionId } : { resume: sessionId },
20595
20624
  canUseTool,
20596
20625
  onElicitation,
20597
- // PROJECT + LOCAL by default — see ClaudeQueryStartOptions.settingSources. `[]` was correct while
20598
- // this was a standalone foundation nothing dispatched through; once the broker became the DEFAULT
20626
+ // EVERY scope by default — see ClaudeQueryStartOptions.settingSources. `[]` was correct while this
20627
+ // was a standalone foundation nothing dispatched through; once the broker became the DEFAULT
20599
20628
  // Claude transport it silently stopped every worker from seeing the repo's own `CLAUDE.md` /
20600
20629
  // `AGENTS.md` and its `.claude/skills`. Measured differential in the frizz repo, one variable:
20601
20630
  // this factory answered `NO-CLAUDE-MD` where a plain `claude -p` in the same cwd answered
20602
- // "# No pull requests — land on local `main`". `"user"` stays OUT on purpose (see the field docs).
20603
- settingSources: options.settingSources ?? ["project", "local"],
20631
+ // "# No pull requests — land on local `main`". That fix restored PROJECT + LOCAL and left USER
20632
+ // out, which kept a second, quieter half of the same regression: the tmux transport ran plain
20633
+ // `claude` and got all three scopes, so a broker session was the only place the operator's
20634
+ // `~/.claude/settings.json` stopped applying. Measured 2026-08-16 against the maintainer's real
20635
+ // config, one variable: `--setting-sources=project,local` reported their `env` block UNSET where
20636
+ // `user,project,local` reported it applied.
20637
+ settingSources: options.settingSources ?? ["user", "project", "local"],
20604
20638
  // The frizz worker environment (see ClaudeQueryStartOptions): load the local cc-worker plugin so a
20605
20639
  // broker session gets the frizz sub-agent profiles + hooks, mount the MCP servers (frizz +
20606
20640
  // chrome-devtools), and pre-approve them.
@@ -21408,7 +21442,10 @@ var EVENT_ERROR_TOLERANCE = 5;
21408
21442
  function runClaudeBroker(config) {
21409
21443
  const generation = config.generation ?? randomUUID();
21410
21444
  let client = null;
21445
+ const MAX_BACKLOG_FRAMES = 2e4;
21446
+ const MAX_BACKLOG_BYTES = 64 * 1024 * 1024;
21411
21447
  const eventBacklog = [];
21448
+ let eventBacklogBytes = 0;
21412
21449
  const pendingPermissions = /* @__PURE__ */ new Map();
21413
21450
  let permSeq = 0;
21414
21451
  let published = false;
@@ -21418,8 +21455,14 @@ function runClaudeBroker(config) {
21418
21455
  const emitEvent = (event) => {
21419
21456
  if (client) write(client, { t: "event", event });
21420
21457
  else {
21421
- eventBacklog.push(JSON.stringify({ t: "event", event }) + "\n");
21422
- if (eventBacklog.length > 2e4) eventBacklog.shift();
21458
+ const frame = JSON.stringify({ t: "event", event }) + "\n";
21459
+ eventBacklog.push(frame);
21460
+ eventBacklogBytes += Buffer.byteLength(frame);
21461
+ while (eventBacklog.length > MAX_BACKLOG_FRAMES || eventBacklogBytes > MAX_BACKLOG_BYTES) {
21462
+ const dropped = eventBacklog.shift();
21463
+ if (dropped === void 0) break;
21464
+ eventBacklogBytes -= Buffer.byteLength(dropped);
21465
+ }
21423
21466
  }
21424
21467
  };
21425
21468
  const writeDiagnostic = config.diagnosticLogPath ? createClaudeBrokerDiagnosticWriter(config.diagnosticLogPath, { daemonPid: process.pid, generation }) : void 0;
@@ -21514,6 +21557,7 @@ function runClaudeBroker(config) {
21514
21557
  write(sock, { t: "hello", sessionId: handle.sessionId, generation });
21515
21558
  for (const [requestId, { request }] of pendingPermissions) write(sock, { t: "permission-request", requestId, request });
21516
21559
  while (eventBacklog.length) sock.write(eventBacklog.shift());
21560
+ eventBacklogBytes = 0;
21517
21561
  let buf = "";
21518
21562
  sock.on("data", (chunk) => {
21519
21563
  buf += chunk;
@@ -21536,7 +21580,8 @@ function runClaudeBroker(config) {
21536
21580
  });
21537
21581
  void handle.send(message).catch((error) => {
21538
21582
  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 };
21583
+ const droppedId = typeof message?.id === "string" ? ` id=${message.id}` : "";
21584
+ const diagnostic = { kind: "stderr", message: `${CLAUDE_INPUT_DROP_DIAGNOSTIC_PREFIX}:${droppedId} ${detail}`, truncated: false };
21540
21585
  writeDiagnostic?.(diagnostic);
21541
21586
  if (client) write(client, { t: "diagnostic", diagnostic });
21542
21587
  });