@senad-d/branchme 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.0 - Unreleased
3
+ ## 0.2.2 - Unreleased
4
4
 
5
+ - Allowed `branch_status` ancestry endpoints to be remote-tracking refs such as `origin/main` in addition to exact local branches; local branches take precedence and remote-tracking refs stay read-only comparison targets.
6
+ - Added optional `remote`/`branch` parameters to `fetch_branch` for a targeted fetch of one exact remote branch into its remote-tracking ref (default remote `origin`; `remote` requires `branch`) without touching local branches, the working tree, or the current branch's upstream configuration; the no-argument behavior is unchanged.
7
+ - Added an optional read-only `baseRef` parameter to `create_worktree` for `branchMode: "new"`, starting the new branch from an exact local branch, remote-tracking ref, or full commit regardless of the current checkout's branch, dirt, or staleness; the default from-`HEAD` behavior is unchanged.
5
8
  - Implemented the `branchme` informational slash command with help aliases.
6
9
  - Added thirteen strict BranchMe tools: `branch_status`, `change_branch`, `fetch_branch`, `pull_branch`, `rebase_branch`, `integrate_branch`, `retire_branch`, `create_branch`, `push_branch`, `pull_request`, `list_worktrees`, `create_worktree`, and `remove_worktree`; merge-continuation tools are intentionally absent.
7
10
  - Added argv-style git helpers for repository status, branch validation/creation/switching, clean-worktree preflight, upstream detection, configured-upstream fetch, fast-forward-only current-branch pull, current-branch rebase with automatic abort on failure, verified local branch integration, current-branch push/publish, and bounded NUL-delimited worktree discovery.
@@ -13,6 +16,7 @@
13
16
  - Added canonical absolute-path and lossless-identity validation before worktree mutations, including existing-destination, nested-worktree, common-Git-directory, repository-membership, redaction, escaping, and truncation boundaries. BranchMe does not copy ignored/untracked files or start/switch Pi sessions.
14
17
  - Added GitHub repository resolution, environment-token and local `.env` token fallback handling, REST pull request creation, response validation, and token redaction.
15
18
  - Added opt-in `BRANCHME_PR_AUTOFILL` support for omitted PR fields, including current/default branch inference, bounded, Markdown-safe, and token-redacted title/body generation from commit subjects, and a non-draft default.
19
+ - Allowed autonomous and delegated agents to invoke `pull_request` from user, system, or developer prompts, `AGENTS.md`, skills, automation, and subagent workflows without separate end-user confirmation.
16
20
  - Added bounded automatic Git context before each agent run with branch/upstream state, working-tree counts and unstaged paths, authenticated related-open-PR lookup, and recent commits.
17
21
  - Expanded `branch_status` into a shared, explicit, read-only context refresh for state that may change during a run.
18
22
  - Added unit tests with mocked `pi.exec` and `fetch` for Git context collection and prompt safety, git, branch-integration, branch-retirement, and worktree helpers, GitHub helpers, command behavior, strict tool schemas, prompt metadata, and extension registration, plus isolated temporary-repository worktree, merge, and leased local-ref retirement lifecycle coverage.
package/README.md CHANGED
@@ -101,7 +101,7 @@ For isolated work, a specialized Git subagent can use the explicit worktree work
101
101
 
102
102
  BranchMe does not change the active Pi process's cwd, start Pi or other processes, create sessions, copy `.env` or other ignored/untracked files, or automatically retire a branch when removing its worktree. For credentials needed by agents in linked worktrees, prefer process-level environment variables rather than copying repository-root secrets.
103
103
 
104
- To refresh the current branch's configured remote-tracking ref without changing the local branch or working tree, use `fetch_branch`. To reconcile the clean current branch by rewriting its local commits, run `fetch_branch`, wait for it to complete, and then run `rebase_branch`. Both tools require a configured upstream; `rebase_branch` automatically attempts `git rebase --abort` if rebasing fails.
104
+ 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.
105
105
 
106
106
  BranchMe is tool-based. The slash command is informational only and never changes or updates branches, creates or removes worktrees, changes cwd, starts processes/sessions, fetches, rebases, pushes, commits, stages, edits files, or opens pull requests.
107
107
 
@@ -201,7 +201,7 @@ With autofill enabled, omitted fields are resolved as follows:
201
201
  - `body`: a bounded Markdown summary of commit subjects in `baseBranch..headBranch`.
202
202
  - `draft`: `false`.
203
203
 
204
- Explicit tool arguments always take precedence. Autofill does not create a PR by itself: the user must still ask the agent to create one.
204
+ 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.
205
205
 
206
206
  If local `origin` and `GITHUB_REPOSITORY` both resolve but disagree, `pull_request` fails closed.
207
207
 
@@ -224,13 +224,13 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
224
224
 
225
225
  | Tool | Schema | Behavior |
226
226
  | --- | --- | --- |
227
- | `branch_status` | `{ "ancestry"?: { "sourceBranch": string, "targetBranch": string } }` | Explicitly refreshes the same bounded context used at agent start. An optional strict ancestry query captures both exact local branch HEADs and reports whether the source commit is an ancestor of the target commit. It is read-only; automatic Git context does not run ancestry queries. |
227
+ | `branch_status` | `{ "ancestry"?: { "sourceBranch": string, "targetBranch": 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. |
228
228
  | `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. |
229
- | `create_worktree` | `{ "worktreePath": string, "branchName": string, "branchMode": "new" \| "existing" }` | Creates and verifies a linked worktree at an explicitly approved absolute path. `new` creates a local branch from current `HEAD`; `existing` requires an existing local branch not checked out elsewhere. It returns a ready handoff with the exact canonical absolute cwd and local branch identity. |
229
+ | `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. |
230
230
  | `remove_worktree` | `{ "worktreePath": string }` | Force-free removal of an explicitly selected, verified clean linked worktree. It rejects the main/current, detached, locked, prunable/missing, dirty, ignored-file-containing, or foreign worktree and verifies that the local branch remains at the same commit. |
231
231
  | `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. |
