frizz 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -4
- package/dist/claude-agent-broker.js +9 -4
- package/dist/dev-child.js +1868 -529
- package/dist/frizz.js +814 -234
- package/package.json +5 -3
- package/runtime/cc-worker/DECISIONS.md +17 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +159 -174
- package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
- package/web-dist/assets/{TerminalPane-CTdetDJJ.js → TerminalPane-WcpgmCA-.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-BDKrjCMs.js → abnfDiagram-VRR7QNED-Caecklbz.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-BTy2nvYL.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-BJebpUUM.js → architectureDiagram-ZJ3FMSHR-BEk0fg8C.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BNbuk25k.js → blockDiagram-677ZJIJ3-B4yq3xiy.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-lCfyotdU.js → c4Diagram-LMCZKHZV-DaA5NBkz.js} +1 -1
- package/web-dist/assets/channel-WghlT4VS.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-DP72SEkr.js → chunk-32BRIVSS-2XsSs8F_.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-DxO3gN-p.js → chunk-52WLFC77-DWJfECP3.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-BXFx9Vlr.js → chunk-C7G6YPKG-Bz07eaGt.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-CR3sHpPf.js → chunk-EX3LRPZG-D78kZEMM.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-CJ8L__oG.js → chunk-FWX5IMBZ-CeCa17CW.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-BCl3JSWr.js → chunk-HOUHSVGY-9H4GFuBa.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-CZxFMci1.js → chunk-ICXQ74PX--Aa17r4X.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-Ck3_47dB.js → chunk-MOJQB5TN-C3vXhsPV.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-CVXjES-F.js → chunk-OGEWGWER-G4kZRBkX.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-JjrVgaR7.js → chunk-PUDLZKDR-DWd5kvyM.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-C5YMRAQ0.js → chunk-Q4XR5HBZ-u55p39Up.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-DK0mfUHU.js → chunk-V7JOEXUC-BU4YBmV4.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-BKBntpid.js → chunk-VAUOI2AC-VXcmT20q.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-zgE1dG6U.js → chunk-VR4S4FIN-CSjEXLOC.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-CZhh1IBq.js → chunk-WYO6CB5R-4WGk2dpL.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-CrNjIEem.js → chunk-ZGVPDNZ5-D5Xl8ZV6.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-D21-Dy4r.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-D21-Dy4r.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-Ca_BPfTG.js → cynefin-VYW2F7L2-O6wB49wK.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-CsINQf6g.js → cynefinDiagram-TSTJHNR4-pd6ZogNC.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-CE0DYMFy.js → dagre-VKFMJZFB-D8W1S6fw.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-BqTduEYE.js → diagram-FQU43EPY-Bm1NOkHd.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-Cho718s6.js → diagram-G47NLZAW-CjwnXTIc.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-D0Z8t9UC.js → diagram-NH7WQ7WH-DH1jZ_mu.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-CBacyeDx.js → diagram-OA4YK3LP-B-s969Q_.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-CozpXa4j.js → diagram-WEI45ONY-BU3lv6cL.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-ba4NbQQo.js → ebnfDiagram-CCIWWBDH-FsAeyqcC.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-B3BciOYa.js → erDiagram-Q63AITRT-BRvg5Y9d.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-RhX2WA7g.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-C7N14tdV.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-lPmrRit4.js → ganttDiagram-NO4QXBWP-eATKNh9U.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-BjyclOq0.js → gitGraph-TEB2WS4Q-CNYqsua8.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-pzeb4Yrw.js → gitGraphDiagram-IHSO6WYX-BUxgluf8.js} +1 -1
- package/web-dist/assets/index-BFuReKr0.js +368 -0
- package/web-dist/assets/index-BH9U3Zlg.css +1 -0
- package/web-dist/assets/{info-DKCQHKI2-Bwycegvf.js → info-DKCQHKI2-HsVLwo1c.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-BCvRGj_5.js → infoDiagram-FWYZ7A6U-D7U65xDp.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js → ishikawaDiagram-FXEZZL3T-BsfIYXjX.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-C5ROwFio.js → journeyDiagram-5HDEW3XC-CpkREKol.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-YLPLkpeT.js → kanban-definition-HUTT4EX6-2M3KW6Gw.js} +1 -1
- package/web-dist/assets/{line-KtkNqRgI.js → line-Cs7zWs24.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-D_FfqBe7.js → mermaid-parser.core-Dw-IECV5.js} +3 -3
- package/web-dist/assets/{mermaid.core-Ffv8anVf.js → mermaid.core-C1lrddjT.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-B2jj4vfm.js → mindmap-definition-LN4V7U3C-gVS5On1g.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-BjmWHwra.js → packet-7NZHBO7P-Tti1-UWA.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-Dq3iJDyq.js → pegDiagram-2B236MQR-DE5in93G.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-jBbH1lv9.js → pie-RZYD4A2V-DfLNkvHl.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-C3ETW9lq.js → pieDiagram-ENE6RG2P-DcqSqJqb.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DItSmme7.js → quadrantDiagram-ABIIQ3AL-DR-nRPgN.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-6ey6crgP.js → radar-I7S5WNFK-DGLDBfPd.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-Cii-Mn0E.js → railroad-3IZDKUUU-BaiGEHlV.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-BCeoIOWA.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-lZpXJLD8.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-CaUBrHTL.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-M363ils_.js → railroadDiagram-RFXS5EU6-82aEev5_.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-CTs2_V6T.js → requirementDiagram-TGXJPOKE-Bs8opBRj.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-QTLLcDPD.js → sankeyDiagram-HTMAVEWB-gOb3FIr8.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js → sequenceDiagram-DBY2YBRQ-6DvjtRHR.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-BecB6roG.js → stateDiagram-2N3HPSRC-Cg2LWdUf.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-Digf9CWZ.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-COiYgS0w.js → swimlanes-5IMT3BWC-BDpPdYEl.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-DEZRDWHA.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-Ci22coeH.js → timeline-definition-FHXFAJF6-BSjfeutv.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-4DW35czh.js → treeView-QDETBFTQ-DwgPy6V3.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-Dq_-Z6-Y.js → treemap-6X3UGDF4-7eiifTKW.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-CHQSIEYq.js → vennDiagram-L72KCM5P-9u4AhcnV.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-Bwlh7HCY.js → wardley-OPB4EBWU-DqNb5x9U.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-DctjuPYn.js → wardleyDiagram-EHGQE667-DlJ7h3vg.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-ZoIGosv8.js → xychartDiagram-FW5EYKEG-B_9ukg2S.js} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/assets/architecture-TIHT7OUA-ChUMo004.js +0 -1
- package/web-dist/assets/channel-pr7r6raB.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-CvGbPMn_.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-CvGbPMn_.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-DKmyo-jd.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-DluCBvT4.js +0 -1
- package/web-dist/assets/index-BQtjYMpV.css +0 -1
- package/web-dist/assets/index-CLW1Q49U.js +0 -360
- package/web-dist/assets/railroad-abnf-AHOZXSZD-U_vb4BrX.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-BIHG7gNU.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Cpd9r-tB.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-JXw9T96l.js +0 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV--1Wv7FQQ.js +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frizz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A local web UI for running many coding agents at once",
|
|
6
6
|
"keywords": [
|
|
@@ -38,17 +38,19 @@
|
|
|
38
38
|
"frizz-dev:uninstall": "nub scripts/install-global-cli.mjs --uninstall",
|
|
39
39
|
"install:global": "nub scripts/install-global-cli.mjs",
|
|
40
40
|
"typecheck": "tsc -b packages/shared packages/rpc packages/server . && pnpm --filter @frizz/web typecheck",
|
|
41
|
-
"test": "nub
|
|
41
|
+
"test": "nub scripts/run-tests.mjs",
|
|
42
42
|
"prepack": "node ./scripts/prepare-package.mjs && node ./scripts/build-package.mjs && node ./scripts/publish-manifest.mjs --strip",
|
|
43
43
|
"postpack": "node ./scripts/publish-manifest.mjs --restore && node ./scripts/prepare-package.mjs --clean"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@parcel/watcher": "^2.5",
|
|
47
|
-
"node-pty": "^1.1"
|
|
47
|
+
"node-pty": "^1.1",
|
|
48
|
+
"qrcode-generator": "^2.0.4"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/node": "^24",
|
|
51
52
|
"esbuild": "^0.25.0",
|
|
53
|
+
"jsqr": "^1.4.0",
|
|
52
54
|
"puppeteer": "^25.3.0",
|
|
53
55
|
"typescript": "^5.7"
|
|
54
56
|
},
|
|
@@ -125,6 +125,23 @@ decision, not an artifact-portability implementation detail. A future isolation
|
|
|
125
125
|
which settings/auth surfaces are preserved before adding `--settings`, a config-home override, or a
|
|
126
126
|
global-plugin disable mechanism.
|
|
127
127
|
|
|
128
|
+
**2026-08-16 — the SDK broker had drifted off this policy, and is back on it.** The tmux transport
|
|
129
|
+
spawned a plain `claude`, which reads all three settings scopes, so the policy above held by default.
|
|
130
|
+
The Agent-SDK broker takes an explicit `settingSources` and the SDK's own default is `[]` — nothing at
|
|
131
|
+
all — so when the broker became the default Claude transport it read no config whatsoever. The
|
|
132
|
+
2026-07-26 fix restored `project` + `local` (the repo's `CLAUDE.md` / `AGENTS.md` / `.claude/skills`)
|
|
133
|
+
and left `user` out, on a rationale written into the code — "the operator's personal `~/.claude` config
|
|
134
|
+
is theirs, not something a dispatched worker should silently inherit" — that this record had already
|
|
135
|
+
rejected. The gap was invisible because everything it withheld fails QUIETLY: the operator's `env`
|
|
136
|
+
block (which is where an API-proxy front-end writes its base-URL/token pair, so a broker session
|
|
137
|
+
authenticates differently from the CLI in the same shell), `autoCompactWindow` (so sessions compacted
|
|
138
|
+
at the default threshold, not the configured one), `permissions`, `hooks` and `enabledPlugins`.
|
|
139
|
+
Measured against the maintainer's real config, one variable, observed through a project-scope hook:
|
|
140
|
+
`--setting-sources=project,local` reported their `env` block UNSET, `user,project,local` reported it
|
|
141
|
+
applied, both sessions exiting 0 with empty stderr. The default is now all three scopes — the same
|
|
142
|
+
thing a plain `claude` in the same cwd reads. A frizz thread is the operator's own session on their own
|
|
143
|
+
machine, so the surprising behavior was the divergence, not the inheritance.
|
|
144
|
+
|
|
128
145
|
## 2026-07-02: Stop hook removed
|
|
129
146
|
stop-flush.mjs is no longer wired (script kept for reference). User call: under frizz the
|
|
130
147
|
tailer/board already surface worker state live, and the block-until-file-edited nag forced even
|
|
@@ -49,6 +49,9 @@ const DISPATCH_TIMEOUT_MS = 30_000
|
|
|
49
49
|
const SPAWN_THREAD = {
|
|
50
50
|
name: "spawn_thread",
|
|
51
51
|
description:
|
|
52
|
+
"LAST RESORT — try the two cheaper exits FIRST. Follow-up work you discovered is not a reason to spawn: " +
|
|
53
|
+
"if you could DO it (dispatching an in-session sub-agent, whose result comes back to you, so the work " +
|
|
54
|
+
"lands on YOUR card under one review), do that instead; if the human should choose, ASK instead. " +
|
|
52
55
|
"Spawn a brand-new, separate top-level frizz thread — its own board card, session, and scratchpad, " +
|
|
53
56
|
"driving INDEPENDENTLY. This is FIRE-AND-FORGET: the new thread reports to the HUMAN on the board via " +
|
|
54
57
|
"its own final message, and its results NEVER come back to you, the caller. It is NOT an in-session " +
|
|
@@ -60,6 +63,11 @@ const SPAWN_THREAD = {
|
|
|
60
63
|
"those are in-session sub-agents (Claude: the Agent tool with `run_in_background`; Codex: native " +
|
|
61
64
|
"delegation), which return their findings to you. Spawning such a helper here STRANDS it — its work lands " +
|
|
62
65
|
"on another card and never reaches you, so you gain nothing. " +
|
|
66
|
+
"Because nothing it learns ever returns to you OR to its siblings, a chain of spawned threads re-derives " +
|
|
67
|
+
"the same facts in parallel and nobody notices — measured here: one thread spawned four, three of those " +
|
|
68
|
+
"spawned more, and three descendants independently rediscovered the same root cause over twenty hours. " +
|
|
69
|
+
"Spawn only when the work genuinely cannot ride on your own card: a different repo, a different long-lived " +
|
|
70
|
+
"runtime, an effort that must outlive yours. Never spawn merely to clear your own `done` fence. " +
|
|
63
71
|
"You MUST deliberately choose `model` and `effort` to match the NEW thread's task complexity — they are " +
|
|
64
72
|
"required, there is NO default. Do not reflexively pick the cheapest; a hard task on a weak model/effort " +
|
|
65
73
|
"wastes the whole thread.",
|
|
@@ -179,16 +187,6 @@ const RECURRING_PROMPT = {
|
|
|
179
187
|
"summarized, and make the prompt LINK the doc you are keeping in your scratch directory — that " +
|
|
180
188
|
"link arriving in the emptied window is what lets you pick the work back up.",
|
|
181
189
|
},
|
|
182
|
-
pause_on_questions: {
|
|
183
|
-
type: "boolean",
|
|
184
|
-
description:
|
|
185
|
-
"Send NOTHING — on any trigger — for as long as you are waiting on the human: an unanswered " +
|
|
186
|
-
"```question fence, a native ask, or a permission prompt. DEFAULTS TO TRUE, matching the " +
|
|
187
|
-
"thread footer, because being told \"keep going\" while you are holding a question up is the " +
|
|
188
|
-
"one delivery that can only make things worse. Pass false only if you genuinely want a beat " +
|
|
189
|
-
"to reach you mid-question. (The stop hook declines a rest that ends in a question fence " +
|
|
190
|
-
"always, whatever this says; this is the wider version and it covers the other triggers.)",
|
|
191
|
-
},
|
|
192
190
|
},
|
|
193
191
|
required: ["action"],
|
|
194
192
|
},
|
|
@@ -259,32 +257,28 @@ const TIMER = {
|
|
|
259
257
|
},
|
|
260
258
|
}
|
|
261
259
|
|
|
262
|
-
// The blocking mode's bounds. The floor is a poll cycle — below it the call is not a wait, it is a
|
|
263
|
-
// round-trip — and the ceiling matches the worker's own foreground Bash ceiling, so one number governs
|
|
264
|
-
// "the longest a worker may block" wherever it blocks.
|
|
265
|
-
const WATCH_MIN_WAIT_SECONDS = 5
|
|
266
|
-
const WATCH_MAX_WAIT_SECONDS = 24 * 60 * 60
|
|
267
260
|
|
|
268
|
-
const
|
|
269
|
-
name: "
|
|
261
|
+
const WATCH_PR = {
|
|
262
|
+
name: "watch_pr",
|
|
270
263
|
description:
|
|
271
|
-
"REGISTER
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
"
|
|
264
|
+
"REGISTER A PULL REQUEST and frizz brings you back whenever something happens on it — CI turning " +
|
|
265
|
+
"green or red, and every later review, approval or comment, from a human or a bot alike. Register " +
|
|
266
|
+
"it, come to rest, and you are woken. Drop it when it stops mattering.\n\n" +
|
|
267
|
+
"IT REPORTS REPEATEDLY, unlike a timer. One registration covers the whole life of the PR: CI goes " +
|
|
268
|
+
"red, you push a fix, CI goes green, a reviewer comments — that is four wakes from one call, and you " +
|
|
269
|
+
"never have to re-register between them. It settles itself when the PR merges or closes, because " +
|
|
270
|
+
"there is then nothing left to report.\n\n" +
|
|
271
|
+
"REGISTER IT THE MOMENT YOU OPEN OR PUSH A PR. Nothing else watches for you: your runtime knows " +
|
|
272
|
+
"nothing about GitHub, and an ```awaiting fence STATES what you are waiting on without creating any " +
|
|
273
|
+
"wait at all. This tool is the wait.\n\n" +
|
|
274
|
+
"THE ```awaiting FENCE IS STILL WORTH WRITING, and it is a different job: it is how you come to REST " +
|
|
275
|
+
"without frizz asking you for a handoff, and how the human sees what you are waiting for. Register " +
|
|
276
|
+
"the watcher with this tool, then name the same PR on a `pr-watch:` line in your fence.\n\n" +
|
|
277
|
+
"REGISTERING IS IDEMPOTENT per pull request: asking twice returns the SAME id and tells you it was " +
|
|
278
|
+
"already armed, so re-registering after a compaction is safe and is the right instinct. Use `list` " +
|
|
279
|
+
"when you want to know what you are holding without changing anything — it answers with each PR's " +
|
|
280
|
+
"current check state too.\n\n" +
|
|
281
|
+
"You can only ever watch a PR on your OWN thread — there is no parameter for anyone else's.",
|
|
288
282
|
inputSchema: {
|
|
289
283
|
type: "object",
|
|
290
284
|
properties: {
|
|
@@ -292,46 +286,29 @@ const WATCH = {
|
|
|
292
286
|
type: "string",
|
|
293
287
|
enum: ["add", "list", "drop"],
|
|
294
288
|
description:
|
|
295
|
-
"`add` registers a watcher (idempotent
|
|
296
|
-
"
|
|
297
|
-
"answers with the full armed set
|
|
289
|
+
"`add` registers a watcher (idempotent per PR); `drop` withdraws one by id; `list` reads back " +
|
|
290
|
+
"everything armed on this thread, with each PR's latest check state, without changing " +
|
|
291
|
+
"anything. Every action answers with the full armed set.",
|
|
298
292
|
},
|
|
299
|
-
|
|
293
|
+
target: {
|
|
300
294
|
type: "string",
|
|
301
|
-
enum: ["shell"],
|
|
302
295
|
description:
|
|
303
|
-
"Required for `add`. The
|
|
296
|
+
"Required for `add`. The pull request, as `owner/repo#123` or a GitHub PR URL. A ref that " +
|
|
297
|
+
"cannot be parsed is REFUSED rather than stored — a watcher that can never fire is worse than " +
|
|
298
|
+
"no watcher, because you would come to rest believing you were covered.",
|
|
304
299
|
},
|
|
305
|
-
|
|
300
|
+
for: {
|
|
306
301
|
type: "string",
|
|
307
302
|
description:
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
"
|
|
303
|
+
"REQUIRED for `add`. How long to watch, as a DURATION — `30m`, `2h`, `3d` (max 24h). Never an " +
|
|
304
|
+
"instant, and there is no default. A PR nobody ever reviews would otherwise be polled forever " +
|
|
305
|
+
"and hold your thread with it; the watcher settles itself when this runs out and tells you, " +
|
|
306
|
+
"and you re-register if you still care.",
|
|
312
307
|
},
|
|
313
308
|
id: {
|
|
314
309
|
type: "string",
|
|
315
310
|
description: "Required for `drop`. The watcher id returned by `add` (or listed by `list`).",
|
|
316
311
|
},
|
|
317
|
-
wait: {
|
|
318
|
-
type: "boolean",
|
|
319
|
-
description:
|
|
320
|
-
"For `add`: BLOCK here until it resolves, instead of returning immediately. Requires " +
|
|
321
|
-
"`timeout_seconds`. Use it when the wait is short enough that keeping the work in ONE turn is " +
|
|
322
|
-
"worth more than the durability of resting — you keep your context and your place in the " +
|
|
323
|
-
"reasoning, and no wake message interrupts you. For anything long, register it WITHOUT this " +
|
|
324
|
-
"and rest: a background registration survives your turn ending, a compaction and a frizz " +
|
|
325
|
-
"restart, none of which a blocking call survives.",
|
|
326
|
-
},
|
|
327
|
-
timeout_seconds: {
|
|
328
|
-
type: "integer",
|
|
329
|
-
description:
|
|
330
|
-
`REQUIRED when \`wait\` is true (minimum ${WATCH_MIN_WAIT_SECONDS}, maximum ` +
|
|
331
|
-
`${WATCH_MAX_WAIT_SECONDS}). When it expires the call RETURNS rather than failing, and the ` +
|
|
332
|
-
"watcher is handed to frizz to keep — so the wait is never lost by choosing to block on it. " +
|
|
333
|
-
"You are then free to do something else and be woken.",
|
|
334
|
-
},
|
|
335
312
|
},
|
|
336
313
|
required: ["action"],
|
|
337
314
|
},
|
|
@@ -344,14 +321,53 @@ const WATCH = {
|
|
|
344
321
|
const MIN_INTERVAL_SECONDS = 60
|
|
345
322
|
const MAX_INTERVAL_SECONDS = 24 * 60 * 60
|
|
346
323
|
|
|
347
|
-
const
|
|
324
|
+
const ACTIVITY = {
|
|
325
|
+
name: "activity",
|
|
326
|
+
description:
|
|
327
|
+
"EVERYTHING YOU CURRENTLY HAVE RUNNING, with the id each one is named by — your background shells, " +
|
|
328
|
+
"your sub-agents, your armed timers, and the pull requests you registered.\n\n" +
|
|
329
|
+
"WHY YOU NEED IT: an ```awaiting fence names what you are waiting on BY ID, and frizz checks every " +
|
|
330
|
+
"one against what is actually live. A name that matches nothing is not a park — you are bumped and " +
|
|
331
|
+
"your thread queues. So if you have lost an id (a compaction, a long turn, a wake you did not " +
|
|
332
|
+
"expect), call this rather than guessing. Guessing is the failure this tool exists to remove.\n\n" +
|
|
333
|
+
"It takes nothing and changes nothing. You can only ever read your OWN thread.",
|
|
334
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const TOOLS = [SPAWN_THREAD, RECURRING_PROMPT, TIMER, WATCH_PR, ACTIVITY]
|
|
348
338
|
|
|
349
339
|
/** @type {Record<string, (args: Record<string, unknown>) => Promise<string>>} */
|
|
350
340
|
const HANDLERS = {
|
|
351
341
|
[SPAWN_THREAD.name]: spawnThread,
|
|
352
342
|
[RECURRING_PROMPT.name]: recurringPrompt,
|
|
353
343
|
[TIMER.name]: timer,
|
|
354
|
-
[
|
|
344
|
+
[WATCH_PR.name]: watchPr,
|
|
345
|
+
[ACTIVITY.name]: activity,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Read out every background thing this thread has running, in the shape an awaiting fence names them.
|
|
349
|
+
* @returns {Promise<string>} */
|
|
350
|
+
async function activity() {
|
|
351
|
+
const result = (await callRpc("listOwnThreadActivity", { slug: threadSlug() }))?.result
|
|
352
|
+
const items = Array.isArray(result?.activity) ? result.activity : []
|
|
353
|
+
if (!items.length) {
|
|
354
|
+
return (
|
|
355
|
+
"Nothing is running on this thread — no background shells, no sub-agents, no armed timers, no " +
|
|
356
|
+
"registered PRs.\n\nSo there is nothing to wait on: an ```awaiting fence would have nothing to " +
|
|
357
|
+
"name, and a fence naming nothing is not a park. End with ```done, or with a ```question if you " +
|
|
358
|
+
"need the human."
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
const lines = items.map((i) => {
|
|
362
|
+
const when = i.until ? ` (fires ${i.until})` : i.since ? ` (since ${i.since})` : ""
|
|
363
|
+
return ` ${i.kind}: ${i.id}${when}\n ${i.label}`
|
|
364
|
+
})
|
|
365
|
+
return (
|
|
366
|
+
`${items.length} thing${items.length === 1 ? "" : "s"} running on this thread:\n\n${lines.join("\n")}\n\n` +
|
|
367
|
+
"Name the ones you are ACTUALLY waiting on in your ```awaiting fence, one `<kind>: <id>` line each, " +
|
|
368
|
+
"plus a required `for:` duration and a one-line `reason:`. Do not name something you are not waiting " +
|
|
369
|
+
"on — a dev server you left running is not a wait."
|
|
370
|
+
)
|
|
355
371
|
}
|
|
356
372
|
|
|
357
373
|
/** @param {unknown} obj */
|
|
@@ -446,10 +462,17 @@ function serverLockPort() {
|
|
|
446
462
|
}
|
|
447
463
|
}
|
|
448
464
|
if (candidates.length === 0) throw new Error("FRIZZ_STATE_DIR / FRIZZ_SERVER_LOCK not set — cannot locate the frizz server")
|
|
465
|
+
// SAY THAT NOTHING WAS SAVED, and say to retry. A worker reads "is frizz running?" as a fact about the
|
|
466
|
+
// world rather than as a fact about ITS OWN call, and moves on — so whatever it was arming is silently
|
|
467
|
+
// gone. Measured 2026-08-17: a worker's `recurring_prompt start` hit a restart window, got this error,
|
|
468
|
+
// carried on, and its Goal — the thing keeping a long autonomous effort alive — never existed. The
|
|
469
|
+
// window is ordinary (frizz restarts, and this process outlives every one of them), so the recovery has
|
|
470
|
+
// to be ordinary too: try again.
|
|
449
471
|
throw new Error(
|
|
450
472
|
`no running frizz server found (looked at ${candidates.join(", ")} and every project lock under ` +
|
|
451
473
|
`${root ? join(root, "projects") : "the frizz root"}; each was missing, malformed, or written by a process that is gone). ` +
|
|
452
|
-
`
|
|
474
|
+
`NOTHING WAS SAVED — this call had no effect. frizz is probably mid-restart, which is ordinary and ` +
|
|
475
|
+
`brief; RETRY this exact call before you do anything else, and do not come to rest assuming it took.`,
|
|
453
476
|
)
|
|
454
477
|
}
|
|
455
478
|
|
|
@@ -550,8 +573,35 @@ async function spawnThread(args) {
|
|
|
550
573
|
/** POST a frizz RPC procedure and return its parsed payload. Shares spawn_thread's transport rules:
|
|
551
574
|
* the port comes from server.lock and `sec-fetch-site: same-origin` satisfies the loopback gate.
|
|
552
575
|
* @param {string} procedure @param {Record<string, unknown>} body @returns {Promise<any>} */
|
|
576
|
+
// HOW LONG A RESTART WINDOW IS ALLOWED TO BE INVISIBLE. frizz replaces its own server routinely
|
|
577
|
+
// ("Update & Restart", a dev rebuild), and this process is deliberately still here across every one of
|
|
578
|
+
// them — so a call landing in that gap is ORDINARY, and failing it is the shim reporting frizz's
|
|
579
|
+
// housekeeping as the worker's problem. Measured 2026-08-17: a `recurring_prompt start` landed in one,
|
|
580
|
+
// failed, and the Goal that was keeping a long autonomous effort alive silently never existed.
|
|
581
|
+
//
|
|
582
|
+
// Telling the model to retry (which the error also does) is strictly weaker than retrying, because it
|
|
583
|
+
// only works if the model complies. Bounded and short: a genuinely-down frizz still fails, promptly,
|
|
584
|
+
// with the same message — this only covers the seconds where a new server is coming up.
|
|
585
|
+
const LOCK_RETRY_MS = 6_000
|
|
586
|
+
const LOCK_RETRY_INTERVAL_MS = 400
|
|
587
|
+
|
|
588
|
+
/** The port, waiting out a brief restart window rather than failing into one. Rethrows the real
|
|
589
|
+
* "no running frizz server" error once the budget is spent, so a frizz that is actually down still
|
|
590
|
+
* says so — and says it with the retry guidance attached. */
|
|
591
|
+
async function serverLockPortWaiting() {
|
|
592
|
+
const deadline = Date.now() + LOCK_RETRY_MS
|
|
593
|
+
for (;;) {
|
|
594
|
+
try {
|
|
595
|
+
return serverLockPort()
|
|
596
|
+
} catch (err) {
|
|
597
|
+
if (Date.now() >= deadline) throw err
|
|
598
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_INTERVAL_MS))
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
553
603
|
async function callRpc(procedure, body) {
|
|
554
|
-
const port =
|
|
604
|
+
const port = await serverLockPortWaiting()
|
|
555
605
|
const controller = new AbortController()
|
|
556
606
|
const timer = setTimeout(() => controller.abort(), DISPATCH_TIMEOUT_MS)
|
|
557
607
|
let res
|
|
@@ -604,7 +654,7 @@ function cadenceLabel(seconds) {
|
|
|
604
654
|
* each last fired, and the text VERBATIM (never truncated — reading back a summary of your own
|
|
605
655
|
* instruction is exactly as blind as not reading it).
|
|
606
656
|
* @param {{ prompt: string, stopHook: boolean, heartbeat: boolean, postCompaction: boolean,
|
|
607
|
-
*
|
|
657
|
+
* intervalSeconds?: number, armedAt: string, lastRestFiredAt?: string,
|
|
608
658
|
* lastScheduleFiredAt?: string, lastCompactFiredAt?: string }} rp */
|
|
609
659
|
function recurringPromptReport(rp) {
|
|
610
660
|
const fired = (/** @type {string|undefined} */ at) => (at ? `last fired ${at}` : "never fired yet")
|
|
@@ -622,13 +672,7 @@ function recurringPromptReport(rp) {
|
|
|
622
672
|
const head = triggers.length
|
|
623
673
|
? `Armed since ${rp.armedAt}, on:\n${triggers.join("\n")}`
|
|
624
674
|
: `Text is parked (armed ${rp.armedAt}) but EVERY TRIGGER IS OFF — nothing will fire until one is switched back on.`
|
|
625
|
-
|
|
626
|
-
// in it, so a worker reading this must already know what would fire before it is told what suspends it.
|
|
627
|
-
const hold = rp.pauseOnQuestions
|
|
628
|
-
? "\n\nHELD while you are waiting on the human — nothing is sent for as long as a question fence, a " +
|
|
629
|
-
"native ask or a permission prompt is unanswered."
|
|
630
|
-
: ""
|
|
631
|
-
return `${head}${hold}\n\nThe text, verbatim:\n\n${rp.prompt}`
|
|
675
|
+
return `${head}\n\nThe text, verbatim:\n\n${rp.prompt}`
|
|
632
676
|
}
|
|
633
677
|
|
|
634
678
|
/** The `recurring_prompt` handler: arm, disarm, or READ BACK this thread's re-prompt.
|
|
@@ -663,7 +707,7 @@ async function recurringPrompt(args) {
|
|
|
663
707
|
}
|
|
664
708
|
|
|
665
709
|
if (action === "stop") {
|
|
666
|
-
await callRpc("setOwnThreadRecurringPrompt", { slug, prompt: null, stopHook: false, heartbeat: false, postCompaction: false
|
|
710
|
+
await callRpc("setOwnThreadRecurringPrompt", { slug, prompt: null, stopHook: false, heartbeat: false, postCompaction: false })
|
|
667
711
|
return "Recurring prompt disarmed and cleared. No trigger will fire — not the stop hook, not the heartbeat, not the post-compaction one — and the text is gone from the thread footer."
|
|
668
712
|
}
|
|
669
713
|
|
|
@@ -682,9 +726,6 @@ async function recurringPrompt(args) {
|
|
|
682
726
|
}
|
|
683
727
|
}
|
|
684
728
|
const postCompaction = args.post_compaction === true
|
|
685
|
-
// ON unless the caller says otherwise — the same default the footer panel seeds, so a worker-armed row
|
|
686
|
-
// and a human-armed one read identically in the panel. An older frizz server ignores the field.
|
|
687
|
-
const pauseOnQuestions = typeof args.pause_on_questions === "boolean" ? args.pause_on_questions : true
|
|
688
729
|
// DEFAULTED, not required: a `start` that names no trigger at all is a model asking to be re-prompted
|
|
689
730
|
// and leaving the mechanism to us, and the rest trigger is the safe reading of that — it cannot talk
|
|
690
731
|
// over a running turn, and it cannot fire on a thread that has stopped needing it.
|
|
@@ -700,7 +741,6 @@ async function recurringPrompt(args) {
|
|
|
700
741
|
stopHook,
|
|
701
742
|
heartbeat,
|
|
702
743
|
postCompaction,
|
|
703
|
-
pauseOnQuestions,
|
|
704
744
|
...(heartbeat ? { intervalSeconds: interval } : {}),
|
|
705
745
|
})
|
|
706
746
|
// `replaced` is absent against a server that predates it, which is indistinguishable from "there was
|
|
@@ -725,12 +765,12 @@ async function recurringPrompt(args) {
|
|
|
725
765
|
? `\n\nIT REPLACED an existing recurring prompt — check that discarding it was intended, and restore ` +
|
|
726
766
|
`it with another \`start\` if it was not:\n\n${recurringPromptReport(replaced)}\n`
|
|
727
767
|
: ""
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
768
|
+
// NO QUESTION HOLD ANY MORE (2026-08-16). Every trigger fires while you are waiting on the human, and
|
|
769
|
+
// the at-rest one fires over your own unanswered ```question fence — the delivery says so, and expects
|
|
770
|
+
// you to decide the question yourself rather than re-ask it. A ```done fence, and an ```awaiting on a
|
|
771
|
+
// wait frizz itself will deliver, still stop the at-rest trigger.
|
|
732
772
|
return (
|
|
733
|
-
`Recurring prompt armed — frizz will send you this ${when}.${
|
|
773
|
+
`Recurring prompt armed — frizz will send you this ${when}.${superseded}\n\n` +
|
|
734
774
|
"Call this tool again with `action: \"stop\"` once the work it drives is finished — one left armed on " +
|
|
735
775
|
"a finished thread wakes it forever. The human can also edit or switch it off in the thread footer. " +
|
|
736
776
|
"Signing off with a ```done fence stops it too, but only when there is genuinely nothing left: it " +
|
|
@@ -816,18 +856,32 @@ async function timer(args) {
|
|
|
816
856
|
)
|
|
817
857
|
}
|
|
818
858
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
859
|
+
|
|
860
|
+
/** How the armed PR-watcher set reads back, on every action, so a worker never needs a second call.
|
|
861
|
+
* @param {{ watches?: Array<{id: string, target: string, github?: {checks: string, running: number, passed: number, failed: number, failing: string[], merge: string, state: string}}> }|undefined} result */
|
|
862
|
+
function armedPrWatchList(result) {
|
|
822
863
|
const watches = Array.isArray(result?.watches) ? result.watches : []
|
|
823
|
-
if (!watches.length) return "
|
|
824
|
-
const lines = watches.map((w) =>
|
|
825
|
-
|
|
864
|
+
if (!watches.length) return "No pull requests are watched on this thread — nothing will wake you."
|
|
865
|
+
const lines = watches.map((w) => {
|
|
866
|
+
const g = w.github
|
|
867
|
+
// The CHECK STATE rides the read-back because it is the reason a worker is listing at all: "where do
|
|
868
|
+
// my PRs stand" is one call, not one per PR through `gh`.
|
|
869
|
+
const state = !g
|
|
870
|
+
? "not polled yet"
|
|
871
|
+
: g.state !== "open"
|
|
872
|
+
? g.state
|
|
873
|
+
: g.checks === "passing" ? `checks green (${g.passed})`
|
|
874
|
+
: g.checks === "failing" ? `checks FAILING${g.failing.length ? `: ${g.failing.join(", ")}` : ""}`
|
|
875
|
+
: g.checks === "running" ? `checks running (${g.running} left)`
|
|
876
|
+
: "no checks"
|
|
877
|
+
return ` ${w.id} ${w.target} — ${state}${g && g.state === "open" && g.merge === "mergeable" ? ", mergeable" : ""}`
|
|
878
|
+
})
|
|
879
|
+
return `Watched on this thread now:\n${lines.join("\n")}`
|
|
826
880
|
}
|
|
827
881
|
|
|
828
|
-
/** The `
|
|
882
|
+
/** The `watch_pr` handler: register, withdraw, or read back this thread's PR watchers.
|
|
829
883
|
* @param {Record<string, unknown>} args @returns {Promise<string>} */
|
|
830
|
-
async function
|
|
884
|
+
async function watchPr(args) {
|
|
831
885
|
const slug = threadSlug()
|
|
832
886
|
const action = typeof args.action === "string" ? args.action.trim() : ""
|
|
833
887
|
if (action !== "add" && action !== "list" && action !== "drop") {
|
|
@@ -835,103 +889,34 @@ async function watch(args) {
|
|
|
835
889
|
}
|
|
836
890
|
|
|
837
891
|
if (action === "list") {
|
|
838
|
-
|
|
839
|
-
return armedWatchList(result)
|
|
892
|
+
return armedPrWatchList((await callRpc("listOwnPrWatches", { slug }))?.result)
|
|
840
893
|
}
|
|
841
894
|
|
|
842
895
|
if (action === "drop") {
|
|
843
896
|
const id = typeof args.id === "string" ? args.id.trim() : ""
|
|
844
897
|
if (!id) throw new Error("`id` is required to drop a watcher — take it from `add` or from `list`")
|
|
845
|
-
const result = (await callRpc("
|
|
898
|
+
const result = (await callRpc("dropOwnPrWatch", { slug, id }))?.result
|
|
846
899
|
// A drop that matched nothing is reported rather than swallowed: the id was wrong, already settled,
|
|
847
900
|
// or another thread's — and a worker that believes it withdrew a wait it still holds will rest.
|
|
848
901
|
const head = result?.dropped
|
|
849
902
|
? `Watcher ${id} dropped. It will not wake you.`
|
|
850
903
|
: `No ARMED watcher ${id} on this thread — it was already settled, or the id is not one of yours.`
|
|
851
|
-
return `${head}\n\n${
|
|
904
|
+
return `${head}\n\n${armedPrWatchList(result)}`
|
|
852
905
|
}
|
|
853
906
|
|
|
854
|
-
const kind = typeof args.kind === "string" ? args.kind.trim() : ""
|
|
855
|
-
if (kind !== "shell") {
|
|
856
|
-
// REFUSED rather than stored. `pr` and `ci` rows are valid in the registry and the scheduler will
|
|
857
|
-
// poll them once its PR watcher moves off the fence — but today nothing wakes them, and a tool that
|
|
858
|
-
// accepts a wait it cannot honour is how a worker comes to rest believing it is covered.
|
|
859
|
-
throw new Error(
|
|
860
|
-
kind === "pr" || kind === "ci"
|
|
861
|
-
? `a ${kind} wait does not belong here — use an \`\`\`awaiting fence with a \`pr-watch: owner/repo#123\` line, which is durable and replays review that is already on the PR`
|
|
862
|
-
: "`kind` is required to add a watcher, and the only kind is \"shell\"",
|
|
863
|
-
)
|
|
864
|
-
}
|
|
865
907
|
const target = typeof args.target === "string" ? args.target.trim() : ""
|
|
866
|
-
if (!target) throw new Error("`target` is required — the
|
|
867
|
-
|
|
868
|
-
// THE BLOCKING MODE. `foreground` tells frizz to SETTLE this watcher silently rather than wake us —
|
|
869
|
-
// we are the ones waiting, and a wake landing mid-turn while this call is still blocked would hand the
|
|
870
|
-
// worker its own answer twice.
|
|
871
|
-
const wait = args.wait === true
|
|
872
|
-
let timeoutSeconds = 0
|
|
873
|
-
if (wait) {
|
|
874
|
-
if (typeof args.timeout_seconds !== "number" || !Number.isFinite(args.timeout_seconds)) {
|
|
875
|
-
throw new Error("`timeout_seconds` is required when `wait` is true — a blocking wait with no deadline is a hang")
|
|
876
|
-
}
|
|
877
|
-
timeoutSeconds = Math.round(args.timeout_seconds)
|
|
878
|
-
if (timeoutSeconds < WATCH_MIN_WAIT_SECONDS || timeoutSeconds > WATCH_MAX_WAIT_SECONDS) {
|
|
879
|
-
throw new Error(`\`timeout_seconds\` must be between ${WATCH_MIN_WAIT_SECONDS} and ${WATCH_MAX_WAIT_SECONDS}`)
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const result = (await callRpc("addOwnThreadWatch", { slug, kind, target, ...(wait ? { foreground: true } : {}) }))?.result
|
|
908
|
+
if (!target) throw new Error("`target` is required — the pull request, as `owner/repo#123` or a PR URL")
|
|
909
|
+
const result = (await callRpc("addOwnPrWatch", { slug, target, for: typeof args.for === "string" ? args.for.trim() : "" }))?.result
|
|
884
910
|
const id = result?.id ?? "(unknown)"
|
|
885
|
-
|
|
911
|
+
const ref = result?.target ?? target
|
|
886
912
|
const head = result?.alreadyArmed
|
|
887
|
-
? `Already watching ${
|
|
888
|
-
: `Watching ${
|
|
889
|
-
"survives your turn ending, a compaction and a frizz restart."
|
|
890
|
-
return (
|
|
891
|
-
`${head}\n\nDROP IT when it stops mattering (\`action: "drop", id: "${id}"\`) — a watcher you no ` +
|
|
892
|
-
`longer care about is a wake you did not want.\n\n${armedWatchList(result)}`
|
|
893
|
-
)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/** Block until a foreground watcher settles, or until its deadline — then hand it back to frizz.
|
|
897
|
-
*
|
|
898
|
-
* POLLED, not pushed, because the MCP transport has no way to be told. The interval BACKS OFF: a wait
|
|
899
|
-
* that resolves in ten seconds should not be found thirty seconds late, and a wait that runs for hours
|
|
900
|
-
* should not cost thousands of round-trips to discover that nothing changed.
|
|
901
|
-
*
|
|
902
|
-
* The deadline RETURNS rather than throwing, and promotes the row on the way out. That is the property
|
|
903
|
-
* that makes choosing this mode safe: the worst case of guessing the timeout too short is that the wait
|
|
904
|
-
* becomes an ordinary durable one and frizz wakes you, not that the wait is silently lost.
|
|
905
|
-
*
|
|
906
|
-
* @param {string} slug @param {string} id @param {string} kind @param {string} target @param {number} timeoutSeconds
|
|
907
|
-
* @returns {Promise<string>} */
|
|
908
|
-
async function blockUntilResolved(slug, id, kind, target, timeoutSeconds) {
|
|
909
|
-
const deadline = Date.now() + timeoutSeconds * 1000
|
|
910
|
-
const started = Date.now()
|
|
911
|
-
for (;;) {
|
|
912
|
-
const elapsed = Date.now() - started
|
|
913
|
-
// 2s for the first minute, then 5s, then 15s — see the back-off note above.
|
|
914
|
-
const interval = elapsed < 60_000 ? 2_000 : elapsed < 600_000 ? 5_000 : 15_000
|
|
915
|
-
const remaining = deadline - Date.now()
|
|
916
|
-
if (remaining <= 0) break
|
|
917
|
-
await new Promise((r) => setTimeout(r, Math.min(interval, remaining)))
|
|
918
|
-
const listed = (await callRpc("listOwnThreadWatches", { slug }))?.result
|
|
919
|
-
const still = Array.isArray(listed?.watches) && listed.watches.some((w) => w.id === id)
|
|
920
|
-
if (!still) {
|
|
921
|
-
const waited = Math.round((Date.now() - started) / 1000)
|
|
922
|
-
return (
|
|
923
|
-
`${kind === "shell" ? "Your background shell" : target} resolved after ${waited}s — that is what you were ` +
|
|
924
|
-
`waiting for (${target}). The watcher is spent; you were not interrupted, because you were the one waiting.`
|
|
925
|
-
)
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
// The deadline, not a failure. Hand it to frizz so the wait survives this turn.
|
|
929
|
-
const promoted = (await callRpc("promoteOwnThreadWatch", { slug, id }))?.result
|
|
913
|
+
? `Already watching ${ref} as ${id} — nothing new was registered, and you will be woken once per event.`
|
|
914
|
+
: `Watching ${ref} as ${id}. Frizz wakes you when CI passes or fails and on every later review or ` +
|
|
915
|
+
"comment, and the registration survives your turn ending, a compaction and a frizz restart."
|
|
930
916
|
return (
|
|
931
|
-
|
|
932
|
-
`
|
|
933
|
-
`
|
|
934
|
-
"mattering."
|
|
917
|
+
`${head}\n\nNAME IT IN YOUR \`\`\`awaiting FENCE TOO (\`pr-watch: ${ref}\`) — the watcher does the ` +
|
|
918
|
+
`waking, the fence is what lets you come to rest and shows the human what you are waiting for.\n\n` +
|
|
919
|
+
`DROP IT when it stops mattering (\`action: "drop", id: "${id}"\`).\n\n${armedPrWatchList(result)}`
|
|
935
920
|
)
|
|
936
921
|
}
|
|
937
922
|
|
|
@@ -67,7 +67,7 @@ const core =
|
|
|
67
67
|
'⟦frizz worker contract⟧ You are a frizz WORKER driving EXACTLY ONE effort. Your FULL operating contract — the end-of-turn signal fences, scratch-directory rules, sub-agent rules, and the question handback — lives in your SYSTEM PROMPT; follow it there (this is a runtime re-grounding, not a second copy). The human + the frizz app are the ORCHESTRATOR; you drive ONE effort and never scan the board or touch other efforts. There is no orchestrator mode and no fleet to run: doing the work yourself is the default, and you dispatch a sub-agent only when the work genuinely decomposes into independent prongs.\n' +
|
|
68
68
|
'SCRATCH DIRECTORY (OPTIONAL): `' + scratch + '` — a folder kept FOR YOU: any files you like, no format expected, nothing in it read automatically, and never a substitute for doing the work. A single direct task usually needs nothing. On a long effort write the doc you would want if you lost your context — the approach, what you rejected, the human\'s decisions — AS YOU GO, mid-work, then KEEP WORKING, and arm mcp__frizz__recurring_prompt with post_compaction: true and a prompt LINKING that file, because the arming is what brings it back. Give each sub-agent its OWN file rather than a shared one.\n' +
|
|
69
69
|
'DO NOT REST WHILE THE INSTRUCTION HAS PARTS LEFT — finish them in THIS turn; a milestone, a green test run and a long turn are none of them stopping points, and announcing the next step or writing it into a scratch file is not doing it.\n' +
|
|
70
|
-
'ALWAYS SIGN OFF WITH A FENCE, per the fence rules in your system prompt. A rest with NO fence is not a handoff — it is an item nobody can triage, and frizz will tell you so. ```done only when the effort\'s real work is COMPLETE (code LANDED on the mainline — an open PR is NOT done, park it on ```awaiting until it MERGES) and is a DISMISSAL (its card files the thread away where nobody looks again), so if the thread points at future work AT ALL — a pre-fix investigation, a live code-change discussion — ask a ```question instead, and uncertain is not done; the ONE exception is a planning session whose plan file is fully written and persisted, because that artifact outlives the thread; ```awaiting parks
|
|
70
|
+
'ALWAYS SIGN OFF WITH A FENCE, per the fence rules in your system prompt. A rest with NO fence is not a handoff — it is an item nobody can triage, and frizz will tell you so. ```done only when the effort\'s real work is COMPLETE (code LANDED on the mainline — an open PR is NOT done, park it on ```awaiting until it MERGES) and is a DISMISSAL (its card files the thread away where nobody looks again), so if the thread points at future work AT ALL — a pre-fix investigation, a live code-change discussion, follow-up work you DISCOVERED even when someone else will do it — ask a ```question instead, and uncertain is not done; a verdict that ends in something the HUMAN must now do (post this comment, merge or decline, pick one of these) is a ```question carrying your recommendation as option A, never a done card, because a draft you wrote but did not send is filed away with the thread; when your OWN mandate is complete and what is left is a separable effort, DO it first — dispatch a sub-agent, whose result comes BACK to you, so it lands on YOUR card — and ASK second; mcp__frizz__spawn_thread is the LAST resort, for work that genuinely cannot ride on your card, because a spawned thread reports only to the human and nothing it learns returns to you or its siblings; the ONE exception is a planning session whose plan file is fully written and persisted, because that artifact outlives the thread; ```awaiting parks a human:/timer:/pr-watch: gate or your OWN background work, never CI/releases/merge progression (those stay ACTIVE); ```question is the operator ask. Waiting on your own background shell? Your shells and sub-agents are watched AUTOMATICALLY — frizz wakes you when one finishes, fence or no fence. NAME it on a `watch: <id>` line anyway and rest: that is what lets you stop without frizz asking you for a handoff, and what shows the human what you are waiting for. Frizz checks the name against what you actually have running. Keep the write-up SHORT: 1-3 sentences, then bullets starting with a **bolded verb phrase**. The fence and the prose above it are TWO SURFACES, not one message written twice — the card is the ledger of what shipped, the prose is only what a ledger cannot hold, and a sentence that would read the same in either belongs in exactly one. And a done message says what HAPPENED: delete every dangling "one thing to carry forward" or "a follow-up could" — do it, spawn it onto its own card, ask about it, or DROP it, and if it is not worth a card it is not worth a sentence.\n' +
|
|
71
71
|
'DECIDE rather than ask: anything derivable from the code, the conventions, or ordinary engineering judgment is YOURS to settle — asking permission to do the work you were dispatched to do is not a question, it is the job. Reserve the operator for the irreversible and the genuinely human-owned.';
|
|
72
72
|
|
|
73
73
|
const grounding =
|