Repository navigation
Description
Problem Statement
Before starting a Run, I routinely git pull in the Launch directory, usually on the default branch, so that I'm working with the latest code. The Run itself doesn't need this: it already fetches origin and builds its worktree from origin/<Base branch>, so the worktree always starts from the newest code on origin. But my own checkout of the Base branch in the Launch directory falls behind unless I remember to pull by hand, and that manual step is a chore I want thirdshift to do for me on my machine. Not everyone would want thirdshift to touch their Launch directory, so it must be opt-in.
Solution
A new User config setting, launch.pull:
[launch]
pull = trueWhen it is on, every Run on that machine brings the Launch directory's checkout of the Base branch up to date with origin. It uses a fast-forward-only update, after the pre-flight checks pass and before the worktree is created. It is off by default. If the update can't happen, thirdshift prints a warning: line and carries on, because the Run doesn't depend on it. When the branch checked out in the Launch directory isn't the Base branch, the update is skipped.
User Stories
- As a thirdshift user, I want my Launch directory's Base branch brought up to date at the start of a Run, so that I no longer have to
git pullby hand before every Run. - As a thirdshift user, I want this controlled by a setting in my User config, so that I set it once per machine rather than on every command.
- As a thirdshift user who doesn't want thirdshift touching my Launch directory, I want the setting off by default, so that a Run with no User config, or one that says nothing about
launch.pull, leaves my checkout exactly as it was. - As a thirdshift user, I want to turn it off explicitly with
pull = false, so that I can document the choice in my config. - As a thirdshift user, I want a typo such as
[launch] pul = true, or a value that isn't true or false, to stop the Run before any work with an error naming the User config file and the offending key, so that a mistake can't silently leave the setting off. This is the same behavior asmerge.always. - As a thirdshift user, I want the update to be fast-forward only, so that it never creates a merge commit, never rebases, never opens an editor and never leaves my Launch directory mid-merge while nobody is watching.
- As a thirdshift user, I want the update to happen only after the pre-flight checks pass (Origin match, issue open, git identity set, Base branch on origin and not ahead of it), so that a Run that was never going to happen doesn't touch my Launch directory.
- As a thirdshift user, I want the update to happen before the worktree is created, so that my Launch directory is current from the very start of the Run.
- As a thirdshift user, I want a failed update (for example, uncommitted changes in files the update would overwrite) to print a
warning:line on stderr and let the Run continue, so that a convenience never costs me a night's Run. - As a thirdshift user, I want a failed update to leave my uncommitted changes exactly as they were, so that nothing of mine is lost.
- As a thirdshift user, I want the warning to say what failed and what I can run by hand, so that I can fix my checkout later.
- As a thirdshift user, I want a progress line on stderr when the update runs, so that I can see what thirdshift did to my Launch directory.
- As a thirdshift user scripting thirdshift, I want stdout to keep carrying only the pull request URL, so that
url=$(thirdshift "$issue")keeps working. - As a thirdshift user, I want a failed update never to change the Run's outcome or exit code, so that success and failure mean the same things they always have.
- As a thirdshift user running a Continuation whose open pull request targets a base other than my checked-out branch, I want the update skipped, so that thirdshift never moves a branch that the Run isn't based on.
- As a thirdshift user whose Launch directory is already up to date, I want the update to be a quiet no-op, so that nothing changes and nothing alarming is printed.
- As a thirdshift user, I want the Run's worktree to keep starting from
origin/<Base branch>whether or not the setting is on, so that the setting never changes what code the agent works from. - As a thirdshift user reading the README, I want the User config section to document
launch.pull, so that I can discover it and understand what it does and doesn't do.
Implementation Decisions
- Setting:
launch.pull, a boolean in a new[launch]section of the User config, off by default. There is no per-Run flag for now. Parsing follows the existingmerge.alwaysrules: an unknown key in[launch], a non-boolean value, or alaunchthat isn't a table is a config error, reported before any work and naming the file and key.update,versionandhelpstill never read the User config. - Config to Run: the loaded User config's
launch.pullvalue reaches the Run alongside its Goal. The Run module is the only consumer. - Placement in the Run: after
preflight::check, Issue branch selection, and the Base branch check. That check already fetchesorigin/<base>and fails if the local Base branch has commits that origin lacks. The update comes before the interrupt check and worktree creation. - Condition: the update runs only when the setting is on and the branch checked out in the Launch directory equals the Base branch. Otherwise it is skipped. A detached HEAD already means either a failed Run (no Base branch) or a Continuation whose pull request supplies the base, so it is always skipped.
- Operation: a fast-forward-only update of the checked-out Base branch to
origin/<base>, which has just been fetched. For example,git merge --ff-only origin/<base>in the Launch directory, which is equivalent togit pull --ff-only origin <base>. It is always againstorigin, never the branch's configured upstream. This matches Origin match, sinceoriginis the only remote thirdshift knows. Because preflight rejects a local Base branch that is ahead of origin, the only outcomes are already up to date, fast-forwarded, or blocked by the working tree. - Failure handling: any error from the update becomes a
warning:line on stderr carrying git's error. The Run then continues unchanged. This is the samewarning:convention used after a Self-merge. - Output: a progress line on stderr when the update is attempted. Nothing new goes to stdout.
- Glossary: Launch directory is now defined in
CONTEXT.md, and the User config entry mentions this setting. - No ADR: the decision is cheap to reverse and not surprising.
Testing Decisions
- A good test drives the real binary and asserts on externally visible results: the Launch directory's branch head, its working tree, stderr lines, stdout, and exit code. It does not assert on which git commands ran.
- End-to-end, through the existing
Scenarioharness (real bareorigin, real Launch directory, fakeghandclaude). Prior art: the User config tests (merge_always_*) and the preflight tests. Cases:[launch] pull = truewith origin's Base branch ahead of the Launch directory: afterwards the Launch directory's Base branch is at origin's head, and the Run succeeds as usual.- No User config, and
pull = false: the Launch directory's Base branch is unchanged. - An uncommitted change to a file that origin's new commits also change: stderr has a
warning:line, the Run still exits 0 with the pull request URL on stdout, and the uncommitted change is intact. - A Continuation whose open pull request targets a base other than the checked-out branch: the checked-out branch is not moved.
- A Run stopped by pre-flight (for example, a closed issue or an Origin match failure): the Launch directory's branch is not moved.
- Unit, in the config module (prior art:
merge_always_is_read,errors_name_the_file_and_the_offending_key):launch.pullis read, a non-boolean value is an error, and an unknownlaunch.*key is an error.
Out of Scope
- A per-Run flag (
--pull/--no-pull). - Updating any branch other than the Base branch, or pulling from any remote other than
origin. - Plain
git pullsemantics: merge, rebase, or anything that honorspull.rebase/pull.ffconfig. - Stashing, resetting, or otherwise touching uncommitted changes to make the update succeed.
- Changing where the worktree starts. It stays
origin/<Base branch>. - Updating the Launch directory again at the end of the Run, for example after a Self-merge.
Further Notes
- The setting is purely a convenience for the Launch directory. The Run's worktree already starts from freshly fetched
origin/<Base branch>, so the agent's code is current whether or not the setting is on. - Glossary update (Launch directory) came out of the grilling session for this issue.
Activity