frizz-server 0.13.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 (155) hide show
  1. package/dist/claude-agent-broker.js +35911 -0
  2. package/dist/codex-app-server-daemon.js +423 -0
  3. package/dist/dev-child.js +56233 -0
  4. package/package.json +34 -0
  5. package/runtime/board/agent-bindings.mjs +287 -0
  6. package/runtime/board/agent-liveness.mjs +367 -0
  7. package/runtime/board/agent-status.mjs +178 -0
  8. package/runtime/board/config.mjs +993 -0
  9. package/runtime/board/decisions.mjs +97 -0
  10. package/runtime/board/index.mjs +704 -0
  11. package/runtime/board/notify-shared.mjs +90 -0
  12. package/runtime/board/notify.mjs +81 -0
  13. package/runtime/board/ownership.mjs +120 -0
  14. package/runtime/board/rest-detect.mjs +213 -0
  15. package/runtime/board/thread-excerpt.mjs +162 -0
  16. package/runtime/board/thread-update.mjs +289 -0
  17. package/runtime/cc-worker/.claude-plugin/plugin.json +10 -0
  18. package/runtime/cc-worker/DECISIONS.md +1172 -0
  19. package/runtime/cc-worker/LICENSE +21 -0
  20. package/runtime/cc-worker/agents/high.md +7 -0
  21. package/runtime/cc-worker/agents/low.md +7 -0
  22. package/runtime/cc-worker/agents/max.md +7 -0
  23. package/runtime/cc-worker/agents/medium.md +7 -0
  24. package/runtime/cc-worker/agents/xhigh.md +7 -0
  25. package/runtime/cc-worker/bin/frizz +17 -0
  26. package/runtime/cc-worker/bin/frizz-mcp.mjs +1612 -0
  27. package/runtime/cc-worker/bin/frizz-update +18 -0
  28. package/runtime/cc-worker/hooks/agent-bind.mjs +40 -0
  29. package/runtime/cc-worker/hooks/agent-dispatch.mjs +121 -0
  30. package/runtime/cc-worker/hooks/bash-background.d.mts +6 -0
  31. package/runtime/cc-worker/hooks/bash-background.mjs +247 -0
  32. package/runtime/cc-worker/hooks/deny-ask.mjs +39 -0
  33. package/runtime/cc-worker/hooks/deny-plan.mjs +62 -0
  34. package/runtime/cc-worker/hooks/hooks.json +102 -0
  35. package/runtime/cc-worker/hooks/perm-policy.mjs +211 -0
  36. package/runtime/cc-worker/hooks/scratchpad.mjs +417 -0
  37. package/runtime/cc-worker/hooks/session-seed.mjs +107 -0
  38. package/runtime/cc-worker/scripts/frizz/agent-bindings.mjs +9 -0
  39. package/runtime/cc-worker/scripts/frizz/config.mjs +12 -0
  40. package/runtime/cc-worker/skills/gh/SKILL.md +141 -0
  41. package/runtime/cc-worker/skills/gh/scripts/ci-watch.mjs +60 -0
  42. package/runtime/cc-worker/skills/gh/scripts/github-watch.mjs +130 -0
  43. package/runtime/cc-worker/skills/gh/scripts/review-watch.mjs +54 -0
  44. package/web-dist/apple-touch-icon.png +0 -0
  45. package/web-dist/assets/TerminalPane-DyLvW_rQ.js +7 -0
  46. package/web-dist/assets/abnfDiagram-VRR7QNED-DIPgkiM8.js +1 -0
  47. package/web-dist/assets/arc-BSyeo0Gb.js +1 -0
  48. package/web-dist/assets/architecture-TIHT7OUA-B8qUD5-C.js +1 -0
  49. package/web-dist/assets/architectureDiagram-ZJ3FMSHR-DBAKToiy.js +36 -0
  50. package/web-dist/assets/array-BifhSqXX.js +1 -0
  51. package/web-dist/assets/blockDiagram-677ZJIJ3-Ba0xt8st.js +132 -0
  52. package/web-dist/assets/c4Diagram-LMCZKHZV-DFham1h_.js +10 -0
  53. package/web-dist/assets/channel-5l10tOPT.js +1 -0
  54. package/web-dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  55. package/web-dist/assets/chunk-32BRIVSS-Bl-817K-.js +1 -0
  56. package/web-dist/assets/chunk-52WLFC77-DBLTDz2W.js +10 -0
  57. package/web-dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  58. package/web-dist/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  59. package/web-dist/assets/chunk-C7G6YPKG-ClL6Ebv8.js +1 -0
  60. package/web-dist/assets/chunk-EX3LRPZG--3vJLCZP.js +231 -0
  61. package/web-dist/assets/chunk-FWX5IMBZ-DMOdhcCP.js +2 -0
  62. package/web-dist/assets/chunk-HOUHSVGY-DkgTGLCa.js +1 -0
  63. package/web-dist/assets/chunk-ICXQ74PX-7X6iir1H.js +2 -0
  64. package/web-dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  65. package/web-dist/assets/chunk-KEIR6QF5-BfrZ3jm6.js +161 -0
  66. package/web-dist/assets/chunk-MOJQB5TN-OpO5flE4.js +88 -0
  67. package/web-dist/assets/chunk-OGEWGWER-BbAMAzTZ.js +1 -0
  68. package/web-dist/assets/chunk-PUDLZKDR-avcvDgZl.js +156 -0
  69. package/web-dist/assets/chunk-Q4XR5HBZ-BaiGN1cd.js +70 -0
  70. package/web-dist/assets/chunk-RYQCIY6F-Cu_KplZW.js +1 -0
  71. package/web-dist/assets/chunk-V7JOEXUC-CKVakdOJ.js +206 -0
  72. package/web-dist/assets/chunk-VAUOI2AC-DzG-rM3_.js +1 -0
  73. package/web-dist/assets/chunk-VR4S4FIN-t3j3HHQF.js +1 -0
  74. package/web-dist/assets/chunk-WYO6CB5R-SnP0NDTw.js +127 -0
  75. package/web-dist/assets/chunk-XXDRQBXY-DYlTP5J-.js +1 -0
  76. package/web-dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  77. package/web-dist/assets/chunk-ZGVPDNZ5-pXn3giwS.js +62 -0
  78. package/web-dist/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
  79. package/web-dist/assets/classDiagram-OUVF2IWQ-vIfzHupB.js +1 -0
  80. package/web-dist/assets/classDiagram-v2-EOCWNBFH-vIfzHupB.js +1 -0
  81. package/web-dist/assets/cose-bilkent-JH36ORCC-BUIsLrGc.js +1 -0
  82. package/web-dist/assets/cynefin-VYW2F7L2-C4qNLMkm.js +1 -0
  83. package/web-dist/assets/cynefinDiagram-TSTJHNR4-2vzWUUfl.js +62 -0
  84. package/web-dist/assets/cytoscape.esm-B3I8pqwA.js +321 -0
  85. package/web-dist/assets/dagre-CXRCoUWR.js +1 -0
  86. package/web-dist/assets/dagre-VKFMJZFB-DUdNHEM9.js +4 -0
  87. package/web-dist/assets/defaultLocale-C8Fc0cco.js +1 -0
  88. package/web-dist/assets/diagram-FQU43EPY-C_EHNL09.js +3 -0
  89. package/web-dist/assets/diagram-G47NLZAW-DOt98NB-.js +24 -0
  90. package/web-dist/assets/diagram-NH7WQ7WH-uIgVP9iZ.js +24 -0
  91. package/web-dist/assets/diagram-OA4YK3LP-CGWe4oxq.js +30 -0
  92. package/web-dist/assets/diagram-WEI45ONY-C-5f7o9T.js +41 -0
  93. package/web-dist/assets/dist-DoH_9pyS.js +1 -0
  94. package/web-dist/assets/ebnfDiagram-CCIWWBDH-DoSFLtL-.js +1 -0
  95. package/web-dist/assets/erDiagram-Q63AITRT-DsCLMzEE.js +85 -0
  96. package/web-dist/assets/eventmodeling-45OFAUF4-D7GQYhiK.js +1 -0
  97. package/web-dist/assets/flowDiagram-23GEKE2U-CR371xZs.js +1 -0
  98. package/web-dist/assets/ganttDiagram-NO4QXBWP-D8UNGcBR.js +292 -0
  99. package/web-dist/assets/gitGraph-TEB2WS4Q-mC-XQzTE.js +1 -0
  100. package/web-dist/assets/gitGraphDiagram-IHSO6WYX-CkpPggS7.js +106 -0
  101. package/web-dist/assets/graphlib-B8gBHxth.js +1 -0
  102. package/web-dist/assets/index-CT6k_A5y.css +1 -0
  103. package/web-dist/assets/index-Dmo0zJc8.js +319 -0
  104. package/web-dist/assets/info-DKCQHKI2-Drg-xVbr.js +1 -0
  105. package/web-dist/assets/infoDiagram-FWYZ7A6U-CsGMTGpl.js +2 -0
  106. package/web-dist/assets/init-D6jRqBbL.js +1 -0
  107. package/web-dist/assets/ishikawaDiagram-FXEZZL3T-DNgGBlL6.js +70 -0
  108. package/web-dist/assets/journeyDiagram-5HDEW3XC-BFN2bObi.js +139 -0
  109. package/web-dist/assets/kanban-definition-HUTT4EX6-BnDPclXf.js +89 -0
  110. package/web-dist/assets/katex-CddkPoXu.js +257 -0
  111. package/web-dist/assets/line-DmLw74JM.js +1 -0
  112. package/web-dist/assets/linear-z2V0wJk9.js +1 -0
  113. package/web-dist/assets/map-DsCK-0Cs.js +1 -0
  114. package/web-dist/assets/mermaid-parser.core-DGJk39E-.js +7 -0
  115. package/web-dist/assets/mermaid.core-8aee8nsf.js +11 -0
  116. package/web-dist/assets/mindmap-definition-LN4V7U3C-CGWK_Qbm.js +96 -0
  117. package/web-dist/assets/ordinal-hYBb2elL.js +1 -0
  118. package/web-dist/assets/packet-7NZHBO7P-C5HYQyS5.js +1 -0
  119. package/web-dist/assets/path-BWPyau1x.js +1 -0
  120. package/web-dist/assets/pegDiagram-2B236MQR-WiQm887Q.js +1 -0
  121. package/web-dist/assets/pie-RZYD4A2V-DKBNMtMn.js +1 -0
  122. package/web-dist/assets/pieDiagram-ENE6RG2P-F1A8_3DO.js +39 -0
  123. package/web-dist/assets/quadrantDiagram-ABIIQ3AL-bf6a3f_f.js +7 -0
  124. package/web-dist/assets/radar-I7S5WNFK-AOKDUn-C.js +1 -0
  125. package/web-dist/assets/railroad-3IZDKUUU-DNHhkFAC.js +1 -0
  126. package/web-dist/assets/railroad-abnf-AHOZXSZD-DOXbu4iv.js +1 -0
  127. package/web-dist/assets/railroad-ebnf-EBAXGLYW-C1oE2RHD.js +1 -0
  128. package/web-dist/assets/railroad-peg-LSFZ7HO6-B4GD-bq-.js +1 -0
  129. package/web-dist/assets/railroadDiagram-RFXS5EU6-Be52T90z.js +1 -0
  130. package/web-dist/assets/requirementDiagram-TGXJPOKE-BqdMvEGK.js +84 -0
  131. package/web-dist/assets/rolldown-runtime-Bh1tDfsg.js +1 -0
  132. package/web-dist/assets/rough.esm-CSKSodPl.js +1 -0
  133. package/web-dist/assets/sankeyDiagram-HTMAVEWB-BP3X6Ofp.js +40 -0
  134. package/web-dist/assets/sequenceDiagram-DBY2YBRQ-yDHhaUzc.js +162 -0
  135. package/web-dist/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  136. package/web-dist/assets/src-C4XfhTaE.js +1 -0
  137. package/web-dist/assets/stateDiagram-2N3HPSRC-xvctsgCU.js +1 -0
  138. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-xFk0N3Cq.js +1 -0
  139. package/web-dist/assets/swimlanes-5IMT3BWC-DZMLgrjk.js +2 -0
  140. package/web-dist/assets/swimlanesDiagram-G3AALYLV-BoWrxkxy.js +8 -0
  141. package/web-dist/assets/timeline-definition-FHXFAJF6-n8sU0qlT.js +120 -0
  142. package/web-dist/assets/treeView-QDETBFTQ-Su8KloaY.js +1 -0
  143. package/web-dist/assets/treemap-6X3UGDF4-CNgRuVWf.js +1 -0
  144. package/web-dist/assets/vennDiagram-L72KCM5P-B5I9YxaY.js +34 -0
  145. package/web-dist/assets/wardley-OPB4EBWU-khMe_Wbq.js +1 -0
  146. package/web-dist/assets/wardleyDiagram-EHGQE667-d8LsqhTO.js +78 -0
  147. package/web-dist/assets/xychartDiagram-FW5EYKEG-b0CH_-wy.js +7 -0
  148. package/web-dist/favicon-16.png +0 -0
  149. package/web-dist/favicon-32.png +0 -0
  150. package/web-dist/favicon.svg +34 -0
  151. package/web-dist/icon-192.png +0 -0
  152. package/web-dist/icon-512.png +0 -0
  153. package/web-dist/icon-maskable-512.png +0 -0
  154. package/web-dist/index.html +44 -0
  155. package/web-dist/manifest.webmanifest +16 -0
