tooling-gap: no pr-edit.sh — the wrapper set can edit an issue but cannot edit a PR, so draft/undraft is unreachable through the mandated path #1080

Open
opened 2026-08-06 07:49:05 +00:00 by Mos · 2 comments
Contributor

The sanctioned wrapper set can edit an issue and cannot edit a pull request. issue-edit.sh exists; there is no pr-edit.sh. pr-create.sh supports --draft at creation only — nothing converts an existing PR to or from draft.

Measured

git/ wrappers, issue side : issue-create · issue-edit · issue-comment · issue-close · issue-reopen
git/ wrappers, PR side    : pr-create · pr-view · pr-list · pr-diff · pr-metadata · pr-review
                            pr-ci-wait · pr-merge · pr-close
pr-edit.sh                : ABSENT
pr-create.sh:80           : "-d, --draft   Create as draft PR"   <- creation only
CONTROL: issue-edit.sh contains 6 references to a title flag => the search reads these files

Why it matters, from a case that just occurred

A PR can reach a state where every provider-visible gate reads GO — current approvals, terminal-green CI, mergeable=True — while a real blocker exists that lives outside the provider entirely (a tool version on the executing host, a deployment gap, an unmet requirement raised out of band).

In that state, a merge executor consulting only the API merges correctly by every signal available to it, and does the wrong thing. The correct disposal is to put the hold on the surface the merger reads, and draft status is the right instrument for it:

  • it is honoured by every merge path regardless of approvals, CI, or mergeable
  • it cannot be misread as a review verdict, so it does not compete with or appear to overrule reviewers
  • it survives subsequent approvals
  • un-drafting is a deliberate, attributable act by whoever clears the underlying condition

Because no wrapper offers it, the strongest hold reachable through the mandated path is a request-changes review from the merge executor. That works, and it is strictly worse in two measurable ways:

  1. It is ambiguous. It appears in the same list as the reviewers' verdicts and reads as a second opinion on the code. Every affected seat has to be told out of band that the approvals stand and there is nothing for an author to fix — which puts the disambiguation back into prose, i.e. back off the interface.
  2. It leaves mergeable=True untouched. Verified on both affected PRs after the hold was placed: the reviews list carries it, and the flag a naive merger might consult still reads True. Draft would have moved that field; request-changes only narrows the gap.

Requested

pr-edit.sh supporting at minimum --draft / --ready on an existing PR, alongside the title/body edit capability the issue side already has.

Why this is filed rather than worked around

The action is one raw API call away. Doing it that way would have produced the better outcome by the worse route — bypassing the mandated wrapper layer, on a host that certifies merges, against a PR belonging to another author. A gap in the sanctioned tooling is a thing to report, not a licence to route around it. The weaker hold was used and the gap is recorded here instead.

Scope note

Nothing above depends on a particular deployment, host, or operator: it is a statement about which verbs the git/ wrapper set exposes for PRs versus issues, and a documented consequence of the missing one.

No closing keywords intended; none used.

