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.
- package/dist/claude-agent-broker.js +35911 -0
- package/dist/codex-app-server-daemon.js +423 -0
- package/dist/dev-child.js +56233 -0
- package/package.json +34 -0
- package/runtime/board/agent-bindings.mjs +287 -0
- package/runtime/board/agent-liveness.mjs +367 -0
- package/runtime/board/agent-status.mjs +178 -0
- package/runtime/board/config.mjs +993 -0
- package/runtime/board/decisions.mjs +97 -0
- package/runtime/board/index.mjs +704 -0
- package/runtime/board/notify-shared.mjs +90 -0
- package/runtime/board/notify.mjs +81 -0
- package/runtime/board/ownership.mjs +120 -0
- package/runtime/board/rest-detect.mjs +213 -0
- package/runtime/board/thread-excerpt.mjs +162 -0
- package/runtime/board/thread-update.mjs +289 -0
- package/runtime/cc-worker/.claude-plugin/plugin.json +10 -0
- package/runtime/cc-worker/DECISIONS.md +1172 -0
- package/runtime/cc-worker/LICENSE +21 -0
- package/runtime/cc-worker/agents/high.md +7 -0
- package/runtime/cc-worker/agents/low.md +7 -0
- package/runtime/cc-worker/agents/max.md +7 -0
- package/runtime/cc-worker/agents/medium.md +7 -0
- package/runtime/cc-worker/agents/xhigh.md +7 -0
- package/runtime/cc-worker/bin/frizz +17 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +1612 -0
- package/runtime/cc-worker/bin/frizz-update +18 -0
- package/runtime/cc-worker/hooks/agent-bind.mjs +40 -0
- package/runtime/cc-worker/hooks/agent-dispatch.mjs +121 -0
- package/runtime/cc-worker/hooks/bash-background.d.mts +6 -0
- package/runtime/cc-worker/hooks/bash-background.mjs +247 -0
- package/runtime/cc-worker/hooks/deny-ask.mjs +39 -0
- package/runtime/cc-worker/hooks/deny-plan.mjs +62 -0
- package/runtime/cc-worker/hooks/hooks.json +102 -0
- package/runtime/cc-worker/hooks/perm-policy.mjs +211 -0
- package/runtime/cc-worker/hooks/scratchpad.mjs +417 -0
- package/runtime/cc-worker/hooks/session-seed.mjs +107 -0
- package/runtime/cc-worker/scripts/frizz/agent-bindings.mjs +9 -0
- package/runtime/cc-worker/scripts/frizz/config.mjs +12 -0
- package/runtime/cc-worker/skills/gh/SKILL.md +141 -0
- package/runtime/cc-worker/skills/gh/scripts/ci-watch.mjs +60 -0
- package/runtime/cc-worker/skills/gh/scripts/github-watch.mjs +130 -0
- package/runtime/cc-worker/skills/gh/scripts/review-watch.mjs +54 -0
- package/web-dist/apple-touch-icon.png +0 -0
- package/web-dist/assets/TerminalPane-DyLvW_rQ.js +7 -0
- package/web-dist/assets/abnfDiagram-VRR7QNED-DIPgkiM8.js +1 -0
- package/web-dist/assets/arc-BSyeo0Gb.js +1 -0
- package/web-dist/assets/architecture-TIHT7OUA-B8qUD5-C.js +1 -0
- package/web-dist/assets/architectureDiagram-ZJ3FMSHR-DBAKToiy.js +36 -0
- package/web-dist/assets/array-BifhSqXX.js +1 -0
- package/web-dist/assets/blockDiagram-677ZJIJ3-Ba0xt8st.js +132 -0
- package/web-dist/assets/c4Diagram-LMCZKHZV-DFham1h_.js +10 -0
- package/web-dist/assets/channel-5l10tOPT.js +1 -0
- package/web-dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web-dist/assets/chunk-32BRIVSS-Bl-817K-.js +1 -0
- package/web-dist/assets/chunk-52WLFC77-DBLTDz2W.js +10 -0
- package/web-dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web-dist/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- package/web-dist/assets/chunk-C7G6YPKG-ClL6Ebv8.js +1 -0
- package/web-dist/assets/chunk-EX3LRPZG--3vJLCZP.js +231 -0
- package/web-dist/assets/chunk-FWX5IMBZ-DMOdhcCP.js +2 -0
- package/web-dist/assets/chunk-HOUHSVGY-DkgTGLCa.js +1 -0
- package/web-dist/assets/chunk-ICXQ74PX-7X6iir1H.js +2 -0
- package/web-dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web-dist/assets/chunk-KEIR6QF5-BfrZ3jm6.js +161 -0
- package/web-dist/assets/chunk-MOJQB5TN-OpO5flE4.js +88 -0
- package/web-dist/assets/chunk-OGEWGWER-BbAMAzTZ.js +1 -0
- package/web-dist/assets/chunk-PUDLZKDR-avcvDgZl.js +156 -0
- package/web-dist/assets/chunk-Q4XR5HBZ-BaiGN1cd.js +70 -0
- package/web-dist/assets/chunk-RYQCIY6F-Cu_KplZW.js +1 -0
- package/web-dist/assets/chunk-V7JOEXUC-CKVakdOJ.js +206 -0
- package/web-dist/assets/chunk-VAUOI2AC-DzG-rM3_.js +1 -0
- package/web-dist/assets/chunk-VR4S4FIN-t3j3HHQF.js +1 -0
- package/web-dist/assets/chunk-WYO6CB5R-SnP0NDTw.js +127 -0
- package/web-dist/assets/chunk-XXDRQBXY-DYlTP5J-.js +1 -0
- package/web-dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web-dist/assets/chunk-ZGVPDNZ5-pXn3giwS.js +62 -0
- package/web-dist/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
- package/web-dist/assets/classDiagram-OUVF2IWQ-vIfzHupB.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-vIfzHupB.js +1 -0
- package/web-dist/assets/cose-bilkent-JH36ORCC-BUIsLrGc.js +1 -0
- package/web-dist/assets/cynefin-VYW2F7L2-C4qNLMkm.js +1 -0
- package/web-dist/assets/cynefinDiagram-TSTJHNR4-2vzWUUfl.js +62 -0
- package/web-dist/assets/cytoscape.esm-B3I8pqwA.js +321 -0
- package/web-dist/assets/dagre-CXRCoUWR.js +1 -0
- package/web-dist/assets/dagre-VKFMJZFB-DUdNHEM9.js +4 -0
- package/web-dist/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/web-dist/assets/diagram-FQU43EPY-C_EHNL09.js +3 -0
- package/web-dist/assets/diagram-G47NLZAW-DOt98NB-.js +24 -0
- package/web-dist/assets/diagram-NH7WQ7WH-uIgVP9iZ.js +24 -0
- package/web-dist/assets/diagram-OA4YK3LP-CGWe4oxq.js +30 -0
- package/web-dist/assets/diagram-WEI45ONY-C-5f7o9T.js +41 -0
- package/web-dist/assets/dist-DoH_9pyS.js +1 -0
- package/web-dist/assets/ebnfDiagram-CCIWWBDH-DoSFLtL-.js +1 -0
- package/web-dist/assets/erDiagram-Q63AITRT-DsCLMzEE.js +85 -0
- package/web-dist/assets/eventmodeling-45OFAUF4-D7GQYhiK.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-CR371xZs.js +1 -0
- package/web-dist/assets/ganttDiagram-NO4QXBWP-D8UNGcBR.js +292 -0
- package/web-dist/assets/gitGraph-TEB2WS4Q-mC-XQzTE.js +1 -0
- package/web-dist/assets/gitGraphDiagram-IHSO6WYX-CkpPggS7.js +106 -0
- package/web-dist/assets/graphlib-B8gBHxth.js +1 -0
- package/web-dist/assets/index-CT6k_A5y.css +1 -0
- package/web-dist/assets/index-Dmo0zJc8.js +319 -0
- package/web-dist/assets/info-DKCQHKI2-Drg-xVbr.js +1 -0
- package/web-dist/assets/infoDiagram-FWYZ7A6U-CsGMTGpl.js +2 -0
- package/web-dist/assets/init-D6jRqBbL.js +1 -0
- package/web-dist/assets/ishikawaDiagram-FXEZZL3T-DNgGBlL6.js +70 -0
- package/web-dist/assets/journeyDiagram-5HDEW3XC-BFN2bObi.js +139 -0
- package/web-dist/assets/kanban-definition-HUTT4EX6-BnDPclXf.js +89 -0
- package/web-dist/assets/katex-CddkPoXu.js +257 -0
- package/web-dist/assets/line-DmLw74JM.js +1 -0
- package/web-dist/assets/linear-z2V0wJk9.js +1 -0
- package/web-dist/assets/map-DsCK-0Cs.js +1 -0
- package/web-dist/assets/mermaid-parser.core-DGJk39E-.js +7 -0
- package/web-dist/assets/mermaid.core-8aee8nsf.js +11 -0
- package/web-dist/assets/mindmap-definition-LN4V7U3C-CGWK_Qbm.js +96 -0
- package/web-dist/assets/ordinal-hYBb2elL.js +1 -0
- package/web-dist/assets/packet-7NZHBO7P-C5HYQyS5.js +1 -0
- package/web-dist/assets/path-BWPyau1x.js +1 -0
- package/web-dist/assets/pegDiagram-2B236MQR-WiQm887Q.js +1 -0
- package/web-dist/assets/pie-RZYD4A2V-DKBNMtMn.js +1 -0
- package/web-dist/assets/pieDiagram-ENE6RG2P-F1A8_3DO.js +39 -0
- package/web-dist/assets/quadrantDiagram-ABIIQ3AL-bf6a3f_f.js +7 -0
- package/web-dist/assets/radar-I7S5WNFK-AOKDUn-C.js +1 -0
- package/web-dist/assets/railroad-3IZDKUUU-DNHhkFAC.js +1 -0
- package/web-dist/assets/railroad-abnf-AHOZXSZD-DOXbu4iv.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-C1oE2RHD.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-B4GD-bq-.js +1 -0
- package/web-dist/assets/railroadDiagram-RFXS5EU6-Be52T90z.js +1 -0
- package/web-dist/assets/requirementDiagram-TGXJPOKE-BqdMvEGK.js +84 -0
- package/web-dist/assets/rolldown-runtime-Bh1tDfsg.js +1 -0
- package/web-dist/assets/rough.esm-CSKSodPl.js +1 -0
- package/web-dist/assets/sankeyDiagram-HTMAVEWB-BP3X6Ofp.js +40 -0
- package/web-dist/assets/sequenceDiagram-DBY2YBRQ-yDHhaUzc.js +162 -0
- package/web-dist/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- package/web-dist/assets/src-C4XfhTaE.js +1 -0
- package/web-dist/assets/stateDiagram-2N3HPSRC-xvctsgCU.js +1 -0
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-xFk0N3Cq.js +1 -0
- package/web-dist/assets/swimlanes-5IMT3BWC-DZMLgrjk.js +2 -0
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-BoWrxkxy.js +8 -0
- package/web-dist/assets/timeline-definition-FHXFAJF6-n8sU0qlT.js +120 -0
- package/web-dist/assets/treeView-QDETBFTQ-Su8KloaY.js +1 -0
- package/web-dist/assets/treemap-6X3UGDF4-CNgRuVWf.js +1 -0
- package/web-dist/assets/vennDiagram-L72KCM5P-B5I9YxaY.js +34 -0
- package/web-dist/assets/wardley-OPB4EBWU-khMe_Wbq.js +1 -0
- package/web-dist/assets/wardleyDiagram-EHGQE667-d8LsqhTO.js +78 -0
- package/web-dist/assets/xychartDiagram-FW5EYKEG-b0CH_-wy.js +7 -0
- package/web-dist/favicon-16.png +0 -0
- package/web-dist/favicon-32.png +0 -0
- package/web-dist/favicon.svg +34 -0
- package/web-dist/icon-192.png +0 -0
- package/web-dist/icon-512.png +0 -0
- package/web-dist/icon-maskable-512.png +0 -0
- package/web-dist/index.html +44 -0
- 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.
|