|
| 1 | +--- |
| 2 | +name: review-autolearn |
| 3 | +description: | |
| 4 | + Mine human code-review comments from this repo, distill recurring catches into REVIEW.md |
| 5 | + rules so reviewers (and automated review) flag them before a human has to. Uses a |
| 6 | + multi-agent workflow (parallel clusterers + adversarial verify). Opens a PR with the |
| 7 | + proposed additions to the Recurring Catches section. |
| 8 | +argument-hint: "[dry] [<days>]" |
| 9 | +--- |
| 10 | + |
| 11 | +# /review-autolearn [dry] [<days>] |
| 12 | + |
| 13 | +Humans keep catching the same things in review. Read what they caught recently, turn the |
| 14 | +recurring ones into `REVIEW.md` guidance, PR it. |
| 15 | + |
| 16 | +## Arguments |
| 17 | + |
| 18 | +| Arg | Description | |
| 19 | +|-----|-------------| |
| 20 | +| (none) | 14-day window, open PR | |
| 21 | +| `dry` | Print proposed diff, no PR | |
| 22 | +| `<N>` | N-day lookback window | |
| 23 | + |
| 24 | +## Resolved variables |
| 25 | + |
| 26 | +``` |
| 27 | +REPO = $(gh repo view --json nameWithOwner --jq .nameWithOwner) |
| 28 | +TARGET = REVIEW.md |
| 29 | +``` |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## 1. Dedup — skip if already in flight |
| 34 | + |
| 35 | +```bash |
| 36 | +gh pr list --repo "$REPO" --state open --label review-autolearn --json number,url |
| 37 | +``` |
| 38 | + |
| 39 | +Open PR exists → report its link and stop (unless `dry`). |
| 40 | + |
| 41 | +## 2. Harvest |
| 42 | + |
| 43 | +Three GitHub comment surfaces feed the corpus. Fetch each, then derive four files. |
| 44 | + |
| 45 | +```bash |
| 46 | +since=$(date -u -d "${DAYS:-14} days ago" +%F 2>/dev/null || date -u -v-${DAYS:-14}d +%F) |
| 47 | +RAW=/tmp/review-autolearn-raw.jsonl |
| 48 | +HUMAN=/tmp/review-autolearn-human.jsonl |
| 49 | +BOTAGREE=/tmp/review-autolearn-botagree.jsonl |
| 50 | +BOTPUSHBACK=/tmp/review-autolearn-botpushback.jsonl |
| 51 | + |
| 52 | +# (a) Inline review comments — pinned to diff lines |
| 53 | +gh api "repos/$REPO/pulls/comments?since=${since}T00:00:00Z&sort=created&direction=desc" \ |
| 54 | + --paginate --jq '.[] | {kind:"inline", id, in_reply_to_id, |
| 55 | + pr:(.pull_request_url|split("/")|last), |
| 56 | + author:.user.login, path:.path, body:.body, diff:.diff_hunk}' \ |
| 57 | + > $RAW |
| 58 | + |
| 59 | +# (b) Review submission bodies — the text on APPROVE/REQUEST_CHANGES/COMMENT |
| 60 | +gh api "repos/$REPO/pulls?state=all&sort=updated&direction=desc&per_page=100" --paginate \ |
| 61 | + --jq ".[] | select(.updated_at >= \"${since}\") | .number" \ |
| 62 | + | while read pr; do |
| 63 | + gh api "repos/$REPO/pulls/$pr/reviews" \ |
| 64 | + --jq ".[] | select(.body != \"\") | select(.submitted_at >= \"${since}\") |
| 65 | + | {kind:\"review\", id, in_reply_to_id:null, pr:\"$pr\", |
| 66 | + author:.user.login, path:null, body:.body, diff:null}" |
| 67 | + done >> $RAW |
| 68 | + |
| 69 | +# (c) PR conversation comments — filtered to drop noise |
| 70 | +gh api "repos/$REPO/issues/comments?since=${since}T00:00:00Z&sort=created&direction=desc" \ |
| 71 | + --paginate --jq '.[] | select(.html_url | contains("/pull/")) |
| 72 | + | {kind:"convo", id, in_reply_to_id:null, |
| 73 | + pr:(.html_url|capture("/pull/(?<n>[0-9]+)").n), |
| 74 | + author:.user.login, path:null, body:.body, diff:null}' \ |
| 75 | + | jq -c 'select(.body | length > 40) |
| 76 | + | select(.body | test("^@claude\\b"; "i") | not) |
| 77 | + | select(.body | test("^(addressed|rebased|thanks|done|merged|fixed in|updated)\\b"; "i") | not)' \ |
| 78 | + >> $RAW |
| 79 | + |
| 80 | +# --- derive corpora --- |
| 81 | + |
| 82 | +# Human-authored, top-level (inline) + all review bodies + filtered convo |
| 83 | +jq -c 'select(.author // "" | endswith("[bot]") | not) |
| 84 | + | select(.kind != "inline" or .in_reply_to_id == null)' $RAW > $HUMAN |
| 85 | + |
| 86 | +# Human replies to bot inline comments, split by sentiment. |
| 87 | +# Agreements = bot catch a human validated → learn TO flag it. |
| 88 | +# Pushback = bot false positive → learn NOT to flag it. |
| 89 | +jq -s '(map(select(.kind=="inline")) | map({(.id|tostring): {author,body}}) | add) as $p |
| 90 | + | .[] |
| 91 | + | select(.kind=="inline" and .in_reply_to_id != null) |
| 92 | + | select(.author // "" | endswith("[bot]") | not) |
| 93 | + | ($p[.in_reply_to_id|tostring]) as $parent |
| 94 | + | select($parent.author // "" | endswith("[bot]")) |
| 95 | + | . + {parent_author:$parent.author, parent_body:$parent.body}' -c $RAW \ |
| 96 | + > /tmp/review-autolearn-botreplies.jsonl |
| 97 | + |
| 98 | +jq -c 'select(.body | test("^(fixed|amended|done|addressed|thanks|good catch|legitimate|updated|resolved|makes sense|agreed|fair)\\b"; "i"))' \ |
| 99 | + /tmp/review-autolearn-botreplies.jsonl > $BOTAGREE |
| 100 | +jq -c 'select(.body | test("^(fixed|amended|done|addressed|thanks|good catch|legitimate|updated|resolved|makes sense|agreed|fair)\\b"; "i") | not)' \ |
| 101 | + /tmp/review-autolearn-botreplies.jsonl > $BOTPUSHBACK |
| 102 | + |
| 103 | +wc -l $HUMAN $BOTAGREE $BOTPUSHBACK |
| 104 | +``` |
| 105 | + |
| 106 | +Zero in all → report "no review activity in window" and stop. |
| 107 | + |
| 108 | +## 3. Cluster + verify (Workflow) |
| 109 | + |
| 110 | +Launch the Workflow tool with this script. Pass `args = { humanFile, botAgreeFile, botPushbackFile, targetFile, repo }`. |
| 111 | + |
| 112 | +```js |
| 113 | +export const meta = { |
| 114 | + name: 'review-autolearn', |
| 115 | + description: 'Distill recurring review catches into REVIEW.md rules', |
| 116 | + phases: [ |
| 117 | + { title: 'Cluster', detail: '6 angles over the comment corpus' }, |
| 118 | + { title: 'Verify', detail: '3-vote adversarial check per candidate rule' }, |
| 119 | + { title: 'Synthesize' }, |
| 120 | + ], |
| 121 | +} |
| 122 | + |
| 123 | +const { humanFile, botAgreeFile, botPushbackFile, targetFile, repo } = args |
| 124 | +const ANGLES = [ |
| 125 | + { src: humanFile, focus: 'spec/protocol compliance (schema.ts mismatches, HTTP status codes, transport assumptions)' }, |
| 126 | + { src: humanFile, focus: 'API surface creep (unnecessary exports, speculative abstractions, parallel APIs)' }, |
| 127 | + { src: humanFile, focus: 'cross-SDK consistency (naming, patterns diverging from the other SDK)' }, |
| 128 | + { src: humanFile, focus: 'async/lifecycle correctness (race conditions, cleanup, cancellation, task lifecycle)' }, |
| 129 | + { src: botAgreeFile, focus: 'human-validated bot catches - each entry is a bot review comment a human accepted (parent_body is the bot finding, body is the human acknowledgment). Cluster by what the bot caught; produce rules so reviewers flag the same pattern' }, |
| 130 | + { src: botPushbackFile, focus: 'reviewer false positives - humans disagreeing with bot review comments (parent_body is the bot claim, body is the human rebuttal). Produce rules of the form "do NOT flag X; it is intentional/correct because Y"' }, |
| 131 | +] |
| 132 | +const RULE = { type:'object', required:['rule','section','prs'], |
| 133 | + properties:{ rule:{type:'string'}, section:{type:'string'}, |
| 134 | + prs:{type:'array',items:{type:'string'}} } } |
| 135 | +const RULES = { type:'object', required:['rules'], properties:{ rules:{type:'array',items:RULE} } } |
| 136 | +const VERDICT = { type:'object', required:['keep','reason'], |
| 137 | + properties:{ keep:{type:'boolean'}, reason:{type:'string'} } } |
| 138 | + |
| 139 | +const verified = await pipeline( |
| 140 | + ANGLES, |
| 141 | + a => agent( |
| 142 | + `Read ${a.src} (one JSON/line: fields include pr,author,path,body,diff,kind and for bot-reply files also parent_author,parent_body). `+ |
| 143 | + `Focusing ONLY on ${a.focus}: drop praise/LGTM/nit/question-only; group remaining comments by underlying pattern. `+ |
| 144 | + `A cluster qualifies iff >=2 distinct PRs AND mechanically detectable from a diff AND not already covered in ${targetFile} (grep it). `+ |
| 145 | + `Return rules[] - each <=4 lines, imperative, why-in-one-clause, cite PR#s, name the REVIEW.md section to append under. Zero is valid.`, |
| 146 | + { phase:'Cluster', label:`cluster:${a.focus.split(' ')[0]}`, schema: RULES }).then(out => ({...out, src: a.src})), |
| 147 | + out => parallel((out?.rules ?? []).map(r => () => |
| 148 | + parallel(Array.from({length:3}, () => () => agent( |
| 149 | + `Adversarially verify this proposed REVIEW.md rule for ${repo}:\n${JSON.stringify(r)}\n`+ |
| 150 | + `REFUTE (keep=false) if: <2 distinct PRs in ${out.src} actually back it, OR ${targetFile} already covers it, `+ |
| 151 | + `OR it isn't mechanically detectable from a diff, OR it's too vague to act on. Default to refute when unsure.`, |
| 152 | + { phase:'Verify', label:`verify:${r.section}`, schema: VERDICT }))) |
| 153 | + .then(votes => ({ ...r, keep: votes.filter(v => v?.keep).length >= 2 })) |
| 154 | + )), |
| 155 | +) |
| 156 | +const kept = verified.flat().filter(r => r?.keep) |
| 157 | +if (!kept.length) return { diff: null, summary: 'nothing recurring' } |
| 158 | + |
| 159 | +phase('Synthesize') |
| 160 | +const diff = await agent( |
| 161 | + `Produce a minimal patch for ${targetFile} appending these verified rules under the "## Recurring Catches" section. `+ |
| 162 | + `Slot each rule under its "### ${'{'}section${'}'}" subsection within Recurring Catches (create the subsection if absent). `+ |
| 163 | + `Do not modify Guiding Principles, Review Ordering, or Checklist sections. Dedupe overlaps with existing Recurring Catches entries. `+ |
| 164 | + `Return a unified diff only.\nRules:\n${JSON.stringify(kept,null,2)}`, |
| 165 | + { label:'synthesize' }) |
| 166 | +return { diff, rules: kept } |
| 167 | +``` |
| 168 | +
|
| 169 | +## 4. Ship |
| 170 | +
|
| 171 | +`dry` → print `result.diff` + backing PR#s per rule, stop. |
| 172 | +
|
| 173 | +Otherwise: |
| 174 | +
|
| 175 | +``` |
| 176 | +branch: review-autolearn/<date> |
| 177 | +- gh label create review-autolearn --repo "$REPO" -c FBCA04 -d "Auto-mined review conventions" 2>/dev/null || true |
| 178 | +- git checkout -b <branch> |
| 179 | +- Apply result.diff to REVIEW.md |
| 180 | +- Commit, push, open PR: |
| 181 | + title: "docs(REVIEW.md): encode review catches from last <N>d" |
| 182 | + label: review-autolearn |
| 183 | + body: one bullet per rule → linking the backing PR comments |
| 184 | +``` |
| 185 | +
|
| 186 | +Do NOT auto-merge — a human signs off on guidance changes. |
| 187 | +
|
| 188 | +## 5. Report |
| 189 | +
|
| 190 | +`<N rules | nothing recurring> · <PR link | dry>` |
0 commit comments