Skip to content

review: turn on the review-feedback signal (thumbs-sweep GitHub port + live-counters report)#225

Merged
jwbron merged 12 commits into
mainfrom
review-feedback-signal
Jul 13, 2026
Merged

review: turn on the review-feedback signal (thumbs-sweep GitHub port + live-counters report)#225
jwbron merged 12 commits into
mainfrom
review-feedback-signal

Conversation

@jwbron

@jwbron jwbron commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Was stacked on #223 (thumbs-sweep seeding + self-reaction filtering); #223 has landed and main is merged in.

Implements the Khan/actions side of the design doc's "The tuning loop" -> "Signal to turn on": the production GitHub wiring for the thumbs sweep and the live counters. Consumer-repo PRs (webapp/frontend scheduled workflows, frontend ROUTING) follow separately and pin the release this ships in.

Thumbs sweep

  • lib/thumbs-sweep-github.ts — the octokit-backed ThumbsSweepPort. Reviewer comments are identified per grain by the workflow's markers, not by author alone (github-actions[bot] authors many workflows' comments):
    • summary: issue comments carrying the pr-reviewer:risks-and-patterns marker or gh-aw's engine-emitted gh-aw-workflow-call-id: <owner>/<repo>/review marker. Verified against live comments: production summary comments carry only the latter today.
    • inline: review comments whose body starts with one of the code-owned Conventional-Comment label prefixes (built from render-comment.ts's taxonomy). Inline comments carry no hidden marker at current releases; the label grammar is code-owned and templated, so the match is exact.
    • Reactions are listed per comment with reactor logins, so the self-reaction filtering excludes the bot's own reactions (the post-time seeded nudges, once those exist) from all signal. Live tallies use the shared positive/negative sets (👍/❤️/🎉/🚀 vs 👎/😕, matching gh-aw's outcome evaluation).
    • Resolved inline threads are counted (one GraphQL reviewThreads query per PR that has reviewer inline comments) and reported as their own positive column; resolution never triggers a follow-up.
    • Bounded: PRs updated in the last 14 days (configurable), capped at 200, skipping PRs closed/merged more than 3 days ago (REVIEW_SWEEP_CLOSED_GRACE_DAYS; feedback lands around merge time, after which a landed PR stops changing). Reaction detail is fetched only when a comment has any reactions. Every API request is counted and reported.
  • lib/run-thumbs-sweep.ts — env-configured CLI for the consumer workflows (GITHUB_TOKEN + pull-requests: write is all it needs). Writes the SweepResult JSON to stdout and an auditable Markdown digest to the job summary. REVIEW_SWEEP_DRY_RUN=true traverses and decides without posting, for first-run audits.
  • The review package gains its first pinned runtime dependency, octokit (throttling/retry built in), with the transitive tree locked by a committed package-lock.json. Consumer sweep workflows run npm ci --omit=dev in the checked-out workflows/review/; all other lib scripts remain dependency-free. Loaded via dynamic import() because the package is ESM-only and tsx runs the lib as CJS.

Live counters

  • lib/counters-report.ts — aggregates a directory of downloaded per-run artifacts into the weekly counters job summary via counters.ts. Where summary.json doesn't exist (all current production runs), it synthesizes the run summary from gh-aw engine artifacts every run already uploads: safeoutputs.jsonl (verdict event), safe-output-items.jsonl (comments actually posted), agent_usage.json (cost/tokens). Artifact files are located by basename at any depth, tolerating the known gh-aw staging-path nesting.
  • lib/counters.ts — normalization extended to the artifact shapes production actually emits: validator files keyed {"claims": [...]} with three-state verification (confirmed/plausible fold to keep, refuted to drop), and an opt-in fallback source ((unknown)) for decisions that can't be joined to claims.json (which production runs don't upload). Default join behavior is unchanged.

