frizz-server 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/dist/claude-agent-broker.js +35911 -0
  2. package/dist/codex-app-server-daemon.js +423 -0
  3. package/dist/dev-child.js +56233 -0
  4. package/package.json +34 -0
  5. package/runtime/board/agent-bindings.mjs +287 -0
  6. package/runtime/board/agent-liveness.mjs +367 -0
  7. package/runtime/board/agent-status.mjs +178 -0
  8. package/runtime/board/config.mjs +993 -0
  9. package/runtime/board/decisions.mjs +97 -0
  10. package/runtime/board/index.mjs +704 -0
  11. package/runtime/board/notify-shared.mjs +90 -0
  12. package/runtime/board/notify.mjs +81 -0
  13. package/runtime/board/ownership.mjs +120 -0
  14. package/runtime/board/rest-detect.mjs +213 -0
  15. package/runtime/board/thread-excerpt.mjs +162 -0
  16. package/runtime/board/thread-update.mjs +289 -0
  17. package/runtime/cc-worker/.claude-plugin/plugin.json +10 -0
  18. package/runtime/cc-worker/DECISIONS.md +1172 -0
  19. package/runtime/cc-worker/LICENSE +21 -0
  20. package/runtime/cc-worker/agents/high.md +7 -0
  21. package/runtime/cc-worker/agents/low.md +7 -0
  22. package/runtime/cc-worker/agents/max.md +7 -0
  23. package/runtime/cc-worker/agents/medium.md +7 -0
  24. package/runtime/cc-worker/agents/xhigh.md +7 -0
  25. package/runtime/cc-worker/bin/frizz +17 -0
  26. package/runtime/cc-worker/bin/frizz-mcp.mjs +1612 -0
  27. package/runtime/cc-worker/bin/frizz-update +18 -0
  28. package/runtime/cc-worker/hooks/agent-bind.mjs +40 -0
  29. package/runtime/cc-worker/hooks/agent-dispatch.mjs +121 -0
  30. package/runtime/cc-worker/hooks/bash-background.d.mts +6 -0
  31. package/runtime/cc-worker/hooks/bash-background.mjs +247 -0
  32. package/runtime/cc-worker/hooks/deny-ask.mjs +39 -0
  33. package/runtime/cc-worker/hooks/deny-plan.mjs +62 -0
  34. package/runtime/cc-worker/hooks/hooks.json +102 -0
  35. package/runtime/cc-worker/hooks/perm-policy.mjs +211 -0
  36. package/runtime/cc-worker/hooks/scratchpad.mjs +417 -0
  37. package/runtime/cc-worker/hooks/session-seed.mjs +107 -0
  38. package/runtime/cc-worker/scripts/frizz/agent-bindings.mjs +9 -0
  39. package/runtime/cc-worker/scripts/frizz/config.mjs +12 -0
  40. package/runtime/cc-worker/skills/gh/SKILL.md +141 -0
  41. package/runtime/cc-worker/skills/gh/scripts/ci-watch.mjs +60 -0
  42. package/runtime/cc-worker/skills/gh/scripts/github-watch.mjs +130 -0
  43. package/runtime/cc-worker/skills/gh/scripts/review-watch.mjs +54 -0
  44. package/web-dist/apple-touch-icon.png +0 -0
  45. package/web-dist/assets/TerminalPane-DyLvW_rQ.js +7 -0
  46. package/web-dist/assets/abnfDiagram-VRR7QNED-DIPgkiM8.js +1 -0
  47. package/web-dist/assets/arc-BSyeo0Gb.js +1 -0
  48. package/web-dist/assets/architecture-TIHT7OUA-B8qUD5-C.js +1 -0
  49. package/web-dist/assets/architectureDiagram-ZJ3FMSHR-DBAKToiy.js +36 -0
  50. package/web-dist/assets/array-BifhSqXX.js +1 -0
  51. package/web-dist/assets/blockDiagram-677ZJIJ3-Ba0xt8st.js +132 -0
  52. package/web-dist/assets/c4Diagram-LMCZKHZV-DFham1h_.js +10 -0
  53. package/web-dist/assets/channel-5l10tOPT.js +1 -0
  54. package/web-dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  55. package/web-dist/assets/chunk-32BRIVSS-Bl-817K-.js +1 -0
  56. package/web-dist/assets/chunk-52WLFC77-DBLTDz2W.js +10 -0
  57. package/web-dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  58. package/web-dist/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  59. package/web-dist/assets/chunk-C7G6YPKG-ClL6Ebv8.js +1 -0
  60. package/web-dist/assets/chunk-EX3LRPZG--3vJLCZP.js +231 -0
  61. package/web-dist/assets/chunk-FWX5IMBZ-DMOdhcCP.js +2 -0
  62. package/web-dist/assets/chunk-HOUHSVGY-DkgTGLCa.js +1 -0
  63. package/web-dist/assets/chunk-ICXQ74PX-7X6iir1H.js +2 -0
  64. package/web-dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  65. package/web-dist/assets/chunk-KEIR6QF5-BfrZ3jm6.js +161 -0
  66. package/web-dist/assets/chunk-MOJQB5TN-OpO5flE4.js +88 -0
  67. package/web-dist/assets/chunk-OGEWGWER-BbAMAzTZ.js +1 -0
  68. package/web-dist/assets/chunk-PUDLZKDR-avcvDgZl.js +156 -0
  69. package/web-dist/assets/chunk-Q4XR5HBZ-BaiGN1cd.js +70 -0
  70. package/web-dist/assets/chunk-RYQCIY6F-Cu_KplZW.js +1 -0
  71. package/web-dist/assets/chunk-V7JOEXUC-CKVakdOJ.js +206 -0
  72. package/web-dist/assets/chunk-VAUOI2AC-DzG-rM3_.js +1 -0
  73. package/web-dist/assets/chunk-VR4S4FIN-t3j3HHQF.js +1 -0
  74. package/web-dist/assets/chunk-WYO6CB5R-SnP0NDTw.js +127 -0
  75. package/web-dist/assets/chunk-XXDRQBXY-DYlTP5J-.js +1 -0
  76. package/web-dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  77. package/web-dist/assets/chunk-ZGVPDNZ5-pXn3giwS.js +62 -0
  78. package/web-dist/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
  79. package/web-dist/assets/classDiagram-OUVF2IWQ-vIfzHupB.js +1 -0
  80. package/web-dist/assets/classDiagram-v2-EOCWNBFH-vIfzHupB.js +1 -0
  81. package/web-dist/assets/cose-bilkent-JH36ORCC-BUIsLrGc.js +1 -0
  82. package/web-dist/assets/cynefin-VYW2F7L2-C4qNLMkm.js +1 -0
  83. package/web-dist/assets/cynefinDiagram-TSTJHNR4-2vzWUUfl.js +62 -0
  84. package/web-dist/assets/cytoscape.esm-B3I8pqwA.js +321 -0
  85. package/web-dist/assets/dagre-CXRCoUWR.js +1 -0
  86. package/web-dist/assets/dagre-VKFMJZFB-DUdNHEM9.js +4 -0
  87. package/web-dist/assets/defaultLocale-C8Fc0cco.js +1 -0
  88. package/web-dist/assets/diagram-FQU43EPY-C_EHNL09.js +3 -0
  89. package/web-dist/assets/diagram-G47NLZAW-DOt98NB-.js +24 -0
  90. package/web-dist/assets/diagram-NH7WQ7WH-uIgVP9iZ.js +24 -0
  91. package/web-dist/assets/diagram-OA4YK3LP-CGWe4oxq.js +30 -0
  92. package/web-dist/assets/diagram-WEI45ONY-C-5f7o9T.js +41 -0
  93. package/web-dist/assets/dist-DoH_9pyS.js +1 -0
  94. package/web-dist/assets/ebnfDiagram-CCIWWBDH-DoSFLtL-.js +1 -0
  95. package/web-dist/assets/erDiagram-Q63AITRT-DsCLMzEE.js +85 -0
  96. package/web-dist/assets/eventmodeling-45OFAUF4-D7GQYhiK.js +1 -0
  97. package/web-dist/assets/flowDiagram-23GEKE2U-CR371xZs.js +1 -0
  98. package/web-dist/assets/ganttDiagram-NO4QXBWP-D8UNGcBR.js +292 -0
  99. package/web-dist/assets/gitGraph-TEB2WS4Q-mC-XQzTE.js +1 -0
  100. package/web-dist/assets/gitGraphDiagram-IHSO6WYX-CkpPggS7.js +106 -0
  101. package/web-dist/assets/graphlib-B8gBHxth.js +1 -0
  102. package/web-dist/assets/index-CT6k_A5y.css +1 -0
  103. package/web-dist/assets/index-Dmo0zJc8.js +319 -0
  104. package/web-dist/assets/info-DKCQHKI2-Drg-xVbr.js +1 -0
  105. package/web-dist/assets/infoDiagram-FWYZ7A6U-CsGMTGpl.js +2 -0
  106. package/web-dist/assets/init-D6jRqBbL.js +1 -0
  107. package/web-dist/assets/ishikawaDiagram-FXEZZL3T-DNgGBlL6.js +70 -0
  108. package/web-dist/assets/journeyDiagram-5HDEW3XC-BFN2bObi.js +139 -0
  109. package/web-dist/assets/kanban-definition-HUTT4EX6-BnDPclXf.js +89 -0
  110. package/web-dist/assets/katex-CddkPoXu.js +257 -0
  111. package/web-dist/assets/line-DmLw74JM.js +1 -0
  112. package/web-dist/assets/linear-z2V0wJk9.js +1 -0
  113. package/web-dist/assets/map-DsCK-0Cs.js +1 -0
  114. package/web-dist/assets/mermaid-parser.core-DGJk39E-.js +7 -0
  115. package/web-dist/assets/mermaid.core-8aee8nsf.js +11 -0
  116. package/web-dist/assets/mindmap-definition-LN4V7U3C-CGWK_Qbm.js +96 -0
  117. package/web-dist/assets/ordinal-hYBb2elL.js +1 -0
  118. package/web-dist/assets/packet-7NZHBO7P-C5HYQyS5.js +1 -0
  119. package/web-dist/assets/path-BWPyau1x.js +1 -0
  120. package/web-dist/assets/pegDiagram-2B236MQR-WiQm887Q.js +1 -0
  121. package/web-dist/assets/pie-RZYD4A2V-DKBNMtMn.js +1 -0
  122. package/web-dist/assets/pieDiagram-ENE6RG2P-F1A8_3DO.js +39 -0
  123. package/web-dist/assets/quadrantDiagram-ABIIQ3AL-bf6a3f_f.js +7 -0
  124. package/web-dist/assets/radar-I7S5WNFK-AOKDUn-C.js +1 -0
  125. package/web-dist/assets/railroad-3IZDKUUU-DNHhkFAC.js +1 -0
  126. package/web-dist/assets/railroad-abnf-AHOZXSZD-DOXbu4iv.js +1 -0
  127. package/web-dist/assets/railroad-ebnf-EBAXGLYW-C1oE2RHD.js +1 -0
  128. package/web-dist/assets/railroad-peg-LSFZ7HO6-B4GD-bq-.js +1 -0
  129. package/web-dist/assets/railroadDiagram-RFXS5EU6-Be52T90z.js +1 -0
  130. package/web-dist/assets/requirementDiagram-TGXJPOKE-BqdMvEGK.js +84 -0
  131. package/web-dist/assets/rolldown-runtime-Bh1tDfsg.js +1 -0
  132. package/web-dist/assets/rough.esm-CSKSodPl.js +1 -0
  133. package/web-dist/assets/sankeyDiagram-HTMAVEWB-BP3X6Ofp.js +40 -0
  134. package/web-dist/assets/sequenceDiagram-DBY2YBRQ-yDHhaUzc.js +162 -0
  135. package/web-dist/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  136. package/web-dist/assets/src-C4XfhTaE.js +1 -0
  137. package/web-dist/assets/stateDiagram-2N3HPSRC-xvctsgCU.js +1 -0
  138. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-xFk0N3Cq.js +1 -0
  139. package/web-dist/assets/swimlanes-5IMT3BWC-DZMLgrjk.js +2 -0
  140. package/web-dist/assets/swimlanesDiagram-G3AALYLV-BoWrxkxy.js +8 -0
  141. package/web-dist/assets/timeline-definition-FHXFAJF6-n8sU0qlT.js +120 -0
  142. package/web-dist/assets/treeView-QDETBFTQ-Su8KloaY.js +1 -0
  143. package/web-dist/assets/treemap-6X3UGDF4-CNgRuVWf.js +1 -0
  144. package/web-dist/assets/vennDiagram-L72KCM5P-B5I9YxaY.js +34 -0
  145. package/web-dist/assets/wardley-OPB4EBWU-khMe_Wbq.js +1 -0
  146. package/web-dist/assets/wardleyDiagram-EHGQE667-d8LsqhTO.js +78 -0
  147. package/web-dist/assets/xychartDiagram-FW5EYKEG-b0CH_-wy.js +7 -0
  148. package/web-dist/favicon-16.png +0 -0
  149. package/web-dist/favicon-32.png +0 -0
  150. package/web-dist/favicon.svg +34 -0
  151. package/web-dist/icon-192.png +0 -0
  152. package/web-dist/icon-512.png +0 -0
  153. package/web-dist/icon-maskable-512.png +0 -0
  154. package/web-dist/index.html +44 -0
  155. package/web-dist/manifest.webmanifest +16 -0
