Skip to content

F2d: already-fixed check and opening the task's PR - #84

Merged
mchwang merged 66 commits into
mainfrom
feat/f2d-already-fixed-pr
Oct 1, 2026
Merged

mchwang merged 66 commits into
mainfrom
feat/f2d-already-fixed-pr

Conversation

@mchwang

@mchwang mchwang commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

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.

Not in this PR

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.
  • Stacked work: Give the merge and issue gh runners the allowlisted environment #86 (gh environment allowlist for the merge and issue adapters) is rebased onto 6c15c41 (head 1aebc6d); it will be moved onto main and marked ready after this PR merges.
  • Files: github/already-fixed.ts, github/pull-requests.ts, github/run-with-input.ts, github/gh-env.ts, github/validate.ts, github/merge.ts (exports RunGh), 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.
  • Deferred follow-ups:
    • The real branch push (BranchPusher) needs D: inspect task changes and make runner commits (needed by F2) #66's commit export.
    • Continue and Cancel from possibly already fixed, and closing the draft on cancel.
    • F6: move GhMergeGateway's pre-merge check onto github/already-fixed.ts.
    • 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.
    • Give the merge and issue gh adapters the same environment allowlist: Give the merge and issue gh runners the allowlisted environment #86, stacked on this PR.

🤖 Generated with Claude Code

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Ambiguous recovery, stale-response races, PR-state handling, and subprocess environment exposure can produce unsafe publishing outcomes.

Review effort: Balanced
Findings: 1 High severity · 6 Medium severity

Open (7)
What changed in this PR

Adds the F2d workflow for detecting already-fixed issues and opening or reusing task pull requests.

Changes:

  • Adds bounded, fail-closed GitHub issue/commit checks.
  • Persists and coordinates PR opening, recovery, and reuse.
  • Generates bounded PR content with expanded tests and documentation.
File Description
core/​pull-request-body.ts Builds safe, bounded PR titles and bodies.
docs/​implementation/​pull-request-opening.md Documents the F2d workflow.
github/​already-fixed.ts Implements already-fixed detection.
github/​merge.ts Exports the shared GitHub runner type.
github/​pull-requests.ts Implements GitHub PR operations.
runner/​publish.ts Coordinates checking, pushing, recovery, and opening.
runner/​store.ts Persists checks and PR-opening lifecycle state.
test/​already-fixed.test.ts Tests already-fixed detection and bounds.
test/​publish.test.ts Tests publishing, recovery, bodies, and adapters.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread runner/publish.ts Outdated
Comment thread github/already-fixed.ts Outdated
Comment thread github/pull-requests.ts Outdated
Comment thread github/pull-requests.ts Outdated
Comment thread runner/publish.ts
Comment thread runner/publish.ts Outdated
Comment thread runner/store.ts Outdated
mchwang and others added 3 commits September 29, 2026 09:11
- 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mchwang
mchwang marked this pull request as ready for review September 29, 2026 16:27
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread runner/publish.ts Outdated
Comment thread github/already-fixed.ts Outdated
Comment thread github/already-fixed.ts Outdated
Comment thread github/pull-requests.ts Outdated
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Stale-review and overlapping-refresh races can still produce incorrect external PR state.

Review effort: Balanced
Findings: 4 Medium severity

Open (4)
Resolved since last review (4)
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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.

Comment thread github/already-fixed.ts Outdated
Comment thread runner/publish.ts
Comment thread runner/publish.ts Outdated
Comment thread runner/store.ts
mchwang and others added 3 commits September 29, 2026 13:59
… 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>
…Store

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread github/pull-requests.ts Outdated
Comment thread core/pull-request-body.ts Outdated
Comment thread github/already-fixed.ts Outdated
mchwang and others added 4 commits September 29, 2026 14:31
…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>
…ways settles

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved deadline, stale-generation, cancellation, reference-matching, PR-state, and mention-safety issues remain.

Review effort: Balanced
Findings: 2 High severity · 2 Medium severity

Open (4)
Resolved since last review (3)

Comment thread github/pull-requests.ts Outdated
Comment thread runner/publish.ts Outdated
Comment thread core/pull-request-body.ts Outdated
Comment thread github/already-fixed.ts
mchwang and others added 9 commits September 30, 2026 19:16
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Branch lookup can miss or misclassify PRs, and several draft operations can act on stale task generations.

Review effort: Balanced
Findings: 3 Medium severity

Open (3)
Resolved since last review (3)

Comment thread github/pull-requests.ts
Comment thread github/pull-requests.ts
Comment thread runner/publish.ts Outdated
mchwang and others added 2 commits September 30, 2026 21:56
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Recovery can arbitrarily adopt or settle a PR when multiple open PRs carry the same editable marker.

Review effort: Balanced
Findings: None

Resolved since last review (3)
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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>
@mchwang

mchwang commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

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.

🤖 Addressed by Claude Code

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>
@mchwang
mchwang requested a balanced review from Copilot October 1, 2026 06:35
@mchwang

mchwang commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

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.

🤖 Addressed by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium severity 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>
@mchwang

mchwang commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

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.

🤖 Addressed by Claude Code

- 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>
@mchwang
mchwang merged commit 4baf097 into main Oct 1, 2026
1 check passed
mchwang added a commit that referenced this pull request Oct 1, 2026
#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants