You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
.github/workflows/approval-check.yml requires the approved pipeline label on every PR into dev. Two categories can never satisfy it:
Dependabot — its PRs never enter the pipeline, so nothing applies approved. They are permanently unmergeable. PR #296 (Bump prettier from 3.8.4 to 3.9.3) has been open since 14 August for this reason.
Human PRs — the workflow comment says the gate is satisfied when "review-agent (or a human, for manual PRs) applies approved", so a person must hand-apply a pipeline label to their own PR purely to satisfy a check.
Author-based detection is not available: pipeline PRs are opened under the operator's gh token, so agent and human PRs share the same authors (e.g. PR #500, a pipeline PR, is authored by cielbellerose). A label is the only usable signal.
Design
Every PR authored by the pipeline carries the claude label. The approval gate applies only to those PRs.
PR has claude → must carry approved and no blocking pipeline label
PR lacks claude → the job is skipped; the PR merges on its own merits
This covers Dependabot without a special case — Dependabot PRs have no claude label.
Skip at the job level, not the workflow level
Use a job-level if: condition. The workflow still triggers, a check run is created with conclusion skipped, and GitHub treats a skipped required check as satisfied — so the PR shows "Skipped" and merges.
Never filter the trigger (paths, branches, removing the event) to achieve this. A workflow that does not run never creates the check run, which then sits pending forever and blocks the merge permanently.
Keep types: [opened, reopened, synchronize, labeled, unlabeled] so the condition re-evaluates when labels change — adding claude to a PR must activate the gate.
Trade-off, recorded deliberately
This inverts the failure mode from fail-closed to fail-open.
Today: no approved label → blocked
After: no claude label → no gate at all
So a pipeline PR that never received the claude label silently loses its gate, and its absence is indistinguishable from a human PR. Removing one label also bypasses the gate deliberately.
Accepted, because the gate's purpose is to stop mid-pipeline PRs merging early, not to enforce review universally. The mitigation is documentation plus a cockpit check (below), not a stricter workflow.
Documentation — the load-bearing part
Because the gate now depends on a label being applied, the label being applied has to be impossible to forget. Update every place that describes how pipeline PRs are opened:
.claude/agents/impl-agent.md — the PR-opening step must apply claude alongside the existing title/base/assignee flags. This is the single most important change: it is where the label actually gets set.
.claude/docs/PIPELINE.md — state in the label state machine that claude marks pipeline ownership of a PR and is what activates the approval gate; note that a pipeline PR missing it will merge ungated. Add it to the permission/CI section describing approval-check.yml.
.claude/skills/pipeline/SKILL.md — the cockpit should report any PR it is tracking that lacks claude, so a dropped label surfaces rather than silently disabling the gate.
CLAUDE.md — the Commits/Branches/PRs section lists title, base branch, body and assignee conventions; add the claude label for pipeline-authored PRs.
.github/workflows/approval-check.yml — rewrite the header comment, which currently says humans must hand-apply approved.
Acceptance criteria
impl-agent applies the claude label to every PR it opens
The approval job runs only when the PR carries claude, via a job-level if:
A PR without claude shows Skipped and can merge
A Dependabot PR into dev merges with no label and no special case
A PR with claude still requires approved and no blocking label, exactly as today
Adding claude to an existing PR activates the gate on the next event
The trigger is not filtered — no paths/branches change that would leave the check pending
branches: [dev] scoping and the warning about the main/dev ruleset are preserved — release PRs carry no pipeline labels
The cockpit reports tracked PRs missing the claude label
This ticket edits .claude/ files, so under #503 it is implemented from an operator session rather than dispatched.
Implementation Plan
Overview
One-line change in CI, four documentation changes, one new cockpit sweep. The gate becomes conditional on the claude label via a job-level if: on run-approval-check; impl-agent starts applying claude at gh pr create time so the gate is live from the PR's first event; the cockpit gains an "ungated PR" sweep so a dropped label is visible rather than silent. No source, schema, or UI changes.
The job ID run-approval-check must not change — that string is the registered required check on the dev ruleset. Renaming it would leave the required check pending forever, which is the same failure the ticket forbids.
Changes
.github/workflows/approval-check.yml — job-level if: gate on the claude label; rewritten header comment; updated ::error:: copy (no longer tells humans to hand-apply approved).
.claude/agents/impl-agent.md — --label "claude" in the step-6 gh pr create block, plus one line on why it matters.
.claude/docs/PIPELINE.md — claude row in the PR labels table; a line in the permission/CI narrative describing approval-check.yml's new condition; a recovery-runbook row for a pipeline PR showing Skipped.
.claude/skills/pipeline/SKILL.md — the ungated-PR sweep query in the tick procedure + its report line.
CLAUDE.md — claude label added to the PR bullet in Commits, Branches, PRs.
Implementation
Add the job-level condition to run-approval-check in .github/workflows/approval-check.yml, directly under runs-on: if: contains(github.event.pull_request.labels.*.name, 'claude'). Do not touch on: — branches: [dev] and the five types stay exactly as they are, and the job ID stays run-approval-check.
Rewrite the header comment: the gate now applies only to pipeline PRs (those labeled claude); a PR without claude produces a skipped check run, which GitHub counts as satisfied; the trigger must never be narrowed (a workflow that never runs leaves the required check pending forever); keep the existing branches: [dev] / ruleset-15248252 warning verbatim.
Update the missing-approved::error:: message — it currently references "a human, for manual PRs". New copy: Missing the 'approved' label. This PR carries 'claude', so it is a pipeline PR and merges only once review-agent applies 'approved'. If it is not a pipeline PR, remove the 'claude' label and this check will skip. Leave the blocking-label message and the success line as they are.
.claude/agents/impl-agent.md step 6: add --label "claude" to the gh pr create invocation (alongside --base/--title/--body-file/--assignee/--head) and one sentence after the --assignee note: the claude label is what activates approval-check.yml; a PR opened without it merges with no approval gate.
.claude/docs/PIPELINE.md — add a claude row to the PR labels table: Set byimpl-agent (at gh pr create), Type marker, Meaning "Pipeline owns this PR — activates the approved merge gate in approval-check.yml; a pipeline PR missing it merges ungated".
.claude/docs/PIPELINE.md — one short paragraph (near the permission/CI material, after the "Deny rules" / branch-protection discussion) stating: approval-check.yml gates PRs into dev on approvedonly when the PR carries claude; every other PR (human, Dependabot) gets a skipped check run and merges on its own merits; this is deliberately fail-open, and the mitigations are impl-agent's --label plus the cockpit's ungated-PR sweep.
.claude/docs/PIPELINE.md — recovery-runbook row: Symptom "run-approval-check shows Skipped on a pipeline PR" / Cause "the PR is missing the claude label, so the gate is inactive" / Fix "gh pr edit <n> --add-label \"claude\" — the labeled event re-evaluates the condition and the gate activates on that run".
.claude/skills/pipeline/SKILL.md — add one query to the tick block (in the sweep group, after the unowned sweep) that lists this operator's open PRs carrying any pipeline label but notclaude: gh pr list --repo SGAOperations/aplio --assignee "@me" --json number,title,labels --jq '[.[] | select((.labels | map(.name)) as $l | ($l | any(. == "ready for review" or . == "reviewing" or . == "needs revision" or . == "revising" or . == "approved" or . == "refresh branch" or . == "refreshing" or . == "needs human")) and ($l | index("claude") | not)) | {number, title}]'
.claude/skills/pipeline/SKILL.md — add an Ungated-PR sweep (each tick) note beside the unowned/denial reports: report only when the set changes, one line, never act automatically — e.g. ⚠️ Pipeline PRs without the 'claude' label (approval gate inactive): #501. Say "gate #501" or add the label on GitHub. Add the sweep to the ordered tick steps (fold it into step 4 alongside the unowned report) and to the status command's output so it is reachable on demand.
CLAUDE.md — extend the PR bullet: after the assignee clause, add "pipeline-authored PRs also carry the claude label — it is what activates the approved merge gate (approval-check.yml); without it the PR merges ungated."
Verify the docs stay consistent: no remaining text claims a human must hand-apply approved to a manual PR (approval-check.yml header, PIPELINE.md, CLAUDE.md).
Run npm run prettier:check — the four markdown files and the YAML are all prettier-formatted.
Testing
On this PR itself (it carries claude, and pull_request workflows run from the merge ref, so the new condition is live here): run-approval-checkruns and fails on the missing approved label, with the new error copy.
Remove claude from this PR → the next event's run-approval-check reports Skipped, and GitHub's merge box no longer lists it as failing. Re-add claude → the gate runs again and fails. This exercises both acceptance criteria for label-driven re-evaluation.
Confirm the check run name in the merge box is still run-approval-check (unchanged required check, no "Expected — waiting for status" entry).
Add approved with claude present → the job passes with the "merge gate satisfied" line.
Add a blocking label (e.g. reviewing) alongside claude + approved → the job still fails on the blocking label.
After merge into dev: on PR Bump prettier from 3.8.4 to 3.9.3 #296, comment @dependabot rebase so its merge ref picks up the new workflow, then confirm run-approval-check reports Skipped and the PR is mergeable (or close it if the prettier bump is stale — Dependabot will reopen a current one).
Confirm a dev → main release PR is unaffected: the workflow's branches: [dev] scoping means it never runs there, and no pipeline label is involved.
In a cockpit session, run status with a pipeline-labeled PR that lacks claude → it is reported on the ungated line; with claude present → it is not.
Risks / notes
Fail-open is the accepted trade-off (recorded in the ticket). The only technical mitigations are impl-agent's --label and the cockpit sweep; nothing in CI can distinguish an unlabeled pipeline PR from a human one.
gh pr create --label applies the label in the creation request, so the opened payload already contains it and the gate is active on the PR's first check run. Even if it were applied afterwards, labeled is in types and would activate it — no ordering hazard either way.
Dependabot PRs opened before this lands run the old workflow from their own merge ref until rebased; @dependabot rebase is the fix, not a workflow change.
claude is a repo-wide label already used on issues (this one included). No new label needs creating, and GitHub shares the label set between issues and PRs.
Problem
.github/workflows/approval-check.ymlrequires theapprovedpipeline label on every PR intodev. Two categories can never satisfy it:Dependabot — its PRs never enter the pipeline, so nothing applies
approved. They are permanently unmergeable. PR #296 (Bump prettier from 3.8.4 to 3.9.3) has been open since 14 August for this reason.Human PRs — the workflow comment says the gate is satisfied when "review-agent (or a human, for manual PRs) applies
approved", so a person must hand-apply a pipeline label to their own PR purely to satisfy a check.Author-based detection is not available: pipeline PRs are opened under the operator's
ghtoken, so agent and human PRs share the same authors (e.g. PR #500, a pipeline PR, is authored bycielbellerose). A label is the only usable signal.Design
Every PR authored by the pipeline carries the
claudelabel. The approval gate applies only to those PRs.claude→ must carryapprovedand no blocking pipeline labelclaude→ the job is skipped; the PR merges on its own meritsThis covers Dependabot without a special case — Dependabot PRs have no
claudelabel.Skip at the job level, not the workflow level
Use a job-level
if:condition. The workflow still triggers, a check run is created with conclusionskipped, and GitHub treats a skipped required check as satisfied — so the PR shows "Skipped" and merges.Never filter the trigger (
paths,branches, removing the event) to achieve this. A workflow that does not run never creates the check run, which then sits pending forever and blocks the merge permanently.Keep
types: [opened, reopened, synchronize, labeled, unlabeled]so the condition re-evaluates when labels change — addingclaudeto a PR must activate the gate.Trade-off, recorded deliberately
This inverts the failure mode from fail-closed to fail-open.
approvedlabel → blockedclaudelabel → no gate at allSo a pipeline PR that never received the
claudelabel silently loses its gate, and its absence is indistinguishable from a human PR. Removing one label also bypasses the gate deliberately.Accepted, because the gate's purpose is to stop mid-pipeline PRs merging early, not to enforce review universally. The mitigation is documentation plus a cockpit check (below), not a stricter workflow.
Documentation — the load-bearing part
Because the gate now depends on a label being applied, the label being applied has to be impossible to forget. Update every place that describes how pipeline PRs are opened:
.claude/agents/impl-agent.md— the PR-opening step must applyclaudealongside the existing title/base/assignee flags. This is the single most important change: it is where the label actually gets set..claude/docs/PIPELINE.md— state in the label state machine thatclaudemarks pipeline ownership of a PR and is what activates the approval gate; note that a pipeline PR missing it will merge ungated. Add it to the permission/CI section describingapproval-check.yml..claude/skills/pipeline/SKILL.md— the cockpit should report any PR it is tracking that lacksclaude, so a dropped label surfaces rather than silently disabling the gate.CLAUDE.md— the Commits/Branches/PRs section lists title, base branch, body and assignee conventions; add theclaudelabel for pipeline-authored PRs..github/workflows/approval-check.yml— rewrite the header comment, which currently says humans must hand-applyapproved.Acceptance criteria
impl-agentapplies theclaudelabel to every PR it opensclaude, via a job-levelif:claudeshows Skipped and can mergedevmerges with no label and no special caseclaudestill requiresapprovedand no blocking label, exactly as todayclaudeto an existing PR activates the gate on the next eventpaths/brancheschange that would leave the check pendingbranches: [dev]scoping and the warning about themain/devruleset are preserved — release PRs carry no pipeline labelsclaudelabelNote
This ticket edits
.claude/files, so under #503 it is implemented from an operator session rather than dispatched.Implementation Plan
Overview
One-line change in CI, four documentation changes, one new cockpit sweep. The gate becomes conditional on the
claudelabel via a job-levelif:onrun-approval-check;impl-agentstarts applyingclaudeatgh pr createtime so the gate is live from the PR's first event; the cockpit gains an "ungated PR" sweep so a dropped label is visible rather than silent. No source, schema, or UI changes.The job ID
run-approval-checkmust not change — that string is the registered required check on thedevruleset. Renaming it would leave the required check pending forever, which is the same failure the ticket forbids.Changes
.github/workflows/approval-check.yml— job-levelif:gate on theclaudelabel; rewritten header comment; updated::error::copy (no longer tells humans to hand-applyapproved)..claude/agents/impl-agent.md—--label "claude"in the step-6gh pr createblock, plus one line on why it matters..claude/docs/PIPELINE.md—clauderow in the PR labels table; a line in the permission/CI narrative describingapproval-check.yml's new condition; a recovery-runbook row for a pipeline PR showingSkipped..claude/skills/pipeline/SKILL.md— the ungated-PR sweep query in the tick procedure + its report line.CLAUDE.md—claudelabel added to the PR bullet in Commits, Branches, PRs.Implementation
run-approval-checkin.github/workflows/approval-check.yml, directly underruns-on:if: contains(github.event.pull_request.labels.*.name, 'claude'). Do not touchon:—branches: [dev]and the fivetypesstay exactly as they are, and the job ID staysrun-approval-check.claude); a PR withoutclaudeproduces askippedcheck run, which GitHub counts as satisfied; the trigger must never be narrowed (a workflow that never runs leaves the required check pending forever); keep the existingbranches: [dev]/ ruleset-15248252 warning verbatim.approved::error::message — it currently references "a human, for manual PRs". New copy:Missing the 'approved' label. This PR carries 'claude', so it is a pipeline PR and merges only once review-agent applies 'approved'. If it is not a pipeline PR, remove the 'claude' label and this check will skip.Leave the blocking-label message and the success line as they are..claude/agents/impl-agent.mdstep 6: add--label "claude"to thegh pr createinvocation (alongside--base/--title/--body-file/--assignee/--head) and one sentence after the--assigneenote: theclaudelabel is what activatesapproval-check.yml; a PR opened without it merges with no approval gate..claude/docs/PIPELINE.md— add aclauderow to the PR labels table: Set byimpl-agent(atgh pr create), Type marker, Meaning "Pipeline owns this PR — activates theapprovedmerge gate inapproval-check.yml; a pipeline PR missing it merges ungated"..claude/docs/PIPELINE.md— one short paragraph (near the permission/CI material, after the "Deny rules" / branch-protection discussion) stating:approval-check.ymlgates PRs intodevonapprovedonly when the PR carriesclaude; every other PR (human, Dependabot) gets askippedcheck run and merges on its own merits; this is deliberately fail-open, and the mitigations are impl-agent's--labelplus the cockpit's ungated-PR sweep..claude/docs/PIPELINE.md— recovery-runbook row: Symptom "run-approval-checkshows Skipped on a pipeline PR" / Cause "the PR is missing theclaudelabel, so the gate is inactive" / Fix "gh pr edit <n> --add-label \"claude\"— thelabeledevent re-evaluates the condition and the gate activates on that run"..claude/skills/pipeline/SKILL.md— add one query to the tick block (in the sweep group, after the unowned sweep) that lists this operator's open PRs carrying any pipeline label but notclaude:gh pr list --repo SGAOperations/aplio --assignee "@me" --json number,title,labels --jq '[.[] | select((.labels | map(.name)) as $l | ($l | any(. == "ready for review" or . == "reviewing" or . == "needs revision" or . == "revising" or . == "approved" or . == "refresh branch" or . == "refreshing" or . == "needs human")) and ($l | index("claude") | not)) | {number, title}]'.claude/skills/pipeline/SKILL.md— add an Ungated-PR sweep (each tick) note beside the unowned/denial reports: report only when the set changes, one line, never act automatically — e.g.⚠️ Pipeline PRs without the 'claude' label (approval gate inactive): #501. Say "gate #501" or add the label on GitHub.Add the sweep to the ordered tick steps (fold it into step 4 alongside the unowned report) and to thestatuscommand's output so it is reachable on demand.CLAUDE.md— extend the PR bullet: after the assignee clause, add "pipeline-authored PRs also carry theclaudelabel — it is what activates theapprovedmerge gate (approval-check.yml); without it the PR merges ungated."approvedto a manual PR (approval-check.ymlheader, PIPELINE.md, CLAUDE.md).npm run prettier:check— the four markdown files and the YAML are all prettier-formatted.Testing
claude, andpull_requestworkflows run from the merge ref, so the new condition is live here):run-approval-checkruns and fails on the missingapprovedlabel, with the new error copy.claudefrom this PR → the next event'srun-approval-checkreports Skipped, and GitHub's merge box no longer lists it as failing. Re-addclaude→ the gate runs again and fails. This exercises both acceptance criteria for label-driven re-evaluation.run-approval-check(unchanged required check, no "Expected — waiting for status" entry).approvedwithclaudepresent → the job passes with the "merge gate satisfied" line.reviewing) alongsideclaude+approved→ the job still fails on the blocking label.dev: on PR Bump prettier from 3.8.4 to 3.9.3 #296, comment@dependabot rebaseso its merge ref picks up the new workflow, then confirmrun-approval-checkreports Skipped and the PR is mergeable (or close it if the prettier bump is stale — Dependabot will reopen a current one).dev → mainrelease PR is unaffected: the workflow'sbranches: [dev]scoping means it never runs there, and no pipeline label is involved.statuswith a pipeline-labeled PR that lacksclaude→ it is reported on the ungated line; withclaudepresent → it is not.Risks / notes
--labeland the cockpit sweep; nothing in CI can distinguish an unlabeled pipeline PR from a human one.gh pr create --labelapplies the label in the creation request, so theopenedpayload already contains it and the gate is active on the PR's first check run. Even if it were applied afterwards,labeledis intypesand would activate it — no ordering hazard either way.@dependabot rebaseis the fix, not a workflow change.claudeis a repo-wide label already used on issues (this one included). No new label needs creating, and GitHub shares the label set between issues and PRs..claude/, so implementation happens in an operator session rather than a dispatchedimpl-agent.