[PR #350] [MERGED] feat(cursor-approve): axis base, correctness + conformance axes, and the decide workflow #355

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

📋 Pull Request Information

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

Base: main ← Head: matt/be-19325-cursor-approve-axes


📝 Commits (3)

  • 81901e5 feat(cursor-approve): axis base + correctness/conformance axes and the decide workflow
  • dd0391a fix(cursor-approve): close the approval-window, forged-verdict and stale-base gaps the panel found
  • a815cfc Merge origin/main into matt/be-19325-cursor-approve-axes

📊 Changes

15 files changed (+1965 additions, -16 deletions)

View changed files

📝 .github/cursor-approve/aggregate.py (+109 -7)
➕ .github/cursor-approve/card.py (+278 -0)
📝 .github/cursor-approve/tests/test_aggregate.py (+123 -0)
➕ .github/cursor-approve/tests/test_card.py (+242 -0)
📝 .github/cursor-review/auto-approve.py (+197 -6)
➕ .github/cursor-review/tests/test_approve_external.py (+204 -0)
📝 .github/workflow-pins/tests/test_check_workflow_pins.py (+6 -3)
📝 .github/workflow-pins/tests/test_inputs_docs_drift.py (+10 -0)
➕ .github/workflows/axis-conformance.yml (+43 -0)
➕ .github/workflows/axis-correctness.yml (+43 -0)
➕ .github/workflows/cursor-approve.yml (+322 -0)
➕ .github/workflows/cursor-axis-base.yml (+205 -0)
📝 .github/workflows/test-cursor-review-scripts.yml (+10 -0)
📝 README.md (+1 -0)
➕ docs/callers/cursor-approve.md (+172 -0)

📄 Description

ELI-5

Once a cursor-review round passes, two new "axis" reviewers (correctness, conformance) each judge the PR on their own and return green/yellow/red. A new cursor-approve.yml collects their verdicts, keeps one live status card on the PR, and approves only when no axis is red and few enough are yellow. It goes through the existing auto-approve protections.

What changed

  • .github/workflows/cursor-axis-base.yml is the shared axis body. It renders the prompt (aggregate.py render), installs the pinned cursor-agent with install-cursor-cli.sh, runs it over a read-only full-history checkout of the PR head (persist-credentials: false), extracts exactly one JSON verdict (new aggregate.py extract, which fails closed on duplicate keys, a second "verdict" key or an over-cap reply) stamped with the judged commit_sha, and uploads axis-<name>. The job has contents: read only, a 20-min timeout, refuses fork PRs, and a failed run uploads nothing. mcp_config and no_shell are reserved extension points that fail if set.
  • axis-correctness.yml and axis-conformance.yml are thin wrappers that declare only CURSOR_API_KEY. A test enforces this.
  • cursor-approve.yml has phase: start|decide. start first withdraws the approver's own approvals, because cursor-review has already approved when approve_gate == 'pass'. In the caller example the axes needs: approve-start. decide reads each axis only from its own axis-<name> artifact, and only for commit_sha. Its guard step is a byte-for-byte copy of cursor-review's (a test enforces this). It holds APPROVER_TOKEN; if the token is empty it warns and exits 0, with no fallback. It never checks out PR code.
  • .github/cursor-approve/card.py is the status card. It finds the card by <!-- cursor-approve-card --> and the token's own login, and edits it in place. It covers four states: pending, verdicts + result + rule, superseded, round-limit. Model strings go through neutralize_mentions + markdown/HTML escaping.
  • auto-approve.py approve-external is the new subcommand. It re-reads the head and base: if the head moved or the base differs from the axes' --base-ref, it posts no review (outcome superseded) and withdraws only approvals that are stale against the live head and base. never approves over needs-human-review, re-checks the decision itself (every expected axis green/yellow), and adds the reviewed-SHA + base markers. It withdraws its own earlier approvals on every other non-approval and re-reads after the POST. A new withdraw subcommand backs the start phase. An empty approver login is refused everywhere. The approval body is one line that links the card.
  • Docs: docs/callers/cursor-approve.md with the example caller, plus a README row. The workflow-pins guarded-site count goes 20 → 21, and docs-drift rows are added for the four new reusables.

Judgment calls

  • The base loads its scripts at job.workflow_sha, not a workflows_ref input. The ticket's base input list has no ref, and the wrappers can't pin their own SHA. The base fails closed when job.workflow_sha is empty, so it never falls back to the default branch.
  • The wrappers call the base via ./.github/workflows/cursor-axis-base.yml. GitHub documents a ./ call as resolving to the same commit as the calling workflow. I have not checked this against a live cross-repo run (see Residual).
  • Sanitizer: a summary is not a path, so card text uses neutralize_mentions plus backslash-escaping of markdown/HTML characters rather than render_code_ref's code-span rendering. Escaping < also breaks a forged marker.
  • The CLI pin is duplicated into the base's env:. A test asserts it equals cursor-review.yml's pin.
  • The decide job runs approve-external under always(). A failed aggregate.py decide then still withdraws an earlier approval instead of leaving it in place.
  • The example caller's decide job uses always(). A failed axis then reads as "no result" instead of skipping the decision.

Residual

  • No live run. No workflow here has run on GitHub: nested ./ reusable resolution from a consumer repo, the artifact handoff between the axis jobs and the decide job, the cursor-agent invocation and the prompt's {{context_file}} path (written under the checkout's .git/) are all untested end to end. A first pilot caller should confirm them.
  • actionlint is not clean. actionlint v1.7.12 reports job.workflow_sha as an unknown property, plus shellcheck SC2018/SC2019 info notes on the guard copied byte-for-byte. main's cursor-review.yml already has the same findings (12 + 22 + 22), so they are not new. No other findings.
  • No caller enrolled. Enrolling a consumer is out of scope. No roster secret exists for these workflows yet; the bump-callers fleet for cursor-approve/axis-* needs its own wiring before any caller can be SHA-bumped automatically.
  • The context axes (business, design, completeness) are not built. mcp_config and no_shell are reserved for them but not implemented.

