frizz 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/dist/claude-agent-broker.js +21714 -0
  4. package/dist/codex-app-server-daemon.js +354 -0
  5. package/dist/dev-child.js +39342 -0
  6. package/dist/frizz.js +10517 -0
  7. package/package.json +54 -20
  8. package/runtime/board/agent-bindings.mjs +287 -0
  9. package/runtime/board/agent-liveness.mjs +367 -0
  10. package/runtime/board/agent-status.mjs +178 -0
  11. package/runtime/board/config.mjs +982 -0
  12. package/runtime/board/decisions.mjs +97 -0
  13. package/runtime/board/index.mjs +699 -0
  14. package/runtime/board/notify-shared.mjs +90 -0
  15. package/runtime/board/notify.mjs +81 -0
  16. package/runtime/board/ownership.mjs +120 -0
  17. package/runtime/board/rest-detect.mjs +213 -0
  18. package/runtime/board/thread-excerpt.mjs +162 -0
  19. package/runtime/board/thread-update.mjs +285 -0
  20. package/runtime/cc-worker/.claude-plugin/plugin.json +10 -0
  21. package/runtime/cc-worker/DECISIONS.md +1025 -0
  22. package/runtime/cc-worker/LICENSE +21 -0
  23. package/runtime/cc-worker/agents/fable-high.md +8 -0
  24. package/runtime/cc-worker/agents/fable-low.md +8 -0
  25. package/runtime/cc-worker/agents/fable-max.md +8 -0
  26. package/runtime/cc-worker/agents/fable-medium.md +8 -0
  27. package/runtime/cc-worker/agents/fable-xhigh.md +8 -0
  28. package/runtime/cc-worker/agents/haiku.md +7 -0
  29. package/runtime/cc-worker/agents/opus-high.md +8 -0
  30. package/runtime/cc-worker/agents/opus-low.md +8 -0
  31. package/runtime/cc-worker/agents/opus-max.md +8 -0
  32. package/runtime/cc-worker/agents/opus-medium.md +8 -0
  33. package/runtime/cc-worker/agents/opus-xhigh.md +8 -0
  34. package/runtime/cc-worker/agents/sonnet-high.md +8 -0
  35. package/runtime/cc-worker/agents/sonnet-low.md +8 -0
  36. package/runtime/cc-worker/agents/sonnet-max.md +8 -0
  37. package/runtime/cc-worker/agents/sonnet-medium.md +8 -0
  38. package/runtime/cc-worker/agents/sonnet-xhigh.md +8 -0
  39. package/runtime/cc-worker/bin/frizz +17 -0
  40. package/runtime/cc-worker/bin/frizz-mcp.mjs +564 -0
  41. package/runtime/cc-worker/bin/frizz-update +18 -0
  42. package/runtime/cc-worker/hooks/agent-bind.mjs +40 -0
  43. package/runtime/cc-worker/hooks/agent-dispatch.mjs +98 -0
  44. package/runtime/cc-worker/hooks/bash-background.d.mts +6 -0
  45. package/runtime/cc-worker/hooks/bash-background.mjs +236 -0
  46. package/runtime/cc-worker/hooks/deny-ask.mjs +38 -0
  47. package/runtime/cc-worker/hooks/deny-plan.mjs +61 -0
  48. package/runtime/cc-worker/hooks/hooks.json +111 -0
  49. package/runtime/cc-worker/hooks/perm-policy.mjs +211 -0
  50. package/runtime/cc-worker/hooks/precompact-instructions.mjs +122 -0
  51. package/runtime/cc-worker/hooks/scratchpad-stop.mjs +125 -0
  52. package/runtime/cc-worker/hooks/scratchpad.mjs +446 -0
  53. package/runtime/cc-worker/hooks/session-seed.mjs +106 -0
  54. package/runtime/cc-worker/scripts/frizz/agent-bindings.mjs +9 -0
  55. package/runtime/cc-worker/scripts/frizz/config.mjs +12 -0
  56. package/runtime/cc-worker/skills/gh/SKILL.md +154 -0
  57. package/runtime/cc-worker/skills/gh/scripts/ci-watch.mjs +60 -0
  58. package/runtime/cc-worker/skills/gh/scripts/github-watch.mjs +130 -0
  59. package/runtime/cc-worker/skills/gh/scripts/review-watch.mjs +54 -0
  60. package/runtime/cc-worker/skills/handoff/SKILL.md +209 -0
  61. package/runtime/cc-worker/skills/waits/SKILL.md +83 -0
  62. package/web-dist/apple-touch-icon.png +0 -0
  63. package/web-dist/assets/TerminalPane-ROKHp1ib.js +7 -0
  64. package/web-dist/assets/abnfDiagram-VRR7QNED-DcpdhBs3.js +1 -0
  65. package/web-dist/assets/arc-BSyeo0Gb.js +1 -0
  66. package/web-dist/assets/architecture-TIHT7OUA-CAviNivx.js +1 -0
  67. package/web-dist/assets/architectureDiagram-ZJ3FMSHR-CUAKf0mn.js +36 -0
  68. package/web-dist/assets/array-BifhSqXX.js +1 -0
  69. package/web-dist/assets/blockDiagram-677ZJIJ3-BPwpJIzx.js +132 -0
  70. package/web-dist/assets/c4Diagram-LMCZKHZV-1lptuHzZ.js +10 -0
  71. package/web-dist/assets/channel-CqKDIFQF.js +1 -0
  72. package/web-dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  73. package/web-dist/assets/chunk-32BRIVSS-CFR9AKjY.js +1 -0
  74. package/web-dist/assets/chunk-52WLFC77-CM9uct7m.js +10 -0
  75. package/web-dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  76. package/web-dist/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  77. package/web-dist/assets/chunk-C7G6YPKG-DiveJARw.js +1 -0
  78. package/web-dist/assets/chunk-EX3LRPZG-BE1CBw8F.js +231 -0
  79. package/web-dist/assets/chunk-FWX5IMBZ-DL42uXiO.js +2 -0
  80. package/web-dist/assets/chunk-HOUHSVGY-DPhJWgDw.js +1 -0
  81. package/web-dist/assets/chunk-ICXQ74PX-CwYy-6AP.js +2 -0
  82. package/web-dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  83. package/web-dist/assets/chunk-KEIR6QF5-BfrZ3jm6.js +161 -0
  84. package/web-dist/assets/chunk-MOJQB5TN-Ds5I9wxq.js +88 -0
  85. package/web-dist/assets/chunk-OGEWGWER-DHiZJwQD.js +1 -0
  86. package/web-dist/assets/chunk-PUDLZKDR-B-eyQTsF.js +156 -0
  87. package/web-dist/assets/chunk-Q4XR5HBZ-DK7dB3Ti.js +70 -0
  88. package/web-dist/assets/chunk-RYQCIY6F-Cu_KplZW.js +1 -0
  89. package/web-dist/assets/chunk-V7JOEXUC-Dn59m74L.js +206 -0
  90. package/web-dist/assets/chunk-VAUOI2AC-DK7x36hd.js +1 -0
  91. package/web-dist/assets/chunk-VR4S4FIN-D7-CI3Yl.js +1 -0
  92. package/web-dist/assets/chunk-WYO6CB5R-B3l-mLCs.js +127 -0
  93. package/web-dist/assets/chunk-XXDRQBXY-DYlTP5J-.js +1 -0
  94. package/web-dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  95. package/web-dist/assets/chunk-ZGVPDNZ5-Dobxlxie.js +62 -0
  96. package/web-dist/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
  97. package/web-dist/assets/classDiagram-OUVF2IWQ-B_-6iXYY.js +1 -0
  98. package/web-dist/assets/classDiagram-v2-EOCWNBFH-B_-6iXYY.js +1 -0
  99. package/web-dist/assets/cose-bilkent-JH36ORCC-BUIsLrGc.js +1 -0
  100. package/web-dist/assets/cynefin-VYW2F7L2-Dh7RuEUJ.js +1 -0
  101. package/web-dist/assets/cynefinDiagram-TSTJHNR4-ClPi2mZZ.js +62 -0
  102. package/web-dist/assets/cytoscape.esm-B3I8pqwA.js +321 -0
  103. package/web-dist/assets/dagre-CXRCoUWR.js +1 -0
  104. package/web-dist/assets/dagre-VKFMJZFB-52_WP1QV.js +4 -0
  105. package/web-dist/assets/defaultLocale-C8Fc0cco.js +1 -0
  106. package/web-dist/assets/diagram-FQU43EPY-D_1zVsTL.js +3 -0
  107. package/web-dist/assets/diagram-G47NLZAW-CdZxuGUy.js +24 -0
  108. package/web-dist/assets/diagram-NH7WQ7WH-C8pSFu0P.js +24 -0
  109. package/web-dist/assets/diagram-OA4YK3LP-C5bjZLre.js +30 -0
  110. package/web-dist/assets/diagram-WEI45ONY-Bxzhiuzn.js +41 -0
  111. package/web-dist/assets/dist-DoH_9pyS.js +1 -0
  112. package/web-dist/assets/ebnfDiagram-CCIWWBDH-g-Z0J2wP.js +1 -0
  113. package/web-dist/assets/erDiagram-Q63AITRT-DVNkgIHp.js +85 -0
  114. package/web-dist/assets/eventmodeling-45OFAUF4-MpmeH5YZ.js +1 -0
  115. package/web-dist/assets/flowDiagram-23GEKE2U-D-QgjjhF.js +1 -0
  116. package/web-dist/assets/ganttDiagram-NO4QXBWP-_71pQYEK.js +292 -0
  117. package/web-dist/assets/gitGraph-TEB2WS4Q-ChIZiGZS.js +1 -0
  118. package/web-dist/assets/gitGraphDiagram-IHSO6WYX-DCHAFI0l.js +106 -0
  119. package/web-dist/assets/graphlib-B8gBHxth.js +1 -0
  120. package/web-dist/assets/index-w4v-GZEc.js +358 -0
  121. package/web-dist/assets/index-zyi22LPz.css +1 -0
  122. package/web-dist/assets/info-DKCQHKI2-BW-n_T1j.js +1 -0
  123. package/web-dist/assets/infoDiagram-FWYZ7A6U-CgDYsKi9.js +2 -0
  124. package/web-dist/assets/init-D6jRqBbL.js +1 -0
  125. package/web-dist/assets/ishikawaDiagram-FXEZZL3T-ClzGNt9N.js +70 -0
  126. package/web-dist/assets/journeyDiagram-5HDEW3XC-DSCQxkHC.js +139 -0
  127. package/web-dist/assets/kanban-definition-HUTT4EX6-CdrdX9N8.js +89 -0
  128. package/web-dist/assets/katex-CddkPoXu.js +257 -0
  129. package/web-dist/assets/line-ha38Dc-1.js +1 -0
  130. package/web-dist/assets/linear-z2V0wJk9.js +1 -0
  131. package/web-dist/assets/map-DsCK-0Cs.js +1 -0
  132. package/web-dist/assets/mermaid-parser.core-Z4uMcpip.js +7 -0
  133. package/web-dist/assets/mermaid.core-iZRq3hbu.js +11 -0
  134. package/web-dist/assets/mindmap-definition-LN4V7U3C-DmhInJO_.js +96 -0
  135. package/web-dist/assets/ordinal-hYBb2elL.js +1 -0
  136. package/web-dist/assets/packet-7NZHBO7P-DBPB36Kl.js +1 -0
  137. package/web-dist/assets/path-BWPyau1x.js +1 -0
  138. package/web-dist/assets/pegDiagram-2B236MQR-CAH3ljfj.js +1 -0
  139. package/web-dist/assets/pie-RZYD4A2V-_h_eX4Ca.js +1 -0
  140. package/web-dist/assets/pieDiagram-ENE6RG2P-DFBPus8j.js +39 -0
  141. package/web-dist/assets/quadrantDiagram-ABIIQ3AL-DMvOCjt8.js +7 -0
  142. package/web-dist/assets/radar-I7S5WNFK-2EzoPHEZ.js +1 -0
  143. package/web-dist/assets/railroad-3IZDKUUU-BPJnn-hm.js +1 -0
  144. package/web-dist/assets/railroad-abnf-AHOZXSZD-YeUoiySk.js +1 -0
  145. package/web-dist/assets/railroad-ebnf-EBAXGLYW-Ddw1SuGG.js +1 -0
  146. package/web-dist/assets/railroad-peg-LSFZ7HO6-Dd8BOGeW.js +1 -0
  147. package/web-dist/assets/railroadDiagram-RFXS5EU6-DKq5FagA.js +1 -0
  148. package/web-dist/assets/requirementDiagram-TGXJPOKE-BJ5tGazp.js +84 -0
  149. package/web-dist/assets/rolldown-runtime-Bh1tDfsg.js +1 -0
  150. package/web-dist/assets/rough.esm-CSKSodPl.js +1 -0
  151. package/web-dist/assets/sankeyDiagram-HTMAVEWB-XSJjcBhX.js +40 -0
  152. package/web-dist/assets/sequenceDiagram-DBY2YBRQ-CBb8emSe.js +162 -0
  153. package/web-dist/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  154. package/web-dist/assets/src-C4XfhTaE.js +1 -0
  155. package/web-dist/assets/stateDiagram-2N3HPSRC-DDfRW94V.js +1 -0
  156. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-hc41W5Lx.js +1 -0
  157. package/web-dist/assets/swimlanes-5IMT3BWC-DvRYbkZi.js +2 -0
  158. package/web-dist/assets/swimlanesDiagram-G3AALYLV-BZyGdgSG.js +8 -0
  159. package/web-dist/assets/timeline-definition-FHXFAJF6-BNUa_DwI.js +120 -0
  160. package/web-dist/assets/treeView-QDETBFTQ-I6-IW6nJ.js +1 -0
  161. package/web-dist/assets/treemap-6X3UGDF4-CWWmEUYJ.js +1 -0
  162. package/web-dist/assets/vennDiagram-L72KCM5P-DTDrPGLk.js +34 -0
  163. package/web-dist/assets/wardley-OPB4EBWU-CNsdgXXA.js +1 -0
  164. package/web-dist/assets/wardleyDiagram-EHGQE667-YE0tq3Kh.js +78 -0
  165. package/web-dist/assets/xychartDiagram-FW5EYKEG-D0ofMX8C.js +7 -0
  166. package/web-dist/favicon-16.png +0 -0
  167. package/web-dist/favicon-32.png +0 -0
  168. package/web-dist/favicon.svg +78 -0
  169. package/web-dist/icon-192.png +0 -0
  170. package/web-dist/icon-512.png +0 -0
  171. package/web-dist/icon-maskable-512.png +0 -0
  172. package/web-dist/index.html +33 -0
  173. package/web-dist/manifest.webmanifest +16 -0
  174. package/index.d.ts +0 -1
  175. package/index.js +0 -2
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ // SCRATCHPAD REINFORCEMENT hook (frizz-worker) — keeps the ONE per-thread scratchpad
4
+ // (`.frizz/threads/<sid>/scratch.md`) written and re-grounded across compaction. Run directly with
5
+ // node (zero deps, max Node compat), mirroring the other hooks in this plugin.
6
+ //
7
+ // WHY THIS EXISTS: compaction is the largest source of context loss in a long session, and the
8
+ // scratchpad is frizz's answer to it — but the scratchpad only works if two things happen, and
9
+ // nothing was enforcing either. It has to be WRITTEN while the work is happening, and it has to be
10
+ // RE-READ after the context is gone. A worker that forgets the first has nothing to recover; one
11
+ // that forgets the second has a recovery it never opens.
12
+ //
13
+ // This hook closes both, and deliberately targets scratch.md rather than a second file of its own.
14
+ // An earlier revision shipped a separate `carryover.md` brief; it was redundant with the scratchpad
15
+ // and made the worker maintain two overlapping documents. ONE doc per thread is the rule — the
16
+ // scratchpad the dispatcher already provisions, already names in the system prompt, and shares with
17
+ // sub-agents under the merge-only contract (see agent-dispatch.mjs).
18
+ //
19
+ // CODEX CHILD EPILOGUE — native Codex sub-agents inherit the root conversation's system/user
20
+ // scratchpad instructions even with `fork_turns:"none"`. Sharing the thread pad is useful: a child
21
+ // can persist progress even if the root later compacts. But an undifferentiated "keep it current"
22
+ // mandate once made a child replace the whole document with its task notes, then DELETE that
23
+ // replacement as a misguided rollback after realizing it had clobbered the root's state. The
24
+ // `subagent-start` mode preserves collaborative writes while requiring merge-only, scoped edits. It
25
+ // also carries the codex half of the default-off nesting rule (2026-08-04): a native child does the
26
+ // work itself and does not `spawn_agent` a layer of its own unless its task said to. SubagentStart is
27
+ // the only structural seam that reaches a native child, the way agent-dispatch.mjs's epilogue is for
28
+ // Claude — the same rule, stated once per backend.
29
+ //
30
+ // THE RE-READ SIDE — injection, not a reminder. On compaction/resume the head of scratch.md is
31
+ // spliced into the context window by the harness, before the model's first token, alongside a
32
+ // pointer to read the rest. A bare "remember to read your scratchpad" routes recovery through a
33
+ // decision the model can skip, which is exactly the failure being fixed; injecting the head
34
+ // guarantees a floor of orientation even if the pointer is ignored. The cap keeps the guarantee
35
+ // affordable — a scratchpad is unbounded working memory and the whole file is not owed to every
36
+ // session start.
37
+ //
38
+ // THE WRITE SIDE — a staleness nudge on two channels:
39
+ // UserPromptSubmit — the turn boundary.
40
+ // PostToolUse — MID-TURN, and this is the one that matters. A frizz worker runs enormous
41
+ // autonomous turns (dozens of tool calls between human prompts), so a
42
+ // turn-boundary-only nudge can miss an entire session's worth of work and let
43
+ // it compact unpersisted. PostToolUse additionalContext was verified live
44
+ // against cli 2.1.220: a real session quoted a sentinel injected after a Bash
45
+ // call. Both channels share one state file, so the interval is global — moving
46
+ // to per-tool-call firing does NOT multiply the number of nudges.
47
+ //
48
+ // NO HOOK FIRES ON CONTEXT PRESSURE — measured, not assumed. Claude Code 2.1.220 exposes 31 hook
49
+ // events and not one of them signals an approaching context limit; no hook input carries a token
50
+ // count at all (the docs say plainly: poll the transcript yourself). So this computes the fill
51
+ // itself from the transcript's newest usage record — `input + cache_creation + cache_read` is the
52
+ // live context size — reading only the file's TAIL, since transcripts reach tens of megabytes.
53
+ //
54
+ // STALENESS IS GROWTH SINCE THE LAST WRITE, never an absolute threshold: the window size is not
55
+ // knowable from a hook (a real compaction in this project fired at preTokens 935,291 on a 1M-window
56
+ // session, while a 200k session compacts near 160k). Growth is window-independent and self-resetting.
57
+ //
58
+ // NOT A BLOCKING GATE. A Stop hook could refuse to let the worker rest until it writes, and that was
59
+ // tried and REMOVED on 2026-07-02 (maintainer's call): the block-until-file-edited nag forced even
60
+ // trivial workers into Read/Edit dances that render as noise in the chat UI. This nudges; it never
61
+ // blocks.
62
+ import { readFileSync, writeFileSync, mkdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
63
+ import { join } from 'node:path';
64
+ import { currentSessionId } from '../scripts/frizz/config.mjs';
65
+
66
+ const SCRATCH_FILE = 'scratch.md';
67
+
68
+ /** Hard cap on injected characters. The scratchpad is unbounded working memory, so this bounds what
69
+ * a session start is charged; past the cap we inject the HEAD (a scratchpad's orientation lives at
70
+ * the top) and say plainly that it was clipped, pointing at the file for the rest. */
71
+ const MAX_INJECT_CHARS = intFromEnv('FRIZZ_SCRATCHPAD_MAX_CHARS', 12000);
72
+
73
+ /** Context-token growth since the last scratchpad write that marks it stale. 60k is ~a third of a
74
+ * 200k window and ~6% of a 1M one: frequent enough that the pad is never many turns behind, rare
75
+ * enough not to be chatter. Also the first-write trigger — an untouched template counts as
76
+ * unwritten, so the baseline is zero and the first nudge lands once a session has accumulated 60k
77
+ * tokens actually worth persisting. */
78
+ const STALE_TOKENS = intFromEnv('FRIZZ_SCRATCHPAD_STALE_TOKENS', 60000);
79
+
80
+ /** @param {string} name @param {number} fallback */
81
+ function intFromEnv(name, fallback) {
82
+ const n = parseInt(String(process.env[name] ?? ''), 10);
83
+ return Number.isFinite(n) && n > 0 ? n : fallback;
84
+ }
85
+
86
+ const argv = process.argv.slice(2);
87
+ /** @param {string} flag */
88
+ const flagValue = (flag) => {
89
+ const hit = argv.find((a) => a.startsWith(flag + '='));
90
+ return hit ? hit.slice(flag.length + 1) : null;
91
+ };
92
+ const mode = flagValue('--mode') ?? 'session-start';
93
+ const via = flagValue('--via') ?? 'plugin';
94
+
95
+ // ALWAYS ON — deliberately not settings-gated. The scratchpad is the CANONICAL document for a thread,
96
+ // so re-grounding on it after a compaction is not an opinion a project opts into; it is what makes the
97
+ // pad worth writing at all. An earlier revision put this behind an opt-in setting that defaulted OFF,
98
+ // which meant the default worker got nothing back after a compaction — the exact failure the pad
99
+ // exists to prevent (maintainer's correction: the thing that should be opt-in is the FORK-based
100
+ // auto-updating, not the re-grounding).
101
+ //
102
+ // The escape hatch is an env var, not a setting, because it is for a one-off ("this session is doing
103
+ // something where the injection is in the way"), not a project posture. Anything affirmative-looking
104
+ // is ignored: only an explicit off value disables.
105
+ if (/^(off|0|false|no|disabled)$/i.test((process.env.FRIZZ_SCRATCHPAD_HOOK ?? '').trim())) process.exit(0);
106
+
107
+ // The repo-local registration defers to the plugin one for frizz workers (see --via, and the
108
+ // registration note in DECISIONS.md) so a frizz worker never injects twice.
109
+ if (via === 'project' && (process.env.FRIZZ_THREAD ?? '').trim()) process.exit(0);
110
+
111
+ /** @type {{ agent_id?: unknown, agentId?: unknown, source?: string, trigger?: string, session_id?: string, transcript_path?: string }} */
112
+ let input = {};
113
+ try {
114
+ input = JSON.parse(readFileSync(0, 'utf8'));
115
+ } catch {
116
+ /* no stdin / not JSON → fall back to env for the session id */
117
+ }
118
+ const childId = input.agent_id ?? input.agentId;
119
+ // Sub-agent contexts are silent on every reinforcement mode. The child-only epilogue is the one
120
+ // exception: it constrains the undifferentiated scratchpad instruction the child otherwise inherits.
121
+ if (childId && mode !== 'subagent-start') process.exit(0);
122
+
123
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
124
+
125
+ // WHICH session keys the pad. On Claude the hook's `session_id` IS frizz's thread session id, so the
126
+ // derived path is correct. On CODEX it is NOT: codex reports its own rollout session id (measured —
127
+ // e.g. `019fb427-93aa-…`, with transcript_path pointing into ~/.codex/sessions), which has nothing to
128
+ // do with `.frizz/threads/<frizz sessionId>/scratch.md`. Deriving the path there would silently address
129
+ // a pad that does not exist and the worker would look unreinforced for a reason nobody could see. So
130
+ // frizz bakes `--session=<frizz sessionId>` into the codex hook command, and an explicit value always
131
+ // wins over the reported one.
132
+ const explicitSession = flagValue('--session');
133
+ let sid = null;
134
+ try {
135
+ sid = explicitSession || currentSessionId(input.session_id);
136
+ } catch {
137
+ /* best-effort */
138
+ }
139
+ // Without a session id there is no key, and an unkeyed pad would bleed between sessions.
140
+ if (!sid) process.exit(0);
141
+
142
+ const threadDir = join(projectDir, '.frizz', 'threads', sid);
143
+ const relPath = '.frizz/threads/' + sid + '/' + SCRATCH_FILE;
144
+ const absPath = join(threadDir, SCRATCH_FILE);
145
+
146
+ // ── mode: subagent-start (Codex child-only merge epilogue) ───────────────────────────────────────
147
+ if (mode === 'subagent-start') {
148
+ process.stdout.write(JSON.stringify({
149
+ hookSpecificOutput: {
150
+ hookEventName: 'SubagentStart',
151
+ additionalContext:
152
+ '⟦shared scratchpad — merge only⟧ `' + relPath + '` is the canonical document shared with ' +
153
+ 'the top-level `/root` worker. Update your own task progress in it as you work rather than ' +
154
+ 'leaving the root as its sole writer. Before every edit, re-read the current file, then ' +
155
+ 'patch only your scoped task/progress entry while ' +
156
+ 'preserving every other agent’s content. This exact file is Frizz coordination state, not a ' +
157
+ 'project deliverable or source edit: a scoped merge is an explicit exception to delegated ' +
158
+ 'phrases such as “write only <path>” or “do not modify the repo”; never classify that merge ' +
159
+ 'as unauthorized or roll it back. Never delete, truncate, reinitialize, move, or replace the ' +
160
+ 'whole file — not even to “clean up” or undo your own mistaken change. Other project files, ' +
161
+ 'including files at the repository root, remain governed by your delegated authority; their ' +
162
+ 'location alone neither permits nor forbids editing. If the file is absent or you cannot ' +
163
+ 'merge safely, return your state to the parent without writing.\n' +
164
+ '⟦no fan-out of your own⟧ Do the work yourself: do not spawn agents of your own ' +
165
+ '(`spawn_agent`) unless the task you were given explicitly tells you to. You are already one ' +
166
+ 'prong of the root worker’s fan-out, and another layer below you splits the context you were ' +
167
+ 'handed and buries the real work further from whoever reads the tree. A slice that feels ' +
168
+ 'large is still yours to work through.',
169
+ },
170
+ }));
171
+ process.exit(0);
172
+ }
173
+
174
+ // Ensure the directory exists so a first Write lands. frizz's dispatcher already provisions this for
175
+ // a real thread; this only covers a session that started outside a dispatch.
176
+ try {
177
+ mkdirSync(threadDir, { recursive: true });
178
+ } catch {
179
+ /* a read-only or racing FS just means the agent's Write creates it instead */
180
+ }
181
+
182
+ /** Raw scratchpad text, or null when absent/empty/unreadable. */
183
+ function readPad() {
184
+ try {
185
+ const raw = readFileSync(absPath, 'utf8');
186
+ return raw.trim() ? raw : null;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+
192
+ /** Characters of SUBSTANTIVE content — the pad minus its provisioned skeleton.
193
+ * frizz writes scratch.md up front with an H1, a one-line orientation, section headings and an empty
194
+ * task box, so unlike a file that simply does not exist, "present" no longer means "written". This
195
+ * strips exactly those skeleton shapes and measures what is left. A heuristic on purpose: it only
196
+ * decides whether to NUDGE, so a wrong call costs one redundant reminder, never correctness.
197
+ * @param {string|null} text */
198
+ function substanceLength(text) {
199
+ if (!text) return 0;
200
+ return text
201
+ .split('\n')
202
+ .filter((line) => {
203
+ const t = line.trim();
204
+ if (!t) return false;
205
+ if (t.startsWith('#')) return false; // headings
206
+ if (/^[-*]\s*\[\s*\]\s*$/.test(t)) return false; // an empty task box
207
+ // The visible legend/collaboration guide provisioned in every new pad. They teach the shared
208
+ // editing contract but are not evidence that the worker has recorded any task state yet.
209
+ if (/^>\s*(?:Status legend:|Collaboration:)/.test(t)) return false;
210
+ // The provisioned orientation line. Matched on the CONCEPT rather than a leading word, because
211
+ // the wording has changed once already and pads written under the old shape are still on disk —
212
+ // anchoring on a prefix silently reclassified a template as "written", which made an empty pad
213
+ // skip its re-grounding and made the summarizer swallow a skeleton.
214
+ if (/compaction-survival mechanism|compaction-proof working memory/.test(t)) return false;
215
+ return true;
216
+ })
217
+ .join('')
218
+ .trim().length;
219
+ }
220
+
221
+ /** @param {string} text */
222
+ function capped(text) {
223
+ if (text.length <= MAX_INJECT_CHARS) return text;
224
+ return (
225
+ text.slice(0, MAX_INJECT_CHARS) +
226
+ '\n\n[…clipped at ' + MAX_INJECT_CHARS.toLocaleString('en-US') + ' characters — read `' + relPath +
227
+ '` for the rest.]'
228
+ );
229
+ }
230
+
231
+ /** @param {string} additionalContext @param {'SessionStart'|'UserPromptSubmit'|'PostToolUse'} hookEventName */
232
+ function emitJson(additionalContext, hookEventName) {
233
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }));
234
+ process.exit(0);
235
+ }
236
+
237
+ // ── mode: session-start ──────────────────────────────────────────────────────────────────────────
238
+ if (mode === 'session-start') {
239
+ const pad = readPad();
240
+ const written = substanceLength(pad) > 0;
241
+ const parts = [];
242
+
243
+ // The sources where the deep model of the work is actually GONE. On these the worker is ALWAYS
244
+ // re-grounded on its scratchpad — whether or not there is anything in it yet, because "your pad is
245
+ // empty and you just lost your context" is itself the most urgent thing the next turn can be told.
246
+ const lostContext = input.source === 'compact' || input.source === 'resume' || input.source === 'clear'
247
+
248
+ // Claude Code opens every compaction summary with the fixed preamble "This session is being
249
+ // continued from a previous conversation that ran out of context." That sentence is about the
250
+ // conversation just SUMMARIZED, but it lands at the moment the window is emptiest, and workers read
251
+ // it as a report on their own state and start winding down. Measured: nub session 5258ebe4 took the
252
+ // auto-compaction at line 20239 and then declared "I'm out of context" / "I'm at the end of this
253
+ // context window" on 13 consecutive turns at fills of 176k-244k, before self-diagnosing at line
254
+ // 20628 — "I've been treating 'low context' as a stopping condition ... and winding down instead of
255
+ // working." This hook is the only frizz text that lands in that exact window, so it is where the
256
+ // preamble gets answered. Kept to two sentences: the re-grounding instruction is the payload.
257
+ const compactedNote =
258
+ input.source === 'compact'
259
+ ? ' The summary opens "a previous conversation that ran out of context" — that describes the ' +
260
+ 'conversation just summarized, not your situation now: this window is close to EMPTY again, ' +
261
+ 'and the harness will compact and continue as many times as the effort needs. Context is not ' +
262
+ 'a reason to wind down, hand off, or leave the next step to a fresh session.'
263
+ : ''
264
+
265
+ if (lostContext && written) {
266
+ // Inject the head AND point at the file: the injection is the floor (it cannot be skipped), the
267
+ // pointer is the ceiling (the pad may be longer than the cap, and it is the canonical doc).
268
+ const lead =
269
+ input.source === 'compact'
270
+ ? '⟦scratchpad — reground here⟧ Context was just compacted. Your scratchpad `' + relPath +
271
+ '` is the CANONICAL record of this thread and the head of it follows. RE-GROUND ON IT BEFORE ' +
272
+ 'DOING ANYTHING ELSE: re-read the full file, treat it as authoritative over anything the ' +
273
+ 'summary implies, and re-read only the code you are about to describe or change.' + compactedNote
274
+ : '⟦scratchpad — reground here⟧ This session resumed and lost its working context. Your ' +
275
+ 'scratchpad `' + relPath + '` is the CANONICAL record of this thread and the head of it ' +
276
+ 'follows. Re-read the full file before acting.';
277
+ parts.push(lead + '\n\n' + capped(/** @type {string} */ (pad)) + '\n\n⟦end scratchpad⟧');
278
+ } else if (lostContext) {
279
+ // Context is gone and there is nothing to restore. Say plainly that the exact pad is empty and
280
+ // constrain reconstruction to the compact summary + named handoffs. Searching neighbouring
281
+ // thread pads is both expensive and unsafe: they belong to unrelated workers.
282
+ parts.push(
283
+ '⟦scratchpad — reground here⟧ Context was just compacted or resumed. Your scratchpad `' +
284
+ relPath + '` is the CANONICAL record of this thread, but it is absent or has nothing ' +
285
+ 'substantive in it. That exact path is authoritative: do not search other ' +
286
+ '`.frizz/threads/*/scratch.md` files for a substitute, and do not broadly reload repo docs or ' +
287
+ 'skills merely to reconstruct context. Recover from the retained compaction summary and any ' +
288
+ 'task-specific handoff it directly names, then WRITE this exact pad: the problem, the approach ' +
289
+ 'and the approaches you rejected, the decisions the human made, what is verified versus merely ' +
290
+ 'believed, and the next action.' + compactedNote,
291
+ );
292
+ } else {
293
+ // A fresh start has lost nothing — teach the contract so the pad gets written in the first place.
294
+ parts.push(
295
+ '⟦scratchpad⟧ `' + relPath + '` is the CANONICAL document for this thread and your ONE durable ' +
296
+ 'working doc: its head is injected back into your context automatically whenever the context ' +
297
+ 'is lost, so it is the only thing guaranteed to survive a compaction. Keep it current as you ' +
298
+ 'work — the problem, the approach and the approaches you REJECTED and why, decisions the human ' +
299
+ 'made or reversed, what is VERIFIED by running it versus merely believed, and the next action.',
300
+ );
301
+ }
302
+ emitJson(parts.join('\n\n'), 'SessionStart');
303
+ }
304
+
305
+ // ── mode: precompact ─────────────────────────────────────────────────────────────────────────────
306
+ // PLAIN STDOUT — joined with the other PreCompact hooks' stdout into the summarizer's
307
+ // `Additional Instructions:`. Worded as an ordinary editorial note: precompact-instructions.mjs
308
+ // records that a summarizer REFUSES instructions that read like prompt-hijacking.
309
+ if (mode === 'precompact') {
310
+ const pad = readPad();
311
+ if (substanceLength(pad) === 0) process.exit(0);
312
+ process.stdout.write(
313
+ 'The worker keeps a running scratchpad of this effort at `' + relPath + '`, written by hand as ' +
314
+ 'the work progressed. Its current head is reproduced below. Treat it as the authoritative ' +
315
+ 'account of the problem, the chosen approach, and the decisions behind them, and make sure the ' +
316
+ 'summary preserves its substance — where it disagrees with your reading of the transcript, ' +
317
+ 'prefer it.\n\n' +
318
+ capped(/** @type {string} */ (pad)) + '\n',
319
+ );
320
+ process.exit(0);
321
+ }
322
+
323
+ // ── nudge modes (UserPromptSubmit + PostToolUse) ─────────────────────────────────────────────────
324
+ // Everything below answers one question: has the context moved on since the pad was last written?
325
+
326
+ /** Live context fill in tokens from the transcript's newest usage record, or null.
327
+ * Reads only the TAIL — transcripts reach tens of megabytes, and on PostToolUse this runs after
328
+ * every single tool call. Scanning backwards means the one line the tail read may have cut in half
329
+ * is reached last, and its parse failure is simply skipped.
330
+ * @param {string} path */
331
+ function contextTokens(path) {
332
+ let fd = null;
333
+ try {
334
+ const size = statSync(path).size;
335
+ const want = Math.min(size, 128 * 1024);
336
+ const buf = Buffer.alloc(want);
337
+ fd = openSync(path, 'r');
338
+ readSync(fd, buf, 0, want, size - want);
339
+ const lines = buf.toString('utf8').split('\n');
340
+ for (let i = lines.length - 1; i >= 0; i--) {
341
+ const line = lines[i].trim();
342
+ if (!line) continue;
343
+ let rec;
344
+ try {
345
+ rec = JSON.parse(line);
346
+ } catch {
347
+ continue;
348
+ }
349
+ const u = rec?.message?.usage;
350
+ if (!u) continue;
351
+ const n =
352
+ (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0);
353
+ if (Number.isFinite(n) && n > 0) return n;
354
+ }
355
+ return null;
356
+ } catch {
357
+ return null;
358
+ } finally {
359
+ if (fd !== null) {
360
+ try {
361
+ closeSync(fd);
362
+ } catch {
363
+ /* ignore */
364
+ }
365
+ }
366
+ }
367
+ }
368
+
369
+ const statePath = join(threadDir, '.scratchpad-state.json');
370
+ /** @returns {{ mtimeMs?: number, tokensAtWrite?: number, tokensAtNudge?: number }} */
371
+ function readState() {
372
+ try {
373
+ const s = JSON.parse(readFileSync(statePath, 'utf8'));
374
+ return s && typeof s === 'object' ? s : {};
375
+ } catch {
376
+ return {};
377
+ }
378
+ }
379
+ /** @param {{ mtimeMs?: number, tokensAtWrite?: number, tokensAtNudge?: number }} s */
380
+ function writeState(s) {
381
+ try {
382
+ writeFileSync(statePath, JSON.stringify(s) + '\n');
383
+ } catch {
384
+ /* best-effort — a lost state file costs at most one extra nudge */
385
+ }
386
+ }
387
+
388
+ if (mode === 'nudge') {
389
+ const transcript = input.transcript_path;
390
+ const tokens = transcript ? contextTokens(transcript) : null;
391
+ // No readable usage yet, or an unparseable transcript → say nothing. The nudge is an optimization;
392
+ // silence is always safe.
393
+ if (!tokens) process.exit(0);
394
+
395
+ const pad = readPad();
396
+ const written = substanceLength(pad) > 0;
397
+
398
+ let mtimeMs = 0;
399
+ try {
400
+ mtimeMs = written ? statSync(absPath).mtimeMs : 0;
401
+ } catch {
402
+ mtimeMs = 0;
403
+ }
404
+
405
+ let state = readState();
406
+ // A changed mtime means the pad was just written — rebase the baseline to NOW and go quiet. This is
407
+ // also the first-ever observation, and it is why a fresh write buys a full interval of silence. It
408
+ // fires for a human's hand-edit exactly as for the agent's Write: both are just an mtime change.
409
+ if (state.mtimeMs !== mtimeMs) {
410
+ state = { mtimeMs, tokensAtWrite: tokens, tokensAtNudge: 0 };
411
+ writeState(state);
412
+ }
413
+
414
+ // An unwritten pad (absent, or still the provisioned skeleton) measures growth from ZERO: the whole
415
+ // session is unpersisted, so the clock starts at the beginning, not at whenever this first looked.
416
+ const baseline = mtimeMs ? (state.tokensAtWrite ?? tokens) : 0;
417
+ const grown = tokens - baseline;
418
+ if (grown < STALE_TOKENS) process.exit(0);
419
+ // Space repeat nudges by the same interval. Both channels share this state, so firing on every tool
420
+ // call does not multiply reminders — it only makes the existing budget land sooner and mid-turn.
421
+ if (state.tokensAtNudge && tokens - state.tokensAtNudge < STALE_TOKENS) process.exit(0);
422
+
423
+ writeState({ ...state, tokensAtNudge: tokens });
424
+
425
+ const k = Math.round(grown / 1000);
426
+ const event = /** @type {'UserPromptSubmit'|'PostToolUse'} */ (
427
+ input.transcript_path && flagValue('--event') === 'PostToolUse' ? 'PostToolUse' : 'UserPromptSubmit'
428
+ );
429
+ emitJson(
430
+ mtimeMs
431
+ ? '⟦scratchpad stale⟧ Your context has grown ~' + k + 'k tokens since you last wrote `' +
432
+ relPath + '`. If this effort is long enough that losing your reasoning would hurt, top it up ' +
433
+ 'in passing — the approach, what you rejected, the human\'s decisions, what is verified ' +
434
+ 'versus believed. Its head is injected back after a compaction. This is a background note, ' +
435
+ 'NOT a task and NOT a reason to pause: do not stop working to service it, and never end a ' +
436
+ 'turn on it while the human\'s instruction still has parts left.'
437
+ : '⟦scratchpad empty⟧ This session is ~' + k + 'k tokens deep and `' + relPath + '` is empty. ' +
438
+ 'That is fine for a single direct task — the pad is optional and writing in it is not doing ' +
439
+ 'the work. If this effort is long or branching, a few lines on the approach and the human\'s ' +
440
+ 'decisions will survive a compaction. This is a background note, NOT a task and NOT a reason ' +
441
+ 'to pause: keep going with what you were asked to do.',
442
+ event,
443
+ );
444
+ }
445
+
446
+ process.exit(0);
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ // SessionStart hook (frizz-worker) — SEEDS a frizz WORKER session's context. Run directly with
4
+ // node (zero deps, max Node compat), mirroring cc's hook idiom.
5
+ //
6
+ // A frizz worker is a top-level interactive `claude` the UI spawns per effort; the slug arrives in
7
+ // env FRIZZ_THREAD (and a `THREAD:` line in the first prompt). There are NO thread files, no
8
+ // frontmatter, no status field — a worker SIGNALS through its final message (fences) and PERSISTS
9
+ // through a scratchpad. This hook injects, on every session start (startup/resume/clear/compact):
10
+ // 1. `core` — a runtime re-grounding + pointer, NOT a second copy of the contract: the full worker
11
+ // contract lives ONCE in the system prompt (workerPrompt.ts) the server injects at spawn. This
12
+ // carries only what a static system prompt can't: the runtime scratchpad PATH + an essential
13
+ // signal-at-rest anchor + a pointer to the system-prompt contract.
14
+ // 2. the SCRATCHPAD PATH — `.frizz/threads/<session_id>/scratch.md`, the worker's compaction-proof memory.
15
+ // 3. on `compact` — a short re-grounding (compaction drops the deep model + the scratchpad reminder).
16
+ //
17
+ // GATE: everything is gated on FRIZZ_THREAD being set, so the plugin is completely inert when
18
+ // loaded outside a frizz worker (e.g. a plain `claude --plugin-dir cc-worker` smoke run).
19
+ //
20
+ // STALE-INSTALL DEFENSE: the `cc` orchestrator plugin is retired — the marketplace ships only this
21
+ // worker plugin, and cc's hooks/skills/agents are gone. But a machine can still carry a CACHED cc
22
+ // install from before the retirement, whose hooks fire in every repo gated on cc's opt-IN sentinel.
23
+ // A fresh worker never runs `frizz on`, so such a cc is already dormant; we write cc's own per-session
24
+ // `off` sentinel anyway via the shared config API (board survives as cc-worker's board
25
+ // implementation) so a stale install is guaranteed inert. Cheap belt-and-suspenders. See DECISIONS.md.
26
+ import { readFileSync } from 'node:fs';
27
+ import { execFileSync } from 'node:child_process';
28
+ import { setSessionOverride, currentSessionId } from '../scripts/frizz/config.mjs';
29
+
30
+ /** @type {{ agent_id?: unknown, agentId?: unknown, source?: string, session_id?: string }} */
31
+ let input = {};
32
+ try {
33
+ input = JSON.parse(readFileSync(0, 'utf8'));
34
+ } catch {
35
+ /* no stdin / not JSON → input stays {} → proceed (fail-open to inject) */
36
+ }
37
+ // Skip inside sub-agent contexts (they carry agent_id) — the seed is for the top-level worker.
38
+ if (input.agent_id ?? input.agentId) process.exit(0);
39
+
40
+ // WORKER GATE — inert unless this is a frizz worker session.
41
+ const thread = (process.env.FRIZZ_THREAD ?? '').trim();
42
+ if (!thread) process.exit(0);
43
+
44
+ const dir = process.env.CLAUDE_PROJECT_DIR ?? '.';
45
+
46
+ // Neutralize the orchestrator cc plugin for THIS session (defensive; see header + DECISIONS.md).
47
+ // The session id also names the worker's scratchpad (`.frizz/threads/<session_id>/scratch.md`).
48
+ let sid = null;
49
+ try {
50
+ sid = currentSessionId(input.session_id);
51
+ if (sid) setSessionOverride(dir, sid, 'off');
52
+ } catch {
53
+ /* best-effort — a failed sentinel write just leaves cc at its dormant default */
54
+ }
55
+ const scratch = sid
56
+ ? '.frizz/threads/' + sid + '/scratch.md'
57
+ : '.frizz/threads/<session-id>/scratch.md';
58
+
59
+ // A RUNTIME re-grounding + pointer, NOT a second copy of the contract. The full worker contract
60
+ // (signal fences, scratchpad rules, sub-agent rules, the question handback) lives
61
+ // ONCE in the system prompt frizz injects at spawn (workerPrompt.ts / loadWorkerPrompt) — which is
62
+ // re-applied on every resume and survives compaction. This hook adds only what a static system prompt
63
+ // CANNOT carry: the runtime-derived scratchpad PATH, an essential signal-at-rest anchor, and (below)
64
+ // the compaction re-read nudge, gh guidance, and the defensive cc-orchestrator off-sentinel.
65
+ const core =
66
+ '⟦frizz worker contract⟧ You are a frizz WORKER driving EXACTLY ONE effort. Your FULL operating contract — the end-of-turn signal fences, scratchpad rules, sub-agent rules, and the question handback — lives in your SYSTEM PROMPT; follow it there (this is a runtime re-grounding, not a second copy). The human + the frizz app are the ORCHESTRATOR; you drive ONE effort and never scan the board or touch other efforts. There is no orchestrator mode and no fleet to run: doing the work yourself is the default, and you dispatch a sub-agent only when the work genuinely decomposes into independent prongs.\n' +
67
+ 'SCRATCHPAD (OPTIONAL): `' + scratch + '` — a scratch file kept FOR YOU, not a deliverable, and never a substitute for doing the work. A single direct task usually needs nothing in it. On a long effort it is crash insurance and your sub-agents\' shared blackboard: write the approach and what you rejected there AS YOU GO, mid-work, then KEEP WORKING; re-read it after any compaction or resume, and pass its PATH into every sub-agent prompt.\n' +
68
+ 'DO NOT REST WHILE THE INSTRUCTION HAS PARTS LEFT — finish them in THIS turn; a milestone, a green test run and a long turn are none of them stopping points, and announcing the next step or recording it in the scratchpad is not doing it.\n' +
69
+ 'SIGNAL AT REST through your FINAL MESSAGE, per the fence rules in your system prompt: bare rest is the ordinary handoff and queues for the human; ```done only when the effort\'s real work is COMPLETE (code LANDED on the mainline — an open PR is NOT done, park it on ```awaiting until it MERGES) and is a DISMISSAL (its card files the thread away where nobody looks again), so if the thread points at future work AT ALL — a pre-fix investigation, a live code-change discussion — bare rest instead, and uncertain is not done; the ONE exception is a planning session whose plan file is fully written and persisted, because that artifact outlives the thread; ```awaiting parks only a human:/timer:/pr-watch: gate, never CI/releases/merge progression (those stay ACTIVE); ```question is the operator ask. Load `frizz:handoff` for the full fence reference.\n' +
70
+ 'DECIDE rather than ask: anything derivable from the code, the conventions, or ordinary engineering judgment is YOURS to settle — asking permission to do the work you were dispatched to do is not a question, it is the job. Reserve the operator for the irreversible and the genuinely human-owned.';
71
+
72
+ const grounding =
73
+ '⟦frizz worker re-grounding (post-compaction)⟧ Context was just compacted. You are still the frizz worker for effort `' + thread + '` — re-read your scratchpad `' + scratch + '` NOW to recover your working state and to-do list before asserting anything, and re-read any code before claiming how it is structured. Signal at rest through your FINAL MESSAGE: bare rest queues an ordinary handoff; ```done queues a checked completion until Archive and is a DISMISSAL — completed work only, and never when the thread still points at future work (a pre-fix investigation, a live code-change discussion); use a question or bare rest; ```awaiting parks only a human:/timer: gate (or a pr-watch: PR watcher, which wakes on any new review/comment, bot or human); ```question is the explicit higher-priority operator ask. CI/releases/merge progression stay active through Monitor/background Bash.';
74
+
75
+ // AUTH-GATED gh guidance — teach the worker to use `gh` well, but ONLY when signed in.
76
+ // Shell `gh auth status --active`: exit 0 = an active gh account is authenticated. The whole gate is
77
+ // wrapped so it can NEVER throw into SessionStart, and it fails CLOSED — no gh binary, not authed, a
78
+ // stall past the timeout, or any other error → we inject NOTHING (guidance is absent, not stale/wrong).
79
+ // It re-evaluates on every start/resume/clear/compact, so a later `gh auth login` starts injecting on
80
+ // the next turn boundary (and a `gh auth logout` stops it). See DECISIONS.md / plan §8.
81
+ const ghBlock =
82
+ '⟦gh available⟧ You are signed into the `gh` CLI and in a GitHub repo. Use `gh` EAGERLY and well — it is the fastest path to issue/PR/CI/release context, and you should reach for it before guessing:\n' +
83
+ '• READ freely: `gh issue view N -R OWNER/REPO --comments`, `gh pr view N`, `gh pr diff N`, `gh pr checks N`, `gh run list`/`gh run view`, `gh api repos/OWNER/REPO/…`. Prefer `--json <fields>` over scraping human text.\n' +
84
+ '• SEARCH across the repo (and GitHub) with `gh search issues`/`gh search prs` when hunting related work, duplicates, or prior art.\n' +
85
+ '• READ-ONLY BOUNDARY: never comment, label, assign, close, review, approve, or merge — no mutation of any kind — UNLESS the human explicitly asks in this session. Default to producing your findings/review as your final message, not as a GitHub post.\n' +
86
+ '• TOON: pipe LARGE, FLAT `gh … --json` output through `toon` when `command -v toon` finds it. Skip it when unavailable, for tiny payloads, or for deeply-nested output — the savings are noise and nesting defeats tabularization.\n' +
87
+ 'Load the `frizz:gh` skill for the full playbook (recipes + explicit project-local monitor selection + native Monitor/background-Bash CI/PR watches).';
88
+
89
+ let ghAuthed = false;
90
+ try {
91
+ execFileSync('gh', ['auth', 'status', '--active'], { stdio: 'ignore', timeout: 4000 });
92
+ ghAuthed = true;
93
+ } catch {
94
+ /* no gh / not authed / stalled → fail CLOSED: leave ghAuthed false, inject nothing */
95
+ }
96
+
97
+ const parts = [core];
98
+ if (input.source === 'compact') parts.push(grounding);
99
+ if (ghAuthed) parts.push(ghBlock);
100
+
101
+ process.stdout.write(
102
+ JSON.stringify({
103
+ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: parts.join('\n\n') },
104
+ }),
105
+ );
106
+ process.exit(0);
@@ -0,0 +1,9 @@
1
+ // @ts-check
2
+ /**
3
+ * THIN SHIM — re-exports `board/agent-bindings.mjs` so the worker's PostToolUse
4
+ * `agent-bind` hook writes `.frizz/.agent-bindings.jsonl` records in the EXACT format the board
5
+ * (`bindingsByThread`) + Stop-hook liveness consume. Keeping the writer shared is what lets a
6
+ * worker's own THREAD-tagged sub-agent show up on the frizz board's per-thread liveness.
7
+ * Never fork this — the record shape is a cross-plugin contract.
8
+ */
9
+ export * from '../../../board/agent-bindings.mjs';
@@ -0,0 +1,12 @@
1
+ // @ts-check
2
+ /**
3
+ * THIN SHIM — do NOT fork config logic. cc-worker shares the repo board's single source of truth
4
+ * for the activation gate, config schema, status vocab, and the per-session sentinel/heartbeat
5
+ * helpers. This re-exports `board/config.mjs` verbatim so cc-worker hooks can `import ... from
6
+ * '../scripts/frizz/config.mjs'` at the plugin-local path while the real code lives in ONE place.
7
+ *
8
+ * Coupling note: cc-worker assumes `board/` is a SIBLING dir (`../../board/` from the plugin root) —
9
+ * the same assumption frizz's server makes (see ARCHITECTURE.md: it imports the board logic from
10
+ * `../../board/*.mjs`). If that layout changes, this one path changes with it.
11
+ */
12
+ export * from '../../../board/config.mjs';