Repository navigation
Description
Problem Statement
I implement GitHub issues by opening a Claude Code session, typing /implement <issue>, and then babysitting it. When /code-review comes back with Standards findings and Spec findings, I tell the agent which to address, ask for a PR, and then keep watching in case CI fails or the base branch moves on and causes merge conflicts. Every issue ties up my attention for the whole run. Much of that work is mechanical and could run unattended: overnight, from any server, while I do something else. Worse, a PR that claims to be ready for review but isn't still costs me attention I shouldn't have to spend.
Solution
thirdshift, a single Rust binary. From a clone of the target repo I run thirdshift <Issue URL>. The humans are the day shift and thirdshift is the third shift: it runs the work unattended and deterministically.
- Checks that the run makes sense (Origin match, open issue, a clean Base branch state).
- Picks or creates the right Issue branch, continuing earlier work when it exists.
- Creates an isolated worktree and runs a headless Claude Code session inside it with the Factory skills embedded in the binary. The session implements the issue, reviews it, addresses the findings it agrees with, and opens a ready-for-review PR that lists every Unaddressed finding.
- Takes over deterministically: pushes the work, checks the PR, merges the Base branch, and watches CI. It starts Repair sessions for merge conflicts or red CI, at most 3 per run.
- Cleans up after itself, and prints only the PR URL to stdout.
A PR is marked ready for review only when thirdshift stands behind it. Otherwise the run is a Failed run: its work is pushed so nothing is lost, and the PR is converted back to a draft. Quality over quantity.
Terms in bold are defined in CONTEXT.md. See also ADR-0001 (Rust binary with embedded skills) and ADR-0002 (an existing issue branch means continue).
User Stories
Invocation and output
- As a developer, I want to run
thirdshift <Issue URL>from a clone of the issue's repo, so that one command takes an issue all the way to a PR. - As a developer, I want thirdshift to be a single binary installed with
cargo install --path ., so that it runs on any machine withclaude,ghandgitwithout a checkout of the thirdshift repo. - As a developer, I want only the PR URL on stdout, so that I can script it (
url=$(thirdshift …)). - As a developer, I want short progress lines on stderr while sessions run (skill invoked, tool calls, commits, pushes, CI status), so that I can see a 10–40 minute run is alive without reading the transcript.
- As a developer, I want exit code 0 only when the run ends with an open, ready-for-review, mergeable PR with passing CI, so that success means something.
- As a developer, I want a non-zero exit code and the path of the most recent session log on any failure, so that I know where to look.
- As a developer, I want a usage message and a non-zero exit when the argument is missing or not a GitHub issue URL, so that typos fail fast.
Pre-flight checks
- As a developer, I want the run to stop with a mismatch error when the Issue URL's repo differs from the raw
originURL of the directory I'm in, so that I never implement an issue in the wrong repo. - As a developer, I want the Origin match to accept HTTPS and SSH origins, a missing
.gitsuffix and any letter casing, so that normal clone variations don't block me. - As a developer, I want non-github.com URLs and non-
/issues/<n>URLs rejected, so that only GitHub issues are accepted. - As a developer, I want the run to stop if the issue is closed, so that I don't spend time and money on done or abandoned work.
- As a developer, I want the run to stop on a detached HEAD, so that there is always a well-defined Base branch.
- As a developer, I want the run to stop if the Base branch doesn't exist on origin, so that the PR has something to target.
- As a developer, I want the run to stop if my local Base branch is ahead of origin, so that unpushed commits don't leak into the PR's diff.
- As a developer, I want uncommitted changes in my directory to be allowed and simply left out, so that my own work in progress doesn't block a run.
- As a developer, I want the run to stop if a local Issue branch exists and points somewhere other than its origin copy, so that local-only commits are never destroyed.
- As a developer, I want all pre-flight checks to happen before any worktree or agent session is created, so that bad runs cost nothing.
Choosing the Issue branch
- As a developer, I want a fresh run to create
issue-<n>from the Base branch, so that branches are predictable. - As a developer, I want a run on an issue whose highest-numbered Issue branch has no PR to continue that branch, so that a Failed run's pushed work is picked up where it stopped.
- As a developer, I want a run on an issue whose highest-numbered Issue branch has an open PR to continue that branch and update that PR, so that I can resume work from a different server or environment.
- As a developer, I want the open PR's base to be the Base branch in a Continuation, whatever I have checked out, so that the review fixed point and the PR target agree.
- As a developer, I want a note on stderr when my checked-out branch differs from the open PR's base, so that the substitution isn't silent.
- As a developer, I want a run on an issue whose highest-numbered Issue branch's PR was merged or closed to create
issue-<n>-branch-<k+1>fresh from the Base branch, so that finished work is never reopened. - As a developer, I want branch numbers to count as used when their PR is merged or closed even if GitHub deleted the branch, so that a deleted
issue-<n>is never reused. - As a developer, I want Issue branch selection to take only a couple of network calls, so that it adds seconds, not minutes.
Worktree and session
- As a developer, I want each run to happen in a new git worktree in a sibling directory of the repo root, so that my own checkout is never touched and runs can happen side by side.
- As a developer, I want the Factory skills written to a temp directory with a generated plugin manifest and loaded as the
thirdshiftplugin, so that the target repo is never polluted and its own skills don't clash. - As a developer, I want sessions to run headless in Claude Code auto mode, so that they run unattended with as much permission as the Linux user has, gated by the auto-mode classifier.
- As a developer, I want a fresh run to use the fresh prompt (below), so that the agent implements, reviews against the Base branch, addresses findings it agrees with, and opens a ready PR.
- As a developer, I want a Continuation to use the continuation prompt (below), so that the agent builds on existing commits instead of starting over, and updates the existing PR.
- As a developer, I want every skipped Standards finding and Spec finding listed with at least a one-line reason in an "Unaddressed findings" section of the PR body, so that I make those calls at review time.
- As a developer, I want the tested seams listed in the PR body, so that I can see what the agent chose to test.
- As a developer, I want "Closes #" in the PR body, so that merging closes the issue.
- As a developer, I want each session's full transcript written to its own log file under
~/.thirdshift/logs/, with the directory created if missing, so that I can debug any run afterwards.
After the implement session
- As a developer, I want thirdshift itself to push the Issue branch after every session, so that work is never stranded because an agent stalled before pushing.
- As a developer, I want thirdshift to check the PR through
gh(it exists, is open, and targets the Base branch), so that success doesn't depend on trusting the agent's last message. - As a developer, I want thirdshift to mark a draft PR ready with
gh pr ready, so that "ready for review" is enforced deterministically.
Repairs
- As a developer, I want thirdshift to merge
origin/<base>into the Issue branch after the PR exists, and push if the merge is clean, so that the PR stays mergeable. - As a developer, I want a conflicting merge to be left in progress and handed to a conflict Repair session using the resolving-merge-conflicts skill, so that conflicts are resolved with the intent of both sides preserved.
- As a developer, I want merges, never rebases or force-pushes, so that a branch worked on from several servers never loses history.
- As a developer, I want conflicts handled before CI is watched, so that CI (which GitHub doesn't run on conflicting PRs) actually runs.
- As a developer, I want thirdshift to wait up to one minute after each push for any check or status to appear on the head commit, and treat none as "no CI", so that repos without CI, or with CI outside GitHub Actions, both work.
- As a developer, I want thirdshift to watch checks until they finish when they appear, so that the result reflects real CI.
- As a developer, I want red CI handed to a CI-fix Repair session with the failed checks and their links, so that failures get fixed without me.
- As a developer, I want the CI-fix session forbidden from skipping, disabling or weakening tests or checks, so that a "fix" never hides the problem.
- As a developer, I want failures not caused by the branch (flaky, or also failing on the Base branch) recorded in a "CI notes" section of the PR body instead of "fixed", so that I see them without code being changed for them.
- As a developer, I want the merge-then-CI cycle to repeat after each Repair, so that a Base branch that moved during a run is still caught.
- As a developer, I want at most 3 Repair sessions per run, conflict and CI-fix combined, so that a flaky test can't keep an unattended run going forever.
Failure and cleanup
- As a developer, I want a Failed run to commit any uncommitted work in a failure commit (
thirdshift: failed run (<reason>), with a timestamp and hostname) and push it, so that the branch itself shows it came from a Failed run and nothing is lost. - As a developer, I want no push at all when the Issue branch has no changes against the Base branch, so that empty branches don't appear on GitHub.
- As a developer, I want a Failed run that has an open PR to convert it back to a draft, so that a PR only claims to be ready when thirdshift stands behind it.
- As a developer, I want the next successful Continuation to mark that PR ready again, so that recovering is automatic.
- As a developer, I want Ctrl-C to go through the same failure path (failure commit, push, draft, cleanup), so that interrupting a run is safe.
- As a developer, I want the worktree and the local Issue branch deleted at the end of every run, success or failure, so that my workspace stays tidy.
- As a developer, I want the temp plugin directory removed at the end of every run, so that nothing is left behind.
Ongoing use
- As a developer, I want to re-run thirdshift on the same issue after a Failed run and have it continue from the failure commit, so that retrying is one command.
- As a developer, I want to run thirdshift on an issue I started on another server and have it pick up the pushed branch and open PR, so that work moves freely between environments.
Implementation Decisions
Shape
- A Rust binary crate at the repo root, named
thirdshift. No flags in v1; the only argument is the Issue URL. - The Factory skills (the repo's
skills/directory: implement, code-review, pr, tdd, resolving-merge-conflicts) are embedded at compile time. Each run writes them plus a generated.claude-plugin/plugin.json(name: "thirdshift") into a temp directory and passes it with--plugin-dir. Editing a skill requires a rebuild (ADR-0001). - thirdshift is deterministic glue around three external programs:
git,ghandclaude. Everything the program can decide itself (branch choice, worktrees, pushes, PR readiness, merging the Base branch, CI watching, drafts, cleanup) it does itself. Agents only do judgement work: implementing, reviewing, writing the PR, resolving conflicts, fixing CI.
Pre-flight
- The Origin match reads the raw configured origin URL (
git config remote.origin.url), notgit remote get-url, which expandsinsteadOfrewrites. Both the origin and the Issue URL are normalised togithub.com/<owner>/<repo>(HTTPS or SSH, optional.git, case-insensitive) and compared. - Issue state comes from
gh issue view. A closed issue is an error. - Order: argument parsing → Origin match → issue open → Issue branch selection (which may redefine the Base branch) → Base branch checks (not detached, exists on origin, local not ahead) → local Issue branch consistency. Nothing is created until all of them pass.
Issue branch selection
- Inputs: remote branches matching
issue-<n>andissue-<n>-branch-<k>(onegit ls-remotecall), and all PRs, any state, whose head is one of those names (onegh pr list --state allcall). - A branch number is "used" if its branch exists on origin or it has a PR. Take the highest used number:
- None → Fresh on
issue-<n>from the Base branch. - Branch exists and its PR is absent or open → Continuation on that branch. With an open PR, the PR's base becomes the Base branch.
- Its PR is merged or closed → Fresh on the next number, from the Base branch.
- None → Fresh on
- The worktree goes in a sibling directory of the repo root, named after the repo and the Issue branch. A Fresh run creates the branch from
origin/<base>. A Continuation fetches and checks out the existing branch.
Sessions
-
Every session:
claude -p --permission-mode auto --plugin-dir <temp> --output-format stream-json --verbose, with the worktree as its working directory. The stream is written to that session's log and summarised to stderr as progress lines. -
Logs:
~/.thirdshift/logs/<owner>-<repo>-issue-<n>-<timestamp>-implement.jsonl, and…-repair-<i>.jsonlfor Repairs. All sessions in a run share the run's timestamp. -
Fresh prompt:
/thirdshift:implement <Issue URL> The base branch is <base>. Review with /thirdshift:code-review using <base> as the fixed point. Address the Standards and Spec findings you agree with. Push branch <branch> and create a pull request against <base> using /thirdshift:pr, marked ready for review. In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason. Include "Closes #<n>" in the PR body. -
Continuation prompt:
/thirdshift:implement <Issue URL> You are continuing work on branch <branch>, which already has commits (see git log <base>..HEAD). Build on them; don't start over. The base branch is <base>. Review with /thirdshift:code-review using <base> as the fixed point. Address the Standards and Spec findings you agree with. Push branch <branch>. [If a PR exists:] Update PR <PR URL> using /thirdshift:pr, rewriting its body to cover the whole branch, marked ready for review. [Otherwise:] Create a pull request against <base> using /thirdshift:pr, marked ready for review. In the PR body, add an "Unaddressed findings" section listing each skipped finding under Standards or Spec, with at least a one-line reason. Include "Closes #<n>" in the PR body. -
Conflict Repair prompt:
/thirdshift:resolving-merge-conflicts A merge of origin/<base> into <branch> is in progress in this worktree and has conflicts. <branch> implements <Issue URL>; its pull request is <PR URL>. Resolve the conflicts, finish the merge, and push <branch>. Do not rebase or force-push. -
CI-fix Repair prompt:
CI failed on pull request <PR URL> (branch <branch>, implementing <Issue URL>). Failed checks: - <check name>: <details URL> - … Read the failure logs (e.g. `gh run view <run-id> --log-failed`), find the root cause, and fix it. Do not skip, disable, or weaken tests or checks to make them pass. Run the affected checks locally, commit, and push <branch>. If a failure is not caused by this branch (it is flaky, or also fails on <base>), do not change code for it. Instead, add it to a "CI notes" section of the pull request body with a one-line explanation.
After the implement session
- Push the branch (a no-op if already pushed). Then
gh pr view <branch> --json url,state,baseRefName,isDraft: it must exist, be open, and target the Base branch, or the run fails. A draft is fixed withgh pr ready.
Repair loop
repairs = 0
loop
fetch origin <base>; merge origin/<base> into <branch>
clean → push
conflict → if repairs == 3: fail; run conflict Repair; repairs++; push; continue loop
wait ≤ grace period for any check/status on the head commit
none → success
watch checks until done
green → success
red → if repairs == 3: fail; run CI-fix Repair with failed checks; repairs++; push; continue loop
- The grace period defaults to 60 seconds. Poll intervals and the grace period can be overridden through environment variables (for tests only, undocumented for users).
- Success also re-checks that the PR is open, ready, and mergeable.
Failure path
- Triggered by any failure after the worktree exists, including a failed session, a missing or wrong PR, repairs exhausted, or SIGINT/SIGTERM.
- In the worktree:
git add -A, thengit commit --allow-emptywith messagethirdshift: failed run (<reason>), a blank line, then<ISO timestamp>, host <hostname>. Uncommitted work at the time of failure is included in this commit.. If an unfinished merge is in progress, it is aborted first so the commit can be made. - If the Issue branch has no changes against the Base branch, skip the commit and push. Otherwise push it.
- If an open PR exists for the branch, convert it to a draft (
gh pr ready --undo). - Reasons:
interrupted,claude exited <code>,no PR found,PR targets <x>, not <base>,repairs exhausted: <conflict|CI red>, and so on.
Cleanup (always)
- Remove the worktree, delete the local Issue branch (its work is on origin, or there was none), and remove the temp plugin directory. This runs on success, failure and interruption.
Testing Decisions
- One seam: the
thirdshiftCLI. Tests run the compiled binary and assert only on externally observable behaviour: stdout, stderr, exit code, the state of the git remote, the state of the fake GitHub, and what the fakeclaudewas asked to do. No tests call internal functions, so the internals can be refactored freely. Origin match and Issue branch selection cases are covered through the binary (pre-flight is fast) rather than through a second seam on pure functions. - Harness:
- git is real. Each test builds a temp bare repo as "origin" and a launch clone whose
originishttps://github.com/<owner>/<repo>.git, redirected to the bare repo with a gitinsteadOfrule. Fetches, pushes, worktrees, merges and conflicts are therefore real. ghis a fake executable onPATH, backed by a JSON state file (issues and their state; PRs with head, base, state and draft; check runs per commit). It supports only the subcommands thirdshift uses, and tests seed and inspect that state.claudeis a fake executable onPATH, scripted per test: make commits, push or not, create or update a PR via the fakegh, leave it as a draft, resolve a conflict, exit non-zero, or hang. It records its argv and prompt so tests can assert the prompt variant, the plugin directory contents (manifest namethirdshift, all skills present),--permission-mode auto, and log file creation.- Timing overrides through environment variables keep the CI grace period and polling to milliseconds. Interruption is tested by sending SIGINT to the child process.
- git is real. Each test builds a temp bare repo as "origin" and a launch clone whose
- Scenarios to cover (at minimum): every pre-flight rejection; the Origin match variants; Fresh, Continuation with no PR, Continuation with an open PR (including a base that differs from the checkout), and merged-or-closed → next number (including a deleted branch); the PR checks and draft fixing; clean merge, a conflict Repair, a CI-fix Repair, no-CI (grace expiry), green CI, and the repair cap; every failure-path branch (failure commit contents, the no-changes skip, draft demotion); SIGINT; and cleanup always leaving no worktree, local branch or temp directory.
- What a good test is: it reads like a scenario ("continuing an issue whose PR was merged starts issue-7-branch-2 from main") and asserts on outcomes a user could observe, not on how the program got there. Expected values come from the scenario, not recomputed the way the code computes them.
- Prior art: none. This repo has no code yet; these are its first tests.
- Manual acceptance run: once the automated suite passes, run thirdshift for real on
JacobStephens2/vacation#3("Skeleton: render the packing list from the docs directory", a sub-issue of specvacation#2) from a clone of that repo. It passes if it produces a ready-for-review PR with an Unaddressed findings section and passing (or absent) CI, and leaves no worktree or local branch behind.
Out of Scope
- Any command-line flags: budget limits (
--max-budget-usd), timeouts, model choice, an interactive--watchmode. - Limits on a runaway implement session beyond Ctrl-C. The only cap is the 3 Repairs.
- Rebasing the Issue branch or force-pushing, ever.
- Agent personas or a separate git identity for agent commits.
- A landing page on thirdshift.app.
- Issue trackers other than GitHub, and hosts other than github.com.
- Running several issues in one invocation, or queueing and scheduling runs.
- Reusing an Issue branch whose PR was merged or closed.
Further Notes
- The runner's Linux user needs
claude(logged in),gh(logged in) andgitwithuser.nameanduser.email, plus the Rust toolchain to build. The runner can usesudoif the user has it; the auto-mode classifier may still block risky actions, and a blocked action can end in a Failed run. - Credit: the Factory skills are adapted from mattpocock/skills (MIT). Keep the copyright notice and license with the embedded skills, and credit the project in the README.
- The design discussion is recorded in
CONTEXT.md, ADR-0001, ADR-0002 anddocs/research/naming.md.
Activity
JacobStephens2 commentedon Sep 25, 2026
Spec complete. Every sub-issue is closed:
cargo testonmainpasses: 87 tests, 0 failures.Follow-ups filed from the acceptance run:
thirdshift-plugin-*temp dir can outlive its Run. This is a leak outside the normal and SIGINT/SIGTERM paths, which user story 55 doesn't cover.