frizz 0.3.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 +9 -4
- package/dist/claude-agent-broker.js +9 -4
- package/dist/dev-child.js +1783 -527
- package/dist/frizz.js +709 -233
- package/package.json +5 -3
- package/runtime/cc-worker/DECISIONS.md +17 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +151 -174
- package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
- package/web-dist/assets/{TerminalPane-CTdetDJJ.js → TerminalPane-sd3XXjx5.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-BDKrjCMs.js → abnfDiagram-VRR7QNED-Bry_FIqg.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-CoMJQ5Ya.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-BJebpUUM.js → architectureDiagram-ZJ3FMSHR-Baok4OeI.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BNbuk25k.js → blockDiagram-677ZJIJ3-BgHIO9lI.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-lCfyotdU.js → c4Diagram-LMCZKHZV-rfj9Z17Z.js} +1 -1
- package/web-dist/assets/channel-D6uS1y7I.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-DP72SEkr.js → chunk-32BRIVSS-BwaHfK1Y.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-DxO3gN-p.js → chunk-52WLFC77-BmdQDZOA.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-BXFx9Vlr.js → chunk-C7G6YPKG-ByQFjoZe.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-CR3sHpPf.js → chunk-EX3LRPZG-DQFudBKr.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-CJ8L__oG.js → chunk-FWX5IMBZ-OW9qEPiq.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-BCl3JSWr.js → chunk-HOUHSVGY-BdKLF8nO.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-CZxFMci1.js → chunk-ICXQ74PX-C1roqKR1.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-Ck3_47dB.js → chunk-MOJQB5TN-Cx0u1ZDK.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-CVXjES-F.js → chunk-OGEWGWER-rxv9xtNR.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-JjrVgaR7.js → chunk-PUDLZKDR-BXWPPPPi.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-C5YMRAQ0.js → chunk-Q4XR5HBZ-CpqbD101.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-DK0mfUHU.js → chunk-V7JOEXUC-CP8_3qVt.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-BKBntpid.js → chunk-VAUOI2AC-Dl7xahpk.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-zgE1dG6U.js → chunk-VR4S4FIN-BKtMX64l.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-CZhh1IBq.js → chunk-WYO6CB5R-DmLOdJcu.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-CrNjIEem.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-Ca_BPfTG.js → cynefin-VYW2F7L2-DUYahBXb.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-CsINQf6g.js → cynefinDiagram-TSTJHNR4-Rm0i30nE.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-CE0DYMFy.js → dagre-VKFMJZFB-3uhsB-W3.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-BqTduEYE.js → diagram-FQU43EPY-BUvflNRQ.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-Cho718s6.js → diagram-G47NLZAW-DUluIZel.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-D0Z8t9UC.js → diagram-NH7WQ7WH-BBrwsbed.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-CBacyeDx.js → diagram-OA4YK3LP-DWnTQBw4.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-CozpXa4j.js → diagram-WEI45ONY-BjEavoC0.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-ba4NbQQo.js → ebnfDiagram-CCIWWBDH-CW4xZAZH.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-B3BciOYa.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-lPmrRit4.js → ganttDiagram-NO4QXBWP-lDdG5I_z.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-BjyclOq0.js → gitGraph-TEB2WS4Q-Bw87a_Se.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-pzeb4Yrw.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-Bwycegvf.js → info-DKCQHKI2-DGOv6Gx0.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-BCvRGj_5.js → infoDiagram-FWYZ7A6U-DL92kAzf.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js → ishikawaDiagram-FXEZZL3T-CFYd4OtU.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-C5ROwFio.js → journeyDiagram-5HDEW3XC-QHuYL5zq.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-YLPLkpeT.js → kanban-definition-HUTT4EX6-DwmMluQ8.js} +1 -1
- package/web-dist/assets/{line-KtkNqRgI.js → line-Je_PFFHm.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-D_FfqBe7.js → mermaid-parser.core-BeiGWsn3.js} +3 -3
- package/web-dist/assets/{mermaid.core-Ffv8anVf.js → mermaid.core-BP8JrW48.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-B2jj4vfm.js → mindmap-definition-LN4V7U3C-BsmjCIrM.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-BjmWHwra.js → packet-7NZHBO7P-D6ekYYGJ.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-Dq3iJDyq.js → pegDiagram-2B236MQR-CQc2Kt0u.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-jBbH1lv9.js → pie-RZYD4A2V-C_45fO8J.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-C3ETW9lq.js → pieDiagram-ENE6RG2P-Dg5z6iWt.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DItSmme7.js → quadrantDiagram-ABIIQ3AL-c7gZxuCX.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-6ey6crgP.js → radar-I7S5WNFK-D80XNJQq.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-Cii-Mn0E.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-M363ils_.js → railroadDiagram-RFXS5EU6-Db9K7bty.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-CTs2_V6T.js → requirementDiagram-TGXJPOKE-m1ipMrDe.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-QTLLcDPD.js → sankeyDiagram-HTMAVEWB-4Zz9ry0k.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js → sequenceDiagram-DBY2YBRQ-CZeGHw4X.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-BecB6roG.js → stateDiagram-2N3HPSRC-rLUVs1Pg.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-w7QDFEvx.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-COiYgS0w.js → swimlanes-5IMT3BWC-BAeBdscz.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-UhdgWpNZ.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-Ci22coeH.js → timeline-definition-FHXFAJF6-DL0eWMF-.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-4DW35czh.js → treeView-QDETBFTQ-gEtzLMA8.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-Dq_-Z6-Y.js → treemap-6X3UGDF4-CCym1Gz6.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-CHQSIEYq.js → vennDiagram-L72KCM5P-cF97-nb5.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-Bwlh7HCY.js → wardley-OPB4EBWU-DQJHCGbM.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-DctjuPYn.js → wardleyDiagram-EHGQE667-0CnHzXLX.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-ZoIGosv8.js → xychartDiagram-FW5EYKEG-9YDOp4m5.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.4.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
|
|
@@ -179,16 +179,6 @@ const RECURRING_PROMPT = {
|
|
|
179
179
|
"summarized, and make the prompt LINK the doc you are keeping in your scratch directory — that " +
|
|
180
180
|
"link arriving in the emptied window is what lets you pick the work back up.",
|
|
181
181
|
},
|
|
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
182
|
},
|
|
193
183
|
required: ["action"],
|
|
194
184
|
},
|
|
@@ -259,32 +249,28 @@ const TIMER = {
|
|
|
259
249
|
},
|
|
260
250
|
}
|
|
261
251
|
|
|
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
252
|
|
|
268
|
-
const
|
|
269
|
-
name: "
|
|
253
|
+
const WATCH_PR = {
|
|
254
|
+
name: "watch_pr",
|
|
270
255
|
description:
|
|
271
|
-
"REGISTER
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
"
|
|
256
|
+
"REGISTER A PULL REQUEST and frizz brings you back whenever something happens on it — CI turning " +
|
|
257
|
+
"green or red, and every later review, approval or comment, from a human or a bot alike. Register " +
|
|
258
|
+
"it, come to rest, and you are woken. Drop it when it stops mattering.\n\n" +
|
|
259
|
+
"IT REPORTS REPEATEDLY, unlike a timer. One registration covers the whole life of the PR: CI goes " +
|
|
260
|
+
"red, you push a fix, CI goes green, a reviewer comments — that is four wakes from one call, and you " +
|
|
261
|
+
"never have to re-register between them. It settles itself when the PR merges or closes, because " +
|
|
262
|
+
"there is then nothing left to report.\n\n" +
|
|
263
|
+
"REGISTER IT THE MOMENT YOU OPEN OR PUSH A PR. Nothing else watches for you: your runtime knows " +
|
|
264
|
+
"nothing about GitHub, and an ```awaiting fence STATES what you are waiting on without creating any " +
|
|
265
|
+
"wait at all. This tool is the wait.\n\n" +
|
|
266
|
+
"THE ```awaiting FENCE IS STILL WORTH WRITING, and it is a different job: it is how you come to REST " +
|
|
267
|
+
"without frizz asking you for a handoff, and how the human sees what you are waiting for. Register " +
|
|
268
|
+
"the watcher with this tool, then name the same PR on a `pr-watch:` line in your fence.\n\n" +
|
|
269
|
+
"REGISTERING IS IDEMPOTENT per pull request: asking twice returns the SAME id and tells you it was " +
|
|
270
|
+
"already armed, so re-registering after a compaction is safe and is the right instinct. Use `list` " +
|
|
271
|
+
"when you want to know what you are holding without changing anything — it answers with each PR's " +
|
|
272
|
+
"current check state too.\n\n" +
|
|
273
|
+
"You can only ever watch a PR on your OWN thread — there is no parameter for anyone else's.",
|
|
288
274
|
inputSchema: {
|
|
289
275
|
type: "object",
|
|
290
276
|
properties: {
|
|
@@ -292,46 +278,29 @@ const WATCH = {
|
|
|
292
278
|
type: "string",
|
|
293
279
|
enum: ["add", "list", "drop"],
|
|
294
280
|
description:
|
|
295
|
-
"`add` registers a watcher (idempotent
|
|
296
|
-
"
|
|
297
|
-
"answers with the full armed set
|
|
281
|
+
"`add` registers a watcher (idempotent per PR); `drop` withdraws one by id; `list` reads back " +
|
|
282
|
+
"everything armed on this thread, with each PR's latest check state, without changing " +
|
|
283
|
+
"anything. Every action answers with the full armed set.",
|
|
298
284
|
},
|
|
299
|
-
|
|
285
|
+
target: {
|
|
300
286
|
type: "string",
|
|
301
|
-
enum: ["shell"],
|
|
302
287
|
description:
|
|
303
|
-
"Required for `add`. The
|
|
288
|
+
"Required for `add`. The pull request, as `owner/repo#123` or a GitHub PR URL. A ref that " +
|
|
289
|
+
"cannot be parsed is REFUSED rather than stored — a watcher that can never fire is worse than " +
|
|
290
|
+
"no watcher, because you would come to rest believing you were covered.",
|
|
304
291
|
},
|
|
305
|
-
|
|
292
|
+
for: {
|
|
306
293
|
type: "string",
|
|
307
294
|
description:
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
"
|
|
295
|
+
"REQUIRED for `add`. How long to watch, as a DURATION — `30m`, `2h`, `3d` (max 24h). Never an " +
|
|
296
|
+
"instant, and there is no default. A PR nobody ever reviews would otherwise be polled forever " +
|
|
297
|
+
"and hold your thread with it; the watcher settles itself when this runs out and tells you, " +
|
|
298
|
+
"and you re-register if you still care.",
|
|
312
299
|
},
|
|
313
300
|
id: {
|
|
314
301
|
type: "string",
|
|
315
302
|
description: "Required for `drop`. The watcher id returned by `add` (or listed by `list`).",
|
|
316
303
|
},
|
|
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
304
|
},
|
|
336
305
|
required: ["action"],
|
|
337
306
|
},
|
|
@@ -344,14 +313,53 @@ const WATCH = {
|
|
|
344
313
|
const MIN_INTERVAL_SECONDS = 60
|
|
345
314
|
const MAX_INTERVAL_SECONDS = 24 * 60 * 60
|
|
346
315
|
|
|
347
|
-
const
|
|
316
|
+
const ACTIVITY = {
|
|
317
|
+
name: "activity",
|
|
318
|
+
description:
|
|
319
|
+
"EVERYTHING YOU CURRENTLY HAVE RUNNING, with the id each one is named by — your background shells, " +
|
|
320
|
+
"your sub-agents, your armed timers, and the pull requests you registered.\n\n" +
|
|
321
|
+
"WHY YOU NEED IT: an ```awaiting fence names what you are waiting on BY ID, and frizz checks every " +
|
|
322
|
+
"one against what is actually live. A name that matches nothing is not a park — you are bumped and " +
|
|
323
|
+
"your thread queues. So if you have lost an id (a compaction, a long turn, a wake you did not " +
|
|
324
|
+
"expect), call this rather than guessing. Guessing is the failure this tool exists to remove.\n\n" +
|
|
325
|
+
"It takes nothing and changes nothing. You can only ever read your OWN thread.",
|
|
326
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const TOOLS = [SPAWN_THREAD, RECURRING_PROMPT, TIMER, WATCH_PR, ACTIVITY]
|
|
348
330
|
|
|
349
331
|
/** @type {Record<string, (args: Record<string, unknown>) => Promise<string>>} */
|
|
350
332
|
const HANDLERS = {
|
|
351
333
|
[SPAWN_THREAD.name]: spawnThread,
|
|
352
334
|
[RECURRING_PROMPT.name]: recurringPrompt,
|
|
353
335
|
[TIMER.name]: timer,
|
|
354
|
-
[
|
|
336
|
+
[WATCH_PR.name]: watchPr,
|
|
337
|
+
[ACTIVITY.name]: activity,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Read out every background thing this thread has running, in the shape an awaiting fence names them.
|
|
341
|
+
* @returns {Promise<string>} */
|
|
342
|
+
async function activity() {
|
|
343
|
+
const result = (await callRpc("listOwnThreadActivity", { slug: threadSlug() }))?.result
|
|
344
|
+
const items = Array.isArray(result?.activity) ? result.activity : []
|
|
345
|
+
if (!items.length) {
|
|
346
|
+
return (
|
|
347
|
+
"Nothing is running on this thread — no background shells, no sub-agents, no armed timers, no " +
|
|
348
|
+
"registered PRs.\n\nSo there is nothing to wait on: an ```awaiting fence would have nothing to " +
|
|
349
|
+
"name, and a fence naming nothing is not a park. End with ```done, or with a ```question if you " +
|
|
350
|
+
"need the human."
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
const lines = items.map((i) => {
|
|
354
|
+
const when = i.until ? ` (fires ${i.until})` : i.since ? ` (since ${i.since})` : ""
|
|
355
|
+
return ` ${i.kind}: ${i.id}${when}\n ${i.label}`
|
|
356
|
+
})
|
|
357
|
+
return (
|
|
358
|
+
`${items.length} thing${items.length === 1 ? "" : "s"} running on this thread:\n\n${lines.join("\n")}\n\n` +
|
|
359
|
+
"Name the ones you are ACTUALLY waiting on in your ```awaiting fence, one `<kind>: <id>` line each, " +
|
|
360
|
+
"plus a required `for:` duration and a one-line `reason:`. Do not name something you are not waiting " +
|
|
361
|
+
"on — a dev server you left running is not a wait."
|
|
362
|
+
)
|
|
355
363
|
}
|
|
356
364
|
|
|
357
365
|
/** @param {unknown} obj */
|
|
@@ -446,10 +454,17 @@ function serverLockPort() {
|
|
|
446
454
|
}
|
|
447
455
|
}
|
|
448
456
|
if (candidates.length === 0) throw new Error("FRIZZ_STATE_DIR / FRIZZ_SERVER_LOCK not set — cannot locate the frizz server")
|
|
457
|
+
// SAY THAT NOTHING WAS SAVED, and say to retry. A worker reads "is frizz running?" as a fact about the
|
|
458
|
+
// world rather than as a fact about ITS OWN call, and moves on — so whatever it was arming is silently
|
|
459
|
+
// gone. Measured 2026-08-17: a worker's `recurring_prompt start` hit a restart window, got this error,
|
|
460
|
+
// carried on, and its Goal — the thing keeping a long autonomous effort alive — never existed. The
|
|
461
|
+
// window is ordinary (frizz restarts, and this process outlives every one of them), so the recovery has
|
|
462
|
+
// to be ordinary too: try again.
|
|
449
463
|
throw new Error(
|
|
450
464
|
`no running frizz server found (looked at ${candidates.join(", ")} and every project lock under ` +
|
|
451
465
|
`${root ? join(root, "projects") : "the frizz root"}; each was missing, malformed, or written by a process that is gone). ` +
|
|
452
|
-
`
|
|
466
|
+
`NOTHING WAS SAVED — this call had no effect. frizz is probably mid-restart, which is ordinary and ` +
|
|
467
|
+
`brief; RETRY this exact call before you do anything else, and do not come to rest assuming it took.`,
|
|
453
468
|
)
|
|
454
469
|
}
|
|
455
470
|
|
|
@@ -550,8 +565,35 @@ async function spawnThread(args) {
|
|
|
550
565
|
/** POST a frizz RPC procedure and return its parsed payload. Shares spawn_thread's transport rules:
|
|
551
566
|
* the port comes from server.lock and `sec-fetch-site: same-origin` satisfies the loopback gate.
|
|
552
567
|
* @param {string} procedure @param {Record<string, unknown>} body @returns {Promise<any>} */
|
|
568
|
+
// HOW LONG A RESTART WINDOW IS ALLOWED TO BE INVISIBLE. frizz replaces its own server routinely
|
|
569
|
+
// ("Update & Restart", a dev rebuild), and this process is deliberately still here across every one of
|
|
570
|
+
// them — so a call landing in that gap is ORDINARY, and failing it is the shim reporting frizz's
|
|
571
|
+
// housekeeping as the worker's problem. Measured 2026-08-17: a `recurring_prompt start` landed in one,
|
|
572
|
+
// failed, and the Goal that was keeping a long autonomous effort alive silently never existed.
|
|
573
|
+
//
|
|
574
|
+
// Telling the model to retry (which the error also does) is strictly weaker than retrying, because it
|
|
575
|
+
// only works if the model complies. Bounded and short: a genuinely-down frizz still fails, promptly,
|
|
576
|
+
// with the same message — this only covers the seconds where a new server is coming up.
|
|
577
|
+
const LOCK_RETRY_MS = 6_000
|
|
578
|
+
const LOCK_RETRY_INTERVAL_MS = 400
|
|
579
|
+
|
|
580
|
+
/** The port, waiting out a brief restart window rather than failing into one. Rethrows the real
|
|
581
|
+
* "no running frizz server" error once the budget is spent, so a frizz that is actually down still
|
|
582
|
+
* says so — and says it with the retry guidance attached. */
|
|
583
|
+
async function serverLockPortWaiting() {
|
|
584
|
+
const deadline = Date.now() + LOCK_RETRY_MS
|
|
585
|
+
for (;;) {
|
|
586
|
+
try {
|
|
587
|
+
return serverLockPort()
|
|
588
|
+
} catch (err) {
|
|
589
|
+
if (Date.now() >= deadline) throw err
|
|
590
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_INTERVAL_MS))
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
553
595
|
async function callRpc(procedure, body) {
|
|
554
|
-
const port =
|
|
596
|
+
const port = await serverLockPortWaiting()
|
|
555
597
|
const controller = new AbortController()
|
|
556
598
|
const timer = setTimeout(() => controller.abort(), DISPATCH_TIMEOUT_MS)
|
|
557
599
|
let res
|
|
@@ -604,7 +646,7 @@ function cadenceLabel(seconds) {
|
|
|
604
646
|
* each last fired, and the text VERBATIM (never truncated — reading back a summary of your own
|
|
605
647
|
* instruction is exactly as blind as not reading it).
|
|
606
648
|
* @param {{ prompt: string, stopHook: boolean, heartbeat: boolean, postCompaction: boolean,
|
|
607
|
-
*
|
|
649
|
+
* intervalSeconds?: number, armedAt: string, lastRestFiredAt?: string,
|
|
608
650
|
* lastScheduleFiredAt?: string, lastCompactFiredAt?: string }} rp */
|
|
609
651
|
function recurringPromptReport(rp) {
|
|
610
652
|
const fired = (/** @type {string|undefined} */ at) => (at ? `last fired ${at}` : "never fired yet")
|
|
@@ -622,13 +664,7 @@ function recurringPromptReport(rp) {
|
|
|
622
664
|
const head = triggers.length
|
|
623
665
|
? `Armed since ${rp.armedAt}, on:\n${triggers.join("\n")}`
|
|
624
666
|
: `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}`
|
|
667
|
+
return `${head}\n\nThe text, verbatim:\n\n${rp.prompt}`
|
|
632
668
|
}
|
|
633
669
|
|
|
634
670
|
/** The `recurring_prompt` handler: arm, disarm, or READ BACK this thread's re-prompt.
|
|
@@ -663,7 +699,7 @@ async function recurringPrompt(args) {
|
|
|
663
699
|
}
|
|
664
700
|
|
|
665
701
|
if (action === "stop") {
|
|
666
|
-
await callRpc("setOwnThreadRecurringPrompt", { slug, prompt: null, stopHook: false, heartbeat: false, postCompaction: false
|
|
702
|
+
await callRpc("setOwnThreadRecurringPrompt", { slug, prompt: null, stopHook: false, heartbeat: false, postCompaction: false })
|
|
667
703
|
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
704
|
}
|
|
669
705
|
|
|
@@ -682,9 +718,6 @@ async function recurringPrompt(args) {
|
|
|
682
718
|
}
|
|
683
719
|
}
|
|
684
720
|
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
721
|
// DEFAULTED, not required: a `start` that names no trigger at all is a model asking to be re-prompted
|
|
689
722
|
// and leaving the mechanism to us, and the rest trigger is the safe reading of that — it cannot talk
|
|
690
723
|
// over a running turn, and it cannot fire on a thread that has stopped needing it.
|
|
@@ -700,7 +733,6 @@ async function recurringPrompt(args) {
|
|
|
700
733
|
stopHook,
|
|
701
734
|
heartbeat,
|
|
702
735
|
postCompaction,
|
|
703
|
-
pauseOnQuestions,
|
|
704
736
|
...(heartbeat ? { intervalSeconds: interval } : {}),
|
|
705
737
|
})
|
|
706
738
|
// `replaced` is absent against a server that predates it, which is indistinguishable from "there was
|
|
@@ -725,12 +757,12 @@ async function recurringPrompt(args) {
|
|
|
725
757
|
? `\n\nIT REPLACED an existing recurring prompt — check that discarding it was intended, and restore ` +
|
|
726
758
|
`it with another \`start\` if it was not:\n\n${recurringPromptReport(replaced)}\n`
|
|
727
759
|
: ""
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
760
|
+
// NO QUESTION HOLD ANY MORE (2026-08-16). Every trigger fires while you are waiting on the human, and
|
|
761
|
+
// the at-rest one fires over your own unanswered ```question fence — the delivery says so, and expects
|
|
762
|
+
// you to decide the question yourself rather than re-ask it. A ```done fence, and an ```awaiting on a
|
|
763
|
+
// wait frizz itself will deliver, still stop the at-rest trigger.
|
|
732
764
|
return (
|
|
733
|
-
`Recurring prompt armed — frizz will send you this ${when}.${
|
|
765
|
+
`Recurring prompt armed — frizz will send you this ${when}.${superseded}\n\n` +
|
|
734
766
|
"Call this tool again with `action: \"stop\"` once the work it drives is finished — one left armed on " +
|
|
735
767
|
"a finished thread wakes it forever. The human can also edit or switch it off in the thread footer. " +
|
|
736
768
|
"Signing off with a ```done fence stops it too, but only when there is genuinely nothing left: it " +
|
|
@@ -816,18 +848,32 @@ async function timer(args) {
|
|
|
816
848
|
)
|
|
817
849
|
}
|
|
818
850
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
851
|
+
|
|
852
|
+
/** How the armed PR-watcher set reads back, on every action, so a worker never needs a second call.
|
|
853
|
+
* @param {{ watches?: Array<{id: string, target: string, github?: {checks: string, running: number, passed: number, failed: number, failing: string[], merge: string, state: string}}> }|undefined} result */
|
|
854
|
+
function armedPrWatchList(result) {
|
|
822
855
|
const watches = Array.isArray(result?.watches) ? result.watches : []
|
|
823
|
-
if (!watches.length) return "
|
|
824
|
-
const lines = watches.map((w) =>
|
|
825
|
-
|
|
856
|
+
if (!watches.length) return "No pull requests are watched on this thread — nothing will wake you."
|
|
857
|
+
const lines = watches.map((w) => {
|
|
858
|
+
const g = w.github
|
|
859
|
+
// The CHECK STATE rides the read-back because it is the reason a worker is listing at all: "where do
|
|
860
|
+
// my PRs stand" is one call, not one per PR through `gh`.
|
|
861
|
+
const state = !g
|
|
862
|
+
? "not polled yet"
|
|
863
|
+
: g.state !== "open"
|
|
864
|
+
? g.state
|
|
865
|
+
: g.checks === "passing" ? `checks green (${g.passed})`
|
|
866
|
+
: g.checks === "failing" ? `checks FAILING${g.failing.length ? `: ${g.failing.join(", ")}` : ""}`
|
|
867
|
+
: g.checks === "running" ? `checks running (${g.running} left)`
|
|
868
|
+
: "no checks"
|
|
869
|
+
return ` ${w.id} ${w.target} — ${state}${g && g.state === "open" && g.merge === "mergeable" ? ", mergeable" : ""}`
|
|
870
|
+
})
|
|
871
|
+
return `Watched on this thread now:\n${lines.join("\n")}`
|
|
826
872
|
}
|
|
827
873
|
|
|
828
|
-
/** The `
|
|
874
|
+
/** The `watch_pr` handler: register, withdraw, or read back this thread's PR watchers.
|
|
829
875
|
* @param {Record<string, unknown>} args @returns {Promise<string>} */
|
|
830
|
-
async function
|
|
876
|
+
async function watchPr(args) {
|
|
831
877
|
const slug = threadSlug()
|
|
832
878
|
const action = typeof args.action === "string" ? args.action.trim() : ""
|
|
833
879
|
if (action !== "add" && action !== "list" && action !== "drop") {
|
|
@@ -835,103 +881,34 @@ async function watch(args) {
|
|
|
835
881
|
}
|
|
836
882
|
|
|
837
883
|
if (action === "list") {
|
|
838
|
-
|
|
839
|
-
return armedWatchList(result)
|
|
884
|
+
return armedPrWatchList((await callRpc("listOwnPrWatches", { slug }))?.result)
|
|
840
885
|
}
|
|
841
886
|
|
|
842
887
|
if (action === "drop") {
|
|
843
888
|
const id = typeof args.id === "string" ? args.id.trim() : ""
|
|
844
889
|
if (!id) throw new Error("`id` is required to drop a watcher — take it from `add` or from `list`")
|
|
845
|
-
const result = (await callRpc("
|
|
890
|
+
const result = (await callRpc("dropOwnPrWatch", { slug, id }))?.result
|
|
846
891
|
// A drop that matched nothing is reported rather than swallowed: the id was wrong, already settled,
|
|
847
892
|
// or another thread's — and a worker that believes it withdrew a wait it still holds will rest.
|
|
848
893
|
const head = result?.dropped
|
|
849
894
|
? `Watcher ${id} dropped. It will not wake you.`
|
|
850
895
|
: `No ARMED watcher ${id} on this thread — it was already settled, or the id is not one of yours.`
|
|
851
|
-
return `${head}\n\n${
|
|
896
|
+
return `${head}\n\n${armedPrWatchList(result)}`
|
|
852
897
|
}
|
|
853
898
|
|
|
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
899
|
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
|
|
900
|
+
if (!target) throw new Error("`target` is required — the pull request, as `owner/repo#123` or a PR URL")
|
|
901
|
+
const result = (await callRpc("addOwnPrWatch", { slug, target, for: typeof args.for === "string" ? args.for.trim() : "" }))?.result
|
|
884
902
|
const id = result?.id ?? "(unknown)"
|
|
885
|
-
|
|
903
|
+
const ref = result?.target ?? target
|
|
886
904
|
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
|
|
905
|
+
? `Already watching ${ref} as ${id} — nothing new was registered, and you will be woken once per event.`
|
|
906
|
+
: `Watching ${ref} as ${id}. Frizz wakes you when CI passes or fails and on every later review or ` +
|
|
907
|
+
"comment, and the registration survives your turn ending, a compaction and a frizz restart."
|
|
930
908
|
return (
|
|
931
|
-
|
|
932
|
-
`
|
|
933
|
-
`
|
|
934
|
-
"mattering."
|
|
909
|
+
`${head}\n\nNAME IT IN YOUR \`\`\`awaiting FENCE TOO (\`pr-watch: ${ref}\`) — the watcher does the ` +
|
|
910
|
+
`waking, the fence is what lets you come to rest and shows the human what you are waiting for.\n\n` +
|
|
911
|
+
`DROP IT when it stops mattering (\`action: "drop", id: "${id}"\`).\n\n${armedPrWatchList(result)}`
|
|
935
912
|
)
|
|
936
913
|
}
|
|
937
914
|
|
|
@@ -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, mcp__frizz__spawn_thread gives it its own card and only THEN is done honest — link the returned thread in the body; 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 =
|