Provenance

  • Authored by: agent-work loop
  • Verified:
    • Tests: all green at the current head. .github/cursor-approve/tests (80), .github/cursor-review/tests (842), .github/workflow-pins/tests (530), .github/agents-md-integrity/tests (46)
    • Lints: check_workflow_pins.py OK, check_agents_md.py passed, check-org-repo-literals.sh OK
    • actionlint: only the findings main's cursor-review.yml already has (see Residual)
    • Not run: any live workflow run
  • Deviations: the base takes no workflows_ref (it loads at job.workflow_sha). The ticket's actionlint-clean criterion is met only up to the findings main's cursor-review.yml already has.

🔄 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/350 **Author:** [@mattmillerai](https://github.com/mattmillerai) **Created:** 10/7/2026 **Status:** ✅ Merged **Merged:** 10/7/2026 **Merged by:** [@mattmillerai](https://github.com/mattmillerai) **Base:** `main` ← **Head:** `matt/be-19325-cursor-approve-axes` --- ### 📝 Commits (3) - [`81901e5`](https://github.com/Comfy-Org/github-workflows/commit/81901e56fe6c1f050b7ee1c27282b00b494fb5f0) feat(cursor-approve): axis base + correctness/conformance axes and the decide workflow - [`dd0391a`](https://github.com/Comfy-Org/github-workflows/commit/dd0391a887f7e799c6a23ff2b7f52211e8331857) fix(cursor-approve): close the approval-window, forged-verdict and stale-base gaps the panel found - [`a815cfc`](https://github.com/Comfy-Org/github-workflows/commit/a815cfc02430a17373e6405552ae30d78d7c3eae) Merge origin/main into matt/be-19325-cursor-approve-axes ### 📊 Changes **15 files changed** (+1965 additions, -16 deletions) <details> <summary>View changed files</summary> 📝 `.github/cursor-approve/aggregate.py` (+109 -7) ➕ `.github/cursor-approve/card.py` (+278 -0) 📝 `.github/cursor-approve/tests/test_aggregate.py` (+123 -0) ➕ `.github/cursor-approve/tests/test_card.py` (+242 -0) 📝 `.github/cursor-review/auto-approve.py` (+197 -6) ➕ `.github/cursor-review/tests/test_approve_external.py` (+204 -0) 📝 `.github/workflow-pins/tests/test_check_workflow_pins.py` (+6 -3) 📝 `.github/workflow-pins/tests/test_inputs_docs_drift.py` (+10 -0) ➕ `.github/workflows/axis-conformance.yml` (+43 -0) ➕ `.github/workflows/axis-correctness.yml` (+43 -0) ➕ `.github/workflows/cursor-approve.yml` (+322 -0) ➕ `.github/workflows/cursor-axis-base.yml` (+205 -0) 📝 `.github/workflows/test-cursor-review-scripts.yml` (+10 -0) 📝 `README.md` (+1 -0) ➕ `docs/callers/cursor-approve.md` (+172 -0) </details> ### 📄 Description ## ELI-5 Once a cursor-review round passes, two new "axis" reviewers (correctness, conformance) each judge the PR on their own and return green/yellow/red. A new `cursor-approve.yml` collects their verdicts, keeps one live status card on the PR, and approves only when no axis is red and few enough are yellow. It goes through the existing auto-approve protections. ## What changed - `.github/workflows/cursor-axis-base.yml` is the shared axis body. It renders the prompt (`aggregate.py render`), installs the pinned `cursor-agent` with `install-cursor-cli.sh`, runs it over a read-only full-history checkout of the PR head (`persist-credentials: false`), extracts exactly one JSON verdict (new `aggregate.py extract`, which fails closed on duplicate keys, a second `"verdict"` key or an over-cap reply) stamped with the judged `commit_sha`, and uploads `axis-<name>`. The job has `contents: read` only, a 20-min timeout, refuses fork PRs, and a failed run uploads nothing. `mcp_config` and `no_shell` are reserved extension points that fail if set. - `axis-correctness.yml` and `axis-conformance.yml` are thin wrappers that declare **only** `CURSOR_API_KEY`. A test enforces this. - `cursor-approve.yml` has `phase: start|decide`. `start` first withdraws the approver's own approvals, because cursor-review has already approved when `approve_gate == 'pass'`. In the caller example the axes `needs: approve-start`. `decide` reads each axis only from its own `axis-<name>` artifact, and only for `commit_sha`. Its guard step is a byte-for-byte copy of cursor-review's (a test enforces this). It holds `APPROVER_TOKEN`; if the token is empty it warns and exits 0, with no fallback. It never checks out PR code. - `.github/cursor-approve/card.py` is the status card. It finds the card by `<!-- cursor-approve-card -->` **and** the token's own login, and edits it in place. It covers four states: pending, verdicts + result + rule, superseded, round-limit. Model strings go through `neutralize_mentions` + markdown/HTML escaping. - `auto-approve.py approve-external` is the new subcommand. It re-reads the head and base: if the head moved or the base differs from the axes' `--base-ref`, it posts no review (outcome `superseded`) and withdraws only approvals that are stale against the live head and base. never approves over `needs-human-review`, re-checks the decision itself (every expected axis green/yellow), and adds the reviewed-SHA + base markers. It withdraws its own earlier approvals on every other non-approval and re-reads after the POST. A new `withdraw` subcommand backs the start phase. An empty approver login is refused everywhere. The approval body is one line that links the card. - Docs: `docs/callers/cursor-approve.md` with the example caller, plus a README row. The workflow-pins guarded-site count goes 20 → 21, and docs-drift rows are added for the four new reusables. ## Judgment calls - **The base loads its scripts at `job.workflow_sha`, not a `workflows_ref` input.** The ticket's base input list has no ref, and the wrappers can't pin their own SHA. The base fails closed when `job.workflow_sha` is empty, so it never falls back to the default branch. - **The wrappers call the base via `./.github/workflows/cursor-axis-base.yml`.** GitHub documents a `./` call as resolving to the same commit as the calling workflow. I have not checked this against a live cross-repo run (see Residual). - **Sanitizer:** a summary is not a path, so card text uses `neutralize_mentions` plus backslash-escaping of markdown/HTML characters rather than `render_code_ref`'s code-span rendering. Escaping `<` also breaks a forged marker. - **The CLI pin is duplicated into the base's `env:`.** A test asserts it equals cursor-review.yml's pin. - **The decide job runs `approve-external` under `always()`.** A failed `aggregate.py decide` then still withdraws an earlier approval instead of leaving it in place. - **The example caller's decide job uses `always()`.** A failed axis then reads as "no result" instead of skipping the decision. ## Residual - **No live run.** No workflow here has run on GitHub: nested `./` reusable resolution from a consumer repo, the artifact handoff between the axis jobs and the decide job, the `cursor-agent` invocation and the prompt's `{{context_file}}` path (written under the checkout's `.git/`) are all untested end to end. A first pilot caller should confirm them. - **actionlint is not clean.** actionlint v1.7.12 reports `job.workflow_sha` as an unknown property, plus shellcheck SC2018/SC2019 info notes on the guard copied byte-for-byte. main's `cursor-review.yml` already has the same findings (12 + 22 + 22), so they are not new. No other findings. - **No caller enrolled.** Enrolling a consumer is out of scope. No roster secret exists for these workflows yet; the bump-callers fleet for cursor-approve/axis-* needs its own wiring before any caller can be SHA-bumped automatically. - **The context axes (business, design, completeness) are not built.** `mcp_config` and `no_shell` are reserved for them but not implemented. ## Provenance - **Authored by:** agent-work loop - **Verified:** - Tests: all green at the current head. `.github/cursor-approve/tests` (80), `.github/cursor-review/tests` (842), `.github/workflow-pins/tests` (530), `.github/agents-md-integrity/tests` (46) - Lints: `check_workflow_pins.py` OK, `check_agents_md.py` passed, `check-org-repo-literals.sh` OK - actionlint: only the findings main's cursor-review.yml already has (see Residual) - Not run: any live workflow run - **Deviations:** the base takes no `workflows_ref` (it loads at `job.workflow_sha`). The ticket's actionlint-clean criterion is met only up to the findings main's cursor-review.yml already has. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-26 22:38:56 +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#355
No description provided.