@senad-d/branchme 0.3.3 → 0.3.4

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,6 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.3.3 - Unreleased
3
+ ## 0.3.4 - Unreleased
4
+
5
+ - Added `fetch_remote` for tool-native discovery of unknown remote branch names: refresh one configured remote's branch cache (default `origin`) with optional, remote-tracking-only pruning (default false). Atomic explicit mappings disable configured refmaps, tags, tag pruning, submodules, and maintenance; symbolic destination checks include dangling loose refs, and unsupported non-files ref backends fail closed.
6
+ - Extended read-only `list_branches` with optional local/remote-tracking `kind` and bounded Git branch-list glob `patterns`, applied before output limits. Documented the sequential replacement for issue branch discovery via `git fetch origin --prune` and `git branch -r --list`.
4
7
 
5
8
  - `update_from_base` now lists every conflicting path in its text result (bounded, with an omitted count) and states whether the merge was aborted or kept.
6
9
  - Added optional `update_from_base.keepConflicts` (default `false`, unchanged behavior). When `true` and the merge conflicts, the merge is verified and left in progress (status `conflict_kept`, MERGE_HEAD equal to the captured base commit, HEAD unchanged, conflicted paths unmerged) instead of being aborted; if the conflict paths cannot be captured, the verified abort runs and the error says `keepConflicts` could not be honored.
@@ -25,7 +28,7 @@
25
28
  - 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.
26
29
  - 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.
27
30
  - Implemented the `branchme` informational slash command with help aliases.
28
- - Registered twenty strict BranchMe tools, including `list_branches`, `track_branch`, `update_from_base`, `conclude_merge`, and `pull_request_status`, alongside: `init_repository`, `branch_status`, `change_branch`, `fetch_branch`, `pull_branch`, `rebase_branch`, `integrate_branch`, `retire_branch`, `land_branch`, `create_branch`, `push_branch`, `pull_request`, `list_worktrees`, `create_worktree`, and `remove_worktree`; generic `continue_merge`/`abort_merge` tools are intentionally absent.
31
+ - Registered twenty-one strict BranchMe tools, including `fetch_remote`, `list_branches`, `track_branch`, `update_from_base`, `conclude_merge`, and `pull_request_status`, alongside: `init_repository`, `branch_status`, `change_branch`, `fetch_branch`, `pull_branch`, `rebase_branch`, `integrate_branch`, `retire_branch`, `land_branch`, `create_branch`, `push_branch`, `pull_request`, `list_worktrees`, `create_worktree`, and `remove_worktree`; generic `continue_merge`/`abort_merge` tools are intentionally absent.
29
32
  - 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.
30
33
  - Added `integrate_branch` for exact local source-to-target integration from a clean already-current target control worktree. It rejects branch-specific target merge options that could alter the fixed policy and supports already-integrated, fast-forward, verified normal merge-commit, and automatically aborted/restored conflict outcomes with before/after ref and ancestry proofs, while exposing no fetch, push, reset, merge-message, or continuation controls.
31
34
  - Added `retire_branch` for explicit deletion of one exact unoccupied local branch ref after full expected-`HEAD` matching and captured target-ancestry verification. It uses `git update-ref --no-deref -d` with an expected-old-value lease, requires explicit force authorization for unmerged history, leaves branch configuration and remote/remote-tracking refs untouched, and reports uncertain postconditions for manual inspection without reset rollback.
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  ---
18
18
 
19
- 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 twenty agent-callable tools that initialize a repository, refresh state, manage, integrate, and retire local branches, conclude a kept merge, inspect/create/remove linked worktrees, push the current branch, and create GitHub pull requests.
19
+ 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 twenty-one agent-callable tools that initialize a repository, refresh state, manage, integrate, and retire local branches, conclude a kept merge, inspect/create/remove linked worktrees, push the current branch, and create GitHub pull requests.
20
20
 
21
21
  <table align="center">
22
22
  <tr>
@@ -81,7 +81,16 @@ Refresh the repository state with branch_status, then create a branch named feat
81
81
 
82
82
  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.
83
83
 
84
- Use `list_branches` to discover local and cached remote-tracking branches, upstream ahead/behind counts, and worktree occupancy. To join an existing remote branch in the active checkout, use `track_branch` rather than creating from the wrong `HEAD`.
84
+ 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`.
85
+
86
+ For example, replace `git fetch origin --prune` followed by `git branch -r --list 'origin/feat/23' 'origin/feat/23-*'` with these **sequential** calls:
87
+
88
+ ```text
89
+ fetch_remote({ remote: "origin", prune: true })
90
+ list_branches({ kind: "remote-tracking", patterns: ["origin/feat/23", "origin/feat/23-*"] })
91
+ ```
92
+
93
+ 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.
85
94
 
86
95
  A typical BranchMe flow is:
87
96
 
@@ -193,7 +202,7 @@ $EDITOR .env
193
202
  pi
194
203
  ```
195
204
 
