[PR #434] [MERGED] feat(cursor-review): axes_alongside_panel — run cursor-approve's start phase and the gating axes beside the panel #435

Closed
opened 2026-10-26 22:39:47 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/Comfy-Org/github-workflows/pull/434
Author: @mattmillerai
Created: 10/15/2026
Status: ✅ Merged
Merged: 10/16/2026
Merged by: @mattmillerai

Base: main ← Head: be-20927-axes-alongside-panel


📝 Commits (2)

  • d69d32a feat(cursor-review): axes_alongside_panel — run cursor-approve's start phase and the gating axes beside the panel
  • a17a8ec fix(cursor-review): harden axes_alongside_panel per the panel's review

📊 Changes

14 files changed (+1011 additions, -24 deletions)

View changed files

📝 .github/cursor-approve/card.py (+121 -4)
📝 .github/cursor-approve/tests/test_card.py (+103 -0)
📝 .github/cursor-review/README.md (+5 -1)
📝 .github/cursor-review/auto-approve.py (+21 -3)
📝 .github/cursor-review/tests/test_approve_external.py (+11 -0)
📝 .github/cursor-review/tests/test_auto_approve.py (+1 -1)
➕ .github/cursor-review/tests/test_workflow_axes_alongside.py (+233 -0)
📝 .github/cursor-review/tests/test_workflow_job_isolation.py (+27 -3)
📝 .github/cursor-review/tests/test_workflow_ref_guard_parity.py (+1 -1)
📝 .github/workflow-pins/tests/test_check_workflow_pins.py (+5 -2)
📝 .github/workflows/cursor-approve.yml (+54 -3)
📝 .github/workflows/cursor-review.yml (+322 -3)
📝 docs/callers/cursor-approve.md (+100 -2)
📝 docs/callers/cursor-review.md (+7 -1)

📄 Description

ELI5

When a pull request passes automated review, it currently waits for the review panel to finish and then waits about five more minutes for a second set of checks. This adds an opt-in switch that runs those second checks at the same time as the panel, so a passing pull request gets approved about five minutes sooner. With the switch off, nothing changes.

Summary

New cursor-review.yml input axes_alongside_panel (boolean, default false). When it is on, cursor-approve's start phase and the gating axes (correctness, conformance) start as soon as round-cap has run, instead of after the whole reusable finishes. Every job it adds has inputs.axes_alongside_panel as a top-level && conjunct of its if:, so with the input off they are all skipped.

  • approve-start-early: uses: ./.github/workflows/cursor-approve.yml, phase: start, approve_gate = pending (or capped at the cap), round = round-cap's real next_round (the cap's rounds when capped).
    • It requires defer_approval: true. Without it the early start is skipped and the caller's own start and axes run as before.
    • It runs only for a round within the diff-size cap whose preflight passed, or for the capped hand-off, which runs no axis.
    • It is ordered after dismiss-stale-approval, and post-review is ordered after it, so a late pending card cannot overwrite decide's round card.
  • axis-correctness / axis-conformance: !cancelled() && … && needs.approve-start-early.result == 'success'. Skipped when capped or labelled. The axis list comes from the start phase's new normalized axes output. CURSOR_API_KEY and ANTHROPIC_API_KEY are passed explicitly, never inherit. They are not in panel-integrity's or blocking-gate's needs:.
  • axes-closeout: runs under !cancelled() for any start that ran, after the axis jobs. It checks out this repo at workflows_ref (with its own copy of the guard step), resolves the approver login, and runs the new card.py closeout with the workflow-level approve_gate expression rewritten over needs (a test pins the two equal). It does nothing when APPROVER_TOKEN is empty.
  • New outputs:
    • axes_started (= jobs.approve-start-early.result).
    • axes_ok: false when an axis job started here did not succeed. Artifact names are shared across the whole run, so a panel cell running beside the axes could claim axis-<name> first; the real upload then fails. The documented decide if: therefore adds (axes_started != 'success' || axes_ok == 'true'). New inputs axes, axis_prior_rounds, axis_direct_model. All are documented in the header comment, the input descriptions, docs/callers/cursor-review.md, .github/cursor-review/README.md, and a new "Axes alongside the panel" section in docs/callers/cursor-approve.md. The cursor-approve guide's status-card table is updated: the pass row now covers the early card, and there is a new close-out no_decision/push_then_relabel row.
  • card.py:
    • render_start(..., "pending") keeps the pass/none head and the pending table, adds a "panel is still running" line, and leaves out LANE_MARKER. This is load-bearing: approve_only_blocker trusts in_axes_lane to let a push skip the panel.
    • New closeout subcommand. It rewrites the card only when all of these hold: it is a pass card for the same SHA, round and workflow run; it has at least one ⏳ pending row; and the gate is not pass, or --axes-ok false is given. On any failure it warns and exits 0.
  • cursor-approve.yml: a new Check the axes step normalizes axes and exports it as the workflow output axes. Under approve_gate: pending it fails unless axes is a non-empty subset of correctness,conformance.
  • auto-approve.py:
    • withdraw --approve-gate pending is handled as pass: it withdraws this identity's approvals and leaves the standing REQUEST_CHANGES alone.
    • The approve-external doc block now says the caller's approve_gate == 'pass' if: is the only panel-pass guard.

