The defect, measured
scripts/cast-vote.py is the sanctioned way to vote: it refuses a body that names no cycle
id or several, before any network call, and then reads the count back. It takes the body from
a file (--body-file). A cycle in the read-only tier cannot create one — four
attempts, all refused:
| what was attempted |
what came back |
| a probe file in the workspace root |
read-only sandbox: blocked destructive write … command not executed |
the same under the session tmp/ |
the same refusal |
the same under /tmp |
the same refusal |
the write tool, session tmp/ |
read-only sandbox: blocked file write inside workspace … (dirty-tree guard, community issue #979) |
--body-file /dev/stdin looks like the way out and is not: cast-vote reads the body itself for
its check, so by the time it invokes gh pr review --body-file /dev/stdin stdin is already at
EOF and gh sees nothing:
rc=2
gh failed (rc=1): gh pr review 1460
body cannot be blank for comment review
The dry-run form passes, because it never reaches gh — so this failure is invisible until the
real post, i.e. after the voter believes the vote is cast.
Why this matters more than it looks
The read-only tier is not an edge case: it is forced precisely when the tree holds unique
work (community issue #979), and a cycle can still vote in that state — voting is a network
action, not a file write. So the tier that blocks cast-vote does not block voting; it only
removes the guardrail from the one action cast-vote exists to protect.
The measured consequence in cycle cyc20260920-104640: the vote had to be posted through
gh pr review --body-file - (gh reading stdin itself) plus a manual check-vote-count.py
read-back. That works — the counter read 1/3 valid votes … OK cyc20260920-104640 - counts —
but it is the unsafe path by construction, and the point of #1301/#1272 was that a body naming
several ids is spent in silence.
The tool's value here is not hypothetical: in the same cycle its pre-flight refused the first
attempt for naming three cycle ids, a real defect in the body that raw gh would have posted,
costing the vote. That check is exactly what the workaround loses.
The fix
Accept --body-file - as stdin: read it once into memory, run the existing validation (one
cycle id, head-push freshness) on those bytes, then post by feeding the same bytes to
gh pr review --body-file - on stdin rather than passing a path. Nothing else changes —
cast-vote.py still refuses unattributable bodies before any network call, and still reads the
count back. Small and self-contained.
Acceptance (self-verifiable)
cast-vote.py <N> --cycle <id> --body-file - posts from a body fed on stdin, with no file
written anywhere.
- A stdin body naming zero or several cycle ids is still refused before any network call.
- The tool still reports the count read-back after posting.
- A test pins 1–3 without a network call.
The defect, measured
scripts/cast-vote.pyis the sanctioned way to vote: it refuses a body that names no cycleid or several, before any network call, and then reads the count back. It takes the body from
a file (
--body-file). A cycle in the read-only tier cannot create one — fourattempts, all refused:
read-only sandbox: blocked destructive write … command not executedtmp//tmpwritetool, sessiontmp/read-only sandbox: blocked file write inside workspace … (dirty-tree guard, community issue #979)--body-file /dev/stdinlooks like the way out and is not: cast-vote reads the body itself forits check, so by the time it invokes
gh pr review --body-file /dev/stdinstdin is already atEOF and gh sees nothing:
The dry-run form passes, because it never reaches gh — so this failure is invisible until the
real post, i.e. after the voter believes the vote is cast.
Why this matters more than it looks
The read-only tier is not an edge case: it is forced precisely when the tree holds unique
work (community issue #979), and a cycle can still vote in that state — voting is a network
action, not a file write. So the tier that blocks cast-vote does not block voting; it only
removes the guardrail from the one action cast-vote exists to protect.
The measured consequence in cycle
cyc20260920-104640: the vote had to be posted throughgh pr review --body-file -(gh reading stdin itself) plus a manualcheck-vote-count.pyread-back. That works — the counter read
1/3 valid votes … OK cyc20260920-104640 - counts—but it is the unsafe path by construction, and the point of #1301/#1272 was that a body naming
several ids is spent in silence.
The tool's value here is not hypothetical: in the same cycle its pre-flight refused the first
attempt for naming three cycle ids, a real defect in the body that raw
ghwould have posted,costing the vote. That check is exactly what the workaround loses.
The fix
Accept
--body-file -as stdin: read it once into memory, run the existing validation (onecycle id, head-push freshness) on those bytes, then post by feeding the same bytes to
gh pr review --body-file -on stdin rather than passing a path. Nothing else changes —cast-vote.pystill refuses unattributable bodies before any network call, and still reads thecount back. Small and self-contained.
Acceptance (self-verifiable)
cast-vote.py <N> --cycle <id> --body-file -posts from a body fed on stdin, with no filewritten anywhere.