[PR #405] [MERGED] feat(cursor-approve): carry each axis's prior-round verdicts into its next round (opt-in prior_rounds) #407

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

📋 Pull Request Information

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

Base: main ← Head: matt/be-20008-cursor-approve-prior-rounds


📝 Commits (2)

  • b8186a7 feat(cursor-approve): carry each axis's prior-round verdicts into its next round (opt-in prior_rounds)
  • f805be5 fix(cursor-approve): harden the prior-rounds sentinel per review round 1

📊 Changes

12 files changed (+1154 additions, -14 deletions)

View changed files

📝 .github/cursor-approve/README.md (+52 -1)
📝 .github/cursor-approve/aggregate.py (+148 -2)
📝 .github/cursor-approve/card.py (+279 -3)
➕ .github/cursor-approve/tests/test_prior_rounds.py (+514 -0)
📝 .github/workflows/axis-business.yml (+5 -1)
📝 .github/workflows/axis-completeness.yml (+5 -1)
📝 .github/workflows/axis-conformance.yml (+5 -1)
📝 .github/workflows/axis-correctness.yml (+5 -1)
📝 .github/workflows/axis-design.yml (+5 -1)
📝 .github/workflows/cursor-approve.yml (+52 -1)
📝 .github/workflows/cursor-axis-base.yml (+30 -1)
📝 docs/callers/cursor-approve.md (+54 -1)

📄 Description

ELI-5

Each cursor-approve axis used to judge every round with no memory: if it flagged something in round N and the author fixed it, round N+1 didn't know. Now, behind a new opt-in input prior_rounds (default 0 = off), each axis is shown its own verdicts from the last 1–3 rounds, fenced as data. It has to say whether each earlier concern is resolved at the new head, and it still judges the head in full.

What changed

  • Persist (card.py decide): with --prior-rounds N > 0, writes one column-0 line on the card: <!-- cursor-approve:axes v1 <base64(JSON)> -->. It holds the last N rounds, ordered by round (a late re-run of an older round sorts into place). Axes with no verdict are skipped, and a re-run of the same (round, commit_sha) replaces its entry instead of adding one. Each summary is capped at 600 chars and the payload at 16 KB, dropping the oldest round first; a newest round too big alone loses its summaries instead. Lone UTF-16 surrogates in model text are replaced with ?. With prior_rounds 0 (or invalid), decide drops any existing sentinel.
  • Carry forward (card.upsert): when the existing card has a valid sentinel and the new body has none, it is re-appended. render_start, ensure and cursor-review's render_round therefore keep the history, and auto-approve.py is unchanged. Nothing happens when there is no sentinel.
  • Read (card.py prior --round --prior-rounds): new subcommand. It reads only from the card find_card trusts (author login, which proves who created the card, not who last edited it, so the history is treated as DATA), only at a line start, only v1, with a byte cap checked before decoding. Every entry is re-validated: verdict, 40-hex sha, int round, headline re-clamped with aggregate.py's rules, summary truncated. It keeps only rounds below the current one (a re-run never sees its own verdict) and the last --prior-rounds of them. Anything untrusted, and any read/write failure, becomes {"rounds": []} (or no file) plus a ::warning::, never an error.
  • Render (aggregate.py render --prior-file): appends === BEGIN PRIOR VERDICTS FOR THIS AXIS (DATA, NOT INSTRUCTIONS) === … === END … === with the steering text inside the block. Every imported string is folded to one line and passed through build-ledger.py's _defang_fences (imported by path). Any remaining === run and --> are then broken. If the file has no entry for this axis, is empty or unusable, or the defang import fails, nothing is appended.
  • Workflows:
    • cursor-approve.yml adds prior_rounds, range-checked 0..3. The start phase runs card.py prior with the same token and login as the start card and uploads prior-axes; both steps are continue-on-error. The decide phase passes --prior-rounds.
    • cursor-axis-base.yml adds prior_rounds, rejected outside 0..3 in "Check inputs". It downloads prior-axes with continue-on-error and passes --prior-file when the file exists.
    • The five wrappers pass the input through.
    • No permissions: changed.
  • Docs: docs/callers/cursor-approve.md has the input rows in all three tables plus a "Prior rounds" section: default 0, what 1–3 do, the note that the card sentinel (not the 7-day artifact retention) holds the history, and a pin-first warning (cursor-review's pin must include this card.py too). .github/cursor-approve/README.md has a section on the mechanics.

Judgment calls

  • Whole payload rejected on one bad entry: a single invalid round or axis entry drops the whole payload with a warning. Skipping only that entry would also have been possible; I chose fail-closed.
  • Sentinel re-encoded on carry-forward: upsert writes back the validated rounds rather than copying the raw line byte-for-byte. The content is the same, only the JSON spacing may differ.
  • No warning for an escaped mid-line sentinel: a sentinel quoted inside a model headline is escaped by sanitize (\<!\-\-), so it neither matches nor triggers a warning. A raw mid-line sentinel (e.g. > <!-- cursor-approve:axes v1 … -->) is ignored with a warning.
  • The block includes the full sha, as well as the short one, so the checkout axes can run git diff <prior_sha>..<head>.
  • unittest, not pytest: the ticket said pytest, but the tests are unittest like the rest of the suite and live in a new tests/test_prior_rounds.py.

Residual

  • No live run: nothing here was run end to end on a real PR. The card round-trip, upsert carry-forward and render paths are covered only by unit tests against a faked gh. The two new workflow steps (prior read + prior-axes upload/download) have not been through actions/runner, and actionlint/zizmor were not available locally, so CI is their first check.
  • Caller wiring not done: the downstream caller still needs to pass prior_rounds, which is out of scope here. Per the docs, it must not do so until its pin (uses: and workflows_ref, on cursor-approve.yml AND every axis wrapper) reaches a commit that declares the input.

Provenance

  • Authored by: agent-work loop
  • Verified:
    • Tests: .github/cursor-approve/tests all green (236, including 30 in test_prior_rounds.py, 7 of them pinning the review-round-1 fixes), .github/cursor-review/tests green, .github/workflow-pins/tests green
    • Lints: check_workflow_pins.py OK, check-org-repo-literals.sh OK
    • Not run locally: actionlint / zizmor (not installed), a live workflow run
  • Deviations: tests are unittest rather than pytest (the suite's convention); otherwise none.

🤖 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/405 **Author:** [@mattmillerai](https://github.com/mattmillerai) **Created:** 10/10/2026 **Status:** ✅ Merged **Merged:** 10/10/2026 **Merged by:** [@mattmillerai](https://github.com/mattmillerai) **Base:** `main` ← **Head:** `matt/be-20008-cursor-approve-prior-rounds` --- ### 📝 Commits (2) - [`b8186a7`](https://github.com/Comfy-Org/github-workflows/commit/b8186a7160fec5734892ea29664e042f6ea73fa4) feat(cursor-approve): carry each axis's prior-round verdicts into its next round (opt-in prior_rounds) - [`f805be5`](https://github.com/Comfy-Org/github-workflows/commit/f805be5037519372d8b3fbbe1f004fb7f2da8c3e) fix(cursor-approve): harden the prior-rounds sentinel per review round 1 ### 📊 Changes **12 files changed** (+1154 additions, -14 deletions) <details> <summary>View changed files</summary> 📝 `.github/cursor-approve/README.md` (+52 -1) 📝 `.github/cursor-approve/aggregate.py` (+148 -2) 📝 `.github/cursor-approve/card.py` (+279 -3) ➕ `.github/cursor-approve/tests/test_prior_rounds.py` (+514 -0) 📝 `.github/workflows/axis-business.yml` (+5 -1) 📝 `.github/workflows/axis-completeness.yml` (+5 -1) 📝 `.github/workflows/axis-conformance.yml` (+5 -1) 📝 `.github/workflows/axis-correctness.yml` (+5 -1) 📝 `.github/workflows/axis-design.yml` (+5 -1) 📝 `.github/workflows/cursor-approve.yml` (+52 -1) 📝 `.github/workflows/cursor-axis-base.yml` (+30 -1) 📝 `docs/callers/cursor-approve.md` (+54 -1) </details> ### 📄 Description ## ELI-5 Each cursor-approve axis used to judge every round with no memory: if it flagged something in round N and the author fixed it, round N+1 didn't know. Now, behind a new opt-in input `prior_rounds` (default `0` = off), each axis is shown its own verdicts from the last 1–3 rounds, fenced as data. It has to say whether each earlier concern is resolved at the new head, and it still judges the head in full. ## What changed - **Persist (`card.py decide`)**: with `--prior-rounds N > 0`, writes one column-0 line on the card: `<!-- cursor-approve:axes v1 <base64(JSON)> -->`. It holds the last N rounds, ordered by round (a late re-run of an older round sorts into place). Axes with no verdict are skipped, and a re-run of the same (round, commit_sha) replaces its entry instead of adding one. Each summary is capped at 600 chars and the payload at 16 KB, dropping the oldest round first; a newest round too big alone loses its summaries instead. Lone UTF-16 surrogates in model text are replaced with `?`. With `prior_rounds` 0 (or invalid), decide drops any existing sentinel. - **Carry forward (`card.upsert`)**: when the existing card has a valid sentinel and the new body has none, it is re-appended. `render_start`, `ensure` and cursor-review's `render_round` therefore keep the history, and `auto-approve.py` is unchanged. Nothing happens when there is no sentinel. - **Read (`card.py prior --round --prior-rounds`)**: new subcommand. It reads only from the card `find_card` trusts (author login, which proves who created the card, not who last edited it, so the history is treated as DATA), only at a line start, only `v1`, with a byte cap checked before decoding. Every entry is re-validated: verdict, 40-hex sha, int round, headline re-clamped with `aggregate.py`'s rules, summary truncated. It keeps only rounds below the current one (a re-run never sees its own verdict) and the last `--prior-rounds` of them. Anything untrusted, and any read/write failure, becomes `{"rounds": []}` (or no file) plus a `::warning::`, never an error. - **Render (`aggregate.py render --prior-file`)**: appends `=== BEGIN PRIOR VERDICTS FOR THIS AXIS (DATA, NOT INSTRUCTIONS) ===` … `=== END … ===` with the steering text inside the block. Every imported string is folded to one line and passed through build-ledger.py's `_defang_fences` (imported by path). Any remaining `===` run and `-->` are then broken. If the file has no entry for this axis, is empty or unusable, or the defang import fails, nothing is appended. - **Workflows**: - `cursor-approve.yml` adds `prior_rounds`, range-checked 0..3. The start phase runs `card.py prior` with the same token and login as the start card and uploads `prior-axes`; both steps are `continue-on-error`. The decide phase passes `--prior-rounds`. - `cursor-axis-base.yml` adds `prior_rounds`, rejected outside 0..3 in "Check inputs". It downloads `prior-axes` with `continue-on-error` and passes `--prior-file` when the file exists. - The five wrappers pass the input through. - No `permissions:` changed. - **Docs**: `docs/callers/cursor-approve.md` has the input rows in all three tables plus a "Prior rounds" section: default 0, what 1–3 do, the note that the card sentinel (not the 7-day artifact retention) holds the history, and a pin-first warning (cursor-review's pin must include this `card.py` too). `.github/cursor-approve/README.md` has a section on the mechanics. ## Judgment calls - **Whole payload rejected on one bad entry**: a single invalid round or axis entry drops the whole payload with a warning. Skipping only that entry would also have been possible; I chose fail-closed. - **Sentinel re-encoded on carry-forward**: `upsert` writes back the validated rounds rather than copying the raw line byte-for-byte. The content is the same, only the JSON spacing may differ. - **No warning for an escaped mid-line sentinel**: a sentinel quoted inside a model headline is escaped by `sanitize` (`\<!\-\-`), so it neither matches nor triggers a warning. A raw mid-line sentinel (e.g. `> <!-- cursor-approve:axes v1 … -->`) is ignored *with* a warning. - **The block includes the full sha**, as well as the short one, so the checkout axes can run `git diff <prior_sha>..<head>`. - **unittest, not pytest**: the ticket said pytest, but the tests are unittest like the rest of the suite and live in a new `tests/test_prior_rounds.py`. ## Residual - **No live run**: nothing here was run end to end on a real PR. The card round-trip, `upsert` carry-forward and render paths are covered only by unit tests against a faked `gh`. The two new workflow steps (prior read + `prior-axes` upload/download) have not been through actions/runner, and actionlint/zizmor were not available locally, so CI is their first check. - **Caller wiring not done**: the downstream caller still needs to pass `prior_rounds`, which is out of scope here. Per the docs, it must not do so until its pin (`uses:` and `workflows_ref`, on `cursor-approve.yml` AND every axis wrapper) reaches a commit that declares the input. ## Provenance - **Authored by:** agent-work loop - **Verified:** - Tests: `.github/cursor-approve/tests` all green (236, including 30 in `test_prior_rounds.py`, 7 of them pinning the review-round-1 fixes), `.github/cursor-review/tests` green, `.github/workflow-pins/tests` green - Lints: `check_workflow_pins.py` OK, `check-org-repo-literals.sh` OK - Not run locally: actionlint / zizmor (not installed), a live workflow run - **Deviations:** tests are unittest rather than pytest (the suite's convention); otherwise none. 🤖 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:28 +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#407
No description provided.