Skip to content

pr-unblock: derive fix-ci-subscription's start and the restart cap from trusted values #8890

Description

@kyle-sexton

No related issue: follow-up hardening from the Phase 5b review (fix-ci on run-claude-cli).

Problem

Two pr-unblock decisions about fix-ci still read outputs of the read runner's model job (pr-run-activity-read.yml, job run). The 5b review treats those outputs as untrusted, because that job runs PR head code:

  • fix-ci-subscription starts when needs.fix-ci.outputs.lane-class == 'credit-stop' || needs.fix-ci.outputs.credential-present == 'false' (.github/workflows/pr-unblock.yml:395-397).
  • The read runner's report job skips its check-run post when the run reports handed-off == 'true'.

A run that misreports these can end with no pr-unblock / fix-ci check posted and no fix-ci-subscription call. fix-ci-dispatch then dispatches again, and the restart cap never counts it. The worst case is repeated lane runs on one PR, which spend prepaid API credits (auto-refill is off, so it costs credits, not a charge).

Fix

  • Derive fix-ci-subscription's start from values the model job cannot write. Two options: the job's own step outcome read through the jobs API, or the fix-ci-context job's result.
  • Make fix-ci-dispatch count a dispatched run that posted no check toward fix-ci-restarts, so the loop is capped whatever the run reports.

Acceptance

  • fix-ci-subscription's start condition reads no needs.fix-ci.outputs.* value. A test pins this.
  • A dispatched fix-ci run that posts no check counts toward the restart cap. A test covers it.

Priority: high within the lane-funding follow-ups.