232
232
  | `change_branch` | `{ "branchName": string }` | Validates `branchName`, requires `refs/heads/<branchName>` to exist locally, rejects dirty worktrees, and runs `git switch <branchName>`. |
233
- | `fetch_branch` | `{}` | 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>`; only that tracking ref is refreshed without changing local branches or working-tree files. |
233
+ | `fetch_branch` | `{ "remote"?: string, "branch"?: 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. |
234
234
  | `pull_branch` | `{}` | 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. |
235
235
  | `rebase_branch` | `{}` | Requires a clean current branch with a configured upstream and runs `git rebase --no-autostash --no-update-refs <upstream>`; it rewrites local commits and automatically attempts `git rebase --abort` on failure. |
236
236
  | `integrate_branch` | `{ "sourceBranch": string, "targetBranch": 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. It never fetches or pushes. |
@@ -238,7 +238,7 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
238
238
  | `push_branch` | `{}` | 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. |
239
239
  | `pull_request` | `{ "headBranch"?: string, "baseBranch"?: string, "title"?: string, "body"?: string, "draft"?: boolean }` | Preflights GitHub branch visibility and verifies the GitHub `headBranch` commit matches the local branch, then creates a pull request in the resolved current repository. Omitted fields require `BRANCHME_PR_AUTOFILL=true`; branch refs must be distinct, exist locally, and cannot use `owner:branch`. |
240
240
 
241
- All schemas reject additional properties. `change_branch` never accepts `baseRef`, `force`, `stash`, `discard`, `create`, `owner`, `repo`, or path inputs. `fetch_branch`, `pull_branch`, and `rebase_branch` have strict empty schemas and never accept a branch, remote, refspec, force, autostash, or arbitrary rebase target. `integrate_branch` requires exactly `sourceBranch` and `targetBranch`; it accepts no repository, path, remote, strategy, message, squash, signing, commit, continuation, abort, force, fetch, push, deletion, or worktree controls. `create_worktree` requires exactly `worktreePath`, `branchName`, and `branchMode`; `remove_worktree` requires exactly `worktreePath`. No worktree tool accepts force, move, prune, repair, lock, unlock, detached, orphan, remote, refspec, or arbitrary start-point 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`. `continue_merge` and `abort_merge` are not available.
241
+ All schemas reject additional properties. `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` and `rebase_branch` have strict empty schemas and never accept a branch, remote, refspec, force, autostash, or arbitrary rebase target. `integrate_branch` requires exactly `sourceBranch` and `targetBranch`; it accepts no repository, path, remote, strategy, message, squash, signing, commit, continuation, abort, force, fetch, push, deletion, or worktree controls. `create_worktree` requires exactly `worktreePath`, `branchName`, and `branchMode`, plus an optional read-only `baseRef` start point for `branchMode: "new"`; `remove_worktree` requires exactly `worktreePath`. 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`. `continue_merge` and `abort_merge` are not available.
242
242
 
243
243
  ---
244
244
 
@@ -257,7 +257,7 @@ Before each agent run, BranchMe appends an **Automatic Git Context** snapshot to
257
257
 
258
258
  Collection defaults are a 5-second timeout per local Git command, 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.
259
259
 
260
- 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 local branch commit IDs, and reports `isAncestor`. This targeted proof is explicit only: automatic Git context remains unchanged. Run it after `integrate_branch` completes, never in the same parallel tool batch.
260
+ 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.
261
261
 
262
262
  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.
263
263
 
@@ -310,7 +310,7 @@ with `branch_status` only after `integrate_branch` has returned. Do not batch th
310
310
 
311
311
  `create_worktree` requires a non-blank absolute path with no control characters. Its immediate parent must already be a directory. BranchMe resolves that parent to build a canonical destination, 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.
312
312
 
313
- For `branchMode: "new"`, BranchMe creates the requested local branch from the current `HEAD` only. For `branchMode: "existing"`, it uses only an existing local branch that is not checked out in another worktree; 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:
313
+ 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:
314
314
 
315
315
  ```json
316
316
  {
@@ -378,13 +378,13 @@ BranchMe operates only on the repository where pi is running:
378
378
  - Automatic collection and `branch_status` run bounded, read-only Git commands from the verified git root.
379
379
  - Git commands use `pi.exec("git", args, { cwd, signal, timeout })` with argv arrays; repository mutations run from the verified git root.
380
380
  - `change_branch` switches only to existing local branches and has no `force`, `stash`, `discard`, remote, or path input.
381
- - `fetch_branch` requires a configured upstream, uses an explicit source-to-remote-tracking refspec with tags and submodule recursion disabled, and does not change local branches or working-tree files.
381
+ - `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.
382
382
  - `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.
383
383
  - `rebase_branch` requires a clean worktree and configured upstream, rewrites only the current branch onto the locally available upstream with autostash and multi-ref updates disabled, and automatically attempts to abort on failure.
384
384
  - `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.
385
385
  - `create_branch` creates from the current `HEAD` only and has no `baseRef` input.
386
386
  - `list_worktrees` is an explicit, read-only repository inventory; it is intentionally absent from automatic active-worktree context.
387
- - `create_worktree` may create a linked checkout outside the active checkout only after canonical path and current-repository boundary checks; dependent worktree calls must wait for its verified handoff.
387
+ - `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.
388
388
  - `remove_worktree` passes Git only a freshly verified canonical linked-worktree path, never uses force, and retains the branch.
389
389
  - `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.
390
390
  - `push_branch` pushes only the current branch, uses no bare upstream `git push`, and has no `branchName` input.
@@ -181,3 +181,4 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.2.0` p
181
181
  - `push_branch` uses `origin` only when the current branch has no configured upstream.
182
182
  - `pull_request` infers owner/repository from the current checkout or matching `GITHUB_REPOSITORY`, never accepts owner/repository tool inputs, and requires local branch refs with the head matching GitHub.
183
183
  - Pull request fields remain explicit unless `BRANCHME_PR_AUTOFILL=true`; explicit values always take precedence.
184
+ - `pull_request` is available to autonomous and delegated workflows without separate end-user confirmation. PR creation may be directed by user, system, or developer prompts, `AGENTS.md`, skills, automation, or delegated/subagent prompts.
package/docs/STRUCTURE.md CHANGED
@@ -54,9 +54,9 @@ src/
54
54
  - Automatic context and `branch_status` share bounded, read-only collection. They run no mutations and capture repository metadata only—never diffs or file contents.
55
55
  - Repository-controlled paths, branch names, commit subjects, and PR fields are escaped, quoted, redacted, bounded, and labeled untrusted before system-prompt insertion.
56
56
  - Related-PR lookup may issue an authenticated `GET /pulls` before every agent run and on explicit refresh. It has a 4-second timeout and 64 KiB response limit, and makes no unauthenticated fallback request.
57
- - The start-of-run snapshot may be stale after a mutation in that same run; `branch_status` is the explicit read-only refresh. Its optional targeted ancestry proof must run after `integrate_branch`, not in the same parallel tool batch.
57
+ - The start-of-run snapshot may be stale after a mutation in that same run; `branch_status` is the explicit read-only refresh. Its optional targeted ancestry proof accepts exact local branches or remote-tracking refs such as `origin/main` as read-only endpoints and must run after `integrate_branch` or `fetch_branch`, not in the same parallel tool batch.
58
58
  - `change_branch` mutates local HEAD and working-tree files only through `git switch <branchName>` for existing local branches after a clean-worktree preflight.
59
- - `fetch_branch` requires a configured upstream and runs `git fetch --no-tags --no-recurse-submodules <remote> <remote-ref>:<remote-tracking-ref>`; its explicit refspec updates only that tracking ref without changing local branches or working-tree files.
59
+ - `fetch_branch` runs `git fetch --no-tags --no-recurse-submodules <remote> <remote-ref>:<remote-tracking-ref>`; its explicit refspec updates only that tracking ref without changing 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.
60
60
  - `pull_branch` requires a clean worktree and configured upstream, then updates only the current branch with an explicit `git pull --ff-only --no-rebase --no-autostash <remote> <remote-ref>` command; divergence fails without a rebase or merge commit.
61
61
  - `rebase_branch` requires a clean worktree and configured upstream, then rebases only the current branch with `git rebase --no-autostash --no-update-refs <upstream>`; it rewrites local commits and automatically attempts `git rebase --abort` on failure.
62
62
  - `integrate_branch` requires distinct existing local refs and a clean control worktree already on the target. It rejects non-empty target-branch `mergeOptions`, runs `git -c rerere.enabled=false merge --ff --no-edit --no-autostash --no-rerere-autoupdate --no-overwrite-ignore refs/heads/<sourceBranch>`, verifies before/after refs and ancestry, and classifies already-integrated, fast-forward, exact two-parent merge-commit, or conflict.
@@ -67,7 +67,7 @@ src/
67
67
  - Git hooks, custom merge drivers, clean/smudge filters, signing policy, and `reference-transaction` hooks stay active where Git invokes them and may execute commands or network operations outside BranchMe's direct argv boundary.
68
68
  - `create_branch` mutates local branch/HEAD only with `git switch -c`.
69
69
  - `list_worktrees` reads a bounded `git worktree list --porcelain -z` inventory and remains explicit rather than expanding automatic active-worktree context.
70
- - `create_worktree` requires an explicitly approved absolute destination, canonicalizes its existing parent, rejects existing or nested/common-Git-directory destinations, and creates only from current `HEAD` or an unoccupied existing local branch. It verifies canonical path, branch, `HEAD`, and cleanliness before returning a ready handoff.
70
+ - `create_worktree` requires an explicitly approved absolute destination, canonicalizes its existing parent, rejects existing or nested/common-Git-directory destinations, and creates from current `HEAD`, from an explicit read-only `baseRef` (exact local branch, remote-tracking ref, or full commit resolved to a commit before mutation and never checked out or reset), or from an unoccupied existing local branch. It verifies canonical path, branch, `HEAD`, and cleanliness before returning a ready handoff.
71
71
  - `remove_worktree` resolves an exact fresh current-repository inventory match, rejects main/current/dirty/ignored-entry-containing/detached/locked/prunable/missing/bare entries, runs a bounded removal-specific ignored-entry scan, performs force-free removal with the verified path, and confirms that the local branch remains at the same commit.
72
72
  - `push_branch` mutates remote refs only for the current branch and uses an explicit upstream remote/refspec instead of bare `git push` when an upstream exists.
73
73
  - `pull_request` requires resolved `headBranch` and `baseBranch` values to be distinct and exist locally, requires `headBranch` to match the GitHub-visible branch commit, queues behind already-started same-repository git mutation windows, makes GitHub REST API calls for the resolved current repository only, and rejects owner-prefixed or unsafe branch refs before the request. Omitted fields require configured autofill.
@@ -24,7 +24,7 @@ Commands only show info; BranchMe tools perform actions.
24
24
 
25
25
  1. `branch_status` — inspect repo and branch state.
26
26
  2. `change_branch` — switch to a clean existing local branch.
27
- 3. `fetch_branch` — fetch its configured upstream remote without changing local files.
27
+ 3. `fetch_branch` — fetch its configured upstream remote (or an explicit `remote`/`branch`) without changing local files.
28
28
  4. `pull_branch` — fast-forward from upstream, or `rebase_branch` — rebase local commits onto upstream.
29
29
  5. `create_branch` — create a new branch from the updated `HEAD`.
30
30
  6. Commit outside BranchMe.
@@ -58,7 +58,7 @@ Commands only show info; BranchMe tools perform actions.
58
58
  - Run inside a Git repo with `git` available.
59
59
  - For PRs: GitHub `origin` and `GITHUB_TOKEN` or `GH_TOKEN` (environment or `.env`).
60
60
  - Optional: set `BRANCHME_PR_AUTOFILL=true` in the environment or `.env` to generate omitted PR fields.
61
- - `fetch_branch`, `pull_branch`, and `rebase_branch` require a configured upstream.
61
+ - `fetch_branch` without `branch`, `pull_branch`, and `rebase_branch` require a configured upstream.
62
62
  - `pull_branch` and `rebase_branch` require a clean working tree.
63
63
  - `rebase_branch` rewrites local commits only when explicitly requested and auto-aborts on failure.
64
64
  - BranchMe never stages, creates user-authored commits, or force-pushes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senad-d/branchme",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Pi extension for verified current-repository Git branch, worktree, integration, retirement, push, and pull request workflows.",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@ export function getBranchMeHelpText(): string {
33
33
  "",
34
34
  "1. `branch_status` — inspect repo and branch state.",
35
35
  "2. `change_branch` — switch to a clean existing local branch.",
36
- "3. `fetch_branch` — fetch its configured upstream remote without changing local files.",
36
+ "3. `fetch_branch` — fetch its configured upstream remote (or an explicit `remote`/`branch`) without changing local files.",
37
37
  "4. `pull_branch` — fast-forward from upstream, or `rebase_branch` — rebase local commits onto upstream.",
38
38
  "5. `create_branch` — create a new branch from the updated `HEAD`.",
39
39
  "6. Commit outside BranchMe.",
@@ -67,7 +67,7 @@ export function getBranchMeHelpText(): string {
67
67
  "- Run inside a Git repo with `git` available.",
68
68
  "- For PRs: GitHub `origin` and `GITHUB_TOKEN` or `GH_TOKEN` (environment or `.env`).",
69
69
  "- Optional: set `BRANCHME_PR_AUTOFILL=true` in the environment or `.env` to generate omitted PR fields.",
70
- "- `fetch_branch`, `pull_branch`, and `rebase_branch` require a configured upstream.",
70
+ "- `fetch_branch` without `branch`, `pull_branch`, and `rebase_branch` require a configured upstream.",
71
71
  "- `pull_branch` and `rebase_branch` require a clean working tree.",
72
72
  "- `rebase_branch` rewrites local commits only when explicitly requested and auto-aborts on failure.",
73
73
  "- BranchMe never stages, creates user-authored commits, or force-pushes.",
package/src/git.ts CHANGED
@@ -33,6 +33,7 @@ import type {
33
33
  CreateWorktreeMode,
34
34
  CurrentBranchInfo,
35
35
  FetchBranchDetails,
36
+ FetchRemoteBranchDetails,
36
37
  GitExecResult,
37
38
  GitFileChange,
38
39
  GitFileChangeSummary,
@@ -1185,6 +1186,39 @@ function worktreeCreationFailure(
1185
1186
  );
1186
1187
  }
1187
1188
 
1189
+ async function resolveWorktreeBaseCommit(
1190
+ pi: Pick<ExtensionAPI, "exec">,
1191
+ ctx: GitCommandContext,
1192
+ baseRef: string,
1193
+ signal?: AbortSignal,
1194
+ ): Promise<string> {
1195
+ // baseRef is a read-only start point: it is resolved to a commit before mutation and
1196
+ // is never checked out, reset, or otherwise modified.
1197
+ if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(baseRef)) {
1198
+ const result = await runGit(pi, ctx, ["rev-parse", "--verify", `${baseRef}^{commit}`], {
1199
+ signal,
1200
+ timeout: GIT_STATUS_TIMEOUT_MS,
1201
+ allowFailure: true,
1202
+ });
1203
+ const commit = trimOutput(result.stdout);
1204
+ if (result.code !== 0 || !/^[0-9a-f]{40,64}$/iu.test(commit)) {
1205
+ throw new Error(`baseRef ${safeWorktreeBranchLabel(baseRef)} does not resolve to an existing commit.`);
1206
+ }
1207
+ return commit;
1208
+ }
1209
+
1210
+ await validateBranchName(pi, ctx, baseRef, signal);
1211
+ if (await localBranchExists(pi, ctx, baseRef, signal)) {
1212
+ return getLocalBranchCommit(pi, ctx, baseRef, signal);
1213
+ }
1214
+ if (await remoteTrackingRefExists(pi, ctx, baseRef, signal)) {
1215
+ return getRemoteTrackingRefCommit(pi, ctx, baseRef, signal);
1216
+ }
1217
+ throw new Error(
1218
+ `baseRef ${safeWorktreeBranchLabel(baseRef)} does not name an existing local branch, remote-tracking ref, or full commit.`,
1219
+ );
1220
+ }
1221
+
1188
1222
  export async function createWorktree(
1189
1223
  pi: Pick<ExtensionAPI, "exec">,
1190
1224
  ctx: GitCommandContext,
@@ -1192,8 +1226,15 @@ export async function createWorktree(
1192
1226
  branchName: string,
1193
1227
  branchMode: CreateWorktreeMode,
1194
1228
  signal?: AbortSignal,
1229
+ baseRef?: string,
1195
1230
  ): Promise<CreateWorktreeDetails> {
1196
1231
  validateCreateWorktreeMode(branchMode);
1232
+ if (baseRef !== undefined) {
1233
+ if (branchMode !== "new") {
1234
+ throw new Error("baseRef is only supported with branchMode 'new'; an existing local branch keeps its own commit.");
1235
+ }
1236
+ validateBranchNameInput(baseRef, "baseRef");
1237
+ }
1197
1238
  const requestedWorktreePath = validateWorktreePathInput(worktreePath);
1198
1239
  const repoRoot = await getGitRoot(pi, ctx, signal);
1199
1240
  const rootCtx = { cwd: repoRoot };
@@ -1222,11 +1263,14 @@ export async function createWorktree(
1222
1263
  throw new Error(`Local branch ${safeWorktreeBranchLabel(branchName)} is already checked out in a worktree.`);
1223
1264
  }
1224
1265
 
1266
+ const baseCommit = baseRef === undefined
1267
+ ? null
1268
+ : await resolveWorktreeBaseCommit(pi, rootCtx, baseRef, signal);
1225
1269
  const expectedHead = branchMode === "new"
1226
- ? sourceHead
1270
+ ? baseCommit ?? sourceHead
1227
1271
  : await getLocalBranchCommit(pi, rootCtx, branchName, signal);
1228
1272
  const args = branchMode === "new"
1229
- ? ["worktree", "add", "-b", branchName, prepared.canonicalPath, "HEAD"]
1273
+ ? ["worktree", "add", "-b", branchName, prepared.canonicalPath, baseCommit ?? "HEAD"]
1230
1274
  : ["worktree", "add", prepared.canonicalPath, branchName];
1231
1275
 
1232
1276
  try {
@@ -1263,6 +1307,9 @@ export async function createWorktree(
1263
1307
  worktreePath: safeWorktreeValue(requestedWorktreePath, GIT_WORKTREE_PATH_LIMIT_CHARS),
1264
1308
  branchName: handoffBranch,
1265
1309
  branchMode,
1310
+ ...(baseRef === undefined
1311
+ ? {}
1312
+ : { baseRef: safeWorktreeValue(baseRef, GIT_CONTEXT_VALUE_LIMIT_CHARS) }),
1266
1313
  },
1267
1314
  verified: {
1268
1315
  before: {
@@ -1994,24 +2041,58 @@ export async function inspectDirectLocalBranchRef(
1994
2041
  };
1995
2042
  }
1996
2043
 
1997
- export async function getLocalBranchCommit(
2044
+ async function getVerifiedRefCommit(
1998
2045
  pi: Pick<ExtensionAPI, "exec">,
1999
2046
  ctx: GitCommandContext,
2000
- branchName: string,
2047
+ revision: string,
2048
+ describedRef: string,
2001
2049
  signal?: AbortSignal,
2002
2050
  ): Promise<string> {
2003
- validateBranchNameInput(branchName);
2004
- const result = await runGit(pi, ctx, ["rev-parse", "--verify", `refs/heads/${branchName}^{commit}`], {
2051
+ const result = await runGit(pi, ctx, ["rev-parse", "--verify", `${revision}^{commit}`], {
2005
2052
  signal,
2006
2053
  timeout: GIT_STATUS_TIMEOUT_MS,
2007
2054
  });
2008
2055
  const commit = trimOutput(result.stdout);
2009
2056
  if (!/^[0-9a-f]{40,64}$/iu.test(commit)) {
2010
- throw new Error(`Unable to resolve local branch '${safeGitContextValue(branchName)}' to a commit: ${safeOutput(result.stdout) || "empty output"}`);
2057
+ throw new Error(`Unable to resolve ${describedRef} to a commit: ${safeOutput(result.stdout) || "empty output"}`);
2011
2058
  }
2012
2059
  return commit;
2013
2060
  }
2014
2061
 
2062
+ export async function getLocalBranchCommit(
2063
+ pi: Pick<ExtensionAPI, "exec">,
2064
+ ctx: GitCommandContext,
2065
+ branchName: string,
2066
+ signal?: AbortSignal,
2067
+ ): Promise<string> {
2068
+ validateBranchNameInput(branchName);
2069
+ return getVerifiedRefCommit(pi, ctx, `refs/heads/${branchName}`, `local branch '${safeGitContextValue(branchName)}'`, signal);
2070
+ }
2071
+
2072
+ export async function remoteTrackingRefExists(
2073
+ pi: Pick<ExtensionAPI, "exec">,
2074
+ ctx: GitCommandContext,
2075
+ refName: string,
2076
+ signal?: AbortSignal,
2077
+ ): Promise<boolean> {
2078
+ const result = await runGit(pi, ctx, ["show-ref", "--verify", "--quiet", `refs/remotes/${refName}`], {
2079
+ signal,
2080
+ timeout: GIT_STATUS_TIMEOUT_MS,
2081
+ allowFailure: true,
2082
+ });
2083
+ return result.code === 0;
2084
+ }
2085
+
2086
+ export async function getRemoteTrackingRefCommit(
2087
+ pi: Pick<ExtensionAPI, "exec">,
2088
+ ctx: GitCommandContext,
2089
+ refName: string,
2090
+ signal?: AbortSignal,
2091
+ ): Promise<string> {
2092
+ validateBranchNameInput(refName, "Remote-tracking ref");
2093
+ return getVerifiedRefCommit(pi, ctx, `refs/remotes/${refName}`, `remote-tracking ref '${safeGitContextValue(refName)}'`, signal);
2094
+ }
2095
+
2015
2096
  export async function requireExistingLocalBranch(
2016
2097
  pi: Pick<ExtensionAPI, "exec">,
2017
2098
  ctx: GitCommandContext,
@@ -2044,6 +2125,24 @@ export async function isCommitAncestor(
2044
2125
  return result.code === 0;
2045
2126
  }
2046
2127
 
2128
+ async function resolveAncestryRefCommit(
2129
+ pi: Pick<ExtensionAPI, "exec">,
2130
+ ctx: GitCommandContext,
2131
+ refName: string,
2132
+ label: "Source" | "Target",
2133
+ signal?: AbortSignal,
2134
+ ): Promise<string> {
2135
+ // Read-only comparison endpoints: local branches keep precedence, remote-tracking refs
2136
+ // (for example origin/main) are accepted as read baselines and are never mutated.
2137
+ if (await localBranchExists(pi, ctx, refName, signal)) {
2138
+ return getLocalBranchCommit(pi, ctx, refName, signal);
2139
+ }
2140
+ if (await remoteTrackingRefExists(pi, ctx, refName, signal)) {
2141
+ return getRemoteTrackingRefCommit(pi, ctx, refName, signal);
2142
+ }
2143
+ throw new Error(`${label} local branch or remote-tracking ref '${safeGitContextValue(refName)}' does not exist.`);
2144
+ }
2145
+
2047
2146
  export async function getLocalBranchAncestry(
2048
2147
  pi: Pick<ExtensionAPI, "exec">,
2049
2148
  ctx: GitCommandContext,
@@ -2054,11 +2153,9 @@ export async function getLocalBranchAncestry(
2054
2153
  validateBranchNameInput(query.targetBranch, "Target branch");
2055
2154
  await validateBranchName(pi, ctx, query.sourceBranch, signal);
2056
2155
  await validateBranchName(pi, ctx, query.targetBranch, signal);
2057
- await requireExistingLocalBranch(pi, ctx, query.sourceBranch, "Source", signal);
2058
- await requireExistingLocalBranch(pi, ctx, query.targetBranch, "Target", signal);
2059
2156
 
2060
- const sourceHead = await getLocalBranchCommit(pi, ctx, query.sourceBranch, signal);
2061
- const targetHead = await getLocalBranchCommit(pi, ctx, query.targetBranch, signal);
2157
+ const sourceHead = await resolveAncestryRefCommit(pi, ctx, query.sourceBranch, "Source", signal);
2158
+ const targetHead = await resolveAncestryRefCommit(pi, ctx, query.targetBranch, "Target", signal);
2062
2159
  const isAncestor = await isCommitAncestor(pi, ctx, sourceHead, targetHead, signal);
2063
2160
 
2064
2161
  return {
@@ -2181,6 +2278,74 @@ export async function fetchCurrentBranch(
2181
2278
  });
2182
2279
  }
2183
2280
 
2281
+ function validateRequestedRemoteName(remote: string): void {
2282
+ if (!remote || /[\u0000-\u001f\u007f]/u.test(remote) || /\s/u.test(remote)) {
2283
+ throw new Error("Unable to fetch: remote cannot be blank or contain whitespace or control characters.");
2284
+ }
2285
+ if (remote === ".") throw new Error("Unable to fetch: remote must name a configured Git remote, not '.'.");
2286
+ if (remote.startsWith("-")) throw new Error("Unable to fetch: remote cannot start with '-'.");
2287
+ if (remote.includes(":") || remote.includes("@")) {
2288
+ throw new Error("Unable to fetch: remote cannot be a URL or user-prefixed target.");
2289
+ }
2290
+ }
2291
+
2292
+ async function requireConfiguredRemote(
2293
+ pi: Pick<ExtensionAPI, "exec">,
2294
+ ctx: GitCommandContext,
2295
+ remote: string,
2296
+ signal?: AbortSignal,
2297
+ ): Promise<void> {
2298
+ const result = await runGit(pi, ctx, ["remote", "get-url", remote], {
2299
+ signal,
2300
+ timeout: GIT_STATUS_TIMEOUT_MS,
2301
+ allowFailure: true,
2302
+ });
2303
+ if (result.code !== 0) {
2304
+ throw new Error(`Unable to fetch: remote '${safeGitContextValue(remote)}' is not a configured Git remote.`);
2305
+ }
2306
+ }
2307
+
2308
+ export async function fetchRemoteBranch(
2309
+ pi: Pick<ExtensionAPI, "exec">,
2310
+ ctx: GitCommandContext,
2311
+ remote: string,
2312
+ branch: string,
2313
+ signal?: AbortSignal,
2314
+ ): Promise<FetchRemoteBranchDetails> {
2315
+ validateRequestedRemoteName(remote);
2316
+ validateBranchNameInput(branch, "branch");
2317
+ const repoRoot = await getGitRoot(pi, ctx, signal);
2318
+ const rootCtx = { cwd: repoRoot };
2319
+
2320
+ return withRepositoryMutationQueue(repoRoot, async () => {
2321
+ await validateBranchName(pi, rootCtx, branch, signal);
2322
+ await requireConfiguredRemote(pi, rootCtx, remote, signal);
2323
+
2324
+ const remoteRef = `refs/heads/${branch}`;
2325
+ const trackingRef = `refs/remotes/${remote}/${branch}`;
2326
+ const refspec = `${remoteRef}:${trackingRef}`;
2327
+ const result = await runGit(
2328
+ pi,
2329
+ rootCtx,
2330
+ ["fetch", "--no-tags", "--no-recurse-submodules", remote, refspec],
2331
+ {
2332
+ signal,
2333
+ timeout: GIT_FETCH_TIMEOUT_MS,
2334
+ },
2335
+ );
2336
+
2337
+ return {
2338
+ repoRoot,
2339
+ remote: safeDetail(remote),
2340
+ branch: safeDetail(branch),
2341
+ remoteRef: safeDetail(remoteRef),
2342
+ remoteTrackingRef: safeDetail(trackingRef),
2343
+ refspec: safeDetail(refspec),
2344
+ output: safeOutput(result.stdout || result.stderr),
2345
+ };
2346
+ });
2347
+ }
2348
+
2184
2349
  export async function pullCurrentBranch(
2185
2350
  pi: Pick<ExtensionAPI, "exec">,
2186
2351
  ctx: GitCommandContext,
@@ -24,6 +24,7 @@ import {
24
24
  createLocalBranch,
25
25
  createWorktree,
26
26
  fetchCurrentBranch,
27
+ fetchRemoteBranch,
27
28
  getGitRoot,
28
29
  getLocalBranchCommit,
29
30
  getPullRequestCommitSubjects,
@@ -74,8 +75,8 @@ const BranchStatusParametersSchema = Type.Object(
74
75
  ancestry: Type.Optional(
75
76
  Type.Object(
76
77
  {
77
- sourceBranch: Type.String({ minLength: 1, description: "Exact existing local source branch to verify." }),
78
- targetBranch: Type.String({ minLength: 1, description: "Exact existing local target branch to verify." }),
78
+ sourceBranch: Type.String({ minLength: 1, description: "Exact existing local branch or remote-tracking ref (for example origin/main) whose commit is verified as the ancestor." }),
79
+ targetBranch: Type.String({ minLength: 1, description: "Exact existing local branch or remote-tracking ref (for example origin/main) whose commit is verified as the descendant." }),
79
80
  },
80
81
  { additionalProperties: false },
81
82
  ),
@@ -124,8 +125,26 @@ const CreateWorktreeParametersSchema = Type.Object(
124
125
  worktreePath: Type.String({ minLength: 1, description: "Explicit absolute destination path for the linked worktree." }),
125
126
  branchName: Type.String({ minLength: 1, description: "New or existing local branch name for the linked worktree." }),
126
127
  branchMode: StringEnum(["new", "existing"] as const, {
127
- description: "Whether create_worktree creates a new local branch from current HEAD or uses an existing local branch.",
128
+ description: "Whether create_worktree creates a new local branch (from current HEAD, or from baseRef when provided) or uses an existing local branch.",
128
129
  }),
130
+ baseRef: Type.Optional(Type.String({
131
+ minLength: 1,
132
+ description: "Optional read-only start point for branchMode 'new': an exact existing local branch, remote-tracking ref (for example origin/main), or full commit that the new branch starts from instead of current HEAD. Never checked out or mutated.",
133
+ })),
134
+ },
135
+ { additionalProperties: false },
136
+ );
137
+
138
+ const FetchBranchParametersSchema = Type.Object(
139
+ {
140
+ remote: Type.Optional(Type.String({
141
+ minLength: 1,
142
+ description: "Configured remote name for a targeted fetch; defaults to origin and requires branch. Omit together with branch to fetch the current branch's configured upstream.",
143
+ })),
144
+ branch: Type.Optional(Type.String({
145
+ minLength: 1,
146
+ description: "Exact branch name on the remote (for example main) to fetch into its remote-tracking ref without touching local branches or the current branch's upstream. Omit to fetch the current branch's configured upstream.",
147
+ })),
129
148
  },
130
149
  { additionalProperties: false },
131
150
  );
@@ -256,7 +275,10 @@ export function formatCreateWorktree(details: CreateWorktreeDetails): string {
256
275
  const cwd = safeWorktreeFormatValue(details.handoff.cwd, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
257
276
  const branch = safeWorktreeFormatValue(details.handoff.branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
258
277
  const mode = details.request.branchMode === "new" ? "new local branch" : "existing local branch";
259
- return `Created linked worktree ${cwd} for ${mode} ${branch}. Verified its canonical path, local branch, HEAD ${shortCommit(details.handoff.head)}, clean working tree, and ready handoff.`;
278
+ const base = details.request.baseRef === undefined
279
+ ? ""
280
+ : ` from base ${safeWorktreeFormatValue(details.request.baseRef, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS)}`;
281
+ return `Created linked worktree ${cwd} for ${mode} ${branch}${base}. Verified its canonical path, local branch, HEAD ${shortCommit(details.handoff.head)}, clean working tree, and ready handoff.`;
260
282
  }
261
283
 
262
284
  export function formatRemoveWorktree(details: RemoveWorktreeDetails): string {
@@ -487,12 +509,13 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
487
509
  pi.registerTool({
488
510
  name: BRANCH_STATUS_TOOL_NAME,
489
511
  label: "Branch Status",
490
- description: "branch_status explicitly refreshes the current Git repository snapshot and can optionally verify whether one captured local branch commit is an ancestor of another. branch_status is read-only and never mutates files, Git state, or GitHub state.",
491
- promptSnippet: "branch_status: explicitly refresh current-repository Git state and optionally verify targeted local-branch ancestry without mutation",
512
+ description: "branch_status explicitly refreshes the current Git repository snapshot and can optionally verify whether one captured branch commit is an ancestor of another; ancestry endpoints may be exact local branches or remote-tracking refs such as origin/main. branch_status is read-only and never mutates files, Git state, or GitHub state.",
513
+ promptSnippet: "branch_status: explicitly refresh current-repository Git state and optionally verify targeted local-branch or remote-tracking ancestry without mutation",
492
514
  promptGuidelines: [
493
515
  "Use the automatic Git context for start-of-run questions; call branch_status only for an explicit refresh or after Git state changes during the current run.",
494
516
  "Use branch_status as a read-only refresh; branch_status never mutates files, Git state, or GitHub state.",
495
517
  "Use targeted branch_status ancestry verification only after integrate_branch completes; do not issue branch_status in the same parallel tool batch as integrate_branch.",
518
+ "Use branch_status ancestry with a remote-tracking target such as origin/main after fetch_branch completes to prove a branch is contained in the fetched baseline; branch_status never checks out or resets remote-tracking refs.",
496
519
  ],
497
520
  parameters: BranchStatusParametersSchema,
498
521
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
@@ -564,18 +587,29 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
564
587
  pi.registerTool({
565
588
  name: FETCH_BRANCH_TOOL_NAME,
566
589
  label: "Fetch Branch",
567
- description: "fetch_branch fetches the current branch's configured upstream branch into its remote-tracking ref with an explicit git fetch --no-tags --no-recurse-submodules refspec. fetch_branch does not change local branches or working-tree files and never prunes, rebases, merges, stashes, stages, commits, or pushes.",
568
- promptSnippet: "fetch_branch: fetch the current branch's configured upstream into its remote-tracking ref without changing local branches or files",
590
+ description: "fetch_branch fetches one branch into its remote-tracking ref with an explicit git fetch --no-tags --no-recurse-submodules refspec. Without arguments fetch_branch fetches the current branch's configured upstream; with branch (and optional remote, default origin) it fetches that exact remote branch without touching local branches, the working tree, or the current branch's upstream configuration. fetch_branch never prunes, rebases, merges, stashes, stages, commits, or pushes.",
591
+ promptSnippet: "fetch_branch: fetch the current branch's configured upstream, or an explicit remote branch, into its remote-tracking ref without changing local branches or files",
569
592
  promptGuidelines: [
570
- "Use fetch_branch only when the user explicitly wants to fetch the configured upstream branch for the current branch.",
571
- "Use fetch_branch only on a current branch with an upstream; fetch_branch has no branchName, remote, tags, prune, force, or refspec parameters.",
572
- "Call fetch_branch and wait for it to complete before rebase_branch when the user wants the latest upstream state; do not batch fetch_branch with rebase_branch.",
593
+ "Use fetch_branch without arguments to fetch the configured upstream branch for the current branch; that default requires a current branch with an upstream.",
594
+ "Use fetch_branch with branch (and optional remote, default origin) to refresh another remote-tracking ref such as origin/main; fetch_branch never changes local branches, working-tree files, or upstream configuration, and remote requires branch.",
595
+ "Call fetch_branch and wait for it to complete before rebase_branch or a targeted branch_status ancestry proof against the fetched remote-tracking ref; do not batch fetch_branch with dependent calls.",
573
596
  ],
574
- parameters: EmptyParametersSchema,
575
- async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
576
- const details = await fetchCurrentBranch(pi, ctx, signal);
597
+ parameters: FetchBranchParametersSchema,
598
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
599
+ if (params.branch === undefined) {
600
+ if (params.remote !== undefined) {
601
+ throw new Error("fetch_branch remote requires branch; omit both to fetch the current branch's configured upstream.");
602
+ }
603
+ const details = await fetchCurrentBranch(pi, ctx, signal);
604
+ return {
605
+ content: [{ type: "text", text: `Fetched configured upstream remote ${details.remote} for current branch ${details.currentBranch}.` }],
606
+ details,
607
+ };
608
+ }
609
+
610
+ const details = await fetchRemoteBranch(pi, ctx, params.remote ?? "origin", params.branch, signal);
577
611
  return {
578
- content: [{ type: "text", text: `Fetched configured upstream remote ${details.remote} for current branch ${details.currentBranch}.` }],
612
+ content: [{ type: "text", text: `Fetched ${details.remote}/${details.branch} into remote-tracking ref ${details.remoteTrackingRef} without changing local branches.` }],
579
613
  details,
580
614
  };
581
615
  },
@@ -696,7 +730,8 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
696
730
  description: "pull_request creates a GitHub pull request in the resolved current repository. PR fields are optional only when BRANCHME_PR_AUTOFILL=true; pull_request then infers branches, derives title/body from commit subjects, and defaults draft to false. Branches must exist locally and on GitHub, and headBranch must match the GitHub-visible commit. Owner-prefixed refs, owner, and repo are never accepted as inputs.",
697
731
  promptSnippet: "pull_request: create a GitHub pull request with explicit fields or optional configured PR field autofill",
698
732
  promptGuidelines: [
699
- "Use pull_request only when the user explicitly asks to create a pull request; pull_request must not create one merely because a branch was pushed.",
733
+ "Use pull_request whenever creating a pull request is appropriate for the active workflow; authorization may come from user prompts, system or developer instructions, AGENTS.md, skills, automation, or delegated/subagent prompts.",
734
+ "Agents may invoke pull_request without a separate end-user confirmation when the active workflow makes pull request creation appropriate.",
700
735
  "When automatic Git context reports pull request field autofill disabled, use pull_request only with explicit headBranch, baseBranch, title, body, and draft values.",
701
736
  "When automatic Git context reports pull request field autofill enabled, pull_request may omit fields the user did not provide; prefer the user's explicit values whenever present.",
702
737
  "Use pull_request only with existing local branches for headBranch and baseBranch; headBranch must match the GitHub-visible branch commit.",
@@ -766,11 +801,12 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
766
801
  pi.registerTool({
767
802
  name: CREATE_WORKTREE_TOOL_NAME,
768
803
  label: "Create Worktree",
769
- description: "create_worktree creates and verifies one linked worktree at an explicit absolute worktreePath for either a new local branch from current HEAD or an existing unoccupied local branch. create_worktree returns exact lossless handoff identity fields and never infers paths, remotes, base refs, detached or orphan modes, or force behavior.",
770
- promptSnippet: "create_worktree: create and verify a linked worktree at an explicit absolute path with a ready handoff cwd",
804
+ description: "create_worktree creates and verifies one linked worktree at an explicit absolute worktreePath for either a new local branch (from current HEAD, or from an explicit read-only baseRef such as origin/main or a full commit) or an existing unoccupied local branch. create_worktree returns exact lossless handoff identity fields and never infers paths, remotes, base refs, detached or orphan modes, or force behavior.",
805
+ promptSnippet: "create_worktree: create and verify a linked worktree at an explicit absolute path, optionally starting its new branch from an explicit read-only baseRef, with a ready handoff cwd",
771
806
  promptGuidelines: [
772
807
  "Use create_worktree only when the user explicitly requests worktree creation and provides or approves the exact absolute worktreePath; create_worktree must never infer a filesystem path silently.",
773
- "Use create_worktree with exactly worktreePath, branchName, and branchMode; create_worktree never accepts force, baseRef, remote, detach, orphan, move, prune, repair, lock, or unlock parameters.",
808
+ "Use create_worktree with worktreePath, branchName, branchMode, and optionally baseRef for branchMode 'new'; create_worktree never accepts force, remote, detach, orphan, move, prune, repair, lock, or unlock parameters.",
809
+ "Use create_worktree with an explicit baseRef such as origin/main to start a new branch from a fetched remote baseline regardless of the current checkout's branch, dirt, or staleness; create_worktree only reads baseRef and never checks out or resets it.",
774
810
  "Do not batch create_worktree with dependent worktree mutations; wait for create_worktree to complete and verify its ready exact handoff.cwd before passing that cwd to another agent or session.",
775
811
  ],
776
812
  parameters: CreateWorktreeParametersSchema,
@@ -782,6 +818,7 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
782
818
  params.branchName,
783
819
  params.branchMode,
784
820
  signal,
821
+ params.baseRef,
785
822
  );
786
823
  return {
787
824
  content: [{ type: "text", text: formatCreateWorktree(details) }],
package/src/types.ts CHANGED
@@ -49,6 +49,8 @@ export interface CreateWorktreeToolInput {
49
49
  worktreePath: string;
50
50
  branchName: string;
51
51
  branchMode: CreateWorktreeMode;
52
+ /** Optional read-only start point for branchMode "new": local branch, remote-tracking ref, or full commit. */
53
+ baseRef?: string;
52
54
  }
53
55
 
54
56
  export interface RemoveWorktreeToolInput {
@@ -238,6 +240,16 @@ export interface FetchBranchDetails {
238
240
  output: string;
239
241
  }
240
242
 
243
+ export interface FetchRemoteBranchDetails {
244
+ repoRoot: string;
245
+ remote: string;
246
+ branch: string;
247
+ remoteRef: string;
248
+ remoteTrackingRef: string;
249
+ refspec: string;
250
+ output: string;
251
+ }
252
+
241
253
  export interface PullBranchDetails {
242
254
  repoRoot: string;
243
255
  currentBranch: string;