Judgment calls / deviations from the plan

  1. approve-start-early grants contents: read only, and cursor-approve.yml's approve job no longer requests actions: read. The plan specified {contents: read, actions: read}.
    • A nested job cannot exceed its caller's grant, and GitHub checks this at startup for every job whatever its if: says. The documented cursor-review caller (and this repo's own ci-cursor-review.yml) grants only contents + pull-requests.
    • So the plan as written would have made every cursor-review caller fail at startup on the next pin bump, input on or off. This file's gate job comment already records the same trap for issues: read.
    • Chesterton check: actions: read arrived with the initial cursor-approve workflow, and that commit gives no reason for it. No step in cursor-approve.yml calls the Actions API. The download-artifact steps pass no github-token/run-id, and same-run artifact upload/download goes through the runtime token, not an actions: scope.
    • Dropping a requested scope can never break a caller that grants it. A new test asserts that no job in cursor-review.yml, cursor-approve.yml, axis-*.yml or cursor-axis-base.yml asks for more than the documented caller grants. I checked by re-adding the scope: the test fails.
  2. axes-closeout also needs: gate and approve-only. The plan said "the same expression as the workflow-level approve_gate output, computed from needs", and that expression reads both jobs.
  3. The axis list is normalized and validated in cursor-approve's start, not matched raw. The first draft also accepted ' <axis>,'; review showed that still missed other spellings and unsupported axes. The axis if:s now read the start's normalized axes output.
  4. withdraw adds no validation that rejects other values. Today it accepts any value and only treats capped specially. Adding a rejection would change existing behaviour, so it is unchanged apart from pending → pass.

Testing

  • python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py': 1732 OK (at a17a8ec).
    • New test_workflow_axes_alongside.py checks:
      • every new job has the flag as a top-level conjunct, and no other job does;
      • the new jobs are not in panel-integrity/blocking-gate needs:;
      • the start phase is pending/capped, never pass;
      • the axes need a successful start and get explicit secrets;
      • the close-out gate equals the workflow output;
      • axes_started wiring;
      • no nested job asks for more than the caller grants.
    • I mutation-checked two of these: an || beside the flag fails the test, and so does actions: read.
    • Also new: a withdraw test for pending. The isolation, guard-parity and guard-count tests were updated for the new jobs.
  • python3 -m unittest discover -s .github/cursor-approve/tests -p 'test_*.py': 338 OK. New card tests:
    • a pending start card has no lane marker and in_axes_lane is False on it;
    • closeout leaves decided cards alone, and cards for another SHA or round;
    • closeout rewrites on untrusted/fail/off, and is idempotent;
    • closeout leaves a card written by another run alone, and rewrites a pass card whose axis job failed;
    • the CLI edits only the approver's card and never fails.
  • .github/workflow-pins/tests: 551 OK. check_workflow_pins.py, check_agents_md.py --root ., check-org-repo-literals.sh and shellcheck are all clean. Every new uses: is a ./ local path.
  • actionlint (no shellcheck) compared with main: one new finding, property "result" is not defined on jobs.approve-start-early.result in the axes_started output. GitHub documents jobs.<job_id>.result for reusable-workflow outputs, so this is actionlint's stale type, not a bug. Everything else matches the baseline job.workflow_sha noise.
  • Doc-drift sweep: 41 references checked, 5 updated. The sweep listed 1644 lines, mostly generic tokens (read, write, contents). I judged only the lane-marker, approver-environment-job-list, approve_gate, actions: read and axis-workflow references. The five stale ones were: the lane-marker bullet in docs/callers/cursor-approve.md, the LANE_MARKER comment in card.py, the approver_environment job list in the workflow and in .github/cursor-review/README.md, and the approve_gate input doc in cursor-approve.yml's header.