Changes from review feedback

  • Sweep-time reaction seeding is removed (the core's seedReactions, the port's addReactions, REVIEW_SWEEP_SEED_REACTIONS). The nudge pair will instead be seeded at post time by a custom safe-output job in the consumer repos' review workflow (follow-up PR); the sweep's self-reaction filtering stays, keeping those seeds out of all signal.
  • Resolved threads count as success: counted per PR via GraphQL, reported as a separate positive column (threads also get resolved just to clear noise, so resolutions aren't folded into 👍).
  • Wider reaction sets in the live tally: the reported numbers now use the same 👍/❤️/🎉/🚀 vs 👎/😕 sets the follow-up logic already acted on.
  • Closed/merged PRs age out: swept for a 3-day grace window after closing (captures the around-merge feedback tail), then skipped; keeps the sweep stateless.
  • README section positioning the sweep vs the gh-aw outcome-collector (passive Sentry telemetry vs active reason elicitation), including the documented add_comment inflation once post-time seeding exists.

Verification

  • 750 lib tests pass (post-merge with main; includes a new renderSweepSummary unit test), typecheck and lint clean.
  • Live dry-run against both consumer repos (3-day window, 15 PRs each): Khan/frontend identified 72 inline + 24 summary reviewer comments, Khan/webapp 14 + 2, 31 API requests each, zero false matches spot-checked; write paths exercised end-to-end in tests against a fake GitHub. (Dry-run predates the resolved-thread/grace-window changes; the traversal shape is unchanged.)

No review semantics change anywhere; the reviewer's prompt and workflow are untouched.

cc @jeresig

Human steps (non-review)

jwbron added 3 commits July 8, 2026 16:56
The production wiring for the review-feedback signal (design doc, 'The
tuning loop' -> 'Signal to turn on'):

- thumbs-sweep-github.ts: the octokit-backed ThumbsSweepPort. Reviewer
  comments are identified per grain (risks/patterns marker for summary
  comments; the code-owned Conventional-Comment label prefixes for
  inline ones, which carry no hidden marker at current releases), with
  per-reactor logins so seeded reactions never count as signal.
  Traversal is bounded to PRs updated in the last 14 days (capped) and
  every API request is counted for the auditable job summary.
- run-thumbs-sweep.ts: env-configured CLI for the consumer repos'
  scheduled sweep workflows; writes the SweepResult JSON to stdout and
  a Markdown digest to GITHUB_STEP_SUMMARY.
- counters-report.ts: aggregates downloaded per-run artifacts into the
  weekly live-counters job summary, synthesizing the run summary from
  gh-aw engine artifacts (safeoutputs.jsonl, safe-output-items.jsonl,
  agent_usage.json) when summary.json is absent, tolerating the known
  staging-path nesting.
- counters.ts: accept the production validator artifact shape
  ({"claims": [...]} with three-state verification) and an optional
  fallback source for unjoinable decisions.
- The review package gains its first pinned runtime dependency
  (octokit); consumer sweep workflows npm-install it in the checkout.

No review semantics change.
Verified against live data on both consumer repos (dry-run): production
summary comments carry gh-aw's engine-emitted workflow-call-id marker
but not the pr-reviewer risks/patterns marker, so the port accepts
either (workflow ids configurable via REVIEW_SWEEP_WORKFLOW_IDS,
default 'review'). REVIEW_SWEEP_DRY_RUN=true traverses and decides
without posting or seeding, for first-run audits. octokit is ESM-only
while tsx runs these scripts as CJS, so the CLI loads it with a
dynamic import.
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cd6e961

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
review Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@khan-actions-bot khan-actions-bot requested review from a team, kevinb-khan and somewhatabstract and removed request for a team July 9, 2026 00:27
@khan-actions-bot khan-actions-bot requested a review from a team July 9, 2026 00:42
@jwbron jwbron requested a review from jeresig July 9, 2026 04:28
@jwbron jwbron self-assigned this Jul 9, 2026

@jeresig jeresig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is running counter to what we already have going with the gh-outcome-collector logic. I think we want to be using that instead of this. Or, this needs much better documentation as right now it's really not clear how this is being run and when and how the data is being used (or not). Like, is this being integrated into our existing OTel data?

If we are using this we need to integrate the other ways that we're determining success (such as a resolved thread = success, and also other reactions equaling success/rejection). I raised this in your other PRs already.

@jeresig jeresig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now that I see how this is being used, a number of changes requested:

  • Resolved threads should be counted as success.
  • Closed or merged PRs should only be analyzed once (they aren't going to be changing once they've landed)
  • We should take into account other affirmative/negative reactions (I raised this in another PR)

Question on leaving the default thumbs-up/down on the review comment - are you doing that here as it's not possible to do in the gh-aw safe outputs?

@jwbron jwbron changed the base branch from review-thumbs-seeding to main July 9, 2026 17:08
An error occurred while trying to automatically change base from review-thumbs-seeding to main July 9, 2026 17:08
@jwbron

jwbron commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, responding to both reviews together.

Outcome-collector overlap: agreed it needs explicit positioning. The sweep isn't replaceable by the collector: the collector never writes to GitHub, so it can't ask why a comment was downvoted, and those reason labels (incorrect / unimportant / unclear / duplicate) are what calibrate the eval judge. I'll add a README section spelling out the division: collector = passive acceptance telemetry to Sentry; sweep = active reason elicitation, results in the job summary + stdout JSON (not otel today; I can export the tallies over the same OTLP endpoint review.md uses if you want that).

Resolved threads = success: agreed. Matches the gh-aw spec; notably the JS evaluator we run doesn't implement it yet either (only the Go CLI has the dedicated evaluator). I'll add a GraphQL reviewThreads pass and report resolutions as their own positive column rather than folding them into 👍, since threads also get resolved just to clear noise.

Closed/merged analyzed once: partly agreed. The sweep is deliberately stateless, and feedback often lands right around merge, so I'd rather skip PRs closed more than ~3 days ago than track analyzed-once state. Same effect at current scale (~31 API requests/run) without a state store.

Other reactions: agreed, half done. The core sets already match the collector's (👍 ❤️ 🎉 🚀 / 👎 😕) and the follow-up already triggers on 😕; the live tally only counts literal +1/-1, which I'll fix to use the shared sets.

Seeding question: close. gh-aw can't do it natively (the comment IDs don't exist until the handlers post them, and GITHUB_TOKEN-posted comments don't emit events, so an event-triggered seeder never fires). But a custom safe-output job (safe-outputs.jobs with needs: safe_outputs) can react seconds after posting by reading the handler's safe-output-items record. So I'll drop REVIEW_SWEEP_SEED_REACTIONS from this PR and do post-time seeding in the consumer review.md instead.

One interaction to flag: seeded pairs make the collector read every review summary comment as accepted (its add_comment evaluator counts any reaction, identity-blind). The damage is bounded to that one metric (inline comments use an existence fallback in JS; submit_pull_request_review is scored by commits/merge state), and the sweep measures reviewer-comment engagement strictly better (identity-filtered, both grains, reason labels). Plan: seed anyway, document that the collector's add_comment result for the review workflow is inflated by design with the sweep authoritative there, and file an upstream gh-aw issue for identity-aware reaction counting (their spec already prefers bot-identity provenance). Shout if you'd rather hold seeding until upstream fixes it.

So for this PR: drop sweep-time seeding, count resolved threads, widen the reaction tally, closed-PR grace window, README section on the two systems. Follow-ups: post-time seeding in the consumer repos, upstream gh-aw issue. Sound right?

jwbron added 2 commits July 9, 2026 12:04
…eding, count resolved threads, widen reaction tally, add closed-PR grace window
@jwbron

jwbron commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Changes from the review are in (7c601d3): sweep-time seeding removed (post-time seeding follows in the consumer repos), resolved inline threads counted via GraphQL as their own positive column, the live tally widened to the shared reaction sets (👍/❤️/🎉/🚀 vs 👎/😕), closed/merged PRs age out after a 3-day grace window, and the README now has a section positioning the sweep against the outcome-collector (including the add_comment inflation caveat). PR description updated to match. 298 lib tests pass, typecheck and lint clean.

@jeresig jeresig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great - thanks for the fixes!

@github-actions

This comment has been minimized.

Comment thread workflows/review/lib/run-thumbs-sweep.ts
Comment thread workflows/review/package.json
const closedAt = pull["closed_at"];
if (
pull["state"] === "closed" &&
typeof closedAt === "string" &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note (non-blocking): The closed-PR age-out only fires when closed_at is a string; a closed PR whose payload lacks closed_at falls through and is swept every run while inside the lookback window. GitHub always populates closed_at for closed PRs, so this is defensive-only — at worst a few extra API calls, with no correctness or idempotency impact.

Comment thread workflows/review/lib/counters-report.ts
for (const pullNumber of pullNumbers) {
this.pullsScanned += 1;

const inline = await this.paginate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): The per-PR traversal is ~1 + 2·N requests (with the 200-PR cap, 400+ per sweep) against the shared per-repo GITHUB_TOKEN budget. GitHub's repo-wide GET /repos/{owner}/{repo}/pulls/comments and .../issues/comments (both support since/sort) list comments in ~(comments/100) requests, and each comment carries pull_request_url/issue_url so the comment→PR map survives; closed-grace filtering would then only need a lazy per-PR fetch for PRs that actually have candidate comments. Not required, but it would cut the sweep's API footprint substantially on busy repos.

jwbron added 2 commits July 13, 2026 10:54
…nto review-feedback-signal

# Conflicts:
#	pnpm-lock.yaml
…ack (lockfile, summary test, verdict guard)

- Commit workflows/review/package-lock.json so the exact octokit pin also
  locks the transitive tree; consumer sweep workflows install with
  npm ci --omit=dev (docstring + README updated).
- Add run-thumbs-sweep.test.ts covering renderSweepSummary (normal vs
  dry-run header, already-followed-up count, conditional downvoted table),
  making the run-on-import guard comment true.
- Prefer REQUEST_CHANGES when a malformed artifact carries multiple review
  submissions in counters-report's synthesized verdict.
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review live A/B

No reviewable delta: review.md is byte-identical in both arms (baseline origin/main, sha 00f75b3dbf3a), so the extracted prompts and the orchestrator body match and no arms were run. Pass --force-arms for a deliberate wobble control.

@jwbron

jwbron commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Merged main (pnpm-lock regenerated; octokit + main's claude-agent-sdk coexist) and addressed the bot's non-blocking feedback in ab0b556:

  • Lockfile: workflows/review/package-lock.json is now committed (generated in isolation, registry-resolved), so the exact octokit pin also locks the transitive tree. Docstring and README updated to npm ci --omit=dev; the consumer workflows (webapp#40711) will use npm ci too.
  • renderSweepSummary test: added run-thumbs-sweep.test.ts covering the dry-run vs normal header, the already-followed-up count, and the conditional downvoted table; the "tests import renderSweepSummary" comment is now true.
  • Verdict precedence: the synthesized verdict now prefers REQUEST_CHANGES if a malformed artifact carries multiple review submissions, with a test.

Skipped two:

  • The closed_at fallthrough is defensive-only (GitHub always populates it for closed PRs); worst case is a few extra reads.
  • The repo-wide comment-listing traversal is a real efficiency idea but a redesign of the traversal shape post-approval; noting it as a follow-up candidate if sweep API usage ever matters (today's sweeps run ~31 requests).

750 lib tests pass, typecheck and lint clean.

@github-actions

This comment has been minimized.

}
const value = Number.parseInt(raw, 10);
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`${name} must be a positive integer, got: ${raw}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note (non-blocking): intEnv's positive-integer validation has no test coverage — run-thumbs-sweep.test.ts imports only renderSweepSummary. A regression that let a bad REVIEW_SWEEP_* value (0 or non-numeric) coerce past this guard would ship an unbounded or empty sweep window unnoticed. Exporting and unit-testing intEnv would cheaply lock in the fail-loud path this code intends.

Low-confidence note (1) — surfaced for completeness, not blocking
  • workflows/review/lib/counters-report.ts:139 — Add a source pointer for the 1 ai_credit = $0.01 conversion. It is corroborated in-repo (review.md:231-235 documents gh-aw's defaults as 1000/run $10, 5000/day $50, i.e. $0.01/credit), but that is inferred from the cap config rather than a direct gh-aw source that defines the unit; a one-line pointer to gh-aw's agent_usage.json definition would pin it.

…nto review-feedback-signal

# Conflicts:
#	workflows/review/package.json
@github-actions

Copy link
Copy Markdown
Contributor

Review Guidance

github-actions (7 files)
File Reason
counters-report.ts Aggregates per-run artifacts into the weekly counters report and synthesizes run summaries from gh-aw engine artifacts; a parsing/shape regression silently skews the tuning metrics.
counters.ts Normalization extended to the production validator/artifact shapes; a fold error would mis-join or drop decisions in the feedback signal.
run-thumbs-sweep.ts CLI entrypoint the consumer sweep workflows run; env parsing and the dry-run write-suppression wrapper gate whether live GitHub writes happen.
thumbs-sweep-github.ts The octokit-backed port that performs all GitHub reads/writes for the sweep; grain-matching and self-reaction filtering determine what counts as signal.
thumbs-sweep.ts Sweep core; the reaction-seeding removal changes the port interface and result shape consumed elsewhere.
package.json Adds octokit as a dependency (dependency widening).
package.json Adds the octokit runtime dependency and a committed lockfile the consumer npm ci --omit=dev relies on.

Common patterns

2 files: Removed reaction-seeding (seedReactions / addReactions / seeded) from the sweep core and its tests — seeding moves to post-time in the consumer repos.

2 files: Added octokit as a dependency in both the root package.json (devDep, ^5.0.5) and the review sub-package package.json (runtime, exact 5.0.5).

Excluded from review (4 files)

Not individually reviewed — generated, formatting-only, or fully explained by a common pattern above:

  • pnpm-lock.yaml — generated
  • workflows/review/package-lock.json — generated
  • .changeset/review-feedback-signal.md — formatting-only
  • workflows/review/README.md — formatting-only

"version": "1.5.0"
"version": "1.5.0",
"dependencies": {
"octokit": "5.0.5"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): The port consumes only octokit.request and hand-rolls pagination, so the octokit meta-package pulls in @octokit/app, @octokit/oauth-app, @octokit/webhooks, and @types/aws-lambda that consumer sweeps npm ci and audit but never execute (all four are in the committed lockfile). @octokit/core + @octokit/plugin-retry + @octokit/plugin-throttling gives the same throttling/retry behavior with roughly half the transitive tree. Non-blocking.

@jwbron jwbron merged commit 55523e4 into main Jul 13, 2026
10 checks passed
@jwbron jwbron deleted the review-feedback-signal branch July 13, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants