affinage · diff

git:20260728.273585a to git:20260729.c7b8b8e

42 added, 195 removed. Audit A to A.

---
name: affinage
description: 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 <pr>", "fix the failing build", "resolve the conflicts and respond". Do NOT use for a bare diff with no PR (route to /age).
license: MIT
metadata: {dispatches-agents: true}
---
# /affinage
- 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.
+ Act on external claims about a PR — review comments from humans or bots, plus failing CI checks and merge conflicts — grading them through the same lens `/age` uses for fresh review, then handing them 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.
## Inputs
```text
/affinage [<pr-ref>] [--auto --stake <floor>] [--plate] [--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` — also gate the cure-selection and merge-conflict-resolution steps (which otherwise run autonomously by default). Reply posting is **gated by default regardless of this flag** — only `--auto` posts without prompting. Use `--safe` when you also want to choose before anything is fixed or a conflict is resolved.
- - `--open-pr` — allow affinage's terminal `/plate` to open a *new* PR when none exists (otherwise plate only updates the already-open one).
- - `--plate` — one-shot publish combo, equivalent to `--auto --stake medium+ --open-pr`: autonomously triage, cure the recommended floor (medium-and-above plus cheap contained-fix lows), post every reply, then plate. Because it carries `--auto`, replies post without prompting. An explicit `--stake <floor>` overrides the `medium+` default.
- - `--hard` — propagated metacognitive-gate flag. `/affinage` does not fire the gate; passes `--hard` forward to its terminal `/plate` at publication.
+ - `--auto --stake <floor>` — autonomous mode; `<floor>` (`blocker`, `high`, `medium+`, `all`) matches `/cure`'s semantics. Skips selection, dispatches `/cure --auto --stake <floor>`, posts replies without prompting. Mechanics: `references/auto-mode.md`.
+ - `--safe` — also gates cure-selection and merge-conflict resolution (autonomous by default). Reply posting is **gated by default regardless** — only `--auto` skips it.
+ - `--open-pr` — let terminal `/plate` open a *new* PR when none exists (else it only updates the open one).
+ - `--plate` — one-shot publish combo = `--auto --stake medium+ --open-pr`: triage, cure the recommended floor, post every reply, then plate. An explicit `--stake <floor>` overrides `medium+`.
+ - `--hard` — propagated metacognitive-gate flag; forwarded to terminal `/plate`, not fired here.
- `--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.
+ - `--include-outdated` — include outdated review threads (default: skip).
+ - `--no-age` — skip the standalone fresh `/age` pass; no effect when chained.
Portability reference: [`../cheese/references/harness-portability.md`](../cheese/references/harness-portability.md). It covers helper resolution, sub-agent dispatch, GitHub operations, and handoff transitions; prefer the bundled or repo-local helper first, and treat `${CLAUDE_SKILL_DIR}` as optional host-provided fallback.
The handoff blocks below are the portable contract; slash commands are host renderings, not the control model.
## 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 skills/affinage/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:
- - **Exit 0** — proceed with grading.
- - **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`."*
- - **Any other non-zero** (1 PR/gh API error, 2 missing gh binary) — write `status: halt: pr-status-unavailable` and stop.
- - **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`.
- 3. **Fresh-window review.** If this is a standalone run and `--no-age` was not passed: compute the PR diff's `review_surface` score via the `review-surface` CLI (source: `src/fanout/review_surface_cli.py`, wrapping `src/fanout/review_surface.py::score()`) over the diff's git numstat rows. Run it through the `.pyz` bundle — the direct script imports `cli`/`git_utils` from `shared/scripts/`, which are only co-staged flat inside the bundle, so running it directly fails with `ModuleNotFoundError: No module named 'cli'`: `python3 skills/affinage/scripts/affinage.pyz review-surface --repo . <base>...HEAD` — the range must be the PR's full diff against its base branch (after `gh pr checkout <pr>`, `origin/<base>...HEAD`), never the CLI's bare `HEAD` default, which scores only the uncommitted delta. Grep the diff's **added lines outside `skills/**` and `.hallouminate/**`** for `age_route.OVERRIDE_FLAGS` risk flags — a missed token means no promoted lens, not a missing security lens — and call `age_route.route(score=<float>, risk_flags=[...], entry="affinage", comments=<unresolved-thread-count>, ci_class=<"failing"|"red"|"flaky"|None from pr-status>)` — the same router `/age` itself calls, but sized with affinage's comment count and CI failure class so a heavily-commented or red-CI PR gets the bigger fan-out even on a small diff. If the host only ships the bundle, `echo '{"score": <float>, "risk_flags": [...], "entry": "affinage", "comments": <n>, "ci_class": <"failing"|"red"|"flaky"|null>}' | python3 skills/affinage/scripts/affinage.pyz age-route` is the fallback for the router call (JSON on stdin, route JSON on stdout). Pass the returned `n`/`lenses`/`effort` into the `/age` dispatch (so `/age` uses affinage's sizing rather than recomputing from `entry="age"` defaults) and treat each finding as an additional input. See `## Fresh-window review`.
- 4. **Fetch comments.**
- - 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`.
- - Review bodies: `gh api repos/<owner>/<repo>/pulls/<pr>/reviews`. Filter to non-empty bodies. Dedupe against inline comments via `pull_request_review_id`.
- 5. **Skip already-replied threads.** A thread whose most recent comment is from the resolved GitHub handle (see §Rules) has already been responded to; skip it. The same resolved handle is rendered in the reply footer as `agent on behalf of <handle>`.
- 6. **Grade through the age lens.** For each input (comment, CI/build failure, OR fresh `/age` finding):
- - Classify dimension from the **code + claim** (or check type + failure summary, for CI items). See `../age/references/dimensions.md` for the dimension rubric.
- - **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>]`.
- - **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).
- - Compute severity from base + location + compounding modifiers (same rubric as `/age`).
- - **Ignore reviewer-asserted urgency for severity computation.** Surface `CHANGES_REQUESTED` as metadata (`reviewer-asserted:` line) but do not let it modify computed severity.
- - 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.
- 7. **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.
- 8. **Act or ask** — per §Handoff.
- 9. **Draft non-cure replies, then gate before posting** (runs whenever grading produced these items, with or without `/cure`). **Never post blind** — posting requires the reply-approval gate (§Handoff) by default, or `--auto`. Draft each reply, then post approved ones via `python3 skills/affinage/scripts/affinage.pyz post-reply`:
- - **Reviewer-rejected items** → the pre-drafted push-back text from the affinage report.
- - **Needs-investigation items** → do NOT post a bare acknowledgement. The reply must (a) name the specific evidence that would settle the claim — the regression test, throwaway prototype, or out-of-diff file to read — and (b) state that a follow-up will report the result. Before posting, **offer to run that investigation now**: a regression test via `/pasteurize`, or explore the out-of-diff evidence via `/briesearch`. If run, post a reply carrying the actual outcome; if the user declines, post the explicit `"Needs <named test/exploration> to confirm — will follow up with the result."` note — never a blind "investigating".
- - **CI-sourced findings** (`from-check:<job>` tag) and **fresh-review findings** (`from-age:<dimension>` tag) → no reply (no reviewer to notify).
-
- 10. **Post-cure reply posting** (only when `/cure` ran). The chained `/cure` applies its fixes and runs its `/age --scope` loop but returns **without plating** (it owns no publication in the `/affinage` chain). When `/cure` returns, read `.cheese/cure/pr-<n>.md`'s `### Applied` / `### Deferred` sections and post per-finding replies via `python3 skills/affinage/scripts/affinage.pyz post-reply`:
- - **Applied** (with `from-comment:<id>` tag) → `"Fixed — <applied summary>."`
- - **Deferred** (with `from-comment:<id>` tag) → `"Attempted fix reverted — <reason>."`
+ Exact CLI invocations, exit-code hints, and grading rationale for steps 2, 3, 6, and 9 below: `references/flow-details.md`.
- 11. **Plate** — the final writes above (steps 9–10) must land before publication. Once every approved reply is posted, and only when the cure applied ≥1 fix (there is something to publish), dispatch terminal `/plate [--open-pr] [--hard] [--safe]` to commit and publish cure's fixes to the PR. `/affinage` owns this dispatch; publication lands after all replies. After publication lands, run the **§ Post-PR learnings write-back** (`../cure/SKILL.md` § Handoff) — affinage owns terminal `/plate`, so it owns the write-back the chained `/cure` suppressed. Skip plate and write-back when no fix was applied — there is nothing to publish.
+ 1. **Resolve PR.** From `<pr-ref>` or `gh pr view --json number`; resolve `<owner>/<repo>` from the git remote.
+ 2. **Fetch PR status.** `affinage.pyz pr-status <pr>`. Exit 3 halts `status: halt: pr-status-logs-expired`; any other non-zero halts `status: halt: pr-status-unavailable`. Conflicting/dirty merge state routes to `## Merge-conflict resolution` before grading. Exit-code detail: `references/flow-details.md`.
+ 3. **Fresh-window review.** Standalone and `--no-age` not passed: score the PR diff, route it through `age_route.route(...)` sized with affinage's comment count and CI failure class, run `/age` with the returned `n`/`lenses`/`effort`, and fold each finding tagged `[from-age:<dimension>]`. See `## Fresh-window review`.
+ 4. **Fetch comments.** Inline threads: `gh api repos/<owner>/<repo>/pulls/<pr>/comments` (REST; no thread-resolution state, so skip `position: null` comments unless `--include-outdated`). Review bodies: `gh api repos/<owner>/<repo>/pulls/<pr>/reviews`, filtered to non-empty bodies, deduped against inline comments via `pull_request_review_id`.
+ 5. **Skip already-replied threads.** A thread last-commented by the resolved GitHub handle (§Rules) is already answered — skip it; the footer renders as `agent on behalf of <handle>`.
+ 6. **Grade through the age lens.** Classify each input (comment, CI failure, or fresh `/age` finding) by dimension — code/claim, or check type/failure for CI — per `../age/references/dimensions.md`, and by severity (base + location + compounding, same rubric as `/age`); ignore reviewer-asserted urgency (`CHANGES_REQUESTED` is metadata, never a severity bump). Bucket into severity sections (contained fixes), `## Needs-investigation` (needs out-of-diff evidence), or `## Reviewer-rejected` (wrong/ungrounded, or a lot of follow-up work). Full bucketing criteria: `references/flow-details.md`.
+ 7. **Write report** to `.cheese/affinage/pr-<n>.md`: four-line handoff slug, then the age-format body plus two extra sections. See `## Output`.
+ 8. **Act or ask** — per §Handoff.
+ 9. **Draft non-cure replies, then gate before posting** (whenever grading produced these items, with or without `/cure`). Never post blind — requires the reply-approval gate (§Handoff), or `--auto`. Draft per `references/flow-details.md`; post approved ones via `affinage.pyz post-reply`. CI-sourced (`from-check:<job>`) and fresh-review (`from-age:<dimension>`) findings get no reply.
+ 10. **Post-cure reply posting** (only when `/cure` ran). Once `/cure` returns, read `.cheese/cure/pr-<n>.md`'s `### Applied`/`### Deferred` and post per-finding replies via `affinage.pyz post-reply`: **Applied** (`from-comment:<id>`) → `"Fixed — <applied summary>."`; **Deferred** (`from-comment:<id>`) → `"Attempted fix reverted — <reason>."`
+ 11. **Plate** — once every approved reply is posted (steps 9–10) and the cure applied ≥1 fix, dispatch terminal `/plate [--open-pr] [--hard] [--safe]`; publication lands after every reply. After it lands, run the **§ Post-PR learnings write-back** (`../cure/SKILL.md` § Handoff) — affinage owns the write-back the chained `/cure` suppressed. Skip plate and write-back when no fix was applied.
## Fresh-window review
- Detection: standalone = no upstream `handoff_context`; chained = a `handoff_context` with `source_skill: /cook` or `/cure`. Behaviour:
-
- - **Standalone** (and `--no-age` not passed): compute the `entry="affinage"` router call (Flow step 3) and run `/age <pr-ref>` over the PR diff, passing the router's `n`/`lenses`/`effort` so `/age` doesn't recompute a smaller `entry="age"` sizing from the diff alone. 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.
+ Standalone runs (see intro) compute the `entry="affinage"` router call (Flow step 3) and run `/age <pr-ref>` over the PR diff, passing the router's `n`/`lenses`/`effort` so `/age` doesn't recompute a smaller `entry="age"` sizing from the diff alone. Fold each returned finding into the report's severity sections tagged `[from-age:<dimension>]` — they flow to `/cure` like any other finding but get no GitHub reply (no reviewer to notify, same as `[from-check:…]` items).
- 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`).
+ Run the fresh pass before grading external claims so an echoing comment can be deduped, under the same sub-agent gate as grading (`## Sub-agent context gate`) to keep the parent context lean.
## 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`. `/plate` owns the verified commit and existing-PR update transaction.
-
- - **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.
- - **`--safe` mode**: gate the checkout + `/melt` behind the handoff prompt — offer "Resolve merge conflicts" alongside the cure-selection options.
+ When `pr-status` reports unresolved conflicts, `/affinage` routes to `/melt` (mergiraf → rerere → kdiff3) rather than resolving by hand. Default/`--auto` run checkout + `/melt` automatically before `/cure`; `--safe` gates it behind the handoff prompt. If `/melt` cannot resolve, write `status: halt: merge-conflicts-need-human` and stop. Full steps: `references/merge-conflict.md`.
## Sub-agent context gate
- `/affinage` keeps dialogue, selection, approval state, and reply posting in the parent context. When the parent context would balloon, resolve a fresh read-only `reviewer` through the shared agent resolver. A compatible reviewer is preferred; a general worker qualifies only with prompt-only no-write enforcement and `degraded: true`:
-
- - 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, confidence, 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 `../age/references/sub-agent-gate.md`.
+ `/affinage` keeps dialogue, selection, approval state, and reply posting in the parent context. When the parent context would balloon — inputs exceed 10, diff exceeds ~25 KB, or threads span more than 5 files — resolve a fresh read-only `reviewer` through the shared agent resolver (a general worker qualifies only with `degraded: true`). The sub-agent returns a digest of graded findings (dimension, severity, confidence, evidence cite, pre-drafted push-back for `Reviewer-rejected` items); the parent owns the report write, selection gate, `/cure` dispatch, and reply posting. Digest size and selection detail: `../age/references/sub-agent-gate.md`.
## Preferred tools and fallbacks
- Call source-code search and read backends directly according to the shared [`code-intelligence-routing.md`](../cheese/references/code-intelligence-routing.md) contract. Beyond source-code routing there are affinage-specific tools:
+ Call source-code search/read backends per [`code-intelligence-routing.md`](../cheese/references/code-intelligence-routing.md). Affinage-specific tools:
| Need | Prefer | Fallback |
| --- | --- | --- |
| PR status (build + merge) | `skills/affinage/scripts/affinage.pyz pr-status` | manual `gh pr checks` + `gh pr view` |
| GitHub fetch | `gh api` | none (skill halts) |
| Reply posting | `skills/affinage/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:
+ Write to `.cheese/affinage/pr-<n>.md`: the four-line handoff slug, then the age-style body plus two extra sections (`## PR status` and the same severity / `## Needs-investigation` / `## Reviewer-rejected` shape `/age` uses). Full annotated template: `references/report-template.md`.
```markdown
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 · confidence: certain
- - 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 · confidence: certain
- - 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 · confidence: certain
- - 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 · confidence: speculating
- - 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 · confidence: certain
- - 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`; drafted replies are held for the reply-approval gate before posting (`--auto` posts directly). Replies post before terminal `/plate` publishes cure's fixes. On a reason to ask / `--safe`, the cure-selection prompt renders 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.
-
- Per-finding `confidence:` uses the voice-kernel scale (`../age/references/voice.md` § Reasoning posture): `certain` — the defect is verified by direct evidence (diff/code read, command output); `speculating` — inferred from indirect signal. A `don't know` grading never ships as a severity row — route it to `## Needs-investigation`.
-
- `status: ok` when grading completed; `status: halt: <reason>` when `gh` or `pr-status.py` failed in a way that blocks honest grading. `next:` is set per §Handoff — `cure` when ≥1 finding meets the `medium+` floor; `done` otherwise.
+ Empty severity sections are omitted; so are `## Needs-investigation`/`## Reviewer-rejected` when empty. `status: ok` when grading completed; `halt: <reason>` when `gh`/`pr-status` failed. `next:` per `## Handoff` § Slug `next:` values.
## Handoff
**Pipeline:** culture → mold → cook → press → age → cure → plate · `/affinage` is parallel to `/age` and feeds `/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, dispatch `/cure` (below), then render the **reply-approval gate** before posting the non-cure replies (Flow step 9) and the cure-dependent replies (step 10) — never post blind. `--auto` posts without the gate (§Auto mode). On a reason to ask or `--safe`: render the cure-selection table inline (per `../cure/references/selection.md`) and ask via `../cheese/references/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:
-
- - The five severity-floor options (recommended `all-medium, cheap`, then `all`, `all-medium`, `all-high`, `all-blocker`) are exactly age's — see [`../age/SKILL.md`](../age/SKILL.md) § Selection gate for their labels and semantics.
-
- 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`.
-
- The "present all four severity options on every run, empty-set-resolves-to-`none`" rule is age's — see [`../age/SKILL.md`](../age/SKILL.md) § Selection gate.
-
- **When no severity-section finding exists but `Reviewer-rejected` or `Needs-investigation` has items** — `/cure` has nothing to act on, so skip it. Posting is gated by default; render the **reply-approval gate** (below) and post nothing until the user chooses. Only `--auto` posts without this gate.
-
- **Reply-approval gate** — the single gate both Handoff branches use before any `post-reply` call:
-
- - **Post pushbacks only** *(recommended)* — post `Reviewer-rejected` drafts; hold `Needs-investigation` items for investigation.
- - **Investigate now, then post** — for each `Needs-investigation` item, run the follow-up investigation (`/pasteurize` for a regression test, `/briesearch` to explore the out-of-diff evidence), then post a reply carrying the actual result.
- - **Post all** — post every drafted push-back and the explicit `Needs-investigation` follow-up notes (naming the needed evidence) without running the investigation first.
- - **Skip posting** — leave the report for later; post nothing.
- - **Per-finding** — free-text pick of which drafts to post or investigate.
-
- After the selection, post the approved replies via Flow step 9. Then — only when the cure applied ≥1 fix — dispatch terminal `/plate` (Flow step 11) so publication follows the replies; when no cure ran or no fix applied, there is nothing to plate. Exit with `status: ok / next: done` — see `## Auto mode` § "no findings meet the floor" for the auto path.
-
- **Slug `next:` values.** Write `next: cure` when at least one finding meets the `medium+` floor (medium-or-above, or a cheap contained-fix low). Write `next: done` when no severity-section finding exists or all meeting items are empty-selection after floor resolution.
+ Default: affinage acts without asking, and asks only for a genuine reason (a sprawling/structural fix in the recommended set, conflicting findings) or under `--safe` (Flow step 8).
- 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:
+ - **Severity-section findings exist (any severity, including `Low`)** — compute the recommended composite (`all-medium, cheap`). No reason to ask and no `--safe`: announce the selection, dispatch `/cure` with the locked `handoff_context` (shape: `references/handoff-templates.md` § Cure dispatch context), then render the **reply-approval gate** before posting (Flow steps 9–10) — never post blind. A reason to ask, or `--safe`: render the **cure-selection gate** per `../cheese/references/handoff-gate.md` instead, pre-selecting the composite and flagging heavy rows. `--auto` skips both gates (`## Auto mode`).
+ - **No severity-section findings, but `Reviewer-rejected`/`Needs-investigation` items exist** — nothing for `/cure` to act on; render the reply-approval gate and post nothing until chosen. Only `--auto` skips it.
- ```yaml
- handoff_context:
- source_skill: /affinage
- source_report: .cheese/affinage/pr-<n>.md
- selection: "<verb or explicit ids>"
- resolved_ids: [<expanded ids>]
- ```
+ After the selection, post approved replies (Flow step 9–10), then — only when the cure applied ≥1 fix — dispatch terminal `/plate [--open-pr] [--hard] [--safe]` (Flow step 11); publication lands after every reply. Exit `status: ok / next: done` when there is nothing to act on.
- `/cure` re-confirms cited ids and goes straight to apply. Because the handoff carries `source_skill: /affinage`, `/cure` applies its fixes and runs its `/age --scope` loop but **suppresses its own terminal `/plate`** and returns — affinage owns publication. Propagate `--safe`, `--open-pr`, and `--hard` to `/cure` when in scope. On return, `/affinage` posts every reply (Flow steps 9–10) — its terminal final writes — and only then dispatches terminal `/plate [--open-pr] [--hard] [--safe]` (Flow step 11), so publication lands after every reply. Skip the plate dispatch when the cure applied no fix.
+ **Slug `next:` values.** `cure` when ≥1 finding meets the `medium+` floor; `done` when no severity-section finding exists or all meeting items resolve to an empty selection.
## Auto mode
- When invoked with `--auto --stake <floor>` (or `--plate`, which enters this mode with `--stake medium+ --open-pr`):
-
- - 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 explicit follow-up note naming the evidence that would settle the claim (`"Needs <named test/prototype> to confirm — will follow up with the result."`). Auto mode does not pause to run the spike; it posts the honest follow-up note, never a blind acknowledgement.
- - After the cure chain settles and **all** replies are posted (previous two bullets), `/affinage` dispatches terminal `/plate --open-pr [--hard]` to publish cure's fixes — the final writes precede publication. `/cure` suppresses its own terminal `/plate` for the `/affinage` chain (keyed on `source_skill: /affinage`). Skip the dispatch when no fix was applied.
-
- 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. Spawning the cure chain in a sub-agent 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`.
+ Skips the selection gate. Resolves merge conflicts via `/melt` first (halt `status: halt: merge-conflicts-need-human` if unresolved). If standalone, runs the fresh `/age` pass. Auto-selects every finding meeting `<floor>` (`--plate` enters this mode at `--stake medium+ --open-pr`) and dispatches `/cure --auto --stake <floor>`; once its chain settles, posts replies for the originally graded items only, then dispatches terminal `/plate --open-pr [--hard]` once every reply posts (skipped if no fix applied). If no findings meet the floor: skip `/cure`, post rejection/investigation replies only, exit `status: ok / next: done`. Full mechanics: `references/auto-mode.md`.
## --hard mode
- `/affinage` passes `--hard` to its own terminal `/plate` (dispatched after replies), which fires `/hard-cheese` after verifying the final artifact state. `/cure` does not dispatch plate in the `/affinage` chain, so the gate fires once — at affinage's publication boundary.
+ `/affinage` passes `--hard` to its terminal `/plate`, which fires `/hard-cheese` after verifying the final artifact state. `/cure` never dispatches plate in this chain, so the gate fires once — at affinage's publication boundary.
## 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 `../age/references/voice.md`.
- - Never auto-apply fixes from `/affinage` itself. Code fixes go through `/cure`; merge conflicts go through `/melt`.
- - Never post a GitHub reply without approval. Reply posting is gated by default (§Handoff reply-approval gate); only `--auto` posts autonomously. A `Needs-investigation` reply must name the follow-up test or exploration and offer to run it (`/pasteurize` or `/briesearch`) before posting — never a blind acknowledgement.
- - 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. After resolution, affinage's terminal `/plate` owns the verified commit and push.
- - `/affinage` owns terminal publication. The chained `/cure` suppresses its own `/plate` (keyed on `source_skill: /affinage`); after a clean cure, `/affinage` posts every reply and *then* dispatches `/plate` to update the existing PR — or, with `--open-pr`, apply the explicit-choice and review-shape policy before publishing a new PR. Replies always precede plate: no final write lands after publication.
+ - Grading is code-grounded, not reviewer-asserted — see Flow step 6.
+ - Prefer fixing over pushing back. A grounded nit with a contained fix goes to `/cure` as `Low`; reserve `## Reviewer-rejected` for claims that are wrong, ungrounded, or a lot of work (Flow step 6, `../age/references/voice.md`).
+ - Never auto-apply fixes itself — code fixes go through `/cure`, merge conflicts through `/melt` (`## Merge-conflict resolution`).
+ - Never post a reply without approval — see the reply-approval gate (`## Handoff`, `references/handoff-templates.md`).
- Every posted reply ends with the literal `agent on behalf of <handle>` attribution via `skills/affinage/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).
+ - Idempotent re-runs rely on the latest-comment-from-self heuristic (Flow step 5) — the REST `/comments` endpoint exposes no thread resolution state; use GraphQL `reviewThreads` if cross-session resolution state is ever needed.
- Apply the shared voice kernel (`../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.
- - `../age/references/dimensions.md` — per-dimension rubrics and severity computation.
- - `skills/cure/SKILL.md` — apply pipeline, `--auto --stake` floors, handoff context shape.
- - `../cure/references/selection.md` — selection verbs and composition.
- - `skills/melt/SKILL.md` — merge-conflict resolution cascade (mergiraf → rerere → kdiff3).
- - `../cheese/references/handoff-gate.md` — gate primitives.
- - `python3 skills/affinage/scripts/affinage.pyz post-reply` — reply posting with `agent on behalf of <handle>` attribution.
- - `python3 skills/affinage/scripts/affinage.pyz pr-status` — PR status fetcher.
+ Affinage-local, each also routed inline above: `references/flow-details.md`, `references/merge-conflict.md`, `references/report-template.md`, `references/handoff-templates.md`, `references/auto-mode.md`. `references/sub-agent-gate.md` is `../age/references/sub-agent-gate.md` (shared, not affinage-local).
+
+ Scripts: `skills/affinage/scripts/affinage.pyz post-reply` (reply posting), `pr-status` (PR status fetcher).
## Agent resolution
Resolve each dispatch through [`../cheese/references/agent-resolution.md`](../cheese/references/agent-resolution.md).
| Work | Preferred types | Permissions/isolation | Minimum power | Effort | Fallback |
| --- | --- | --- | --- | --- | --- |
| Triage review claims and CI evidence | reviewer | read-only, fresh-context | powerful | high | compatible reviewer, then general |
The canonical affinage report carries the shared `agent_resolution` block.