🤖 Generated with Claude Code

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 21, 2026
  2. github-actions commented on Oct 21, 2026

    @github-actions
    Contributor

    Proposed work class: work-class: scoped
    Basis: automated intake triage; the lanes App applied this class.

    Size: Medium
    Complexity: Medium
    Uncertainty: Known
    Flags: none
    Blocked by: none

    Decision recorded.

    The fix-ci part landed in #9363 (e5fd363e05). The three fix-docs criteria from decision comment 6424948616 are still open, so this issue stays open for them. They count fix-docs runs toward a dispatch cap. That cap is a loop breaker, a safety limit that stays active under the owner's standing direction §10.

    #8890 (comment)

    That decision settles the open questions. The fix-ci boxes are done. What remains is the three fix-docs dispatch-cap boxes: no re-dispatch after a completed run without a check, one retry for a lost run, and no dispatch slot for a run stopped at the trigger gate. Each needs a test.

    Automated triage, applied by the lanes App.

  3. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 21, 2026
  4. kyle-sexton commented on Oct 22, 2026

    @kyle-sexton
    MemberAuthor

    Scope widened after the #8964 security pass (fix-docs moved into pr-unblock):

    • fix-docs-subscription repeats the same start condition: it reads needs.fix-docs.outputs.lane-class and credential-present (.github/workflows/pr-unblock.yml:516-517, fallback input at :532, as of feat(pr-pipeline): run fix-docs on run-claude-cli with an apply-patch push job #8964 head e89dc01). Its start must come from trusted values too.
    • fix-docs-dispatch.mjs marks a PR handled only when it finds a pr-unblock / fix-docs check run (:145-147, :187-188). A successful dispatch marks nothing (:199-212), and the in-flight guard skips only runs that are not completed (:177-181). A fix-docs run that ends without posting its check is therefore dispatched again on every push to main, with no per-PR cap. Fix: treat a completed pr-unblock fix-docs #<n> run (runTitle) as handled.

    Acceptance additions:

    • fix-docs-subscription's start condition reads no needs.fix-docs.outputs.* value. A test pins this.
    • A PR whose fix-docs run completed without posting a check is not dispatched again. A test covers it.

    Lands right after #8964 and before Phase 7.2, so the address-feedback and review-dependency subscription jobs don't copy the pattern.

  5. kyle-sexton commented on Oct 22, 2026

    @kyle-sexton
    MemberAuthor

    Security review findings on #8964 (all LOW, all assigned here):

    • L1, re-fire: when the read runner's run job leaves its head output empty, or reports handed-off=true (pr-run-activity-read.yml:2424, :2534-2543), the report job posts no check. The PR is then dispatched again on every push to main, about once per PR per push and at most 2 per push. At last week's main push rate that is up to about 356 runs a day, each spending prepaid API credits. Precondition: a steered model on a listed author's PR. Fix: record the dispatched head at dispatch time (fix-docs-dispatch.mjs:212), or pass a base-derived head so the report posts a neutral not-assessed check.
    • L2, cap starvation: a same-repository PR by an author not on the trusted list stops at the trigger gate and posts no check. It takes a dispatch slot on every push (lowest number first, :160, :175), so two such PRs block fix-docs for every other PR. No model spend. The same fix covers it.
    • L3, subscription start: fix-docs-subscription starts on run-job outputs a forged run can set (pr-unblock.yml:516-517, :532), the same pattern as fix-ci. It widens no credential reach and triggers no metered charge (usage credits stay off).
  6. removed
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 25, 2026
  7. kyle-sexton commented on Oct 25, 2026

    @kyle-sexton
    MemberAuthor

    Released for re-triage under the owner's 2026-10-24 rule: an issue stops for a person only for an intent or vision question, a security call, a host or attended run, or spend outside prepaid credit. The hold label came off and intake triage runs again now; it re-applies a hold, with a numbered question, when one of those applies.

    To hold this issue again, add needs-human (or status: needs-decision) and say why in a comment.

  8. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.
    priority: highSignificant impact, or blocks an imminent release; staff this cycle.
    on Oct 25, 2026
  9. 22 remaining items

  10. kyle-sexton commented on Oct 27, 2026

    @kyle-sexton
    MemberAuthor

    Released for re-triage under the owner's standing direction (2026-10-27): a person is involved only for "1. A change to the original intent or vision. 2. A finding during implementation ... that changes the intent, vision or primary design. 3. A real HIGH/CRITICAL security or cost risk", where cost is anything that can bill (Azure, GitHub larger runners or private-repo Actions, paid storage, plan or seat changes) and model usage under the subscriptions or API credits is not cost. None applies here. The hold label came off and intake triage runs again; an open blocked-by edge, if any, makes admission wait for that blocker.

    To hold this issue again, add needs-human (or status: needs-decision) and name which of the three applies.

  11. removed
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    on Oct 27, 2026
  12. github-actions commented on Oct 27, 2026

    @github-actions
    Contributor

    The triage sweep dispatched intake triage for this issue (attempt 1 of 2).

  13. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    status: needs-decisionAwaiting a human or maintainer judgment call.
    and removed
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    on Oct 27, 2026
  14. changed the title [-]pr-unblock: derive fix-ci-subscription's start and the restart cap from trusted values[/-] [+]pr-unblock: derive `fix-ci-subscription`'s start and the restart cap from trusted values[/+] on Oct 29, 2026
  15. kyle-sexton commented on Oct 30, 2026

    @kyle-sexton
    MemberAuthor

    Decision recorded.

    The fix-ci part landed in #9363 (e5fd363e05). The three fix-docs criteria from decision comment 6424948616 are still open, so this issue stays open for them. They count fix-docs runs toward a dispatch cap. That cap is a loop breaker, a safety limit that stays active under the owner's standing direction §10.

    • "fix-ci-subscription's start condition reads no needs.fix-ci.outputs.* value. A test pins this." Its if: reads only needs.fix-ci-handoff.outputs.posted (.github/workflows/pr-unblock.yml:2210-2214), pinned in workflow-gates.test.mjs. Landed in fix(pr-pipeline): derive fix-ci's fallback, restart count and head binding from trusted jobs #9363 (e5fd363e05).
    • "A dispatched fix-ci run that posts no check counts toward the restart cap. A test covers it." fix-ci-unreported posts a fix-ci failure "which counts toward fix-ci-restarts" (pr-unblock.yml:2582-2590), covered in callers.test.mjs. Landed in fix(pr-pipeline): derive fix-ci's fallback, restart count and head binding from trusted jobs #9363 (e5fd363e05).
    • A PR whose fix-docs run completed without posting a check, other than the lost run in the next line, is not dispatched again. A test covers it. (decision comment 6424948616, unchanged)
    • A PR whose only fix-docs run was lost (cancelled, timed_out or startup_failure) without a check is dispatched once more, and not after a second such run. A test covers both. (decision comment 6424948616, unchanged)
    • A same-repository PR whose fix-docs dispatch stopped at the trigger gate does not take a dispatch slot on later runs. A test covers it. (decision comment 6424948616, unchanged)

    #10455 (fc07a5d179, for #9247) added a per-head fix-docs cap on main. It stops a head after limit-stops-per-head runs (default 3) whose broker-allowed job succeeded (.github/scripts/pr-unblock/fix-docs-dispatch.mjs:284-305). That does not meet these three boxes as written. It allows three runs, not one. It does not treat a lost run (cancelled, timed_out or startup_failure) differently from other runs. A run that stops at the trigger gate does not count toward the cap, so the PR is dispatched again on every run. The PR that closes this issue checks each box against that code.

    The status: needs-decision hold comes off: the body says the worst case "costs credits, not a charge", and §3 lists "each account's promotional $200 API credits" under "Not cost, so spend it freely".

  16. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    admission-deferredAdmitted but waiting for WIP room; the hourly work-unblock sweep labels it agent-ready.
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    and removed
    admission-deferredAdmitted but waiting for WIP room; the hourly work-unblock sweep labels it agent-ready.
    on Oct 30, 2026
  17. melodic-automation-lanes commented on Oct 30, 2026

    @melodic-automation-lanes
    Contributor

    Refinement. A read-only run explored this repository, searched its issues and pull requests, and read the allowed docs to draft what follows. On an issue a listed human filed, it counts as the issue's spec once posted, while unedited; it is still model text, so check it.

    Intended outcome: fix-docs-dispatch.mjs treats a completed fix-docs run that posted no check as handled, with one retry for a lost run, so a trigger-gate stop no longer takes a dispatch slot.

    Acceptance criteria

    • In .github/scripts/pr-unblock/fix-docs-dispatch.mjs, a completed run titled runTitle(workflow, activity, number) for a PR, found in the workflow run listing already read, marks the PR handled when no pr-refine or pr-unblock fix-docs check run exists.
    • fix-docs-dispatch.test.mjs has a test where a PR's only fix-docs run completed (conclusion success or failure) with no check, and dispatchDocsFixes does not dispatch it again.
    • A completed run with conclusion cancelled, timed_out or startup_failure and no check does not mark the PR handled the first time, so the PR is dispatched once more.
    • fix-docs-dispatch.test.mjs has a test that a PR with one such lost run is dispatched, and a test that a PR with a second lost run (or a later completed run without a check) is not dispatched.
    • A same-repository PR whose dispatch stopped at the trigger gate leaves a completed run with no check, and on later dispatcher runs it takes no dispatch slot against maxDispatch.
    • fix-docs-dispatch.test.mjs has a test with two such trigger-gate-stopped PRs of lowest numbers plus another eligible PR, and the eligible PR is dispatched within maxDispatch.
    • The completed-run rule needs no new API call: it uses the listWorkflowRuns result, which is currently filtered to non-completed runs at fix-docs-dispatch.mjs:227.
    • The existing per-head cap (maxRunsPerHead, limit-stops-per-head) tests and the header comment in fix-docs-dispatch.mjs are kept consistent with the new rule, and the fix-docs-dispatch tests pass with the repo's node test command.

    External state: none

    Related work

    Sources read

    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/scripts/pr-unblock/fix-docs-dispatch.mjs:227: listWorkflowRuns result is filtered to status !== completed, so completed runs are dropped now
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/scripts/pr-unblock/fix-docs-dispatch.mjs:163: runTitle builds the run display title 'pr-refine fix-docs #n'
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/scripts/pr-unblock/fix-docs-dispatch.mjs:204-208: a PR is handled only on an existing check run, otherwise it is a candidate
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/scripts/pr-unblock/fix-docs-dispatch.mjs:280-309: per-head cap allows maxRunsPerHead (3) runs counted by broker-allowed job success, not by completed runs without a check
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/scripts/pr-unblock/fix-docs-dispatch.test.mjs: existing test file where the new tests belong
    • refine-data/context.json: the 2026-10-30 decision comment lists the three open fix-docs boxes and marks the fix-ci boxes landed in #​9363

    Automated refinement by the lanes App. Admission counts only criteria a listed human wrote: to confirm these, post them as your own comment whose first line is **Decision recorded.**, which sends the issue back through intake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.priority: mediumReal value, no hard deadline; normal backlog flow.status: readyTriaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions