Skip to content

/affinage

Skill metadata

When to invoke: Triage a PR's review comments and failing CI (plus merge conflicts) through the /age lens, deciding which claims are worth acting on. Use when the user says "respond to PR comments", "handle review feedback", "affinage the PR", "/affinage ", "fix the failing build", "resolve the conflicts and respond". Fetches inline + review-body comments via gh and CI failures via affinage.pyz pr-status, grades each through the ten age dimensions, and writes a report at .cheese/affinage/pr-<n>.md. Resolves merge conflicts via /melt and routes build/CI failures to fixes through /cure. When invoked standalone (no upstream handoff_context) it also runs /age over the PR diff and folds fresh findings in; when chained, it skips that pass. By default it auto-applies the recommended set via /cure and posts the drafted replies, gating only on sprawling/structural fixes or conflicts; --safe re-gates. Supports --auto --stake <floor>. Before /cure; parallel to /age.


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:

  • Standalone โ€” the user typed /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).
  • Chained โ€” reached from /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.

Inputs

/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.

Flow

  1. Resolve PR. From <pr-ref> or gh pr view --json number on the current branch. Resolve <owner>/<repo> from the git remote.
  2. 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:
  3. Exit 0 โ€” proceed with grading.
  4. Exit 3 (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.
  5. Any other non-zero (1 PR/gh API error, 2 missing gh binary) โ€” write status: halt: pr-status-unavailable and stop.
  6. Merge conflicts. If merge.mergeable is CONFLICTING or merge.state is DIRTY, the PR has unresolved conflicts. Resolve them before grading โ€” see ## Merge-conflict resolution.
  7. 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.
  8. Fetch comments.
  9. Inline threads: 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.
  10. Review bodies: gh api repos/<owner>/<repo>/pulls/<pr>/reviews. Filter to non-empty bodies. Dedupe against inline comments via pull_request_review_id.
  11. 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.
  12. Grade through the age lens. For each input (comment, CI/build failure, OR fresh /age finding):
  13. Classify dimension from the code + claim (or check type + failure summary, for CI items). See skills/age/references/dimensions.md for the dimension rubric.
  14. Build failures count, not just test failures. A failing check is a finding whether the failure is a compile error, a lint/type-check failure, or a failing test โ€” grade the 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>].
  15. Fresh /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).
  16. Compute severity from base + location + compounding modifiers (same rubric as /age).
  17. Ignore reviewer-asserted urgency for severity computation. Surface CHANGES_REQUESTED as metadata (reviewer-asserted: line) but do not let it modify computed severity.
  18. Bucket into:
    • Standard severity sections (## 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.
  19. 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.
  20. 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):
  21. At least one finding in a severity section (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.
  22. No severity-section findings, but 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).
  23. Nothing graded into any section. Exit cleanly with the report path; there is nothing to post or cure.
  24. 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:
  25. Reviewer-rejected items โ†’ post the pre-drafted push-back text from the affinage report.
  26. Needs-investigation items โ†’ post "Human investigating โ€” will follow up."
  27. CI-sourced findings (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. 10. 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: - Applied (with from-comment:<id> tag) โ†’ "Fixed โ€” <applied summary>." - Deferred (with from-comment:<id> tag) โ†’ "Attempted fix reverted โ€” <reason>."

Fresh-window review

Detection โ€” read the chain signal, not the flags:

  • Standalone: no upstream handoff_context is in scope (the user invoked /affinage <pr> directly). The PR diff has not been reviewed this session.
  • Chained: a handoff_context with source_skill: /cook or /cure is in scope. /age already reviewed this diff upstream.

Behaviour:

  • Standalone (and --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.
  • Chained, or --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).

Merge-conflict resolution

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).

  1. Materialise the conflicts locally: 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.)
  2. Hand off to /melt. It first checks for squash-merge residue and stops with remedies if found โ€” surface those verbatim and do not auto-apply.
  3. After /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).

  4. Default and --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.

  5. --safe mode: gate the checkout + /melt behind the handoff prompt โ€” offer "Resolve merge conflicts" alongside the cure-selection options.

Sub-agent context gate

/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:

  • Total input count (comments + CI failures) exceeds 10.
  • Diff exceeds ~25 KB.
  • Threads span more than 5 files.

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.

Preferred tools and fallbacks

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

Output

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.

Handoff

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:

  • Fix mediums-and-above plus cheap lows (recommended) โ€” equivalent to 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.
  • Fix everything โ€” equivalent to all (every finding regardless of severity).
  • Fix medium-severity and above โ€” equivalent to 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).
  • Fix high-severity and blockers โ€” equivalent to all-high.
  • Fix blockers only (strict) โ€” equivalent to all-blocker.

Then offer the non-floor options last:

  • Pick findings to fix โ€” free-text reply using /age//cure verbs (1,3,5, all-blocker, all-medium, all-high, cheap, all, none, skip N).
  • Resolve merge conflicts (offered only when the PR has conflicts) โ€” checkout + /melt per ## Merge-conflict resolution, then re-render this gate.
  • Stop โ€” leave the report for later โ€” equivalent to 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:

  • Post all (recommended) โ€” post every drafted push-back and human-investigating note.
  • Post pushbacks only โ€” post Reviewer-rejected drafts; skip Needs-investigation notices.
  • Skip posting โ€” leave the report for later; post nothing.
  • Per-finding โ€” free-text pick of which drafts to post.

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.

Auto mode

When invoked with --auto --stake <floor>:

  • Skip the selection gate.
  • If the PR has merge conflicts, resolve them via /melt first (see ## Merge-conflict resolution). If /melt cannot resolve, halt with status: halt: merge-conflicts-need-human before any /cure dispatch.
  • If standalone (and --no-age not passed), run the fresh /age pass so [from-age:โ€ฆ] findings join the floor-based auto-selection.
  • Auto-select every finding (comment-sourced, CI-sourced, OR fresh-/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).
  • Dispatch /cure --auto --stake <floor>.
  • After /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.
  • Reviewer-rejected items: post the pre-drafted push-back.
  • Needs-investigation items: post the human-investigating reply.
  • /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>".

--hard mode

/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.

Rules

  • Grading is code-grounded, not reviewer-asserted. CHANGES_REQUESTED is metadata, not a severity bump.
  • Prefer fixing over pushing back. A valid, grounded nit whose fix is contained (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.
  • Never auto-apply fixes from /affinage itself. Code fixes go through /cure; merge conflicts go through /melt.
  • Fresh /age runs only on standalone invocations (no upstream handoff_context) and only when --no-age is absent. Chained runs never re-review the diff.
  • Merge conflicts are resolved through /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.
  • Every posted reply ends with the literal 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.
  • Idempotent re-runs: skip threads where the latest comment is from the resolved handle. The REST /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.
  • CI-sourced findings get no reply (no reviewer to notify).
  • /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).
  • Apply the shared voice kernel (skills/age/references/voice.md): name confidence as certain | speculating | don't know; agree when no findings warrant grading.

References

  • 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.