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.
- package/README.md +25 -20
- package/dist/claude-agent-broker.js +72 -27
- package/dist/dev-child.js +8302 -4628
- package/dist/frizz.js +1302 -701
- package/package.json +10 -4
- package/runtime/cc-worker/.claude-plugin/plugin.json +1 -1
- package/runtime/cc-worker/DECISIONS.md +81 -4
- package/runtime/cc-worker/bin/frizz-mcp.mjs +472 -58
- package/runtime/cc-worker/hooks/agent-dispatch.mjs +1 -1
- package/runtime/cc-worker/hooks/deny-plan.mjs +1 -1
- package/runtime/cc-worker/hooks/hooks.json +0 -7
- package/runtime/cc-worker/hooks/precompact-instructions.mjs +39 -28
- package/runtime/cc-worker/hooks/scratchpad.mjs +142 -169
- package/runtime/cc-worker/hooks/session-seed.mjs +16 -15
- package/runtime/cc-worker/skills/waits/SKILL.md +2 -2
- package/web-dist/apple-touch-icon.png +0 -0
- package/web-dist/assets/{TerminalPane-ROKHp1ib.js → TerminalPane-sd3XXjx5.js} +2 -2
- package/web-dist/assets/{abnfDiagram-VRR7QNED-DcpdhBs3.js → abnfDiagram-VRR7QNED-Bry_FIqg.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-CoMJQ5Ya.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CUAKf0mn.js → architectureDiagram-ZJ3FMSHR-Baok4OeI.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BPwpJIzx.js → blockDiagram-677ZJIJ3-BgHIO9lI.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-1lptuHzZ.js → c4Diagram-LMCZKHZV-rfj9Z17Z.js} +1 -1
- package/web-dist/assets/channel-D6uS1y7I.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-CFR9AKjY.js → chunk-32BRIVSS-BwaHfK1Y.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-CM9uct7m.js → chunk-52WLFC77-BmdQDZOA.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-DiveJARw.js → chunk-C7G6YPKG-ByQFjoZe.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-BE1CBw8F.js → chunk-EX3LRPZG-DQFudBKr.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-DL42uXiO.js → chunk-FWX5IMBZ-OW9qEPiq.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-DPhJWgDw.js → chunk-HOUHSVGY-BdKLF8nO.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-CwYy-6AP.js → chunk-ICXQ74PX-C1roqKR1.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-Ds5I9wxq.js → chunk-MOJQB5TN-Cx0u1ZDK.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-DHiZJwQD.js → chunk-OGEWGWER-rxv9xtNR.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-B-eyQTsF.js → chunk-PUDLZKDR-BXWPPPPi.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-DK7dB3Ti.js → chunk-Q4XR5HBZ-CpqbD101.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-Dn59m74L.js → chunk-V7JOEXUC-CP8_3qVt.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-DK7x36hd.js → chunk-VAUOI2AC-Dl7xahpk.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-D7-CI3Yl.js → chunk-VR4S4FIN-BKtMX64l.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-B3l-mLCs.js → chunk-WYO6CB5R-DmLOdJcu.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-Dobxlxie.js → chunk-ZGVPDNZ5-5wN7eXvS.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-BW5YiBf4.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-BW5YiBf4.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-Dh7RuEUJ.js → cynefin-VYW2F7L2-DUYahBXb.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-ClPi2mZZ.js → cynefinDiagram-TSTJHNR4-Rm0i30nE.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-52_WP1QV.js → dagre-VKFMJZFB-3uhsB-W3.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-D_1zVsTL.js → diagram-FQU43EPY-BUvflNRQ.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-CdZxuGUy.js → diagram-G47NLZAW-DUluIZel.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-C8pSFu0P.js → diagram-NH7WQ7WH-BBrwsbed.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-C5bjZLre.js → diagram-OA4YK3LP-DWnTQBw4.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-Bxzhiuzn.js → diagram-WEI45ONY-BjEavoC0.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-g-Z0J2wP.js → ebnfDiagram-CCIWWBDH-CW4xZAZH.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-DVNkgIHp.js → erDiagram-Q63AITRT-BemQ1R4B.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-BF5KB7ja.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-BYo4Eytx.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-_71pQYEK.js → ganttDiagram-NO4QXBWP-lDdG5I_z.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-ChIZiGZS.js → gitGraph-TEB2WS4Q-Bw87a_Se.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-DCHAFI0l.js → gitGraphDiagram-IHSO6WYX-B762r0yi.js} +1 -1
- package/web-dist/assets/index-B3QArXu2.css +1 -0
- package/web-dist/assets/index-CAapfhMv.js +365 -0
- package/web-dist/assets/{info-DKCQHKI2-BW-n_T1j.js → info-DKCQHKI2-DGOv6Gx0.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-CgDYsKi9.js → infoDiagram-FWYZ7A6U-DL92kAzf.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-ClzGNt9N.js → ishikawaDiagram-FXEZZL3T-CFYd4OtU.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-DSCQxkHC.js → journeyDiagram-5HDEW3XC-QHuYL5zq.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-CdrdX9N8.js → kanban-definition-HUTT4EX6-DwmMluQ8.js} +1 -1
- package/web-dist/assets/{line-ha38Dc-1.js → line-Je_PFFHm.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-Z4uMcpip.js → mermaid-parser.core-BeiGWsn3.js} +3 -3
- package/web-dist/assets/{mermaid.core-iZRq3hbu.js → mermaid.core-BP8JrW48.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-DmhInJO_.js → mindmap-definition-LN4V7U3C-BsmjCIrM.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-DBPB36Kl.js → packet-7NZHBO7P-D6ekYYGJ.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-CAH3ljfj.js → pegDiagram-2B236MQR-CQc2Kt0u.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-_h_eX4Ca.js → pie-RZYD4A2V-C_45fO8J.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-DFBPus8j.js → pieDiagram-ENE6RG2P-Dg5z6iWt.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DMvOCjt8.js → quadrantDiagram-ABIIQ3AL-c7gZxuCX.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-2EzoPHEZ.js → radar-I7S5WNFK-D80XNJQq.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-BPJnn-hm.js → railroad-3IZDKUUU-Brz7eZQ0.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-D44pcGhJ.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-CiWVH5SP.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Cy1B6-pN.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-DKq5FagA.js → railroadDiagram-RFXS5EU6-Db9K7bty.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-BJ5tGazp.js → requirementDiagram-TGXJPOKE-m1ipMrDe.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-XSJjcBhX.js → sankeyDiagram-HTMAVEWB-4Zz9ry0k.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-CBb8emSe.js → sequenceDiagram-DBY2YBRQ-CZeGHw4X.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-DDfRW94V.js → stateDiagram-2N3HPSRC-rLUVs1Pg.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-w7QDFEvx.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-DvRYbkZi.js → swimlanes-5IMT3BWC-BAeBdscz.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-UhdgWpNZ.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-BNUa_DwI.js → timeline-definition-FHXFAJF6-DL0eWMF-.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-I6-IW6nJ.js → treeView-QDETBFTQ-gEtzLMA8.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-CWWmEUYJ.js → treemap-6X3UGDF4-CCym1Gz6.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-DTDrPGLk.js → vennDiagram-L72KCM5P-cF97-nb5.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-CNsdgXXA.js → wardley-OPB4EBWU-DQJHCGbM.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-YE0tq3Kh.js → wardleyDiagram-EHGQE667-0CnHzXLX.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-D0ofMX8C.js → xychartDiagram-FW5EYKEG-9YDOp4m5.js} +1 -1
- package/web-dist/favicon-16.png +0 -0
- package/web-dist/favicon-32.png +0 -0
- package/web-dist/favicon.svg +17 -61
- package/web-dist/icon-192.png +0 -0
- package/web-dist/icon-512.png +0 -0
- package/web-dist/icon-maskable-512.png +0 -0
- package/web-dist/index.html +19 -8
- package/web-dist/manifest.webmanifest +3 -3
- package/runtime/cc-worker/hooks/scratchpad-stop.mjs +0 -125
- package/runtime/cc-worker/skills/handoff/SKILL.md +0 -209
- package/web-dist/assets/architecture-TIHT7OUA-CAviNivx.js +0 -1
- package/web-dist/assets/channel-CqKDIFQF.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-B_-6iXYY.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-B_-6iXYY.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-MpmeH5YZ.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-D-QgjjhF.js +0 -1
- package/web-dist/assets/index-w4v-GZEc.js +0 -358
- package/web-dist/assets/index-zyi22LPz.css +0 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-YeUoiySk.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-Ddw1SuGG.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Dd8BOGeW.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-hc41W5Lx.js +0 -1
- 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"
|
|
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/
|
|
33
|
+
$ cd path/to/acme
|
|
34
34
|
$ npx frizz
|
|
35
35
|
|
|
36
|
-
FRIZZ v0.
|
|
36
|
+
FRIZZ v0.4.0 ready in 4.0s
|
|
37
37
|
|
|
38
|
-
➜ Local: http://127.0.0.1:
|
|
39
|
-
➜ Project:
|
|
40
|
-
➜ Logs:
|
|
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
|
|
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
|
|
141
|
-
FRIZZ_ALLOWED_HOSTS
|
|
142
|
-
FRIZZ_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
|
-
|
|
146
|
-
|
|
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.
|
|
150
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
>
|
|
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
|
-
|
|
20249
|
-
|
|
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.
|
|
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")
|
|
20508
|
+
if (event.kind === "result") {
|
|
20509
|
+
this.providerProgressCovered = false;
|
|
20510
|
+
this.mainThreadResults += 1;
|
|
20511
|
+
}
|
|
20511
20512
|
}
|
|
20512
20513
|
}
|
|
20513
20514
|
releaseOldestOutstandingInput() {
|
|
20514
|
-
|
|
20515
|
-
|
|
20516
|
-
|
|
20517
|
-
|
|
20518
|
-
|
|
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.
|
|
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
|
-
//
|
|
20598
|
-
//
|
|
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`".
|
|
20603
|
-
|
|
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
|
-
|
|
21422
|
-
|
|
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
|
|
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
|
});
|