@@ -0,0 +1,107 @@
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 anything that
9
+ // must outlive its context window is an arrangement it makes for itself. This hook injects, on every
10
+ // session start (startup/resume/clear/compact):
11
+ // 1. `core` — a runtime POINTER, not a copy of the contract: the full worker contract lives ONCE in
12
+ // the system prompt (workerPrompt.ts) the server injects at spawn. This carries only what a
13
+ // static system prompt can't: the runtime scratch-directory PATH, and a one-line pointer.
14
+ // 2. the SCRATCH DIRECTORY — `.frizz/threads/<session_id>/`, a folder the worker may use as it likes.
15
+ // 3. on `compact` — a short re-grounding (compaction drops the deep model + this orientation).
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 scratch directory (`.frizz/threads/<session_id>/`).
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 + '/'
57
+ : '.frizz/threads/<session-id>/';
58
+
59
+ // A RUNTIME pointer, NOT a copy of the contract. The full worker contract (signal fences,
60
+ // scratch-directory rules, sub-agent rules, the question handback, the stop criterion) lives ONCE in
61
+ // the system prompt frizz injects at spawn (workerPrompt.ts / loadWorkerPrompt) — re-applied on every
62
+ // resume, and it survives compaction. This hook adds only what a static system prompt CANNOT carry:
63
+ // the runtime-derived scratch-directory PATH, plus (below) the compaction re-read nudge and the
64
+ // auth-gated gh guidance.
65
+ //
66
+ // It used to restate the fence protocol, the stop criterion and the autonomy rule in full — ~4.4 KB
67
+ // (~1,100 tokens) on every startup, resume, clear AND compact, all of it already in the 42 KB system
68
+ // prompt. Trimmed 2026-08-26 (maintainer: "Definitely trim the session seed hook if it's fully
69
+ // repetitive") as part of cutting the per-session token overhead Frizz adds over a plain TUI session.
70
+ const core =
71
+ '⟦frizz worker contract⟧ You are a frizz WORKER driving EXACTLY ONE effort. Your FULL operating contract — the end-of-turn signal fences (```done / ```awaiting — a question is a registered row, `mcp__frizz__ask`, and the only ```question fence is the EMPTY placement marker naming its id), the scratch-directory rules, the sub-agent rules, the question handback and the stop criterion — lives in your SYSTEM PROMPT; follow it there. ALWAYS SIGN OFF — a fence OR a registration (`ask`, `watch`, `done`); an open question is one, so rest normally and write no question fence for it.\n' +
72
+ 'SCRATCH DIRECTORY (OPTIONAL): `' + scratch + '` — a folder kept FOR YOU, nothing in it read automatically, never a substitute for doing the work. Give each sub-agent its OWN file rather than a shared one.';
73
+
74
+ const grounding =
75
+ '⟦frizz worker re-grounding (post-compaction)⟧ Context was just compacted. You are still the frizz worker for effort `' + thread + '` — read whatever you left yourself in `' + 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. Your system prompt still carries the full contract; sign off as it says.';
76
+
77
+ // AUTH-GATED gh guidance — teach the worker to use `gh` well, but ONLY when signed in.
78
+ // Shell `gh auth status --active`: exit 0 = an active gh account is authenticated. The whole gate is
79
+ // wrapped so it can NEVER throw into SessionStart, and it fails CLOSED — no gh binary, not authed, a
80
+ // stall past the timeout, or any other error → we inject NOTHING (guidance is absent, not stale/wrong).
81
+ // It re-evaluates on every start/resume/clear/compact, so a later `gh auth login` starts injecting on
82
+ // the next turn boundary (and a `gh auth logout` stops it). See DECISIONS.md / plan §8.
83
+ const ghBlock =
84
+ '⟦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' +
85
+ '• 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' +
86
+ '• SEARCH across the repo (and GitHub) with `gh search issues`/`gh search prs` when hunting related work, duplicates, or prior art.\n' +
87
+ '• 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' +
88
+ 'Load the `frizz:gh` skill for the full playbook (recipes + explicit project-local monitor selection + native Monitor/background-Bash CI/PR watches).';
89
+
90
+ let ghAuthed = false;
91
+ try {
92
+ execFileSync('gh', ['auth', 'status', '--active'], { stdio: 'ignore', timeout: 4000 });
93
+ ghAuthed = true;
94
+ } catch {
95
+ /* no gh / not authed / stalled → fail CLOSED: leave ghAuthed false, inject nothing */
96
+ }
97
+
98
+ const parts = [core];
99
+ if (input.source === 'compact') parts.push(grounding);
100
+ if (ghAuthed) parts.push(ghBlock);
101
+
102
+ process.stdout.write(
103
+ JSON.stringify({
104
+ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: parts.join('\n\n') },
105
+ }),
106
+ );
107
+ 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';
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: gh
3
+ description: The gh-CLI playbook for a frizz worker signed into GitHub (invoke as frizz:gh). Load this whenever your effort touches GitHub — reading or triaging an issue or PR, reviewing a diff, checking CI/release status, or searching issues/PRs — to use `gh` eagerly and correctly: the read-vs-write boundary (never comment/label/close/merge unless the human asks), concrete read recipes, and active Monitor/background-Bash CI/PR watches. Only meaningful when you are signed in (`gh auth status --active` exit 0); the session-seed hook injects a pointer here when you are.
4
+ version: 0.1.2
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # frizz:gh — the gh-CLI playbook
10
+
11
+ You are a **frizz worker** and you are **signed into the `gh` CLI in a GitHub repo** (the session-seed hook confirmed `gh auth status --active` before pointing you here). `gh` is the fastest path to issue / PR / CI / release context — reach for it before guessing, and prefer it over scraping the web UI or reasoning from memory.
12
+
13
+ This skill is the full playbook the injected `⟦gh available⟧` block summarizes: the **read-vs-write boundary**, concrete **read recipes**, and how to keep a **CI/PR watch** active until the next actionable event.
14
+
15
+ ## The one hard rule — READ freely, WRITE only when asked
16
+
17
+ `gh` can mutate the repo, and your token has the scopes to do it. **Do not.** Unless the human **explicitly asks in this session**, you are strictly read-only:
18
+
19
+ - **NEVER** comment, review, approve, request-changes, label, assign, milestone, edit, close, reopen, merge, or push — no state change of any kind on GitHub.
20
+ - Your deliverable is your **final message** (a findings write-up, a review, a recommendation) — NOT a GitHub post. Producing the review in-session is the job; posting it is a separate action the human authorizes.
21
+ - If posting would genuinely help, don't just do it — **ask** with a two-option `mcp__frizz__ask` question ("Post this review to the PR" / "Keep it in-session only", the recommended one first), then rest. When the destructive edge is real (a force-merge, a close), that's the same question with `danger` set. Never a ` ```question ` fence — that fence is retired (2026-09-11), and a question in a fence body is plain prose.
22
+ - When the human HAS asked you to write, do exactly the scoped thing and report the resulting URL — nothing extra.
23
+
24
+ There is no server-side enforcement of this; the boundary is yours to hold.
25
+
26
+ ## Read recipes
27
+
28
+ Always scope with `-R OWNER/REPO` so a command is dir-independent, and prefer `--json <fields>` (+ `-q <jq>`) so you pull exactly what you need.
29
+
30
+ **Issues**
31
+ ```bash
32
+ gh issue view N -R OWNER/REPO --comments # full thread, human-readable
33
+ gh issue view N -R OWNER/REPO --json title,body,labels,state,url # structured
34
+ gh issue list -R OWNER/REPO --search "sort:updated-desc" --json number,title,url,updatedAt --limit 30
35
+ gh issue list -R OWNER/REPO --search "sort:reactions-desc is:open" --json number,title,url --limit 30
36
+ ```
37
+
38
+ **PRs + diffs**
39
+ ```bash
40
+ gh pr view N -R OWNER/REPO --json title,body,state,labels,files,additions,deletions,url
41
+ gh pr diff N -R OWNER/REPO # the unified diff
42
+ gh pr checks N -R OWNER/REPO # CI check rollup for the PR
43
+ gh pr view N -R OWNER/REPO --comments # review threads + conversation
44
+ ```
45
+ Read the changed files **in context**, not just the hunks — `gh pr diff` shows what changed, but correctness lives in the surrounding code.
46
+
47
+ **Reading ONE review (what a `watch_pr` wake hands you)**
48
+
49
+ A wake permalink ending `#pullrequestreview-<id>` is a **review**, and a review's `body` is routinely
50
+ **empty** — review apps (pullfrog, coderabbit) and humans doing an inline pass put every word in the
51
+ review's *inline comments*. Reading the body and concluding the review is empty is the wrong turn here.
52
+ One endpoint answers it in one call:
53
+
54
+ ```bash
55
+ gh api --paginate repos/OWNER/REPO/pulls/N/reviews/REVIEW_ID/comments \
56
+ --jq '.[] | "\(.path):\(.line // .original_line // "file")\n\(.body)\n"'
57
+ ```
58
+
59
+ Do **not** sweep `…/pulls/N/comments` and filter by `pull_request_review_id` — it pulls the whole PR's
60
+ history to find a handful of lines. Add the review's own body only if you need it
61
+ (`gh api repos/OWNER/REPO/pulls/N/reviews/REVIEW_ID --jq .body`). A `#issuecomment-<id>` permalink is
62
+ the other shape and *does* carry its substance in its body:
63
+ `gh api repos/OWNER/REPO/issues/comments/ID --jq .body`.
64
+
65
+ **`--paginate` is the default for any list endpoint.** `gh api` returns **30** items per page and caps
66
+ `per_page` at **100**, silently — a truncated page reads exactly like "that's all there is," so a
67
+ missing `--paginate` becomes a wrong answer rather than an error.
68
+
69
+ **CI / runs / releases**
70
+ ```bash
71
+ gh run list -R OWNER/REPO --branch BRANCH --limit 10
72
+ gh run view RUN_ID -R OWNER/REPO --log-failed # just the failing step logs
73
+ gh release view -R OWNER/REPO # latest release
74
+ ```
75
+
76
+ **Search (across issues/PRs)**
77
+ ```bash
78
+ gh search issues -R OWNER/REPO "crash on startup" --state open --json number,title,url --limit 30
79
+ gh search prs --repo OWNER/REPO "author:@me" --json number,title,url --limit 30
80
+ ```
81
+ Use search to find duplicates, related work, and prior art before you conclude something is novel.
82
+
83
+ **Raw API** for anything the porcelain doesn't cover:
84
+ ```bash
85
+ gh api repos/OWNER/REPO/commits/SHA/check-runs --jq '.check_runs[] | {name, conclusion}'
86
+ gh api "repos/OWNER/REPO/issues?state=open&labels=bug&per_page=50" --jq '.[] | {number, title, html_url}'
87
+ ```
88
+
89
+ ## Keep GitHub automation active
90
+
91
+ CI, automated review, releases, merge queues, and already-authorized merge progression are work you
92
+ can observe with `gh`; they do not earn an `awaiting` fence. Keep a live operation attached to the
93
+ thread and continue when it reports.
94
+
95
+ ### Select monitor tooling explicitly
96
+
97
+ Before launching any CI/review monitor, inspect project-local `AGENTS.md`, active skills, repository
98
+ docs, `package.json` scripts, and declared monitor tooling. Prefer an explicit project-local monitor
99
+ only if it documents terminal semantics for this gate. Validate its absolute command and terminal
100
+ event/exit contract before launch. If declared tooling is missing, invalid, or has no terminal
101
+ semantics, stop and report that configuration error; never silently shadow it with a Frizz script, and
102
+ never execute a monitor merely because its filename looks plausible.
103
+
104
+ When no project monitor is declared, the bundled fallback scripts are
105
+ `<this-skill-dir>/scripts/ci-watch.mjs` and `review-watch.mjs`. They are generated byte-for-byte from
106
+ Frizz's canonical `monitors/` source and require only Node plus logged-in `gh`. Their stdout is
107
+ `frizz.github-monitor/v1` NDJSON: `status` means keep waiting; `terminal` is a verdict. They join
108
+ exact-head workflow runs with PR checks, keeping `ACTION_REQUIRED` pending, and baseline every review
109
+ and comment so any new one wakes — bot or human, with no actor
110
+ filter. A GitHub/auth error is terminal exit 3; SIGINT/SIGTERM produces terminal
111
+ `cancelled` and exit 130. A `--once` pending/baseline snapshot is deliberately non-terminal exit 0.
112
+ For CI, retries are collapsed only within the same workflow name and event; distinct exact-head events
113
+ such as `push` and `pull_request` both contribute to the aggregate verdict.
114
+
115
+ - One-shot completion: launch `Bash` with `run_in_background: true`, for example
116
+ `gh run watch RUN_ID -R OWNER/REPO --exit-status` or a repo watcher that exits when all PR checks
117
+ settle. The completion task-notification re-invokes you. Diagnose/fix on red; continue the authorized
118
+ release/merge path on green.
119
+ - State transitions: use native `Monitor` with a quiet loop that prints only changes or the terminal
120
+ event. It is the Claude adapter for the selected script; do not make a sub-agent the monitor
121
+ abstraction.
122
+ Finite monitors run up to one hour; `persistent: true` runs until `TaskStop` or the Claude session
123
+ ends. Stop a watch once its gate is obsolete.
124
+ - A background Bash launch exposes an output-file path. Use `Read` on that path only for diagnostics;
125
+ `TaskOutput` is deprecated. Do not fake waiting with `echo waiting` or sleep-only Bash calls.
126
+
127
+ Both mechanisms are session-bound. If the next check deliberately belongs at a named wall-clock
128
+ instant, set a durable timer with `mcp__frizz__timer` and park with its id in your fence's `timers:`
129
+ list. If a specific external human reviewer/approver is the only remaining gate, that is a
130
+ registered question (`mcp__frizz__ask`) — waiting on a person is never a park. For a GitHub PR, register it with
131
+ `mcp__frizz__watch_pr` and name it in the fence's `prs:` list (`prs: [OWNER/REPO#NUMBER]`): frizz
132
+ baselines current reviews/comments and wakes on ANY new activity after registration — bot or human —
133
+ durably across restarts. The registration creates the wait; the fence only declares it. The dashboard
134
+ operator's own go/no-go remains a registered question.
135
+
136
+ ## Fitting gh work into your thread type
137
+
138
+ - **Investigating an issue** (a research thread): reproduce → trace to `file:line` (cite every load-bearing claim) → recommend the smallest correct fix; read the full thread and linked issues/PRs with `gh` for context. Don't implement — stop at the recommendation. Handback = findings in your final message.
139
+ - **Reviewing a PR** (an audit thread): read the diff AND the files in context, verify correctness/edges/tests, check CI (`gh pr checks`), then produce a review (blocking issues vs nits, each citing `file:line`) as your final message. Approve/merge only if explicitly asked.
140
+
141
+ In both cases: read-only on GitHub unless told otherwise, and the review/findings live in your session, not in a GitHub post.
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { classifyChecks, gh, latestWorkflowRuns, parseArgs, report, sleep } from "./github-watch.mjs"
3
+
4
+ const usage = "Usage: ci-watch.mjs --repo OWNER/REPO --pr NUMBER [--interval SECONDS] [--once]"
5
+
6
+ async function main() {
7
+ let options
8
+ try { options = parseArgs(process.argv.slice(2), usage) } catch (error) {
9
+ process.stderr.write(`ci-watch: ${error instanceof Error ? error.message : String(error)}\n`)
10
+ report("terminal", { kind: "ci", state: "error", error: error instanceof Error ? error.message : String(error) })
11
+ process.exitCode = 3
12
+ return
13
+ }
14
+ if (options.help) return console.log(usage)
15
+ let emittedTerminal = false
16
+ const terminal = (state, exitCode) => {
17
+ if (emittedTerminal) return
18
+ emittedTerminal = true
19
+ report("terminal", { kind: "ci", repo: options.repo, pr: options.pr, state })
20
+ process.exitCode = exitCode
21
+ }
22
+ const cancelled = () => { terminal("cancelled", 130); process.exit(130) }
23
+ process.once("SIGINT", cancelled)
24
+ process.once("SIGTERM", cancelled)
25
+ let previous
26
+ for (;;) {
27
+ try {
28
+ const pr = JSON.parse(await gh(["pr", "view", String(options.pr), "--repo", options.repo, "--json", "headRefOid"]))
29
+ const checks = JSON.parse(await gh(["pr", "checks", String(options.pr), "--repo", options.repo, "--json", "name,state,bucket,workflow,link"]))
30
+ const runs = pr.headRefOid ? JSON.parse(await gh(["run", "list", "--repo", options.repo, "--commit", pr.headRefOid, "--limit", "100", "--json", "name,workflowName,status,conclusion,databaseId,event,createdAt"])) : []
31
+ const workflows = latestWorkflowRuns(runs).map((run) => ({
32
+ name: run.name,
33
+ state: String(run.status).toUpperCase() === "COMPLETED" ? run.conclusion : run.status,
34
+ workflow: run.event,
35
+ link: run.databaseId ? `https://github.com/${options.repo}/actions/runs/${run.databaseId}` : undefined,
36
+ }))
37
+ const result = classifyChecks([...checks, ...workflows])
38
+ const signature = JSON.stringify(result)
39
+ if (signature !== previous) {
40
+ if (result.state === "pending") report("status", { kind: "ci", repo: options.repo, pr: options.pr, ...result })
41
+ else { emittedTerminal = true; report("terminal", { kind: "ci", repo: options.repo, pr: options.pr, ...result }) }
42
+ }
43
+ previous = signature
44
+ if (result.state === "passed") process.exitCode = 0
45
+ if (result.state === "failed") process.exitCode = 2
46
+ if (result.state !== "pending" || options.once) return
47
+ } catch (error) {
48
+ process.stderr.write(`ci-watch: ${error instanceof Error ? error.message : String(error)}\n`)
49
+ terminal("error", 3)
50
+ return
51
+ }
52
+ await sleep(options.interval * 1000)
53
+ }
54
+ }
55
+
56
+ main().catch((error) => {
57
+ process.stderr.write(`ci-watch: ${error instanceof Error ? error.message : String(error)}\n`)
58
+ report("terminal", { kind: "ci", state: "error", error: error instanceof Error ? error.message : String(error) })
59
+ process.exitCode = 3
60
+ })
@@ -0,0 +1,130 @@
1
+ import { spawn } from "node:child_process"
2
+
3
+ export const PROTOCOL = "frizz.github-monitor/v1"
4
+ const SUCCESS = new Set(["SUCCESS", "SKIPPED", "NEUTRAL"])
5
+ const FAILURE = new Set(["FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "STARTUP_FAILURE"])
6
+ export const GH_TIMEOUT_MS = 30_000
7
+ const GH_ATTEMPTS = 3
8
+
9
+ export function classifyChecks(checks) {
10
+ if (!Array.isArray(checks) || checks.length === 0) return { state: "pending", checks: [] }
11
+ let failed = false
12
+ let pending = false
13
+ for (const check of checks) {
14
+ const state = String(check?.state ?? check?.bucket ?? "").toUpperCase()
15
+ // A fork-gated workflow is reported as ACTION_REQUIRED after its skipped run completes. It is
16
+ // not CI success: continue watching for an approved replacement run instead of waking green.
17
+ if (state.includes("ACTION_REQUIRED") || state === "PENDING" || state === "QUEUED" || state === "IN_PROGRESS" || state === "WAITING") pending = true
18
+ else if (FAILURE.has(state) || state.includes("FAIL")) failed = true
19
+ else if (!SUCCESS.has(state) && state !== "PASS") pending = true
20
+ }
21
+ return { state: failed ? "failed" : pending ? "pending" : "passed", checks }
22
+ }
23
+
24
+ // Every review and comment counts, whoever filed it. Most PR review now arrives from an app —
25
+ // Pullfrog, Copilot, CodeRabbit, Greptile — and the reviewers that post their findings as a
26
+ // conversation comment are exactly what an actor-type filter used to throw away.
27
+ export function reviewActivity(raw) {
28
+ const pr = raw?.data?.repository?.pullRequest
29
+ const nodes = [...(pr?.reviews?.nodes ?? []), ...(pr?.comments?.nodes ?? [])]
30
+ return new Set(nodes.map((node) => String(node?.id ?? "")).filter(Boolean))
31
+ }
32
+
33
+ export function latestWorkflowRuns(runs) {
34
+ const latest = new Map()
35
+ for (const run of runs ?? []) {
36
+ const key = `${run.workflowName ?? run.name ?? "unknown"}\u0000${run.event ?? ""}`
37
+ const old = latest.get(key)
38
+ const stamp = String(run.createdAt ?? "")
39
+ const oldStamp = String(old?.createdAt ?? "")
40
+ const id = Number(run.databaseId ?? 0)
41
+ const oldId = Number(old?.databaseId ?? 0)
42
+ if (!old || stamp > oldStamp || (stamp === oldStamp && id > oldId)) latest.set(key, run)
43
+ }
44
+ return [...latest.values()]
45
+ }
46
+
47
+ function ghOnce(args, timeoutMs) {
48
+ return new Promise((resolve, reject) => {
49
+ let settled = false
50
+ const child = spawn("gh", args, {
51
+ env: { ...process.env, GH_PAGER: "cat", GH_PROMPT_DISABLED: "1" },
52
+ stdio: ["ignore", "pipe", "pipe"],
53
+ // A separate process group lets POSIX hosts terminate a hung credential helper too. Windows
54
+ // uses ChildProcess.kill below; both paths remain Node-only and need no `timeout` utility.
55
+ detached: process.platform !== "win32",
56
+ })
57
+ let stdout = ""
58
+ let stderr = ""
59
+ const fail = (error) => {
60
+ if (settled) return
61
+ settled = true
62
+ reject(error)
63
+ }
64
+ const kill = (signal) => {
65
+ if (process.platform !== "win32" && child.pid) {
66
+ try { process.kill(-child.pid, signal); return } catch { /* child may already be gone */ }
67
+ }
68
+ child.kill(signal)
69
+ }
70
+ const timeout = setTimeout(() => {
71
+ kill("SIGTERM")
72
+ // Some broken credential helpers ignore SIGTERM. Do not let one wedged gh process hold a
73
+ // monitor forever; a second, portable child-process kill keeps the watch bounded.
74
+ setTimeout(() => kill("SIGKILL"), 1_000).unref()
75
+ fail(new Error(`gh timed out after ${timeoutMs / 1000}s`))
76
+ }, timeoutMs)
77
+ child.stdout.on("data", (chunk) => { stdout += chunk })
78
+ child.stderr.on("data", (chunk) => { stderr += chunk })
79
+ child.once("error", (error) => { clearTimeout(timeout); fail(error) })
80
+ child.once("close", (status) => {
81
+ clearTimeout(timeout)
82
+ if (settled) return
83
+ settled = true
84
+ if (status !== 0) reject(new Error(stderr.trim() || `gh exited ${status ?? "without a status"}`))
85
+ else resolve(stdout)
86
+ })
87
+ })
88
+ }
89
+
90
+ export async function gh(args, { attempts = GH_ATTEMPTS, timeoutMs = GH_TIMEOUT_MS } = {}) {
91
+ if (!Number.isInteger(attempts) || attempts < 1) throw new Error("gh attempts must be a positive integer")
92
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) throw new Error("gh timeout must be positive")
93
+ let lastError
94
+ for (let attempt = 0; attempt < attempts; attempt++) {
95
+ try { return await ghOnce(args, timeoutMs) } catch (error) { lastError = error }
96
+ // A short bounded backoff covers transient GitHub and credential-helper failures. The final
97
+ // failure remains a terminal monitor error rather than an unbounded retry loop.
98
+ if (attempt + 1 < attempts) await sleep(250 * 2 ** attempt)
99
+ }
100
+ throw lastError
101
+ }
102
+
103
+ export function sleep(ms) {
104
+ return new Promise((resolve) => setTimeout(resolve, ms))
105
+ }
106
+
107
+ export function parseArgs(argv, usage) {
108
+ const out = { interval: 60, once: false }
109
+ for (let i = 0; i < argv.length; i++) {
110
+ const arg = argv[i]
111
+ if (arg === "--help") return { help: true }
112
+ if (arg === "--once") out.once = true
113
+ else if (arg === "--repo" || arg === "--pr" || arg === "--interval") out[arg.slice(2)] = argv[++i]
114
+ else throw new Error(`Unknown argument: ${arg}\n${usage}`)
115
+ }
116
+ if (!out.repo || !out.pr) throw new Error(usage)
117
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(out.repo)) throw new Error("--repo must be OWNER/REPO")
118
+ if (!/^\d+$/.test(String(out.pr)) || Number(out.pr) < 1) throw new Error("--pr must be a positive number")
119
+ out.interval = Number(out.interval)
120
+ if (!Number.isFinite(out.interval) || out.interval < 5) throw new Error("--interval must be at least 5 seconds")
121
+ return out
122
+ }
123
+
124
+ // Every stdout line is one schema-versioned NDJSON event. A monitor may emit many status events
125
+ // but exactly one terminal event when it reaches a terminal verdict. Exit codes: passed/new activity
126
+ // 0, CI failure 2, invocation/GitHub error 3. `--once` may end after a non-terminal snapshot with 0.
127
+ export function report(type, value) {
128
+ if (type !== "status" && type !== "terminal") throw new Error(`invalid monitor event type: ${type}`)
129
+ process.stdout.write(`${JSON.stringify({ protocol: PROTOCOL, type, at: new Date().toISOString(), ...value })}\n`)
130
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { gh, parseArgs, report, reviewActivity, sleep } from "./github-watch.mjs"
3
+
4
+ const usage = "Usage: review-watch.mjs --repo OWNER/REPO --pr NUMBER [--interval SECONDS] [--once]"
5
+ const QUERY = `query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviews(last:50){nodes{id}} comments(last:50){nodes{id}}}}}`
6
+
7
+ async function main() {
8
+ let options
9
+ try { options = parseArgs(process.argv.slice(2), usage) } catch (error) {
10
+ process.stderr.write(`review-watch: ${error instanceof Error ? error.message : String(error)}\n`)
11
+ report("terminal", { kind: "review", state: "error", error: error instanceof Error ? error.message : String(error) })
12
+ process.exitCode = 3
13
+ return
14
+ }
15
+ if (options.help) return console.log(usage)
16
+ const [owner, repo] = options.repo.split("/")
17
+ if (!owner || !repo) throw new Error("--repo must be OWNER/REPO")
18
+ let emittedTerminal = false
19
+ const terminal = (state, exitCode, extra = {}) => {
20
+ if (emittedTerminal) return
21
+ emittedTerminal = true
22
+ report("terminal", { kind: "review", repo: options.repo, pr: options.pr, state, ...extra })
23
+ process.exitCode = exitCode
24
+ }
25
+ const cancelled = () => { terminal("cancelled", 130); process.exit(130) }
26
+ process.once("SIGINT", cancelled)
27
+ process.once("SIGTERM", cancelled)
28
+ let baseline
29
+ for (;;) {
30
+ try {
31
+ const raw = JSON.parse(await gh(["api", "graphql", "-f", `query=${QUERY}`, "-F", `owner=${owner}`, "-F", `repo=${repo}`, "-F", `number=${options.pr}`]))
32
+ const current = reviewActivity(raw)
33
+ if (!baseline) {
34
+ baseline = current
35
+ report("status", { kind: "review", repo: options.repo, pr: options.pr, state: "armed", seen: current.size })
36
+ } else {
37
+ const added = [...current].filter((id) => !baseline.has(id))
38
+ if (added.length) { terminal("new-activity", 0, { ids: added }); return }
39
+ }
40
+ if (options.once) return
41
+ } catch (error) {
42
+ process.stderr.write(`review-watch: ${error instanceof Error ? error.message : String(error)}\n`)
43
+ terminal("error", 3)
44
+ return
45
+ }
46
+ await sleep(options.interval * 1000)
47
+ }
48
+ }
49
+
50
+ main().catch((error) => {
51
+ process.stderr.write(`review-watch: ${error instanceof Error ? error.message : String(error)}\n`)
52
+ report("terminal", { kind: "review", state: "error", error: error instanceof Error ? error.message : String(error) })
53
+ process.exitCode = 3
54
+ })
Binary file