@@ -0,0 +1,1172 @@
1
+ # cc-worker — design decisions
2
+
3
+ The **frizz** worker-side plugin (dir `cc-worker/`, manifest `name: "frizz"` since 2026-07-08) is
4
+ consumed by frizz worker sessions: one interactive top-level
5
+ `claude` per `.frizz/` thread, loaded via `claude --plugin-dir <repo>/cc-worker`. Each session is a
6
+ **worker bound to ONE thread** (slug in env `FRIZZ_THREAD` + a `THREAD:` line in its prompt). The
7
+ human + the frizz app are the orchestrator; the worker just drives its one thread. This records
8
+ what was ported from the orchestrator `cc/` plugin, what was dropped, and why.
9
+
10
+ ## Shared source, bundled runtime closure
11
+
12
+ - **`scripts/frizz/config.mjs` and `scripts/frizz/agent-bindings.mjs` are THIN SHIMS** that
13
+ `export *` from `../../../board/*.mjs`. cc-worker never copies config/vocab/binding
14
+ logic — there is exactly one source of truth (cc's). This assumes cc is a sibling dir (`../../cc/`
15
+ from the plugin root), the same assumption frizz's server makes (`ARCHITECTURE.md`: it imports
16
+ the board logic from `../../board/*.mjs`).
17
+ - **`bin/frizz` and `bin/frizz-update`** are cc's exact shim pattern, resolving cc's real scripts at
18
+ `../../board/{index,thread-update}.mjs` relative to the bin file (cwd-independent). They
19
+ land on the worker's Bash PATH the way cc's do. `frizz-update` is the worker's primary tool for
20
+ owning its one thread file; `frizz` lets it read/validate the board.
21
+ - **Portable artifact rule:** `src/artifacts.ts` copies the exact sibling
22
+ `board/` module closure to `runtime/board/`, beside `runtime/cc-worker/`.
23
+ The existing shims therefore resolve inside an immutable artifact when the source checkout is gone;
24
+ both the worker and cc closure are hashed in `manifest.runtimeFiles` and required at read time.
25
+ - **`agents/*.md`** are copied UNCHANGED from `cc/agents/` (16 profiles) — a worker dispatches its
26
+ own helpers at the same model/effort cells as the orchestrator.
27
+
28
+ ## Hooks ported (all gated on `FRIZZ_THREAD` — inert if the plugin is loaded anywhere else)
29
+
30
+ | Hook | Event | Ported from | What it does for a worker |
31
+ | --- | --- | --- | --- |
32
+ | `session-seed.mjs` | SessionStart (startup/resume/clear/compact) | cc `session-seed.mjs` | Injects the single-thread worker contract + the bound slug + its file path; re-grounds on compact. Also writes cc's `off` sentinel defensively (see interplay). |
33
+ | `precompact-instructions.mjs` | PreCompact (auto/manual) | new — no cc equivalent | Steers WHAT SURVIVES compaction: emits an editorial brief asking the summarizer to preserve the high-level approach (plan, alternatives rejected, rationale) at high fidelity, plus a closing "Re-grounding before continuing:" section naming the thread's scratch-directory path and the files to re-read. **PLAIN STDOUT is the channel** — cc's usual `hookSpecificOutput` JSON would be handed to the summarizer as its literal instructions. Skips sub-agent contexts (the derived scratch-directory path is only guaranteed for the top-level worker). |
34
+ | `scratchpad.mjs` | SessionStart (startup/resume/clear/compact) + PreCompact (auto/manual) + UserPromptSubmit + PostToolUse | new — no cc equivalent | Keeps a worker aware of its per-thread scratch DIRECTORY (`.frizz/threads/<sid>/`). `--mode=session-start` NAMES the files in it on the context-losing sources (compact/resume/clear) — a listing, never their content — and otherwise teaches the arrangement (write a doc, then arm `post_compaction`); `--mode=precompact` (**plain stdout**) tells the summarizer those files exist so it carries their paths forward; `--mode=nudge` fires on BOTH UserPromptSubmit and PostToolUse (mid-turn — the one that matters for long autonomous turns) once context has grown past `STALE_TOKENS` since the last write. Both nudge channels share one state file, so mid-turn firing does not multiply reminders. NOT gated on `FRIZZ_THREAD`; `--via=project` marks the repo-local registration and defers to this one inside a frizz worker. |
35
+ | `agent-dispatch.mjs` | PreToolUse(Agent) | cc `agent-dispatch.mjs` | Enforces `run_in_background:true`, strips `name`/`team_name`, appends a worker-flavored orchestration epilogue. |
36
+ | `bash-background.mjs` | PreToolUse(Bash) | new | Denies a local shell job that escapes through `&` without a later `wait` or EXIT trap, including one hidden inside a command-position `bash -c '…'`. Shell job control bypasses Claude's task registry, so Frizz cannot track or wake it; self-contained probe concurrency, and a wrapper handed to another program (`ssh`/`docker run`/`limactl shell`), remain allowed. |
37
+ | `agent-bind.mjs` | PostToolUse(Agent) | cc `agent-bind.mjs` (verbatim behavior) | Records `agentId → thread` into `.frizz/.agent-bindings.jsonl` in cc's exact format, so a worker's THREAD-tagged helper renders on the frizz board's per-thread liveness. |
38
+ | `stop-flush.mjs` | Stop | cc `frizz-stop-reminder.mjs` (dirty-check idea only) | If the worker's ONE thread file wasn't edited since the last rest, nudges it to flush its state (mtime dirty-check, cooldown-limited, least-alarming `additionalContext` channel). |
39
+
40
+ ## cc hooks DROPPED — one line each on why
41
+
42
+ - **`frizz-reminder.mjs` (UserPromptSubmit per-turn pulse)** — DROPPED. It nags the *orchestrator*
43
+ about the whole board (pending-by-status, reconcile-stale, un-drained follow-ups, revalidate-due).
44
+ A worker owns one thread and does not orchestrate a board; a per-turn board pulse is pure noise.
45
+ - **`frizz-stop-reminder.mjs` (board-wide Stop reconcile + pop-one decision queue)** — DROPPED as-is;
46
+ only its per-thread dirty-check idea is reused in `stop-flush.mjs`. The rest (reconcile every
47
+ rested agent, pop the next human-blocked thread and present it) is orchestrator decision-queue work
48
+ the frizz app + human own, not the worker.
49
+ - **`frizz-notify-surface.mjs` (Stop) + `frizz-notify` bin/`notify.mjs`** — DROPPED. The durable
50
+ WIN/DECISION/BLOCKER notification queue is an orchestrator surfacing channel; in frizz the UI
51
+ surfaces "awaiting you" from thread `status: blocked` + `status_text` directly, so the worker needs
52
+ no separate notify queue.
53
+ - **`frizz-subagent-rest.mjs` (SubagentStop recorder) + `frizz-rest-guard.mjs` (SubagentStop guard)**
54
+ — DROPPED. These feed the orchestrator's board-wide "reconcile every rested dispatched agent"
55
+ machinery (`.rested-agents.jsonl`, the `.dispatch-count` gate). A worker actively collects its own
56
+ handful of helpers before resting (contract in SKILL.md); it does not need the board-scale
57
+ rest-reconciliation guard. The worker-facing half of the rest-guard's lesson (run long ops inline,
58
+ don't rest on a waiter) is carried in the dispatch epilogue instead.
59
+ - **`frizz-thread-edit-steer.mjs` (PostToolUse Edit/Write)** — DROPPED. It's an orchestrator
60
+ convenience that steers an in-flight agent when the orchestrator hand-edits a thread; a worker edits
61
+ its OWN thread and dispatches its OWN helpers, so there's nothing to cross-steer.
62
+ - **`session-end.mjs` (SessionEnd heartbeat clear)** — DROPPED. It clears the session-ownership
63
+ HEARTBEAT so a dead orchestrator's threads orphan. Workers don't participate in cc's multi-session
64
+ ownership model (no `owner_session` claims, no heartbeat) — the frizz app tracks which session
65
+ drives which thread — so there's no heartbeat to clear.
66
+ - **The `.dispatch-ledger.jsonl` write + THREAD-existence DENY gate + `.dispatch-count` bump**
67
+ (inside cc's `agent-dispatch.mjs`) — DROPPED from the worker's PreToolUse. The ledger is a
68
+ compaction-durable orchestrator record of which-agent-serves-which-thread across MANY threads; the
69
+ THREAD-existence gate enforces the orchestrator's "file the thread before dispatching" discipline;
70
+ the count only gates the (dropped) SubagentStop recorder. A worker owns exactly one, already-created
71
+ thread and its helpers own no thread, so none apply. The `.agent-bindings.jsonl` write — the piece
72
+ that actually renders sub-agent liveness on the board — IS kept (via `agent-bind.mjs`).
73
+
74
+ ## Interplay with the orchestrator `cc` plugin (double-hook analysis)
75
+
76
+ **Question:** if the user has `cc` (the frizz orchestrator plugin) enabled globally AND a frizz
77
+ worker session starts in the same repo, do both plugins' hooks fire (double-hook)?
78
+
79
+ **Finding — NO, not by default. cc is inert in a fresh worker session.** cc's every hook is gated on
80
+ `frizzActive(projectDir, sessionId)` (`board/config.mjs`). That gate is **opt-IN per
81
+ session**: it requires `.frizz/` to exist AND a per-session sentinel at
82
+ `.frizz/.session-state/<session_id>` containing `on` (written by `frizz on` / the orchestrator frizz
83
+ skill's Step 0). With no sentinel it returns **false** — the documented default:
84
+
85
+ ```
86
+ // config.mjs frizzActive(), final line:
87
+ return false; // DEFAULT: OPT-IN — dormant until this session runs `frizz on`
88
+ ```
89
+
90
+ A freshly-spawned frizz worker has a distinct `CLAUDE_CODE_SESSION_ID` and never runs `frizz on`, so
91
+ cc's `frizzActive()` is false for it → **all cc hooks are silent no-ops in the worker.** Meanwhile
92
+ cc-worker gates on the orthogonal `FRIZZ_THREAD` env, so the two plugins key off different signals
93
+ and do not both activate. No double-hook by default.
94
+
95
+ **Residual risk:** if, inside a worker session, someone runs `frizz on` or loads the orchestrator
96
+ `frizz` skill (whose Step 0 runs `frizz on`), cc's `frizzActive()` flips true AND cc-worker is active →
97
+ both fire (you'd get orchestrator board nags inside a worker — wrong).
98
+
99
+ **Mitigation implemented (cheap + safe + reversible):** `session-seed.mjs` writes cc's OWN per-session
100
+ `off` sentinel for the worker's session id via cc's shared `setSessionOverride(dir, sid, 'off')` on
101
+ every worker SessionStart. `frizzActive()` short-circuits to false on an `off` override
102
+ (`if (override === 'off') return false`), so cc is **guaranteed dormant** in a worker session even if
103
+ something later attempts to activate it — unless the human deliberately runs `frizz on` afterward
104
+ (which overwrites the sentinel), the explicit "I want this session to orchestrate too" escape hatch.
105
+ This uses cc's own public API (identical to what `frizz off` does), touches only gitignored runtime
106
+ state keyed on this worker's session id, and does NOT disable cc-worker (which gates on
107
+ `FRIZZ_THREAD`, not the sentinel). It's the safest cheap option: it neutralizes the other plugin
108
+ without a UI-side plugin-disable flag (Claude Code has no per-invocation "disable plugin X" flag), and
109
+ the worker SKILL.md additionally tells the worker not to run `frizz on` / load the orchestrator skill.
110
+
111
+ ## plugin.json
112
+
113
+ `name: "frizz"` (renamed from `frizz-worker` on 2026-07-08 — see the follow-up note), `version: "0.1.2"`,
114
+ `license: "MIT"`. Hooks are auto-discovered from `hooks/hooks.json` (same as cc — plugin.json carries
115
+ no explicit hooks reference); every hook command is wired via `${CLAUDE_PLUGIN_ROOT}`.
116
+
117
+ ## Claude settings-source isolation — deliberately deferred
118
+
119
+ The portable worker launch passes its per-session plugin with `--plugin-dir` on both spawn and
120
+ resume, and clears only `CLAUDE_CODE_SUBAGENT_MODEL` plus `CLAUDE_CODE_EFFORT_LEVEL`: those inherited
121
+ variables would silently defeat Frizz's selected worker/profile. It deliberately does **not** replace
122
+ `HOME`, `CLAUDE_CONFIG_DIR`, or Claude's settings sources. Doing so would also change authentication,
123
+ user-approved permissions, MCP configuration, and global plugin behavior; that is a product-policy
124
+ decision, not an artifact-portability implementation detail. A future isolation policy must specify
125
+ which settings/auth surfaces are preserved before adding `--settings`, a config-home override, or a
126
+ global-plugin disable mechanism.
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
+
145
+ ## 2026-07-02: Stop hook removed
146
+ stop-flush.mjs is no longer wired (script kept for reference). User call: under frizz the
147
+ tailer/board already surface worker state live, and the block-until-file-edited nag forced even
148
+ trivial workers into Read/Edit dances that render as noise in the chat UI. Thread-file discipline
149
+ remains a prompt-level contract (worker system prompt + SKILL), not a hook-enforced gate.
150
+
151
+ ## 2026-07-08: Developer-experience port — doctrine, thread-type presets, dialectic
152
+ Goal: carry the old cc/ plugin's developer experience (minus the orchestrator machinery) into
153
+ frizz workers. What landed:
154
+ - **`skills/dialectic/SKILL.md`** — ported from `cc/skills/dialectic/` (self-contained dueling-
155
+ sub-agents methodology; no board/reconcile dependency). Its model-tier references use this
156
+ plugin's namespace (`frizz:opus-high`, etc. — see the naming note below).
157
+ - **`skills/worker/SKILL.md`** — added two sections: "Choosing a helper's model + effort" (the
158
+ full Haiku/Sonnet/Opus tiering doctrine + effort ladder + bias-to-Opus corollary, adapted from
159
+ `cc/skills/frizz/SKILL.md:218-234` for a worker dispatching its OWN helpers) and "Thread-type
160
+ presets" (research / audit / implementation / planning — deliverable shape + "done" bar each,
161
+ derived from that skill's `:92-150`/`:242-317`/`:463` framing). Also added a status-field
162
+ discipline section (later split into `activity` + `status_text` — see the follow-up) and an
163
+ "awaiting your OWN sub-agent is NOT blocked" clarification.
164
+ - **`packages/server/src/workerPrompt.ts`** (frizz system prompt, not in this plugin) — carries the terse version
165
+ of the same three: a "Status discipline" block, the model/effort doctrine in the Sub-agents
166
+ section, and a "Thread types" section. This is the maintainer's explicit ask that the preset
167
+ vocabulary ride in the SYSTEM prompt passed to every worker. No dispatch.ts change, so no frizz
168
+ server restart is needed — `loadWorkerPrompt()` re-reads the file per dispatch and each spawned
169
+ `claude` rescans this plugin dir fresh.
170
+
171
+ ### Naming: subagent_type is `frizz:<model>-<effort>` (plugin renamed to `frizz` on 2026-07-08)
172
+ The plugin's manifest `name` (`.claude-plugin/plugin.json`) drives the subagent-type NAMESPACE, and
173
+ each agent file's frontmatter `name` drives the agent name — verified empirically with a throwaway
174
+ plugin (`name:"frizz"` + agent `name:opus-high` → `frizz:opus-high`); the plugin DIRECTORY name does
175
+ not matter. So the profiles dispatch as `frizz:opus-high` / `frizz:sonnet-medium` / `frizz:haiku`, and
176
+ the skills as `frizz:worker` + `frizz:dialectic`. A BARE name (`opus-high`, `haiku`) does NOT resolve
177
+ — the Agent tool returns "Agent type '…' not found. Available agents: … frizz:haiku …" — so every
178
+ doctrine/dialectic reference uses the `frizz:`-prefixed form (confirmed end-to-end: `subagent_type:
179
+ frizz:haiku` returns PONG). See the follow-up note below for why the name is `frizz`, not `frizz-worker`.
180
+
181
+ ## 2026-07-08 (follow-up): status split, validation hook, `frizz:` namespace rename
182
+ Three maintainer refinements landed on top of the port:
183
+
184
+ **1. `status_text` split into `activity` + `status_text`.** The single overloaded field became two:
185
+ `activity` = the form-constrained LIVE label the UI renders beside the spinner (single line, ≤100
186
+ chars, present-progressive gerund); `status_text` = the classic 1–2-sentence human gloss that also
187
+ doubles as THE ask on a human-`blocked` thread (queue cards headline it; no gerund constraint). The
188
+ gerund/≤100 discipline moved OFF `status_text` and ONTO `activity` in `packages/server/src/workerPrompt.ts` +
189
+ `skills/worker/SKILL.md`. (UI-side `activity` plumbing/rendering — board JSON → ThreadView → listing
190
+ row — is a SIBLING agent's scope; not touched here beyond `packages/server/src/workerPrompt.ts`.)
191
+
192
+ **2. New PostToolUse validation hook — `hooks/thread-frontmatter-validate.mjs`** (matcher
193
+ `Edit|Write|MultiEdit`, wired in `hooks/hooks.json`). Gated on FRIZZ_THREAD + a top-level
194
+ `.frizz/<slug>.md` path (dotfiles + `.findings/` sidecars skipped via the vendored `threadSlug`). On
195
+ every thread-file edit it re-reads + validates the frontmatter and, on a HARD violation, returns
196
+ `{"decision":"block","reason":…}` (PostToolUse block — the worker sees the quoted reason and
197
+ re-edits); soft issues warn via `systemMessage`; a clean edit is silent; ANY error fails OPEN
198
+ (exit 0). Rules (mirroring cc's board validator `index.mjs:302-335`): required `title`+`status`;
199
+ `status` ∈ the vocab (legacy aliases warn, not block); `activity` single-line/≤100/gerund-heuristic
200
+ (first word ends in "ing"); `blocked` ⇒ at most ONE of `blocking_threads`/`revalidate_at`, and
201
+ human-blocked (neither) ⇒ `status_text` required; `status_text` >240 chars warns. SELF-CONTAINED: it
202
+ VENDORS minimal copies of cc's frontmatter parser (`index.mjs:82`), path matcher
203
+ (`frizz-thread-edit-steer.mjs:67`), and vocab (`config.mjs:291`/`:301`) — cc-worker must not import cc
204
+ at runtime. Verified: 10 direct unit cases (pass/block/warn/inert) + a real headless worker whose bad
205
+ Write was blocked with the exact quoted reason.
206
+
207
+ **3. Plugin renamed `frizz-worker` → `frizz`; worker skill renamed `frizz-worker` → `worker`.** The
208
+ maintainer disliked the old worker-prefixed dispatch namespace. Renamed the manifest `name` to `frizz` and the 16 agent
209
+ files + their frontmatter from `frizz-<model>-<effort>` to `<model>-<effort>` (bare `haiku`), so
210
+ dispatches read `frizz:opus-high` etc. Renamed the worker skill dir + frontmatter `frizz-worker` →
211
+ `worker` (giving `frizz:worker`, not the stutter `frizz:frizz-worker`). The plugin DIRECTORY stays
212
+ `cc-worker/` (dispatch.ts points at that path — unchanged, no server restart). Every reference in
213
+ `packages/server/src/workerPrompt.ts`, both skills, the hooks, and this file was updated; a grep for the old prefix token is clean.
214
+
215
+ **Accepted name collision.** The old GLOBAL orchestrator plugin (`cc/`) is ALSO named `frizz`. This is
216
+ accepted as harmless: `cc/` is disabled in `~/.claude/settings.json` (`"frizz@frizz": false`) and, even
217
+ when enabled, loads only in ORCHESTRATOR sessions (a different process class) — never in a frizz
218
+ worker, which loads ONLY this plugin via `--plugin-dir cc-worker`. Verified: the headless worker env
219
+ (same settings.json) registers a clean `frizz:*` set with no `cc/` agents present. If a user ever
220
+ loaded BOTH plugins in one session, Claude Code would namespace-collide two `frizz` plugins — but that
221
+ combination does not occur on the worker path.
222
+
223
+ ## 2026-07-08 (campaign): `needs-human` first-class status + interactive-prompt deny hooks
224
+ Part of the board-wide redesign (owned jointly with the frizz UI half): `needs-human` becomes a
225
+ first-class frizz status — the declared "awaiting a human" state and the queue's definition — while
226
+ `blocked` narrows to MACHINE-waits only. The vocab/parser/validator changes live in the shared cc/
227
+ scripts (`config.mjs` STATUS/STATUS_ALIASES + new `effectiveStatus()`, `index.mjs`, `decisions.mjs`,
228
+ `statusline-frizz.mjs`, `thread-update.mjs`, `frizz-reminder.mjs`) — consumed by frizz's readBoard
229
+ shell-out, so they take effect on the next board rebuild with NO server restart. cc-worker-side:
230
+
231
+ - **`hooks/thread-frontmatter-validate.mjs`** — vendored vocab bumped: `needs-human` is canonical,
232
+ `needs-decision` aliases to it. New rules: a `needs-human` thread (incl. a legacy `blocked` with no
233
+ machine field, which reads as needs-human via the inlined `effectiveStatus`) REQUIRES a
234
+ `status_text` (hard BLOCK); a machine-`blocked` thread with no mechanism field is a WARN suggesting
235
+ needs-human (not a block — legacy tolerance); >1 mechanism stays a block.
236
+ - **`skills/worker/SKILL.md` + `packages/server/src/workerPrompt.ts` + `hooks/session-seed.mjs`** — the worker status
237
+ guidance rewritten to the new contract: an ask OR a result needing review → `status: needs-human`
238
+ with a `status_text` ask ("Review: …"); `blocked` is machine-only; `done` means NOTHING is left for
239
+ the human (Mark-as-done is the human's acknowledgment — never jump straight to `done` when review
240
+ is pending).
241
+ - **`hooks/deny-ask.mjs`** (existing, PreToolUse `AskUserQuestion`) — deny reason re-pointed at
242
+ `--status needs-human`. `AskUserQuestion` is a real tool → cleanly deniable via PreToolUse; verified
243
+ by piping the hook its exact payload (deny + reason) and confirming inert when FRIZZ_THREAD unset.
244
+ - **`hooks/deny-plan.mjs`** (NEW, PermissionRequest `ExitPlanMode`) — denies the plan-approval prompt.
245
+ MECHANISM (per the Claude Code hooks docs): `ExitPlanMode` is a PERMISSION surface, not a plain
246
+ tool, so it is denied via a **PermissionRequest** hook (matcher exactly `ExitPlanMode`), NOT
247
+ PreToolUse. The deny JSON is `{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":
248
+ {"behavior":"deny"}}, "additionalContext":"<redirect>"}` — the instructive redirect rides
249
+ **top-level `additionalContext`** (NOT `decision.message`), which Claude injects to the model as a
250
+ plain-text system-reminder; exit 0 with the JSON on stdout (exit 2 makes Claude ignore it). CAVEAT:
251
+ PermissionRequest hooks do NOT fire under `claude -p` (headless print mode), so the `-p` smoke test
252
+ can't exercise it — but a frizz worker runs as an INTERACTIVE `claude` session inside its
253
+ session-broker daemon, where they DO fire. Both deny hooks are FRIZZ_THREAD-gated and fail-open. (`AskUserQuestion` was not surfaced as
254
+ an invokable tool in the `-p` harness either, so both hooks were verified by piping their exact hook
255
+ payloads rather than by driving a live prompt.)
256
+
257
+ ### `hooks/perm-policy.mjs` — the worker's permission policy (2026-07-25)
258
+
259
+ Replaces the observe-only `perm-observe.mjs`. Same registration (PermissionRequest, matcher `*`), but
260
+ it now DECIDES: allow / deny / defer, first match wins.
261
+
262
+ WHY IT DECIDES. frizz dispatches Claude workers at `--permission-mode auto`
263
+ (`WORKER_DISPATCH_PERMISSION`), and `auto` is **not** non-interactive — its classifier still raises a
264
+ prompt for anything it judges risky. With nobody at the keyboard that parks a thread invisibly; one sat
265
+ blocked on a `git push` for over a day. The blunt alternative was dispatching at `bypassPermissions`,
266
+ which was deliberately NOT chosen: bypass removes the decision POINT, so nothing can ever inspect a
267
+ request again. Keeping `auto` and deciding here preserves the seam — and Claude Code labels the outcome
268
+ in the pane ("Allowed by PermissionRequest hook"), so an auto-approval stays attributable rather than
269
+ being indistinguishable from bypass.
270
+
271
+ THE TABLE ships UNIVERSAL rules only; this plugin loads for every project frizz drives, so a rule that
272
+ is right for one repo is wrong for the next. `catastrophic-delete` and `raw-disk-write` deny outright
273
+ (strictly safer than bypass, which would have allowed both). `restrictive-mode` DEFERS whenever
274
+ `permission_mode !== "auto"`, which is what makes a genuine lower-permission mode usable: move a thread
275
+ to `default` with the live permission control and its prompts come back. `FRIZZ_PERM_POLICY=review`
276
+ defers everything. Fail-safe INVERTS the old observer's fail-open — for a hook that can APPROVE, any
277
+ error must fall back to asking, never to allowing.
278
+
279
+ PAYLOAD (verified live, correcting an earlier note that claimed otherwise): the PermissionRequest hook
280
+ input carries `session_id`, `transcript_path`, `cwd`, `prompt_id`, `permission_mode`, `effort`,
281
+ `hook_event_name`, `tool_name`, `tool_input`, and `permission_suggestions`. That is enough for a real
282
+ per-tool/per-command policy. Allow JSON is
283
+ `{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","updatedInput":<tool_input>}}}`.
284
+
285
+ KNOWN LIMIT: an explicit `ask` RULE outranks this hook. `"permissions":{"ask":["Bash(git push:*)"]}`
286
+ raises a prompt an `allow` here does not override — Claude Code says so on the prompt itself. Isolated
287
+ against a hook that allows unconditionally (it prompted too), so this is Claude Code precedence, not a
288
+ defect, and arguably correct: an explicit human rule should beat a blanket policy. A repo carrying
289
+ `ask` rules can therefore still park a worker; the fix is that repo's settings.
290
+
291
+ MARKER: `<stateDir>/perm-requests/<slug>.json` now carries `decision` / `rule` / `reason` / `command`
292
+ alongside the original fields. The tailer treats ONLY a deferred request as a human block (an
293
+ auto-resolved one must never card as "Needs you"), and surfaces the last DENIAL as `permPolicy`.
294
+ A marker with no `decision` (an older plugin build) still blocks exactly as before.
295
+
296
+ APPROVALS ARE NOT SURFACED — reversed 2026-08-07. The tailer originally retained allows too, on the
297
+ reasoning that an approval leaves no other durable record (Claude Code renders "Allowed by
298
+ PermissionRequest hook" in the pane and writes nothing to the transcript), and Chat rendered them as
299
+ one quiet `Auto-approved <command> · rule <name>` line. The flaw was that `permPolicy` has no CLEAR:
300
+ the line stuck to the bottom of the thread permanently, so one routine `git status --short` read as
301
+ the thread's standing condition long after the turn that ran it (maintainer: *"This message just
302
+ showed up randomly, and now it's stuck showing up in the thread forever … it's fucking useless"*).
303
+ Nothing was blocked and nobody was waiting, so the visibility it bought was worth less than the
304
+ permanent noise. Denials keep the card: they are rare, they changed what the worker could do, and the
305
+ refusal also lands in the transcript, so the card is a pointer to something real rather than the only
306
+ copy of it. If approval visibility is ever wanted again it needs an EXPIRY (or a per-turn scope) —
307
+ re-adding the sticky line is not the fix.
308
+
309
+ VERIFICATION: `scripts/verify-perm-marker.mjs` runs the REAL hook the way Claude Code does and
310
+ asserts every branch. The allow path was additionally driven against a REAL interactive `claude` (it
311
+ cannot be exercised under `claude -p`, where PermissionRequest hooks do not fire).
312
+
313
+ ### The `--settings` permission floor — BUILT, verified, then deliberately REMOVED (2026-07-08)
314
+ A `--settings` permission FLOOR was added to `ui/dispatch.ts` (`WORKER_DENY_SETTINGS =
315
+ {"permissions":{"deny":["AskUserQuestion","ExitPlanMode"]}}`, both command builders) and verified to
316
+ work: `claude -p --settings '{deny:[AskUserQuestion,ExitPlanMode,Bash]}'` → the model reports
317
+ `Bash: No` (control — a bare-name deny removes a normally-present tool from context), `Read/Write:
318
+ Yes`, `AskUserQuestion/ExitPlanMode: No`. It was then **removed on the maintainer's call**, and the
319
+ reasoning is worth keeping (it corrects the earlier "floor first, hooks failover" story):
320
+
321
+ - A bare-name deny removes the tool from the tool LIST, but a model **knows `AskUserQuestion` /
322
+ `ExitPlanMode` from TRAINING** and can still attempt them. With the floor in place, that attempt
323
+ hits a generic "no such tool" permission error — NOT the hook's instructive `needs-human` redirect
324
+ (a permission `deny` is evaluated regardless of hook output and takes precedence, so the model sees
325
+ the generic denial). So the floor can actually **prevent the tool-block education process**.
326
+ - Therefore: **hooks-only enforcement.** The deny HOOKS are themselves a hard deny AND they teach on
327
+ contact (their `needs-human` redirect reaches the model — verified: deny-ask emits a PreToolUse
328
+ `permissionDecisionReason`, deny-plan a PermissionRequest top-level `additionalContext`). The floor
329
+ is gone; `dispatch.ts` + `server.test.ts` are reverted to clean.
330
+
331
+ ### Plan-mode softlock fix (2026-07-08)
332
+ A worker in plan mode that calls `ExitPlanMode` would be denied by deny-plan — but plan mode ALSO
333
+ blocks file edits, so the redirect ("write the plan into your thread") is impossible to follow: a
334
+ softlock. deny-plan does not branch on the session's mode, so it cannot pass plan mode through. Fixed
335
+ at the SOURCE instead: `ui/dispatch.ts` `workerPermissionMode()` coerces `--permission-mode plan` → `auto` inside
336
+ BOTH command builders (so dispatch, adopt, AND resume never spawn a worker in plan mode). Workers plan
337
+ by writing the plan into the thread + `status: needs-human` (the contract), which has no plan-mode
338
+ requirement — so nothing is lost. deny-plan then denies `ExitPlanMode` UNCONDITIONALLY when gated:
339
+ for a real frizz worker (never in plan mode) that is always a spurious call → deny + redirect is
340
+ correct. RESIDUAL GAP (accepted, documented): the deny could softlock only a FOREIGN session that is
341
+ simultaneously in plan mode AND running with FRIZZ_THREAD set AND this plugin loaded — a combination
342
+ frizz never produces. A normal plan-mode session outside frizz is untouched (deny-plan is inert
343
+ without FRIZZ_THREAD). Follow-up for UI honesty: `web/src/lib/options.ts` still offers "plan" in the
344
+ dispatch permission-mode dropdown (coerced to `auto` at spawn) — the sibling can drop it there.
345
+
346
+ ### Malformed-thread one-click repair — read-side recovery (2026-07-09)
347
+ INCIDENT: a worker that spawned before the frontmatter-validation write-hook existed wrote
348
+ `nub/.frizz/sandbox-windows-backend.md` with its metadata in **bold prose** instead of YAML
349
+ frontmatter. The board banner correctly reported "sandbox-windows-backend.md: no YAML frontmatter",
350
+ but the thread was INVISIBLE to the queue/status system (the parser can't read its title/status →
351
+ `status: ?`) until the orchestrator hand-edited YAML. Maintainer directive: "make sure that doesn't
352
+ happen again."
353
+
354
+ TWO-SIDED DEFENSE:
355
+ - **Write-side (already existed):** the frontmatter-validation file-tool hook blocks a compliant
356
+ worker from writing a thread `.md` with no frontmatter in the first place.
357
+ - **Read-side (this change):** RECOVERY for any straggler the write-hook can't catch — pre-hook
358
+ files, hand edits, and (the residual, by design) **Bash-written files that bypass the file-tool
359
+ hooks entirely**. The board now ships STRUCTURED, classified errors so the UI can offer one-click
360
+ repair.
361
+
362
+ MECHANISM:
363
+ - `board/index.mjs` `--json` gains a parallel `errorItems: [{file, kind, message}]`
364
+ (`kind: 'no-frontmatter'` = repairable, else `'other'`). The parser classifies — it knows exactly
365
+ why it rejected the file. The legacy `errors: string[]` array is emitted UNCHANGED alongside it.
366
+ - `errorItems` flows through `readBoard`/`frizz.ts` → `board.ts assemble()` → `BoardSnapshot` +
367
+ `BoardMeta` (so the repair affordance survives a board delta, not just the keyframe) → the
368
+ TodosView banner, which renders a **Repair** button per `no-frontmatter` item.
369
+ - `repairThread({file})` RPC (`repair.ts`) validates the file is a real `.md` DIRECTLY under `.frizz/`
370
+ (resolve + dirname===root guard; rejects `../`, `sub/`, absolute paths), refuses any file that
371
+ already has a `---` block (repair is ONLY the missing-frontmatter case), then PREPENDS minimal
372
+ frontmatter: `title` from the first `# H1` (else the filename slug), `status: active`, and a
373
+ standing `status_text` flag that the status is unverified. Then a board rebuild.
374
+
375
+ CONSERVATIVE ON PURPOSE: repair NEVER infers status from prose. This morning's file said "DONE" in
376
+ bold — guessing wrong silently is worse than surfacing. `status: active` makes the thread visible;
377
+ if its agent is gone, the runtime crash-net cards it for human attention — the correct escalation.
378
+
379
+ RESIDUAL (accepted, documented): a Bash-written `.frizz/*.md` bypasses the file-tool hooks by design,
380
+ so the write-side can't prevent it — the read-side repair is the safety net that heals it in one
381
+ click.
382
+
383
+ ## 2026-07-09: v2 worker contract — the session-first rebuild (fences + scratchpad; thread-file contract DELETED)
384
+ The maintainer settled frizz on a SESSION-FIRST model: threads ARE claude sessions and the human's
385
+ dashboard shows the session TRANSCRIPT. Queue membership is explicit: `question` hands off to the
386
+ human, `done` queues a checked completion, process-level blocks surface themselves, `awaiting`
387
+ excuses a machine wait, and bare rest stays quiet. The entire `.frizz/<slug>.md` ownership contract is
388
+ GONE: no thread files, no frontmatter, no `status`/`activity`/`status_text`, no `needs-human`, no
389
+ `blocked` machine fields, no `hasPlan`/`## Plan`, no `frizz-update`. Workers now SIGNAL through their
390
+ FINAL MESSAGE and PERSIST through a SCRATCHPAD. This is the cc-worker-side realignment.
391
+
392
+ **The new signal model (taught in `packages/server/src/workerPrompt.ts` §"End-of-turn signals" + `skills/worker/SKILL.md`):**
393
+ - **Bare rest is quiet** — a rested thread with no fence does NOT enter Needs-you and is not a human
394
+ handoff. Human handoff is explicit via `question` (or a real process-level block).
395
+ - **` ```done `** — work complete + stands; body = 1–4 lines of what shipped + where. Renders a
396
+ checked success card in the queue until explicit Archive; the fence MUTATES NOTHING
397
+ (maintainer-settled), and a follow-up may still wake the worker.
398
+ - **` ```awaiting `** — waiting on a MACHINE (CI/PR/timer/session); body may lead with `kind: value`
399
+ hint lines, kind ∈ pr|ci|timer|session. NEVER for a human wait.
400
+ - **` ```question `** — unchanged grammar (question / approval / multi / danger; trailing `- A. …`
401
+ options + `Recommendation:`), now the ONLY handback-for-input; no status flip accompanies it.
402
+ - CONSISTENCY: the taught grammar matches the shared parser (`packages/shared/src/index.ts`
403
+ `ThreadFence` kind ∈ done|awaiting, `AwaitingHint` kind ∈ pr|ci|timer|session). Opening line is
404
+ exactly ` ```done `/` ```awaiting ` (nothing after the language word); exactly one fence, at the end.
405
+
406
+ **The scratchpad (`.frizz/threads/<session-id>/scratch.md`) — new §"Scratchpad" in both docs:** free-form
407
+ markdown, NO schema, NO validation. It is the worker's compaction-proof working memory (survive-
408
+ compaction to-do lists / work queues / Ralph-style epic checklists live here, not in ephemeral
409
+ context) AND the shared blackboard for parallel sub-agents (shared state is written into it; its PATH
410
+ is passed into every sub-agent prompt; helpers READ it, the worker folds their results back in). The
411
+ path is server-established convention already wired through `shared` (`scratchpadPath`), `router.ts`
412
+ (reads `.frizz/threads/<session_id>/scratch.md`), and `dispatch.ts`.
413
+
414
+ **Hooks changed:**
415
+ - **DELETED `hooks/thread-frontmatter-validate.mjs`** + its `hooks.json` PostToolUse `Edit|Write|
416
+ MultiEdit` entry. It validated thread-file frontmatter (status vocab / gerund `activity` / machine
417
+ fields) — a contract that no longer exists. Nothing left to validate on file edits.
418
+ - **DELETED `hooks/stop-flush.mjs`** (already UNWIRED since the 2026-07-02 Stop-hook removal; kept
419
+ "for reference" then). Its sole job was nudging the worker to flush state into its thread FILE —
420
+ dead with the thread-file contract gone. No `hooks.json` entry to remove (it was never re-wired).
421
+ - **`hooks/session-seed.mjs`** — reseeded to the v2 contract: signal via the final message (bare rest
422
+ is quiet; done queues checked completion; awaiting excuses a machine wait; question asks) + the
423
+ scratchpad, whose concrete path is derived from
424
+ the SessionStart `session_id` (`currentSessionId(input.session_id)` → `.frizz/threads/<sid>/scratch.md`) and
425
+ named in the seed. FRIZZ_THREAD gating + the cc double-hook `off`-sentinel defense KEPT verbatim;
426
+ the compact re-grounding now points at the scratchpad, not a thread file.
427
+ - **`hooks/agent-dispatch.mjs`** — epilogue no longer says "don't edit `.frizz/` thread files or
428
+ config.yml"; now "don't edit the dispatcher's scratchpad (`.frizz/threads/<session-id>/scratch.md`) — READ it for shared
429
+ context if its path is in your prompt, report in your FINAL MESSAGE." Background/name-strip
430
+ enforcement unchanged.
431
+ - **`hooks/deny-ask.mjs`** — redirect retargeted off `frizz-update … --status needs-human`: now "ask
432
+ in your FINAL MESSAGE with ```question blocks, then rest; a question IS the handback (no extra
433
+ fence)." Still PreToolUse(AskUserQuestion), FRIZZ_THREAD-gated, fail-open.
434
+ - **`hooks/deny-plan.mjs`** — redirect retargeted off "write the plan into your thread `.frizz/<slug>.md`
435
+ + status: needs-human": now "write the plan into `.frizz/plans/<topic>.md` and/or the scratchpad;
436
+ ask via a ```question approval block." PermissionRequest(ExitPlanMode) mechanism + the plan-mode
437
+ softlock reasoning (dispatch.ts coerces plan→auto) unchanged.
438
+ - **`hooks/agent-bind.mjs`** — UNCHANGED (functionally). It records `agentId → thread` into
439
+ `.frizz/.agent-bindings.jsonl` from a helper's `THREAD: <slug>` tag; that tag still rides the
440
+ per-thread dispatch and references no dead status/frontmatter contract. NOTE for the server/tailer
441
+ verticals: session-first sub-agent liveness is now TAILER-derived (`ThreadView.subAgents`), so the
442
+ `.agent-bindings.jsonl` binding this hook writes may be VESTIGIAL. Left in place (harmless,
443
+ fail-open, out of this vertical's delete scope) — a candidate for removal once the tailer path is
444
+ confirmed to fully supersede board-side `bindingsByThread`.
445
+
446
+ **`skills/worker/SKILL.md`** — rewritten to the same pillars (version 0.2.0): the signal model
447
+ replaces the status-vocabulary sections; a scratchpad section replaces the "own ONE thread file"
448
+ sections; thread-type presets keep the research/audit/implementation/planning taxonomy but strip
449
+ every status reference (planning now delivers a `.frizz/plans/<topic>.md` artifact). The sub-agents
450
+ section adds "pass the scratchpad path into helper prompts." `skills/dialectic` untouched.
451
+
452
+ **`plugin.json`** description updated: no more "validate thread-file frontmatter"; now fence signals
453
+ + scratchpad blackboard + sub-agent profiles + deny-ask/deny-plan.
454
+
455
+ **What the server/web verticals must know:** (a) FRIZZ_THREAD must keep being passed at spawn — every
456
+ hook still gates on it. (b) The scratchpad path convention is `.frizz/threads/<session-id>/scratch.md` where
457
+ `<session-id>` is the pinned `--session-id` (the same id the SessionStart hook sees as `session_id`);
458
+ the seed hook NAMES that concrete path to the worker, so dispatch must keep pinning `--session-id`.
459
+ (c) `packages/server/src/dispatch.ts` `composePrompt()` STILL emits the dead per-thread contract
460
+ ("You own `.frizz/<slug>.md` … set `status: blocked` … Set `status: done`") — that is server-vertical
461
+ scope (not editable here), but it now CONTRADICTS the v2 WORKER_PROMPT and must be rewritten to the
462
+ fence/scratchpad model (or dropped) by the dispatch owner. Flagged to server-core.
463
+
464
+ ## 2026-07-12: awaiting reversal — park only human/timestamp gates; keep automation active
465
+
466
+ The v2 rule above made `awaiting` a broad machine-wait bucket. In practice workers emitted a fence
467
+ for CI, bots, releases, and merge progression, then returned with no process actually owning the
468
+ next transition. The rail's hourglass therefore implied a watcher that often did not exist. The
469
+ contract is now narrower:
470
+
471
+ - `awaiting` is a deliberate PARK for either `human: <actor + exact external review/approval>` or
472
+ `timer: <ISO-8601 instant>`. The dashboard operator's own decision remains `question`.
473
+ - CI, automated review, releases/deploys, merge queues, and already-authorized merge progression
474
+ stay ACTIVE. Claude workers use a background `Bash` one-shot or `Monitor`; Codex workers keep a
475
+ blocking exec session alive and poll it. Their completion/event re-invokes the worker.
476
+ - `pr:` / `ci:` / `session:` continue to parse so old transcripts do not break. The existing PR/CI
477
+ waker remains a compatibility bridge, but workers must not create new waits with those hints.
478
+ `timer:` remains the durable scheduler path across process/session restart. `human:` is descriptive
479
+ and intentionally not auto-fired.
480
+ - Every follow-up clears the old fence. “Back to awaiting” requires a fresh check: re-emit a current
481
+ human/timer fence, or re-arm automation and remain active.
482
+
483
+ Claude Code 2.1.207 was audited before teaching this. frizz does not pass `--tools`,
484
+ `--allowedTools`, or `--disallowedTools`, and its helper profiles only select model/effort, so wait
485
+ tools are available to top-level workers and helpers. `Monitor` defaults to 300,000 ms (maximum
486
+ 3,600,000 ms); `persistent:true` runs until `TaskStop` or session end. Background Bash reports an
487
+ output path; `TaskOutput` exists but is deprecated, so workers should `Read` that path for diagnostics.
488
+ Both Monitor and background Bash are session/process-bound, which is why durable wall-clock checks
489
+ remain `timer:` fences. Helpers must not return a final handoff while they still own a live watcher;
490
+ the top-level worker owns long-lived CI/PR/merge progression.
491
+
492
+ ## 2026-07-13: ordinary rest returns to Queue; human Snooze/Archive own triage
493
+
494
+ The quiet-bare-rest rule above was reversed after live use showed that an owned Frizz worker could
495
+ come to rest without choosing a fence and disappear from the only surface the operator routinely
496
+ triages. Queue membership is now server-derived from process rest, not dependent on perfect worker
497
+ signaling:
498
+
499
+ - Every owned, open session whose top-level turn is genuinely at rest enters Queue by default.
500
+ - A live child/Monitor still counts as in-flight work. A truthful external-human or future-timer
501
+ `awaiting` fence remains dimmed in Held. Legacy CI/PR/session and hintless waits do not excuse rest.
502
+ - A human may durably Snooze an ordinary handoff (default one day, presets/custom exact instant) or
503
+ Archive it. Due snoozes automatically re-enter Queue; Archive never does.
504
+ - Questions, permissions/native approvals, typed interactions, and crashes break through Snooze so a
505
+ provider cannot be stranded behind an invisible hard gate. `done` remains the checked presentation,
506
+ but it is still a resting handoff and can be snoozed.
507
+
508
+ The worker contract still teaches explicit `question`, `done`, and narrow `awaiting` fences because
509
+ they improve priority and presentation. A fence is no longer required merely to make a rested worker
510
+ discoverable.
511
+
512
+ ## 2026-07-12: runtime release gate — real CDP evidence plus independent review
513
+
514
+ Major UI, server, and control-plane work may no longer reach `done` from unit/integration/mocked
515
+ evidence alone. The canonical `packages/server/src/workerPrompt.ts` contract now requires real Chrome CDP QA against
516
+ a disposable full stack, relevant active/idle/error/restart coverage, desktop+narrow screenshots,
517
+ console/network inspection, and an explicit correctness+aesthetics assessment. Chrome DevTools MCP is
518
+ preferred when it is available to the current provider; `agent-browser` or the repository Puppeteer
519
+ harness are explicit fallbacks. Mocked DOM/routes can supplement but cannot be the sole evidence.
520
+
521
+ Completion also requires two distinct review passes: the implementer's self-review of diff+evidence,
522
+ then an independent fresh-context adversarial review; confirmed findings are fixed and affected gates
523
+ rerun. The exception is proportional and narrow: trivial non-runtime docs-only or provably mechanical
524
+ changes may skip CDP/independent review, while uncertainty applies the gate. This rule is mirrored in
525
+ `skills/worker/SKILL.md` (v0.2.2) and the SessionStart seed; the backend-aware prompt contract test pins
526
+ all four delivered surfaces, and the Claude expansion golden changes intentionally. The Codex addendum
527
+ no longer mislabels an author's inline second read as independent review: use delegation when available,
528
+ or report the gate unmet.
529
+
530
+ ## 2026-07-21: Plugin slim-down — one contract copy, gh is the only injected skill
531
+
532
+ The plugin stops shipping three of its four skills. `skills/worker` is DELETED: it was a second copy
533
+ of the worker contract whose single source is `packages/server/src/workerPrompt.ts` (the system
534
+ prompt, rebuilt on every dispatch/resume and compaction-immune) — every contract edit had to be made
535
+ twice, and the copies drifted. The session-seed pointer sentences that said "Load the `frizz:worker`
536
+ skill for the full contract" now point at the system-prompt contract only, and the contract tests pin
537
+ the two backend prompts (not a skill copy). `skills/dialectic` is dropped from the plugin (generic
538
+ methodology nobody wired into the seed or prompt; workers on other people's projects never asked for
539
+ it). `skills/adhoc-cdp` MOVED to the frizz repo's own `.agents/skills/adhoc-cdp` (agent-neutral; `.claude/skills/adhoc-cdp` is a symlink to it so Claude and Codex share one copy) — its content is
540
+ frizz-specific (adhoc-stack.mjs / shot.mjs), so it is a project skill, not global plugin cargo; the
541
+ generic "verify in a real browser" principle already lives in the prompt's runtime-gate section.
542
+ `skills/gh` remains the ONE injected skill: bulky, conditionally relevant, and its pointer is already
543
+ auth-gated in the seed — exactly the on-demand shape skills are for.
544
+
545
+ ## 2026-07-22: The no-PR rule now also lives in AGENTS.md (docs, not a hook)
546
+
547
+ Root cause of "agents keep opening PRs despite FRIZZ.md": the worker contract + injected FRIZZ.md are a
548
+ SNAPSHOT frozen at session creation. The Codex worker that opened PR #17 and #18 (thread `862831cf`,
549
+ born 09:50 Jul 21) spawned minutes before FRIZZ.md injection first landed and hours before the no-PR
550
+ rule existed, so it never saw the rule and carried the base contract's "open a PR and report its URL"
551
+ on every turn. Editing FRIZZ.md does nothing for a session already in flight, and sub-agents never
552
+ receive FRIZZ.md at all.
553
+
554
+ Mitigation (docs only): the no-PR rule was added to `AGENTS.md` — the agent-neutral home Codex
555
+ re-reads FRESH every session and sub-agents load. That reaches NEW sessions of both backends without a
556
+ frozen snapshot. It does NOT retroactively reach an already-running frozen session; restart such a
557
+ session to pick up a rule change. A tool-layer enforcement hook (`deny-pr` PreToolUse) was built and
558
+ then deliberately reverted as overkill for a single-user repo — the doc reach is the intended fix.
559
+
560
+ ## 2026-07-30: Carryover — a per-session brief the harness re-injects, so context survives compaction
561
+
562
+ The two existing mitigations for compaction loss both routed the context through a DECISION, and that
563
+ is where they leaked. The scratchpad survives on disk but only helps if the post-compaction turn
564
+ chooses to read it — a model decision, and it gets skipped. `precompact-instructions.mjs` steers the
565
+ summarizer, but the summary is a lossy retelling by a model that never saw the reasoning, and it is
566
+ regenerated from scratch every time. `carryover.mjs` removes the decision: whatever is in
567
+ `.frizz/threads/<sid>/carryover.md` is spliced into the context window by the harness, before the
568
+ model's first token. The model cannot forget to read it and the summarizer cannot paraphrase it away.
569
+
570
+ **The file is authored by the AGENT, not by the hook** — it is a plain markdown file written with the
571
+ Write tool. Rejected: a CLI (`frizz carryover set …`) and a dedicated MCP tool. Both add something to
572
+ learn and to keep installed, and neither buys anything over a file the agent already knows how to
573
+ write and a human can hand-edit mid-session (an edit is just an mtime change, which the nudge treats
574
+ exactly like an agent's write — pinned by a test).
575
+
576
+ **Keyed by session id**, taken from the hook's stdin `session_id` and falling back to
577
+ `CLAUDE_CODE_SESSION_ID` via the shared `currentSessionId`. Those two are equal (already relied on by
578
+ the activation sentinel), which is what lets the AGENT compute its own path from a Bash/Write call.
579
+ Stored beside the scratchpad rather than under `.frizz/.session-state/` because the agent is already
580
+ told the thread dir. `.frizz/` is gitignored in full, so a brief never reaches a commit, and nothing
581
+ enumerates `.frizz/threads/*` to build the board (threads come from the DB), so writing a directory
582
+ there for a non-frizz session cannot conjure a phantom thread card.
583
+
584
+ **Staleness is measured in context TOKENS, and as GROWTH, never an absolute.** The transcript's
585
+ newest usage record gives live context fill (`input + cache_creation + cache_read`); the hook reads
586
+ only the last 256 KB of the file, since transcripts reach tens of megabytes here. An absolute
587
+ threshold is unusable because the window is not knowable from a hook — a real compaction in this
588
+ project fired at preTokens 935,291 on a 1M-window session while a 200k session compacts near 160k.
589
+ Growth since the last write is window-independent and self-resetting. Wall clock and transcript BYTES
590
+ were both rejected: neither tracks context pressure (one large tool result adds megabytes to the file
591
+ without moving the window much).
592
+
593
+ **Why a nudge at all.** Without it the file is never written and the whole mechanism is decorative —
594
+ an agent that is never reminded does not stop to journal. It is the one part that is a heuristic, so
595
+ it is the one part with a cheap escape hatch: `FRIZZ_CARRYOVER_STALE_TOKENS` retunes it and
596
+ `FRIZZ_CARRYOVER=off` disables every mode.
597
+
598
+ **Registered twice, deduped deterministically.** The plugin registration ships to every frizz worker
599
+ everywhere. The repo's own `.claude/settings.json` carries the same three registrations with
600
+ `--via=project` so plain (non-frizz) `claude` sessions in this repo get the behavior too; that flag
601
+ exits when `FRIZZ_THREAD` is set, because such a session already loads the plugin and would
602
+ otherwise inject the brief twice. A flag plus an env check — no lock file, no race.
603
+
604
+ VERIFIED LIVE against cli 2.1.220, in an isolated `/tmp` project, not by proxy:
605
+ - `SessionStart:startup` fired (exit 0, 63 ms) and a real session quoted a sentinel that existed ONLY
606
+ in the brief — with **zero tool calls** in the transcript, so it came from context, not a file read.
607
+ - A real `/compact` produced a `compact_boundary`, and `SessionStart:compact` then delivered a brief
608
+ whose sentinel had been swapped to a value that had NEVER appeared in that session's context — so
609
+ the post-compaction injection demonstrably re-reads the file rather than echoing the summary.
610
+ - `PreCompact:manual [carryover.mjs --mode=precompact] completed with status 0` in the hook debug log.
611
+ - The `UserPromptSubmit` nudge fired live and reported ~31k tokens computed from the real transcript,
612
+ confirming the usage parser works against Claude Code's actual format and not just a fixture.
613
+
614
+ Regression net: `packages/server/src/carryover-hook.test.ts` executes the real script over its
615
+ wire contract (argv + stdin JSON + stdout) rather than asserting on its source.
616
+
617
+ ## 2026-07-30 (same day, revised): carryover COLLAPSED into the scratchpad; the nudge moved mid-turn
618
+
619
+ The `carryover.md` brief shipped earlier today was redundant with the scratchpad and made the worker
620
+ maintain two overlapping documents (maintainer's call, and correct). ONE doc per thread is the rule.
621
+ `carryover.mjs` is DELETED and replaced by `scratchpad.mjs`, which does the same three things to
622
+ `scratch.md` — the pad the dispatcher already provisions, already names in the system prompt, and
623
+ already forbids sub-agents from writing.
624
+
625
+ Two behavior changes fell out of the retarget:
626
+
627
+ **Injection is scoped to the context-losing sources.** A brand-new `startup` has lost nothing, so it
628
+ gets the contract text only; `compact`/`resume`/`clear` get the pad's head injected verbatim PLUS an
629
+ explicit "re-read the full file" pointer. Injecting the head rather than only pointing at the file is
630
+ deliberate: a bare reminder routes recovery through a decision the model can skip, which is the exact
631
+ failure being fixed. The head is the floor; the pointer is the ceiling. The cap (12k chars, was 24k)
632
+ keeps that floor affordable now that the target is unbounded working memory rather than a bounded brief.
633
+
634
+ **"Present" no longer means "written".** frizz provisions scratch.md with a skeleton, so file-absence
635
+ is gone as the unwritten signal. `substanceLength()` strips headings, the provisioned orientation line
636
+ and empty task boxes, and measures what remains. It is a heuristic on purpose — it only decides
637
+ whether to NUDGE, so a wrong call costs one redundant reminder, never correctness.
638
+
639
+ **The nudge now also fires on PostToolUse, and that is the real fix.** UserPromptSubmit alone only
640
+ fires at turn boundaries, and a frizz worker runs enormous autonomous turns — dozens of tool calls
641
+ between human prompts — so a whole session's work could compact unpersisted without a single nudge.
642
+ PostToolUse `additionalContext` was verified live against cli 2.1.220 (a real session quoted a
643
+ sentinel injected after a Bash call, and through the plugin the model received the nudge mid-turn and
644
+ said it would update the pad). Both channels share one state file, so the interval is global: firing
645
+ per tool call makes the existing budget land SOONER and mid-turn, it does not multiply reminders.
646
+
647
+ ### Researched: there is NO context-pressure hook, on either backend
648
+
649
+ Measured, so do not go looking again. Claude Code 2.1.220 exposes **31 hook events** and not one
650
+ signals an approaching context limit; **no hook input carries a token count at all**, and the docs say
651
+ plainly to poll the transcript yourself. Codex is the same. So the fill is computed here from the
652
+ transcript's newest usage record (`input + cache_creation + cache_read`), reading only the file's tail
653
+ because transcripts reach tens of megabytes. Growth-since-last-write remains the metric because the
654
+ window is not knowable from a hook.
655
+
656
+ Useful events noticed while enumerating, none wired yet: `PostToolBatch` (fires after a batch of
657
+ parallel tool calls resolves, before the next model call), `SubagentStart` (could carry the
658
+ read-only-pad rule structurally instead of via the dispatch epilogue's prose), `PostCompact`
659
+ (receives the summary), and `SessionStart`'s `initialUserMessage` output (injects a VISIBLE user
660
+ message rather than a system reminder) and `fork` matcher.
661
+
662
+ ### Rejected: a blocking Stop gate
663
+
664
+ A Stop hook could refuse to let the worker rest until it writes. That was tried and removed on
665
+ 2026-07-02 (maintainer's call): the block-until-file-edited nag forced even trivial workers into
666
+ Read/Edit dances that render as noise in the chat UI. This nudges; it never blocks.
667
+
668
+ ### Verified feasible, NOT yet built: the background checkpoint fork
669
+
670
+ `claude -p --resume <sid> --fork-session` was tested end to end. The fork carries the FULL original
671
+ conversation (it reproduced a human quote that existed only in that transcript), it wrote to the
672
+ ORIGINAL thread's scratchpad when handed the absolute path, the original transcript was
673
+ **byte-identical before and after** (15,078 → 15,078 — the live session is never interrupted), the
674
+ original continued normally afterwards, and it is cheap because it HITS THE PROMPT CACHE (28,904
675
+ cache-read vs 413 cache-create). `SessionStart` has a `fork` matcher so hooks can tell they are in one.
676
+
677
+ Open problems before this could ship: the fork gets a NEW session id, so it must be handed the
678
+ ORIGINAL pad path explicitly (its own hooks would derive a different one); concurrent writes against
679
+ the live worker need an ownership rule; the spawn needs a single-flight lock so one threshold crossing
680
+ cannot fan out into many forks; and frizz's discover/tailer must not adopt the fork's transcript as a
681
+ board thread.
682
+
683
+ ### Codex parity gap
684
+
685
+ Codex 0.144.6 (installed) HAS a hooks system with nearly the same schema — SessionStart, SessionEnd,
686
+ PreToolUse, PostToolUse, PermissionRequest, UserPromptSubmit, Stop, PreCompact, PostCompact,
687
+ SubagentStart, SubagentStop — plus `additionalContext`, all confirmed present in the installed binary.
688
+ Config lives at `~/.codex/hooks.json` or `[hooks]` in config.toml, repo-level `.codex/hooks.json`, or
689
+ plugin-bundled. SessionStart distinguishes a `compact` source. Codex is BETTER in one respect: it lets
690
+ you SET the compaction threshold (`model_auto_compact_token_limit`, with
691
+ `model_auto_compact_token_limit_scope` = `total` | `body_after_prefix`), so compaction can be made
692
+ predictable with headroom rather than merely detected. Wrinkle: codex enforces hook TRUST
693
+ (`--dangerously-bypass-hook-trust` exists for automation). **frizz currently wires ZERO hooks for
694
+ codex**, so a codex worker has only prompt-level scratchpad discipline — the largest remaining gap.
695
+
696
+ ## 2026-07-30 (third pass): reinforcement is OPT-IN, and the two backends are gated differently
697
+
698
+ > **SUPERSEDED the same day by the fourth pass below** — the opt-in gate was REMOVED. Re-grounding is
699
+ > unconditional now. The measured codex findings in this entry all still hold; only the gating does not.
700
+
701
+ Maintainer's call: the mechanism is opinionated, so it should be chosen rather than inherited. New
702
+ `scratchpadReinforcement` setting, **OFF by default** — the inverse of `runtimeGate` /
703
+ `autoResumeOnLimit`, which are opt-OUT. (Both of those settings were deleted on 2026-08-03 — see the
704
+ last entry in this file — so this comparison is historical.) Toggle sits in the Settings drawer under
705
+ "Auto-resume after usage limits". `scratchpad.mjs` inverted its kill switch into an opt-in gate:
706
+ absence means off.
707
+
708
+ **The two backends cannot be gated in the same place**, which is the whole design constraint here:
709
+
710
+ - **Claude → a worker ENV VAR.** `hooks.json` is static, so `FRIZZ_SCRATCHPAD_HOOK=on` is what decides
711
+ whether the registered hooks do anything. Stamped on the tmux spawn, and on the broker/SDK path via
712
+ a new per-fork `extraWorkerEnv` on `ClaudeAgentBrokerBridge` — evaluated per fork, so flipping the
713
+ setting reaches the next dispatch or cold-resume without a server restart.
714
+ - **Codex → per-conversation CONFIG**, on the `config` override `CodexAppServerBridge` already
715
+ supported, plus a `--enabled` flag. The env var is unusable there because the `codex app-server`
716
+ daemon is SHARED per project and its environment cannot express a per-conversation decision;
717
+ building the config at all already means the setting is on.
718
+
719
+ ### Measured, so nobody re-derives it (codex-cli 0.144.6)
720
+
721
+ - **`codex exec` runs NO lifecycle hooks, from any discovery path** — not `<repo>/.codex/hooks.json`,
722
+ not `$CODEX_HOME/hooks.json`, not `-c hooks.…`, with or without `bypass_hook_trust=true`, inside a
723
+ git repo or outside one — even though the `hooks` feature flag reports as enabled. Probed with a
724
+ marker file so "did the hook RUN" stayed separate from "did its output reach the model".
725
+ - **`codex app-server` DOES run them** when they arrive as config overrides. Verified through the real
726
+ `CodexAppServerBridge` with the real `scratchpad.mjs`: SessionStart, UserPromptSubmit and Stop all
727
+ fired. Probe kept at `backend/_live_codex_hooks.mts`.
728
+ - **`bypass_hook_trust` is required** — codex SILENTLY SKIPS untrusted hook definitions, so without it
729
+ the config is delivered and ignored, which looks exactly like a broken feature.
730
+ - **Codex reports its OWN rollout session id to the hook** (e.g. `019fb427-…`, `transcript_path` under
731
+ `~/.codex/sessions`), NOT frizz's thread id. Hence the mandatory `--session=<frizz sessionId>`:
732
+ deriving the path would address a scratchpad that does not exist, and the worker would look
733
+ unreinforced for a reason nobody could see. Codex does send `source` (`"startup"`), same field name
734
+ as Claude, so the compact/resume branch works unchanged.
735
+ - **Codex has no PreCompact/PostCompact context-injection wire type** (only SessionStart /
736
+ UserPromptSubmit / PostToolUse / PreToolUse / PermissionRequest / SubagentStart), so the
737
+ summarizer-steering channel stays Claude-only.
738
+
739
+ ### Known gap, deliberately not papered over
740
+
741
+ The codex NUDGE cannot fire yet: staleness is computed from Claude's transcript shape
742
+ (`message.usage`), and a codex rollout is a different format, so `contextTokens()` returns null and
743
+ the nudge degrades to SILENCE rather than to a wrong number — pinned by a test. Closing it needs a
744
+ rollout-aware token parser. The load-bearing channel (restoring the pad on SessionStart) works on both.
745
+
746
+ ## 2026-07-30 (fourth pass): the gate comes OFF — the scratchpad is canonical, so re-grounding is not optional
747
+
748
+ Maintainer's correction, and it reverses the third pass's central decision: the thing that deserves an
749
+ opt-in is the FORK-based auto-updating (still unbuilt, and called "a little overkill"), NOT the
750
+ re-grounding. The scratchpad is the CANONICAL document for a thread, so reading it back after a
751
+ compaction is what makes the pad worth writing at all — a posture no project should have to opt into.
752
+
753
+ The bug this fixes is worse than a mis-scoped setting: `scratchpadReinforcement` defaulted to FALSE, so
754
+ the DEFAULT worker — every worker, in practice — got nothing back after a compaction. The mechanism was
755
+ shipped and inert. A feature that is off by default is a feature that does not exist.
756
+
757
+ Removed entirely: the `scratchpadReinforcement` setting (shared schema, server defaults, Settings
758
+ drawer toggle), the `FRIZZ_SCRATCHPAD_HOOK=on` env plumbing (`scratchpadHookEnv`, the tmux spawn stamp,
759
+ the broker bridge's `extraWorkerEnv` dep), and codex's `--enabled` flag. A dead toggle that gates
760
+ nothing is worse than no toggle; when the fork checkpointer is built it brings its own setting.
761
+
762
+ What survives: `--session=<frizz sessionId>` on the codex path stays MANDATORY (codex reports its own
763
+ rollout session id, so the derived path would address a scratchpad that does not exist), and
764
+ `bypass_hook_trust` stays required (codex silently skips untrusted hook definitions). The escape hatch
765
+ is now `FRIZZ_SCRATCHPAD_HOOK=off` — env only, and only an explicit off value disables, because it is
766
+ for a one-off session, not a project posture.
767
+
768
+ Behavior change beyond ungating: **an EMPTY pad now re-grounds too.** Previously a compaction with an
769
+ unwritten pad fell through to the generic contract text; now it says plainly that context was just
770
+ lost, that the pad is the canonical record, to re-read it NOW, and that it currently has nothing in it
771
+ so it must be written. "You lost your context and your pad is empty" is the most actionable thing the
772
+ next turn can hear, and staying quiet about it left the worker with nothing at the exact moment it had
773
+ nothing.
774
+
775
+ VERIFIED against a real `/compact` with NO env var and NO flag set anywhere: `SessionStart:compact`
776
+ exit 0, delivering the re-grounding lead plus the pad head carrying a sentinel from the file. 17 hook
777
+ tests; full suite 2419 pass / 0 fail; typecheck clean.
778
+
779
+ ## 2026-07-30 (fifth pass): the worker CONTRACT now explains what the scratchpad is FOR
780
+
781
+ The hooks made the scratchpad load-bearing, but the shipped worker contract still described it as a
782
+ place that "survives compaction" and handed over a path. That undersells it in the way that matters:
783
+ a worker told "here is a file that survives compaction" writes a task list; a worker told "this is the
784
+ canonical record of the thread and your compaction-survival mechanism" writes the REASONING, which is
785
+ precisely what compaction destroys and what a summary cannot reconstruct.
786
+
787
+ Rewritten in all four places a worker meets the pad, so the framing is consistent wherever it lands:
788
+
789
+ - `workerPrompt.ts` `SCRATCHPAD` (both backends) — now headed "the canonical record of this thread",
790
+ and says plainly WHY: compaction drops the reasoning first (the plan, the alternatives ruled out,
791
+ why the human chose what they chose), a summary preserves what you did and not why, so write that
792
+ here AS YOU GO and re-read the file after any compaction or resume. Enumerates what belongs in it.
793
+ The claude variant keeps the sub-agent blackboard job and now states that helpers READ but never
794
+ EDIT it — the one-scratchpad rule, said where the worker actually reads it.
795
+ - `dispatch.ts` `scratchpadOrientation()` (system-level, rebuilt on every resume) and the first
796
+ user-message line — same framing, one sentence each.
797
+ - `dispatch.ts` `scratchpadContent()` — the provisioned skeleton's own orientation line.
798
+ - `cc-worker/hooks/session-seed.mjs` — the runtime `SCRATCHPAD:` line.
799
+
800
+ **Deliberately NOT promised: automatic re-injection.** The prompt says frizz "helps by feeding the head
801
+ of this file back into your context", and the IMPERATIVE it gives is unconditional — re-read the file
802
+ yourself after any compaction or resume. A contract that leans on a runtime guarantee degrades badly
803
+ wherever that channel is absent, and one such gap is known: codex's `thread/resume` does NOT re-send
804
+ the per-conversation `config`, so a COLD-resumed codex thread (frizz restart, app-server daemon death)
805
+ may lose its hooks. Unverified either way — flagged, not assumed.
806
+
807
+ Fallout fixed in the same change (the hook's own template detector): `substanceLength()` recognised the
808
+ provisioned skeleton by a `^(Your|SCRATCHPAD:)` prefix plus the old wording. The new orientation line
809
+ starts differently, so a freshly provisioned pad read as WRITTEN — which made an empty pad skip its
810
+ re-grounding branch and made the summarizer swallow a skeleton. It now matches on the concept
811
+ (`compaction-survival mechanism|compaction-proof working memory`), which is also backward compatible
812
+ with pads already on disk under the old wording.
813
+
814
+ Goldens regenerated (a deliberate contract change); the diff is the scratchpad section and nothing
815
+ else. Two `dispatch.test.ts` assertions re-anchored — they pin the claude-keeps/codex-drops blackboard
816
+ asymmetry and were using the retired phrase as their anchor; the behavior they check is unchanged.
817
+ Suite 2413 pass / 0 fail, typecheck clean.
818
+
819
+ ## 2026-07-30 (sixth pass): keep the helper epilogue universal
820
+
821
+ `hooks/agent-dispatch.mjs` appends its epilogue to every Claude `Agent` helper launched by a frizz
822
+ worker, regardless of the repository or kind of task. That makes it the wrong layer for requirements
823
+ about compilation, shared build locks, build/test ownership, or how long-running operations should
824
+ be managed.
825
+
826
+ The epilogue keeps only universal coordination: return a useful handoff, do not mutate the owning
827
+ worker's `.frizz/` state unless explicitly assigned, and use the always-available `SendMessage`
828
+ upward channel when the dispatcher acting mid-flight could change the outcome. Task-specific
829
+ verification and process-lifecycle instructions belong in the dispatch prompt; repository-specific
830
+ ones belong in the repository's own guidance. Background dispatch enforcement and `name` /
831
+ `team_name` stripping are unchanged.
832
+
833
+ Correction during implementation: the first reduction also removed `SendMessage`. That was too
834
+ aggressive. Every helper receives the tool, and reaching the dispatcher before returning is
835
+ universally useful orchestration rather than repository policy. The restored wording treats it as
836
+ available directly and therefore drops the old deferred-tool / `ToolSearch` caveat.
837
+
838
+ ## 2026-07-30 (sixth pass): the operator's Settings instructions move to the SYSTEM prompt
839
+
840
+ Question from the maintainer: is FRIZZ.md eagerly loaded, and did the Settings prompt get replaced by
841
+ it? Answer, from the code: **both exist, neither replaced the other** — and they were being treated
842
+ very differently.
843
+
844
+ - **FRIZZ.md** → `frizzConfigBlock(projectDir)` → `extraSystemPrompt` at EVERY site (claude dispatch,
845
+ codex dispatch, adopt, resume, broker follow-up, router follow-up). System-level, so it already
846
+ survives compaction and is rebuilt on every resume. Nothing to fix.
847
+ - **The Settings preamble** (drawer label "Subagent instructions", schema field `dispatchPreamble`) →
848
+ `composePrompt()` → the **FIRST USER MESSAGE** only. Never re-applied on resume, and the first user
849
+ message is exactly what compaction replaces with a summary. So the repo's conventions survived while
850
+ the operator's own standing instructions quietly did not.
851
+
852
+ Fixed by giving it the same treatment as FRIZZ.md: new `operatorInstructionsBlock(preamble)`, added to
853
+ the system-prompt composition at all six sites, and REMOVED from `composePrompt` (which loses its
854
+ `customInstructions` parameter). Moved rather than duplicated — leaving it in both would carry a second
855
+ copy of a potentially long preamble in context for the whole pre-compaction window, and the block sits
856
+ ABOVE the task banner either way, so the visible chat bubble is unchanged.
857
+
858
+ Two things fall out for free: an edited setting now reaches a thread that is ALREADY RUNNING (the
859
+ system prompt is rebuilt on every resume), and the block states its relationship to FRIZZ.md — follow
860
+ both, prefer the more specific where they genuinely conflict — so a worker meeting two operator-authored
861
+ surfaces knows how to reconcile them.
862
+
863
+ VERIFIED with a real `/compact`: a sentinel placed in `--append-system-prompt` was still readable by the
864
+ model after the compaction boundary (`compact_boundary` present in the transcript, model returned
865
+ `SORREL-EGRET-4402`). That is the property the move depends on, tested rather than assumed.
866
+
867
+ One test needed real rework rather than a signature patch: `server.test.ts` asserted the preamble
868
+ appeared in the composed first message — the exact behavior being moved — so it now asserts the
869
+ opposite plus a dedicated `operatorInstructionsBlock` test. A second (`dispatch.test.ts`'s banner test)
870
+ used `PROJECT INSTRUCTIONS` as its above-the-banner anchor; on an ABSENT string `indexOf(...) < banner`
871
+ passes VACUOUSLY (-1 < banner), so it was re-anchored on the scratchpad line and given an explicit
872
+ `doesNotMatch` instead. Suite 2460 pass / 0 fail, typecheck clean.
873
+
874
+ ## 2026-07-30 (seventh pass): converge on FRIZZ.md — the Settings preamble is GONE
875
+
876
+ Maintainer's call, one pass after making the preamble durable: rather than maintain two
877
+ operator-authored surfaces, keep the one that is already versioned with the repo. `dispatchPreamble`
878
+ (drawer label "Subagent instructions") is deleted outright — schema, server default, the drawer field
879
+ and its draft plumbing, `operatorInstructionsBlock` and all six of its call sites.
880
+
881
+ **FRIZZ.md is now the ONLY place project conventions live.** It was already the better surface and
882
+ needed no work: `frizzConfigBlock()` puts it in `extraSystemPrompt` at every site (claude dispatch,
883
+ codex dispatch, adopt, resume, broker follow-up, router follow-up), so it survives compaction and is
884
+ rebuilt on every resume. It is also reviewable in a diff, travels with a clone, and can differ per
885
+ branch — none of which a value in a local SQLite settings blob can do.
886
+
887
+ Checked before deleting rather than after: `DEFAULT_PREAMBLE` shipped as `""`, and a read of every
888
+ `~/.frizz/projects/*/ui.db` found no project with a non-empty `dispatchPreamble`. So there is nothing to
889
+ migrate and no operator text is silently dropped. `Settings` is a plain `z.object`, so an older blob
890
+ that still carries the key parses fine — zod strips the unknown field.
891
+
892
+ Fallout fixed in the same change, since it was this change that made them false: two dispatch.ts
893
+ header comments still described the preamble as the prompt's "orchestration wisdom", and
894
+ `SettingsDrawer.test.ts` iterated a help-key list containing `subagentInstructions`. The
895
+ `operatorInstructionsBlock` test added one pass earlier was removed with the function.
896
+
897
+ Suite 2419 pass / 0 fail (two timing-sensitive tests — `app-socket` coalescing and the tmux SIGKILL
898
+ buffer — flaked under parallel load and pass in isolation; neither touches this change), typecheck
899
+ clean, and the drawer was re-driven in a real browser: the field is gone, and the Prompts section
900
+ measures as exactly two children at the standard 24px `gap-6` with no orphaned container left behind.
901
+
902
+ ## 2026-07-30 (eighth pass): native Codex children merge the shared scratchpad
903
+
904
+ An apparent post-compaction continuity failure in thread `9540edc3-4807-4bb0-8e36-5940f92b452b`
905
+ was not path loss and was not Claude's on-disk token broker. The affected thread was a Codex session.
906
+ At its first compaction the worker immediately read the correct exact path; at a later compaction it
907
+ again addressed the correct path and got `ENOENT`. The directory and sibling artifacts remained.
908
+
909
+ The missing file was child corruption. Native child `/root/auditstatus_prepare`, launched with
910
+ `fork_turns:"none"`, still inherited the parent's developer-level `SCRATCHPAD:` mandate. It reasoned
911
+ "Planning isolated scratch creation" and replaced the parent's exact pad with its own `# SEA corpus
912
+ preparation` notes. Two minutes later it noticed the mistake, reasoned "Reverting unauthorized root
913
+ scratch changes", and tried to remove its replacement. A shell `rm -f` was denied, but its fallback
914
+ `apply_patch` `*** Delete File` succeeded. It could not restore the content it had overwritten, so the
915
+ pad remained absent until the root compacted nearly an hour later. `fork_turns:"none"` removes
916
+ conversational turns; it does not remove the root worker's base/developer instructions.
917
+
918
+ The deliberately prompt-level fix preserves the useful part of that inheritance:
919
+
920
+ - The Codex worker contract, system orientation, and first task message now make the pad explicitly
921
+ collaborative. Children may read it and persist their own progress, but every edit is a merge:
922
+ re-read first, patch only a scoped agent/task section, and preserve all existing state. Every
923
+ native-child dispatch restates that rule.
924
+ - Codex registers `scratchpad.mjs --mode=subagent-start` as a `SubagentStart` hook. It injects the
925
+ child-only epilogue structurally on every native dispatch: the child may update its scoped progress,
926
+ but may never delete, truncate, reinitialize, move, or replace the whole file — including as cleanup
927
+ or rollback after a mistake. This fixes the observed failure without a filesystem guard or a
928
+ single-writer restriction.
929
+ - Newly provisioned pads recommend a light structure rather than enforce a machine schema: Goal,
930
+ Task list, Decisions, Shared context, per-agent progress, Verification, and Next action. A visible
931
+ legend uses `[ ]` pending, `[/]` in progress, `[x]` complete, `[-]` cancelled, and `[?]` blocked.
932
+ Frizz's Markdown renderer recognizes all five while the source stays readable in Obsidian/plain text.
933
+ - If a pad is unexpectedly absent or empty after compaction/resume, the re-grounding injection says
934
+ the exact path is authoritative, forbids searching neighboring thread pads or broadly reloading repo
935
+ docs, and tells the root to reconstruct from the retained summary plus directly named handoffs.
936
+
937
+ The root's apparently confused recovery is now explained too. The post-compaction hook correctly
938
+ reported that the pad had no substantive content, and the root's first call addressed the correct
939
+ exact path and got `ENOENT`. Only then did it search worktrees, list the surviving thread directory,
940
+ and read older handoffs to reconstruct state. Its "Writing initial scratch file" reasoning label was
941
+ model narration for recreating a currently absent file, not evidence that no pad had existed before.
942
+
943
+ ## 2026-07-31: shared scratchpad updates are expected, not merely permitted
944
+
945
+ The collaborative contract above said children *may* update the shared scratchpad. A root worker then
946
+ treated batch file ownership and a dynamic-orchestrator mega-doc's single-writer rule as reasons to
947
+ centralize scratchpad writes and even make the pad immutable. That was the opposite of the intended
948
+ blackboard pattern.
949
+
950
+ The worker contract, dispatch orientation, Codex child hook, and Claude dispatch epilogue now say each
951
+ sub-agent should merge its own scoped progress as it works. Deliverable file ownership never includes
952
+ the Frizz scratchpad, and a root must reconcile concurrent scoped updates rather than act as sole
953
+ writer. The existing merge-safety rules remain unchanged: re-read first, preserve all other content,
954
+ and never delete, truncate, reinitialize, move, or replace the pad.
955
+
956
+ ## 2026-07-31: the epilogue teaches a helper how to collect a helper of its OWN
957
+
958
+ A three-level dispatch tree read, on the board, as one job done three times — root "Improve server
959
+ logging…", child "Researching dev server readouts", grandchild "Survey dev server readouts". The
960
+ topology was actually sound: the root's task had five research prongs, the child kept four (Vite,
961
+ Next and wrangler source, repaint techniques, log-dir conventions, frizz's own state-dir convention)
962
+ and delegated only the pure-web prong. The grandchild's prompt was a strict subset, not a copy.
963
+
964
+ What was broken was COLLECTION. The child backgrounded its helper, then hand-rolled a wait loop over
965
+ `/private/tmp/claude-<uid>/<proj>/<session>/tasks/<agentId>.output`. That path is a **symlink** into
966
+ `~/.claude/projects/<proj>/<session>/subagents/agent-<id>.jsonl`, so `stat -f %z` without `-L`
967
+ returned the LINK's size — 153, the length of its target path — and `stat -f %m` returned the link's
968
+ frozen creation mtime. Its other predicate, `grep -c '"type":"result"'`, is a known false negative
969
+ because that record is not reliably written. Both halves false-negatived at once: a helper that was
970
+ 195 records and 413,723 bytes deep read as `size=153 age=325s results=0`. The child concluded "the
971
+ helper's transcript is stale at only 153 bytes", discarded a live agent, and redid its work — which
972
+ is precisely why all three levels appeared to be doing the same thing.
973
+
974
+ The audience was the root cause. `workerPrompt.ts` does carry the rule ("keep fan-out shallow: a
975
+ rested sub-agent is not reliably re-woken by grandchildren"), but that contract reaches only the ROOT
976
+ worker — the one agent that does not spawn grandchildren — and the contract itself states children
977
+ inherit none of it. Into that silence an inherited user-level `CLAUDE.md` ("the parent stays awake and
978
+ polls the child's transcript inside its own turn") supplied the polling recipe, overriding the Agent
979
+ tool's own result text ("you will be notified automatically when it completes").
980
+
981
+ `hooks/agent-dispatch.mjs` is the fix's home because it fires at EVERY depth — verified against the
982
+ real transcripts: both the depth-1 and depth-2 children received the epilogue. It is the only seam
983
+ that reaches a nested dispatcher without depending on its parent remembering to restate the norm. The
984
+ new paragraph states that a helper's completion arrives automatically, forbids hand-rolled wait loops
985
+ over a transcript or `.output` path, names the symlink and `"type":"result"` failure modes concretely
986
+ enough that a model cannot rationalize its way back into polling, and asks a dispatcher to give its
987
+ helper a `description` naming the narrower slice so the tree stays readable.
988
+
989
+ This stays within the universal-coordination boundary set on 2026-07-30: it is agent-lifecycle
990
+ coordination, like the handoff contract and the `SendMessage` upward channel already in the epilogue,
991
+ and imposes no build, test, git, or repo-specific policy. Rejected: denying depth-2 dispatch in the
992
+ hook (too blunt — the decomposition was good, and this hook exists because a worker MAY spin up
993
+ helpers), and rewriting the deliberate "keep fan-out shallow" line in `workerPrompt.ts`.
994
+
995
+ `packages/server/src/agent-dispatch-hook.test.ts` is new — this hook had no test at all despite being
996
+ load-bearing. It spawns the real hook over the real stdin contract and pins the foreground denial,
997
+ `name`/`team_name` stripping, endsWith-idempotence (including the prompt that merely QUOTES the
998
+ marker), the nested-dispatch paragraph, the retained coordination text, the non-worker inert path,
999
+ and fail-open on bad input.
1000
+
1001
+ ## 2026-07-31: REJECTED — a Stop hook that nudges a fenceless rest toward a ```question card
1002
+
1003
+ Built as `hooks/fence-stop.mjs`, landed, then ripped out the same day on the maintainer's call: the
1004
+ approach is inelegant and they do not want it. Do not rebuild it. Recorded here only so the next agent
1005
+ does not re-derive the same idea from the same symptom, and because one measurement is worth keeping.
1006
+
1007
+ THE MEASUREMENT STANDS, whatever is done about it. A scan of 532 real worker transcripts (session ids
1008
+ cross-referenced against every `~/.frizz/projects/*/ui.db`), 4,709 rest turns:
1009
+
1010
+ rest turn #1 #2-3 #4-6 #7-10 #11-20 #21+
1011
+ ```question 23% 22% 20% 20% 16% 9%
1012
+ ```done 31% 23% 17% 13% 10% 2%
1013
+ no fence 45% 53% 58% 60% 68% 83%
1014
+
1015
+ Fence use decays with session DEPTH, not with compaction — turns before a compaction boundary are
1016
+ already 82% fenceless, so the decay is complete before any summary is written, and compaction only
1017
+ correlates because only long sessions reach it. 9.8% of fenceless rests close by handing a decision
1018
+ back in prose ("your call", "want me to …?"), which renders as a card with nothing to click.
1019
+
1020
+ Two incidental findings worth keeping, since both cost a debugging cycle:
1021
+ - The Stop payload carries `last_assistant_message` and `prompt_id` (cli 2.1.220). The transcript is
1022
+ written ASYNCHRONOUSLY, so at Stop time the message that just ended the turn is often not yet on
1023
+ disk — a hook that parses `transcript_path` to read the final message fired on only two of four
1024
+ live broker workers. Read the payload.
1025
+ - Stop-hook feedback reaches the model but never the human: frizz drops `isMeta` user records
1026
+ (`packages/server/src/transcript.ts`), so a blocked rest shows the worker's original message
1027
+ followed by whatever it sends next, with no visible trace of the hook.
1028
+
1029
+ ## 2026-08-03: the Runtime QA gate is DELETED, and the settings drawer stops duplicating the composer
1030
+
1031
+ Maintainer's call, verbatim: *"This is obviously something that should not be a global setting inside of frizz. This is extremely overfit to our specific requirements inside of this repo. Wipe it entirely."*
1032
+
1033
+ Three removals, one theme — Frizz's shipped worker contract states frizz MECHANICS, and everything else belongs to the repo or to the dispatch that starts the thread.
1034
+
1035
+ - **`runtimeGate` (setting) + `RUNTIME_GATE` (prompt module) — gone.** The browser-QA loop (drive it in Chrome, screenshot into the handoff, escalate to an adversarial reviewer) was frizz's own engineering norm shipped to every worker Frizz dispatches anywhere. That is what `FRIZZ.md` / `CLAUDE.md` are for: `frizzConfigBlock` already injects a repo's own conventions into every spawn/adopt/resume, and this repo keeps its full browser-QA section there. What remains in the shipped contract is the repo-agnostic line that was already in the Quality bar — *"Verify behavior end-to-end before calling anything done."* `VISUAL_EVIDENCE` **stays**: it documents Frizz's guarded local-image proxy, a platform capability, not a QA policy. `chrome-devtools` also stays always-mounted — giving a worker a browser is a capability, not an opinion about when to use it. **(Reversed 2026-08-26 — see the entry at the end of this file. The reasoning above survives one step further than it should have: mounting a browser is itself an opinion, and it was billed to every worker in tokens.)**
1036
+ - **Model + Effort — removed from the settings drawer.** They are chosen per dispatch in the prompt box (`DispatchPreferences`, one profile per runtime); a second global copy only made it ambiguous which one applied. `Settings.model` / `Settings.effort` survive on the wire: `dispatch-preferences.ts` still seeds the composer's first-run profile from them and `dispatch.ts` still falls back to them for GitHub batch dispatch.
1037
+ - **`autoResumeOnLimit` — gone; auto-resume is unconditional.** A thread cut off mid-turn by an exhausted subscription window always gets its own "continue" when the window rolls. `ThreadView.limitPause.autoResume` survives on the wire but is now purely a STALENESS verdict (`resolveLimitPause`): a fault old enough that the wake will never arrive stops promising one.
1038
+
1039
+ Both settings keys are simply absent from the `Settings` zod object now. The object is non-strict and `getSettings` re-parses `{...defaults, ...stored}`, so an old DB carrying either key has it stripped on read — no migration.
1040
+
1041
+ ## 2026-08-04: a helper does NOT fan out again unless its prompt said to
1042
+
1043
+ Maintainer's call: the prompt a sub-agent receives should tell it to refrain from spinning up sub-agents of its own unless it was explicitly instructed to.
1044
+
1045
+ Nesting had never been *encouraged*, but nothing ever told a child not to. The Claude epilogue spoke about a helper's own helper only in the conditional — *"If you dispatch a helper of your own, its completion is delivered to you automatically…"* — which is collection advice that reads as neutral permission, and the 2026-07-31 entry above shows what a child does with permission: it decomposes again because it can. The root worker contract's *"keep fan-out shallow"* line does not help, because it reaches only the root.
1046
+
1047
+ The rule now LEADS both child-facing epilogues, with the collection paragraph kept for the case where a prompt does ask for a helper:
1048
+
1049
+ - **Claude — `hooks/agent-dispatch.mjs`.** The epilogue's last paragraph opens with *"Do the work yourself: do NOT dispatch sub-agents of your own unless your dispatch prompt explicitly tells you to"*, then names the cost (it splits the context the dispatcher assembled, buries the work further from the board, and leaves the child collecting a handoff instead of doing the task). The old paragraph follows, re-hinged on *"If your prompt DOES ask you to dispatch a helper"*. This hook fires at EVERY depth, so the rule reaches a depth-2 dispatcher without depending on its parent to restate it.
1050
+ - **Codex — `hooks/scratchpad.mjs --mode=subagent-start`.** Same rule, named for `spawn_agent`, appended to the `SubagentStart` context as its own `⟦no fan-out of your own⟧` block so it does not blur into the scratchpad merge contract. `SubagentStart` is the only structural seam that reaches a native codex child, exactly as the dispatch epilogue is for Claude.
1051
+
1052
+ This is a PROMPT-level default, deliberately NOT the hook-level depth-2 DENY rejected on 2026-07-31 as too blunt: a dispatcher that genuinely wants a prong fanned out says so in the prompt and the child dispatches, unmodified. Also left alone: `workerPrompt.ts` — the root contract already authorizes fan-out *"when work genuinely decomposes"*, and the root is the one agent whose fan-out is wanted.
1053
+
1054
+ Pinned by a new test in `agent-dispatch-hook.test.ts` (the lead-with-the-default wording plus the re-hinged conditional) and one in `scratchpad-hook.test.ts` (the codex block).
1055
+
1056
+
1057
+ ## 2026-08-06: the canonical scratchpad is DELETED — a free scratch directory, and compaction recovery moves to the recurring prompt
1058
+
1059
+ Frizz provisioned one canonical `scratch.md` per thread and a `SessionStart` hook spliced its HEAD into
1060
+ the context window after every compaction. The argument for the injection was sound and is worth
1061
+ restating, because it is what any replacement has to answer: a bare "remember to read your scratchpad"
1062
+ routes recovery through a decision the model can skip, which is exactly the failure the pad existed to
1063
+ prevent.
1064
+
1065
+ It still lost, on three counts. It made a MAINTAINED FILE the price of admission for every worker,
1066
+ including the ones whose whole task was one edit. It needed a merge-only contract per backend to stop
1067
+ sub-agents clobbering the one document — an epilogue in `agent-dispatch.mjs`, a `--mode=subagent-start`
1068
+ epilogue for codex, a legend line in every provisioned pad, and a paragraph of the worker contract, all
1069
+ of it policing a hazard rather than removing it. And the injection was INVISIBLE to the operator, who
1070
+ could neither see what their worker would be handed nor change it.
1071
+
1072
+ **What replaced it, maintainer's design:** the thread gets `.frizz/threads/<session-id>/` as a free-form
1073
+ scratch DIRECTORY — nothing provisioned into it, no reserved filename, no format — and compaction
1074
+ recovery moves to `mcp__frizz__recurring_prompt`'s new POST-COMPACTION trigger (scheduler SOURCE 7,
1075
+ landed the same day). The worker writes whatever doc it likes and LINKS it in the prompt; frizz re-sends
1076
+ that text the instant the context is summarized away. Durable in SQLite, visible and editable in the
1077
+ thread footer, and the worker chooses what it gets back.
1078
+
1079
+ The sub-agent story falls out for free: **one file per writer**. There is nothing to merge, so there is
1080
+ nothing to clobber, and the entire merge contract is deleted rather than reworded. Both child epilogues
1081
+ now say "write your OWN file; never edit or delete a file another agent wrote". The delegated-authority
1082
+ carve-out ("write only <path>" must not forbid the child's own coordination file) MOVED out of the
1083
+ worker contract and into the two epilogues that actually reach a child — it is pinned in
1084
+ `agent-dispatch-hook.test.ts` and `scratchpad-hook.test.ts` rather than in the contract goldens.
1085
+
1086
+ **The accepted cost, stated plainly rather than engineered around.** The maintainer chose this over a
1087
+ hybrid that kept a canonical doc, knowing compaction recovery degrades from a guaranteed injection to a
1088
+ pointer the model can skip. So the hook now NAMES the files in the directory and never their content
1089
+ (`scratchpad-hook.test.ts` pins that a body written into a scratch file does not appear in the
1090
+ injection). Re-growing that listing back into a content injection would restore exactly what was
1091
+ removed. The guaranteed channel is the arming, and every surface that mentions the directory also
1092
+ mentions `post_compaction: true`, because a folder nothing ever reads back is a folder of notes nobody
1093
+ opens.
1094
+
1095
+ **Two migrations that cost nothing.** `discover.ts`'s transcript-recovery sentinel shortened from
1096
+ `threads/<id>/scratch.md` to `threads/<id>/` — the new string is a PREFIX of the old one, so every
1097
+ transcript written before today still matches and no live thread lost its recovery path (pinned by
1098
+ `discover.test.ts`). And the Doc tab now renders the directory: files concatenated under a heading
1099
+ each, newest first, dotfiles excluded (frizz's own `.scratchpad-state.json` is not the worker's notes),
1100
+ binaries named rather than inlined, with per-file and total caps that say when they truncated. It is
1101
+ gated on the directory being NON-EMPTY rather than merely present, since dispatch now creates it empty
1102
+ and "exists" stopped meaning "the worker wrote something".
1103
+
1104
+ ## 2026-08-25: the prompt box's model + effort profile is the MACHINE's, not the project's
1105
+
1106
+ Maintainer: *"Changing the model and effort in a prompt box. That should be remembered, that new setting, but it should also be applied globally across all projects."* It was remembered — `DispatchPreferences` has been durable since the composer grew the selector — but in the per-project SQLite `settings` row, so one server serving N projects showed N different profiles, and a choice made in one project was invisible in the next.
1107
+
1108
+ - **The record is now the `dispatchPreferences` entry of a machine-level key/value store** — `server/machine-config.ts`, one JSON object at `<data>/config.json` keyed by record name, each value validated by its owner's schema — resolved store → this project's stored row → the Settings-derived default. The context exposes `getDispatchPreferences`/`setDispatchPreference` closures over `home`, so the router never learns where the record is.
1109
+ - **The store is the machine-level counterpart of a project database's `settings` table, and it has two tenants from day one.** It first landed as a file of its own, `dispatch-preferences.json`; the maintainer asked *"surely we have some general-purpose system for storing bits of persistent configuration like this?"* — there was none at machine level (`settings.json`, `registry.json` and `cloud.json` each hand-roll a reader and writer), so the answer was to build it and move the machine settings (`font`, `notifications`, `localFileOpener`, `projectRail`) into it as the `settings` record. The old `settings.json` is read as a fallback and never written again; a settings reset removes both. `registry.json` and `cloud.json` were left as they are — each has its own lifecycle and locking story, and neither was the question.
1110
+ - **No migration.** A project that chose a profile before the file existed keeps showing it from its row; the next selection, made anywhere, writes the file and is what every project opens on from then on. Every write still mirrors into the row, so an older server reading that database sees the same choice.
1111
+ - **The whole record moved, including the per-runtime `permissionMode` inside it.** That field is vestigial — no `SetDispatchPreferenceInput` variant can set it and the composer's resolver never reads it (dispatch permission is decided server-side from Settings) — so nothing per-project was given up. Settings' own `permissionMode` stays per project, as before.
1112
+ - **The web cache treats `["dispatchPreferencesGet"]` as machine-wide** (`lib/queryKeyScope.ts`), so the composer's optimistic write on one project is what a client-side switch paints on the next, rather than that project's last-seen copy for the beat before a refetch.
1113
+
1114
+ Verified on a real two-project stack (launcher + tenant on one server): a profile chosen through the tenant's RPC is what the launcher reads, and back; a per-project `settingsSet` on the launcher stayed invisible to the tenant, which is the control that the two prefixes reach different projects.
1115
+ ## 2026-08-26: Frizz mounts NO browser — a `chrome-devtools` server is the project's to bring
1116
+
1117
+ Maintainer: *"Is the Chrome DevTool stuff something that frizz automatically injects? Why not just let the user bring that themselves? That's very confusing to me. I feel like there's a level of opinionation here that we don't really want."*
1118
+
1119
+ This reverses the 2026-08-03 line above (*"`chrome-devtools` also stays always-mounted — giving a worker a browser is a capability, not an opinion"*). The capability argument stopped holding once the bill was measured: the server's 29 tool schemas sat in the prefix of **every** worker session at **~6,400 tokens**, and most workers never open a page. A capability nobody asked for, charged per turn, is an opinion.
1120
+
1121
+ - **Both backends stopped injecting it.** Claude's inline `--mcp-config` (`claudeMcpConfig`, `dispatch.ts`) and codex's `-c mcp_servers.chrome-devtools=…` app-server override (`codex-mcp.ts`) now mount the unified `frizz` server and nothing else. Parity was the point of the original pairing and it is the point of the removal.
1122
+ - **The lazy proxy went with it.** `cc-worker/bin/browser-mcp.mjs` and its committed schema snapshot `browser-mcp-tools.json` existed only to make the always-on mount affordable in MEMORY (159 MB → ~17 MB per worker, 2026-08-19). With no mount there is nothing to proxy, so they are deleted along with `chromeDevtoolsMcpSpec` / `chromeDevtoolsMcpMount` / `resolveBrowserMcpScript` / `CHROME_DEVTOOLS_MCP`, `scripts/harvest-browser-mcp-tools.mjs`, and the worker-plugin closure entries that required them. The memory win it bought is now the token win.
1123
+ - **Nothing about a browser is banned — it is just not Frizz's to decide.** A project adds a `.mcp.json` (plus `enabledMcpjsonServers` in `.claude/settings.json`, so a headless worker is never blocked on the approval prompt); an operator runs `claude mcp add --scope user`. Frizz's `--mcp-config` ADDS to whatever the CLI discovered rather than replacing it (frizz never passes `--strict-mcp-config`), so both paths reach a dispatched worker untouched.
1124
+ - **This repo does exactly that, and that is the whole demonstration.** Frizz's own skills drive Chrome headless, so the repo root carries the `.mcp.json` that used to be injected — pinned `chrome-devtools-mcp@1.7.0 --experimentalPageIdRouting --headless --isolated --no-usage-statistics`, the same argv the deleted spec rendered. Verified end-to-end on a disposable stack: a worker dispatched in a scratch project with no config reported `frizz` tools and no browser; a worker dispatched in this repo reported the full `mcp__chrome-devtools__*` set with no permission prompt.
1125
+ - **What deliberately stayed.** The orphan reaper still reaps `chrome-devtools-mcp` processes by name (a browser the USER brought leaks exactly the same way), and the transcript still renders a `take_screenshot` image result. Neither depends on who mounted the server.
1126
+
1127
+ ## 2026-08-26: Sub-agent profiles are effort-only, and the dispatch epilogue carries mechanics, not a handoff format
1128
+
1129
+ Follow-on to the browser decision above, from the same audit of what Frizz registers into a worker that the worker did not ask for. The maintainer's frame — *"a level of opinionation here that we don't really want"* — applied to two more registrations, and the frizz Goal had this thread decide them itself.
1130
+
1131
+ - **16 `frizz:<model>-<effort>` profiles → 5 `frizz:<effort>` ones.** The model×effort grid existed because the Agent tool once had no way to pin either; today its own `model` parameter overrides a profile's model, so the only knob a profile still has to carry is `effort`. The grid cost ~1,340 tokens of agent descriptions on every session, and those descriptions doubled as a routing doctrine ("Opus matches Fable and costs less", "Haiku for scripted harvest only") that no project asked for. `low` … `max` carry no model and inherit the worker's; Haiku, which takes no effort setting, is dispatched with `model: "haiku"` and no profile. The worker contract's Sub-agents section now states the two knobs and nothing about which tier deserves which task.
1132
+ - **The dispatch-hook epilogue kept only what a helper cannot discover.** Its first paragraph prescribed the shape of a helper's final message (status, files, SHA, evidence, caveats, next action); that is a handoff format, and it is gone — one sentence remains saying the final message is what the dispatcher reads. The rest stayed because each line is a mechanic learned from a real failure: the `SendMessage({to: "main"})` upward channel, the own-file rule for the scratch directory, no fan-out of its own unless asked, and the symlinked-`.output` wait-loop trap.
1133
+ - **Verified** by the live harness (`_live_broker_workerenv.mts`, now dispatching `frizz:low`): a real broker worker through the real server dispatched the profile and it ran.
1134
+
1135
+ ## 2026-08-26: three more worker registrations deleted — the waits skill, the pre-compaction brief, and the toon nudge
1136
+
1137
+ Third pass of the same audit that removed the browser mount and the model×effort profile grid above, and the same test: does Frizz register this into every worker because the worker needs it, or because Frizz has an opinion?
1138
+
1139
+ - **`skills/waits` is DELETED.** Maintainer: *"I feel like the waits skill is being replaced now. I don't think we need that. We have our own system now for registering watchers and whatnot as tools."* The skill's whole job was teaching a worker how to hold a wait open without falling out of the board's Active band, and the durable half of that is now TOOLS — `mcp__frizz__watch_pr`, `mcp__frizz__timer`, `mcp__frizz__recurring_prompt` — which carry their own descriptions and need no playbook. What remains (a sub-agent owns the wait, `run_in_background` does not hold a rest, never fake a wait with a foreground sleep) is already stated once in the system-prompt contract, which is where a mechanical rule belongs.
1140
+ - **The portable monitors STAYED.** `skills/gh/scripts/{ci-watch,github-watch,review-watch}.mjs` are generated byte-for-byte from `monitors/` and are load-bearing for two other readers: the codex worker prompt names their absolute directory (`monitorScriptsDir()`, `dispatch.ts`, pinned by `dispatch.test.ts`), and `skills/gh` documents them as its declared-tooling fallback. Only the skill went.
1141
+ - **`hooks/precompact-instructions.mjs` is DELETED**, with both its `PreCompact` registrations. Maintainer: *"the pre-compaction instructions are probably too opinionated, entirely too opinionated."* It handed the summarizer an editorial brief about WHAT to preserve — plan, alternatives rejected, rationale, at high fidelity — which is a prescription about how someone else's work should be remembered. `scratchpad.mjs --mode=precompact` keeps both matchers: it only says the worker's scratch files exist and names their paths, which is mechanics.
1142
+ - **The `toon` bullet is gone from the seed's `⟦gh available⟧` block and from `skills/gh`.** Maintainer: *"It seems pretty opinionated to tell it to pipe through toon, given that toon is probably not installed on most machines. That might be over the line."* The original reasoning (`plans/github-integration.md` §9: worker-side yes, server-side no) was measured against THIS machine's toolchain; a `command -v toon` guard makes the advice safe but not less opinionated, and it spent prompt tokens on every authed session to describe a binary almost no repo has. The one raw-API recipe that piped through it now uses `--jq`, which ships with `gh`.
1143
+
1144
+ ## 2026-08-28: the worker's tool is `goal`, and the scratch directory is offered rather than pushed
1145
+
1146
+ Two corrections from one maintainer thread, both about Frizz telling the worker what to do instead of what it can do.
1147
+
1148
+ - **`recurring_prompt` is renamed `goal`.** The board panel, the footer mark and the delivered trailer have all said "Goal" since 2026-08-11; only the MCP tool still carried the mechanism's name. Maintainer: *"I don't think the tool is even called Recurring Prompt anymore, is it?"* — then *"Rename it."* Only the TOOL name and the worker-facing wording changed: the RPC procedures (`setOwnThreadRecurringPrompt`, `getOwnThreadRecurringPrompt`) and the `session.recurring_*` columns keep their names, because renaming the procedure is exactly what stranded every in-flight worker the last time (see the legacy aliases in `@frizz/shared`). No alias tool is listed for the old name — Claude Code rejects a tool name it has not listed, so an alias would have to be a visible second tool forever — and the new tool's description names its former name once, for a resumed worker whose summary still says `recurring_prompt`.
1149
+ - **Every surface that mentions the scratch directory stopped prescribing the notes-plus-arming arrangement.** The 2026-08-06 entry above set the rule "every surface that mentions the directory also mentions `post_compaction: true`", and it produced the effect the maintainer noticed: workers armed the post-compaction goal constantly, 49 of the 65 armed prompts in the live db copying the contract's template sentence verbatim. Maintainer: *"this is a very opinionated thing, and I don't think we should be pushing it as the thing to do. We should just be saying that the scratch directory is available to it if it wants it. We can also be saying that it can set these goal hooks."* The contract's scratch sections, the dispatch preamble, `scratchpadOrientation`, the five `scratchpad.mjs` hook texts and the tool's own `post_compaction` wording now say the directory and the goal hooks EXIST and leave the choice to the worker. The literal `post_compaction: true` stays on each surface so the capability is still discoverable; the nudge hook keeps its ~60k-token cadence with the informational wording.
1150
+
1151
+ ## 2026-09-03: a worker mounts ONLY what its project declares — `--strict-mcp-config`, with the project's `.mcp.json` carried inline
1152
+
1153
+ Maintainer, the evening of two memory crashes (a jetsam spiral into a watchdog panic at 19:50, a forced power-off at 20:52): *"Very weird that Frizz eagerly spawns Chrome DevTools."* It was not Frizz. It was the CLI's own discovery doing exactly what the 2026-08-26 entry above asked of it — in every worker at once.
1154
+
1155
+ Measured on the maintainer's machine 2026-09-02, nine live workers: the user-scope `chrome-real` (`claude mcp add --scope user`, an npx chrome-devtools-mcp) and each project's `chrome-devtools` (`.mcp.json`) both booted in EVERY worker — 18 browser servers, 36 processes, 4.2 GB, and 61 thirty-second MCP connection timeouts inside the crash window. The 2026-08-26 line *"Frizz's `--mcp-config` ADDS to whatever the CLI discovered rather than replacing it (frizz never passes `--strict-mcp-config`)"* was written so a project and an operator could each bring a browser. It also meant a fleet multiplied the operator's personal servers by N.
1156
+
1157
+ - **Every worker now launches under `--strict-mcp-config`** (`strictMcpConfig: true` on the SDK path, the flag on the argv path) and is handed its whole MCP surface inline, lowest precedence first: the operator's REMOTE user-scope servers (`type: http`/`sse` in `~/.claude.json` — a URL costs no process), the project's `.mcp.json` filtered by the same approval the CLI applies (`enableAllProjectMcpServers` / `enabledMcpjsonServers` / `disabledMcpjsonServers`, every layer unioned), then the `frizz` server, which wins a name collision. `packages/server/src/backend/project-mcp-servers.ts` is the rule and the reasons; the daemon reads it at every fork, so a `.mcp.json` edit reaches the next dispatch as it reaches the next plain `claude`.
1158
+ - **User-scope STDIO servers are the one thing a worker no longer inherits.** `claude mcp add --scope user` still reaches the operator's own sessions. A project that wants one in its fleet declares it in its `.mcp.json` — the scope that means "this repo's sessions".
1159
+ - **Nothing about settings changed.** `settingSources` stays `user,project,local` (the 2026-08-16 entry): auth, permissions, hooks, plugins and `CLAUDE.md` all still reach a worker. Only MCP discovery is replaced, and only with what discovery would have produced minus the operator's local processes.
1160
+ - **What this does not fix.** Twelve threads restarted at once are still ~9 processes and ~1 GB each before any of them runs a build; the crash was the aggregate, and this removes its largest avoidable term. A dispatch-time back-pressure check is the follow-up.
1161
+
1162
+ ## 2026-09-05: the dispatch epilogue finally carries the lesson it has been credited with — nothing can wake a helper
1163
+
1164
+ The "cc hooks DROPPED" table above has claimed since the `SubagentStop` hooks were dropped that *"the worker-facing half of the rest-guard's lesson (run long ops inline, don't rest on a waiter) is carried in the dispatch epilogue instead."* It was not. The epilogue carried the handoff contract, the own-file rule, the `SendMessage` upward channel, the default-off nesting rule and the nested-collection paragraph — and nothing at all about the child's own lifecycle. The rule existed only in a sentence describing where it had gone.
1165
+
1166
+ A Fable helper on `nubjs/nub` ("Investigate Node SEA as container", 118 tool calls, 393,805 tokens) found the gap four times in one effort. It backgrounded ten Bash shells across its turns and ended each turn waiting for them, returning the same sentence: *"Three background jobs are outstanding … I am waiting for their completion notifications before writing."* Claude's own task-notification states the mechanism from the other side — **"a task-notification fires each time this agent stops with no live background children of its own"** — so the completion goes to the DISPATCHER and the helper is never resumed to read it. Its entire investigation lived only in transient return messages until the dispatcher captured them to a file by hand and resumed it a fourth time with "your first and only action this turn is the Write tool".
1167
+
1168
+ - **The epilogue's second paragraph is now the lifecycle rule**, placed directly under the handoff sentence because it is the same subject: the turn is the unit. It states that ending a turn IS returning, that a backgrounded job reports to the dispatcher and never to the child, and that a turn ending on a waiter hands back unfinished work. It then gives the three things to do instead — run long commands in the foreground; where one would outrun the foreground limit, make the command itself loop to its own terminal condition; and if something was backgrounded, collect it in the same turn.
1169
+ - **It also closes the second half of the same failure**: write anything you would not want to lose into your own scratch file AS YOU GO. The findings were nearly lost not because the helper stopped, but because it had written nothing down when it did.
1170
+ - **`agent-dispatch.mjs` is the home for the same reason the 2026-07-31 entry gives**: it fires at every depth and is the only seam that reaches a helper, whose dispatch prompt is otherwise whatever its parent thought to write. The root worker contract carries no equivalent — it reaches the one agent that cannot be stranded this way.
1171
+ - **The same change CORRECTED the nested-collection sentence**, which had promised since 2026-07-31 that a helper's *"completion is delivered to you automatically"*. It is not, and the experiment is one dispatch: a child told to dispatch one helper and stop the instant the Agent tool returned came back `DISPATCHED: yes / SAW_HELPER_REPLY: no`, and was never resumed to read the reply. That sentence sat four paragraphs below the new rule and is exactly what a helper told not to wait would reason its way back to, so it now reads *"a helper cannot wake you either: a dispatcher that stops is never resumed to read the reply"*. The anti-polling instruction the 2026-07-31 entry wrote it to carry — the `.output` symlink, the unreliable `"type":"result"` record — is untouched; only its false premise is. It also strengthens the 2026-08-04 default-off rule with a mechanism rather than a preference: a child that fans out cannot collect what it dispatched.
1172
+ - **Rejected: denying `run_in_background:true` from a sub-agent in `bash-background.mjs`.** The hook input does identify a child (`agent_id`, which `scratchpad.mjs` already reads), so it is available — but a helper launching several builds and collecting them inside one turn is legitimate, and the foreground cap makes a blanket denial actively worse. Same call as the depth-2 DENY rejected on 2026-07-31: too blunt for a hook that exists because helpers are allowed.