You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Lane F, step F2d (design: "Checking whether the issue is already fixed" and "Needs human"). See docs/implementation/pull-request-opening.md.
What changes
github/already-fixed.ts: the pre-PR check. It matches when something other than the task closed the issue, when another open or merged PR links to it, or when a new base commit mentions it (#N, GH-N, owner/repo#N, the issue URL). Own PRs are excluded by repository and number, and own commits by SHA (AGENTS.md: repository identity, stable identity). Every read is bounded (100 timeline events, 250 base commits). A bound, a non-ancestor base, or a malformed or unreadable answer returns unknown, which is handled like a match.
github/pull-requests.ts: opens a PR or draft with literal gh argv. It finds a lost PR by its description marker and refreshes (and marks ready) the task's existing PR. It checks the head and base repository and branch in every answer.
runner/publish.ts: recover → check → push → re-read → open or reuse. A match opens nothing: a running task moves to possibly already fixed, and a needs-human task stays as it is, with no draft. The GitHub call comes right after a Store transaction that confirms a clear check with no task change since (AGENTS.md: re-read before an irreversible action).
runner/store.ts: schema v7 adds already_fixed_checks and task_pull_requests. An opening is recorded before the call, so a lost outcome is adopted, not opened twice. A PR that opens after a cancel is still recorded, so it can be closed.
core/pull-request-body.ts: plan text and open problems go inside a fence longer than any backtick run in them, so closing keywords and @-mentions do nothing. The description stays under 60,000 characters.
The Continue and Cancel actions from possibly already fixed, and closing the draft on cancel.
Moving the pre-merge check in GhMergeGateway onto this module (F6).
Evidence
test/already-fixed.test.ts and test/publish.test.ts: 97 tests (on 6c15c41); with the Store and recovery suites, 190 pass. After merging main (fa037c9), the F2d, runner, store and merge suites pass together: 367 tests in 8 files.
Mutation checks: 104 guards were broken one at a time, and each broken guard failed a test; the SIGKILL escalation was also checked by removing it (its test then times out). One guard made redundant by a reordering (the extra pre-push re-read) was removed rather than kept untestable.
Full local suite: 956 passed. The 18 failures were Docker daemon contention with other sessions (pool overlap, a name conflict, "cannot connect") and two load timeouts. None are in touched code. The non-Docker suites (history, review, store, runner, merge) pass when re-run: 341/341.
Review rounds
Round
Source
Head
Findings
Fixed in
1
Copilot
cba0a9b
7: branch-name collision; gh inherited the full environment (2); a closed PR could be recorded; the no-changes shortcut skipped the status guard; a lost opening was abandoned after one empty lookup; a stale response could move a newer task.
7e3830b
2
Local /code-review
7e3830b
2: a needs-human task reusing its ready PR moved to in review, and the PR stayed ready; fenced() spread every backtick run into Math.max.
1dcd29e
3
Local /code-review
1dcd29e
1: the gh allowlist left out the D-Bus session bus that Linux keyring sign-in needs.
7f3e5fe
4
Local /code-review
7f3e5fe
None.
—
5
Copilot
ea96836
4: the push ran before the re-read that follows the earlier-PR lookup; a manually connected, then disconnected PR still counted as linked; no overall deadline across the check's gh calls; an update of the open PR had no durable record if its read-back failed.
0f750f1
6
Copilot
0f750f1
4: the 45 s check deadline exceeded the 15 s request budget; an already-aborted signal was ignored on paths with no await; overlapping publishes could clear or overtake an in-flight update; the pre-write guard ignored review_version.
89621e9
7
Local /code-review
89621e9
1: the one-publish-per-task guard was per publisher instance.
6b26e65
8
Local /code-review
6b26e65
None.
—
9
Handed-off /code-review (high effort, from the #86 session)
0f750f1
8: closing keywords in the title and in squash/merge commit messages; publishing stuck when an abandoned opening's PR appears later; overlapping publishes (already fixed in 89621e9/6b26e65); a ProjectV2 closer made the check unknown; the PR-number check ran after the push; the deadline comment (already fixed in 89621e9); sequential timeline and commit reads; title cutting split surrogate pairs.
9980445
10
Independent review agent
uncommitted fixes for round 9
2: adoption was unreachable because the check ran before the lookup that identifies the task's own PR (the test's fake check hid this); cutting problems by code point broke the description size bound.
9980445
11
Copilot
9980445
3: the PR description was one OS argument (a 60,000-character multibyte body or a NUL fails to spawn); shown problems were 2,001 characters, not 2,000; Promise.all returned while the sibling gh was still settling.
fc5887b
12
Local /code-review
fc5887b
1: the new stdin runner sent only SIGTERM, so a gh that ignores it never settles.
b32a330
13
Local /code-review
b32a330
None.
—
14
Local /code-review, high effort
b32a330
5: the check's default gh runner never escalated past SIGTERM, so the deadline could not hold; runWithInput waited on pipes a grandchild could hold open; the refresh read-back raced GitHub's asynchronous head update; a matching check left the earlier ready PR ready for review; #recover read the PR records three times.
07c6630
15
Local /code-review, high effort
07c6630
3: a failed draft change stranded the ready PR after the status moved; the draft record could throw after GitHub changed (number mismatch refused too late); head polls leaked abort listeners.
dcc2b0c
16
Local /code-review, high effort
dcc2b0c
None.
—
17
Copilot
dcc2b0c
4: markDraft and refresh accepted a read-back that did not show the requested draft state; the draft change ran without re-reading the task after the check; @-mentions in the unfenced title could notify; the 12 s deadline plus the runner's SIGTERM and pipe grace periods could exceed the 15 s budget.
0be7147
18
Local /code-review, extra-high effort
0be7147
9: the task's own merged PR (or its own close of the issue) was excluded, so a fixed issue looked clear; repositories without draft PRs looped forever on needs-human publishing; the push moved the open PR before the refresh was recorded; the gh allowlist lacked Windows variables; commit subjects could split a surrogate pair; validation regexes were duplicated across adapters; markDraft re-read a PR its caller had just read; one Store method served two contracts; test Stores were never closed.
f5859f4
19
Copilot
f5859f4
4: the first publish pushed without checking for an open PR on the branch that codeboost did not open; a needs-human refresh pushed and patched the ready PR before trying the draft change; the already-fixed module summary described the old exclusion policy; no v6→v7 upgrade test.
a0b5206
20
Copilot
fa037c9
3: the marker was matched anywhere in the description, so agent-controlled text could block or impersonate a PR; a draft change whose record was lost was never repaired; recovery confirmed a lost draft opening whose PR had been made ready.
46bbeba
21
Copilot
46bbeba
3 (2 inline, 1 in the summary): a comparison that repeats a SHA could hide a commit and read as clear; recovery ended a newer publish with an older opening's PR (wrong task version or draft mode); a draft opening that GitHub created as ready was accepted.
f094853
22
Copilot (summary only, no inline threads)
f094853
2: each PR command got a fresh 30 s, so one refresh or draft change could run for minutes; an abandoned opening's PR seen during a non-clear publish was never adopted, leaving it unrecorded for cleanup.
8a4e5ea
23
Local /engineering:code-review
8a4e5ea
1 applied: beginRefresh kept an unused adopt path after adoption moved to adoptOpening; removed, and beginRefresh now requires an opened row. Other suggestions deferred (see below).
af56100
24
Copilot
af56100
3: recordAlreadyFixed accepted a result after the review changed during the draft change; openings and refreshes owned only the task state version, so a review change during open()/refresh() still moved the task; the design doc still placed adoption in the refresh.
6c15c41
Each fix has a regression test. All 34 Copilot threads are answered and resolved; the summary-only finding of round 21 is answered in the recovery thread.
Review-lesson audit
Finding
Classification
Branch-name collision
Covered: AGENTS.md, Guarded external actions, "Exclude the subject … by stable identity only."
gh inherited the full environment (2 threads)
Covered: AGENTS.md, Owned host and Docker resources, "Give every subprocess an explicit allowlisted environment."
A closed PR could be recorded
Covered: AGENTS.md, Guarded external actions, "Validate every field used to classify an external record as clear."
No-changes shortcut skipped the status guard
New rule (ea96836): "Check an operation's source-state preconditions before any shortcut or early return …"
Lost opening abandoned after one empty lookup
Covered: AGENTS.md, Async jobs and polling, "When an irreversible command has an ambiguous … outcome, retain durable in-flight ownership …"
Stale response could move a newer task
Covered: AGENTS.md, Async jobs and polling, "Never apply a background response without proving it is still current."
Needs-human task moved to in review by a refresh
New rule (ea96836): "A current response may still move a record only along a transition allowed from the state the action was guarded for …"
fenced() spread unbounded runs
New rule (ea96836): "Never spread a collection whose size follows unbounded input into function arguments …"
Push before the re-read after the earlier-PR lookup
Covered: AGENTS.md, Guarded external actions, "After the final asynchronous external validation, re-read the local generation immediately before an irreversible action."
Disconnected PR still counted as linked
New rule (0f750f1): "When a relation can be added and removed …, replay its add and remove events in order and count only its latest state."
No overall deadline for the check
Covered: AGENTS.md, Guarded external actions, "… give the combined operation an overall deadline below the serving request timeout."
Refresh left no durable record
Covered: AGENTS.md, Async jobs and polling, "When an irreversible command has an ambiguous … outcome, retain durable in-flight ownership and reconcile external state before enabling retry."
Check deadline above the request budget
Covered: AGENTS.md, Guarded external actions, "… an overall deadline below the serving request timeout."
Already-aborted signal ignored
New rule (89621e9): "Honour a cancellation signal that is already aborted before the first durable write …"
Overlapping publishes (Copilot and handed-off review)
Covered: AGENTS.md, Async jobs and polling, "… retain durable in-flight ownership and reconcile external state before enabling retry."
Guard ignored review_version
Covered: AGENTS.md, "After the final asynchronous external validation, re-read the local generation …", and the F1 contract (runner-lifecycle.md, Irreversible actions).
Publish guard was per instance
Covered: the same in-flight ownership rule; the guard is now shared per Store.
Closing keywords in commit messages
New rule (9980445): "Neutralise issue references … in any text codeboost writes that can become a commit message …"
Stuck after an abandoned opening's PR appears (and the unreachable first fix)
New rule (9980445): "When recovery looks for the result of an earlier attempt, recognise the identities of all earlier attempts, abandoned ones included …"
ProjectV2 closer
New rule (9980445): "Select every member the API documents for a union or enum you classify …"
PR-number check after the push
Covered: AGENTS.md, "Check an operation's source-state preconditions before any shortcut or early return …" and the re-read rule.
Deadline comment
Covered: AGENTS.md, Review readiness, "Treat every behavioural claim … as something to verify."
Sequential reads
One-off: a latency choice under one deadline, not a correctness rule; fixed in place.
Surrogate-splitting cuts and the size bound
One-off: text-cutting detail with a regression test for both the split and the size bound; too narrow for a repository rule.
PR description passed as an OS argument
New rule (fc5887b): "Pass text whose size follows user or agent input to a subprocess on stdin, never as an argument …"
Problems cut to 2,001 characters
Covered: AGENTS.md, Review readiness, "Treat every behavioural claim … as something to verify."
Check returned before the sibling gh settled
Covered: AGENTS.md, Async jobs and polling, "Keep the job tracked until its underlying invocation or subprocess has terminated."
Runner sent only SIGTERM
Covered: the same rule; a process that ignores SIGTERM has not terminated, so the runner escalates to SIGKILL.
Check runner never escalated past SIGTERM
Covered: AGENTS.md, Async jobs and polling, "Keep the job tracked until its underlying invocation or subprocess has terminated." Fixed by reusing runWithInput.
Runner waited on pipes a grandchild held
Covered: the same rule; the process has terminated, so the runner settles after a pipe grace period.
Refresh read-back raced the head update
One-off: GitHub-specific eventual consistency after a push, handled with a bounded poll and a test.
Matching check left the ready PR ready
Covered: AGENTS.md, "A current response may still move a record only along a transition allowed from the state the action was guarded for …" (a task not published as ready must not offer a ready PR).
#recover read records three times
One-off: redundant reads, simplified in place.
Failed draft change stranded the PR
Covered: AGENTS.md, "Check an operation's source-state preconditions before any shortcut …" and the in-flight ownership rule; the draft change now runs before the status is recorded.
Draft record threw after GitHub changed
Covered: the same preconditions rule; the number check now runs before any GitHub change.
Head polls leaked abort listeners
One-off: listener cleanup, with a test that counts listeners.
Draft or ready change not verified
New rule (0be7147): "After an external state change (ready, draft, close), read the record back and require the new state before recording success …"
No re-read before drafting
Covered: AGENTS.md, "After the final asynchronous external validation, re-read the local generation immediately before an irreversible action."
@-mentions in the title
Rule extended (0be7147): "Neutralise issue references … and neutralise @-mentions in any of that text that is not fenced (a title)."
Deadline ignored shutdown grace periods
New rule (0be7147): "Count the subprocess shutdown grace periods … inside that overall deadline …"
Own merged PR excluded
New rule (f5859f4): "Exclude the subject's own records only in the states the exclusion is for …"
Drafts unsupported looped forever
Covered: AGENTS.md, Async jobs and polling, "… Only a confirmed refusal may become retryable failure." GitHub's "drafts not supported" is a confirmed refusal, so the opening or update is dropped instead of being held as unknown.
Push before the refresh was recorded
New rule (f5859f4): "Record in-flight ownership before the first external write of an operation …"
Windows variables missing
Covered: AGENTS.md, "Build that allowlist from each tool's documented credential and configuration channels on every supported platform …"
Commit subject split a surrogate pair
One-off: the shared core/text.ts cut now serves every bounded text; a test pins the straddling case.
Duplicated validation regexes
One-off: consolidated into github/validate.ts (merge.ts is left to #86's owner).
markDraft extra read
One-off: redundant I/O removed in place.
Store method with two contracts
One-off: split into recordPullRequestOpened and recordRefreshConfirmed.
Test Stores never closed
One-off: afterEach closes them, matching the other runner test files.
Push without checking for a foreign branch PR
Covered: AGENTS.md, "Check an operation's source-state preconditions before any shortcut or early return …"; the lookup now runs even with no known markers.
Refresh changed the PR before the draft step
New rule (a0b5206): "Order an operation's external writes so that any step the remote can definitely refuse comes before the writes that cannot be taken back …"
Stale module summary
Covered: AGENTS.md, Review readiness, "Treat every behavioural claim … as something to verify."
No v6→v7 upgrade test
One-off: added the upgrade test the earlier migrations already have.
Marker matched anywhere in the body
New rule (46bbeba): "Read an identity marker only from its reserved position …"
Lost draft change never repaired
New rule (46bbeba): "When the external record is observed, correct the local copy of any field the operation owns …"
Recovery confirmed a ready PR for a draft opening
Covered: AGENTS.md, "A current response may still move a record only along a transition allowed from the state the action was guarded for …"; the lost opening owned a draft.
Repeated SHA in the comparison
Covered: AGENTS.md, "A bounded safety scan must fail closed when its limit is exceeded. Never truncate evidence and report the result as clear."
Recovery ended a newer publish
Covered: AGENTS.md, "Never apply a background response without proving it is still current …"
Draft opening created as ready
Covered: AGENTS.md, "After an external state change … read the record back and require the new state before recording success …"
Per-command timeouts instead of one operation deadline
Covered: AGENTS.md, "… give the combined operation an overall deadline …" and "Count the subprocess shutdown grace periods … inside that overall deadline …"
Abandoned opening's PR not adopted on a non-clear check
Covered: AGENTS.md, "When the external record is observed, correct the local copy of any field the operation owns …" and "… recognise the identities of all earlier attempts, abandoned ones included …"
Dead adopt path in beginRefresh
One-off: removing code left behind by an earlier move; a test pins the stricter rule.
Review version not owned by checks, openings and refreshes
New rule (6c15c41): "When the local state has more than one version counter … an in-flight action owns all of them …" (the F1 contract, runner-lifecycle.md, Irreversible actions, already required it).
Stale adoption wording in the doc
Covered: AGENTS.md, Review readiness, "Treat every behavioural claim … as something to verify."
D-Bus variables missing from the allowlist
New rule (ea96836): "Build that allowlist from each tool's documented credential and configuration channels on every supported platform …"
Readiness report
Head:6c15c41. It fixes Copilot's tenth review (round 24) and adds one AGENTS.md rule. Main was merged in at fa037c9.
CI:test passed on 6c15c41.
Mergeability: mergeable, no conflicts (CLEAN).
Unresolved review threads: 0 (all 34 resolved).
Latest Copilot review: on af56100, with 3 findings, all fixed in 6c15c41. Copilot has not reviewed 6c15c41.
Optional cleanups from round 23: split #publish into refresh and open paths; also treat HTTP 422 on a draft request as "drafts unsupported", not only GitHub's English message; state in the doc that each PR operation's 60 s limit excludes the runner's shutdown grace.
When the publisher is wired into the runner: pass it the coordinator's shutdown signal and await it before closeWrites(), so a publish in flight settles before the Store's write gate closes.
Before opening a PR, check the issue's close, links from other open or
merged PRs, and new base commits that mention it. Exclude the task's own
PRs by repository and number and its own commits by SHA. Fail closed past
every bound. A match or an incomplete check opens nothing and moves a
running task to possibly already fixed.
Otherwise push the task head, re-read the task right before the GitHub
call, and open the PR (a draft with open problems for needs human).
Record each opening first, with a marker in the description, so a lost
outcome is recovered instead of opening twice. A later run reuses the
task's still-open PR. Plan text and problems are fenced so closing
keywords and mentions in them do nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Branch names end in a hash of the exact task identity, so tasks whose
IDs normalize alike never share a branch or PR.
- gh runs with an allowlisted environment only.
- The PR adapter refuses answers for a PR that is not open.
- Publishing checks the task status before the no-changes shortcut.
- An opening GitHub does not show yet stays owned until its settle time
has passed; publish reports OpeningUnsettled instead of posting again.
- Each opening or refresh owns the task state version it was guarded at;
a response for an older version is recorded but never moves the task.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…id spreading backtick runs
A refresh for a needs-human task turns the earlier PR back into a draft,
and the task status changes only from running. fenced() finds the longest
backtick run in a loop, so unbounded plan text cannot overflow the stack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guarded status transitions, preconditions before shortcuts, platform
credential channels in subprocess allowlists, and no spreading of
unbounded collections.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne, refresh record
- Re-read the task right before the push, after the last await.
- Replay connected and disconnected timeline events; a disconnected PR
no longer counts as linked. Cross-references stay.
- One deadline for the whole already-fixed check; it aborts the running
gh call and makes the check unknown.
- Record an update of the open PR before it starts; if its confirmation
is lost, the next publish drops the record and repeats the update.
- AGENTS.md: replay add and remove events for removable relations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exclude path separators from issue URL pattern matches
github/already-fixed.ts:86
These boundaries do not exclude /, so both patterns match suffixes inside unrelated paths. For example, https://example.com/owner/repo#12 and https://example.com/github.com/owner/repo/issues/12 are reported as mentions of this issue, incorrectly moving the task to “possibly already fixed.” Include / in both leading negative lookbehinds.
… abort, deadline
- Bind each check to the plan's review_version and refuse every push,
open and refresh if approvals, choices or notes changed since.
- Run one publish per task at a time, so an in-flight update is never
cleared or overtaken.
- Check the signal before any durable write.
- Default the check deadline to 12 s, below the 15 s request budget.
- AGENTS.md: honour an aborted signal before the first durable write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tV2, order
- Neutralise issue references in the PR title, plan and problems, since
GitHub closes issues from keywords in default-branch commit messages,
which fences do not protect.
- Look up the branch PR with the markers of every earlier opening,
abandoned ones included, before the check; adopt an abandoned
opening's PR (and count it as own) instead of getting stuck behind
GitHub's one-open-PR-per-branch refusal.
- Treat a ProjectV2 closer as a closing by someone else, not unknown.
- Refuse a PR-number mismatch before the push.
- Run the timeline and base-commit reads together; the first failure
aborts the other.
- Cut titles and problems by UTF-16 unit without splitting a surrogate
pair; an empty summary becomes "codeboost plan".
- AGENTS.md: three rules from these findings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ettle both reads
- Send the PR title and description as a JSON body on stdin
(gh api --input -), so a large multibyte description or a NUL never
hits the per-argument limit. The runner settles after gh exits.
- Cut each shown problem to exactly 2,000 characters.
- The already-fixed check aborts the other read on the first failure
but returns only after both have settled.
- AGENTS.md: pass input-sized text on stdin, not as an argument.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The already-fixed check's default runner reuses runWithInput, so an
aborted stage escalates to SIGKILL and the 12 s deadline holds.
- runWithInput settles after exit even if a process gh started keeps
the pipes open, closing them after a grace period.
- A refresh read-back polls briefly until GitHub shows the pushed head,
so a correct publish is not sent to needs human by a stale head.
- When the check matches, the task's earlier ready PR becomes a draft,
so it is never left ready for review.
- #recover reads the task's PR records once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stays retryable
Also refuse a PR-number mismatch before any GitHub change, and remove
each head-poll abort listener when its wait ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 27 found no high or medium defects. The direct read added in
round 26 treated every open recorded PR missing from the branch's list
as list lag ("try again later"). A PR whose branch a person renamed, or
whose marker was removed and base changed, never comes back, so that
retry looped forever. readPull now returns the PR's branch, base and
marker: only a PR still on the task branch, into the configured base,
with its marker is lag (OpeningUnsettled); any other open PR is refused
as PullRequestMisplaced with what a person has to do.
Tests: the moved-branch refusal; the adapter's readPull validation.
Doc: the two outcomes and one request per recorded PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…using
Round 28 found no high or medium defects.
- A PR the direct read finds moved (branch renamed, retargeted while
the list lags) is made a draft where it is before the misplaced
refusal, while its first line still identifies it; a PR whose marker
was removed is reported as possibly still ready. Before, this refusal
skipped the drafting every other misplaced refusal does.
- Tests: retargeted and unmarked PRs during list lag; the base and
marker conditions of the lag test now fail when removed.
- PullRequestMisplaced's comment covers moved PRs and the drafting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 29 found no high or medium defects; all items were in round 28's
drafting of a moved PR:
- The record write is outside the draft call's catch, so a Store
failure after a draft that landed propagates instead of being told as
"may still be ready".
- The draft flag is recorded only when it differs, so retries of a
refused publish do not move the task's version.
- Tests: the fake draft change checks the head branch, a repeat does not
move the version, an abort during the change rejects with the abort,
and a retargeted PR is drafted in its new base.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 30 found no high or medium defects. A recorded PR whose first-line
marker a person removed was refused as "not opened by codeboost" when
the list showed it, but as misplaced (with what to do) when the list
lagged. The main path now passes its recorded PR numbers to findOpened,
which refuses such a PR as PullRequestMisplaced: restore its first line
or close it. Tests at the gateway and the publisher; doc step 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 31 found no high or medium defects.
- The test harness let the draft step find and draft a PR whose marker
a person removed, which the real gateway cannot: findOwned matches by
first-line marker and markDraft's read-back needs it. The fake now
does the same, and the test asserts what really happens: no draft,
the record still ready.
- The refusal for such a PR says it may still be ready for review, as
the list-lag path already did.
- findOpened's interface comment describes `numbers`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e22ec2a asserted the gateway's new message text through the test
harness, whose fake lookup does not produce it, so the test failed.
The publisher test now checks the error class and what the publisher
does (no push, no draft, the record still ready); the message is
tested at the gateway. The fake mirrors the message anyway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 32 found no code defects; it found places where the test harness
was more forgiving than GitHub:
- The fake refresh now fails, like the adapter's read-back, when GitHub
does not apply the requested draft or ready change, and refuses a
closed PR. The test that relied on a refresh returning a ready PR for
a draft request now asserts what really happens: the publish fails,
the update stays in flight, and the next publish settles it and makes
the PR a draft.
- A PR whose branch a person renamed is no longer listed under the
task's branch by the fake lookups.
Doc: the check counting the task's own abandoned PR when GitHub's list
lags its timeline past the settle time (fails closed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 33 found no defects. Tests now fail if the abort checks after
recovery's lookup or after the direct read of a PR the list does not
show are removed: an abort there leaves the lost opening owned with the
task's version unchanged, and draws no draft change or push.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 34 found no defects. Tests now fail if these break: the abort
check after the draft change a match makes (no match recorded after a
cancel), a non-array GraphQL `errors` field failing closed, and
lowercase gh-N mentions in base commits. Doc: repository renames and
transfers are out of this slice's scope (publish fails closed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vals
- The branch PR list fails closed on a full page of 100: a later page
could hide the task's PR, and this lookup decides whether the branch
is clear to push to.
- Every list entry is validated (number, body null or string, base
ref) before any is set aside as someone else's, so a partial answer
cannot look like a clear branch.
- The draft change for a moved PR found by the direct read, and
recovery's draft change for a lost draft opening, re-read the task
right before the call: a task approved meanwhile keeps its PR ready,
since GitHub does not merge a draft. The draft step uses the same
check (#mayKeepReady).
- Tests for each; doc step 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The list entry check requires a PR number of at least 1, as #pull
does, and a test covers a missing or zero number (the check was
untested).
- A merged task keeps its PR ready in the draft step; the test that
covered in review and approved covers merged too.
- The full-page error says to close the PRs that are not needed.
- Doc: the re-read before draft changes applies to recovery, the draft
step and moved PRs; the head settle is the stated exception (it
drafts a PR whose head on GitHub is not the one reviewed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fail closed when multiple matching PRs make ownership ambiguous
runner/publish.ts:355
findOwned may return multiple PRs carrying this editable marker (for example, one per base), but destructuring the first result makes recovery depend on GitHub's list order. This can clear the durable refresh ownership even though another matching PR makes the update ambiguous; a mismatched first number is also silently cleared by settleUnconfirmedRefresh. Fail closed unless there is at most one result and its number matches the recorded PR.
This issue also appears on line 364 of the same file.
Recovery picked the first PR GitHub listed among those carrying a
marker. A marker is editable text, so a copied description could make
recovery record the wrong PR, or silently clear an unconfirmed update
whose only matching PR had another number. Recovery now refuses with
PullRequestMisplaced, keeping the update or opening in flight, when
more than one open PR carries the update's or the lost opening's
marker, or when the one carrying an update's marker is not the
recorded PR. Tests for each; the earlier test that expected the
mismatched number to be ignored now expects the refusal. Doc step 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addressed Copilot's overview finding ("Fail closed when multiple matching PRs make ownership ambiguous", runner/publish.ts:355 and 364) in 683f538: recovery now refuses with PullRequestMisplaced and keeps the update or opening in flight when more than one open PR carries the update's or the lost opening's marker, or when the only PR carrying an update's marker has another number than the recorded PR. It no longer depends on GitHub's list order or silently clears the update. Tests cover the copy beside the PR, the copy alone, and a lost opening with two matches.
The review of 683f538 found that refusing a lost update when a copy
carries its marker was a regression: the refusal ran before the draft
step, so a stopped task's ready PR stayed ready for as long as anyone
kept a copy open. An update's PR has a recorded number, which cannot be
edited, so recovery now settles it from the PR with that number,
whatever order GitHub lists them in, and leaves copies to the main path
(two own PRs: drafted, refused) and the draft step. The Store's own
number check already kept a copy from being recorded; this makes the
publisher's choice explicit too.
A lost opening has no recorded number, so two PRs carrying its marker
still make recovery refuse; the message now says which to close and
that the task's PR may still be ready.
Tests: copies are into another base and listed first by the fake (one
open PR per branch and base; GitHub's order is undefined); the combined
test is split; a stopped task's PR is drafted with a copy present; the
update's own PR's draft flag is recorded with a copy listed first; the
older test models a closed PR replaced by a copy. Doc step 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Correction to my earlier comment on the ambiguous-marker finding: after a code review, 4cc74ca changes how lost updates are handled. Refusing a lost update whenever a copy carried its marker ran before the draft step, so a stopped task's ready PR could stay ready while a copy stayed open. An update's PR has a recorded number, which cannot be edited, so recovery now settles the update from the PR with that number, whatever order GitHub lists them in, and leaves copies to the main path (two own PRs: drafted, then refused) and the draft step. A lost opening has no recorded number, so two PRs carrying its marker still make recovery refuse, and the message says which to close and that the task's PR may still be ready.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
Embedded path-like issue tokens can produce false already-fixed matches.
Review effort: Balanced Findings: None
Previously missed (1)
In code that hasn't changed since last review
Prevent issue tokens from matching inside URL paths
github/already-fixed.ts:97
These start boundaries allow an issue token to be embedded in a URL path or larger path-like token. For example, both https://example.com/GH-12 and https://example.com/github.com/Owner/Repo/issues/12 currently match and can incorrectly move the task to possibly already fixed. Apply the same path-character exclusion already used by the #N alternative.
The GH-N, owner/repo#N and issue-URL forms in mentionsIssue could start
inside a URL path or a longer path-like token, so a base commit with
`https://example.com/GH-12` or `example.com/github.com/owner/repo/
issues/12` counted as mentioning issue 12 and moved the task to
possibly already fixed. Like `#N` already did, the three forms now
need a start that is not a word character, `/`, `.` or `-`. Tests: four
embedded forms are not mentions; a bracketed URL and `(GH-12)` still
are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addressed Copilot's overview finding ("Prevent issue tokens from matching inside URL paths", github/already-fixed.ts:97) in 2aaa4fd: the GH-N, owner/repo#N and issue-URL forms now exclude a preceding / (and ., -, word characters), as #N already did. Tests show https://example.com/GH-12, https://example.com/github.com/Owner/Repo/issues/12, mirror/owner/repo#12 and x.github.com/owner/repo/issues/12 are not mentions, while a bracketed URL and (GH-12) still are.
- Test: `example.com.GH-12` is not a mention (the `.` in the GH-N rule
was untested).
- The boundary rule moves into mentionsIssue's JSDoc, which covers all
four forms, with the rare real mentions it drops.
- Doc: the check table lists path-embedded tokens as not a match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#84 now sets GH_PAGER per platform (empty on Windows, cat elsewhere).
The test compares against ghEnvironment({}) instead of a hard-coded
list, so it follows the helper's own values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lane F, step F2d (design: "Checking whether the issue is already fixed" and "Needs human"). See
docs/implementation/pull-request-opening.md.What changes
github/already-fixed.ts: the pre-PR check. It matches when something other than the task closed the issue, when another open or merged PR links to it, or when a new base commit mentions it (#N,GH-N,owner/repo#N, the issue URL). Own PRs are excluded by repository and number, and own commits by SHA (AGENTS.md: repository identity, stable identity). Every read is bounded (100 timeline events, 250 base commits). A bound, a non-ancestor base, or a malformed or unreadable answer returnsunknown, which is handled like a match.github/pull-requests.ts: opens a PR or draft with literalghargv. It finds a lost PR by its description marker and refreshes (and marks ready) the task's existing PR. It checks the head and base repository and branch in every answer.runner/publish.ts: recover → check → push → re-read → open or reuse. A match opens nothing: a running task moves topossibly already fixed, and a needs-human task stays as it is, with no draft. The GitHub call comes right after a Store transaction that confirms a clear check with no task change since (AGENTS.md: re-read before an irreversible action).runner/store.ts: schema v7 addsalready_fixed_checksandtask_pull_requests. An opening is recorded before the call, so a lost outcome is adopted, not opened twice. A PR that opens after a cancel is still recorded, so it can be closed.core/pull-request-body.ts: plan text and open problems go inside a fence longer than any backtick run in them, so closing keywords and @-mentions do nothing. The description stays under 60,000 characters.Not in this PR
BranchPusher) needs D: inspect task changes and make runner commits (needed by F2) #66's commit export and a runner-owned host repository.GhMergeGatewayonto this module (F6).Evidence
test/already-fixed.test.tsandtest/publish.test.ts: 97 tests (on6c15c41); with the Store and recovery suites, 190 pass. After merging main (fa037c9), the F2d, runner, store and merge suites pass together: 367 tests in 8 files.Review rounds
cba0a9bghinherited the full environment (2); a closed PR could be recorded; the no-changes shortcut skipped the status guard; a lost opening was abandoned after one empty lookup; a stale response could move a newer task.7e3830b/code-review7e3830bfenced()spread every backtick run intoMath.max.1dcd29e/code-review1dcd29eghallowlist left out the D-Bus session bus that Linux keyring sign-in needs.7f3e5fe/code-review7f3e5feea96836ghcalls; an update of the open PR had no durable record if its read-back failed.0f750f10f750f1review_version.89621e9/code-review89621e96b26e65/code-review6b26e65/code-review(high effort, from the #86 session)0f750f189621e9/6b26e65); a ProjectV2 closer made the check unknown; the PR-number check ran after the push; the deadline comment (already fixed in89621e9); sequential timeline and commit reads; title cutting split surrogate pairs.998044599804459980445Promise.allreturned while the siblingghwas still settling.fc5887b/code-reviewfc5887bghthat ignores it never settles.b32a330/code-reviewb32a330/code-review, high effortb32a330ghrunner never escalated past SIGTERM, so the deadline could not hold;runWithInputwaited on pipes a grandchild could hold open; the refresh read-back raced GitHub's asynchronous head update; a matching check left the earlier ready PR ready for review;#recoverread the PR records three times.07c6630/code-review, high effort07c6630dcc2b0c/code-review, high effortdcc2b0cdcc2b0cmarkDraftandrefreshaccepted a read-back that did not show the requested draft state; the draft change ran without re-reading the task after the check; @-mentions in the unfenced title could notify; the 12 s deadline plus the runner's SIGTERM and pipe grace periods could exceed the 15 s budget.0be7147/code-review, extra-high effort0be7147ghallowlist lacked Windows variables; commit subjects could split a surrogate pair; validation regexes were duplicated across adapters;markDraftre-read a PR its caller had just read; one Store method served two contracts; test Stores were never closed.f5859f4f5859f4a0b5206fa037c946bbeba46bbebaf094853f0948538a4e5ea/engineering:code-review8a4e5eabeginRefreshkept an unusedadoptpath after adoption moved toadoptOpening; removed, andbeginRefreshnow requires an opened row. Other suggestions deferred (see below).af56100af56100recordAlreadyFixedaccepted a result after the review changed during the draft change; openings and refreshes owned only the task state version, so a review change duringopen()/refresh()still moved the task; the design doc still placed adoption in the refresh.6c15c41Each fix has a regression test. All 34 Copilot threads are answered and resolved; the summary-only finding of round 21 is answered in the recovery thread.
Review-lesson audit
ghinherited the full environment (2 threads)ea96836): "Check an operation's source-state preconditions before any shortcut or early return …"ea96836): "A current response may still move a record only along a transition allowed from the state the action was guarded for …"fenced()spread unbounded runsea96836): "Never spread a collection whose size follows unbounded input into function arguments …"0f750f1): "When a relation can be added and removed …, replay its add and remove events in order and count only its latest state."89621e9): "Honour a cancellation signal that is already aborted before the first durable write …"review_versionrunner-lifecycle.md, Irreversible actions).9980445): "Neutralise issue references … in any text codeboost writes that can become a commit message …"9980445): "When recovery looks for the result of an earlier attempt, recognise the identities of all earlier attempts, abandoned ones included …"9980445): "Select every member the API documents for a union or enum you classify …"fc5887b): "Pass text whose size follows user or agent input to a subprocess on stdin, never as an argument …"ghsettledrunWithInput.#recoverread records three times0be7147): "After an external state change (ready, draft, close), read the record back and require the new state before recording success …"0be7147): "Neutralise issue references … and neutralise @-mentions in any of that text that is not fenced (a title)."0be7147): "Count the subprocess shutdown grace periods … inside that overall deadline …"f5859f4): "Exclude the subject's own records only in the states the exclusion is for …"f5859f4): "Record in-flight ownership before the first external write of an operation …"core/text.tscut now serves every bounded text; a test pins the straddling case.github/validate.ts(merge.tsis left to #86's owner).markDraftextra readrecordPullRequestOpenedandrecordRefreshConfirmed.afterEachcloses them, matching the other runner test files.a0b5206): "Order an operation's external writes so that any step the remote can definitely refuse comes before the writes that cannot be taken back …"46bbeba): "Read an identity marker only from its reserved position …"46bbeba): "When the external record is observed, correct the local copy of any field the operation owns …"adoptpath inbeginRefresh6c15c41): "When the local state has more than one version counter … an in-flight action owns all of them …" (the F1 contract,runner-lifecycle.md, Irreversible actions, already required it).ea96836): "Build that allowlist from each tool's documented credential and configuration channels on every supported platform …"Readiness report
6c15c41. It fixes Copilot's tenth review (round 24) and adds one AGENTS.md rule. Main was merged in atfa037c9.testpassed on6c15c41.CLEAN).af56100, with 3 findings, all fixed in6c15c41. Copilot has not reviewed6c15c41.6c15c41(head1aebc6d); it will be moved onto main and marked ready after this PR merges.github/already-fixed.ts,github/pull-requests.ts,github/run-with-input.ts,github/gh-env.ts,github/validate.ts,github/merge.ts(exportsRunGh),runner/publish.ts,runner/store.ts(schema v7),core/pull-request-body.ts,core/text.ts, two test files,docs/implementation/pull-request-opening.md,AGENTS.md.BranchPusher) needs D: inspect task changes and make runner commits (needed by F2) #66's commit export.GhMergeGateway's pre-merge check ontogithub/already-fixed.ts.#publishinto refresh and open paths; also treat HTTP 422 on a draft request as "drafts unsupported", not only GitHub's English message; state in the doc that each PR operation's 60 s limit excludes the runner's shutdown grace.closeWrites(), so a publish in flight settles before the Store's write gate closes.ghadapters the same environment allowlist: Give the merge and issue gh runners the allowlisted environment #86, stacked on this PR.🤖 Generated with Claude Code