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.
Files changed (98) hide show
  1. package/README.md +9 -4
  2. package/dist/claude-agent-broker.js +9 -4
  3. package/dist/dev-child.js +1783 -527
  4. package/dist/frizz.js +709 -233
  5. package/package.json +5 -3
  6. package/runtime/cc-worker/DECISIONS.md +17 -0
  7. package/runtime/cc-worker/bin/frizz-mcp.mjs +151 -174
  8. package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
  9. package/web-dist/assets/{TerminalPane-CTdetDJJ.js → TerminalPane-sd3XXjx5.js} +1 -1
  10. package/web-dist/assets/{abnfDiagram-VRR7QNED-BDKrjCMs.js → abnfDiagram-VRR7QNED-Bry_FIqg.js} +1 -1
  11. package/web-dist/assets/architecture-TIHT7OUA-CoMJQ5Ya.js +1 -0
  12. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-BJebpUUM.js → architectureDiagram-ZJ3FMSHR-Baok4OeI.js} +1 -1
  13. package/web-dist/assets/{blockDiagram-677ZJIJ3-BNbuk25k.js → blockDiagram-677ZJIJ3-BgHIO9lI.js} +1 -1
  14. package/web-dist/assets/{c4Diagram-LMCZKHZV-lCfyotdU.js → c4Diagram-LMCZKHZV-rfj9Z17Z.js} +1 -1
  15. package/web-dist/assets/channel-D6uS1y7I.js +1 -0
  16. package/web-dist/assets/{chunk-32BRIVSS-DP72SEkr.js → chunk-32BRIVSS-BwaHfK1Y.js} +1 -1
  17. package/web-dist/assets/{chunk-52WLFC77-DxO3gN-p.js → chunk-52WLFC77-BmdQDZOA.js} +1 -1
  18. package/web-dist/assets/{chunk-C7G6YPKG-BXFx9Vlr.js → chunk-C7G6YPKG-ByQFjoZe.js} +1 -1
  19. package/web-dist/assets/{chunk-EX3LRPZG-CR3sHpPf.js → chunk-EX3LRPZG-DQFudBKr.js} +1 -1
  20. package/web-dist/assets/{chunk-FWX5IMBZ-CJ8L__oG.js → chunk-FWX5IMBZ-OW9qEPiq.js} +2 -2
  21. package/web-dist/assets/{chunk-HOUHSVGY-BCl3JSWr.js → chunk-HOUHSVGY-BdKLF8nO.js} +1 -1
  22. package/web-dist/assets/{chunk-ICXQ74PX-CZxFMci1.js → chunk-ICXQ74PX-C1roqKR1.js} +1 -1
  23. package/web-dist/assets/{chunk-MOJQB5TN-Ck3_47dB.js → chunk-MOJQB5TN-Cx0u1ZDK.js} +1 -1
  24. package/web-dist/assets/{chunk-OGEWGWER-CVXjES-F.js → chunk-OGEWGWER-rxv9xtNR.js} +1 -1
  25. package/web-dist/assets/{chunk-PUDLZKDR-JjrVgaR7.js → chunk-PUDLZKDR-BXWPPPPi.js} +1 -1
  26. package/web-dist/assets/{chunk-Q4XR5HBZ-C5YMRAQ0.js → chunk-Q4XR5HBZ-CpqbD101.js} +1 -1
  27. package/web-dist/assets/{chunk-V7JOEXUC-DK0mfUHU.js → chunk-V7JOEXUC-CP8_3qVt.js} +1 -1
  28. package/web-dist/assets/{chunk-VAUOI2AC-BKBntpid.js → chunk-VAUOI2AC-Dl7xahpk.js} +1 -1
  29. package/web-dist/assets/{chunk-VR4S4FIN-zgE1dG6U.js → chunk-VR4S4FIN-BKtMX64l.js} +1 -1
  30. package/web-dist/assets/{chunk-WYO6CB5R-CZhh1IBq.js → chunk-WYO6CB5R-DmLOdJcu.js} +1 -1
  31. package/web-dist/assets/{chunk-ZGVPDNZ5-CrNjIEem.js → chunk-ZGVPDNZ5-5wN7eXvS.js} +1 -1
  32. package/web-dist/assets/classDiagram-OUVF2IWQ-BW5YiBf4.js +1 -0
  33. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BW5YiBf4.js +1 -0
  34. package/web-dist/assets/{cynefin-VYW2F7L2-Ca_BPfTG.js → cynefin-VYW2F7L2-DUYahBXb.js} +1 -1
  35. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-CsINQf6g.js → cynefinDiagram-TSTJHNR4-Rm0i30nE.js} +1 -1
  36. package/web-dist/assets/{dagre-VKFMJZFB-CE0DYMFy.js → dagre-VKFMJZFB-3uhsB-W3.js} +1 -1
  37. package/web-dist/assets/{diagram-FQU43EPY-BqTduEYE.js → diagram-FQU43EPY-BUvflNRQ.js} +1 -1
  38. package/web-dist/assets/{diagram-G47NLZAW-Cho718s6.js → diagram-G47NLZAW-DUluIZel.js} +1 -1
  39. package/web-dist/assets/{diagram-NH7WQ7WH-D0Z8t9UC.js → diagram-NH7WQ7WH-BBrwsbed.js} +1 -1
  40. package/web-dist/assets/{diagram-OA4YK3LP-CBacyeDx.js → diagram-OA4YK3LP-DWnTQBw4.js} +1 -1
  41. package/web-dist/assets/{diagram-WEI45ONY-CozpXa4j.js → diagram-WEI45ONY-BjEavoC0.js} +1 -1
  42. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-ba4NbQQo.js → ebnfDiagram-CCIWWBDH-CW4xZAZH.js} +1 -1
  43. package/web-dist/assets/{erDiagram-Q63AITRT-B3BciOYa.js → erDiagram-Q63AITRT-BemQ1R4B.js} +1 -1
  44. package/web-dist/assets/eventmodeling-45OFAUF4-BF5KB7ja.js +1 -0
  45. package/web-dist/assets/flowDiagram-23GEKE2U-BYo4Eytx.js +1 -0
  46. package/web-dist/assets/{ganttDiagram-NO4QXBWP-lPmrRit4.js → ganttDiagram-NO4QXBWP-lDdG5I_z.js} +1 -1
  47. package/web-dist/assets/{gitGraph-TEB2WS4Q-BjyclOq0.js → gitGraph-TEB2WS4Q-Bw87a_Se.js} +1 -1
  48. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-pzeb4Yrw.js → gitGraphDiagram-IHSO6WYX-B762r0yi.js} +1 -1
  49. package/web-dist/assets/index-B3QArXu2.css +1 -0
  50. package/web-dist/assets/index-CAapfhMv.js +365 -0
  51. package/web-dist/assets/{info-DKCQHKI2-Bwycegvf.js → info-DKCQHKI2-DGOv6Gx0.js} +1 -1
  52. package/web-dist/assets/{infoDiagram-FWYZ7A6U-BCvRGj_5.js → infoDiagram-FWYZ7A6U-DL92kAzf.js} +1 -1
  53. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js → ishikawaDiagram-FXEZZL3T-CFYd4OtU.js} +1 -1
  54. package/web-dist/assets/{journeyDiagram-5HDEW3XC-C5ROwFio.js → journeyDiagram-5HDEW3XC-QHuYL5zq.js} +1 -1
  55. package/web-dist/assets/{kanban-definition-HUTT4EX6-YLPLkpeT.js → kanban-definition-HUTT4EX6-DwmMluQ8.js} +1 -1
  56. package/web-dist/assets/{line-KtkNqRgI.js → line-Je_PFFHm.js} +1 -1
  57. package/web-dist/assets/{mermaid-parser.core-D_FfqBe7.js → mermaid-parser.core-BeiGWsn3.js} +3 -3
  58. package/web-dist/assets/{mermaid.core-Ffv8anVf.js → mermaid.core-BP8JrW48.js} +3 -3
  59. package/web-dist/assets/{mindmap-definition-LN4V7U3C-B2jj4vfm.js → mindmap-definition-LN4V7U3C-BsmjCIrM.js} +1 -1
  60. package/web-dist/assets/{packet-7NZHBO7P-BjmWHwra.js → packet-7NZHBO7P-D6ekYYGJ.js} +1 -1
  61. package/web-dist/assets/{pegDiagram-2B236MQR-Dq3iJDyq.js → pegDiagram-2B236MQR-CQc2Kt0u.js} +1 -1
  62. package/web-dist/assets/{pie-RZYD4A2V-jBbH1lv9.js → pie-RZYD4A2V-C_45fO8J.js} +1 -1
  63. package/web-dist/assets/{pieDiagram-ENE6RG2P-C3ETW9lq.js → pieDiagram-ENE6RG2P-Dg5z6iWt.js} +1 -1
  64. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DItSmme7.js → quadrantDiagram-ABIIQ3AL-c7gZxuCX.js} +1 -1
  65. package/web-dist/assets/{radar-I7S5WNFK-6ey6crgP.js → radar-I7S5WNFK-D80XNJQq.js} +1 -1
  66. package/web-dist/assets/{railroad-3IZDKUUU-Cii-Mn0E.js → railroad-3IZDKUUU-Brz7eZQ0.js} +1 -1
  67. package/web-dist/assets/railroad-abnf-AHOZXSZD-D44pcGhJ.js +1 -0
  68. package/web-dist/assets/railroad-ebnf-EBAXGLYW-CiWVH5SP.js +1 -0
  69. package/web-dist/assets/railroad-peg-LSFZ7HO6-Cy1B6-pN.js +1 -0
  70. package/web-dist/assets/{railroadDiagram-RFXS5EU6-M363ils_.js → railroadDiagram-RFXS5EU6-Db9K7bty.js} +1 -1
  71. package/web-dist/assets/{requirementDiagram-TGXJPOKE-CTs2_V6T.js → requirementDiagram-TGXJPOKE-m1ipMrDe.js} +1 -1
  72. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-QTLLcDPD.js → sankeyDiagram-HTMAVEWB-4Zz9ry0k.js} +1 -1
  73. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js → sequenceDiagram-DBY2YBRQ-CZeGHw4X.js} +1 -1
  74. package/web-dist/assets/{stateDiagram-2N3HPSRC-BecB6roG.js → stateDiagram-2N3HPSRC-rLUVs1Pg.js} +1 -1
  75. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-w7QDFEvx.js +1 -0
  76. package/web-dist/assets/{swimlanes-5IMT3BWC-COiYgS0w.js → swimlanes-5IMT3BWC-BAeBdscz.js} +1 -1
  77. package/web-dist/assets/swimlanesDiagram-G3AALYLV-UhdgWpNZ.js +8 -0
  78. package/web-dist/assets/{timeline-definition-FHXFAJF6-Ci22coeH.js → timeline-definition-FHXFAJF6-DL0eWMF-.js} +1 -1
  79. package/web-dist/assets/{treeView-QDETBFTQ-4DW35czh.js → treeView-QDETBFTQ-gEtzLMA8.js} +1 -1
  80. package/web-dist/assets/{treemap-6X3UGDF4-Dq_-Z6-Y.js → treemap-6X3UGDF4-CCym1Gz6.js} +1 -1
  81. package/web-dist/assets/{vennDiagram-L72KCM5P-CHQSIEYq.js → vennDiagram-L72KCM5P-cF97-nb5.js} +1 -1
  82. package/web-dist/assets/{wardley-OPB4EBWU-Bwlh7HCY.js → wardley-OPB4EBWU-DQJHCGbM.js} +1 -1
  83. package/web-dist/assets/{wardleyDiagram-EHGQE667-DctjuPYn.js → wardleyDiagram-EHGQE667-0CnHzXLX.js} +1 -1
  84. package/web-dist/assets/{xychartDiagram-FW5EYKEG-ZoIGosv8.js → xychartDiagram-FW5EYKEG-9YDOp4m5.js} +1 -1
  85. package/web-dist/index.html +2 -2
  86. package/web-dist/assets/architecture-TIHT7OUA-ChUMo004.js +0 -1
  87. package/web-dist/assets/channel-pr7r6raB.js +0 -1
  88. package/web-dist/assets/classDiagram-OUVF2IWQ-CvGbPMn_.js +0 -1
  89. package/web-dist/assets/classDiagram-v2-EOCWNBFH-CvGbPMn_.js +0 -1
  90. package/web-dist/assets/eventmodeling-45OFAUF4-DKmyo-jd.js +0 -1
  91. package/web-dist/assets/flowDiagram-23GEKE2U-DluCBvT4.js +0 -1
  92. package/web-dist/assets/index-BQtjYMpV.css +0 -1
  93. package/web-dist/assets/index-CLW1Q49U.js +0 -360
  94. package/web-dist/assets/railroad-abnf-AHOZXSZD-U_vb4BrX.js +0 -1
  95. package/web-dist/assets/railroad-ebnf-EBAXGLYW-BIHG7gNU.js +0 -1
  96. package/web-dist/assets/railroad-peg-LSFZ7HO6-Cpd9r-tB.js +0 -1
  97. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-JXw9T96l.js +0 -1
  98. 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.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 --test --test-force-exit 'board/*.test.mjs' 'scripts/**/*.test.mjs' 'src/**/*.test.ts' 'packages/shared/src/**/*.test.ts' 'packages/server/src/**/*.test.ts' 'packages/web/src/**/*.test.ts'",
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 WATCH = {
269
- name: "watch",
253
+ const WATCH_PR = {
254
+ name: "watch_pr",
270
255
  description:
271
- "REGISTER something to wait on, so frizz wakes you when it resolves and DROP it when it stops " +
272
- "mattering. Your waits have identities: you can list them, and you can withdraw them.\n\n" +
273
- " kind: \"shell\" — one of YOUR OWN background shells finishing. `target` is its id or its label.\n\n" +
274
- "FOR A PULL REQUEST, use an ```awaiting fence with a `pr-watch: owner/repo#123` line instead. That " +
275
- "watcher is durable, it replays whatever review is already sitting on the PR the first time you park " +
276
- "on it, and it is where PR watching lives this registry is for the waits that have nowhere else to " +
277
- "go.\n\n" +
278
- "WHY THIS RATHER THAN WAITING YOURSELF: a registered watcher is durable. It survives your turn " +
279
- "ending, a compaction, a frizz restart, and your own daemon being replaced none of which a " +
280
- "blocking call or a monitor survives. Register it, come to rest, and frizz brings you back.\n\n" +
281
- "REGISTERING IS IDEMPOTENT on (kind, target): asking twice for the same thing returns the SAME id " +
282
- "and tells you it was already armed, so re-registering after a compaction is safe and is the right " +
283
- "instinct. Use `list` when you want to know what you are holding without changing anything.\n\n" +
284
- "DROP WHAT STOPS MATTERING. A watcher you no longer care about is a wake you did not want and a " +
285
- "thread that looks parked when it is not you are the only one who can know, which is why this " +
286
- "tool exists (maintainer: \"this way the agent can decide when they're not necessary\").\n\n" +
287
- "You can only ever register a wait on your OWN thread — there is no parameter for anyone else's.",
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 on kind+target); `drop` withdraws one by id; `list` " +
296
- "reads back everything currently armed on this thread without changing anything. Every action " +
297
- "answers with the full armed set, so you never need a second call to see where you stand.",
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
- kind: {
285
+ target: {
300
286
  type: "string",
301
- enum: ["shell"],
302
287
  description:
303
- "Required for `add`. The only kind a PR wait belongs in an ```awaiting fence, see above.",
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
- target: {
292
+ for: {
306
293
  type: "string",
307
294
  description:
308
- "Required for `add`. A background shell's id or its label. A target that does not match one " +
309
- "of your live shells still registers, but the watcher only fires once frizz has SEEN it " +
310
- "alive so a typo'd label simply never fires rather than reporting a completion that never " +
311
- "happened.",
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 TOOLS = [SPAWN_THREAD, RECURRING_PROMPT, TIMER, WATCH]
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
- [WATCH.name]: watch,
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
- `Is frizz running?`,
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 = serverLockPort()
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
- * pauseOnQuestions?: boolean, intervalSeconds?: number, armedAt: string, lastRestFiredAt?: string,
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
- // Reported only when ON, and after the triggers: it is a HOLD over the list above rather than an item
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, pauseOnQuestions: 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
- const held = pauseOnQuestions
729
- ? " Nothing is sent while you are waiting on the human — a question fence, a native ask or a " +
730
- "permission prompt holds every trigger."
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}.${held}${superseded}\n\n` +
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
- /** How the armed watcher set reads back, on every action, so a worker never needs a second call.
820
- * @param {{ watches?: Array<{id: string, kind: string, target: string}> }|undefined} result */
821
- function armedWatchList(result) {
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 "Nothing is armed on this thread now no watcher will wake you."
824
- const lines = watches.map((w) => ` ${w.id} ${w.kind} ${w.target}`)
825
- return `Armed on this thread now:\n${lines.join("\n")}`
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 `watch` handler: register, withdraw, or read back this thread's waits.
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 watch(args) {
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
- const result = (await callRpc("listOwnThreadWatches", { slug }))?.result
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("dropOwnThreadWatch", { slug, id }))?.result
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${armedWatchList(result)}`
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 id or label of one of your own background shells")
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
- if (wait) return await blockUntilResolved(slug, id, kind, target, timeoutSeconds)
903
+ const ref = result?.target ?? target
886
904
  const head = result?.alreadyArmed
887
- ? `Already watching ${kind} ${target} as ${id} — nothing new was registered, and you will be woken once.`
888
- : `Watching ${kind} ${target} as ${id}. Frizz will wake you when it resolves, and the registration ` +
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
- `Waited ${timeoutSeconds}s and ${target} has NOT resolved yet. The watcher is still armed and is now ` +
932
- `frizz's to keep${promoted?.promoted === false ? " (it had already settled)" : ""} go do something else ` +
933
- `and you will be woken when it fires, or drop it with \`action: "drop", id: "${id}"\` if it has stopped ` +
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 only a human:/timer:/pr-watch: gate, never CI/releases/merge progression (those stay ACTIVE); ```question is the operator ask. Blocked on a PR, CI or your own background shell? REGISTER it with mcp__frizz__watch and rest it is durable, and you can drop it when it stops mattering. Keep the write-up SHORT: 1-3 sentences, then bullets starting with a **bolded verb phrase**.\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, 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 =