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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/claude-agent-broker.js +21714 -0
- package/dist/codex-app-server-daemon.js +354 -0
- package/dist/dev-child.js +39342 -0
- package/dist/frizz.js +10517 -0
- package/package.json +54 -20
- 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 +982 -0
- package/runtime/board/decisions.mjs +97 -0
- package/runtime/board/index.mjs +699 -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 +285 -0
- package/runtime/cc-worker/.claude-plugin/plugin.json +10 -0
- package/runtime/cc-worker/DECISIONS.md +1025 -0
- package/runtime/cc-worker/LICENSE +21 -0
- package/runtime/cc-worker/agents/fable-high.md +8 -0
- package/runtime/cc-worker/agents/fable-low.md +8 -0
- package/runtime/cc-worker/agents/fable-max.md +8 -0
- package/runtime/cc-worker/agents/fable-medium.md +8 -0
- package/runtime/cc-worker/agents/fable-xhigh.md +8 -0
- package/runtime/cc-worker/agents/haiku.md +7 -0
- package/runtime/cc-worker/agents/opus-high.md +8 -0
- package/runtime/cc-worker/agents/opus-low.md +8 -0
- package/runtime/cc-worker/agents/opus-max.md +8 -0
- package/runtime/cc-worker/agents/opus-medium.md +8 -0
- package/runtime/cc-worker/agents/opus-xhigh.md +8 -0
- package/runtime/cc-worker/agents/sonnet-high.md +8 -0
- package/runtime/cc-worker/agents/sonnet-low.md +8 -0
- package/runtime/cc-worker/agents/sonnet-max.md +8 -0
- package/runtime/cc-worker/agents/sonnet-medium.md +8 -0
- package/runtime/cc-worker/agents/sonnet-xhigh.md +8 -0
- package/runtime/cc-worker/bin/frizz +17 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +564 -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 +98 -0
- package/runtime/cc-worker/hooks/bash-background.d.mts +6 -0
- package/runtime/cc-worker/hooks/bash-background.mjs +236 -0
- package/runtime/cc-worker/hooks/deny-ask.mjs +38 -0
- package/runtime/cc-worker/hooks/deny-plan.mjs +61 -0
- package/runtime/cc-worker/hooks/hooks.json +111 -0
- package/runtime/cc-worker/hooks/perm-policy.mjs +211 -0
- package/runtime/cc-worker/hooks/precompact-instructions.mjs +122 -0
- package/runtime/cc-worker/hooks/scratchpad-stop.mjs +125 -0
- package/runtime/cc-worker/hooks/scratchpad.mjs +446 -0
- package/runtime/cc-worker/hooks/session-seed.mjs +106 -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 +154 -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/runtime/cc-worker/skills/handoff/SKILL.md +209 -0
- package/runtime/cc-worker/skills/waits/SKILL.md +83 -0
- package/web-dist/apple-touch-icon.png +0 -0
- package/web-dist/assets/TerminalPane-ROKHp1ib.js +7 -0
- package/web-dist/assets/abnfDiagram-VRR7QNED-DcpdhBs3.js +1 -0
- package/web-dist/assets/arc-BSyeo0Gb.js +1 -0
- package/web-dist/assets/architecture-TIHT7OUA-CAviNivx.js +1 -0
- package/web-dist/assets/architectureDiagram-ZJ3FMSHR-CUAKf0mn.js +36 -0
- package/web-dist/assets/array-BifhSqXX.js +1 -0
- package/web-dist/assets/blockDiagram-677ZJIJ3-BPwpJIzx.js +132 -0
- package/web-dist/assets/c4Diagram-LMCZKHZV-1lptuHzZ.js +10 -0
- package/web-dist/assets/channel-CqKDIFQF.js +1 -0
- package/web-dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web-dist/assets/chunk-32BRIVSS-CFR9AKjY.js +1 -0
- package/web-dist/assets/chunk-52WLFC77-CM9uct7m.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-DiveJARw.js +1 -0
- package/web-dist/assets/chunk-EX3LRPZG-BE1CBw8F.js +231 -0
- package/web-dist/assets/chunk-FWX5IMBZ-DL42uXiO.js +2 -0
- package/web-dist/assets/chunk-HOUHSVGY-DPhJWgDw.js +1 -0
- package/web-dist/assets/chunk-ICXQ74PX-CwYy-6AP.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-Ds5I9wxq.js +88 -0
- package/web-dist/assets/chunk-OGEWGWER-DHiZJwQD.js +1 -0
- package/web-dist/assets/chunk-PUDLZKDR-B-eyQTsF.js +156 -0
- package/web-dist/assets/chunk-Q4XR5HBZ-DK7dB3Ti.js +70 -0
- package/web-dist/assets/chunk-RYQCIY6F-Cu_KplZW.js +1 -0
- package/web-dist/assets/chunk-V7JOEXUC-Dn59m74L.js +206 -0
- package/web-dist/assets/chunk-VAUOI2AC-DK7x36hd.js +1 -0
- package/web-dist/assets/chunk-VR4S4FIN-D7-CI3Yl.js +1 -0
- package/web-dist/assets/chunk-WYO6CB5R-B3l-mLCs.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-Dobxlxie.js +62 -0
- package/web-dist/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
- package/web-dist/assets/classDiagram-OUVF2IWQ-B_-6iXYY.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-B_-6iXYY.js +1 -0
- package/web-dist/assets/cose-bilkent-JH36ORCC-BUIsLrGc.js +1 -0
- package/web-dist/assets/cynefin-VYW2F7L2-Dh7RuEUJ.js +1 -0
- package/web-dist/assets/cynefinDiagram-TSTJHNR4-ClPi2mZZ.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-52_WP1QV.js +4 -0
- package/web-dist/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/web-dist/assets/diagram-FQU43EPY-D_1zVsTL.js +3 -0
- package/web-dist/assets/diagram-G47NLZAW-CdZxuGUy.js +24 -0
- package/web-dist/assets/diagram-NH7WQ7WH-C8pSFu0P.js +24 -0
- package/web-dist/assets/diagram-OA4YK3LP-C5bjZLre.js +30 -0
- package/web-dist/assets/diagram-WEI45ONY-Bxzhiuzn.js +41 -0
- package/web-dist/assets/dist-DoH_9pyS.js +1 -0
- package/web-dist/assets/ebnfDiagram-CCIWWBDH-g-Z0J2wP.js +1 -0
- package/web-dist/assets/erDiagram-Q63AITRT-DVNkgIHp.js +85 -0
- package/web-dist/assets/eventmodeling-45OFAUF4-MpmeH5YZ.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-D-QgjjhF.js +1 -0
- package/web-dist/assets/ganttDiagram-NO4QXBWP-_71pQYEK.js +292 -0
- package/web-dist/assets/gitGraph-TEB2WS4Q-ChIZiGZS.js +1 -0
- package/web-dist/assets/gitGraphDiagram-IHSO6WYX-DCHAFI0l.js +106 -0
- package/web-dist/assets/graphlib-B8gBHxth.js +1 -0
- package/web-dist/assets/index-w4v-GZEc.js +358 -0
- package/web-dist/assets/index-zyi22LPz.css +1 -0
- package/web-dist/assets/info-DKCQHKI2-BW-n_T1j.js +1 -0
- package/web-dist/assets/infoDiagram-FWYZ7A6U-CgDYsKi9.js +2 -0
- package/web-dist/assets/init-D6jRqBbL.js +1 -0
- package/web-dist/assets/ishikawaDiagram-FXEZZL3T-ClzGNt9N.js +70 -0
- package/web-dist/assets/journeyDiagram-5HDEW3XC-DSCQxkHC.js +139 -0
- package/web-dist/assets/kanban-definition-HUTT4EX6-CdrdX9N8.js +89 -0
- package/web-dist/assets/katex-CddkPoXu.js +257 -0
- package/web-dist/assets/line-ha38Dc-1.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-Z4uMcpip.js +7 -0
- package/web-dist/assets/mermaid.core-iZRq3hbu.js +11 -0
- package/web-dist/assets/mindmap-definition-LN4V7U3C-DmhInJO_.js +96 -0
- package/web-dist/assets/ordinal-hYBb2elL.js +1 -0
- package/web-dist/assets/packet-7NZHBO7P-DBPB36Kl.js +1 -0
- package/web-dist/assets/path-BWPyau1x.js +1 -0
- package/web-dist/assets/pegDiagram-2B236MQR-CAH3ljfj.js +1 -0
- package/web-dist/assets/pie-RZYD4A2V-_h_eX4Ca.js +1 -0
- package/web-dist/assets/pieDiagram-ENE6RG2P-DFBPus8j.js +39 -0
- package/web-dist/assets/quadrantDiagram-ABIIQ3AL-DMvOCjt8.js +7 -0
- package/web-dist/assets/radar-I7S5WNFK-2EzoPHEZ.js +1 -0
- package/web-dist/assets/railroad-3IZDKUUU-BPJnn-hm.js +1 -0
- package/web-dist/assets/railroad-abnf-AHOZXSZD-YeUoiySk.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-Ddw1SuGG.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Dd8BOGeW.js +1 -0
- package/web-dist/assets/railroadDiagram-RFXS5EU6-DKq5FagA.js +1 -0
- package/web-dist/assets/requirementDiagram-TGXJPOKE-BJ5tGazp.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-XSJjcBhX.js +40 -0
- package/web-dist/assets/sequenceDiagram-DBY2YBRQ-CBb8emSe.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-DDfRW94V.js +1 -0
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-hc41W5Lx.js +1 -0
- package/web-dist/assets/swimlanes-5IMT3BWC-DvRYbkZi.js +2 -0
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-BZyGdgSG.js +8 -0
- package/web-dist/assets/timeline-definition-FHXFAJF6-BNUa_DwI.js +120 -0
- package/web-dist/assets/treeView-QDETBFTQ-I6-IW6nJ.js +1 -0
- package/web-dist/assets/treemap-6X3UGDF4-CWWmEUYJ.js +1 -0
- package/web-dist/assets/vennDiagram-L72KCM5P-DTDrPGLk.js +34 -0
- package/web-dist/assets/wardley-OPB4EBWU-CNsdgXXA.js +1 -0
- package/web-dist/assets/wardleyDiagram-EHGQE667-YE0tq3Kh.js +78 -0
- package/web-dist/assets/xychartDiagram-FW5EYKEG-D0ofMX8C.js +7 -0
- package/web-dist/favicon-16.png +0 -0
- package/web-dist/favicon-32.png +0 -0
- package/web-dist/favicon.svg +78 -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 +33 -0
- package/web-dist/manifest.webmanifest +16 -0
- package/index.d.ts +0 -1
- package/index.js +0 -2
|
@@ -0,0 +1,154 @@
|
|
|
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), optional toon use for large JSON, 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.1
|
|
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**, optional **toon** use for large JSON, 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 ` ```question ` block ("A. Post this review to the PR / B. Keep it in-session only", Recommendation), then rest. When the destructive edge is real (a force-merge, a close), that's a ` ```question danger ` gate.
|
|
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
|
+
## toon — pipe LARGE, FLAT gh JSON through the shim
|
|
27
|
+
|
|
28
|
+
`toon` (Token-Oriented Object Notation) losslessly re-encodes JSON ~30–40% smaller for LLM context. Use it only when a `gh … --json` result you're reading into YOUR context is **large and flat** (a list page: `gh issue list`, `gh pr list`, `gh search`, `gh api` list endpoints) and `command -v toon` succeeds. It is optional: do not install it or assume a home-directory-specific location.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
if command -v toon >/dev/null 2>&1; then
|
|
32
|
+
gh issue list -R OWNER/REPO --json number,title,url,updatedAt --limit 50 | toon
|
|
33
|
+
else
|
|
34
|
+
gh issue list -R OWNER/REPO --json number,title,url,updatedAt --limit 50
|
|
35
|
+
fi
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Skip toon** for tiny payloads (a handful of fields, one item) and for **deeply-nested** JSON (`reactionGroups`, review threads, nested files) — nesting defeats tabularization, so the savings collapse to noise and you add a parse tax for yourself. A single `gh pr view N --json …` is small — read it raw.
|
|
39
|
+
|
|
40
|
+
## Read recipes
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
**Issues**
|
|
45
|
+
```bash
|
|
46
|
+
gh issue view N -R OWNER/REPO --comments # full thread, human-readable
|
|
47
|
+
gh issue view N -R OWNER/REPO --json title,body,labels,state,url # structured
|
|
48
|
+
gh issue list -R OWNER/REPO --search "sort:updated-desc" --json number,title,url,updatedAt --limit 30
|
|
49
|
+
gh issue list -R OWNER/REPO --search "sort:reactions-desc is:open" --json number,title,url --limit 30
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**PRs + diffs**
|
|
53
|
+
```bash
|
|
54
|
+
gh pr view N -R OWNER/REPO --json title,body,state,labels,files,additions,deletions,url
|
|
55
|
+
gh pr diff N -R OWNER/REPO # the unified diff — pipe through toon only if HUGE and you just need shape
|
|
56
|
+
gh pr checks N -R OWNER/REPO # CI check rollup for the PR
|
|
57
|
+
gh pr view N -R OWNER/REPO --comments # review threads + conversation
|
|
58
|
+
```
|
|
59
|
+
Read the changed files **in context**, not just the hunks — `gh pr diff` shows what changed, but correctness lives in the surrounding code.
|
|
60
|
+
|
|
61
|
+
**Reading ONE review (what a `pr-watch` wake hands you)**
|
|
62
|
+
|
|
63
|
+
A wake permalink ending `#pullrequestreview-<id>` is a **review**, and a review's `body` is routinely
|
|
64
|
+
**empty** — review apps (pullfrog, coderabbit) and humans doing an inline pass put every word in the
|
|
65
|
+
review's *inline comments*. Reading the body and concluding the review is empty is the wrong turn here.
|
|
66
|
+
One endpoint answers it in one call:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
gh api --paginate repos/OWNER/REPO/pulls/N/reviews/REVIEW_ID/comments \
|
|
70
|
+
--jq '.[] | "\(.path):\(.line // .original_line // "file")\n\(.body)\n"'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Do **not** sweep `…/pulls/N/comments` and filter by `pull_request_review_id` — it pulls the whole PR's
|
|
74
|
+
history to find a handful of lines. Add the review's own body only if you need it
|
|
75
|
+
(`gh api repos/OWNER/REPO/pulls/N/reviews/REVIEW_ID --jq .body`). A `#issuecomment-<id>` permalink is
|
|
76
|
+
the other shape and *does* carry its substance in its body:
|
|
77
|
+
`gh api repos/OWNER/REPO/issues/comments/ID --jq .body`.
|
|
78
|
+
|
|
79
|
+
**`--paginate` is the default for any list endpoint.** `gh api` returns **30** items per page and caps
|
|
80
|
+
`per_page` at **100**, silently — a truncated page reads exactly like "that's all there is," so a
|
|
81
|
+
missing `--paginate` becomes a wrong answer rather than an error.
|
|
82
|
+
|
|
83
|
+
**CI / runs / releases**
|
|
84
|
+
```bash
|
|
85
|
+
gh run list -R OWNER/REPO --branch BRANCH --limit 10
|
|
86
|
+
gh run view RUN_ID -R OWNER/REPO --log-failed # just the failing step logs
|
|
87
|
+
gh release view -R OWNER/REPO # latest release
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Search (across issues/PRs)**
|
|
91
|
+
```bash
|
|
92
|
+
gh search issues -R OWNER/REPO "crash on startup" --state open --json number,title,url --limit 30
|
|
93
|
+
gh search prs --repo OWNER/REPO "author:@me" --json number,title,url --limit 30
|
|
94
|
+
```
|
|
95
|
+
Use search to find duplicates, related work, and prior art before you conclude something is novel.
|
|
96
|
+
|
|
97
|
+
**Raw API** for anything the porcelain doesn't cover:
|
|
98
|
+
```bash
|
|
99
|
+
gh api repos/OWNER/REPO/commits/SHA/check-runs --jq '.check_runs[] | {name, conclusion}'
|
|
100
|
+
gh api "repos/OWNER/REPO/issues?state=open&labels=bug&per_page=50" | { command -v toon >/dev/null 2>&1 && toon || cat; }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Keep GitHub automation active
|
|
104
|
+
|
|
105
|
+
CI, automated review, releases, merge queues, and already-authorized merge progression are work you
|
|
106
|
+
can observe with `gh`; they do not earn an `awaiting` fence. Keep a live operation attached to the
|
|
107
|
+
thread and continue when it reports.
|
|
108
|
+
|
|
109
|
+
### Select monitor tooling explicitly
|
|
110
|
+
|
|
111
|
+
Before launching any CI/review monitor, inspect project-local `AGENTS.md`, active skills, repository
|
|
112
|
+
docs, `package.json` scripts, and declared monitor tooling. Prefer an explicit project-local monitor
|
|
113
|
+
only if it documents terminal semantics for this gate. Validate its absolute command and terminal
|
|
114
|
+
event/exit contract before launch. If declared tooling is missing, invalid, or has no terminal
|
|
115
|
+
semantics, stop and report that configuration error; never silently shadow it with a Frizz script, and
|
|
116
|
+
never execute a monitor merely because its filename looks plausible.
|
|
117
|
+
|
|
118
|
+
When no project monitor is declared, the bundled fallback scripts are
|
|
119
|
+
`<this-skill-dir>/scripts/ci-watch.mjs` and `review-watch.mjs`. They are generated byte-for-byte from
|
|
120
|
+
Frizz's canonical `monitors/` source and require only Node plus logged-in `gh`. Their stdout is
|
|
121
|
+
`frizz.github-monitor/v1` NDJSON: `status` means keep waiting; `terminal` is a verdict. They join
|
|
122
|
+
exact-head workflow runs with PR checks, keeping `ACTION_REQUIRED` pending, and baseline every review
|
|
123
|
+
and comment so any new one wakes — bot or human, with no actor
|
|
124
|
+
filter. A GitHub/auth error is terminal exit 3; SIGINT/SIGTERM produces terminal
|
|
125
|
+
`cancelled` and exit 130. A `--once` pending/baseline snapshot is deliberately non-terminal exit 0.
|
|
126
|
+
For CI, retries are collapsed only within the same workflow name and event; distinct exact-head events
|
|
127
|
+
such as `push` and `pull_request` both contribute to the aggregate verdict.
|
|
128
|
+
|
|
129
|
+
- One-shot completion: launch `Bash` with `run_in_background: true`, for example
|
|
130
|
+
`gh run watch RUN_ID -R OWNER/REPO --exit-status` or a repo watcher that exits when all PR checks
|
|
131
|
+
settle. The completion task-notification re-invokes you. Diagnose/fix on red; continue the authorized
|
|
132
|
+
release/merge path on green.
|
|
133
|
+
- State transitions: use native `Monitor` with a quiet loop that prints only changes or the terminal
|
|
134
|
+
event. It is the Claude adapter for the selected script; do not make a sub-agent the monitor
|
|
135
|
+
abstraction.
|
|
136
|
+
Finite monitors run up to one hour; `persistent: true` runs until `TaskStop` or the Claude session
|
|
137
|
+
ends. Stop a watch once its gate is obsolete.
|
|
138
|
+
- A background Bash launch exposes an output-file path. Use `Read` on that path only for diagnostics;
|
|
139
|
+
`TaskOutput` is deprecated. Do not fake waiting with `echo waiting` or sleep-only Bash calls.
|
|
140
|
+
|
|
141
|
+
Both mechanisms are session-bound. If the next check deliberately belongs at a named wall-clock
|
|
142
|
+
instant, park with a durable `timer:` fence. If a specific external human reviewer/approver is the
|
|
143
|
+
only remaining gate, park with `human: <actor + exact action>`. For a GitHub PR, pair it with
|
|
144
|
+
`pr-watch: OWNER/REPO#NUMBER`: frizz baselines current reviews/comments and wakes on ANY new
|
|
145
|
+
activity after the fence — bot or human — durably across restarts. Otherwise optionally pair a timer for
|
|
146
|
+
a scheduled recheck. The dashboard operator's own go/no-go remains a ` ```question ` block. `pr:` / `ci:` /
|
|
147
|
+
`session:` awaiting hints are legacy compatibility only — do not emit them for new automated waits.
|
|
148
|
+
|
|
149
|
+
## Fitting gh work into your thread type
|
|
150
|
+
|
|
151
|
+
- **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.
|
|
152
|
+
- **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.
|
|
153
|
+
|
|
154
|
+
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} seconds`))
|
|
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
|
+
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handoff
|
|
3
|
+
description: The full frizz end-of-turn signal reference for a frizz worker (invoke as frizz:handoff) — every `awaiting` hint kind, the `question` fence tags (`danger`, `multi`), `done` body formatting, and worked examples of each. Your system-prompt contract carries the rules you need for the common case; load this when you are emitting an unusual fence, need a worked example of a tagged question card, or are unsure which fence a situation calls for.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# frizz handoff reference
|
|
7
|
+
|
|
8
|
+
Your system prompt states the fence rules. This is the elaboration: the exact shapes, the tags, and
|
|
9
|
+
worked examples. Nothing here overrides the contract.
|
|
10
|
+
|
|
11
|
+
**Before you use any of it: is the turn actually over?** Every shape below is for a turn that has
|
|
12
|
+
genuinely ended. If the human's instruction still has parts left, none of them apply — you do not pick
|
|
13
|
+
a fence, you make the next tool call. Reaching for this reference at a milestone is the most common way
|
|
14
|
+
an effort dies half-finished; a verified increment, a green test run and a long turn are not endings,
|
|
15
|
+
and neither is announcing the next step or recording it in the scratchpad.
|
|
16
|
+
|
|
17
|
+
## Which fence?
|
|
18
|
+
|
|
19
|
+
| Situation | Fence |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Ordinary handoff, turn has no other final state | **bare rest** (no fence) |
|
|
22
|
+
| Effort's real work is complete — code merged, plan/doc written, commissioned report finished | `done` |
|
|
23
|
+
| Waiting on a named third-party human, a wall-clock instant, or a PR's review | `awaiting` |
|
|
24
|
+
| You need the operator's input or approval | `question` |
|
|
25
|
+
| Mid-conversation (still working, or answering and continuing) | none |
|
|
26
|
+
|
|
27
|
+
Automatable waits — CI, releases, deploys, merge progression — are **never** `awaiting`. Dispatch a
|
|
28
|
+
sub-agent to own the wait, or use a `timer:` when the next check belongs at a later wall-clock time.
|
|
29
|
+
|
|
30
|
+
## `done`
|
|
31
|
+
|
|
32
|
+
Body is a bullet list, one `- ` item per completed task, each naming what shipped and where. The card
|
|
33
|
+
renders inline markdown: backtick every path/identifier/command, and make references real links.
|
|
34
|
+
|
|
35
|
+
```done
|
|
36
|
+
- Fixed the cache collision in [`src/resolver.ts`](https://github.com/acme/app/pull/391) — the lookup now keys on the normalized id.
|
|
37
|
+
- Added a regression test for the collision case; `npm test` green.
|
|
38
|
+
- Self-review folded in; `npm run lint` clean.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Do not write a narrative paragraph. Do not fence `done` on work that is not landed.
|
|
42
|
+
|
|
43
|
+
## `awaiting` — the three hint kinds
|
|
44
|
+
|
|
45
|
+
Lead the body with one or more `kind: value` lines, then prose naming the exact wake condition.
|
|
46
|
+
|
|
47
|
+
`pr-watch: owner/repo#NUMBER` — frizz polls the PR and resumes you on ANY new activity after the fence:
|
|
48
|
+
a review, an approval, or a comment, from a **human or a bot alike** (review agents that post findings
|
|
49
|
+
as a conversation comment count exactly like a human reviewer). Baselined at the fence and durable
|
|
50
|
+
across a server/worker restart. Your thread **stays in the queue** as a visible "PR is up, watching it"
|
|
51
|
+
handoff — it does not hide in Held, because a PR whose reviews may never arrive must not silently
|
|
52
|
+
vanish. The human can Snooze it; new activity bumps it back.
|
|
53
|
+
|
|
54
|
+
```awaiting
|
|
55
|
+
pr-watch: acme/app#391
|
|
56
|
+
PR is open and CI is green. Watching for review — I'll address comments or merge on approval.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**One line watches one PR, and a fence may carry several.** The scheduler evaluates every hint in the
|
|
60
|
+
body, so repeat the line per PR — across any mix of repos — and activity on ANY of them wakes you. A
|
|
61
|
+
set of open PRs is NOT a reason to fall back to a periodic `timer:` sweep, which trades instant wakes
|
|
62
|
+
for a poll that can sit a day behind a review. The body keeps its first **8** hint lines and silently
|
|
63
|
+
drops the rest, so past 8 watch the 8 that matter and cover the tail with a `timer:`.
|
|
64
|
+
|
|
65
|
+
```awaiting
|
|
66
|
+
pr-watch: withastro/astro#17487
|
|
67
|
+
pr-watch: vitejs/vite#23019
|
|
68
|
+
pr-watch: strapi/strapi#26864
|
|
69
|
+
All three adoption PRs are open and green, in their maintainers' hands. Whichever gets a review first
|
|
70
|
+
wakes me and I'll address it.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`human: <actor + exact review/approval>` — a third party whose action cannot be supplied in this frizz
|
|
74
|
+
conversation. **Parks you in the dimmed Held band.** A bot, automated reviewer, CI gate, or merge queue
|
|
75
|
+
is NOT a human wait. Pair with `pr-watch:` when a machine-readable PR exists (the `human:` supplies the
|
|
76
|
+
Held park, the `pr-watch:` supplies the cursor), or with `timer:` when none does.
|
|
77
|
+
|
|
78
|
+
```awaiting
|
|
79
|
+
human: dependabot maintainer review on dependabot/dependabot-core#15524
|
|
80
|
+
pr-watch: dependabot/dependabot-core#15524
|
|
81
|
+
The implementation and actionable checks are complete; address requested changes when review lands.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`timer: <ISO-8601 instant>` — the durable frizz scheduler resumes you at that instant, across process
|
|
85
|
+
exits and restarts. The prose says exactly what to re-check.
|
|
86
|
+
|
|
87
|
+
```awaiting
|
|
88
|
+
timer: 2026-07-15T17:00:00Z
|
|
89
|
+
Re-check whether the external maintainer review arrived and reclassify any new failure.
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`pr:` / `ci:` / `session:` remain parser compatibility for existing transcripts only. Never emit them.
|
|
93
|
+
|
|
94
|
+
### Re-entering a wait after a follow-up
|
|
95
|
+
|
|
96
|
+
Every human follow-up clears the previous fence. Never answer that you are "already parked" and never
|
|
97
|
+
rely on the old fence, scratchpad, or thread status: re-check the blocker, then either re-emit a fresh
|
|
98
|
+
`awaiting` with a current hint, or arm the active wait if it turns out to be automatable.
|
|
99
|
+
|
|
100
|
+
## `question` — the tags
|
|
101
|
+
|
|
102
|
+
Plain — an open question:
|
|
103
|
+
|
|
104
|
+
```question
|
|
105
|
+
Should the settings store use SQLite or a JSON file?
|
|
106
|
+
|
|
107
|
+
- A. SQLite — transactional, matches how sessions are already stored (recommended: consistency)
|
|
108
|
+
- B. JSON file — zero deps, human-editable, racy under concurrent writes
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A GO/NO-GO gate has NO tag of its own — it is a plain `question` with two options, the go and the
|
|
112
|
+
decline. (There used to be an `approval` tag rendering one Approve button that SENT on click; it was
|
|
113
|
+
dropped 2026-07-26 because it couldn't express the decline and it bypassed the staging every other
|
|
114
|
+
block uses. A legacy `approval` token still parses — as a plain question — so old transcripts render,
|
|
115
|
+
but never write one.)
|
|
116
|
+
|
|
117
|
+
```question
|
|
118
|
+
Ready to create CONTRIBUTING.md with the draft above?
|
|
119
|
+
|
|
120
|
+
- A. Approve as-is (recommended)
|
|
121
|
+
- B. Hold — tell me what to change first
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`danger` — reserve for the genuinely hard-to-undo (force-merge, deletion, history rewrite,
|
|
125
|
+
prod rollback). Renders in red. A routine ship is a plain question:
|
|
126
|
+
|
|
127
|
+
```question danger
|
|
128
|
+
Force-merge PR #391 over the failing flaky check and delete the `legacy-api` branch?
|
|
129
|
+
|
|
130
|
+
- A. Do it — the failure is the known-flaky timeout
|
|
131
|
+
- B. Hold — I'll wait for a green run
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`multi` — select-several triage. Options render as checkboxes; the answer returns the chosen letters:
|
|
135
|
+
|
|
136
|
+
```question multi
|
|
137
|
+
Which of these findings should I fix in this pass?
|
|
138
|
+
|
|
139
|
+
- A. Null-deref in parse() — crashes on empty input
|
|
140
|
+
- B. Off-by-one in slice() — drops the last row
|
|
141
|
+
- C. Flaky timeout in the retry test — passes on rerun
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Rules that apply to every question
|
|
145
|
+
|
|
146
|
+
- Open the message with 2-4 sentences of status before the blocks.
|
|
147
|
+
- One block per independent question — never bundle.
|
|
148
|
+
- Lettered options, one markdown list item each, with a one-line tradeoff.
|
|
149
|
+
- Mark exactly one option `recommended` **on that option's line**, and put it first as `A`. Use
|
|
150
|
+
`(recommended: one-line why)` to carry the rationale into the chip's tooltip. Do not use a separate
|
|
151
|
+
`Recommendation:` line.
|
|
152
|
+
- Answerable COLD, in the human's own vocabulary — see the next section.
|
|
153
|
+
- A question IS the handback: no second fence.
|
|
154
|
+
- Before you write one, re-read the stop criterion. A question about work you were dispatched to do,
|
|
155
|
+
or a fix you already recommend, is not a question.
|
|
156
|
+
|
|
157
|
+
### Write it in the human's own vocabulary
|
|
158
|
+
|
|
159
|
+
The reader has their original prompt and nothing else — not your plan, not your scratchpad, not the
|
|
160
|
+
transcript, not the names you settled on while working. A question that reads perfectly from inside the
|
|
161
|
+
session is routinely unanswerable from outside it. This is the most common defect in real question
|
|
162
|
+
cards, and it is entirely a wording problem: the decision was fine, the phrasing made it unavailable.
|
|
163
|
+
|
|
164
|
+
- **Translate the nomenclature you coined mid-effort.** Anything you named while working is invisible to
|
|
165
|
+
the reader: phase / lane / tier / mode names, step or section numbers, "the C path", "the second
|
|
166
|
+
variant", "the reconciler", "option 3 from earlier", "as in §2 of the plan". They will not go read your
|
|
167
|
+
transcript to decode it, so say what the thing does instead of what you called it.
|
|
168
|
+
- **Minimize code identifiers; default to plain behavior.** File paths, function / type / component
|
|
169
|
+
names, flags, env vars, table and column names usually cost the reader more than they give — lead with
|
|
170
|
+
the behavior. Spend an identifier where it genuinely earns its place: the human already uses it, or the
|
|
171
|
+
decision is literally about that name (they asked you to rename it, or to pick a flag's spelling). One
|
|
172
|
+
well-chosen identifier is fine; a card assembled out of them is the failure mode.
|
|
173
|
+
- **Carry every decision input inside the block.** What happens today, each option's user-visible
|
|
174
|
+
consequence, the cost of guessing wrong, and any number that matters. "As discussed above", a pointer
|
|
175
|
+
to a file, or a reference to an earlier turn all point at something the reader cannot see from the card.
|
|
176
|
+
- **Define a load-bearing new term, or drop it.** If one unfamiliar word genuinely cannot be avoided,
|
|
177
|
+
define it in the same sentence. Otherwise it is decoration, and it costs you the answer.
|
|
178
|
+
|
|
179
|
+
Bad — every noun here was invented during the effort, so the reader has no way into it:
|
|
180
|
+
|
|
181
|
+
```question
|
|
182
|
+
Should the queue lane keep the tier-2 fallback from step 3, or move to the unified resolver?
|
|
183
|
+
|
|
184
|
+
- A. Keep the tier-2 fallback (recommended)
|
|
185
|
+
- B. Move to the unified resolver
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Good — same decision, stated in terms the human already owns:
|
|
189
|
+
|
|
190
|
+
```question
|
|
191
|
+
When you've read everything in a thread, should it stay in the "needs attention" group until you archive it, or drop out on its own?
|
|
192
|
+
|
|
193
|
+
- A. Drop out once it's read (recommended: keeps the group to threads that still need you)
|
|
194
|
+
- B. Stay until archived — nothing ever disappears without you acting on it
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
(That question line is deliberately unwrapped. Inside a fence a single newline is a HARD break —
|
|
198
|
+
`breaks: true` in the web markdown renderer — so hard-wrapping the sentence at your editor's column
|
|
199
|
+
renders a ragged break mid-question in the card. Keep the question, and each option, on one line.)
|
|
200
|
+
|
|
201
|
+
Test it before you send: read the block with your session forgotten, as if it were the only thing you
|
|
202
|
+
had ever seen about this work. Any word that only means something because of what you just did is a word
|
|
203
|
+
to reword.
|
|
204
|
+
|
|
205
|
+
## Never use the interactive question tool
|
|
206
|
+
|
|
207
|
+
`AskUserQuestion` (or any blocking prompt tool) would hang your session invisibly under the dashboard.
|
|
208
|
+
It is removed from your tool set at spawn; if you somehow reach it, a hook denies it. Use a `question`
|
|
209
|
+
fence.
|