196
- `fetch_branch`, `pull_branch`, and `push_branch` use your normal Git remote credentials. BranchMe does not inject `GITHUB_TOKEN` into `git fetch`, `git pull`, or `git push`. `rebase_branch` operates on the locally available configured upstream ref and makes no network request itself.
205
+ `fetch_remote`, `fetch_branch`, `pull_branch`, and `push_branch` use your normal Git remote credentials. BranchMe does not inject `GITHUB_TOKEN` into `git fetch`, `git pull`, or `git push`. `rebase_branch` operates on the locally available configured upstream ref and makes no network request itself.
197
206
  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`.
198
207
  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.
199
208
 
@@ -243,7 +252,8 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
243
252
 
244
253
  | Tool | Schema | Behavior |
245
254
  | --- | --- | --- |
246
- | `list_branches` | `{}` | Read up to 200 local and cached remote-tracking branches, commits, current/upstream state, ahead/behind counts, symbolic refs, and worktree occupancy. 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. |
255
+ | `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. |
256
+ | `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. |
247
257
  | `track_branch` | `{ "branchName": string, "remote"?: string, "remoteBranch"?: 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. |
248
258
  | `update_from_base` | `{ "baseBranch": string, "remote"?: string, "keepConflicts"?: boolean }` | 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. |
249
259
  | `conclude_merge` | `{ "action": "conclude" \| "abort" }` | Finish or abandon the in-progress merge on the current checkout. `conclude` refuses default/custom-sized `<<<<<<<`, `|||||||`, `=======`, or `>>>>>>>` marker lines in unmerged working-tree paths and staged blobs (including CRLF and binary content, naming the paths), stages only the currently unmerged paths, rechecks the candidate index, commits with `git commit --no-edit`, and verifies MERGE_HEAD is gone and HEAD is a two-parent merge of the previous HEAD and MERGE_HEAD; unrelated unstaged changes stay unstaged. `abort` runs `git merge --abort` and verifies the restored HEAD, cleared operation state, and clean tree. Refuses when no merge is in progress. |
@@ -266,7 +276,7 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
266
276
 
267
277
  `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.
268
278
 
269
- All schemas reject additional properties. `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` 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. `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.
279
+ 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` 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. `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.
270
280
 
271
281
  ---
272
282
 
@@ -518,6 +528,7 @@ Ensure the token and Git credentials have permission for the branch and pull req
518
528
  | Linked-worktree agent cannot find credentials | Pass credentials through the process environment. BranchMe does not copy repository-root `.env` or other ignored/untracked files. |
519
529
  | Dirty worktree before branch switch, pull, rebase, or integration | Commit, stash, or discard changes outside BranchMe before using `change_branch`, `pull_branch`, `rebase_branch`, or a target control worktree for `integrate_branch`. |
520
530
  | Fetch, pull, or rebase has no upstream | Configure the current branch upstream outside BranchMe, then retry the tool. |
531
+ | 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. |
521
532
  | 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. |
522
533
  | Rebase fails or conflicts | `rebase_branch` automatically attempts `git rebase --abort`. Inspect repository state before continuing if automatic cleanup also fails. |
523
534
  | 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. |
@@ -553,7 +564,7 @@ npm run check:pack
553
564
  printf '/branchme help\n/quit\n' | pi --no-extensions -e .
554
565
  ```
555
566
 
556
- 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 all twenty BranchMe tools are 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`](docs/SMOKE_TEST.md), and TUI/help captures are stored in [`docs/TUI_CAPTURE.md`](docs/TUI_CAPTURE.md).
567
+ 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 all twenty-one BranchMe tools are 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`](docs/SMOKE_TEST.md), and TUI/help captures are stored in [`docs/TUI_CAPTURE.md`](docs/TUI_CAPTURE.md).
557
568
 
558
569
  Refresh TUI captures intentionally with:
559
570
 
package/SECURITY.md CHANGED
@@ -21,6 +21,7 @@ Implemented git mutations are limited to:
21
21
  - `track_branch`: narrow fetch followed by `git switch --no-overwrite-ignore --track=direct -c <branchName> refs/remotes/<remote>/<remoteBranch>`. Requires a new local branch, clean idle checkout, direct fetched commit ref, and verified final HEAD/upstream.
22
22
  - `update_from_base`: narrow fetch followed by the fixed integration merge policy against a captured remote-base commit. Reuses verified outcomes and automatic conflict abort; with explicit `keepConflicts: true` a conflicted merge is instead verified and left in progress (MERGE_HEAD equals the captured base commit, HEAD unchanged) for `conclude_merge`. Does not rewrite published history or change upstream. Both new fetch workflows explicitly disable pruning, extra ref mappings, tags, and submodule recursion, and reject symbolic remote-tracking destinations before fetching so Git cannot dereference them into unrelated refs. Base updates compare stored upstream configuration, not whether cached upstream refs happen to resolve.
23
23
  - `conclude_merge`: on the current checkout with MERGE_HEAD present, `action: "conclude"` lists unmerged paths with `git diff --name-only --diff-filter=U -z`, refuses if `git --literal-pathspecs grep` finds a default/custom-sized conflict marker in those working-tree files or staged changed blobs (including CRLF and binary content), then runs `git --literal-pathspecs add -- <those paths>` and checks the candidate index again before `git commit --no-edit`, then verifies MERGE_HEAD is gone and HEAD is an exact two-parent merge of the previous HEAD and MERGE_HEAD on the same branch. It never uses `commit -a`, never stages other paths, accepts no message, and runs repository hooks as Git normally would. `action: "abort"` runs `git merge --abort` and verifies the same restoration as the automatic abort path. It refuses multi-head merges before mutation, honors cancellation until staging begins, and reports lost commit responses as uncertain postconditions. The commit includes the entire index, including previously staged entries; callers must not stage unrelated edits during a merge. Marker-like lines are conservatively refused, and configured marker sizes above 4,096 are rejected.
24
+ - `fetch_remote`: atomic fetch of `+refs/heads/*:refs/remotes/<remote>/*` for one validated configured remote, with `--refmap=`, `--no-tags`, `--no-prune-tags`, `--no-recurse-submodules`, and `--no-auto-maintenance`. Explicit `--no-prune` is the default even when Git configuration enables pruning; optional `prune: true` adds `--prune` only within that remote's heads mapping. Configured extra fetch mappings, tag options, and tag-pruning settings cannot expand this scope. Rewritten tips may update cached remote-tracking refs; no local branch, tag, checkout, or upstream configuration is changed. A negative `^refs/heads/HEAD` refspec prevents writes through conventional remote HEAD aliases. Preflight rejects unsafe symbolic destinations, symlinked/malformed loose-ref namespaces, and overlapping configured remote names such as `origin` and `origin/team` so pruning cannot consume another remote's namespace. Since Git silently omits dangling symrefs from `for-each-ref`, bounded loose-ref inspection is also required, and non-files ref backends such as reftable are refused. The common-Git-directory queue serializes these fetches across linked worktrees; external processes can still race preflight. Fetch failure/cancellation is not a rollback guarantee: inspect refs before retrying.
24
25
  - `fetch_branch`: `git fetch --no-tags --no-recurse-submodules <remote> <remoteBranchRef>:<remoteTrackingRef>`. With no arguments it validates and fetches the current branch's configured upstream; with explicit `branch` and optional configured `remote` (default `origin`) it fetches that exact remote branch. The explicit destination is limited to the selected remote-tracking ref, so local branches and working-tree files are not changed.
25
26
  - `pull_branch`: `git pull --ff-only --no-rebase --no-autostash <upstreamRemote> <upstreamBranchRef>` for the clean current branch after validating its configured upstream target.
26
27
  - `rebase_branch`: `git rebase --no-autostash --no-update-refs <upstream>` for the clean current branch after validating its configured upstream target. It rewrites local commits and automatically attempts `git rebase --abort` without the cancelled caller signal if the rebase fails or is killed.
@@ -41,7 +42,7 @@ BranchMe does not force checkout/removal, stash, create user-authored commits, a
41
42
 
42
43
  ## Network behavior
43
44
 
44
- `track_branch`, `update_from_base`, `fetch_branch`, `pull_branch`, `land_branch`, and `push_branch` contact the configured Git remote through the user's normal Git transport and credentials. They do not use or inject `GITHUB_TOKEN` or `GH_TOKEN`. `rebase_branch`, `integrate_branch`, and `retire_branch` use locally available refs; BranchMe's direct retirement argv never fetches, pulls, pushes, or names a remote or remote-tracking ref for deletion.
45
+ `track_branch`, `update_from_base`, `fetch_remote`, `fetch_branch`, `pull_branch`, `land_branch`, and `push_branch` contact the configured Git remote through the user's normal Git transport and credentials. They do not use or inject `GITHUB_TOKEN` or `GH_TOKEN`. `rebase_branch`, `integrate_branch`, and `retire_branch` use locally available refs; BranchMe's direct retirement argv never fetches, pulls, pushes, or names a remote or remote-tracking ref for deletion.
45
46
 
46
47
  Git extension points are a separate trust boundary. `integrate_branch` preserves repository-configured hooks, custom merge drivers, clean/smudge filters, and signature policy; it does not pass `--no-verify`. Retirement's `git update-ref` can invoke repository-configured `reference-transaction` hooks. Those configurations may execute arbitrary local commands, mutate other state, or contact networks under the user's identity. BranchMe's fixed argv, direct no-network contract, and direct no-remote-delete guarantees cannot constrain hook behavior.
47
48
 
@@ -58,7 +59,7 @@ GET https://api.github.com/repos/{owner}/{repo}/pulls?state={open|all}&head={ow
58
59
 
59
60
  Explicit `pull_request_status`, idempotent `pull_request` lookup, and PR-aware `land_branch` use the last two read-only endpoints. Each request has a 10-second transport/body deadline and 64 KiB response limit. Exact PR number, repository, branch identities, and commit IDs are validated. Status does not certify CI checks or review approvals. Idempotent creation preserves existing PR fields and reuses only an exact head-SHA/base match; HTTP 422 races get one read-only recheck. All of these operations require `GITHUB_TOKEN` or `GH_TOKEN` from the process environment or verified-root `.env` fallback.
60
61
 
61
- `list_branches` is an explicit read-only local inventory, limited to 200 returned refs, 128 KiB raw ref output, and 4,000 characters of text. Paths and names are display-safe metadata, not executable handoffs. Upstream counts reflect cached refs, not a fresh remote fetch.
62
+ `list_branches` is an explicit read-only local inventory, limited to 200 returned refs, 128 KiB raw ref output, and 4,000 characters of text. Optional `kind` selects local or remote-tracking refs; optional `patterns` accepts 1–25 nonblank, control-free Git branch-list glob patterns of at most 512 characters, matched before output limits and display redaction. Patterns are argv values after `--`, never shell code or refspecs. Paths and names are display-safe metadata, not executable handoffs. Upstream counts reflect cached refs, not a fresh remote fetch; call `fetch_remote` separately when fresh discovery is needed.
62
63
 
63
64
  The first request is an automatic network boundary: it can run before each agent run and whenever `branch_status` explicitly refreshes context. It is a bounded, read-only lookup for one open pull request whose head is the current local branch (`per_page=1`), with a default 4-second timeout and a 64 KiB response-body limit. It is skipped when repository/branch resolution or authentication is unavailable, so BranchMe never makes an unauthenticated fallback request. Timeout, HTTP, network, malformed, and oversized-response failures become a safe unavailable state without exposing response bodies or raw network errors. Git alone is not used or claimed to provide PR metadata.
64
65
 
@@ -76,6 +77,7 @@ The branch preflight requests have no body. BranchMe uses the resolved `headBran
76
77
  - `remove_worktree` requires `worktreePath`, accepts optional boolean `deleteIgnored`, requires an exact canonical match in a fresh current-repository inventory, removes no branch, and does not accept force.
77
78
  - `retire_branch` accepts exactly `branchName`, a full 40- or 64-hex-character `expectedHead`, a distinct exact local `targetBranch`, and boolean `force`. Both names must resolve to direct local refs. Paths, repositories, remotes, remote-only or full refs, refspecs, patterns, arrays, inferred targets, worktree controls, and bulk deletion are rejected.
78
79
  - `change_branch` accepts only `branchName` and never creates branches, checks out remote branches, forces, stashes, or discards changes.
80
+ - `fetch_remote` accepts only optional configured `remote` and boolean `prune`, not URLs, arbitrary refspecs, force, tag, or checkout controls. Only explicit `prune: true` deletes stale remote-tracking branch refs; local branches, tags, and other remotes remain untouched.
79
81
  - `fetch_branch` accepts optional `branch` and `remote` (remote requires branch), otherwise resolves the current branch's configured upstream. It constructs the source-to-remote-tracking refspec internally and disables tag fetching and submodule recursion; no arbitrary refspec or prune controls are exposed.
80
82
  - `pull_branch` accepts no parameters, updates only the clean current branch from its configured upstream, and uses fast-forward-only semantics.
81
83
  - `rebase_branch` accepts no parameters, rebases only the clean current branch onto its configured upstream, disables autostash and multi-ref updates, never pushes, and attempts to abort on failure.
@@ -15,7 +15,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
15
15
  - Exported extension function: `branchMeExtension`
16
16
  - Repository URL: `https://github.com/senad-d/branchme`
17
17
  - One-sentence pitch: Verified Pi tools for Git repository initialization plus current-repository branch, integration, retirement, linked-worktree, push, and GitHub pull request workflows.
18
- - Tool count: twenty strict agent-callable tools.
18
+ - Tool count: twenty-one strict agent-callable tools.
19
19
 
20
20
  ## 3. Users and use cases
21
21
 
@@ -25,7 +25,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
25
25
  - Inspect bounded current-repository branch, upstream, working-tree, related-PR, and recent-commit state, with an optional explicit local source/target ancestry proof.
26
26
  - Integrate one exact existing local source branch into the already-current clean local target, returning verified no-op, fast-forward, merge-commit, or restored-conflict details.
27
27
  - List the current repository's main and linked worktrees explicitly.
28
- - Discover local/remote-tracking branches, upstream counts, and worktree occupancy with `list_branches`.
28
+ - Refresh unknown remote branch names with `fetch_remote` and optional remote-tracking-only pruning, then discover issue branches, upstream counts, and worktree occupancy with filtered `list_branches`.
29
29
  - Join an existing remote branch with verified `track_branch`, or merge a fresh remote base into the current feature with `update_from_base`; keep a conflicted merge in progress with `keepConflicts: true` and commit or abort it with `conclude_merge` after a file-editing step removes the markers.
30
30
  - Create and verify a linked worktree from current `HEAD`, optional read-only `baseRef`, or an unoccupied existing local branch.
31
31
  - Return an exact, absolute, machine-readable worktree handoff for a caller-managed separate Pi session or subagent.
@@ -40,7 +40,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
40
40
  - Non-goals:
41
41
  - No staging, direct working-tree edits, user-authored commits, commit-message input/generation, diff generation, stashing, resets, or force pushes. Explicit `integrate_branch` may let Git create its standard merge commit for divergent histories.
42
42
  - No automatic Pi cwd changes, process/session creation, or copying of `.env` and other ignored/untracked files into linked worktrees.
43
- - No bulk or pattern branch deletion, inferred retirement targets, remote or remote-tracking deletion, automatic worktree removal during retirement, or reset-based retirement rollback.
43
+ - No bulk or pattern local-branch deletion, inferred retirement targets, server-side remote deletion, arbitrary remote-tracking deletion, automatic worktree removal during retirement, or reset-based retirement rollback. Explicit `fetch_remote.prune` only removes stale cached branch refs for one selected remote.
44
44
  - No GitHub CLI dependency or cross-repository pull requests.
45
45
  - No labels, reviewers, projects, or issue-linking behavior.
46
46
 
@@ -50,7 +50,8 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
50
50
  | --- | --- | --- | --- |
51
51
  | Command | `/branchme` | Compact TUI status and workflow panel | Informational; no Git or GitHub mutations |
52
52
  | Command | `/branchme help` | Runtime requirements and workflow guidance | Informational; no actions |
53
- | Tool | `list_branches` | Discover local and cached remote-tracking refs | Bounded, read-only; includes upstream counts and worktree occupancy |
53
+ | Tool | `list_branches` | Discover local and cached remote-tracking refs | Bounded, read-only; optional kind/glob patterns filter before limits; includes upstream counts and occupancy |
54
+ | Tool | `fetch_remote` | Refresh one remote's full branch cache | Optional stale tracking-ref pruning; no local-ref, tag, checkout, or upstream changes; files ref backend required |
54
55
  | Tool | `track_branch` | Join an existing remote branch | Narrow fetch, clean idle checkout, verified HEAD/upstream |
55
56
  | Tool | `update_from_base` | Fetch and merge an explicit remote base | Preserves published history; fixed merge policy, listed conflict paths, automatic abort unless `keepConflicts` |
56
57
  | Tool | `conclude_merge` | Commit or abort the in-progress kept merge | Refuses remaining markers; stages only formerly unmerged paths; `commit --no-edit`; verified two-parent result or verified abort |
@@ -86,6 +87,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
86
87
  - `src/tools/branchme-tools.ts`
87
88
  - `src/tools/workflow-tools.ts`
88
89
  - `src/git.ts`
90
+ - `src/git-discovery.ts`
89
91
  - `src/git-workflow.ts`
90
92
  - `src/git-integration.ts`
91
93
  - `src/git-retirement.ts`
@@ -93,7 +95,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
93
95
  - `src/github.ts`
94
96
  - `src/ui/branchme-panel.ts`
95
97
  - Module boundaries:
96
- - The extension entry point registers the informational command, twenty tools, and automatic context hook.
98
+ - The extension entry point registers the informational command, twenty-one tools, and automatic context hook.
97
99
  - `src/git-workflow.ts` and `src/tools/workflow-tools.ts` own verified remote tracking, base updates, and the new PR status registration.
98
100
  - The context module owns bounded read-only collection, prompt formatting, targeted ancestry rendering, and the `before_agent_start` hook; automatic context never runs ancestry queries.
99
101
  - The command and UI modules own mode-safe informational help/status behavior and never invoke mutations.
@@ -23,9 +23,9 @@ pi --no-extensions -e .
23
23
  ## Automated smoke behavior
24
24
 
25
25
  - `npm run smoke:pi` first runs isolated checkout Pi processes from a temporary non-Git working directory: one with `pi --no-extensions -e <package> -e <temporary verifier>` and `/branchmeverify verify`, then one with `pi --no-extensions -e <package>` and `/branchme help`.
26
- - The temporary command verifier calls `pi.getAllTools()` after BranchMe loads and confirms exactly twenty tools—`list_branches`, `track_branch`, `update_from_base`, `conclude_merge`, `pull_request_status`, `branch_status`, `init_repository`, `change_branch`, `create_branch`, `create_worktree`, `fetch_branch`, `integrate_branch`, `land_branch`, `list_worktrees`, `pull_branch`, `pull_request`, `push_branch`, `rebase_branch`, `remove_worktree`, and `retire_branch`—are each registered exactly once and active with strict schemas, named prompt guidelines, descriptions, and extension source metadata. It also proves `continue_merge` and `abort_merge` are absent.
26
+ - The temporary command verifier calls `pi.getAllTools()` after BranchMe loads and confirms exactly twenty-one tools—`fetch_remote`, `list_branches`, `track_branch`, `update_from_base`, `conclude_merge`, `pull_request_status`, `branch_status`, `init_repository`, `change_branch`, `create_branch`, `create_worktree`, `fetch_branch`, `integrate_branch`, `land_branch`, `list_worktrees`, `pull_branch`, `pull_request`, `push_branch`, `rebase_branch`, `remove_worktree`, and `retire_branch`—are each registered exactly once and active with strict schemas, named prompt guidelines, descriptions, and extension source metadata. It also proves `continue_merge` and `abort_merge` are absent.
27
27
  - Runtime schema inspection verifies that `integrate_branch` has exactly the required `sourceBranch` and `targetBranch` fields and that `retire_branch` has exactly the required `branchName`, `expectedHead`, `targetBranch`, and `force` fields with `additionalProperties: false`. It also verifies that `branch_status` has only an optional top-level `ancestry` object whose required nested `sourceBranch` and `targetBranch` fields reject additional nested or top-level properties.
28
- - A second temporary verifier registers a deterministic local smoke model, blocks `fetch`, and runs normal prompts through real Pi lifecycle handling. It verifies automatic no-tool Git context in a temporary repository, one real `branch_status` tool refresh after a verifier-created local change, safe credential-free related-PR status with no request, and non-Git startup fallback. It expects all twenty tools but forbids `land_branch`, `retire_branch`, `integrate_branch`, `conclude_merge`, and every remote or worktree mutation tool from executing.
28
+ - A second temporary verifier registers a deterministic local smoke model, blocks `fetch`, and runs normal prompts through real Pi lifecycle handling. It verifies automatic no-tool Git context in a temporary repository, one real `branch_status` tool refresh after a verifier-created local change, safe credential-free related-PR status with no request, and non-Git startup fallback. It expects all twenty-one tools but forbids `land_branch`, `retire_branch`, `integrate_branch`, `conclude_merge`, and every remote or worktree mutation tool (including `fetch_remote`) from executing.
29
29
  - The Pi runtime smoke validates worktree, integration, and retirement tool registration, strict schemas, and prompt contracts only; it never creates or removes a worktree, runs a merge, or retires a branch. Real-Git lifecycle coverage runs under `npm run test` in isolated temporary local repositories with no remote contact.
30
30
  - Isolated real-Git integration tests cover `already_integrated`, `fast_forward`, exact two-parent `merge_commit`, and conflict-path capture followed by verified automatic abort/restoration. They also cover target mismatch, dirty control state, rejection of branch-specific target merge options, unrelated histories, ignored-file overwrite protection, preserved repository hooks, and a committed source ref checked out in another dirty linked worktree. The recorded merge argv proves autostash and rerere are disabled, ignored-file protection is enabled, and `--no-verify` is absent.
31
31
  - Isolated real-Git retirement tests cover merged and explicitly forced-unmerged local-ref deletion, stale expected-`HEAD` rejection, current and linked-worktree occupancy, dirty unrelated worktrees, non-current targets, and preservation of target refs, remote-tracking refs, worktrees, working-tree files, and `branch.<name>.*` configuration without contacting a remote. Recorded argv proves retirement uses `update-ref --no-deref -d` with the captured expected commit and never uses `git branch -d/-D`, fetch, push, or a remote ref target.
package/docs/STRUCTURE.md CHANGED
@@ -16,6 +16,7 @@ src/
16
16
  │ ├── branchme-tools.ts # main registration, including branch discovery
17
17
  │ └── workflow-tools.ts # tracking, base update, and PR status registration
18
18
  ├── git.ts # shared argv-style Git primitives and per-repo mutation queue
19
+ ├── git-discovery.ts # bounded remote cache refresh, optional pruning, symbolic-ref safety
19
20
  ├── git-workflow.ts # narrow fetch, tracking checkout, and feature-base update
20
21
  ├── git-integration.ts # integration preflight, merge, cleanup, and verification state machine
21
22
  ├── git-retirement.ts # leased local-ref retirement and postcondition state machine
@@ -27,7 +28,7 @@ src/
27
28
 
28
29
  ## Module boundaries
29
30
 
30
- 1. `src/extension.ts` stays small and registers the command, twenty tools, and one `before_agent_start` context hook.
31
+ 1. `src/extension.ts` stays small and registers the command, twenty-one tools, and one `before_agent_start` context hook.
31
32
  2. `src/git-context.ts` owns the shared read-only collector, escaped/bounded formatter, automatic system-prompt append, and the current-state output used by `branch_status`.
32
33
  3. `src/commands/branchme-command.ts` parses `/branchme`, `/branchme help`, `--help`, and `-h`; it never performs git or GitHub mutations and avoids raw stdout in JSON mode.
33
34
  4. `src/tools/branchme-tools.ts` owns strict TypeBox schemas, prompt metadata, bounded tool content, and safe structured details. `branch_status` delegates to the shared context collector and optional ancestry verifier; `integrate_branch` and `retire_branch` delegate to focused mutation state machines; worktree tools expose explicit inventory and verified handoff operations.
@@ -41,6 +42,8 @@ src/
41
42
  12. `src/git-workflow.ts` owns tracking and base-update preflight, narrow fetching, and verified checkout/integration including the `keepConflicts` pass-through. `src/tools/workflow-tools.ts` registers those tools, `conclude_merge`, and explicit PR status.
42
43
  13. `src/git-landing.ts` owns execution-only `land_branch` orchestration and receipt types. It resolves the primary checkout, routes every Git call with `-C`, and reuses queue-free targeted fetch/removal/retirement helpers under one primary-root mutation window. Remote ancestry or exact merged-PR evidence gates cleanup; final target sync independently reports exact ref observations. No startup work or persistent landing state is added.
43
44
 
45
+ 14. `src/git-discovery.ts` owns configured-remote-wide branch-cache refresh and optional stale tracking-ref pruning with explicit internal mappings, atomic ref updates, bounded files-backend destination inspection (including dangling symrefs), and active-root/common-directory queues. It never checks out or changes local refs or upstream settings.
46
+
44
47
  ## Pi extension conventions
45
48
 
46
49
  - No long-lived processes, watchers, timers, sockets, or background jobs start in the extension factory.
@@ -62,6 +65,7 @@ src/
62
65
  - 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.
63
66
  - `init_repository` canonicalizes pi's exact current directory, rejects filesystem root, existing `.git` entries, reinitialization, and nesting inside another repository, then runs `git init --no-template --initial-branch <name>`. It verifies the exact non-bare repository root, in-place `.git` directory, requested unborn branch, and absence of a commit. It accepts no path or repository-mode controls and performs no automatic cleanup after an uncertain failure.
64
67
  - `change_branch` mutates local HEAD and working-tree files only through `git switch <branchName>` for existing local branches after a clean-worktree preflight.
68
+ - `fetch_remote` refreshes all heads for one configured remote into only its tracking namespace. Pruning is opt-in, and configured refmaps, tags, tag pruning, submodules, and maintenance are disabled. Conventional same-namespace HEAD aliases are protected by excluding the remote branch named HEAD; unsafe or dangling symbolic destinations are refused, and non-files ref backends fail closed. Active-root locking interoperates with existing same-checkout tools; a common-directory lock additionally serializes `fetch_remote` calls across linked trees. External Git processes can still race preflight.
65
69
  - `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.
66
70
  - `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.
67
71
  - `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.
@@ -82,7 +86,7 @@ src/
82
86
  - `pull_request` reads `GITHUB_TOKEN` or `GH_TOKEN` from process environment first; only when neither process token is set does it read those token keys from a small regular `.env` file in the verified git root as a fallback. `BRANCHME_PR_AUTOFILL` uses the same process-first, `.env`-fallback precedence and defaults off.
83
87
  - BranchMe does not force checkout/removal, move/prune/repair/lock/unlock worktrees, create detached/orphan worktrees, infer remote worktree branches, copy ignored/untracked files such as `.env`, delete ignored worktree residue through standalone `remove_worktree` without explicit `deleteIgnored: true` authorization, delete retained worktree branches during standalone removal, change Pi's cwd, or start Pi sessions. It also does not stash, create user-authored commits, accept commit messages, reset, force-push, directly edit project files, read unsupported `.env` keys, follow unsafe `.env` file types, depend on GitHub CLI, or collect telemetry. Explicit `integrate_branch`, `update_from_base`, and `conclude_merge` may let Git create a standard merge commit for divergent histories; `conclude_merge` is the only tool that stages, and only the formerly unmerged paths; only explicit leased `retire_branch` or merged-only `land_branch` may delete one exact local branch ref. Retirement has no bulk, inferred-target, remote, remote-tracking, rollback, or automatic worktree deletion. Git documents submodule worktree support as incomplete; BranchMe adds no force-based submodule cleanup.
84
88
 
85
- `list_branches` returns bounded display metadata, including cached upstream counts and occupancy; automatic context is unchanged. `pull_request_status` reads exact-number or latest-head PR state, not CI/review requirements. `pull_request` reuses matching open PRs without overwriting their metadata. PR-aware landing verifies repository/remote identity, exact merged head/base, and merge-commit containment before allowing non-ancestor local retirement; it retains the expected-HEAD lease and reports graph ancestry separately from host evidence.
89
+ `list_branches` returns bounded display metadata, including cached upstream counts and occupancy; optional kind and Git branch-list patterns filter before raw/entry limits and display redaction. `{}` preserves the full cached inventory, and automatic context is unchanged. For unknown issue branches, run `fetch_remote` separately and wait before listing. `pull_request_status` reads exact-number or latest-head PR state, not CI/review requirements. `pull_request` reuses matching open PRs without overwriting their metadata. PR-aware landing verifies repository/remote identity, exact merged head/base, and merge-commit containment before allowing non-ancestor local retirement; it retains the expected-HEAD lease and reports graph ancestry separately from host evidence.
86
90
 
87
91
  ## Documentation
88
92
 
@@ -34,7 +34,10 @@ Commands only show info; BranchMe tools perform actions.
34
34
 
35
35
  ## Discovery and PR lifecycle
36
36
 
37
- - `list_branches` — discover local/cached remote refs, upstream counts, and worktree occupancy without fetching.
37
+ - `list_branches` — discover local/cached remote refs, upstream counts, and occupancy; optional `kind` and Git glob `patterns` filter before limits.
38
+ - `fetch_remote` — refresh all branch refs for one configured remote (default origin); optional `prune: true` deletes only its stale cached branch refs. Wait before `list_branches`.
39
+ - Issue discovery: `fetch_remote({ remote: "origin", prune: true })`, then `list_branches({ kind: "remote-tracking", patterns: ["origin/feat/23", "origin/feat/23-*"] })`.
40
+ - `fetch_remote` preserves local branches, tags, files, and upstream settings; requires the files ref backend and refuses unsafe symbolic destinations.
38
41
  - `track_branch` — fetch an existing remote branch and verify a new clean local tracking checkout.
39
42
  - `update_from_base` — fetch and merge an explicit base into the current clean feature, preserving published history and upstream; a conflict is aborted unless `keepConflicts: true` leaves it in progress.
40
43
  - `conclude_merge` — `action: "conclude"` commits the kept merge once every conflict marker is removed; `action: "abort"` restores the branch.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senad-d/branchme",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Pi extension for verified Git repository initialization, branch, worktree, integration, retirement, push, and pull request workflows.",
6
6
  "license": "MIT",
@@ -43,7 +43,10 @@ export function getBranchMeHelpText(): string {
43
43
  "",
44
44
  "## Discovery and PR lifecycle",
45
45
  "",
46
- "- `list_branches` — discover local/cached remote refs, upstream counts, and worktree occupancy without fetching.",
46
+ "- `list_branches` — discover local/cached remote refs, upstream counts, and occupancy; optional `kind` and Git glob `patterns` filter before limits.",
47
+ "- `fetch_remote` — refresh all branch refs for one configured remote (default origin); optional `prune: true` deletes only its stale cached branch refs. Wait before `list_branches`.",
48
+ "- Issue discovery: `fetch_remote({ remote: \"origin\", prune: true })`, then `list_branches({ kind: \"remote-tracking\", patterns: [\"origin/feat/23\", \"origin/feat/23-*\"] })`.",
49
+ "- `fetch_remote` preserves local branches, tags, files, and upstream settings; requires the files ref backend and refuses unsafe symbolic destinations.",
47
50
  "- `track_branch` — fetch an existing remote branch and verify a new clean local tracking checkout.",
48
51
  "- `update_from_base` — fetch and merge an explicit base into the current clean feature, preserving published history and upstream; a conflict is aborted unless `keepConflicts: true` leaves it in progress.",
49
52
  "- `conclude_merge` — `action: \"conclude\"` commits the kept merge once every conflict marker is removed; `action: \"abort\"` restores the branch.",
package/src/constants.ts CHANGED
@@ -10,6 +10,7 @@ export const INIT_REPOSITORY_TOOL_NAME = "init_repository";
10
10
  export const CREATE_BRANCH_TOOL_NAME = "create_branch";
11
11
  export const CHANGE_BRANCH_TOOL_NAME = "change_branch";
12
12
  export const FETCH_BRANCH_TOOL_NAME = "fetch_branch";
13
+ export const FETCH_REMOTE_TOOL_NAME = "fetch_remote";
13
14
  export const PULL_BRANCH_TOOL_NAME = "pull_branch";
14
15
  export const REBASE_BRANCH_TOOL_NAME = "rebase_branch";
15
16
  export const PUSH_BRANCH_TOOL_NAME = "push_branch";
@@ -33,6 +34,7 @@ export const BRANCHME_TOOL_NAMES = [
33
34
  CREATE_BRANCH_TOOL_NAME,
34
35
  CHANGE_BRANCH_TOOL_NAME,
35
36
  FETCH_BRANCH_TOOL_NAME,
37
+ FETCH_REMOTE_TOOL_NAME,
36
38
  PULL_BRANCH_TOOL_NAME,
37
39
  REBASE_BRANCH_TOOL_NAME,
38
40
  INTEGRATE_BRANCH_TOOL_NAME,
@@ -65,6 +67,7 @@ export const PULL_REQUEST_AUTOFILL_COMMIT_LIMIT = 20;
65
67
  export const PULL_REQUEST_AUTOFILL_SUBJECT_LIMIT_CHARS = 256;
66
68
  export const GIT_BRANCH_RAW_OUTPUT_LIMIT_BYTES = 128 * 1024;
67
69
  export const GIT_BRANCH_ENTRY_LIMIT = 200;
70
+ export const GIT_BRANCH_PATTERN_LIMIT = 25;
68
71
  export const GIT_BRANCH_SUMMARY_LIMIT_CHARS = 4_000;
69
72
  export const GIT_WORKTREE_RAW_OUTPUT_LIMIT_BYTES = 128 * 1024;
70
73
  export const GIT_WORKTREE_ENTRY_LIMIT = 100;
@@ -0,0 +1,231 @@
1
+ import type { Dirent } from "node:fs";
2
+ import { lstat, readdir, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ GIT_BRANCH_RAW_OUTPUT_LIMIT_BYTES,
7
+ GIT_CONTEXT_VALUE_LIMIT_CHARS,
8
+ GIT_FETCH_TIMEOUT_MS,
9
+ GIT_WORKTREE_PATH_LIMIT_CHARS,
10
+ MAX_SUMMARY_OUTPUT_CHARS,
11
+ } from "./constants.ts";
12
+ import {
13
+ getCanonicalCommonGitDirectory,
14
+ getGitRoot,
15
+ isLosslessGitMetadata,
16
+ runGit,
17
+ safeWorktreeValue,
18
+ withRepositoryMutationQueue,
19
+ type GitCommandContext,
20
+ } from "./git.ts";
21
+ import type { FetchRemoteDetails } from "./types.ts";
22
+
23
+ function validateRemoteDiscoveryInput(remote: unknown, prune: unknown): asserts remote is string {
24
+ if (typeof remote !== "string" || !remote || remote === "." || remote.startsWith("-") ||
25
+ /[\s:@\p{Cc}\p{Cf}]/u.test(remote) ||
26
+ !isLosslessGitMetadata(remote, GIT_CONTEXT_VALUE_LIMIT_CHARS)) {
27
+ throw new TypeError("fetch_remote remote must be a safe configured remote name, not a URL, option, or local repository.");
28
+ }
29
+ if (typeof prune !== "boolean") throw new TypeError("fetch_remote prune must be a boolean.");
30
+ }
31
+
32
+ function requireSafeSymbolicDestination(ref: string, target: string, prefix: string): void {
33
+ // Clones commonly have origin/HEAD -> origin/main. All other symbolic
34
+ // destinations are refused to prevent wildcard fetch/prune following aliases.
35
+ if (target && (ref !== `${prefix}HEAD` || !target.startsWith(prefix) ||
36
+ target.length === prefix.length || target === `${prefix}HEAD`)) {
37
+ throw new Error("fetch_remote refuses unsafe symbolic remote-tracking destinations; no fetch ran.");
38
+ }
39
+ }
40
+
41
+ async function findLooseRemoteDirectory(commonGitDir: string, remote: string): Promise<string | undefined> {
42
+ return inspectRemoteAncestors(commonGitDir, ["refs", "remotes", ...remote.split("/")]);
43
+ }
44
+
45
+ async function inspectRemoteAncestors(parent: string, segments: string[]): Promise<string | undefined> {
46
+ if (segments.length === 0) return parent;
47
+ const directory = join(parent, segments[0]);
48
+ let info;
49
+ try {
50
+ info = await lstat(directory);
51
+ } catch (error) {
52
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
53
+ throw new Error("fetch_remote could not inspect its loose ref namespace; no fetch ran.");
54
+ }
55
+ if (!info.isDirectory() || info.isSymbolicLink()) {
56
+ throw new Error("fetch_remote refuses non-directory or symlinked ref namespaces; no fetch ran.");
57
+ }
58
+ return inspectRemoteAncestors(directory, segments.slice(1));
59
+ }
60
+
61
+ async function inspectLooseRemoteFile(path: string, ref: string, prefix: string): Promise<void> {
62
+ if ((await lstat(path)).size > GIT_CONTEXT_VALUE_LIMIT_CHARS + 64) {
63
+ throw new Error("fetch_remote refuses unsafe loose ref files; no fetch ran.");
64
+ }
65
+ const content = (await readFile(path, "utf8")).trimEnd();
66
+ if (content.startsWith("ref: ")) {
67
+ requireSafeSymbolicDestination(ref, content.slice(5), prefix);
68
+ } else if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(content)) {
69
+ throw new Error("fetch_remote received malformed loose ref metadata; no fetch ran.");
70
+ }
71
+ }
72
+
73
+ async function* looseRemoteEntries(
74
+ directory: string,
75
+ prefix: string,
76
+ ): AsyncGenerator<{ entry: Dirent; ref: string; path: string }> {
77
+ const entries = await readdir(directory, { withFileTypes: true });
78
+ for (const entry of entries) {
79
+ const ref = `${prefix}${entry.name}`;
80
+ const path = join(directory, entry.name);
81
+ yield { entry, ref, path };
82
+ // Traverse only after the caller has checked the inventory budget.
83
+ if (entry.isDirectory()) yield* looseRemoteEntries(path, `${ref}/`);
84
+ }
85
+ }
86
+
87
+ async function inspectLooseRemoteRefs(commonGitDir: string, remote: string): Promise<void> {
88
+ const prefix = `refs/remotes/${remote}/`;
89
+ const directory = await findLooseRemoteDirectory(commonGitDir, remote);
90
+ if (directory === undefined) return;
91
+ // for-each-ref silently omits dangling symrefs. Inspect the files backend too,
92
+ // so even an alias to an absent local branch cannot become a fetch destination.
93
+ let inventoryBytes = 0;
94
+ for await (const { entry, ref, path } of looseRemoteEntries(directory, prefix)) {
95
+ inventoryBytes += Buffer.byteLength(ref, "utf8");
96
+ if (inventoryBytes > GIT_BRANCH_RAW_OUTPUT_LIMIT_BYTES) {
97
+ throw new Error("fetch_remote loose ref inventory exceeded the safety limit; no fetch ran.");
98
+ }
99
+ if (entry.isDirectory()) continue;
100
+ if (!entry.isFile()) {
101
+ throw new Error("fetch_remote refuses unsafe loose ref files; no fetch ran.");
102
+ }
103
+ await inspectLooseRemoteFile(path, ref, prefix);
104
+ }
105
+ }
106
+
107
+ async function requireUnambiguousRemoteNamespace(
108
+ pi: Pick<ExtensionAPI, "exec">,
109
+ ctx: GitCommandContext,
110
+ remote: string,
111
+ signal?: AbortSignal,
112
+ ): Promise<void> {
113
+ const result = await runGit(pi, ctx, ["remote"], { signal });
114
+ if (Buffer.byteLength(result.stdout, "utf8") > GIT_BRANCH_RAW_OUTPUT_LIMIT_BYTES) {
115
+ throw new Error("fetch_remote configured remote inventory exceeded the safety limit; no fetch ran.");
116
+ }
117
+ for (const other of result.stdout.trimEnd().split("\n")) {
118
+ if (other !== remote && (other.startsWith(`${remote}/`) || remote.startsWith(`${other}/`))) {
119
+ throw new Error("fetch_remote refuses overlapping configured remote namespaces; no fetch ran.");
120
+ }
121
+ }
122
+ }
123
+
124
+ async function requireSafeRemoteDestinations(
125
+ pi: Pick<ExtensionAPI, "exec">,
126
+ ctx: GitCommandContext,
127
+ remote: string,
128
+ commonGitDir: string,
129
+ signal?: AbortSignal,
130
+ ): Promise<void> {
131
+ const prefix = `refs/remotes/${remote}/`;
132
+ await runGit(pi, ctx, ["check-ref-format", `${prefix}branchme-validation`], { signal });
133
+ const configured = await runGit(pi, ctx, ["remote", "get-url", remote], { signal, allowFailure: true });
134
+ if (configured.code !== 0) throw new Error("fetch_remote remote is not a configured Git remote.");
135
+ await requireUnambiguousRemoteNamespace(pi, ctx, remote, signal);
136
+ const storage = await runGit(pi, ctx, ["config", "--get", "extensions.refStorage"], { signal, allowFailure: true });
137
+ if ((storage.code !== 0 && storage.code !== 1) ||
138
+ (storage.code === 0 && storage.stdout.trimEnd() !== "files")) {
139
+ throw new Error("fetch_remote requires Git's files ref backend to verify dangling symbolic destinations; no fetch ran.");
140
+ }
141
+ await inspectLooseRemoteRefs(commonGitDir, remote);
142
+
143
+ const result = await runGit(pi, ctx, [
144
+ "for-each-ref", "--format=%(refname)%00%(symref)", prefix,
145
+ ], { signal });
146
+ if (Buffer.byteLength(result.stdout, "utf8") > GIT_BRANCH_RAW_OUTPUT_LIMIT_BYTES) {
147
+ throw new Error("fetch_remote destination inventory exceeded the safety limit; no fetch ran.");
148
+ }
149
+ const records = result.stdout === "" ? [] : result.stdout.trimEnd().split("\n");
150
+ for (const record of records) {
151
+ const fields = record.split("\0");
152
+ const [ref, target] = fields;
153
+ if (fields.length !== 2 || !ref.startsWith(prefix) || ref.length === prefix.length) {
154
+ throw new Error("fetch_remote received malformed destination metadata; no fetch ran.");
155
+ }
156
+ requireSafeSymbolicDestination(ref, target, prefix);
157
+ }
158
+ }
159
+
160
+ async function fetchRemoteWithinQueue(
161
+ pi: Pick<ExtensionAPI, "exec">,
162
+ ctx: GitCommandContext,
163
+ remote: string,
164
+ prune: boolean,
165
+ commonGitDir: string,
166
+ signal?: AbortSignal,
167
+ ): Promise<FetchRemoteDetails> {
168
+ try {
169
+ await requireSafeRemoteDestinations(pi, ctx, remote, commonGitDir, signal);
170
+ } catch (error) {
171
+ const reason = error instanceof Error ? error.message : String(error);
172
+ throw new Error(safeWorktreeValue(reason, MAX_SUMMARY_OUTPUT_CHARS));
173
+ }
174
+ const refspec = `+refs/heads/*:refs/remotes/${remote}/*`;
175
+ let result;
176
+ try {
177
+ result = await runGit(pi, ctx, [
178
+ "fetch", "--atomic", "--no-tags", "--no-prune-tags", "--no-recurse-submodules",
179
+ "--no-auto-maintenance", "--refmap=", prune ? "--prune" : "--no-prune",
180
+ "--", remote, refspec, "^refs/heads/HEAD",
181
+ ], { signal, timeout: GIT_FETCH_TIMEOUT_MS });
182
+ } catch (error) {
183
+ const reason = error instanceof Error ? error.message : String(error);
184
+ const diagnostic = safeWorktreeValue(reason, MAX_SUMMARY_OUTPUT_CHARS - 200);
185
+ throw new Error(
186
+ `fetch_remote did not complete: ${diagnostic}. Remote-tracking refs may have changed; inspect before retrying. No rollback was attempted.`,
187
+ );
188
+ }
189
+ return {
190
+ action: "fetch_remote",
191
+ repoRoot: safeWorktreeValue(ctx.cwd, GIT_WORKTREE_PATH_LIMIT_CHARS),
192
+ remote,
193
+ prune,
194
+ refspec,
195
+ output: safeWorktreeValue(result.stdout || result.stderr, MAX_SUMMARY_OUTPUT_CHARS),
196
+ };
197
+ }
198
+
199
+ async function queueCommonRemoteFetch(
200
+ pi: Pick<ExtensionAPI, "exec">,
201
+ ctx: GitCommandContext,
202
+ remote: string,
203
+ prune: boolean,
204
+ commonGitDir: string,
205
+ signal?: AbortSignal,
206
+ ): Promise<FetchRemoteDetails> {
207
+ if (commonGitDir === ctx.cwd) return fetchRemoteWithinQueue(pi, ctx, remote, prune, commonGitDir, signal);
208
+ return withRepositoryMutationQueue(
209
+ commonGitDir,
210
+ fetchRemoteWithinQueue.bind(undefined, pi, ctx, remote, prune, commonGitDir, signal),
211
+ );
212
+ }
213
+
214
+ export async function fetchRemote(
215
+ pi: Pick<ExtensionAPI, "exec">,
216
+ ctx: GitCommandContext,
217
+ remote: string = "origin",
218
+ prune: boolean = false,
219
+ signal?: AbortSignal,
220
+ ): Promise<FetchRemoteDetails> {
221
+ validateRemoteDiscoveryInput(remote, prune);
222
+ const repoRoot = await getGitRoot(pi, ctx, signal);
223
+ const rootCtx = { cwd: repoRoot };
224
+ const commonGitDir = await getCanonicalCommonGitDirectory(pi, rootCtx, signal);
225
+ // Retain the existing active-root lock for sibling BranchMe operations as well
226
+ // as a shared ref-cache lock for concurrent fetch_remote calls in linked trees.
227
+ return withRepositoryMutationQueue(
228
+ repoRoot,
229
+ queueCommonRemoteFetch.bind(undefined, pi, rootCtx, remote, prune, commonGitDir, signal),
230
+ );
231
+ }
package/src/git.ts CHANGED
@@ -3,6 +3,7 @@ import { basename, dirname, isAbsolute, join, normalize, relative, sep } from "n
3
3
  import { withFileMutationQueue, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import {
5
5
  GIT_BRANCH_ENTRY_LIMIT,
6
+ GIT_BRANCH_PATTERN_LIMIT,
6
7
  GIT_BRANCH_RAW_OUTPUT_LIMIT_BYTES,
7
8
  GIT_CONTEXT_CHANGE_LIMIT,
8
9
  GIT_CONTEXT_RECENT_COMMIT_LIMIT,
@@ -42,6 +43,7 @@ import type {
42
43
  GitFileChangeSummary,
43
44
  InitRepositoryDetails,
44
45
  ListBranchesDetails,
46
+ ListBranchesToolInput,
45
47
  ListWorktreesDetails,
46
48
  PullBranchDetails,
47
49
  PushBranchDetails,
@@ -1273,19 +1275,81 @@ export function parseBranchRefs(output: string): Pick<ListBranchesDetails, "bran
1273
1275
  return { branches, omitted: records.length - branches.length };
1274
1276
  }
1275
1277
 
1278
+ function validateBranchFilters(filters: ListBranchesToolInput): void {
1279
+ if (filters.kind !== undefined && filters.kind !== "local" && filters.kind !== "remote-tracking") {
1280
+ throw new TypeError("list_branches kind must be 'local' or 'remote-tracking'.");
1281
+ }
1282
+ if (filters.patterns === undefined) return;
1283
+ if (!Array.isArray(filters.patterns) || filters.patterns.length === 0 || filters.patterns.length > GIT_BRANCH_PATTERN_LIMIT) {
1284
+ throw new TypeError(`list_branches patterns must contain 1 to ${GIT_BRANCH_PATTERN_LIMIT} strings.`);
1285
+ }
1286
+ for (const pattern of filters.patterns) {
1287
+ if (typeof pattern !== "string" || !pattern.trim() || pattern.length > GIT_CONTEXT_VALUE_LIMIT_CHARS ||
1288
+ /[\p{Cc}\p{Cf}\u2028\u2029]/u.test(pattern) || pattern.startsWith("-")) {
1289
+ throw new TypeError("list_branches patterns must be nonblank, bounded strings without leading '-' or control characters.");
1290
+ }
1291
+ }
1292
+ }
1293
+
1294
+ async function readBranchScope(
1295
+ pi: Pick<ExtensionAPI, "exec">,
1296
+ ctx: GitCommandContext,
1297
+ patterns: string[],
1298
+ signal: AbortSignal | undefined,
1299
+ kind: string,
1300
+ ): Promise<string> {
1301
+ const result = await runGit(pi, ctx, [
1302
+ "branch", "--list", "--no-color", "--no-column", "--sort=refname",
1303
+ // Detached/rebasing pseudo-branches are not refs; render them as empty lines.
1304
+ `--format=%(if:equals=refs)%(refname:rstrip=-1)%(then)${GIT_BRANCH_FORMAT}%(end)`,
1305
+ ...(kind === "remote-tracking" ? ["--remotes"] : []), "--", ...patterns,
1306
+ ], { signal });
1307
+ return result.stdout;
1308
+ }
1309
+
1310
+ async function readFilteredBranchRefs(
1311
+ pi: Pick<ExtensionAPI, "exec">,
1312
+ ctx: GitCommandContext,
1313
+ filters: ListBranchesToolInput,
1314
+ signal?: AbortSignal,
1315
+ ): Promise<string> {
1316
+ const kinds = filters.kind === undefined ? ["local", "remote-tracking"] : [filters.kind];
1317
+ if (filters.patterns === undefined) {
1318
+ const prefixes = kinds.map((kind) => kind === "local" ? "refs/heads/" : "refs/remotes/");
1319
+ const result = await runGit(pi, ctx, [
1320
+ "for-each-ref", "--sort=refname", `--format=${GIT_BRANCH_FORMAT}`, ...prefixes,
1321
+ ], { signal });
1322
+ return result.stdout;
1323
+ }
1324
+
1325
+ // Git applies branch-list glob semantics before the raw-output and entry limits.
1326
+ // Separate scopes keep remote names as origin/topic, not remotes/origin/topic.
1327
+ const outputs = await Promise.all(kinds.map(readBranchScope.bind(undefined, pi, ctx, filters.patterns, signal)));
1328
+ let output = "";
1329
+ for (const scopeOutput of outputs) {
1330
+ if (Buffer.byteLength(output, "utf8") + Buffer.byteLength(scopeOutput, "utf8") > GIT_BRANCH_RAW_OUTPUT_LIMIT_BYTES) {
1331
+ throw new TypeError("Unable to parse branches: ref output exceeded the safety limit.");
1332
+ }
1333
+ for (const record of scopeOutput.split("\n")) {
1334
+ if (record !== "") output += `${record}\n`;
1335
+ }
1336
+ }
1337
+ return output;
1338
+ }
1339
+
1276
1340
  export async function listBranches(
1277
1341
  pi: Pick<ExtensionAPI, "exec">,
1278
1342
  ctx: GitCommandContext,
1279
1343
  signal?: AbortSignal,
1344
+ filters: ListBranchesToolInput = {},
1280
1345
  ): Promise<ListBranchesDetails> {
1346
+ validateBranchFilters(filters);
1281
1347
  const repoRoot = await getGitRoot(pi, ctx, signal);
1282
1348
  const rootCtx = { cwd: repoRoot };
1283
- const result = await runGit(pi, rootCtx, [
1284
- "for-each-ref", "--sort=refname", `--format=${GIT_BRANCH_FORMAT}`, "refs/heads/", "refs/remotes/",
1285
- ], { signal });
1286
- const parsed = parseBranchRefs(result.stdout);
1349
+ const output = await readFilteredBranchRefs(pi, rootCtx, filters, signal);
1350
+ const parsed = parseBranchRefs(output);
1287
1351
  const inventory = await collectWorktreeInventory(pi, rootCtx, signal);
1288
- const rawRecords = result.stdout.split("\n");
1352
+ const rawRecords = output.split("\n");
1289
1353
  for (const [index, branch] of parsed.branches.entries()) {
1290
1354
  if (branch.kind !== "local") continue;
1291
1355
  const rawName = rawRecords[index].split(NUL_SEPARATOR, 1)[0].slice("refs/heads/".length);
@@ -8,7 +8,10 @@ import {
8
8
  CREATE_BRANCH_TOOL_NAME,
9
9
  CREATE_WORKTREE_TOOL_NAME,
10
10
  FETCH_BRANCH_TOOL_NAME,
11
+ FETCH_REMOTE_TOOL_NAME,
12
+ GIT_BRANCH_PATTERN_LIMIT,
11
13
  GIT_BRANCH_SUMMARY_LIMIT_CHARS,
14
+ GIT_CONTEXT_VALUE_LIMIT_CHARS,
12
15
  GIT_RETIREMENT_SUMMARY_LIMIT_CHARS,
13
16
  GIT_WORKTREE_SUMMARY_LIMIT_CHARS,
14
17
  INIT_REPOSITORY_TOOL_NAME,
@@ -46,6 +49,7 @@ import {
46
49
  withRepositoryMutationQueue,
47
50
  } from "../git.ts";
48
51
  import { collectGitContext, formatGitContext } from "../git-context.ts";
52
+ import { fetchRemote } from "../git-discovery.ts";
49
53
  import { formatConflictPathList, integrateBranch } from "../git-integration.ts";
50
54
  import { retireBranch } from "../git-retirement.ts";
51
55
  import { formatLandBranch, landBranch } from "../git-landing.ts";
@@ -77,6 +81,28 @@ import type {
77
81
 
78
82
  const EmptyParametersSchema = Type.Object({}, { additionalProperties: false });
79
83
 
84
+ const ListBranchesParametersSchema = Type.Object(
85
+ {
86
+ kind: Type.Optional(StringEnum(["local", "remote-tracking"] as const, {
87
+ description: "Limit discovery to local or remote-tracking branches; omit for both.",
88
+ })),
89
+ patterns: Type.Optional(Type.Array(Type.String({
90
+ minLength: 1,
91
+ maxLength: GIT_CONTEXT_VALUE_LIMIT_CHARS,
92
+ description: "Git branch-list glob on branch names, including remote prefix (for example origin/feat/23-*).",
93
+ }), { minItems: 1, maxItems: GIT_BRANCH_PATTERN_LIMIT, description: "Match any of these patterns before output limits; omit to list all selected branches." })),
94
+ },
95
+ { additionalProperties: false },
96
+ );
97
+
98
+ const FetchRemoteParametersSchema = Type.Object(
99
+ {
100
+ remote: Type.Optional(Type.String({ minLength: 1, description: "Configured remote name; defaults to origin." })),
101
+ prune: Type.Optional(Type.Boolean({ description: "Delete stale cached branch refs only for this remote; defaults to false. Never deletes local branches or tags." })),
102
+ },
103
+ { additionalProperties: false },
104
+ );
105
+
80
106
  const InitRepositoryParametersSchema = Type.Object(
81
107
  {
82
108
  initialBranch: Type.Optional(Type.String({
@@ -522,15 +548,16 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
522
548
  pi.registerTool({
523
549
  name: LIST_BRANCHES_TOOL_NAME,
524
550
  label: "List Branches",
525
- description: "list_branches reads up to 200 local and remote-tracking branches, including commits, upstream counts, symbolic refs, and worktree occupancy. Read-only; cached remote refs are not fetched. Text is bounded to 4000 characters.",
551
+ description: "list_branches reads up to 200 local and remote-tracking branches, including commits, upstream counts, symbolic refs, and worktree occupancy. Optional kind and Git branch-list patterns filter before limits. Read-only; cached remote refs are not fetched. Text is bounded to 4000 characters.",
526
552
  promptSnippet: "list_branches: discover local and cached remote-tracking branches without mutation",
527
553
  promptGuidelines: [
528
554
  "Use list_branches to discover names and worktree occupancy before branch operations; it never fetches or mutates Git state.",
529
555
  "Treat list_branches names and paths as display metadata; redacted or truncated values are not executable identities.",
556
+ "Use list_branches with kind: 'remote-tracking' and patterns such as ['origin/feat/23', 'origin/feat/23-*'] for issue branch discovery; wait for fetch_remote to complete first when fresh remote refs are needed.",
530
557
  ],
531
- parameters: EmptyParametersSchema,
532
- async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
533
- const details = await listBranches(pi, ctx, signal);
558
+ parameters: ListBranchesParametersSchema,
559
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
560
+ const details = await listBranches(pi, ctx, signal, params);
534
561
  const lines = details.branches.map((branch) =>
535
562
  `${branch.current ? "*" : "-"} ${JSON.stringify(branch.name)} (${branch.kind}) ${shortCommit(branch.head)}; upstream ${JSON.stringify(branch.upstream)}; ahead ${branch.ahead ?? "?"}, behind ${branch.behind ?? "?"}; worktrees ${JSON.stringify(branch.worktreePaths)}`);
536
563
  const text = [`Branches: ${details.branches.length}; omitted: ${details.omitted}.`, ...lines].join("\n");
@@ -670,6 +697,27 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
670
697
  },
671
698
  });
672
699
 
700
+ pi.registerTool({
701
+ name: FETCH_REMOTE_TOOL_NAME,
702
+ label: "Fetch Remote",
703
+ description: "fetch_remote refreshes all branch refs for one configured remote (default origin). Optional prune deletes only that remote's stale cached branch refs; default false. Uses an internal atomic heads-to-remote-tracking refspec, ignores configured refmaps, and disables tags, tag pruning, and submodules. Requires the files ref backend and non-overlapping remote namespaces; refuses unsafe symbolic destinations. Preserves safe remote HEAD aliases by excluding the remote branch named HEAD. Never changes local branches, checkout, upstream configuration, or working-tree files.",
704
+ promptSnippet: "fetch_remote: refresh a configured remote's branch cache with optional remote-tracking-only pruning",
705
+ promptGuidelines: [
706
+ "Use fetch_remote when fresh discovery of unknown remote branch names is needed; fetch_branch remains the narrow tool for a known branch.",
707
+ "Use fetch_remote with prune: true only when stale cached branch deletion is intended; it never deletes local branches, tags, or branches on the server.",
708
+ "Call fetch_remote by itself and wait for it to complete before list_branches, track_branch, or other dependent Git operations; do not batch Git mutations.",
709
+ "fetch_remote accepts only a configured remote name and optional prune boolean; never pass URLs, refspecs, force, tags, or checkout controls.",
710
+ ],
711
+ parameters: FetchRemoteParametersSchema,
712
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
713
+ const details = await fetchRemote(pi, ctx, params.remote ?? "origin", params.prune ?? false, signal);
714
+ return {
715
+ content: [{ type: "text", text: `Fetched branch cache for remote ${details.remote}${details.prune ? " and pruned its stale remote-tracking branch refs" : " without pruning"}. No local branches or tags changed.` }],
716
+ details,
717
+ };
718
+ },
719
+ });
720
+
673
721
  pi.registerTool({
674
722
  name: PULL_BRANCH_TOOL_NAME,
675
723
  label: "Pull Branch",
package/src/types.ts CHANGED
@@ -29,7 +29,25 @@ export interface BranchStatusToolInput {
29
29
  ancestry?: BranchStatusAncestryQuery;
30
30
  }
31
31
 
32
- export type ListBranchesToolInput = Record<string, never>;
32
+ export interface ListBranchesToolInput {
33
+ kind?: "local" | "remote-tracking";
34
+ /** Git branch-list glob patterns on names, including the remote prefix for remote-tracking refs. */
35
+ patterns?: string[];
36
+ }
37
+
38
+ export interface FetchRemoteToolInput {
39
+ remote?: string;
40
+ prune?: boolean;
41
+ }
42
+
43
+ export interface FetchRemoteDetails {
44
+ action: "fetch_remote";
45
+ repoRoot: string;
46
+ remote: string;
47
+ prune: boolean;
48
+ refspec: string;
49
+ output: string;
50
+ }
33
51
 
34
52
  export interface BranchEntry {
35
53
  name: string;