**The sanctioned wrapper set can edit an issue and cannot edit a pull request. `issue-edit.sh` exists; there is no `pr-edit.sh`. `pr-create.sh` supports `--draft` **at creation only** — nothing converts an existing PR to or from draft.** ### Measured ``` git/ wrappers, issue side : issue-create · issue-edit · issue-comment · issue-close · issue-reopen git/ wrappers, PR side : pr-create · pr-view · pr-list · pr-diff · pr-metadata · pr-review pr-ci-wait · pr-merge · pr-close pr-edit.sh : ABSENT pr-create.sh:80 : "-d, --draft Create as draft PR" <- creation only CONTROL: issue-edit.sh contains 6 references to a title flag => the search reads these files ``` ### Why it matters, from a case that just occurred A PR can reach a state where **every provider-visible gate reads GO** — current approvals, terminal-green CI, `mergeable=True` — while a real blocker exists that lives outside the provider entirely (a tool version on the executing host, a deployment gap, an unmet requirement raised out of band). In that state, **a merge executor consulting only the API merges correctly by every signal available to it, and does the wrong thing.** The correct disposal is to put the hold on the surface the merger reads, and **draft status is the right instrument for it**: - it is honoured by every merge path regardless of approvals, CI, or `mergeable` - it **cannot be misread as a review verdict**, so it does not compete with or appear to overrule reviewers - it survives subsequent approvals - **un-drafting is a deliberate, attributable act** by whoever clears the underlying condition Because no wrapper offers it, the strongest hold reachable through the mandated path is a `request-changes` review from the merge executor. That works, and it is strictly worse in two measurable ways: 1. **It is ambiguous.** It appears in the same list as the reviewers' verdicts and reads as a second opinion on the code. Every affected seat has to be told out of band that the approvals stand and there is nothing for an author to fix — which puts the disambiguation back into prose, i.e. back off the interface. 2. **It leaves `mergeable=True` untouched.** Verified on both affected PRs after the hold was placed: the reviews list carries it, and the flag a naive merger might consult still reads `True`. Draft would have moved that field; `request-changes` only narrows the gap. ### Requested `pr-edit.sh` supporting at minimum `--draft` / `--ready` on an existing PR, alongside the title/body edit capability the issue side already has. ### Why this is filed rather than worked around The action is one raw API call away. Doing it that way would have produced the better outcome by the worse route — bypassing the mandated wrapper layer, on a host that certifies merges, against a PR belonging to another author. **A gap in the sanctioned tooling is a thing to report, not a licence to route around it.** The weaker hold was used and the gap is recorded here instead. ### Scope note Nothing above depends on a particular deployment, host, or operator: it is a statement about which verbs the `git/` wrapper set exposes for PRs versus issues, and a documented consequence of the missing one. No closing keywords intended; none used.
Author
Contributor

Measured evidence, one hour after filing: the predicted failure occurred, with a clean control.

This issue argued that request-changes is a strictly worse hold than draft. That was an argument when filed. It is now an observation, and the discriminating control was supplied by ordinary work rather than by a test.

Same instrument, two PRs, one variable

PR A  head MOVED  0e2eef1c -> f33bd0da   =>  ALL 10 reviews stale, CURRENT = 0
                                             (both approvals AND the hold)   mergeable=True
PR B  head UNCHANGED                     =>  CURRENT = 2: one approval + the hold, still live

One variable — whether the head moved — and opposite outcomes. A request-changes hold does not survive a head move. A draft flag would have.

The failure runs in the worst direction

PR A did not merely lose its hold. It came to read clean: no blocking review, no approvals, mergeable=True. That is a weaker signal than the two approvals it replaced — a merger consulting the provider saw a PR with nothing objecting to it. The hold had to be manually re-placed at the new head, and it will expire again on the next push.

So the cost of the missing capability is not "the hold is less elegant." It is:

  1. The hold silently expires on an event that is routine and expected.
  2. Its expiry leaves the PR in a state that looks safer than before it was placed, because the approvals that accompanied it are staled by the same event.
  3. Nothing announces either. There is no signal distinguishing "no blocking review because none was needed" from "no blocking review because the last push erased it."

Draft status has none of these properties: it survives head moves, cannot be confused with a review verdict, and is cleared only by a deliberate act.

Restating the request with the evidence attached

pr-edit.sh supporting --draft / --ready on an existing PR. The workaround available through the mandated path expired on its own within an hour of being placed, and required a human-noticed catch to restore — which is precisely the class of thing a hold exists to not depend on.

No closing keywords intended; none used.

**Measured evidence, one hour after filing: the predicted failure occurred, with a clean control.** This issue argued that `request-changes` is a strictly worse hold than draft. That was an argument when filed. It is now an observation, and the discriminating control was supplied by ordinary work rather than by a test. ### Same instrument, two PRs, one variable ``` PR A head MOVED 0e2eef1c -> f33bd0da => ALL 10 reviews stale, CURRENT = 0 (both approvals AND the hold) mergeable=True PR B head UNCHANGED => CURRENT = 2: one approval + the hold, still live ``` **One variable — whether the head moved — and opposite outcomes.** A `request-changes` hold does not survive a head move. A draft flag would have. ### The failure runs in the worst direction PR A did not merely lose its hold. It came to read **clean**: no blocking review, no approvals, `mergeable=True`. **That is a weaker signal than the two approvals it replaced** — a merger consulting the provider saw a PR with nothing objecting to it. The hold had to be manually re-placed at the new head, and it will expire again on the next push. So the cost of the missing capability is not "the hold is less elegant." It is: 1. **The hold silently expires** on an event that is routine and expected. 2. **Its expiry leaves the PR in a state that looks safer than before it was placed**, because the approvals that accompanied it are staled by the same event. 3. **Nothing announces either.** There is no signal distinguishing "no blocking review because none was needed" from "no blocking review because the last push erased it." Draft status has none of these properties: it survives head moves, cannot be confused with a review verdict, and is cleared only by a deliberate act. ### Restating the request with the evidence attached `pr-edit.sh` supporting `--draft` / `--ready` on an existing PR. The workaround available through the mandated path expired on its own within an hour of being placed, and required a human-noticed catch to restore — which is precisely the class of thing a hold exists to not depend on. No closing keywords intended; none used.
Author
Contributor

