Repository navigation
Description
Problem Statement
thirdshift turns a GitHub issue into a ready-for-review pull request, and a human still has to open, review and merge every one. For work where the operator trusts the factory's own bar (its implement session, its /thirdshift:code-review pass, CI, and the Repair loop), that last human step is the only thing between an issue and shipped code. It stops unattended overnight work from finishing: the operator wants to trigger a Run and come back to a merged pull request and a closed issue, not a queue of pull requests to review and merge.
Terms in bold are defined in CONTEXT.md. The choice to merge deterministically rather than through GitHub auto-merge is recorded in ADR-0004 (docs/adr/0004-self-merge-by-thirdshift-not-github-auto-merge.md).
Solution
A new command form, thirdshift merge <Issue URL> (alias thirdshift --merge <Issue URL>), starts a Merge run. It does everything a Run does, then a Self-merge: once the pull request is open, ready for review, mergeable and green on its head commit, thirdshift merges it into the Base branch with a merge commit, merging only the commit whose CI it watched.
Merging becomes the goal of the Repair loop:
- A merge attempt that fails because the Base branch moved into a conflict or CI went red goes back round the loop, and a Repair fixes it.
- A Foreign commit (someone else's push to the Issue branch) is merged in, reviewed and, if needed, fixed by a new review Repair, then merged. Only code an agent wrote or reviewed gets merged.
- If GitHub still refuses a merge when there is nothing left to fix (a policy refusal, such as disallowed merge commits or a required review), the Run fails but leaves the pull request ready for review, not a draft.
After the merge, thirdshift deletes the remote Issue branch and closes the issue if the merge did not close it. The Repair and base-move budgets rise from 3 to 5 for every Run.
User Stories
- As an operator, I want to run
thirdshift merge <Issue URL>, so that the Run ends with its pull request merged instead of waiting for my review. - As an operator, I want
thirdshift --merge <Issue URL>to work the same, so that the flag spelling I reach for also works. - As an operator, I want
thirdshift <Issue URL> --mergeto be rejected with exit 2 and the help text, so that a misplaced keyword never silently starts a normal Run. - As an operator, I want
thirdshift helpto listmergeon its own line, so that I can discover it. - As an operator, I want
thirdshift <Issue URL>to behave as it does today, so that existing scripts keep producing ready-for-review pull requests. - As an operator, I want a Merge run to go through the same preflight checks as a Run, so that a mismatched origin, a closed issue or unpushed commits stop it before any work happens.
- As an operator, I want a Merge run's agent sessions to get the same prompts as a normal Run, so that the factory's bar doesn't drop depending on whether a human will review.
- As an operator, I want a Merge run on an issue with an existing Issue branch to be a normal Continuation, so that re-running
mergeafter a Failed run or a normal Run picks up where it stopped. - As an operator, I want the Self-merge to happen only once the pull request is open, ready for review, mergeable and green, so that the factory merges only what it stands behind.
- As an operator, I want the merge to use a merge commit, so that self-merged pull requests look like the ones I merge by hand.
- As an operator, I want thirdshift to merge only the head commit whose CI it watched, so that a commit pushed at the last moment can't slip in untested.
- As an operator, I want Unaddressed findings not to block the merge, so that a Merge run can finish unattended, while the findings stay listed in the pull request body for later reading.
- As an operator, I want a repository with no CI to still be merged, so that Merge runs work on repositories without CI, as normal Runs do.
- As an operator, I want a merge attempt that fails because the Base branch moved into a conflict to lead to a conflict Repair and another attempt, so that a busy Base branch doesn't make me merge by hand.
- As an operator, I want a merge attempt that fails because CI went red on a new head to lead to a CI-fix Repair and another attempt, so that the Run fixes what it can.
- As an operator, I want commits someone else pushed to the Issue branch during a Merge run to be merged into the Run's work rather than failing the push, so that the goal stays merging the branch that resolves the issue.
- As an operator, I want every Foreign commit reviewed by an agent with
/thirdshift:code-reviewbefore it can be merged, so that only code an agent or a human reviewed reaches the Base branch. - As an operator, I want the review Repair to fix the review findings it agrees with and push, so that a Foreign commit's problems are fixed before the merge.
- As an operator, I want findings the review Repair doesn't fix added to the pull request body's "Unaddressed findings" section, marked as coming from the Foreign commit, so that I can find them later.
- As an operator, I want a Foreign commit that conflicts with the Run's local work handed to a conflict Repair, so that the merge of someone else's work is resolved rather than failing the Run.
- As an operator, I want stderr to say
merging new commit <sha> from origin/<branch>when a Foreign commit is picked up, so that I can see that code I didn't expect came in. - As an operator, I want picking up Foreign commits to count as a round, like a Base branch move, so that someone else's steady pushes can't keep a Run going forever.
- As an operator, I want a Merge run's Repairs, including review Repairs, to share the Run's Repair budget, so that a Run has one allowance however it spends it.
- As an operator, I want the Repair and base-move budgets raised to 5 for every Run, so that a flaky CI or a busy Base branch is less likely to fail a Run that could have succeeded.
- As an operator, I want a Merge run to fail once either budget of 5 is used up, so that a stuck Run ends.
- As an operator, I want a merge refused when there is nothing left to fix to end the Run with exit 1, the pull request URL on stdout and GitHub's error on stderr, so that I know it's ready but couldn't be merged.
- As an operator, I want that refused pull request left ready for review, not converted to a draft, so that it still honestly claims to be ready.
- As an operator, I want any other Merge run failure to behave as a Failed run does today (work pushed, pull request back to draft), so that the rules I already know still apply.
- As an operator, I want the remote Issue branch deleted after the merge, so that merged work doesn't leave branches behind.
- As an operator, I want thirdshift to close the issue itself when the merge didn't, which happens when the Base branch isn't the repository's default branch, so that a Merge run always ends with the issue closed.
- As an operator, I want that closing comment to read
Closed by #<pr>, merged into <base> by a thirdshift Merge run., so that a closed issue with a pull request into a non-default branch doesn't look like a mistake. - As an operator, I want the local Issue branch, the worktree and the plugin directory cleaned up as today, so that a Merge run leaves the same footprint as a Run.
- As an operator, I want a failure to delete the remote branch or close the issue after a successful merge to still exit 0 with a warning on stderr naming what to do by hand, so that an irreversible merge is never reported as a failure.
- As an operator, I want Ctrl-C before the merge succeeds to give a Failed run as usual, so that interrupting a Merge run is as safe as interrupting a Run.
- As an operator, I want Ctrl-C after the merge succeeded to finish the post-merge steps and exit as a success, so that the outcome follows what actually happened on GitHub.
- As an operator scripting thirdshift, I want a successful Merge run to exit 0 and print only the pull request URL on stdout, so that
url=$(thirdshift merge "$issue")works like the normal form. - As an operator watching stderr, I want a successful Merge run's last line to be
PR <url> is merged, so that I can tell it from a ready-for-review result at a glance. - As an operator, I want each review Repair's transcript logged under
~/.thirdshift/logs/as arepair-<i>session like the other Repairs, so that I can read what the agent did with a Foreign commit. - As an operator, I want a review Repair that ends its turn while waiting on background work to get a Resume, as other sessions do, so that it finishes its work in the foreground.
- As a reader of the Prompts and skills page, I want the review Repair prompt shown there, so that everything thirdshift tells its agents is documented.
- As a reader of the README, I want the
mergeform, the Self-merge step, the outcomes, the new budgets and the ready-for-review exception for Failed runs documented, so that I know what a Merge run will do before I trust it with my repository. - As a repository owner who requires reviews, I want a Merge run to be refused and leave the pull request ready for review, so that my branch protection is respected and never bypassed.
Implementation Decisions
-
Command surface. The entry point recognises
mergeand--mergeas the first argument, followed by an Issue URL, as it does forhelp/--helpandversion/--version. Anything after the URL is an argument error (exit 2, error and help on stderr). The help text gains amergeline. The Run entry point takes the mode (normal or Merge run) as a parameter. Nothing else about preflight, branch selection or worktree creation changes. -
Prompts. The implement, Continuation, conflict Repair and CI-fix Repair prompts are unchanged for Merge runs. One new prompt, the review Repair:
- takes the issue, Base branch, Issue branch, pull request URL and a fixed point (the head thirdshift last knew as the Run's own);
- runs
/thirdshift:code-reviewwith that fixed point; - fixes the findings the agent agrees with;
- adds the rest to the pull request body's "Unaddressed findings" section, marked as coming from the Foreign commit;
- pushes the Issue branch, never rebasing or force-pushing;
- ends with the shared headless instruction.
-
Foreign commit detection. thirdshift keeps track of the Issue branch head it last pushed or saw a session push. In a Merge run, each round of the Repair loop starts with a fetch. Commits on
origin's Issue branch that aren't reachable from the local head are Foreign commits. thirdshift merges them into the local branch (fast-forward or merge commit, never rebase), logsmerging new commit <sha> from origin/<branch>on stderr, and starts a review Repair with the previous known head as the fixed point. A conflict in that merge leads to a conflict Repair first. The round counts against the base-move budget. Foreign commits are handled only in Merge runs. -
The Repair loop gains a Merge-run exit. Where a normal Run returns once CI is green or absent and the Base branch hasn't moved, a Merge run then checks the pull request is still open, ready for review and mergeable, and attempts the Self-merge. If the merge fails, the loop goes round again. A round that finds nothing to do (no Foreign commits, Base branch unchanged, no conflict, CI green or absent, mergeable) and still can't merge is a policy refusal. thirdshift never parses GitHub's error text to classify the failure.
-
The merge call.
gh pr mergewith a merge commit and--match-head-commitset to the head commit CI was watched on. It does not use--auto(ADR-0004) or--delete-branch: branch deletion is thirdshift's own step, and gh's local deletion would interfere with the worktree. -
Budgets. Both
MAX_REPAIRSandMAX_BASE_MOVESchange from 3 to 5 for every Run. Review Repairs count as Repairs, and Foreign-commit pickups and Base branch moves share the base-move budget. -
Post-merge steps, after a successful merge:
- Delete the Issue branch on
origin. - Read the issue's state. If it's still open, close it through
gh issue closewith the commentClosed by #<pr>, merged into <base> by a thirdshift Merge run. - Clean up as usual.
A failure in step 1 or 2 is reported on stderr as a warning naming the manual fix, and doesn't change the outcome.
- Delete the Issue branch on
-
Outcomes. The Run's result distinguishes three cases:
- Merged: exit 0, the pull request URL on stdout, and a last stderr line
PR <url> is merged. - Policy refusal: a Failed run that skips the draft conversion. It exits 1, prints the pull request URL on stdout, and puts GitHub's error on stderr. Uncommitted work is still committed and pushed as in any Failed run, although normally there is none.
- Any other failure: the existing Failed run path, unchanged.
- Merged: exit 0, the pull request URL on stdout, and a last stderr line
-
Interruption. The interrupt check before the merge works as today. Once
gh pr mergehas succeeded, an interrupt no longer turns the Run into a Failed run: the post-merge steps run, and the Run exits as merged. -
Logs. Review Repairs are logged as
repair-<i>, numbered with the other Repairs in the Run, and get a Resume like any session. -
Docs.
- README: the Usage section (the
mergeform and the outcomes), What a Run does (the Self-merge step), Failed runs (the ready-for-review exception), and the new budgets. - The Prompts and skills page, regenerated for the new prompt.
CONTEXT.mdand ADR-0004 already cover the language and the merge decision (PR Define Merge run, Self-merge and Foreign commit #89).
- README: the Usage section (the
Testing Decisions
- One seam: the thirdshift binary. Tests drive it end to end through the existing
Scenariointegration harness: real git against a local bareorigin, with the fakeghand fakeclaudefromtests/fakes/. No new seams in the source; the Repair loop is not unit-tested separately. - A good test sets up a scenario (repository state, fake GitHub state, fake agent behaviour), runs the binary, and asserts only on what the operator sees: exit code, stdout, stderr lines,
origin's branches and history, fake GitHub state (pull request merged or ready or draft, issue open or closed, comments), and the recordedgh/claudecalls where the call itself is the behaviour (for example, merge commit and--match-head-commit). Tests don't assert on internal functions or data structures. - Fake
ghextensions:pr mergeperforms a real merge commit of the head into the base onorigin, marks the pull request merged, and closes the linked issue only when the base is the default branch.- It honours
--match-head-commitand refuses when the head differs. - A new
gh fakeknob makes merges refuse with a given error, for policy refusals. issue closewith--commentis added.- Unknown calls still exit 2.
- Scenarios to cover:
- A clean Merge run merges with a merge commit on the watched head, deletes the remote branch, closes the issue, exits 0 and prints the pull request URL.
- A Merge run into a non-default Base branch closes the issue itself, with the comment.
- The
--mergealias works, and a trailing--mergeis rejected with exit 2. - A merge refused while nothing is left to fix is a policy refusal: exit 1, the pull request URL on stdout, the pull request still ready (not draft).
- A Foreign commit pushed during CI (via the fake's on-ci-read script or a fake agent) is merged in, logged, handed to a review Repair whose prompt names the right fixed point, and then merged.
- A Foreign commit conflicting with local work leads to a conflict Repair.
- The Base branch moving into a conflict at merge time leads to a conflict Repair and a later merge.
- The Repair budget of 5 and the base-move budget of 5 each fail a Run once exceeded. Update the existing "three" tests.
- A failed remote branch deletion or issue close after the merge still exits 0 with a warning.
- Ctrl-C after the merge exits as merged, and Ctrl-C before it gives a Failed run.
- A normal Run is unchanged.
- The Prompts and skills page test fails until the page is regenerated with the review Repair prompt.
- Prior art:
tests/ci.rs: Repairs, the shared cap, base moves, on-ci-read scripts.tests/mergeable.rs: mergeability states.tests/failed_run.rs: draft conversion and push-on-failure.tests/command_surface.rs: argument parsing and help.tests/background_work.rs: Resume of Repairs.run_and_signalintests/progress_lines.rsandtests/failed_run.rs: interruption.
Out of Scope
- GitHub auto-merge (
gh pr merge --auto), squash or rebase merges, and bypassing branch protection (--admin). - Triggering a Merge run from an issue label or a config default. The trigger is only the command form.
- Skipping the implement session when a ready, green pull request already exists ("ship what's there"). A Merge run on such an issue is a normal Continuation.
- Telling the agent in its prompts that the pull request will be self-merged, or giving Merge runs a different quality bar.
- Blocking the merge on Unaddressed findings, or requiring CI to exist.
- Running
/thirdshift:code-reviewafter conflict or CI-fix Repairs: code an agent wrote counts as reviewed by that agent. - Handling Foreign commits in normal Runs.
Further Notes
- The glossary terms (Merge run, Self-merge, Foreign commit, the widened Repair and Failed run) and ADR-0004 are in PR Define Merge run, Self-merge and Foreign commit #89. The implementation should build on them and keep
CONTEXT.mdfree of implementation detail. - This repository has no branch protection, so the policy-refusal path won't show up in its own use unless merge commits are disallowed. It's covered by the fake
gh. - A Merge run can merge a Foreign commit written by a human that no human reviewed; the review Repair's agent review is what makes that acceptable.
Activity
[-]make flag which results in self-merge[/-][+]make a way to self-merge[/+][-]make a way to self-merge[/-][+]Merge run: a Run that ends with its PR merged[/+]JacobStephens2 commentedon Sep 27, 2026
tested and worked