Residual

  • Not exercised live. No run of a real caller with axes_alongside_panel: true has happened; that needs the separate caller change, which is out of scope here. In particular:

    • the four-level nesting;
    • the nested axis artifacts being visible to the caller's decide job;
    • jobs.<id>.result as an output value.

    All of these are documented GitHub behaviour but were only checked structurally. The first caller to enable it should watch one passing round and one failing round end to end.

  • actions: read removal is reasoned, not run. No live run shows that cursor-approve's decide phase works without actions: read. The decide phase downloads artifacts by name within the same run, which needs no actions: scope. The next live cursor-approve decide run on this SHA is the confirmation.

  • Artifact delete-and-reupload during the consolidate → post-review handoff. v4 artifacts are immutable, and claiming cursor-review-consolidated first fails consolidate, which post-review refuses. That existing defense still holds. An axis agent with a shell now runs while that handoff happens. Whether the runner's artifact token can delete an artifact in the same run, and so swap one in that window, has not been demonstrated or ruled out.

  • max_rounds: 0 with axis_prior_rounds > 0. With no cap, round-cap emits no next_round, so the early start passes an empty round. card.py prior then keeps every recorded round. The caller-side start behaves the same today, because cursor-review's round output is empty too.

Provenance

  • Authored by: agent-work loop
  • Verified:
    • cursor-review (1732), cursor-approve (338) and workflow-pins (551) unit suites pass at a17a8ec.
    • The pin, agents-md and org-repo-literal lints are clean.
    • The structural tests were mutation-checked: dropping defer_approval, or switching the close-out back to always(), fails them.
    • The workflow has not been run live (see Residual).
  • Deviations:
    • actions: read was removed from cursor-approve.yml and is not granted on the new nested call, because it would fail every caller at startup.
    • axes-closeout also needs gate, approve-only and both axis jobs.
    • The axes list is normalized and validated in cursor-approve's start instead of being matched raw.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/Comfy-Org/github-workflows/pull/434 **Author:** [@mattmillerai](https://github.com/mattmillerai) **Created:** 10/15/2026 **Status:** ✅ Merged **Merged:** 10/16/2026 **Merged by:** [@mattmillerai](https://github.com/mattmillerai) **Base:** `main` ← **Head:** `be-20927-axes-alongside-panel` --- ### 📝 Commits (2) - [`d69d32a`](https://github.com/Comfy-Org/github-workflows/commit/d69d32a150046a1021fb428d0063ea4bf24a7355) feat(cursor-review): axes_alongside_panel — run cursor-approve's start phase and the gating axes beside the panel - [`a17a8ec`](https://github.com/Comfy-Org/github-workflows/commit/a17a8ec79945203eeaa68beb471a3a3a678b0829) fix(cursor-review): harden axes_alongside_panel per the panel's review ### 📊 Changes **14 files changed** (+1011 additions, -24 deletions) <details> <summary>View changed files</summary> 📝 `.github/cursor-approve/card.py` (+121 -4) 📝 `.github/cursor-approve/tests/test_card.py` (+103 -0) 📝 `.github/cursor-review/README.md` (+5 -1) 📝 `.github/cursor-review/auto-approve.py` (+21 -3) 📝 `.github/cursor-review/tests/test_approve_external.py` (+11 -0) 📝 `.github/cursor-review/tests/test_auto_approve.py` (+1 -1) ➕ `.github/cursor-review/tests/test_workflow_axes_alongside.py` (+233 -0) 📝 `.github/cursor-review/tests/test_workflow_job_isolation.py` (+27 -3) 📝 `.github/cursor-review/tests/test_workflow_ref_guard_parity.py` (+1 -1) 📝 `.github/workflow-pins/tests/test_check_workflow_pins.py` (+5 -2) 📝 `.github/workflows/cursor-approve.yml` (+54 -3) 📝 `.github/workflows/cursor-review.yml` (+322 -3) 📝 `docs/callers/cursor-approve.md` (+100 -2) 📝 `docs/callers/cursor-review.md` (+7 -1) </details> ### 📄 Description ## ELI5 When a pull request passes automated review, it currently waits for the review panel to finish and *then* waits about five more minutes for a second set of checks. This adds an opt-in switch that runs those second checks at the same time as the panel, so a passing pull request gets approved about five minutes sooner. With the switch off, nothing changes. ## Summary New `cursor-review.yml` input `axes_alongside_panel` (boolean, default `false`). When it is on, cursor-approve's `start` phase and the gating axes (correctness, conformance) start as soon as `round-cap` has run, instead of after the whole reusable finishes. Every job it adds has `inputs.axes_alongside_panel` as a top-level `&&` conjunct of its `if:`, so with the input off they are all skipped. - **`approve-start-early`**: `uses: ./.github/workflows/cursor-approve.yml`, `phase: start`, `approve_gate` = `pending` (or `capped` at the cap), `round` = round-cap's real `next_round` (the cap's `rounds` when capped). - It requires `defer_approval: true`. Without it the early start is skipped and the caller's own start and axes run as before. - It runs only for a round within the diff-size cap whose preflight passed, or for the capped hand-off, which runs no axis. - It is ordered after `dismiss-stale-approval`, and `post-review` is ordered after it, so a late pending card cannot overwrite decide's round card. - **`axis-correctness` / `axis-conformance`**: `!cancelled() && … && needs.approve-start-early.result == 'success'`. Skipped when capped or labelled. The axis list comes from the start phase's new normalized `axes` output. `CURSOR_API_KEY` and `ANTHROPIC_API_KEY` are passed explicitly, never `inherit`. They are not in `panel-integrity`'s or `blocking-gate`'s `needs:`. - **`axes-closeout`**: runs under `!cancelled()` for any start that ran, after the axis jobs. It checks out this repo at `workflows_ref` (with its own copy of the guard step), resolves the approver login, and runs the new `card.py closeout` with the workflow-level `approve_gate` expression rewritten over `needs` (a test pins the two equal). It does nothing when `APPROVER_TOKEN` is empty. - **New outputs**: - `axes_started` (= `jobs.approve-start-early.result`). - `axes_ok`: `false` when an axis job started here did not succeed. Artifact names are shared across the whole run, so a panel cell running beside the axes could claim `axis-<name>` first; the real upload then fails. The documented decide `if:` therefore adds `(axes_started != 'success' || axes_ok == 'true')`. New inputs `axes`, `axis_prior_rounds`, `axis_direct_model`. All are documented in the header comment, the input descriptions, `docs/callers/cursor-review.md`, `.github/cursor-review/README.md`, and a new "Axes alongside the panel" section in `docs/callers/cursor-approve.md`. The cursor-approve guide's status-card table is updated: the `pass` row now covers the early card, and there is a new close-out `no_decision`/`push_then_relabel` row. - **`card.py`**: - `render_start(..., "pending")` keeps the `pass`/`none` head and the pending table, adds a "panel is still running" line, and leaves out **`LANE_MARKER`**. This is load-bearing: `approve_only_blocker` trusts `in_axes_lane` to let a push skip the panel. - New `closeout` subcommand. It rewrites the card only when all of these hold: it is a `pass` card for the same SHA, round **and workflow run**; it has at least one `⏳ pending` row; and the gate is not `pass`, or `--axes-ok false` is given. On any failure it warns and exits 0. - **`cursor-approve.yml`**: a new `Check the axes` step normalizes `axes` and exports it as the workflow output `axes`. Under `approve_gate: pending` it fails unless `axes` is a non-empty subset of correctness,conformance. - **`auto-approve.py`**: - `withdraw --approve-gate pending` is handled as `pass`: it withdraws this identity's approvals and leaves the standing REQUEST_CHANGES alone. - The `approve-external` doc block now says the caller's `approve_gate == 'pass'` `if:` is the only panel-pass guard. ### Judgment calls / deviations from the plan 1. **`approve-start-early` grants `contents: read` only, and `cursor-approve.yml`'s `approve` job no longer requests `actions: read`.** The plan specified `{contents: read, actions: read}`. - A nested job cannot exceed its caller's grant, and GitHub checks this at startup for every job whatever its `if:` says. The documented cursor-review caller (and this repo's own `ci-cursor-review.yml`) grants only `contents` + `pull-requests`. - So the plan as written would have made **every** cursor-review caller fail at startup on the next pin bump, input on or off. This file's `gate` job comment already records the same trap for `issues: read`. - Chesterton check: `actions: read` arrived with the initial cursor-approve workflow, and that commit gives no reason for it. No step in `cursor-approve.yml` calls the Actions API. The `download-artifact` steps pass no `github-token`/`run-id`, and same-run artifact upload/download goes through the runtime token, not an `actions:` scope. - Dropping a requested scope can never break a caller that grants it. A new test asserts that no job in `cursor-review.yml`, `cursor-approve.yml`, `axis-*.yml` or `cursor-axis-base.yml` asks for more than the documented caller grants. I checked by re-adding the scope: the test fails. 2. **`axes-closeout` also `needs:` `gate` and `approve-only`.** The plan said "the same expression as the workflow-level `approve_gate` output, computed from needs", and that expression reads both jobs. 3. **The axis list is normalized and validated in cursor-approve's start, not matched raw.** The first draft also accepted `' <axis>,'`; review showed that still missed other spellings and unsupported axes. The axis `if:`s now read the start's normalized `axes` output. 4. **`withdraw` adds no validation that rejects other values.** Today it accepts any value and only treats `capped` specially. Adding a rejection would change existing behaviour, so it is unchanged apart from `pending` → `pass`. ## Testing - `python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py'`: 1732 OK (at `a17a8ec`). - New `test_workflow_axes_alongside.py` checks: - every new job has the flag as a top-level conjunct, and no other job does; - the new jobs are not in `panel-integrity`/`blocking-gate` `needs:`; - the start phase is `pending`/`capped`, never `pass`; - the axes need a successful start and get explicit secrets; - the close-out gate equals the workflow output; - `axes_started` wiring; - no nested job asks for more than the caller grants. - I mutation-checked two of these: an `||` beside the flag fails the test, and so does `actions: read`. - Also new: a `withdraw` test for `pending`. The isolation, guard-parity and guard-count tests were updated for the new jobs. - `python3 -m unittest discover -s .github/cursor-approve/tests -p 'test_*.py'`: 338 OK. New card tests: - a `pending` start card has no lane marker and `in_axes_lane` is False on it; - closeout leaves decided cards alone, and cards for another SHA or round; - closeout rewrites on `untrusted`/`fail`/`off`, and is idempotent; - closeout leaves a card written by another run alone, and rewrites a `pass` card whose axis job failed; - the CLI edits only the approver's card and never fails. - `.github/workflow-pins/tests`: 551 OK. `check_workflow_pins.py`, `check_agents_md.py --root .`, `check-org-repo-literals.sh` and shellcheck are all clean. Every new `uses:` is a `./` local path. - `actionlint` (no shellcheck) compared with `main`: one new finding, `property "result" is not defined` on `jobs.approve-start-early.result` in the `axes_started` output. GitHub documents `jobs.<job_id>.result` for reusable-workflow outputs, so this is actionlint's stale type, not a bug. Everything else matches the baseline `job.workflow_sha` noise. - Doc-drift sweep: 41 references checked, 5 updated. The sweep listed 1644 lines, mostly generic tokens (`read`, `write`, `contents`). I judged only the lane-marker, approver-environment-job-list, `approve_gate`, `actions: read` and axis-workflow references. The five stale ones were: the lane-marker bullet in `docs/callers/cursor-approve.md`, the `LANE_MARKER` comment in `card.py`, the `approver_environment` job list in the workflow and in `.github/cursor-review/README.md`, and the `approve_gate` input doc in `cursor-approve.yml`'s header. ## Residual - **Not exercised live.** No run of a real caller with `axes_alongside_panel: true` has happened; that needs the separate caller change, which is out of scope here. In particular: - the four-level nesting; - the nested axis artifacts being visible to the caller's decide job; - `jobs.<id>.result` as an output value. All of these are documented GitHub behaviour but were only checked structurally. The first caller to enable it should watch one passing round and one failing round end to end. - **`actions: read` removal is reasoned, not run.** No live run shows that cursor-approve's decide phase works without `actions: read`. The decide phase downloads artifacts by name within the same run, which needs no `actions:` scope. The next live cursor-approve decide run on this SHA is the confirmation. - **Artifact delete-and-reupload during the consolidate → post-review handoff.** v4 artifacts are immutable, and claiming `cursor-review-consolidated` first fails `consolidate`, which `post-review` refuses. That existing defense still holds. An axis agent with a shell now runs while that handoff happens. Whether the runner's artifact token can *delete* an artifact in the same run, and so swap one in that window, has not been demonstrated or ruled out. - **`max_rounds: 0` with `axis_prior_rounds` > 0.** With no cap, round-cap emits no `next_round`, so the early start passes an empty `round`. `card.py prior` then keeps every recorded round. The caller-side start behaves the same today, because cursor-review's `round` output is empty too. ## Provenance - **Authored by:** agent-work loop - **Verified:** - cursor-review (1732), cursor-approve (338) and workflow-pins (551) unit suites pass at `a17a8ec`. - The pin, agents-md and org-repo-literal lints are clean. - The structural tests were mutation-checked: dropping `defer_approval`, or switching the close-out back to `always()`, fails them. - The workflow has not been run live (see Residual). - **Deviations:** - `actions: read` was removed from cursor-approve.yml and is not granted on the new nested call, because it would fail every caller at startup. - `axes-closeout` also needs `gate`, `approve-only` and both axis jobs. - The axes list is normalized and validated in cursor-approve's start instead of being matched raw. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-26 22:39:47 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Comfy-Org/github-workflows#435
No description provided.