gitkit
The shared git layer other skills borrow: the worktree path, name, and lifecycle, a worktree's declared setup and teardown, the base branch, rebase versus merge, branch sync, hotfix branches, merged-branch cleanup, lost-work recovery, and stacked branches. Use when the user says "make me a worktree for #42", "where's the worktree for this branch", "tear down this worktree", "set up this worktree", "make this worktree's dev database", "what's the base branch here", "should I rebase or merge", "sync this branch with main", "start a hotfix for the login crash", "backport this hotfix", "clean up my merged worktrees", "I lost a commit", "stack this on #43", or runs "/gitkit", and whenever another skill needs any of those answers.
Savant verdict: Review advised
Security analysis or source-hub review identified patterns requiring attention.
Live evaluation
Not evaluated yet. Workspaces can request a live evaluation.
Security assessment
Scan pending.
Structure
- The source hub's security checks flagged this skillGen Agent Trust Hub: warn (The skill manages git worktrees and automates development environment setup. It contains a 'prepare' feature that executes shell commands defined within the repository's own documentation files (AGENTS.md or CLAUDE.md). While intended for automation, this allows a repository to execute arbitrary code on the user's system if the skill is used on an untrusted or malicious codebase.)
SKILL.md
---
name: gitkit
description: >-
The shared git layer other skills borrow: the worktree path, name, and lifecycle, a worktree's declared setup and teardown, the base branch, rebase versus merge, branch sync, hotfix branches, merged-branch cleanup, lost-work recovery, and stacked branches. Use when the user says "make me a worktree for #42", "where's the worktree for this branch", "tear down this worktree", "set up this worktree", "make this worktree's dev database", "what's the base branch here", "should I rebase or merge", "sync this branch with main", "start a hotfix for the login crash", "backport this hotfix", "clean up my merged worktrees", "I lost a commit", "stack this on #43", or runs "/gitkit", and whenever another skill needs any of those answers.
license: MIT
allowed-tools: Bash, Read
metadata:
internal: false
---
# gitkit
One place where the git facts live that every other skill in a toolchain keeps re-deriving: **where a worktree goes and what it's named, how it is created, adopted, and removed, which branch is the base, and when to rebase versus merge.**
The **core** here is not clever. All of it is native [git worktree](https://git-scm.com/docs/git-worktree), with no vendor CLI, no GUI dependency, and no recorded state anywhere but git's own. That is the point: a laptop with a desktop dev tool and a headless Linux box run *identical* commands, so there is no second code path to keep in sync and no "degraded mode" to reason about.
**Two branches of this skill sit outside that core, and say so.** [Stacked branches](#stacked-branches) drive GitHub's `gh stack` extension, so that branch alone needs `gh` and a GitHub remote. It degrades to plain git rather than failing. [`prepare`](#prepare) runs the commands a repository declares for a new worktree, and it chooses none of them itself: a repository that declares nothing gets the install its lockfile implies and no more. Nothing else in this file depends on either branch.
gitkit is a **primitives skill**. Most of its runs come from another skill calling it, not from a human saying its name.
## When this fires
- **worktree.** "Spin up a worktree for this branch", "make me a worktree for #42", "where is the worktree for `issue-42-…`", "tear down this worktree", "list my worktrees".
- **base ref.** "What's the base branch here", "diff me against the base".
- **sync.** "Should I rebase or merge here", "sync this branch with main", "this branch is behind, bring it up to date", "make this PR review-ready on the latest base".
- **prepare.** "Set up this worktree", "install and seed this worktree", "make this worktree's dev database", "prepare again".
- **hotfix.** "Start a hotfix for the login crash", "quick change to main, skip the issue", "backport the hotfix to develop".
- **clean.** "Clean up my merged worktrees", "which branches can I delete", "this repo is full of dead branches".
- **rescue.** "I lost a commit", "my rebase ate my work", "I reset the wrong branch", "what's in this stash".
- **stack.** "Stack this on #43", "start #44 on top of #43's branch", "restack this chain", "show me the stack".
- **called by another skill.** Any skill that needs a worktree, a base ref, a branch name, or a sync decision delegates here and uses what comes back.
**Not this skill:** committing, opening a PR, reviewing a diff, judging code. gitkit answers *where and how*, never *what the change should be*.
## Modes
| mode | does | lives |
|---|---|---|
| `worktree` | create, adopt, look up, list, and remove a worktree | [below](#worktree) |
| `sync` | catch a feature branch up with its own remote, bring it onto the latest base, and force-push it with a lease | [below](#sync) |
| `prepare` | run the repository's declared setup in a worktree, and its teardown before removal | [prepare.md](./prepare.md), and [below](#prepare) |
| `hotfix` | cut a fix or a quick change from the branch production deploys from, with no issue, and backport it after the merge | [hotfix.md](./hotfix.md), and [below](#hotfix) |
| `clean` | sweep the merged worktrees and branches away, one confirmation each | [clean.md](./clean.md) |
| `rescue` | find work that looks lost and restore it onto a new branch | [rescue.md](./rescue.md) |
| `stack` | build and restack a chain of branches through `gh stack` | [stacks.md](./stacks.md), and [Stacked branches](#stacked-branches) |
`worktree` and `sync` sit inline because nearly every run and every calling skill reaches them. The other five load only when their mode fires.
**When the mode is not clear from the ask, pick it from the verb and say which one you picked.** "Tidy this up" after a merge is `clean`; "tidy this up" on a branch behind its base is `sync`. Naming the mode in the first line is what lets the human correct a wrong pick before anything runs.
## The convention
### Path
```
$WORKTREE_ROOT/<repo-basename>/<local-branch-name>
```
`$WORKTREE_ROOT` defaults to `~/worktrees`; honor the environment variable when it is set. `<repo-basename>` is the main checkout's directory name.
**Slashes in a branch name flatten to dashes.** A branch `mimukit/6-marketing-site` gets the directory `mimukit-6-marketing-site`, not a nested `mimukit/` parent. This keeps one flat level per repo, so `ls "$WORKTREE_ROOT/<repo>"` is a complete inventory rather than a tree to walk. The trade is that the directory name no longer round-trips back to the branch name, which costs nothing, because [lookup is always through git](#look-up-a-branchs-worktree), never by reading a path. On the rare collision (branches `a/b` and `a-b` both flatten to `a-b`), `git worktree add` fails on the existing path: report it and let the human pick a name rather than silently appending a suffix.
Worktrees live **outside** the repository, never in a `.worktrees/` directory inside it. An in-repo worktree gets swept into docker build contexts, bind mounts, file watchers, and test globs, and every one of those failures shows up far from its cause.
### The invariant: one branch, one worktree
**A branch has at most one worktree, and the directory is named after it.** A worktree is keyed to a *branch*, not to a workflow stage, so implementing a feature and later reviewing its pull request use the same worktree, because they are the same branch.
This is not a style preference. Git enforces it:
```
$ git worktree add ../wt-b feature-x
fatal: 'feature-x' is already used by worktree at '.../wt-a'
```
Any scheme that names worktrees by stage (`review-…`, `qa-…`) hard-fails the moment two stages touch one branch, which is the normal case, not the edge case.
Consequences:
- **Lookup is by branch, always**, through git, never by reading or guessing at a directory path. This is what makes the flattening above free, and what lets a worktree sitting in some older root keep working untouched.
- **The "kind" of a worktree rides along for free**, because the branch name already carries it. `issue-42-auth-capability-module` is named that because the *branch* was named that when the work started.
### Branch naming
- **`issue-<n>-<slug>`** for work that starts from a tracker issue. Slug from the issue title: if it follows a conventional `type(scope): summary` form, strip the prefix; kebab-case the rest, cap at roughly 50 characters on a word boundary, drop a trailing hyphen. An empty slug degrades to a bare `issue-<n>`. The number guarantees uniqueness, so no tie-break is ever needed.
- **`hotfix-<slug>`** for an urgent fix or a quick change that goes straight to the branch production deploys from, with no issue first. Slug from the symptom for a fix, or from the change for a quick change, kebab-cased, capped at roughly 40 characters. A backport of a merged hotfix appends `-to-<target>`. The number is absent because a hotfix starts from the symptom rather than from a tracker issue; when one *does* trace to an issue, the branch still takes this shape and the pull request carries the `Closes #<n>` link. On a collision with an existing branch, add a short distinguishing word rather than a counter. **The separator is a hyphen: write `hotfix-<slug>`, never `hotfix/<slug>`.** A slash makes git treat `hotfix` as a directory in the ref namespace, so `hotfix-restore-session` and `hotfix/restore-session` cannot both exist, and every skill that reads the type off the branch name (commitkit, prkit) matches the hyphen form only.
- **`pr-<n>-<slug>`** only for a **fork** pull request, where no local branch exists yet and one must be invented to hold the fetched head. Slug from the head branch name, kebab-cased, capped at roughly 40 characters.
- Anything else takes a branch name the human or the repo's convention supplies. gitkit does not rename it.
**An agent that cuts a branch on its own uses one of the three shapes above.** It never invents another prefix (`feature/…`, `fix-…`, `claude/…`). A name the human types, or one the repo documents, is the only other kind of branch.
For a same-repo pull request there is already a local branch name: use it. Inventing a `pr-*` name for a branch that exists is how you land on the two-worktrees-one-branch failure above.
## `worktree`
All four are native git, and all four are **idempotent**, so running one twice is a normal thing to do and must never error or destroy work.
### Look up a branch's worktree
The one lookup primitive everything else is built on:
```sh
git -C "$REPO" worktree list --porcelain
```
Each record carries a `worktree <path>` line and, when a branch is checked out, a `branch refs/heads/<name>` line. Match on the branch to get the path. If the porcelain shape ever shifts, check `git worktree list --help` rather than guessing, but treat the `worktree`/`branch` pairing as stable; that is what porcelain output is for.
### Create, or adopt
Always look first. **If a worktree already exists for the branch, adopt it: report the path and stop.** Never create a second one, never error.
When there is none:
```sh
WT="${WORKTREE_ROOT:-$HOME/worktrees}/$(basename "$REPO")/$(printf '%s' "$BRANCH" | tr / -)" # slashes flatten to dashes
git -C "$REPO" fetch origin --prune
git -C "$REPO" worktree add -b "$BRANCH" "$WT" "$BASE"
```
- Fetch first, always. Branching off a stale base is silent and only surfaces as conflicts later.
- `$BASE` comes from [The base ref](#the-base-ref), never a hard-coded `origin/main`, and never a sibling feature branch. The one exception is a [stack layer](#stacked-branches), whose base *is* the branch below it, named deliberately by the caller.
- Drop `-b` when the branch already exists locally (a same-repo PR you have fetched, a branch you made earlier); `git worktree add <path> <branch>` checks it out.
- For a **fork** pull request, fetch the head into a local branch first with `git fetch origin "pull/<n>/head:pr-<n>-<slug>"`, then add the worktree on that branch, no `-b`.
Then record what gitkit made, because a later session has no other way to tell its own worktrees from someone else's:
```sh
touch "$(git -C "$WT" rev-parse --absolute-git-dir)/gitkit-created" # gitkit made this worktree
git -C "$REPO" config "branch.$BRANCH.gitkitCreated" true # only after -b: gitkit made the branch too
```
- **Both records live in git's own storage**, so they need no state file. The worktree marker sits in the worktree's admin directory under `.git/worktrees/`, and `git worktree remove` deletes it with the worktree. The branch key sits in the branch's config section, and `git branch -d` deletes it with the branch.
- **Write the branch key only when this run passed `-b`.** A branch that already existed belongs to whoever made it, and an adopted worktree gets no marker.
- **No record means not gitkit's.** A worktree or branch made before this rule existed, or made by hand, reads as adopted, and [Remove](#remove) leaves it alone.
When the caller hands the worktree to a person or an agent to work in, run [`prepare`](#prepare) next, on a created and an adopted worktree alike. A scratch worktree that only runs one command skips it.
Report the path and the branch. When the user asked gitkit for the worktree directly, end the reply with [the workspace block](#the-workspace-block). Creating a worktree does not imply doing anything in it. Ends when the worktree exists at `$WT` on `$BRANCH` and its records are written.
### The workspace block
A skill that hands a worktree to a human to work in ends its reply with this block. The labels are fixed; fill the values.
```
Workspace ready
- repo: <absolute path of the main checkout>
- worktree: <absolute worktree path>
- branch: <branch>
- base: <base ref the branch was cut from or tracks>
- state: created | adopted
- for: issue #<n> | PR #<n> | none
```
- **The block is a contract with the host, not with a tool.** It names no harness and no app. A host that can open a new session bound to a worktree reads it; any other host shows six plain lines.
- **It starts nothing.** It carries no prompt and no command to run in the worktree. The human decides how the work begins.
- **Print it once, last, and only to a human.** When another skill called gitkit, return the path and the branch to that caller, and the caller prints the block in its own hand-off. An unattended run prints no block, because it works in the worktree by path in its own session.
### Remove
```sh
test -e "$(git -C "$WT" rev-parse --absolute-git-dir)/gitkit-created" # gitkit made this worktree?
git -C "$REPO" config --get "branch.$BRANCH.gitkitCreated" # gitkit made this branch?
git -C "$REPO" worktree remove "$WT"
git -C "$REPO" branch -d "$BRANCH"
```
**This is the one removal rule in the collection.** Every skill that removes a worktree or deletes a branch, whether a sweep here, a workspace tool's cleanup, or an issue close-out, follows it and cites it rather than restating its own. Four rules, each one guarding against a real way to lose work:
- **A dirty worktree stops teardown.** Show exactly what would be lost (uncommitted changes, untracked files, unpushed commits) and let the human decide. Never reach for `--force` on their behalf.
- **Remove only a worktree whose marker says gitkit created it.** A worktree without the marker was adopted: it is someone else's context, and you have no idea what is open in it.
- **Delete only a branch whose `gitkitCreated` key is set, and delete it with `-d`.** Git itself then refuses an unmerged branch, so a wrong verdict fails loudly instead of deleting the work.
- **One confirmation per removal.** Each worktree and each branch is its own decision, so a list of them takes one answer per row and never one answer for the set.
- **A prepared worktree runs its teardown first.** When the `gitkit-prepared` marker exists and the worktree declares `teardown:`, run it after the dirty check and the confirmation, before `git worktree remove`. A failed teardown keeps the worktree and reports the failure. [Teardown](./prepare.md#teardown) holds the procedure.
**The squash exception is the only `-D` gitkit runs.** A squash-merged branch never passes `-d`, because the squash commit has no ancestry link back to the branch. Use `-D` only when `gh` reports a merged pull request for the branch and that pull request's `headRefOid` equals the local branch tip:
```sh
gh pr list --head "$BRANCH" --state merged --json number,headRefOid --jq '.[0]'
git -C "$REPO" rev-parse "$BRANCH" # must equal headRefOid, or -D is off the table
```
The equal SHA proves that every commit on the branch is in the merged pull request. A tip that moved after the merge holds commits the merge never saw, so the branch stays and the report names it. Name the pull request number in that row's confirmation.
**The merged-PR exception lifts the marker rule and the confirmation rule for one PR's own workspace.** When a human tells a caller to merge or close one pull request, and the caller then sees it merged, that instruction is the confirmation to remove the worktree of the PR's head branch and the local head branch. They go whether gitkit created them or not, with no further prompt. The caller passes the PR number, and the exception reaches that one branch and its worktree only. These guards still hold, and each one keeps its item and names it in the report:
- **The human said keep.** "Merge it but keep the worktree" keeps the worktree, and "keep the branch" keeps the branch.
- **The worktree is dirty.** The dirty rule above still stops the removal.
- **The teardown failed.** The teardown rule above still keeps the worktree. Teardown itself runs with no prompt under this exception.
- **The branch tip moved.** Delete with `-d`, or with `-D` only when the tip equals the merged `headRefOid`, as the squash exception states.
- **A layer stacks on the branch.** An open PR whose base is this branch keeps the branch, and the worktree still goes.
When the worktree to remove is the caller's own working directory, run every command from the main checkout with `git -C "$REPO"`. After the removal, say that the session's working directory is gone, and name the main checkout as the place to continue.
Already gone? Report "already gone" and succeed. Teardown is idempotent in the same spirit as adopt. Ends when each confirmed worktree and branch is gone, or its refusal is reported.
### List
```sh
git -C "$REPO" worktree list
```
A plain inventory. When the user asks to tidy up rather than to look, that is the [`clean`](#clean) mode, which classifies every row before it offers anything.
If paths look wrong after a move or a restore, `git worktree repair <path>` rewrites the back-pointers. Git stores absolute paths in `.git/worktrees/<name>/gitdir` and in each worktree's `.git` file, so moving a worktree by hand always needs a repair.
## The base ref
Never assume `main`. Repos default to `develop`, `trunk`, and `master` in the wild, and getting this wrong silently produces an empty or enormous diff. The ladder, in order:
1. `git config --get "branch.$BRANCH.gh-merge-base"`, for a branch that records its own base. [`hotfix`](#hotfix) sets it, and `gh pr create` reads the same key, so the sync, the diff, and the pull request all agree.
2. `gh repo view --json defaultBranchRef --jq .defaultBranchRef.name`, authoritative when the GitHub CLI is available and authenticated.
3. `git symbolic-ref --short refs/remotes/origin/HEAD`, the local record, if it has been set.
4. `git remote set-head origin --auto` to repair an unset `origin/HEAD`, then retry the previous step. This is the step most implementations skip, and it is the one that turns a failure into an answer.
5. Whichever of `origin/main` / `origin/master` actually exists.
6. Ask the human. Do not guess past this point.
Everything downstream uses the answer: `git diff <base>...HEAD`, ahead/behind counts, and the branch a worktree is cut from.
**A stack layer is the one deliberate exception, and the ladder still runs.** A layer's base is the branch below it rather than the repo default, so the caller names it and this ladder is not consulted for that branch. The ladder is still what resolves the **trunk** the stack's bottom layer sits on. The ban on a sibling-branch base keeps its teeth everywhere else, because what it catches is the *accidental* case: a worktree cut from whatever happened to be checked out. A layer base is legal because somebody said which branch and why. See [Stacked branches](#stacked-branches).
## Rebase or merge
Syncing a feature branch with its base has one default here, and it does not depend on whether a pull request is open:
> **Rebase onto the base to sync a feature branch, published or not. Merging the base in is an exception that needs a stated reason and the user's consent.**
The default is rebase because that is the history worth having: a feature branch that reads as a linear sequence of its own commits, with the base's work underneath rather than braided through it. A repo that merges the base in on every sync accumulates one merge commit per sync, and the branch's own story becomes unreadable long before it lands.
Rebasing does have a real cost, and it is mechanical rather than aesthetic: it rewrites commit SHAs. Once a pull request is open, its review threads are anchored to those commits, so a force-push marks every one of them outdated, human and bot findings alike. That is a cost to **disclose**, not a bar, so the sync reports the count after the push. It is also the usual reason a user asks for the merge exception.
In practice:
```sh
git fetch origin
git rev-list --left-right --count "origin/$BASE"...HEAD # "<behind>\t<ahead>"; left > 0 means behind
git rebase "origin/$BASE"
```
- **Rebase is the answer unless someone has said otherwise for this branch.** No pull request, an open one, a shared branch: the recommendation is the same.
- **A sync runs straight through, published branch or not.** Asking for the sync is the consent to its rebase and its `--force-with-lease` push, because they are one decision about one branch, and the ask already made it. Stop and ask only on a problem: a dirty tree, a conflict that needs a ruling, a failed gate, or a rejected lease.
- **Report the cost after the push.** On a branch with an open PR, query the GraphQL `reviewThreads` connection for unresolved threads before the rebase, and put the number in the hand-off. "3 review threads are now outdated" tells the user to re-request review; "this may have outdated comments" tells them nothing.
- **The merge exception runs only on request.** Unresolved threads are not a reason to stop and offer it. When the user asks for the merge, take [The merge exception](#the-merge-exception).
- **`--force-with-lease`, never bare `--force`.** The lease is what stops you overwriting a commit someone else pushed while you were rebasing.
- **On conflict**, resolve it by [Conflicts](#conflicts).
### Conflicts
The agent resolves every conflict and writes every resolution. The human never edits a conflicted file. The only open question per conflict is whether the resolution needs the human's ruling first, and a ruling is a pick between resolutions the agent has already written out.
**Resolve without asking** when the conflict is mechanical, meaning the intent of both sides survives in full:
- **Both sides add independent entries** (imports, exports, routes, list items, changelog lines). Keep both, in the order the file uses.
- **Both sides make the same change.** Keep one copy.
- **One side only reformats.** Keep the other side's content, then run the repo's formatter on the file.
- **A lockfile or a generated file.** Take the base's version, then regenerate it with the repo's own command (`pnpm install --lockfile-only`, the codegen script).
- **The conflict sits in context only.** The base edited or moved the surrounding lines, and the branch's change applies to the new code unaltered.
**Ask first** when the resolution is a decision rather than a merge:
- Both sides change the same behavior in ways that cannot both hold.
- A modify/delete or rename/rename conflict, or a binary file.
- The resolution drops code that either side added or kept.
- You cannot state the intent of each side in one sentence. This is the check that catches a resolution you do not understand, so run it on every conflict, mechanical ones included.
An ask names the file, the intent of each side in one sentence, the resolution you recommend, and the alternative, each shown as the code it produces. Collect every ask from one rebase stop into a single question round. Write the chosen resolution yourself.
Guards on every resolution:
- **Save the starting point** before the first rebase: `git update-ref "refs/gitkit/pre-sync/$BRANCH" HEAD`. Each sync overwrites it. `ORIG_HEAD` does not survive a second rebase; this ref makes the whole sync reversible with `git reset --hard "refs/gitkit/pre-sync/$BRANCH"`.
- **Build each resolution from both sides.** A whole-side strategy (`-X ours`, `-X theirs`, `git checkout --ours` on a source file) discards one side without a record, so it resolves lockfiles and generated files only.
- **Run `git diff --check` before each `git rebase --continue`.** It flags a leftover conflict marker.
- **Record resolutions with rerere.** Run the rebase as `git -c rerere.enabled=true rebase …`, so a conflict resolved once replays on the next sync or restack.
- **The gate judges the result.** A resolution is a code change, so the repo's test and build gate runs afterward. A gate failure in a resolved file is yours to fix: rework the resolution and rerun the gate. Ask only when the fix needs one of the decisions above.
- **An unattended caller cannot answer.** With nobody to ask, run `git rebase --abort` and return each open ask, with its recommended resolution, to the caller to escalate. When the run resumes with a ruling, re-run the sync and write that resolution.
### `sync`
The runnable form of the rule above. A sync has **two halves, in this order**: first bring the local branch level with its own counterpart on `origin`, then bring it up to date with the base. Skipping the first half is what makes a later `--force-with-lease` reject, or silently drop a commit pushed from another machine. Run it in the branch's worktree. Each step names the condition that ends it.
1. **Fix the ground.** Resolve the base with [the base ref ladder](#the-base-ref). Run `git -C "$WT" fetch origin --prune`. Confirm the worktree is on the feature branch and that `git status --porcelain` is empty. A dirty tree stops the sync: report the files and let the human stash or commit. Save the starting point with `git update-ref "refs/gitkit/pre-sync/$BRANCH" HEAD`. Ends when the base name, the branch name, and a clean tree are all known.
2. **Sync with `origin/$BRANCH`.** Run `git rev-list --left-right --count "origin/$BRANCH"...HEAD` when the remote branch exists. Behind count `0` means nothing to take. Otherwise run `git pull --rebase origin "$BRANCH"` to replay your local commits on the remote ones, and resolve every conflict by [Conflicts](#conflicts). A branch with no remote counterpart skips this step. Ends when the local branch contains every commit on `origin/$BRANCH`.
3. **Measure the gap to the base.** Run `git rev-list --left-right --count "origin/$BASE"...HEAD`. Behind count `0` means the branch is already on the latest base: run the **Refresh a prepared worktree** step below when the previous step took commits, push what it took, report that, and stop. Ends with a behind count and an ahead count.
4. **Count the review threads.** When a pull request is open, count its unresolved threads from the GraphQL `reviewThreads` connection. This step asks nothing; the count goes into the hand-off. Ends with a thread count, or with no open pull request.
5. **Rebase onto the base.** Run `git -c rerere.enabled=true rebase "origin/$BASE"`. Ends when the rebase reports success or stops on a conflict.
6. **Resolve every conflict.** For each stop, list the files with `git diff --name-only --diff-filter=U`. Sort each conflict by [Conflicts](#conflicts), write the mechanical resolutions, and ask one question round for the rest. Write the ruled resolutions, run `git diff --check`, stage the files, then `git rebase --continue`. Repeat for every remaining stop. Ends when no conflict marker remains and the rebase is complete, or, for an unattended caller, on an aborted rebase with its open asks returned.
7. **Refresh a prepared worktree.** When the `gitkit-prepared` marker exists and the sync took commits, run `prepare`'s change check and the block it selects, per [prepare.md](./prepare.md#2-decide-what-runs). It runs before the gate, because a gate that runs on a stale install or a stale schema fails for the wrong reason. A failed block stops the sync before the push. Ends with the block's exit code, or with nothing watched changed, or with no marker.
8. **Prove the branch still works.** Run the repository's own test and build gate. Report a failure with its output and stop before the push. Ends with a pass, or a stop.
9. **Push with a lease.** Run `git push --force-with-lease origin "$BRANCH"`. A rejected lease means somebody pushed while you rebased: fetch, show the new commits, and ask before any retry. Ends when the remote branch matches the local one.
#### Hand off
Report the commits taken from `origin/$BRANCH`, the base, the behind and ahead counts, what the refresh ran, the gate result, the pushed branch, and the review threads the push outdated. List each resolved file with its class and a one-line reason, mark the ones the human ruled on, and name `refs/gitkit/pre-sync/$BRANCH` as the undo point. Say when the branch was already current and nothing changed. Next, review the pull request diff on the new base, and re-request review when threads went outdated.
### The merge exception
When the user takes the merge, or asks for it outright, it is still a sync, and it says so:
```sh
git merge "origin/$BASE" -m "chore(repo): sync with origin $BASE"
```
- **Never git's default subject.** `Merge branch 'main' into issue-42-tailwind-4-foundation` is what git writes when nobody chose a message, and it reads that way forever in `git log`. The fixed subject is the message.
- **Interpolate the base, don't hardcode `main`.** The [base ref](#the-base-ref) ladder exists because `develop` and `trunk` repos are real; a subject that says `main` in one of them is simply false.
- **No `--no-ff`.** If the sync can fast-forward, the branch had nothing of its own to preserve and git writes no commit at all, so there is no subject to fix, and forcing one manufactures an empty merge commit.
- **State the reason in the same breath as the ask.** "Merging instead of rebasing because this PR has 3 unresolved review threads" is a reason. "Merging to be safe" is not; if you cannot name what rebasing would break, rebase.
### Never bare `git pull`
`git pull` on a branch behind its remote merges by default, and writes `Merge branch 'main' of github.com:owner/repo` without asking anyone. Use `git pull --rebase`, or `git pull --ff-only` when you want the sync to fail loudly rather than resolve itself. The same holds for `git pull` invoked to refresh the base branch itself: configure it or pass the flag, but never let the default run.
## `prepare`
Make a worktree ready to run with the commands its repository declares in a `## Worktree setup` section of its agent-guide file: `setup:` on the first run, `update:` when a watched path changed, and `teardown:` before [Remove](#remove) deletes the worktree. A `gitkit-prepared` marker in the worktree's admin directory records the commit each run passed at. Teardown runs only where that marker exists, so gitkit never removes a resource it did not set up.
The procedure, the declaration format, the template, and the lockfile table live in **[prepare.md](./prepare.md)**. Read it when a run prepares a worktree, when a sync took commits on a prepared one, and when a removal meets one.
## `hotfix`
Start an urgent fix or a quick change with no issue: cut a `hotfix-<slug>` worktree from the branch production deploys from, move a change already written in the main checkout into it, and record that branch as the pull request base. After the merge, `backport` replays the commits onto each branch the project names. The project declares its flow in a `## Hotfix flow` section of its agent-guide file. Without one, gitkit reads CI deploy triggers, and with no evidence it uses the default branch.
The procedure, the declaration, and the template live in **[hotfix.md](./hotfix.md)**. Read it when a run starts or backports a hotfix. gitkit stops at the prepared worktree, as everywhere else: the change, the commit, the pull request, and the deploy belong to other skills and to CI.
## `clean`
Sweep away the worktrees and branches whose work has landed, on your machine and on `origin`. It classifies every worktree, local branch, and remote branch into one bucket — active, adopted, dirty, reapable, orphan — and removes only the reapable ones, one confirmation at a time. A remote delete takes its own confirmation and its own proof that the work landed. Last, it deletes qakit screenshot folders in `~/qakit/` older than 14 days whose QA plan is on no branch it kept.
Two things make it more than a `git branch --merged` loop, and both live in **[clean.md](./clean.md)**: a squash-merged branch is invisible to ancestry, so "merged" needs three detections rather than one; and the per-item confirmation is deliberate, because a sweep's rows are not equally safe to delete. Read that file when a run actually sweeps.
The [Remove](#remove) rules govern every removal the sweep makes. It confirms each row on its own, uses `-D` only under the squash exception, never touches a worktree without gitkit's marker, and stops on a dirty one. On `origin` it never deletes the base branch and never deletes the head of an open pull request.
## `rescue`
Find work that looks lost and put it back. A bad rebase, a hard reset, a deleted branch, a stash nobody can find: the commits usually still exist, and git's own logs say where they went.
The procedure is in **[rescue.md](./rescue.md)**. Two rules shape it. It is **read-only until the restore**, and the restore **adds a branch rather than moving one**, because a reset is the operation that lost the work in the first place. It also states the bound rather than implying safety: an unreachable commit lives until git prunes it.
Never run `git gc`, `git prune`, or `git reflog expire` during a rescue, in any mode.
## Stacked branches
A **stack** is a chain of branches in one repository where each branch is cut from the one below it and the bottom sits on trunk. It is what you reach for when work depends on work that is built but not yet merged: instead of waiting for the prerequisite's pull request to land, you branch from it and keep going.
GitHub supports this natively, and the `gh stack` extension drives it. **This is the one part of gitkit that needs `gh` and a GitHub remote.** The full command surface, the preflight, and the degradation path live in **[stacks.md](./stacks.md)**; read it when a run actually touches a stack. What follows is the part every run needs.
### One worktree per layer
**Each layer keeps its own worktree, cut from the layer below.** The [one branch, one worktree](#the-invariant-one-branch-one-worktree) invariant is unchanged: a layer is a branch, so it gets a directory named after it, and [lookup is still by branch](#look-up-a-branchs-worktree).
The alternative, one worktree for the whole stack with `gh stack up` and `down` moving between layers inside it, is what the extension's own navigation assumes. It is not what this collection does, because every caller here keys a worktree to *the issue being worked*, and a stack is several issues. So `gh stack`'s navigation commands are out of scope, and `up`, `down`, `switch`, `top`, and `bottom` are not gitkit's to run: they would move a checkout that another worktree already holds.
### What gitkit owns here, and what it does not
gitkit takes the **plumbing** and nothing else: create a stack, add a layer, restack, sync, inspect, and adopt existing branches into one. Two commands are deliberately excluded, because they cross boundaries this skill holds elsewhere:
- **`gh stack submit` opens pull requests.** gitkit never opens anything. That belongs to whichever skill authors pull requests.
- **`gh stack merge` merges them.** gitkit never merges, and a stack merge is a *cascade* that lands several pull requests at once, which needs a human confirmation naming every one of them. That belongs to whichever skill owns merging.
Naming the owner rather than the command is the point: gitkit prepares ground, and publishing is somebody else's job whether the branch is stacked or not.
### The cost of depth
Every layer added to a stack is one more pull request to review and one more branch to rebase each time anything below it changes. Restacking is cascading, so a change to the bottom layer rewrites every layer above it.
**No number is stated here on purpose.** How deep a stack should go depends on how big each layer is and how fast they get reviewed, which is the human's call. State the mechanism when they ask, so they can size it themselves.
## Containers and remote boxes
Git worktrees store **absolute** paths. If a repo is bind-mounted into a container at a path different from the host's, every worktree inside it breaks.
- Mount **both** `$WORKTREE_ROOT` and the main checkout. A worktree's real git directory lives under the main repo's `.git/worktrees/`; mounting only the worktree yields a broken checkout.
- Mount both at the **same absolute path** inside and outside the container. This is the robust answer.
- Second belt, on git 2.48+: `git config worktree.useRelativePaths true` and `git worktree repair --relative-paths`. Relative pointers survive remapping *as long as the repo and the worktree root keep their relative positions*, which argues for siding them, e.g. `~/code/<repo>` and `~/worktrees/<repo>`.
A GUI dev tool that manages worktrees is welcome to **observe** these. Orca and its peers read `git worktree list` and typically have a setting to surface externally-created worktrees. That is a one-time UI configuration on whichever machine runs the GUI, and it is the only asymmetry between machines.
**No skill may create or remove a worktree through a vendor CLI**: the moment one does, the two machines diverge. A vendor's own *metadata* about a worktree, such as the issue its card links to, a status, or a comment, is a different layer and is fair game, because it exists only inside that tool and has no git equivalent to diverge from. A companion skill may reconcile that layer (**orcakit** does, for Orca) as long as the worktree itself is still created and destroyed here. gitkit never calls in that direction: it must keep working on a machine where no such tool is installed.
## Notes
- **gitkit is the single source of truth for what it owns.** A calling skill may hold policy about *when* to ask, such as that a PR is worth pulling down or that a branch is ready to publish, but never about *how* the git operation is done.
- **Restating a conclusion is allowed; restating the derivation is the bug.** Every public skill is installed on its own, into repos where gitkit may not exist, so a caller that needs a git fact has to carry enough of one to keep working, and that requirement outranks tidiness. The line runs between the two: a caller may state gitkit's **answer** together with its degradation fallback ("gitkit owns the sync rule, and it resolves to rebase; without gitkit, rebase onto the base"), and may not reproduce the **reasoning that produces** the answer, meaning the full base-ref ladder, the worktree path formula, or the rebase-versus-merge argument. The answer is one line that a rename cannot silently invalidate; the derivation is the thing that drifts. Compliant examples worth reading: prkit and mergekit each name the rebase conclusion and its one-confirmation rule without re-deriving them, and wikikit and designkit each name `gh repo view --json defaultBranchRef` as a base-ref fallback without carrying the six-rung ladder.
- **gitkit never implements, commits, or opens anything.** It prepares the ground and tears it down. The commands `prepare` runs are the repository's, never gitkit's own. Writing code in the worktree, committing, and publishing belong to other skills.
- **Destructive steps preview and confirm.** Removing a worktree and deleting a branch each show what is about to happen and wait for an OK. A teardown rides on the confirmation of its removal. Creating, adopting, and syncing run straight through. A sync rewrites a published branch, but the user who asks for it has already made that call, the lease guards the remote, and `refs/gitkit/pre-sync/$BRANCH` undoes it.
- **No shell available** (e.g. a browser-based agent)? Then you cannot run git. Reason from what the user provides and **print the exact commands** as a codeblock for them to run, and never report a worktree created or removed that you could not perform.