From easy-cheese
Triages PR review comments, failing CI checks, and merge conflicts by grading each through ten quality dimensions. Useful when responding to feedback or fixing a build.
How this skill is triggered — by the user, by Claude, or both
Slash command
/easy-cheese:affinageThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill when the user wants to act on external claims about a PR — review comments from humans or bots, plus failing CI checks and merge conflicts — and wants those claims graded through the same lens `/age` uses for fresh review, then handed to `/cure` for application.
Use this skill when the user wants to act on external claims about a PR — review comments from humans or bots, plus failing CI checks and merge conflicts — and wants those claims graded through the same lens /age uses for fresh review, then handed to /cure for application.
/affinage always refines the claims that already exist on the PR (comments, CI failures, conflicts). Whether it also generates fresh /age findings depends on how it was reached:
/affinage <pr> directly, with no upstream handoff_context. The PR diff has not been reviewed in this session, so /affinage runs /age over it and folds the findings into the same report (unless --no-age)./cook or /cure with a handoff_context. /age already ran in that chain, so /affinage skips the fresh pass to avoid double-grading and only refines existing claims.See ## Fresh-window review for the detection rule and ## Merge-conflict resolution for the conflict path.
The metaphor: an affineur evaluates each wheel of cheese by sight / smell / sound and decides its fate. Here the wheels are review comments, CI failures, and merge conflicts.
/affinage [<pr-ref>] [--auto --stake <floor>] [--safe] [--open-pr] [--hard] [--full] [--include-outdated]
<pr-ref> accepts a PR number, a full GitHub PR URL, or nothing (auto-detect via gh pr view --json number on the current branch).
Flags:
--auto --stake <floor> — autonomous mode. <floor> is blocker, high, medium+, or all (same semantics as /cure). Skips the selection gate, dispatches /cure --auto --stake <floor>, posts all replies without prompting.--safe — re-introduce the gates the autonomous default skips: the cure-selection gate, the reply-only gate, and the merge-conflict confirmation. Use it when you want to choose before anything is fixed, replied to, or resolved.--open-pr — propagate to /cure so a clean cure may open a new PR when none exists (otherwise /cure only pushes an already-open one). For an affinage run the PR almost always already exists, so this flag rarely matters here.--hard — propagated metacognitive-gate flag. /affinage does not fire the gate; passes --hard forward to /cure at handoff.--full — un-collapses ## Low when ≥10 low-severity findings exist (mirrors /age --full).--include-outdated — include outdated review threads. Default skips them.--no-age — skip the standalone fresh /age pass. No effect when chained (the pass is already skipped). Use when you only want to triage existing comments, CI failures, and conflicts.Resolve PR. From <pr-ref> or gh pr view --json number on the current branch. Resolve <owner>/<repo> from the git remote.
Fetch PR status. Call python3 ${CLAUDE_SKILL_DIR}/scripts/affinage.pyz pr-status <pr>. The script returns JSON with build status, per-check failure summaries (last ~10 lines of failed logs + parsed failed-test names), and merge state. Map the exit code:
logs-expired) — the build is failing but every failing check's log was unfetchable (typically expired GitHub Actions logs past the retention window), so there is nothing to ground a CI finding on. Write status: halt: pr-status-logs-expired and stop with the hint: "CI is failing but the logs have expired — rerun the failed jobs (gh run rerun <run-id> --failed, where <run-id> is the /actions/runs/<id>/ segment of the failing check's url, or read it from gh pr checks) and re-invoke /affinage." Affineurs often run a few days after a PR opens, so this is routine, not an edge case.status: halt: pr-status-unavailable and stop.merge.mergeable is CONFLICTING or merge.state is DIRTY, the PR has unresolved conflicts. Resolve them before grading — see ## Merge-conflict resolution.Fresh-window review. If this is a standalone run and --no-age was not passed, run /age over the PR diff before grading and treat each finding as an additional input. See ## Fresh-window review.
Fetch comments.
gh api repos/<owner>/<repo>/pulls/<pr>/comments. This REST endpoint returns individual review comments without thread-level resolution state, so the skill cannot filter on isResolved from this surface; it skips comments whose position is null (the diff has moved past the anchored line) unless --include-outdated. For true unresolved-only filtering, switch to the GraphQL pullRequest.reviewThreads { isResolved } endpoint — documented as a future enhancement.gh api repos/<owner>/<repo>/pulls/<pr>/reviews. Filter to non-empty bodies. Dedupe against inline comments via pull_request_review_id.Skip already-replied threads. A thread whose most recent comment is from the resolved GitHub handle (env RESPOND_GH_HANDLE → gh api user --jq .login → git config user.name) has already been responded to; skip it. The same resolved handle is rendered in the reply footer as agent on behalf of <handle>. This keeps re-runs idempotent and makes RESPOND_GH_HANDLE the explicit footer knob.
Grade through the age lens. For each input (comment, CI/build failure, OR fresh /age finding):
skills/age/references/dimensions.md for the dimension rubric.build.status: failing checks from affinage.pyz pr-status and route them to /cure exactly like test failures. Tag CI-sourced items [from-check:<job>]./age findings (standalone runs) arrive already dimension-classified and severity-scored; fold them into the buckets below tagged [from-age:<dimension>]. Dedupe against comment-sourced items echoing the same defect — keep the comment-sourced one (it carries a reviewer to reply to)./age).CHANGES_REQUESTED as metadata (reviewer-asserted: line) but do not let it modify computed severity.## Blocker / ## High / ## Medium / ## Low) when the claim is grounded in the diff and its fix is contained (fix-cost-now: contained — roughly a few lines or a localized refactor). Every such item still maps to a dimension and carries a [<dimension>:<severity>] tag — a style or quality nit maps to deslop (e.g. [deslop:low]). The new rule is to route these grounded, contained-fix nits to /cure (usually as Low) instead of ## Reviewer-rejected, keeping the [from-comment:<id>] tag so /cure's reply still reaches the reviewer; a valid cheap nit is cheaper to fix than to argue, so do not push back on it.## Needs-investigation when the claim is plausible but requires evidence outside the diff (e.g., downstream caller in another repo).## Reviewer-rejected only when the claim is wrong or ungrounded (the code is already correct, the reviewer misread it, or there is no real improvement) OR is valid but a lot of follow-up work (fix-cost-now: moderate/sprawling or fix-cost-later: structural — a refactor or scope expansion beyond this PR). Reject the wrong ones; defer the expensive ones. Per skills/age/references/voice.md, a justified push-back costs more than a small valid fix.Write report to .cheese/affinage/pr-<n>.md with the four-line handoff slug at the top, then the age-format body plus the two extra sections. See ## Output below.
Act or ask. By default affinage acts — auto-applies the recommended set and posts the drafted replies — and asks only on a genuine reason (a sprawling/structural fix in the recommended set, conflicting findings) or under --safe. Branch on what graded out (full gate shapes in ## Handoff):
Blocker / High / Medium / Low). Compute the recommended composite (all-medium, cheap). With no reason to ask and no --safe: announce the selection in one line, first run step 9 to post any drafted push-backs / investigating notes (they don't depend on cure's outcome, so they must reach GitHub even if /cure later halts), then dispatch /cure <slug> with locked handoff_context: and post the cure-dependent replies (step 10) when /cure returns. On a reason to ask or --safe: render the cure-selection table inline using /cure's verbs (skills/cure/references/selection.md), pre-select the recommended composite (flagging heavy rows), ask via shared/handoff-gate.md, then proceed as above for the chosen selection; on none / Stop, run step 9 for the drafted replies and exit with the report path. If the recommended set is empty (only heavy/expensive items graded into severity sections), treat it as the reply-only branch below.Reviewer-rejected or Needs-investigation has items. Skip /cure dispatch entirely — there is nothing to apply. By default run step 9 to post all drafted replies (push-backs + investigating notes). Under --safe, render a small gate first that lets the user pick post all, post pushbacks only, skip posting, or per-finding choices. Exit with status: ok / next: done. This mirrors the documented auto-mode "no findings meet the floor" branch (see ### Auto mode).Post non-cure replies (runs whenever grading produced these items, with or without /cure). Post via python3 ${CLAUDE_SKILL_DIR}/scripts/affinage.pyz post-reply:
"Human investigating — will follow up."from-check:<job> tag) and fresh-review findings (from-age:<dimension> tag) → no reply (no reviewer to notify).Decoupling this from /cure is deliberate: drafted push-backs and investigating notes must reach GitHub even when no severity-section finding exists and /cure never runs — otherwise the drafted reply is write-only, useful to the human reading the report but invisible to the reviewer waiting on GitHub.
Post-cure reply posting (only when /cure ran). When /cure returns, read .cheese/cure/pr-<n>.md's ### Applied / ### Deferred sections and post per-finding replies via python3 ${CLAUDE_SKILL_DIR}/scripts/affinage.pyz post-reply:
from-comment:<id> tag) → "Fixed — <applied summary>."from-comment:<id> tag) → "Attempted fix reverted — <reason>."Detection — read the chain signal, not the flags:
handoff_context is in scope (the user invoked /affinage <pr> directly). The PR diff has not been reviewed this session.handoff_context with source_skill: /cook or /cure is in scope. /age already reviewed this diff upstream.Behaviour:
--no-age not passed): run /age <pr-ref> over the PR diff. Fold each returned finding into the affinage report's severity sections tagged [from-age:<dimension>]. They flow to /cure with every other selected finding and get no GitHub reply — there is no reviewer to notify, same as [from-check:…] items.--no-age: skip the pass. Re-running /age on an already-reviewed diff double-grades.Run the fresh /age before grading external claims so a comment that merely echoes an /age finding can be deduped. To keep the parent context lean, run the pass under the same sub-agent gate as grading (## Sub-agent context gate).
When affinage.pyz pr-status reports merge.mergeable: CONFLICTING or merge.state: DIRTY, the PR cannot merge until conflicts are resolved. /affinage does not resolve conflicts by hand — it routes to /melt, which runs the structural cascade (mergiraf → rerere → kdiff3).
gh pr checkout <pr>, then git merge origin/<base>. (gh pr checkout neither opens nor updates the PR, so it does not breach the no-/gh rule.)/melt. It first checks for squash-merge residue and stops with remedies if found — surface those verbatim and do not auto-apply./melt resolves cleanly, the resolution commit is owned by /melt / /cure. Pushing the merge follows /cure's push contract (push to the already-open PR after a clean cure).--auto mode: run the checkout + /melt automatically before dispatching /cure, then re-run affinage.pyz pr-status to confirm mergeable cleared. If /melt cannot resolve (manual kdiff3 needed, or squash residue), write status: halt: merge-conflicts-need-human and stop.--safe mode: gate the checkout + /melt behind the handoff prompt — offer "Resolve merge conflicts" alongside the cure-selection options./affinage keeps dialogue, selection, approval state, and reply posting in the parent context. Spawn a read-only grading sub-agent only when the parent context would balloon:
The sub-agent returns a digest: graded findings table with dimension, severity, grounded-evidence cite, and pre-drafted push-back text for any Reviewer-rejected items. The parent owns the report write, selection gate, /cure dispatch, and reply posting.
Digest size, parent-vs-sub-agent split, and harness-agnostic sub-agent selection live in skills/age/references/sub-agent-gate.md.
Code search and reading go through cheez-* skills (/cheez-search, /cheez-read). Beyond cheez-* there are affinage-specific tools:
| Need | Prefer | Fallback |
|---|---|---|
| PR status (build + merge) | ${CLAUDE_SKILL_DIR}/scripts/affinage.pyz pr-status | manual gh pr checks + gh pr view |
| GitHub fetch | gh api | none (skill halts) |
| Reply posting | ${CLAUDE_SKILL_DIR}/scripts/affinage.pyz post-reply | none — direct gh api calls bypass the agent on behalf of <handle> attribution |
| Diff inspection | delta | git diff --unified=3 |
Write to .cheese/affinage/pr-<n>.md with the four-line handoff slug at the top, then the age-style body with two extra sections:
status: ok | halt: <one-line reason>
next: cure | done
artifact: <path-to-prior-cure-or-press-report-if-any>
<one-line orientation: what the PR does and what was graded>
# Affinage Report — PR #<n>
## Orientation
<one or two factual sentences about the PR and what was graded>
## PR status
- Build: passing | failing (N jobs)
- Merge: clean | conflicts (resolved via /melt | needs human)
- Comments: K unresolved (M skipped as outdated)
- Fresh review: ran /age (N findings) | skipped (chained) | skipped (--no-age)
## Blocker
- **[from-comment:<id>] [security:blocker]** alice on `src/auth.ts:42` — token parsed without validation.
- location: contract · fix-cost-now: contained · fix-cost-later: structural
- reviewer-asserted: changes-requested
- recommendation: validate `authorization` header; reject with 401 on missing.
- **[from-check:test-suite] [correctness:blocker]** CI job `test-suite` — 3 tests failing in `tests/auth.test.ts`.
- location: contract · fix-cost-now: contained · fix-cost-later: structural
- recommendation: re-run after fixing the missing null check.
- **[from-check:build] [correctness:blocker]** CI job `build` — `tsc` fails: `src/auth.ts:42: 'token' is possibly undefined`.
- location: contract · fix-cost-now: contained · fix-cost-later: structural
- recommendation: narrow `token` before use; build is red until this compiles.
- **[from-age:efficiency] [efficiency:high]** fresh review — `src/api/users.ts:88` re-fetches the user inside the loop body.
- location: hot path · fix-cost-now: contained · fix-cost-later: contained
- recommendation: hoist the fetch above the loop.
## High
... (same shape)
## Medium
... (same shape)
## Low
- **[from-comment:<id>] [deslop:low]** copilot on `src/utils/format.ts:18` — rename `data` to `lineItems` for clarity.
- location: class · fix-cost-now: contained · fix-cost-later: contained
- recommendation: rename `data` → `lineItems`. Valid cheap nit — fixed via `/cure`, not pushed back.
... (same shape; collapsible per --full rules)
## Needs-investigation
- **[from-comment:<id>]** bob on `src/api/users.ts:108` — "might break analytics pipeline."
- reason: claim plausible but pipeline lives in a different repo; diff cannot confirm.
- suggested action: human reads `analytics-svc/consumers/users.ts`.
## Reviewer-rejected
- **[from-comment:<id>]** copilot on `src/auth.ts:30` — "missing `await`; this promise is unhandled."
- reason: wrong — `parseToken` is synchronous (returns `string`, not a `Promise`, see `src/auth.ts:12`); there is nothing to await.
- draft reply: "`parseToken` is synchronous here (returns `string`, `src/auth.ts:12`), so there's no promise to await. Leaving as-is."
- **[from-comment:<id>]** dana on `src/api/users.ts:60` — "extract this into a generic repository layer."
- reason: valid but large — fix-cost-now: sprawling (6 files across 2 slices); scope expansion beyond this PR.
- draft reply: "Agreed this would be cleaner, but it's a cross-slice refactor beyond this PR's scope — filing a follow-up rather than growing this change."
## Confidence
<certain | speculating | don't know> — <one-line justification>
## Next step
Auto-fixing the recommended set via `/cure` and posting the drafted replies (or, on a reason to ask / `--safe`, the selection prompt rendered inline — pick findings to cure or `none` to stop).
Empty severity sections are omitted entirely. ## Needs-investigation and ## Reviewer-rejected are omitted when no items land there.
status: ok when grading completed; status: halt: <reason> when gh or pr-status.py failed in a way that blocks honest grading. next: cure when at least one finding meets the medium+ floor (medium-or-above, or a cheap contained-fix low); next: done when none do.
Pipeline: culture → mold → cook → press → age → cure → ship · /affinage is parallel to /age and feeds the same /cure.
After the report lands, affinage acts by default and asks only on a genuine reason (a sprawling/structural fix in the recommended set, conflicting findings) or under --safe (Flow step 8). What it acts on depends on whether any severity-section finding exists.
When at least one severity-section finding exists (any severity, including Low) — compute the recommended composite (all-medium, cheap). With no reason to ask and no --safe: announce the one-line selection, post the drafted non-cure replies (Flow step 9), dispatch /cure (below), and post the cure-dependent replies (step 10) on return. On a reason to ask or --safe: render the cure-selection table inline (per skills/cure/references/selection.md) and ask via shared/handoff-gate.md, pre-selecting the recommended composite and flagging heavy rows. Lead with the recommended composite, then present the four severity-floor options below it, in the same most-inclusive-to-least order, so the gate is predictable across every run:
all-medium, cheap (floor at medium — blockers + high + medium — unioned with every Low whose fix-cost-now: contained; the small valid nits cheaper to fix than to defer). Sprawling/structural lows are left out.all (every finding regardless of severity).all-medium (floor at medium: blockers + high + medium — the severity-floor portion of the medium+ auto-floor; add cheap to also union the contained-fix lows, i.e. the recommended composite above).all-high.all-blocker.Then offer the non-floor options last:
/age//cure verbs (1,3,5, all-blocker, all-medium, all-high, cheap, all, none, skip N)./melt per ## Merge-conflict resolution, then re-render this gate.none.Present all four severity options on every run even when a severity band is empty: a floor that resolves to an empty set is a valid, predictable no-op — do not drop or reorder options based on which bands happen to be populated. If the user selects a floor (or the recommended composite) that resolves to an empty set, treat the selection as none: report that no findings match and do not dispatch /cure with empty resolved_ids (the non-empty-selection dispatch rule below still holds).
When no severity-section finding exists but Reviewer-rejected or Needs-investigation has items — /cure has nothing to act on, so skip it. By default, post all drafted replies (Flow step 9) and exit. Under --safe, render a reply-only gate first:
Reviewer-rejected drafts; skip Needs-investigation notices.On the selection (or the default post-all), post via Flow step 9 and exit with status: ok / next: done. This mirrors the documented auto-mode "no findings meet the floor" branch (see ### Auto mode).
On a non-empty cure selection (auto-selected by default or chosen at the gate), immediately dispatch /cure <slug> [--safe] [--open-pr] [--hard] with locked context:
handoff_context:
source_skill: /affinage
source_report: .cheese/affinage/pr-<n>.md
selection: "<verb or explicit ids>"
resolved_ids: [<expanded ids>]
/cure re-confirms cited ids and goes straight to apply. Propagate --safe, --open-pr, and --hard to /cure when in scope. /affinage resumes when /cure returns to post replies.
When invoked with --auto --stake <floor>:
/melt first (see ## Merge-conflict resolution). If /melt cannot resolve, halt with status: halt: merge-conflicts-need-human before any /cure dispatch.--no-age not passed), run the fresh /age pass so [from-age:…] findings join the floor-based auto-selection./age-sourced) that meets the floor — severity at or above the floor, plus cheap contained-fix lows when the floor is medium+ (same floor semantics as /cure)./cure --auto --stake <floor>./cure --auto and its downstream /age --scope --auto chain settle, post replies for the originally graded items only. Do NOT re-grade for findings discovered by /age --scope./affinage does not invoke /gh itself. The PR push is owned by /cure's terminal contract: the final cure pass pushes to the already-open PR (and, with --open-pr, may open a new one). Propagate --open-pr to /cure --auto when it is in scope.The whole cure chain (cure → /age --scope --auto → up to the two-cure-pass cap) must run in the parent affinage context so the post-cure reply step still has the original graded findings (slug, ids, from-comment:<id> tags, drafted push-back text) in memory. Same in-session-memory contract as /age --auto's two-pass cap. Spawning the cure chain in a sub-agent silently breaks reply posting — do not.
If no findings meet the floor, skip the /cure dispatch, post replies for Reviewer-rejected + Needs-investigation items only, and exit with status: ok / next: done / "no findings meet <floor>".
/affinage does not fire the /hard-cheese gate. It propagates --hard forward to /cure so the gate can fire at the share-for-review boundary inside /cure --hard. See skills/cure/SKILL.md --hard mode.
CHANGES_REQUESTED is metadata, not a severity bump.fix-cost-now: contained — a few lines or a localized refactor) goes to /cure as a Low finding tagged [from-comment:<id>]; do not draft a push-back for it. Reserve ## Reviewer-rejected for claims that are wrong/ungrounded or whose fix is a lot of work (moderate/sprawling/structural). See skills/age/references/voice.md./affinage itself. Code fixes go through /cure; merge conflicts go through /melt./age runs only on standalone invocations (no upstream handoff_context) and only when --no-age is absent. Chained runs never re-review the diff./melt, not by hand. gh pr checkout to materialise conflicts is allowed — it neither opens nor updates the PR. Pushing the resolved merge follows /cure's push contract (push to the already-open PR after a clean cure); --safe re-gates it.agent on behalf of <handle> attribution via ${CLAUDE_SKILL_DIR}/scripts/affinage.pyz post-reply, where <handle> is resolved from RESPOND_GH_HANDLE → gh api user --jq .login → git config user.name. Never call gh api directly to post./comments endpoint does not expose thread resolution, so honest idempotency relies on the latest-comment-from-self heuristic; switch to GraphQL reviewThreads if cross-session resolution-state visibility becomes required./affinage never invokes /gh itself. The PR push happens inside /cure after a clean cure (push to the already-open PR by default; --open-pr opens a new one; --safe re-gates).skills/age/references/voice.md): name confidence as certain | speculating | don't know; agree when no findings warrant grading.skills/age/SKILL.md — review pipeline, dimensions, sub-agent gate, report shape.skills/age/references/dimensions.md — per-dimension rubrics and severity computation.skills/cure/SKILL.md — apply pipeline, --auto --stake floors, handoff context shape.skills/cure/references/selection.md — selection verbs and composition.skills/melt/SKILL.md — merge-conflict resolution cascade (mergiraf → rerere → kdiff3).shared/handoff-gate.md — gate primitives.${CLAUDE_SKILL_DIR}/scripts/affinage.pyz post-reply — reply posting with agent on behalf of <handle> attribution.${CLAUDE_SKILL_DIR}/scripts/affinage.pyz pr-status — PR status fetcher.npx claudepluginhub paulnsorensen/easy-cheeseResolves GitHub PR issues including review comments, CI failures via triage-dispatch workflow with code edits, replies, and verification.
Interactively responds to PR review feedback: fetches comments, verifies findings, asks for user approval, makes changes, and posts replies. Use when addressing GitHub pull request reviews.
Loads GitHub PR review comments into the AI session for analysis, triage, and fix planning. Default is analysis-only; use --mode fix to enable auto-fixes.