@senad-d/branchme

Pi extension for verified Git repository initialization, commit, branch, worktree, integration, retirement, push, and pull request workflows.

Packages

Package details

extension

Install @senad-d/branchme from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@senad-d/branchme
Package
@senad-d/branchme
Version
0.3.17
Published
Oct 28, 2026
Downloads
1,777/mo · 635/wk
Author
senad-d
License
MIT
Types
extension
Size
1.7 MB
Dependencies
0 dependencies · 4 peers
Pi manifest JSON
{
  "image": "https://raw.githubusercontent.com/senad-d/BranchMe/main/img/demo.gif",
  "extensions": [
    "./src/extension.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README


BranchMe is a Pi extension for safe Git repository, branch, and worktree workflow automation. Before each agent run, it appends a bounded, read-only snapshot of the current Git repository to the system prompt. It also adds an informational /branchme command and agent-callable tools that initialize a repository, refresh state, inspect local diffs, manage, integrate, and retire local branches, conclude a kept merge, inspect/create/remove linked worktrees, push the current branch, create GitHub pull requests, read and answer PR feedback, and list and label a PR's changed files.

  • Context-aware: every agent run starts with bounded branch, working-tree, related-PR, and recent-commit metadata; repository metadata is untrusted data, not instructions.
  • Repository-scoped: init_repository can initialize only pi's exact current working directory when it is not already inside a repository. Other Git and GitHub operations resolve from the checkout where pi is running. Linked worktree directories may be outside that checkout, but must be verified members of the same repository.
  • Explicit history rewrites: rebase_branch runs only when explicitly requested, requires a clean current branch and either an upstream or explicit baseRef, disables autostash and multi-ref updates, and automatically attempts to abort on failure.
  • Verified worktree handoff: create_worktree verifies path, branch, HEAD, and cleanliness before returning an absolute handoff.cwd; starting another Pi session or subagent there remains the caller's responsibility.
  • Commit-safe: context collection is read-only, and BranchMe never force-pushes or resets. Ordinary commits happen only through commit_changes, which validates a one-line Conventional Commit subject and never push. Guarded index operations and explicitly authorized selected-file restoration are supported without ordinary commits. Explicit integrate_branch and update_from_base calls may let Git create a standard merge commit for divergent histories; conclude_merge stages only the formerly unmerged paths of a kept merge and commits them with Git's prepared merge message.
  • Strict tools: tool schemas reject undocumented properties such as stash, discard, owner, repo, or path; baseRef is supported by create_branch, rebase_branch, and create_worktree in new-branch mode; the only force decision is the required boolean on retire_branch, and every tool accepts only its documented fields.
  • PR-ready: create GitHub pull requests from existing local branches after verifying the headBranch matches GitHub and the base is visible. PR fields can stay explicit, or configured autofill can derive omitted fields from the current branch, default branch, and commit subjects.

Security: pi packages run with your full system permissions. BranchMe runs local git commands, may initialize .git metadata in the current directory, may create or remove verified linked-worktree directories outside the active checkout, may integrate local history or retire one exact local branch ref, may make an automatic authenticated GitHub request to find a related open pull request, can update branches and remotes, and can create GitHub pull requests. Repository-configured hooks—including reference-transaction hooks invoked by retirement—merge drivers, filters, and signing policy may run commands or contact networks outside BranchMe's direct argv guarantees. Read SECURITY.md.

Table of Contents


Quick Start

pi install npm:@senad-d/branchme
cd /path/to/your/git/repo
pi

Inside pi:

/branchme
/branchme help

A normal prompt can use the automatic start-of-run snapshot without a tool call. Ask for an explicit refresh when needed, for example:

Refresh the repository state with branch_status, then create a branch named feature/update-docs with create_branch.

When pi starts in a directory that is not already inside a Git repository, explicitly ask it to call init_repository. The tool defaults to an unborn main branch, or accepts an optional initialBranch; it creates no commit, README, .gitignore, remote, or user configuration.

Use list_branches to discover local and cached remote-tracking branches, upstream ahead/behind counts, and worktree occupancy. Optional kind and patterns filter before the 200-entry limit. For fresh discovery of unknown branch names, first run fetch_remote; fetch_branch remains the narrow refresh for a known branch. To join an existing remote branch in the active checkout, use track_branch rather than creating from the wrong HEAD.

For example, replace git fetch origin --prune followed by git branch -r --list 'origin/feat/23' 'origin/feat/23-*' with these sequential calls:

fetch_remote({ remote: "origin", prune: true })
list_branches({ kind: "remote-tracking", patterns: ["origin/feat/23", "origin/feat/23-*"] })

Wait for the fetch to succeed before listing. fetch_remote defaults to origin and does not prune unless prune: true is requested. Pruning removes only stale cached branch refs for that remote, never local branches, tags, other remotes, or branches on the server. It fetches remote heads using an internal refspec, ignoring configured fetch mappings and tag-pruning settings. Dirty and detached checkouts are supported without changing files, HEAD, or upstream configuration. Rewritten remote tips are refreshed too. Conventional same-remote HEAD aliases are preserved; the remote branch literally named HEAD is excluded to prevent alias writes. Unsafe symbolic destinations (including dangling aliases), overlapping configured remote namespaces (such as origin and origin/team), and non-files ref backends such as reftable are refused before fetching.

A typical BranchMe flow is:

  1. Use the automatic snapshot to understand state at the start of the agent run.
  2. Use branch_status when you explicitly need fresh state, especially after a Git mutation in the same run.
  3. Switch to the local base branch with change_branch.
  4. Update it from its configured upstream with pull_branch, which requires a clean worktree and uses fast-forward-only semantics.
  5. Create from the updated HEAD with create_branch.
  6. Make edits and commit outside BranchMe.
  7. While working, use update_from_base({ baseBranch: "main" }) to fetch and merge the remote base into the clean feature branch without rewriting published history or changing its upstream. A conflict is aborted and its paths listed; with keepConflicts: true the conflicted merge stays in progress so a file-editing step can remove the markers, after which conclude_merge({ action: "conclude" }) commits it or conclude_merge({ action: "abort" }) restores the branch.
  8. Push the current branch with push_branch.
  9. After push_branch completes and GitHub can see the branches, create or reuse a matching open pull request with pull_request.
  10. Inspect the PR with pull_request_status({ number: 123 }) and pull_request_checks. When explicitly authorized, use merge_pull_request (or merge on GitHub), then use land_branch from the primary checkout. Supply pullRequestNumber: 123 for squash/rebase merge evidence.

For isolated work, a specialized Git subagent can use the explicit worktree workflow:

  1. Call list_worktrees to inspect the current repository's bounded worktree inventory.
  2. Ask the user to provide or approve an exact absolute destination and call create_worktree with branchMode: "new" or "existing".
  3. Wait for the result and require details.handoff.ready === true.
  4. Have the caller or orchestrator start a separate Pi session or subagent with its working directory set to the returned absolute details.handoff.cwd.
  5. After that session finishes, remove or preserve any staged, unstaged, untracked, or unmerged files, then explicitly call remove_worktree if removal was requested. Ignored residue is protected by default; after preserving anything valuable, explicitly set deleteIgnored: true to delete it with the worktree. Removal retains the local branch.
  6. If the user separately asks to retire that retained branch, obtain its fresh exact HEAD, verify it against an exact local target, and call retire_branch only after worktree removal has completed.

After a pull request merges on the host, use land_branch from the repository root instead of composing fetch/removal/retirement/sync calls. This combined workflow deletes ignored worktree residue and retires the source branch. Standalone remove_worktree always retains the branch and protects ignored residue unless deleteIgnored: true explicitly authorizes its deletion.

BranchMe does not change the active Pi process's cwd, start Pi or other processes, create sessions, or copy .env or other ignored/untracked files. For credentials needed by agents in linked worktrees, prefer process-level environment variables rather than copying repository-root secrets.

To refresh the current branch's configured remote-tracking ref without changing the local branch or working tree, use fetch_branch with no arguments. To refresh another remote-tracking ref — for example origin/main after a pull request merged on GitHub — call fetch_branch with branch (and optional remote, default origin); the targeted fetch never touches local branches, the working tree, or the current branch's upstream configuration. To reconcile the clean current branch by rewriting its local commits, run fetch_branch, wait for it to complete, and then run rebase_branch. The no-argument fetch_branch and rebase_branch require a configured upstream; rebase_branch automatically attempts git rebase --abort if rebasing fails.

BranchMe is tool-based. The slash command is informational only and never initializes repositories, changes or updates branches, creates or removes worktrees, changes cwd, starts processes/sessions, fetches, rebases, pushes, commits, stages, edits files, or opens pull requests.


Installation

Scope Command Notes
Global pi install npm:@senad-d/branchme Loads in every trusted pi project.
Project-local pi install npm:@senad-d/branchme -l Writes to .pi/settings.json in the current project.
One run pi -e npm:@senad-d/branchme Try without changing settings.
Git pi install git:github.com/senad-d/branchme@<tag> Pin a tag or commit.
Local checkout pi --no-extensions -e . Develop or test this repository in isolation.

Once loaded, BranchMe collects automatic Git context before each agent run. This happens for ordinary prompts; there is no /branchme context command and no additional tool to enable it.

Source checkout:

git clone https://github.com/senad-d/branchme.git
cd branchme
npm install --ignore-scripts
npm run validate
pi --no-extensions -e .

Use the checkout globally while developing:

pi install /absolute/path/to/branchme

Repository and GitHub Setup

BranchMe does not bundle Git. Start pi inside an existing repository, or start it in an existing non-repository directory and explicitly request init_repository:

cd /path/to/your/git/repo
git status
git remote get-url origin
pi

For a new repository:

mkdir my-project
cd my-project
pi
# Ask: Initialize this directory with init_repository.

Initialization rejects existing or nested repositories, defaults to an unborn main branch, and creates no commit or project files. Git environment overrides that redirect repository paths or discovery must be unset. Failed or unrecognized repository discovery is not treated as permission to initialize; uncertain postconditions require inspection without automatic cleanup.

For pull requests, the repository must resolve to GitHub from local origin and/or GITHUB_REPOSITORY:

git remote set-url origin git@github.com:OWNER/REPO.git
# or
export GITHUB_REPOSITORY=OWNER/REPO

For automatic related-PR lookup, pull_request_status, pull_request_feedback, PR-aware land_branch, and pull_request, set a token in the process environment before starting pi:

export GITHUB_TOKEN=github_pat_...
# or
export GH_TOKEN=ghp_...
pi

Or copy .env.example to .env in the repository root, fill in one token value, and optionally enable PR field autofill:

cp .env.example .env
$EDITOR .env
# Set BRANCHME_PR_AUTOFILL=true in .env if desired.
pi

fetch_remote, fetch_branch, pull_branch, push_branch, and land_branch run git with terminal prompts disabled, so missing credentials fail at once instead of hanging. Your normal Git credentials are tried first; for https://github.com remotes the same GITHUB_TOKEN/GH_TOKEN (process environment or repository .env) then answers as a credential helper, passed to git through its environment and never on the command line. A token alone is enough. rebase_branch operates on a locally available configured upstream or explicit base and makes no network request itself. When the current branch already has an upstream, BranchMe pushes an explicit HEAD:<upstream-branch-ref> refspec to the configured upstream remote instead of relying on a bare git push. Run pull_request only after push_branch has completed; pull_request preflights the GitHub headBranch and baseBranch before creating the PR and fails with retry guidance if a branch is not visible yet or the GitHub headBranch commit does not match the local branch.


Configuration

BranchMe has no separate project config file. It reads process environment variables and supported keys from a local .env file in the verified git root. Token lookup checks process.env.GITHUB_TOKEN, then process.env.GH_TOKEN; if neither is set, BranchMe checks the matching .env keys. Pull request field autofill checks BRANCHME_PR_AUTOFILL in the process environment first, then .env, and defaults to disabled.

Variable Meaning
GITHUB_TOKEN Preferred token for automatic related-PR lookup, pull_request_status, pull_request_feedback, PR-aware land_branch, and pull_request; process environment first, then local .env fallback.
GH_TOKEN Fallback token for the same GitHub API operations; process environment first, then local .env fallback.
BRANCHME_PR_AUTOFILL=true Allow pull_request to fill omitted PR fields. Accepts true/false, 1/0, yes/no, or on/off; disabled by default.
GITHUB_REPOSITORY=owner/repo Optional CI fallback and boundary check for the current GitHub repository; process environment only.

BranchMe reads only GITHUB_TOKEN, GH_TOKEN, and BRANCHME_PR_AUTOFILL from a small regular .env file; it rejects directories, symlinks, special files, and oversized files. BranchMe does not import other .env keys, read shell profiles, GitHub CLI credentials, or local credential stores. Token values are redacted from automatic context, errors, tool content, and tool details.

With autofill enabled, omitted fields are resolved as follows:

  • headBranch: current local branch.
  • baseBranch: the branch named by origin/HEAD when it exists locally, falling back to an existing local main, master, trunk, or develop branch.
  • title: first commit subject in baseBranch..headBranch, falling back to a title derived from the head branch name.
  • body: a bounded Markdown summary of commit subjects in baseBranch..headBranch.
  • draft: false.

Explicit tool arguments always take precedence. Autofill does not invoke pull_request by itself. Agents may invoke pull_request without separate end-user confirmation when PR creation is appropriate for the active workflow, including directions from user prompts, system or developer instructions, AGENTS.md, skills, automation, or delegated/subagent prompts.

If local origin and GITHUB_REPOSITORY both resolve but disagree, pull_request fails closed.


Commands

Command Description
/branchme Show a compact status panel or fallback message with current branch, GitHub repository resolution, token presence, and workflow notes.
/branchme help Show concise BranchMe workflow help and runtime requirements.
/branchme --help Alias for /branchme help.
/branchme -h Alias for /branchme help.

Commands are informational only. BranchMe actions are performed by agent-callable tools. /branchme uses the TUI panel in TUI mode, notifications in RPC mode, plain text only in print mode, and stays stdout-silent in JSON mode to avoid corrupting protocol output. Unsupported non-empty /branchme arguments return guidance to use /branchme help where the current mode can display it safely.


Tools

Tool Schema Behavior
git_log { "ref"?: string, "excludeRef"?: string, "paths"?: string[], "after"?: string, "worktreePath"?: string } Read 25 locally available commits per page, pinned to captured start/exclusion commits. Reports shallow/unborn history; never fetches.
git_show { "ref"?: string, "output"?: "summary" | "patch" | "file", "path"?: string, "parent"?: integer, "worktreePath"?: string } Inspect a commit or historical blob without checkout; file mode requires a literal path and merge comparisons require an explicit parent. Bounded text, safe binary/oversized summaries.
git_conflicts { "path"?: string, "stage"?: integer, "expectedIndex"?: string, "after"?: string, "worktreePath"?: string } List conflicts or inspect available base/ours/theirs index versions. Missing stages are explicit; never resolves or stages.
list_remotes {} Inspect configured/effective destinations and a configuration fingerprint without network access. Bounded previews with omissions and unsupported settings reported.
add_remote { "name": string, "url": string, "expectedConfig": string } Add an unused non-overlapping remote using credential-free HTTPS/SSH; no fetch, overwrite, URL rewrite, or global config change.
set_upstream { "branchName": string, "upstream"?: string, "unset"?: boolean, "expectedConfig": string } Set one existing remote-tracking upstream or explicitly unset it, preserving refs/files and other branch configuration.
commit_changes { "action"?: "gather" | "commit", "message"?: string, "steeringPrompt"?: string, "paths"?: string[], "confirm"?: boolean, "worktreePath"?: string } gather (default) is read-only and returns bounded commit context for drafting one subject. commit with message commits that validated subject; without message it drafts one with the active model. paths limits staging and committing to exact repo-relative paths. Unsafe files require UI approval or block the commit. Never pushes.
stage_changes { "paths": string[], "expectedHead": string | null, "expectedIndex": string, "worktreePath"?: string } Stage exact approved files with fresh inspection expectations. Preserves working files/unrelated index entries; never commits.
unstage_changes { "paths": string[], "expectedHead": string | null, "expectedIndex": string, "worktreePath"?: string } Return selected index entries to captured HEAD (or unstage unborn additions), preserving working bytes. Never discards edits or commits.
restore_paths { "paths": string[], "expectedHead": string | null, "expectedIndex": string, "preview"?: boolean, "sourceRef"?: string, "discardChanges"?: boolean, "expectedFiles"?: { "path": string, "fingerprint": string }[], "worktreePath"?: string } Preview, then explicitly authorize replacement/deletion of selected tracked working files with fresh per-file expectations. Preserves logical index entries; protects untracked/ignored files.
revert_changes { "commit": string, "expectedHead": string, "expectedIndex": string, "worktreePath"?: string } Stage one non-merge commit's inverse without committing. Requires clean idle state; conflicts attempt verified abort/restoration.
pull_request_checks { "number": integer, "after"?: string } Read all run/status states and available host readiness for a captured head, distinguishing partial/unknown/pending/no-check states from success.
edit_pull_request { "number": integer, "title"?: string, "body"?: string, "draft"?: boolean } Change only explicit metadata fields on an open PR, with verified results and conservative uncertain-outcome handling.
merge_pull_request { "number": integer, "expectedHead": string, "mergeMethod": "merge" | "squash" | "rebase" } Perform an explicitly authorized ready host merge with an expected-head lease; no bypass, automatic cleanup, or remote branch deletion.
git_status { "paths"?: string[], "worktreePath"?: string, "after"?: string } Enumerate staged, unstaged, untracked, renamed/deleted, and conflicted paths with separate index/worktree states. Up to 50 entries / 24 KiB per page; follow next with identical filters/worktree until complete. Literal root-relative filters and verified same-repository worktree targeting; no network or mutation. See below.
git_diff { "mode"?: "unstaged" | "staged" | "range", "baseRef"?: string, "targetRef"?: string, "mergeBase"?: boolean, "output"?: "patch" | "stat" | "name-status", "paths"?: string[], "contextLines"?: integer, "worktreePath"?: string } Read local diffs without staging or changing files/refs. Defaults to unstaged patch output. Range mode requires baseRef, defaults targetRef to HEAD, and optionally compares from the unique merge base. Only range mode accepts ref/merge-base fields. Literal root-relative paths, 0–20 context lines (default 3), same-repository worktree targeting, and bounded redacted display. See below.
list_branches { "kind"?: "local" | "remote-tracking", "patterns"?: string[] } Read up to 200 matching local/cached remote-tracking refs with commits, upstream counts, symbolic refs, and occupancy. Patterns use Git branch-list globs on names (origin/topic for remote refs), match any supplied pattern, and filter before limits. Omit filters for the original full inventory. Accepts 1–25 nonblank patterns of at most 512 characters. No fetch. Raw refs are limited to 128 KiB and text to 4,000 characters; omitted entries are reported. Names/paths are display metadata, not guaranteed executable identities.
fetch_remote { "remote"?: string, "prune"?: boolean } Refresh all heads for one configured remote (default origin) into its remote-tracking namespace. Prune defaults to false; true deletes only that remote's stale cached branch refs. Atomic ref updates, no configured refmap, tags, tag pruning, or submodules. Never changes local refs, checkout, upstream settings, or working-tree files. Files ref backend required for dangling-alias safety.
track_branch { "branchName": string, "remote"?: string, "remoteBranch"?: string, "worktreePath"?: string } Fetch one exact remote branch and create/check out a new local tracking branch. Remote defaults to origin; remoteBranch defaults to branchName. Requires a clean idle checkout; verifies HEAD and upstream.
update_from_base { "baseBranch": string, "remote"?: string, "keepConflicts"?: boolean, "worktreePath"?: string } Fetch the exact remote base (remote defaults to origin) and merge its captured commit into the clean current feature. Uses verified normal-merge policy; a conflict is automatically aborted with its paths listed, or with keepConflicts: true reported as conflict_kept and left in progress (MERGE_HEAD set, conflicted paths unmerged) for conclude_merge. Never rebases, pushes, or changes upstream configuration.
conclude_merge { "action": "conclude" | "abort", "worktreePath"?: string } Finish or abandon the in-progress merge on the current checkout. conclude refuses default/custom-sized <<<<<<<, `
pull_request_files { "number": integer } List a pull request's first 100 changed files (path, change type, additions, deletions; the rest counted) and its current labels. Read-only.
label_pull_request { "number": integer, "labels": string[] } Add 1 to 5 labels that already exist in the repository to a pull request; an unknown label is refused before anything changes, existing labels stay.
reply_pull_request_feedback { "number": integer, "body": string, "threadId"?: string, "resolve"?: boolean } Answer PR feedback on an open, merged or closed PR of this repository (forks included). With threadId (from pull_request_feedback) it replies in that review thread after verifying the thread belongs to the PR, and resolve: true then resolves it; without threadId it posts one conversation comment after confirming the number is a pull request, not an issue. Body is Markdown, at most 4,000 characters. Never edits, deletes, approves, merges, or closes.
pull_request_feedback { "number"?: integer, "headBranch"?: string, "worktreePath"?: string } Read what a same-repository PR asks to be fixed in one GraphQL request: unresolved, current review threads with their comments (resolved and outdated threads are counted only), review summaries with a body or requested changes, the latest 50 conversation comments, and the head commit's failing checks with up to 20 failure and warning annotations each (path:line LEVEL: message). Threads and conversation comments carry their GraphQL ids, every comment its author and time, and comments by the token's own user are marked (you), so a caller can reply to a thread and tell new comments from its own replies. For up to three failing GitHub Actions jobs it adds the log lines leading to the last ##[error]. A job log that cannot be read is reported per check. No mutations.
pull_request_status { "number"?: integer, "headBranch"?: string, "worktreePath"?: string } Read exact same-repository PR lifecycle and head/base/merge identities. Number and headBranch are mutually exclusive. Without number, returns the most recently updated PR for headBranch or the current branch. Does not certify CI checks or review approvals.
init_repository { "initialBranch"?: string } Initializes only pi's exact current working directory as a verified non-bare Git repository with an unborn branch (default main). Rejects existing or nested repositories and accepts no path, bare, template, shared, remote, commit, or project-file controls.
branch_status { "ancestry"?: { "sourceBranch": string, "targetBranch": string }, "worktreePath"?: string } Explicitly refreshes the same bounded context used at agent start. An optional strict ancestry query captures both exact HEADs and reports whether the source commit is an ancestor of the target commit; each endpoint may be an exact local branch or a remote-tracking ref such as origin/main (a local branch of the same name takes precedence). It is read-only and never checks out or resets remote-tracking refs; automatic Git context does not run ancestry queries. worktreePath (an absolute worktree root of the same repository) lets one session act on another worktree; omit it for the current one.
list_worktrees {} Runs a bounded, read-only inventory of the current repository's main and linked worktrees, including path, branch/detached state, HEAD, current/main, locked, prunable, and omitted-entry details. Automatic Git context does not include this inventory.
create_worktree { "worktreePath": string, "branchName": string, "branchMode": "new" | "existing", "baseRef"?: string } Creates and verifies a linked worktree at an explicitly approved absolute path. new creates a local branch from current HEAD, or from the optional read-only baseRef (an exact local branch, remote-tracking ref such as origin/main, or full commit) regardless of the current checkout's branch, dirt, or staleness; existing requires an existing local branch not checked out elsewhere and rejects baseRef. It returns a ready handoff with the exact canonical absolute cwd and local branch identity.
remove_worktree { "worktreePath": string, "deleteIgnored"?: boolean, "discardChanges"?: boolean } Deletes an explicitly selected linked checkout directory but keeps its local branch at the same commit. Uncommitted changes and ignored files are refused by default. deleteIgnored: true permits deleting all ignored entries, including possible .env and .pi/, and reports their top-level paths; discardChanges: true permits discarding uncommitted changes and lists them in discardedPaths. Each destructive option requires explicit user authorization.
retire_branch { "branchName": string, "expectedHead": string, "targetBranch": string, "force": boolean } Deletes only the exact unoccupied local branch ref when its direct ref matches the supplied full commit ID and its relationship to the exact local target has been verified. Unmerged retirement requires explicit force: true; remote and remote-tracking refs are untouched.
land_branch { "sourceBranch": string, "targetBranch": string, "remote"?: string, "worktreePath"?: string, "pullRequestNumber"?: integer } Cleanup after a verified merge, not a PR merge operation: fetch the target, verify ancestry or exact merged-PR evidence, remove a clean linked worktree including ignored files, delete the local source at its expected commit, and independently attempt fast-forward target sync. Never deletes remote branches. Default remote origin; omitted path finds the source's linked worktree. Inspect each receipt because cleanup and sync can have different outcomes.
change_branch { "branchName": string, "worktreePath"?: string } Validates branchName, requires refs/heads/<branchName> to exist locally, rejects staged, unstaged, or unmerged tracked changes (naming the paths), and runs git switch <branchName>. Untracked files such as .env do not block; git itself refuses a switch that would overwrite one.
fetch_branch { "remote"?: string, "branch"?: string, "worktreePath"?: string } With no arguments it requires a current branch with a configured upstream and runs git fetch --no-tags --no-recurse-submodules <upstream-remote> <upstream-branch-ref>:<remote-tracking-ref>. With branch (and optional remote, default origin; remote requires branch) it fetches that exact remote branch into refs/remotes/<remote>/<branch> instead. Either way only that tracking ref is refreshed without changing local branches, working-tree files, or upstream configuration.
pull_branch { "worktreePath"?: string } Requires a clean current branch with a configured upstream and runs git pull --ff-only --no-rebase --no-autostash <upstream-remote> <upstream-branch-ref>; divergence fails without rebasing or creating a merge commit.
rebase_branch { "baseRef"?: string, "worktreePath"?: string } Replays local commits onto cached upstream by default, or an explicitly captured local/remote-tracking/full-commit base without changing upstream. Requires history-rewrite authorization; no fetch/force-push. Explicit-base mode verifies restoration and refuses local-only ignored residue.
integrate_branch { "sourceBranch": string, "targetBranch": string, "keepConflicts"?: boolean, "worktreePath"?: string } Integrates one exact existing local source branch into one distinct existing local target branch. The clean active control worktree must already have the target checked out. It returns already_integrated, fast_forward, merge_commit, or a conflict only after automatic abort and verified restoration; with keepConflicts: true a conflict is reported as conflict_kept and left in progress (MERGE_HEAD set, conflicted paths unmerged) for conclude_merge. It never fetches or pushes. worktreePath (an absolute worktree root of the same repository) lets one session act on another worktree; omit it for the current one.
create_branch { "branchName": string, "baseRef"?: string, "worktreePath"?: string } Creates at current HEAD by default or an explicitly captured local/remote-tracking/full-commit base. Explicit bases require tracked-clean idle state; no fetch or inferred upstream.
push_branch { "worktreePath"?: string } Pushes the current branch to its configured upstream remote with an explicit HEAD:<upstream-branch-ref> refspec, or publishes it with git push --set-upstream origin <currentBranch> when no upstream exists. A branch whose upstream is a differently named main, master, trunk, develop, or origin default branch is published to its own name instead and tracks it.
pull_request { "headBranch"?: string, "baseBranch"?: string, "title"?: string, "body"?: string, "draft"?: boolean, "worktreePath"?: string } Preflights GitHub branch visibility and verifies the GitHub headBranch commit matches the local branch, then reuses an exact matching open PR or creates one in the resolved current repository. Existing PR metadata is preserved. Omitted fields require BRANCHME_PR_AUTOFILL=true; branch refs must be distinct, exist locally, and cannot use owner:branch.

conclude_merge supports only single-head merges and refuses multi-head merges before mutation. Its marker check also covers manually staged resolutions and clean-filter output. Marker-like lines (including Markdown setext underlines) are conservatively refused; configured marker sizes above 4,096 are refused. Git commits the entire index, including already-staged merge entries: do not stage unrelated edits during the merge. Unrelated unstaged edits remain outside the commit. A failed/lost commit response reports uncertain postconditions; inspect HEAD, MERGE_HEAD, and the index before retrying.

All schemas reject additional properties. fetch_remote accepts only optional remote and prune, never arbitrary refspecs or tag/force/checkout controls. list_branches accepts only optional kind and bounded patterns; it remains read-only. init_repository accepts only optional initialBranch, never a path or repository-mode controls. change_branch never accepts baseRef, force, stash, discard, create, owner, repo, or path inputs. fetch_branch accepts only the optional remote and branch pair for a targeted remote-tracking refresh (remote requires branch) and never accepts refspec, tags, prune, or force controls. pull_branch accepts only an optional worktree selector; rebase_branch additionally accepts an explicit read-only baseRef. Neither accepts force, refspec, autostash, or interactive rebase controls. integrate_branch requires exactly sourceBranch and targetBranch plus optional boolean keepConflicts; it accepts no repository, path, remote, strategy, message, squash, signing, commit, continuation, abort, force, fetch, push, deletion, or worktree controls. conclude_merge requires exactly action (conclude or abort) and accepts no path list, commit message, strategy, --ours/--theirs, or force controls. create_worktree requires exactly worktreePath, branchName, and branchMode, plus an optional read-only baseRef start point for branchMode: "new"; remove_worktree requires worktreePath and accepts optional boolean deleteIgnored. No worktree tool accepts force, move, prune, repair, lock, unlock, detached, orphan, remote, or refspec controls. retire_branch requires exactly branchName, a full 40- or 64-hex-character expectedHead, a distinct local targetBranch, and the boolean force decision; it accepts no repository, path, remote, refspec, pattern, branch list, prune, remote-delete, worktree-removal, or inferred-target control. pull_request never accepts owner, repo, or owner-prefixed branch refs; BranchMe resolves the repository from local origin and/or matching GITHUB_REPOSITORY. Generic continue_merge and abort_merge tools are not available; conclude_merge is the only merge-continuation surface and only commits paths whose markers are already gone.


See shell-free workflows for history/conflict inspection, cross-worktree coordination, remote/tracking setup, index control, guarded restore/revert, and PR checks/edit/merge. Ordinary commits are never created automatically; only an explicit commit_changes call commits.

worktreePath selects a verified live checkout of the same repository without changing Pi's cwd. Branch-inferred PR status/feedback reject a selector with an exact number. Mutating tool calls share an outer process-wide coordinator before helper root/common-directory queues; external Git processes/extensions are not locked out.

Complete file status without Bash

git_status({})
git_status({ after: "<next from the previous page>" }) // repeat filters/worktreePath if supplied
git_status({ paths: ["src", "README.md"] })

git_status is separate from the bounded branch_status/automatic summary and from content-reading git_diff. It lists all Git-reported changed paths across pages, including staged-only paths. total counts matching records; hasChanges describes the whole checkout within scope. An empty filtered result is not a cleanliness claim. Ignored files and submodule working-tree contents are excluded; recorded gitlink changes remain visible. Git's assume-unchanged/skip-worktree policies still apply.

Continuation is bound to the canonical checkout, normalized filters, HEAD, logical index entries/flags, operation state, status records, and available changed-path filesystem metadata. A stale cursor requires restarting without after; a checkout changing during inspection fails instead of returning a misleading page. This is observation, not a lock against external Git or file writers. Each Git response is capped at 4 MiB; exceeding that limit fails rather than claiming partial status is complete.

Results include head, a SHA-256 logical index expectation, and snapshot. Explicit paths additionally request content/mode fingerprints for reusable returned files (up to 8 MiB each; no content is returned); missing files and symlinks have distinct fingerprints, while unsupported/oversized objects report no fingerprint. These identities describe observed state for guarded operations; they do not authorize mutations. Redacted, abbreviated, control-containing, invalid-encoding, or symlink-ancestor paths have reusablePath: false and must never be copied into mutation calls. New Git support for --no-lazy-fetch is required. Automatic context is unchanged.

Workflow and Boundaries

Choosing between similar tools

Tool names and parameter contracts remain stable. Short descriptions state the operation and important effects without repeating tool names or schema details. Schemas explain arguments; optional guidelines cover essential safety and sequencing. Brief prompt snippets omit the name prefix Pi adds automatically. Standalone guidelines name their tool because Pi renders them ungrouped.

Intent Choose Important distinction
Enumerate every changed path git_status Follow next until complete; summary context omits paths.
Show changed file contents git_diff Status metadata is not a diff; untracked file contents are excluded.
Refresh observed repository status or check ancestry branch_status Reads local/cached Git state and related PR metadata; never fetches Git refs. Ancestry checks may be independent.
Find branch names list_branches Cached inventory; run fetch_remote first when fresh remote discovery is needed.
Fetch one known remote branch fetch_branch Updates its tracking ref, not the local branch or checkout.
Fetch all remote branch names and heads fetch_remote Refreshes one remote's tracking namespace; pruning is opt-in and local-cache-only.
Switch to an existing local branch change_branch Does not create a branch or fetch one.
Create and switch to a new branch create_branch Defaults to current HEAD; optional explicit base never fetches or moves that base.
Join an existing remote branch track_branch Fetches, creates a new local tracking branch, and switches; cannot configure an existing local branch.
Update the current branch from its own upstream pull_branch Fetches and fast-forwards only; divergence fails.
Merge a fresh remote main into the current feature update_from_base Fetches and merges; may create a merge commit, never rebases or changes upstream configuration.
Merge a local source into a local target integrate_branch Target must already be checked out; never fetches.
Rebase onto upstream or a chosen local base rebase_branch Rewrites local commits; no fetch or force-push, and does not change upstream.
Finish or abandon an existing merge conclude_merge Resolve conflicts by editing first to conclude; commits the entire index or aborts the merge.
Push existing commits on the current branch push_branch Accepts an optional worktree selector, not a caller-supplied push target; never commits.
Create or reuse a matching GitHub PR pull_request Push first; does not push, merge, or edit an existing PR.
Verify a PR was merged pull_request_status Lifecycle evidence, not proof that CI or review requirements passed.
Read review requests and failing CI pull_request_feedback Read-only; use reply_pull_request_feedback to post a reply or comment.
Clean up after a verified merge land_branch Deletes the local source and clean linked worktree, including ignored files; does not merge the PR.
Remove only a linked checkout remove_worktree Retains its branch; destructive options require explicit authorization.
Delete only a local branch retire_branch Requires an unoccupied branch and expected commit; does not remove its checkout or remote branch.

Wait for dependent calls to finish: fetch before inspecting fresh remote refs, switch before pulling, push before creating a PR, and integrate before checking its result. Independent ancestry checks do not require a preceding merge. A kept conflict must be resolved and concluded or aborted before another Git mutation.

Inspect local diffs

git_diff({}) // unstaged: index -> working tree
git_diff({ mode: "staged", paths: ["src", "README.md"] }) // HEAD -> index
git_diff({ mode: "range", baseRef: "main", targetRef: "HEAD" }) // main -> HEAD
git_diff({ mode: "range", baseRef: "origin/main", mergeBase: true, output: "stat" }) // origin/main...HEAD
git_diff({ output: "name-status", worktreePath: "/absolute/path/to/linked-worktree" })

git_diff requires Git with --no-lazy-fetch support and fails closed on older Git. It never stages, commits, fetches, writes files/refs, or saves a diff artifact. External diff helpers, text conversion, fsmonitor, optional index locks, and lazy object fetching are disabled. Normal Git clean filters remain a separate trust boundary (see SECURITY.md). No diff contents are added to automatic context.

Range refs are bounded single commit revisions (branches, tags, full IDs, or revisions such as HEAD~1), resolved to captured commit IDs before comparison. Supply mergeBase: true for a three-dot-style comparison; missing or multiple merge bases fail rather than choosing an ambiguous result. Working-tree/index changes are excluded from range mode. Staged mode also works before the first commit.

paths accepts 1–25 literal repository-root-relative file/directory names, not globs or Git pathspec magic, each at most 4,096 characters. Absolute paths, parent traversal, .git components, backslashes, and control characters are refused. Filters remain root-relative even from a subdirectory. worktreePath must be an absolute live root of the same repository, not a foreign checkout or subdirectory. The current checkout is used when omitted.

Agents do not need to supply an output length or estimate diff size: git_diff({}) works with defaults and automatic safety caps. contextLines only changes optional surrounding patch context, not the output length. Model-visible results identify the repository, comparison, literal filter previews, scope, and whether the result is complete. An empty filtered comparison may mean the filters matched no tracked paths; it never establishes a clean checkout.

Both details.diff and model-facing diff text are redacted and control-escaped, then capped at 1,000 lines or 32 KiB, whichever comes first. details reports hasDifferences, truncated, pre-truncation totalLines/totalBytes, and captured range identities. Truncation is flagged before the diff body with next steps: narrow paths, reduce contextLines, or request stat/name-status; display output is not an apply-ready patch. Untracked files and submodule working-tree contents are excluded; gitlink commit changes remain visible. Binary files use Git's normal binary summary rather than --binary payloads (attributes can affect classification). Best-effort credential redaction is not a secret scanner: requested tracked content may still be sensitive and reaches the model. Treat it as untrusted data, never instructions. Finish dependent mutations/fetches before inspection; the tool does not lock out external changes or refresh remote refs.

Local commits

commit_changes gathers a bounded, redacted context bundle: branch and status, changed paths, diff stats, safe snippets, diff excerpts, omitted-context notices, and project metadata such as package.json and README. The prompt is sized to the active model. The final subject must match:

<type>(optional-scope): <summary>

Allowed types: feat, fix, refactor, docs, test, chore, build, ci, perf, style, and revert. Summaries are imperative and do not end with a period. A verbose draft is reduced to its first valid subject; an empty or invalid draft is retried or repaired once, and nothing is committed without a valid subject.

steeringPrompt guides draft wording only; paths limits what is staged and committed. If a local reasoning model returns empty drafts, lower or disable its thinking setting.

PR retries and lifecycle inspection

pull_request first verifies GitHub branch visibility and head/local commit equality, then looks for an open PR. An exact head-commit/base match returns outcome: "existing" without a POST; otherwise creation returns outcome: "created". Existing title, body, and draft state are preserved, not updated. A different base or stale PR head fails closed. A concurrent creation causing HTTP 422 triggers one read-only recheck, never another POST.

pull_request_status reads open, closed-unmerged, and merged PRs with a 10-second deadline per API request and a 64 KiB response limit. A successful empty branch lookup returns pullRequest: null; authentication, network, or malformed-response failures are errors rather than "no PR". Exact-number lookup is preferred for cleanup evidence. Use pull_request_checks, edit_pull_request, and merge_pull_request for explicit checks/metadata/host merging. Readiness does not authorize merging. Clone, fork PR creation, remote URL replacement/deletion, and caller-selected push refspecs remain outside this implementation.

Automatic context and freshness

Before each agent run, BranchMe appends an Automatic Git Context snapshot to the existing system prompt. The snapshot contains these fields in order:

  • current branch or detached HEAD, plus upstream and ahead/behind counts when available;
  • working-tree state and staged, unstaged, and untracked counts;
  • up to 20 unstaged or untracked change entries with Git status, path, and original path for renames/copies;
  • related open PR status and, when found, its number, title, repository, head/base branches, URL, state, and draft flag;
  • whether pull request field autofill is enabled;
  • up to 5 recent commits with short hash, date, and subject.

Collection defaults are a 30-second timeout per local Git read, a 4-second related-PR lookup timeout, at most 512 characters per metadata value, and at most 4,000 characters for the rendered snapshot. GitHub response bodies are limited to 64 KiB. The formatter can further shorten values or omit entries to stay within the total limit.

The snapshot is fresh at agent start but is not live. A fetch, branch switch, pull, rebase, integration, commit, file change, push, or other mutation later in the same run can make it stale. branch_status performs an explicit current-state refresh through the same shared collector and remains read-only; it does not mutate files, Git state, or GitHub state. Its optional ancestry object requires exact sourceBranch and targetBranch fields together, captures both commit IDs, and reports isAncestor. Each endpoint may be an exact local branch or a remote-tracking ref such as origin/main (local branches take precedence), so a fetched origin/main can serve as a read-only containment proof after a pull request merges. This targeted proof is explicit only: automatic Git context remains unchanged. Run it after integrate_branch or fetch_branch completes, never in the same parallel tool batch.

Related-PR metadata does not come from Git alone. When repository, branch, and credentials resolve, automatic collection and explicit branch_status may make an authenticated GET /repos/{owner}/{repo}/pulls?state=open&head={owner}:{branch}&per_page=1 request. Without a token there is no unauthenticated fallback or GitHub request; the PR field is reported as unavailable while local Git context remains usable.

Repository paths, commit subjects, branch names, and PR titles are treated as untrusted metadata. Automatic context redacts token values, escapes control characters, quotes and bounds values before prompt insertion, and never captures diffs or file contents. Explicit git_diff is the separate opt-in content inspection surface. Staged files contribute only to the staged count; the path list is limited to unstaged and untracked entries.

Use BranchMe from pi prompts or automation that drives pi with explicit tool calls:

Use branch_status.
Switch to the base branch with change_branch, then use pull_branch after the switch completes.
Fetch the current branch upstream with fetch_branch, wait for it to complete, then rebase with rebase_branch.
Create branch feature/docs-refresh from the updated current HEAD with create_branch.
Push the current branch with push_branch.
After push_branch completes, create a draft pull request from feature/docs-refresh to main titled "Refresh docs" with this body: "...".
If pull request field autofill is enabled, after push_branch completes create a pull request and fill any details I did not provide.

Verified local branch integration

integrate_branch requires explicit intent and exactly two distinct existing local branch names: sourceBranch and targetBranch. The active Pi worktree is the control worktree; it must be clean, have no merge/rebase/cherry-pick/revert/sequencer operation in progress, and already have targetBranch checked out. BranchMe never switches to the target or infers one. A source checked out in another dirty linked worktree is allowed because integration reads only its committed local ref. Remote-only refs, commit IDs, paths, repositories, remotes, and owner-prefixed refs are rejected. Optional worktreePath makes another worktree of the same repository the control worktree: it must be the absolute root of the primary checkout or a registered linked worktree with the same Git common directory; relative paths, subdirectories, missing, bare, and foreign paths are refused before integration starts.

For a source not already reachable from the target, BranchMe runs this fixed normal-merge policy from the verified worktree root:

git -c rerere.enabled=false merge --ff --no-edit --no-autostash --no-rerere-autoupdate --no-overwrite-ignore refs/heads/<sourceBranch>

This policy permits a fast-forward or lets Git create a standard two-parent merge commit for divergent histories. Integration does not create user-authored commits, accept a merge message, or expose strategy, squash, unrelated-history, signing, force, or commit controls. Before mutation, it rejects a non-empty branch.<targetBranch>.mergeOptions setting because those branch-specific defaults could silently change the fixed policy. Autostash is disabled, rerere is disabled so recorded resolutions are not applied or updated, and ignored files may not be overwritten. Repository-configured hooks, custom merge drivers, clean/smudge filters, and signing policy remain active; those Git extension points may execute arbitrary local commands or network operations under the user's identity.

The structured status is one of:

  • already_integrated: no merge ran and both branch refs remained stable;
  • fast_forward: the target advanced exactly to the captured source commit;
  • merge_commit: Git created an exact normal two-parent merge whose parents are the captured prior target and source;
  • conflict: bounded, exact repository-relative conflict paths were captured, git merge --abort succeeded, and repository identity, current target, exact source/target refs, clean state, and absence of operation state were verified;
  • conflict_kept (only with keepConflicts: true): the conflict paths were captured and the merge was left in progress with MERGE_HEAD at the captured source commit, HEAD unchanged, and no other operation active; conclude_merge commits it after the markers are removed or aborts it.

Successful and automatically aborted outcomes include captured before/after source and target commit IDs plus final source and prior-target ancestry proof. conflict_kept instead includes the captured source and target commit IDs and in-progress merge facts. Failed non-conflict merges remain errors after cleanup. BranchMe uses no reset-based rollback; if abort or postcondition checks are inconclusive, or a ref moves unexpectedly, it reports that integration may have completed and tells the caller to inspect the repository before retrying.

The same-repository mutation queue covers preflight, merge, cleanup, and verification, but it exists only inside the current BranchMe process. It does not lock another Pi session or an external Git process. integrate_branch itself does not fetch, pull, push, delete branches, remove worktrees, start agents, ask questions, continue a merge, or resolve semantic conflicts. A Mission or other orchestrator may explicitly call it; after a verified conflict, that separate workflow decides whether to delegate analysis to an integration agent or ask the developer about semantic intent.

For an independent read-only proof after integration, call:

{ "ancestry": { "sourceBranch": "feature/example", "targetBranch": "main" } }

with branch_status only after integrate_branch has returned. Do not batch the calls.

Linked worktree verification and handoff

create_worktree requires a non-blank absolute path with no control characters. Missing parent directories are allowed: BranchMe resolves the nearest existing directory ancestor to build a canonical destination, rejecting non-directory or dangling-symlink ancestors. Git creates missing directories only after all preflight checks pass. BranchMe rejects any existing file, directory, or symlink there, and rejects destinations inside a registered worktree or the repository's common Git directory. Before mutation, the canonical path and local branch must be returnable without redaction, escaping, Unicode alteration, or truncation: canonical paths are limited to 4,096 characters and branch identities to 512 characters, and credential-like token text is rejected. A dirty source worktree is allowed because creation does not switch or overwrite it.

For branchMode: "new", BranchMe creates the requested local branch from the current HEAD, or — when the optional baseRef is supplied — from that exact existing local branch, remote-tracking ref (for example origin/main), or full commit. baseRef is resolved to a commit before mutation and is only read: it is never checked out, reset, or given upstream configuration, and the current checkout's branch, dirt, or staleness never blocks it. For branchMode: "existing", it uses only an existing local branch that is not checked out in another worktree and rejects baseRef; it never infers a local branch from a remote. After Git succeeds, BranchMe re-lists worktrees and verifies the canonical path, local branch, HEAD, and clean checkout. A representative structured result subset is:

{
  "action": "create_worktree",
  "handoff": {
    "cwd": "/absolute/path/to/branchme-feature",
    "branch": "feature/worktree-docs",
    "head": "<full-commit-id>",
    "ready": true,
    "summary": "Worktree ready at /absolute/path/to/branchme-feature on branch feature/worktree-docs at <full-commit-id>."
  }
}

The full details also distinguish requested input from verified before/after state. Successful handoff.cwd and handoff.branch values are the exact identities verified against Git; BranchMe never substitutes [REDACTED], escaped control sequences, or a truncation ellipsis in these machine-readable fields. Display content, summaries, and worktree inventory remain sanitized and bounded separately. An orchestrator may use handoff.cwd only after ready is true, and must start the next Pi session or subagent itself with that exact working directory. BranchMe never changes the active process's cwd or starts another process/session.

remove_worktree canonicalizes the approved absolute path and requires an exact match in a fresh inventory for the current repository. It accepts only a present, unlocked, non-prunable, non-bare, branch-attached linked worktree that is neither main nor current, then rejects staged, unstaged, untracked, or unmerged entries. Ignored entries are refused by default without disclosing their paths. Optional deleteIgnored: true explicitly authorizes deleting all ignored files and directories with the worktree; preserve .env, .pi/, dependency caches, build output, or other valuable residue first:

{
  "worktreePath": "/absolute/path/to/branchme-feature",
  "deleteIgnored": true
}

The bounded receipt reports only redacted top-level ignored paths in deletedIgnoredPaths, never contents. The canonical path and retained branch must pass the same pre-mutation lossless-identity checks used for creation. Removal uses git worktree remove <verified-path> without force, verifies the entry is gone, and returns the exact retained branch identity after confirming it still points to the captured commit:

{
  "action": "remove_worktree",
  "deletedIgnoredPaths": [],
  "handoff": {
    "cwd": null,
    "branch": "feature/worktree-docs",
    "head": "<full-commit-id>",
    "ready": false,
    "summary": "Worktree directory /absolute/path/to/branchme-feature was removed; local branch feature/worktree-docs was retained at <full-commit-id>."
  }
}

Ignored and untracked files—including a repository-root .env—are not copied into a new linked worktree. Prefer credentials inherited through the new agent process environment. Git documents support for multiple worktrees of a superproject containing submodules as incomplete; BranchMe adds no force-based submodule cleanup. There is no force, move, prune, repair, lock, unlock, detached, orphan, or remote-inference worktree behavior.

Post-merge cleanup in one call

After the pull request merged on the host, run from the repository root:

{
  "sourceBranch": "docs/74-add-canonical-glossary-context",
  "targetBranch": "main",
  "remote": "origin",
  "worktreePath": "/absolute/path/to/issue-74-worktree"
}

remote defaults to origin. Omit worktreePath to find the linked worktree holding sourceBranch, if any. An explicit path can also select a worktree incorrectly parked on targetBranch; unrelated branches, primary/root checkouts, detached, locked, dirty, or foreign worktrees are not removed. Invocation from inside the removal directory (including a nested or symlinked cwd) is refused: run from the repository root. Other caller directories in the same repository are supported; every Git command has an explicit -C directory, and cleanup runs through the primary checkout. When the merged source branch is checked out in the primary checkout and its tracked files are clean, land_branch switches that checkout to the target branch first (checkout: switched) so the source can be retired; untracked files stay in place.

The tool fetches the exact remote target and by default requires the captured source tip to be its ancestor before cleanup. For squash/rebase merges, supply pullRequestNumber. BranchMe verifies a closed, merged PR in the same GitHub repository as the configured landing remote, exact head/base branch names, a source SHA equal to the PR head or one of its ancestors (a local branch left behind holds nothing the PR lacks; one with commits the PR head does not contain is refused), and containment of the reported merge commit in the fetched target. Supplying a PR number requires valid evidence even when ordinary ancestry succeeds. There is no general force escape hatch. This host-merge proof authorizes retiring the original rewritten source history; those original commits may eventually become unreachable. An existing selected source checkout must be at the proven PR head or at that contained source SHA; a checkout parked on the target still needs target ancestry. Tracked/staged changes and non-ignored untracked files block removal. Ignored .env, .pi/, node_modules/, dist/, and other ignored residue are deleted with the worktree; preserve anything needed first. Only their top-level path entries, never contents, appear in deletedIgnoredPaths. Source retirement uses the captured expected-HEAD lease and the remote-tracking target, not local HEAD or a stale local target.

Target sync is last, even when worktree removal or branch retirement is refused:

Mode Behavior
fetch-refspec Target is not checked out anywhere: non-forced fetch <remote> refs/heads/<target>:refs/heads/<target>. Git enforces fast-forward-only updates.
pull-ff Target is checked out clean: explicit pull --ff-only --no-rebase --no-autostash <remote> refs/heads/<target> in that worktree.
skipped-dirty Target checkout has tracked changes: leave it untouched and report its path and ahead/behind counts against the captured remote target.
noop Local target already equals the captured remote head; no sync mutation.
not-run / failed Initial safety/ancestry refusal or sync failure; a successful fast-forward is never inferred from Git's prose.

The structured result contains repositoryRoot, remote, targetBranch, remoteTargetHead, sourceBranch, sourceHead, ancestry.isAncestor, mergeProof, pullRequest, worktree, branch, targetSync, and ordered steps, plus a one-line summary. Worktree outcomes are removed/absent/refused; branch outcomes are deleted/absent/refused. Full target before/after IDs are read from the exact local ref in this call. Unknown/not-applicable identities and ancestry are null, not invented proofs. A second successful call reports absent cleanup and noop sync. An already-missing directory is reported absent without pruning a remaining registration; such occupancy can still block branch deletion.

land_branch never stashes, resets, checks out/switches branches, force-updates, pushes, prunes, or deletes remote/tracking refs. It holds one process-local primary-root mutation queue; it cannot lock other Pi/external Git processes. Inspect each receipt outcome before declaring landing complete. Standalone remove_worktree and retire_branch retain their original contracts.

Verified local branch retirement

retire_branch is a separate, explicit lifecycle step after integration and any linked-worktree removal. It accepts exactly:

{
  "branchName": "feature/example",
  "expectedHead": "<full-40-or-64-hex-commit-id>",
  "targetBranch": "main",
  "force": false
}

Obtain or supply a fresh exact HEAD for branchName and verify its ancestry against the exact local targetBranch; branch_status.ancestry can provide both captured commit IDs and the read-only proof. Run that refresh and retire_branch sequentially, never in the same parallel tool batch. The retiring and target names must be distinct direct local refs. Any registered worktree that names the retiring branch blocks deletion, including main, current, linked, locked, prunable, or missing records. Removing an occupying linked worktree is a separate explicit remove_worktree call; wait for its verified result before retirement.

Merged retirement uses force: false. If the retiring commit is not an ancestor of the captured target, retirement fails unless the user explicitly authorizes unmerged data loss with force: true. Forced unmerged retirement can remove that commit's last local branch reference. Object recovery is not guaranteed: normal reflog expiry and Git garbage collection can eventually make the commit unreachable.

After validating the expected commit, complete worktree inventory, target ancestry, and repository identity, BranchMe deletes only the exact local ref with an expected-old-value lease:

git update-ref --no-deref -d refs/heads/<branchName> <capturedRetiringHead>

The captured commit lease prevents deletion if the retiring ref moves after preflight. BranchMe does not use git branch -d or git branch -D because those commands do not provide this expected-HEAD lease and their default merge target does not satisfy the explicit targetBranch contract. Retirement never performs bulk or pattern deletion, infers a target, removes a worktree, fetches, pushes, deletes a remote branch or local remote-tracking ref, or recreates/resets a ref as rollback. Local branch.<branchName>.* configuration remains untouched in the initial contract; it can affect a later branch recreated with the same name.

The retirement preflight, immediate reinspection, leased deletion, and postcondition verification share a process-local mutation-queue window keyed by the canonical active worktree root. That queue coordinates BranchMe mutations invoked from the same active checkout, but it does not lock another active worktree, Pi process, or external Git process. The expected-old-value lease protects the retiring ref; target and worktree checks detect other observable movement. After deletion is attempted, BranchMe verifies repository identity, target stability, local-ref absence, and zero occupancy without using a cancelled caller signal. Contradictory or inconclusive outcomes produce a bounded uncertain error stating retirement may have completed; inspect the repository, target ref, retiring ref, and complete worktree inventory manually before retrying. BranchMe does not automatically recreate, reset, switch, merge, or force-update a branch as rollback.

Repository-configured reference-transaction hooks can run during git update-ref. Such hooks are inside the repository trust boundary and can execute arbitrary commands or contact networks; that behavior is distinct from BranchMe's direct local-only, no-remote-delete argv contract.

BranchMe operates only on the repository where pi is running:

  • Automatic collection and branch_status run bounded, read-only Git commands from the verified git root.
  • Git commands use pi.exec("git", args, { cwd, signal, timeout }) with argv arrays; repository mutations run from the verified git root. A command that fails because another git process holds a lock (index.lock, HEAD.lock, a ref lock) is retried every 250 ms for up to 10 seconds, then fails with git's error plus a note that the lock was still held; BranchMe never removes a lock file.
  • change_branch switches only to existing local branches and has no force, stash, discard, remote, or path input.
  • fetch_branch uses an explicit source-to-remote-tracking refspec with tags and submodule recursion disabled and does not change local branches or working-tree files. Without arguments it requires a configured upstream; with an explicit branch (and optional configured remote, default origin) it refreshes only refs/remotes/<remote>/<branch> and never touches upstream configuration.
  • pull_branch requires a clean worktree and configured upstream, updates only the current branch with git pull --ff-only --no-rebase --no-autostash, and has no branch, remote, force, or rebase input.
  • rebase_branch requires clean state and an upstream or explicit base, disables autostash/multi-ref updates, and attempts abort on failure. Explicit-base mode verifies restoration and leaves publication outside BranchMe's force-push-free surface.
  • integrate_branch merges one captured local source ref into the already-current clean local target with the fixed policy documented above. It verifies repository identity, refs, ancestry, clean state, and cleanup without fetching or pushing.
  • create_branch defaults to current HEAD and supports an explicit captured read-only base; it never fetches, resets that base, or infers tracking.
  • list_worktrees is an explicit, read-only repository inventory; it is intentionally absent from automatic active-worktree context.
  • create_worktree may create a linked checkout outside the active checkout only after canonical path and current-repository boundary checks; its optional baseRef is resolved read-only to a commit and never checked out or reset, and dependent worktree calls must wait for its verified handoff.
  • remove_worktree passes Git only a freshly verified canonical linked-worktree path, never uses force, and retains the branch.
  • retire_branch deletes only one exact unoccupied direct local ref with the supplied expected-HEAD lease after captured target ancestry verification; it leaves remote refs, remote-tracking refs, worktrees, and branch configuration untouched.
  • push_branch pushes only the current branch, uses no bare upstream git push, and has no branchName input.
  • pull_request creates PRs only for the resolved current GitHub repository, requires resolved headBranch and baseBranch values to be distinct and exist locally, requires the GitHub headBranch commit to match the local branch, queues behind in-flight same-repository git mutation windows when possible, and rejects owner:branch head refs. Missing PR fields fail unless BRANCHME_PR_AUTOFILL=true.
  • If local origin and GITHUB_REPOSITORY both resolve but disagree, pull_request fails closed.

Outside commit_changes, BranchMe intentionally does not create user-authored commits or accept or generate commit messages. It never forces checkout, stash, force-push, reset, copy ignored/untracked files between worktrees, or delete branches during standalone remove_worktree. Explicit stage_changes/unstage_changes manage approved index entries; restore_paths requires a preview, fresh file expectations, and explicit discard authorization. revert_changes stages an inverse without committing; conclude_merge stages only formerly unmerged paths. Rebase-driven rewriting occurs only through explicit rebase_branch; a Git-generated standard merge commit is possible through explicit integrate_branch or update_from_base for divergent histories; one exact local ref can be deleted only through explicit retire_branch or merged-only land_branch under the leased boundaries above.


GitHub Actions

GitHub Actions example:

name: branchme-smoke
on:
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  branchme:
    runs-on: ubuntu-latest
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      GITHUB_REPOSITORY: ${{ github.repository }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm install -g --ignore-scripts @earendil-works/pi-coding-agent
      - run: pi --no-extensions -e npm:@senad-d/branchme --help

Ensure the token and Git credentials have permission for the branch and pull request operation you ask BranchMe to perform.


Troubleshooting

Problem Try
Not a git repository Start pi from inside a git checkout, or explicitly request init_repository for a new project directory.
Detached HEAD Use change_branch to switch to an existing local branch, or checkout a branch before fetch_branch, pull_branch, rebase_branch, create_branch, or push_branch.
Branch already exists Choose a new local branch name for create_branch, or use change_branch to switch to it.
Branch does not exist locally Use track_branch for an existing remote branch; change_branch and create_worktree existing mode still require local branches.
Worktree destination rejected Provide an exact absolute path with a resolvable directory ancestor (missing parents are allowed); the destination must not exist or be inside another registered worktree or the repository's common Git directory. Its canonical path and branch identity must also fit the documented limits without credential-like token text or characters that require escaping.
Existing worktree branch is occupied Choose another existing local branch or remove its other linked checkout after cleaning it; BranchMe does not force multiple checkouts.
Worktree removal rejected Use list_worktrees and select a non-main/non-current linked worktree. Remove or preserve staged, unstaged, untracked, and unmerged files. Preserve valuable ignored files, then retry with deleteIgnored: true if deleting the remaining ignored residue is intended. Locked, detached, prunable/missing, bare, and foreign paths are not removable.
Linked-worktree agent cannot find credentials Pass credentials through the process environment. BranchMe does not copy repository-root .env or other ignored/untracked files.
Dirty worktree before branch switch, pull, rebase, or integration Commit through the owning workflow, preserve edits, or explicitly authorize guarded restore_paths before using change_branch, pull_branch, rebase_branch, or a target control worktree for integrate_branch.
Fetch, pull, or rebase has no upstream Use list_remotes and set_upstream for an existing tracking ref, or publish with push_branch. An explicit rebase base needs no upstream.
Unknown remote issue branch or stale branch listing Run fetch_remote (optionally prune: true), wait for success, then list_branches with kind: "remote-tracking" and issue-name patterns. For reftable or unsafe symbolic destinations, inspect repository refs outside BranchMe.
Pull is not a fast-forward Run fetch_branch, wait for it to complete, then explicitly run rebase_branch if rewriting local commits is intended; otherwise reconcile outside BranchMe.
Rebase fails or conflicts rebase_branch automatically attempts git rebase --abort. Inspect repository state before continuing if automatic cleanup also fails.
Integration target mismatch or missing local branch Check out the exact local targetBranch in the active control worktree and ensure both distinct branch refs already exist locally; integrate_branch never switches, fetches, or accepts remote-only refs.
Integration rejects branch-specific merge options Clear branch.<targetBranch>.mergeOptions outside BranchMe before retrying; options such as --no-commit, --squash, --no-verify, or a custom strategy would change the fixed policy.
Integration reports conflict The initial merge was automatically aborted and exact restoration was verified; the conflict paths are listed in the result text. BranchMe has no generic continue_merge tool and does not resolve semantic conflicts.
integrate_branch or update_from_base reports conflict_kept The merge is in progress with MERGE_HEAD set and the listed paths unmerged. Have a file-editing step remove every conflict marker, then run conclude_merge({ action: "conclude" }); run conclude_merge({ action: "abort" }) to restore the branch instead. Other BranchMe mutations refuse while the merge is in progress.
conclude_merge refuses with marker paths An unmerged working-tree file or staged blob contains a default/custom-sized conflict marker line. Remove the markers and restage any manually staged resolution; marker-like Markdown underlines also count. Retry or abort.
Integration is uncertain or cleanup fails Inspect branch refs, HEAD, worktree status, and Git operation state manually before retrying. BranchMe does not use reset-based rollback.
Retirement expected HEAD is stale Refresh the exact local branch commit and target ancestry, confirm the intended commit, then make a new sequential retire_branch call. Never reuse a stale commit identity.
Retirement branch is occupied Remove the occupying linked worktree separately only after its own safety checks pass. Current, main, locked, prunable/missing, and any other registered occupancy block retirement.
Retirement rejects unmerged history Preserve or integrate the commits, or obtain explicit user authorization for the data-loss risk before a separate call with force: true.
Retirement outcome is uncertain Retirement may have completed. Manually inspect the repository identity, target ref, retiring local ref, and complete worktree inventory before retrying; do not recreate or reset a branch as automatic rollback.
Push fails Confirm the current branch is correct and your normal Git remote credentials can push.
Related PR is unavailable Set GITHUB_TOKEN or GH_TOKEN before starting pi if related-PR context is wanted. Without a token, BranchMe keeps local context and intentionally makes no unauthenticated GitHub request.
PR auth fails Set GITHUB_TOKEN or GH_TOKEN before starting pi, or copy .env.example to .env and fill in one token.
Missing PR fields Provide all five fields, or set BRANCHME_PR_AUTOFILL=true in the process environment or repository .env.
Autofill cannot infer the base Ensure origin/HEAD names an existing local branch, keep a local main, master, trunk, or develop branch, or provide baseBranch explicitly.
PR branch does not exist locally Create or fetch/check out the local headBranch and baseBranch branches first; BranchMe does not use remote-only or cross-repository PR refs.
PR branch is not visible or is stale on GitHub Run push_branch, wait for it to complete, then retry pull_request; do not batch push_branch and pull_request in the same assistant tool call.
Repository mismatch Make origin and GITHUB_REPOSITORY refer to the same owner/repo.
Need a user-authored commit Use commit_changes. Other tools never accept commit messages; integrate_branch, update_from_base, and conclude_merge may let Git create a standard merge commit.
No active model or API key for a drafted commit Select or configure a Pi model, or pass an explicit message to commit_changes action="commit".
confirm=true requires a UI-capable Pi mode Run Pi with the TUI, or omit confirm in non-UI mode.
Commit flagged for secret files/tokens or unreadable files In the TUI, approve only reviewed safe fixtures or placeholders. In non-UI mode, remove the file or token, make the file readable, or commit manually.
Git status changed while committing Rerun the commit so it gathers fresh context.
Empty, thinking-only, length-stopped, or invalid model draft The draft is retried or repaired once. If it still fails, reduce the change size, lower model thinking, add steering text, or use commit_changes action="gather" and commit with an explicit message.
Other extensions interfere Test with pi --no-extensions -e ..

Diagnostics

From a source checkout:

npm run validate
npm run check:pack
printf '/branchme help\n/quit\n' | pi --no-extensions -e .

Validation covers TypeScript typechecking, formatting checks, automatic context collection and prompt injection, mocked GitHub lookup, isolated real-Git worktree, branch-integration, and leased branch-retirement lifecycle tests, package checks, checkout Pi runtime smoke, and package-content verification. The checkout smoke loads BranchMe through Pi, then uses a temporary verifier command to confirm every tool in the canonical BRANCHME_TOOL_NAMES inventory is visible through pi.getAllTools() with strict schemas and prompt metadata, including integrate_branch, retire_branch, conclude_merge, and targeted branch_status.ancestry, with no generic continue_merge or abort_merge tool. Runtime smoke inspects retirement and landing registration/schema but never executes either cleanup tool. Smoke-test notes are recorded in docs/SMOKE_TEST.md, and TUI/help captures are stored in docs/TUI_CAPTURE.md.

Refresh TUI captures intentionally with:

UPDATE_TUI_CAPTURE=1 node --test test/tui-capture.test.mjs

Update and Uninstall

pi update --extensions                  # update installed pi packages
pi update npm:@senad-d/branchme        # update BranchMe only
pi remove npm:@senad-d/branchme        # remove global install
pi remove npm:@senad-d/branchme -l     # remove project-local install

Development

npm install
npm run typecheck
npm run format:check
npm run test
npm run check:pack
npm run validate
npm run smoke:pi:packed
pi --no-extensions -e .

npm run check:pack verifies the npm package does not include local state, specs, caches, node_modules, real environment files, or other private development artifacts. The safe .env.example template is included. npm run smoke:pi is the checkout runtime smoke and verifies BranchMe command output plus real Pi tool registration metadata. npm run smoke:pi:packed is the release-gate smoke: it packs the npm artifact into a temporary directory, installs it with production dependency settings, and runs pi against the installed package.


Publishing

BranchMe publishes to npm as @senad-d/branchme. You need an npm account with publish access to the @senad-d scope.

npm login
npm whoami
npm run release:check # optional preflight; every publish path runs this gate
node scripts/publish-npm.mjs

npm run release:check is the canonical release gate: it runs checkout validation and then installs and loads the packed npm artifact in isolation. Both the local publish script and the GitHub Publish to npm workflow run this gate before npm publication; a failure prevents publication and the workflow's Git tag creation.

The publish script requires a clean working tree, asks for the version number, runs npm run release:check, runs npm version <version> to update package.json and package-lock.json, creates the v<version> git tag, publishes with npm publish --access public, and then offers to push the release commit and tag.

Run it only from a clean working tree after updating CHANGELOG.md.

License

MIT