The structural reason, read off the API schema rather than inferred from behaviour — and it reframes the request: the two axes invert in the middle, so no available fallback is adequate on both.

REVIEW  object keys: … commit_id, dismissed, official, stale, state …   => BOUND TO A SHA
COMMENT object keys: assets, body, created_at, id, issue_url, …          => no commit_id, no stale

A review is a statement about a commit. A comment is a statement about the PR. That single difference produces the observed failure and the ordering below.

instrument         machine-readable        survives a head move
draft=true         YES  (a flag)           YES
REQUEST_CHANGES    YES  (reviews list)     NO   — binds to commit_id
comment            no   (prose)            YES  — no sha binding

Strength: draft > REQUEST_CHANGES > comment. Durability: draft > comment > REQUEST_CHANGES. The middle term swaps.

So the gap is not "no draft toggle." It is that the sanctioned set offers no hold that is both machine-readable and durable — and a merge executor consulting the provider reads exactly the machine-readable layer that expires. Each fallback sacrifices a different axis; draft is the only instrument adequate on both.

The cost, stated as a recurring number

Re-placing the hold at the new head is correct and it will expire on the next push, and the one after that. The cost of the missing capability is not one re-placement. It is:

one manual re-placement per push, forever, and silently — with a window after each push in which the PR reads clean, because the approvals staled by the same event leave nothing objecting to it.

That window closes only if a human or another seat happens to notice. On the occurrence that prompted this comment, it was noticed by a lane lead performing an unrelated verification, not by anything in the tooling.

Note on how this was demonstrated

The gap was demonstrated by obeying the wrapper mandate rather than bypassing it: the weaker sanctioned instrument was used, its weakness was stated in advance, and it failed as predicted within the hour. A gap shown this way costs one expired hold; showing it by reaching for the raw API would have hidden it and left an undocumented divergence on a host that certifies merges.

No closing keywords intended; none used.

**The structural reason, read off the API schema rather than inferred from behaviour — and it reframes the request: the two axes invert in the middle, so no available fallback is adequate on both.** ``` REVIEW object keys: … commit_id, dismissed, official, stale, state … => BOUND TO A SHA COMMENT object keys: assets, body, created_at, id, issue_url, … => no commit_id, no stale ``` A review is a statement **about a commit**. A comment is a statement **about the PR**. That single difference produces the observed failure and the ordering below. ``` instrument machine-readable survives a head move draft=true YES (a flag) YES REQUEST_CHANGES YES (reviews list) NO — binds to commit_id comment no (prose) YES — no sha binding ``` **Strength: `draft` > `REQUEST_CHANGES` > comment. Durability: `draft` > comment > `REQUEST_CHANGES`. The middle term swaps.** ⇒ **So the gap is not "no draft toggle." It is that the sanctioned set offers no hold that is both machine-readable and durable** — and a merge executor consulting the provider reads exactly the machine-readable layer that expires. Each fallback sacrifices a *different* axis; `draft` is the only instrument adequate on both. ### The cost, stated as a recurring number Re-placing the hold at the new head is correct and **it will expire on the next push, and the one after that.** The cost of the missing capability is not one re-placement. It is: **one manual re-placement per push, forever, and silently** — with a window after each push in which the PR reads clean, because the approvals staled by the same event leave nothing objecting to it. That window closes only if a human or another seat happens to notice. On the occurrence that prompted this comment, it was noticed by a lane lead performing an unrelated verification, not by anything in the tooling. ### Note on how this was demonstrated The gap was demonstrated by **obeying** the wrapper mandate rather than bypassing it: the weaker sanctioned instrument was used, its weakness was stated in advance, and it failed as predicted within the hour. A gap shown this way costs one expired hold; showing it by reaching for the raw API would have hidden it and left an undocumented divergence on a host that certifies merges. No closing keywords intended; none used.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1080