@rethunk/mcp-multi-root-git 2.9.1 → 4.0.0

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.
Files changed (75) hide show
  1. package/AGENTS.md +23 -19
  2. package/CHANGELOG.md +145 -39
  3. package/HUMANS.md +23 -42
  4. package/README.md +30 -13
  5. package/dist/repo-paths.js +57 -13
  6. package/dist/server/batch-commit-tool.js +203 -53
  7. package/dist/server/error-codes.js +31 -16
  8. package/dist/server/git-blame-tool.js +66 -19
  9. package/dist/server/git-branch-tool.js +151 -0
  10. package/dist/server/git-cherry-pick-tool.js +221 -12
  11. package/dist/server/git-conflicts-tool.js +230 -0
  12. package/dist/server/git-diff-summary-tool.js +39 -28
  13. package/dist/server/git-diff-tool.js +56 -31
  14. package/dist/server/git-grep-tool.js +188 -0
  15. package/dist/server/git-inventory-tool.js +30 -10
  16. package/dist/server/git-log-tool.js +37 -6
  17. package/dist/server/git-merge-tool.js +71 -13
  18. package/dist/server/git-parity-tool.js +15 -6
  19. package/dist/server/git-push-tool.js +4 -2
  20. package/dist/server/git-refs.js +48 -17
  21. package/dist/server/git-reset-soft-tool.js +1 -1
  22. package/dist/server/git-revert-tool.js +160 -0
  23. package/dist/server/git-show-tool.js +5 -10
  24. package/dist/server/git-stash-tool.js +112 -78
  25. package/dist/server/git-status-tool.js +2 -2
  26. package/dist/server/git-tag-tool.js +8 -13
  27. package/dist/server/git-worktree-tool.js +67 -59
  28. package/dist/server/git.js +116 -24
  29. package/dist/server/inventory.js +90 -32
  30. package/dist/server/list-presets-tool.js +2 -8
  31. package/dist/server/presets-resource.js +37 -20
  32. package/dist/server/presets.js +87 -15
  33. package/dist/server/roots.js +52 -79
  34. package/dist/server/schemas.js +18 -19
  35. package/dist/server/test-harness.js +11 -4
  36. package/dist/server/tool-parameter-schemas.js +47 -58
  37. package/dist/server/tools.js +36 -17
  38. package/dist/server.js +1 -1
  39. package/docs/install.md +52 -5
  40. package/docs/mcp-tools.md +472 -284
  41. package/package.json +6 -6
  42. package/schemas/batch_commit.json +5 -17
  43. package/schemas/git_blame.json +13 -11
  44. package/schemas/git_branch.json +54 -0
  45. package/schemas/git_cherry_pick.json +13 -15
  46. package/schemas/git_cherry_pick_continue.json +34 -0
  47. package/schemas/git_conflicts.json +38 -0
  48. package/schemas/git_diff.json +12 -10
  49. package/schemas/git_diff_summary.json +1 -15
  50. package/schemas/git_grep.json +85 -0
  51. package/schemas/git_inventory.json +32 -23
  52. package/schemas/git_log.json +20 -24
  53. package/schemas/git_merge.json +3 -15
  54. package/schemas/git_parity.json +13 -23
  55. package/schemas/git_push.json +1 -13
  56. package/schemas/git_reset_soft.json +1 -13
  57. package/schemas/git_revert.json +47 -0
  58. package/schemas/git_show.json +2 -8
  59. package/schemas/git_stash_apply.json +2 -8
  60. package/schemas/git_stash_push.json +47 -0
  61. package/schemas/git_status.json +13 -23
  62. package/schemas/git_tag.json +1 -7
  63. package/schemas/git_worktree_add.json +2 -14
  64. package/schemas/git_worktree_remove.json +2 -14
  65. package/schemas/index.json +28 -23
  66. package/schemas/list_presets.json +13 -23
  67. package/tool-parameters.schema.json +407 -423
  68. package/dist/server/git-branch-list-tool.js +0 -138
  69. package/dist/server/git-fetch-tool.js +0 -266
  70. package/dist/server/git-reflog-tool.js +0 -126
  71. package/schemas/git_branch_list.json +0 -36
  72. package/schemas/git_fetch.json +0 -52
  73. package/schemas/git_reflog.json +0 -44
  74. package/schemas/git_stash_list.json +0 -30
  75. package/schemas/git_worktree_list.json +0 -30
package/docs/mcp-tools.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MCP tools and resources (canonical reference)
2
2
 
3
- Single source of truth for **registered tool ids**, **client naming**, **JSON output shape**, **resource URI**, and **workspace root resolution**.
3
+ Single source of truth for **registered tool ids**, **client naming**, **JSON output shape**, **resource URI**, and **root resolution**.
4
4
  **Install and MCP clients (only canonical location):** [install.md](install.md). **Preset file, dev, CI, publishing:** [HUMANS.md](../HUMANS.md). **Implementation layout (`src/server/` + entry [`server.ts`](../src/server.ts)), contract bumps:** [AGENTS.md](../AGENTS.md).
5
5
 
6
6
  ## Naming
@@ -11,29 +11,30 @@ MCP clients expose tools as `{serverName}_{toolName}`. With the server registere
11
11
 
12
12
  | Short id | Client id (server `rethunk-git`) | Purpose |
13
13
  |----------|-----------------------------------|---------|
14
- | `git_status` | `rethunk-git_git_status` | `git status --short -b` per MCP root and optional submodules (`includeSubmodules`); parallel submodule status. Args include `absoluteGitRoots`, `allWorkspaceRoots`, `rootIndex`, `workspaceRoot`, `format`. **Read-only.** |
15
- | `git_inventory` | `rethunk-git_git_inventory` | Status + ahead/behind per path; default upstream each repo’s `@{u}`; pass **both** `remote` and `branch` for fixed tracking. `nestedRoots`, `preset`, `presetMerge`, `maxRoots`, `format`, plus workspace pick args (`absoluteGitRoots` cannot combine with `preset`/`nestedRoots`). **Read-only.** |
16
- | `git_parity` | `rethunk-git_git_parity` | Compare `git rev-parse HEAD` for path pairs. `pairs`, `preset`, `presetMerge`, `format`, plus workspace pick args (`absoluteGitRoots` for sibling clone batches). **Read-only.** |
17
- | `list_presets` | `rethunk-git_list_presets` | List preset names/counts from `.rethunk/git-mcp-presets.json`; invalid JSON/schema surface as errors. Workspace pick + `format` only (includes `absoluteGitRoots`). **Read-only.** |
18
- | `git_log` | `rethunk-git_git_log` | Path-filtered, time-windowed `git log` across one or more workspace roots. Returns commit history with author, date, subject, and shortstat. Args: `since`, `paths`, `grep`, `author`, `maxCommits`, `branch`, plus workspace pick args (`absoluteGitRoots` for sibling clones) + `format` (`markdown`/`json`/`oneline`). **Read-only.** |
19
- | `git_diff_summary` | `rethunk-git_git_diff_summary` | Structured, token-efficient diff viewer. Returns per-file diffs with additions/deletions counts, truncated to configurable line limits, with lock files/dist/vendor excluded by default. Args: `range`, `fileFilter`, `maxLinesPerFile`, `maxFiles`, `excludePatterns`, plus workspace pick args (optional single-entry `absoluteGitRoots`) + `format`. **Read-only.** |
20
- | `git_diff` | `rethunk-git_git_diff` | Raw diff text for a single repo. Supports unstaged, staged, or `base..head` ranges, scoped to one or more paths with configurable context width. Args: `workspaceRoot`, `rootIndex`, `format`, `base?`, `head?`, `path?`, `paths?`, `unified?`, `staged?`. No `absoluteGitRoots` or `allWorkspaceRoots`. **Read-only.** |
21
- | `git_show` | `rethunk-git_git_show` | Inspect one commit or ref. Returns commit message plus diff (or `--stat` diffstat), or file content at `path` for that ref. Args: `ref`, `path?`, `paths?`, `stat?`, plus single-repo workspace pick + `format`. **Read-only.** |
22
- | `git_worktree_list` | `rethunk-git_git_worktree_list` | List all worktrees (`git worktree list --porcelain`). Workspace pick + `format`. **Read-only.** |
23
- | `git_stash_list` | `rethunk-git_git_stash_list` | List `git stash` entries for one repo. Args: single-repo workspace pick + `format`. **Read-only.** |
24
- | `git_blame` | `rethunk-git_git_blame` | Line-by-line authorship for a file: commit SHA, author, date, summary per line. Args: `path` (required), `ref?`, `startLine?`, `endLine?`, plus single-repo workspace pick + `format`. **Read-only.** |
25
- | `git_branch_list` | `rethunk-git_git_branch_list` | List local branches (sha, current marker, upstream); optional `includeRemotes` adds remote-tracking branches. Args: `includeRemotes?`, plus single-repo workspace pick + `format`. **Read-only.** |
26
- | `git_reflog` | `rethunk-git_git_reflog` | Show the reflog for a ref (default `HEAD`) recent HEAD movements with selector, SHA, and message. Args: `ref?`, `maxEntries?`, plus single-repo workspace pick + `format`. **Read-only.** |
27
- | `git_fetch` | `rethunk-git_git_fetch` | Fetch from a remote without modifying the working tree. Updates refs only and reports updated/new refs, plus structured `updated`/`created`/`pruned` deltas on git 2.41. Args: `remote?`, `branch?`, `prune?`, `tags?`, plus single-repo workspace pick + `format`. **Mutating — refs only.** |
28
- | `git_push` | `rethunk-git_git_push` | Push the current branch to its upstream. Optional `remote`, `branch`, `setUpstream` (passes `-u`). Refuses on detached HEAD; never force-pushes. Workspace pick + `format`. **Mutating.** |
29
- | `git_tag` | `rethunk-git_git_tag` | Create/delete annotated or lightweight tags for one repo. Args: `tag`, `message?`, `ref?`, `delete?`, plus single-repo workspace pick + `format`. **Mutating.** |
30
- | `git_worktree_add` | `rethunk-git_git_worktree_add` | Create a new linked worktree, creating the branch from `baseRef` if it does not yet exist. Refuses on protected branch names. Args: `path`, `branch`, `baseRef?`, plus workspace pick + `format`. **Mutating.** |
31
- | `git_worktree_remove` | `rethunk-git_git_worktree_remove` | Remove a registered worktree; refuses to remove the main worktree. Optional `force: true` for dirty trees. Args: `path`, `force?`, plus workspace pick + `format`. **Mutating.** |
32
- | `git_reset_soft` | `rethunk-git_git_reset_soft` | Soft-reset the current branch to a ref (`HEAD~N`, SHA, branch). Rewound changes land in the staging index; requires a clean working tree. Args: `ref`, plus workspace pick + `format`. **Mutating — not idempotent.** |
33
- | `batch_commit` | `rethunk-git_batch_commit` | Create multiple sequential git commits in a single call. Each entry stages the listed files or line-ranged file hunks, then commits with the given message. Stops on first failure. Optional `push: "after"` pushes once every commit lands; optional `dryRun: true` previews staged content without writing commits. Args: `commits` (array of `{message, files}`), `push?`, `dryRun?`, plus workspace pick args + `format`. **Mutating — not idempotent.** |
34
- | `git_merge` | `rethunk-git_git_merge` | Merge one or more source branches into a destination. Default strategy `auto` cascades fast-forward rebase merge-commit per source, preferring linear history. Refuses on dirty tree; stops on first conflict with structured path report. Optional `deleteMergedBranches` / `deleteMergedWorktrees` cascade cleanup, always skipping protected names (main/master/dev/develop/stable/trunk/prod/production/release\*/hotfix\*). Args: `sources`, `into?`, `strategy?`, `message?`, cleanup flags + workspace pick + `format`. **Mutating.** |
35
- | `git_cherry_pick` | `rethunk-git_git_cherry_pick` | Play commits from one or more sources onto a destination. Sources may be SHAs, `A..B` ranges, or branch names (expanded to `onto..<branch>`, oldest-first). Uses `--empty=drop` so patch-equivalent re-applies add nothing. Refuses on dirty tree; stops on first conflict, aborting cleanly. Same cleanup flags as `git_merge` (branch-kind sources only, protected names skipped); branch deletion uses patch-id equivalence by default so cherry-pick workflows (where SHA differs but diff is identical) clean up correctly. Pass `strictMergedRefEquality: true` for strict `git branch -d` ancestry semantics. Args: `sources`, `onto?`, cleanup flags, `strictMergedRefEquality?` + workspace pick + `format`. **Mutating.** |
36
- | `git_stash_apply` | `rethunk-git_git_stash_apply` | Apply or pop a stash entry for one repo. Args: `index?`, `pop?`, plus single-repo workspace pick + `format`. **Mutating.** |
14
+ | `git_status` | `rethunk-git_git_status` | `git status --short -b` per MCP root and optional submodules (`includeSubmodules`); parallel submodule status. Args: `includeSubmodules`, `root`, `format`. **Read-only.** |
15
+ | `git_inventory` | `rethunk-git_git_inventory` | Status + ahead/behind per path; default upstream each repo’s `@{u}`; pass **both** `remote` and `branch` for fixed tracking; optional `compareRefs: { left, right }` for ahead/behind between arbitrary local refs (independent of upstream). `nestedRoots`, `preset`, `presetMerge`, `maxRoots`, `format`, plus `root` (array form cannot combine with `preset`/`nestedRoots`). **Read-only.** |
16
+ | `git_parity` | `rethunk-git_git_parity` | Compare `git rev-parse HEAD` for path pairs. `pairs`, `preset`, `presetMerge`, `format`, plus `root`. **Read-only.** |
17
+ | `list_presets` | `rethunk-git_list_presets` | List preset names/counts from `.rethunk/git-mcp-presets.json`; invalid JSON/schema surface as errors. `root` + `format` only. **Read-only.** |
18
+ | `git_log` | `rethunk-git_git_log` | Path-filtered, time-windowed `git log` across one or more roots. Returns commit history with author, date, subject, and shortstat. Optional `follow: true` follows renames (`git log --follow`; requires exactly one `paths` entry). Args: `since`, `paths`, `follow?`, `grep`, `author`, `maxCommits`, `branch`, plus `root` + `format` (`markdown`/`json`/`oneline`). **Read-only.** |
19
+ | `git_grep` | `rethunk-git_git_grep` | Read-only pickaxe history search across one or more roots (`pickaxe: { mode: "S"\|"G", term }` → `git log -S`/`-G`, `commits[]` per root). `pickaxe` is required content-mode working-tree search was removed in v6 (use the client's native grep/rg tooling). Args: `pickaxe`, `ref?`, `paths?`, `ignoreCase?`, `maxMatches?`, plus `root` + `format`. **Read-only.** |
20
+ | `git_diff_summary` | `rethunk-git_git_diff_summary` | Structured, token-efficient diff viewer. Returns per-file diffs with additions/deletions counts, truncated to configurable line limits, with lock files/dist/vendor excluded by default. Args: `range`, `fileFilter`, `maxLinesPerFile`, `maxFiles`, `excludePatterns`, plus `workspaceRoot` + `format`. **Read-only.** |
21
+ | `git_diff` | `rethunk-git_git_diff` | Raw diff text for a single repo. Supports unstaged, staged, or `base..head` ranges, scoped to one or more paths with configurable context width. Optional `maxBytes` caps returned UTF-8 bytes (`truncated: true` when cut). Args: `workspaceRoot`, `format`, `base?`, `head?`, `path?`, `paths?`, `unified?`, `staged?`, `maxBytes?`. **Read-only.** |
22
+ | `git_show` | `rethunk-git_git_show` | Inspect one commit or ref. Returns commit message plus patch (or `--stat` diffstat). Optional `path`/`paths` filter the patch to those paths (`git show <ref> -- <path>`), not a raw blob checkout. Args: `ref`, `path?`, `paths?`, `stat?`, plus `workspaceRoot` + `format`. **Read-only.** |
23
+ | `git_conflicts` | `rethunk-git_git_conflicts` | Inspect an in-progress conflicted merge/cherry-pick/revert/rebase in the working tree (markers under the git dir). Per conflicted file, parses `<<<<<<</\|\|\|\|\|\|\|/=======/>>>>>>>` into ours/theirs (and base, for diff3-style) hunks. **Note:** `git_merge` and `git_cherry_pick` always attempt `--abort` before returning a conflict report, so a successful abort leaves a clean tree — call this tool for conflicts still in progress (manual ops, failed abort, or other tools), not as a follow-up to those tools' conflict payloads. Args: `withHunks?` (default `true`), `maxLinesPerFile?` (default `200`, max `2000`), plus `workspaceRoot` + `format`. **Read-only.** |
24
+ | `git_blame` | `rethunk-git_git_blame` | File authorship grouped into contiguous same-commit line runs (SHA, author, date, summary once per run). Args: `path` (required), `ref?`, `startLine?`, `endLine?`, `maxLines?`, plus `workspaceRoot` + `format`. **Read-only.** |
25
+ | `batch_commit` | `rethunk-git_batch_commit` | Create multiple sequential git commits in a single call. Each entry stages the listed files or line-ranged file hunks, then commits with the given message. Stops on first failure. Optional `push: "after"` pushes once every commit lands; optional `dryRun: true` previews staged content without writing commits. Args: `commits` (array of `{message, files}`), `push?`, `dryRun?`, plus `workspaceRoot` + `format`. **Mutating — not idempotent.** |
26
+ | `git_push` | `rethunk-git_git_push` | Push the current branch to its upstream. Optional `remote`, `branch`, `setUpstream` (passes `-u`). Refuses on detached HEAD; never force-pushes. `workspaceRoot` + `format`. **Mutating.** |
27
+ | `git_merge` | `rethunk-git_git_merge` | Merge one or more source branches into a destination. Default strategy `auto` cascades fast-forward → rebase → merge-commit per source, preferring linear history. **`auto`/`rebase` rewrite the source branch tip in place** when rebasing (new SHAs on the source ref), then fast-forward the destination — not destination-only. Refuses on dirty tree; stops on first conflict and attempts `--abort` (abort failure surfaces `rebase_abort_failed` / `merge_abort_failed`). Optional `deleteMergedBranches` / `deleteMergedWorktrees` cascade cleanup, always skipping protected names (main/master/dev/develop/stable/trunk/prod/production/head, plus `release/*`/`release-*`/`hotfix/*`/`hotfix-*` with separator + suffix). Args: `sources`, `into?`, `strategy?`, `message?`, cleanup flags + `workspaceRoot` + `format`. **Mutating.** |
28
+ | `git_cherry_pick` | `rethunk-git_git_cherry_pick` | Play commits from one or more sources onto a destination. Sources may be SHAs, `A..B` ranges, or branch names (expanded to `onto..<branch>`, oldest-first). Hard-caps expanded/deduped picks at **100** commits per call (`cherry_pick_too_many_commits`). Uses `--empty=drop` so patch-equivalent re-applies add nothing. Refuses on dirty tree; refuses when a cherry-pick is already in progress (`cherry_pick_in_progress`). Stops on first conflict: `onConflict: "abort"` (default) attempts `--abort` (abort failure → `cherry_pick_abort_failed`); `onConflict: "pause"` leaves the conflict and native sequencer state in place (`conflict.paused: true`) for `git_cherry_pick_continue`. Same cleanup flags as `git_merge` (branch-kind sources only, protected names skipped, and only run on full success); branch deletion uses patch-id equivalence by default so cherry-pick workflows (where SHA differs but diff is identical) clean up correctly. Pass `strictMergedRefEquality: true` for strict `git branch -d` ancestry semantics. Args: `sources`, `onto?`, cleanup flags, `strictMergedRefEquality?`, `onConflict?` + `workspaceRoot` + `format`. **Mutating.** |
29
+ | `git_cherry_pick_continue` | `rethunk-git_git_cherry_pick_continue` | Resume or abort a cherry-pick left in progress (by `git_cherry_pick`'s `onConflict: "pause"` or any other means — reads `CHERRY_PICK_HEAD` live off `.git`, stateless). `action: "continue"` (default) requires no remaining unmerged paths (`cherry_pick_unresolved_paths` otherwise), then runs `git -c core.editor=true cherry-pick --continue`; if a later pick then conflicts, reports it the same shape as a paused `git_cherry_pick` call (`conflict.paused: true`) so this tool can be called again. `action: "abort"` rolls back via `git cherry-pick --abort` (same abort helper/reporting as `git_cherry_pick`). Errors `no_cherry_pick_in_progress` when nothing is in progress. Args: `action?` + `workspaceRoot` + `format`. **Mutating.** |
30
+ | `git_reset_soft` | `rethunk-git_git_reset_soft` | Soft-reset the current branch to a ref (`HEAD~N`, SHA, branch). Rewound changes land in the staging index; requires a clean working tree. Args: `ref`, plus `workspaceRoot` + `format`. **Mutating — not idempotent.** |
31
+ | `git_revert` | `rethunk-git_git_revert` | Create new commit(s) that undo the changes introduced by one or more source commits (`git revert`), applied in listed order. Never rewrites history — safe on shared/pushed branches, unlike `git_reset_soft`. Refuses on dirty tree; on conflict aborts and leaves the tree clean. Args: `sources`, `noCommit?`, `mainline?`, plus `workspaceRoot` + `format`. **Mutating — not idempotent.** |
32
+ | `git_tag` | `rethunk-git_git_tag` | Create/delete annotated or lightweight tags for one repo. Args: `tag`, `message?`, `ref?`, `delete?`, plus `workspaceRoot` + `format`. **Mutating.** |
33
+ | `git_branch` | `rethunk-git_git_branch` | Create, delete, or rename a local branch. `action: "create"` bases a new branch on `from` (default `HEAD`); `action: "delete"` removes `name` (`force: true` for `-D` on an unmerged branch); `action: "rename"` renames `name` to `newName`. Refuses protected branch names (main/master/dev/develop/stable/trunk/prod/production/head, plus `release/*`/`release-*`/`hotfix/*`/`hotfix-*` with separator + suffix) in any role — as source, target, or rename endpoint. Args: `action`, `name`, `from?`, `newName?`, `force?`, plus `workspaceRoot` + `format`. **Mutating.** |
34
+ | `git_worktree_add` | `rethunk-git_git_worktree_add` | Create a new linked worktree, creating the branch from `baseRef` if it does not yet exist. Sibling paths outside the git toplevel are allowed; leading `-` / option-like basenames and NUL bytes are rejected (`invalid_paths`). Path is passed to git after `--`. Refuses on protected branch names. Args: `path`, `branch`, `baseRef?`, plus `workspaceRoot` + `format`. **Mutating.** |
35
+ | `git_worktree_remove` | `rethunk-git_git_worktree_remove` | Remove a registered worktree; refuses to remove the main worktree. Same path argv rules as add (leading `-` / NUL `invalid_paths`; path after `--`). Optional `force: true` for dirty trees. Args: `path`, `force?`, plus `workspaceRoot` + `format`. **Mutating.** |
36
+ | `git_stash_apply` | `rethunk-git_git_stash_apply` | Apply or pop a stash entry for one repo (`destructiveHint: true` — pop can delete a stash entry). On failure emits `error: stash_apply_failed` and optional `conflictPaths`. Args: `index?`, `pop?`, plus `workspaceRoot` + `format`. **Mutating.** |
37
+ | `git_stash_push` | `rethunk-git_git_stash_push` | Stash working-tree changes (`git stash push`). Optional `message`, `includeUntracked` (-u), `keepIndex` (--keep-index), `paths` to scope. Args: `message?`, `includeUntracked?`, `paths?`, `keepIndex?`, plus `workspaceRoot` + `format`. **Mutating.** |
37
38
 
38
39
  Pass **`format: "json"`** on any tool for structured JSON instead of markdown (default).
39
40
 
@@ -44,10 +45,7 @@ Pass **`format: "json"`** on any tool for structured JSON instead of markdown (d
44
45
  | Parameter | Type | Default | Notes |
45
46
  |-----------|------|---------|-------|
46
47
  | `includeSubmodules` | boolean | `true` | When `true`, reads `.gitmodules` at each git toplevel and runs `git status --short -b` for each checked-out submodule in parallel. Set `false` to skip submodule discovery entirely. |
47
- | `workspaceRoot` | string | — | Explicit root; highest priority. |
48
- | `rootIndex` | int | — | Pick one of several MCP roots (0-based). |
49
- | `allWorkspaceRoots` | boolean | `false` | Fan out across all MCP roots. |
50
- | `absoluteGitRoots` | string[] | — | Explicit list of absolute paths; replaces normal workspace pick. Max 256. Read-only tools only. |
48
+ | `root` | string \| string[] \| `"*"` | — | Repo path (string), explicit list of repo paths (array, max 256), or `"*"` for every MCP root. Default: first MCP root / cwd. |
51
49
  | `format` | `"markdown"` \| `"json"` | `"markdown"` | Output format. |
52
50
 
53
51
  ### `git_status` — JSON shape (`format: "json"`)
@@ -76,11 +74,9 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
76
74
  | `not_a_git_repository` | repo row `statusText` | Root is not inside a git repository. |
77
75
  | `(submodule path escapes repository — rejected)` | repo row `statusText` | `.gitmodules` path resolves outside the git toplevel (security guard). |
78
76
  | `(no .git — submodule not checked out?)` | repo row `statusText` | Submodule directory exists but has no `.git` — not initialized. |
79
- | `root_index_out_of_range` | top-level | `rootIndex` exceeds the number of MCP file roots. |
80
- | `absolute_git_roots_exclusive` | top-level | `absoluteGitRoots` combined with `workspaceRoot`, `rootIndex`, or `allWorkspaceRoots: true`. |
81
- | `invalid_absolute_git_root` | top-level | An `absoluteGitRoots` entry is not a git-recognized directory. |
82
- | `absolute_git_roots_too_many` | top-level | More than 256 entries in `absoluteGitRoots`. |
83
- | `absolute_git_roots_empty` | top-level | `absoluteGitRoots` resolved to zero toplevels. |
77
+ | `invalid_root_path` | top-level | A `root` array entry is empty or not a git-recognized directory. |
78
+ | `root_list_too_many` | top-level | More than 256 entries in the `root` array. |
79
+ | `root_list_empty` | top-level | The `root` array resolved to zero git toplevels. |
84
80
 
85
81
  ---
86
82
 
@@ -88,16 +84,14 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
88
84
 
89
85
  | Parameter | Type | Default | Notes |
90
86
  |-----------|------|---------|-------|
91
- | `nestedRoots` | string[] | — | Relative paths (from the workspace git toplevel) to treat as independent git repos to inventory. Each must be a valid git work tree; invalid paths produce skip entries rather than errors. Cannot combine with `absoluteGitRoots` or `preset`. |
92
- | `preset` | string | — | Preset name from `.rethunk/git-mcp-presets.json`. Loads `nestedRoots` from the preset's entry. Cannot combine with `absoluteGitRoots`. |
87
+ | `nestedRoots` | string[] | — | Relative paths (from the workspace git toplevel) to treat as independent git repos to inventory. Each must be a valid git work tree; invalid paths produce skip entries rather than errors. Cannot combine with an array `root`. May be combined with `preset` when `presetMerge: true` (merged) or when `presetMerge` is false (inline list replaces the preset's `nestedRoots`). |
88
+ | `preset` | string | — | Preset name from `.rethunk/git-mcp-presets.json`. Loads `nestedRoots` from the preset's entry. Cannot combine with an array `root`. |
93
89
  | `presetMerge` | boolean | `false` | When `true`, merge inline `nestedRoots` with preset roots instead of replacing. |
94
90
  | `remote` | string | — | Fixed remote for ahead/behind tracking. Must be paired with `branch`. |
95
91
  | `branch` | string | — | Fixed branch for ahead/behind tracking. Must be paired with `remote`. When both are absent the tool uses each repo's `@{u}` upstream. |
92
+ | `compareRefs` | `{ left: string, right: string }` | — | Optional ahead/behind between arbitrary local refs (independent of upstream). Ahead = commits reachable as `left..right`; behind = `right..left`. Each side validated with `isSafeGitAncestorRef` → `unsafe_ref_token` on rejection. |
96
93
  | `maxRoots` | int | `64` | Max nested roots to process (1–256). Roots beyond the limit are omitted; `nestedRootsTruncated: true` and `nestedRootsOmittedCount` are set on the group. |
97
- | `workspaceRoot` | string | — | Explicit root; highest priority. |
98
- | `rootIndex` | int | — | Pick one of several MCP roots (0-based). |
99
- | `allWorkspaceRoots` | boolean | `false` | Fan out across all MCP roots. |
100
- | `absoluteGitRoots` | string[] | — | Explicit list; replaces normal workspace pick. Cannot combine with `nestedRoots` or `preset`. |
94
+ | `root` | string \| string[] \| `"*"` | — | Repo path (string), explicit list of repo paths (array, max 256), or `"*"` for every MCP root. Default: first MCP root / cwd. Array form cannot combine with `nestedRoots` or `preset`. |
101
95
  | `format` | `"markdown"` \| `"json"` | `"markdown"` | Output format. |
102
96
 
103
97
  ### `git_inventory` — JSON shape (`format: "json"`)
@@ -105,7 +99,7 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
105
99
  ```json
106
100
  {
107
101
  "inventories": [{
108
- "workspace_root": "/abs/path",
102
+ "workspaceRoot": "/abs/path",
109
103
  "entries": [{
110
104
  "label": ".",
111
105
  "path": "/abs/path",
@@ -114,7 +108,13 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
114
108
  "headAbbrev": "a1b2c3d",
115
109
  "upstreamRef": "origin/main",
116
110
  "ahead": "2",
117
- "behind": "0"
111
+ "behind": "0",
112
+ "compareRefs": {
113
+ "left": "main",
114
+ "right": "feature",
115
+ "ahead": "1",
116
+ "behind": "0"
117
+ }
118
118
  }],
119
119
  "nestedRootsTruncated": true,
120
120
  "nestedRootsOmittedCount": 3,
@@ -123,7 +123,7 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
123
123
  }
124
124
  ```
125
125
 
126
- `nestedRootsTruncated` / `nestedRootsOmittedCount` present only when `maxRoots` cut the list. `upstream` object present only when `remote`+`branch` were supplied (fixed mode). `presetSchemaVersion` present only when a preset was loaded. See *v2/v3 field omission* for `entries[*]` optional fields.
126
+ `nestedRootsTruncated` / `nestedRootsOmittedCount` present only when `maxRoots` cut the list. `upstream` object present only when `remote`+`branch` were supplied (fixed mode). `compareRefs` on an entry is present only when the call requested `compareRefs` and counts could be computed; optional `note` appears when a ref is unreadable or counts fail. `presetSchemaVersion` present only when a preset was loaded. Non-git paths produce skip entries with plain `skipReason` text such as `(not a git repository)` (not nested JSON). See [Field omission](#field-omission-consumer-contract-v2) for `entries[*]` optional fields.
127
127
 
128
128
  ### `git_inventory` — error codes
129
129
 
@@ -132,16 +132,13 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
132
132
  | `git_not_found` | `git` binary not on `PATH`. |
133
133
  | `remote_branch_mismatch` | Only one of `remote` / `branch` was provided; supply both or neither. |
134
134
  | `invalid_remote_or_branch` | `remote` or `branch` contains characters outside the safe token set. |
135
- | `absolute_git_roots_nested_or_preset_conflict` | `absoluteGitRoots` combined with `nestedRoots` or `preset`. |
136
- | `absolute_git_roots_exclusive` | `absoluteGitRoots` combined with `workspaceRoot`, `rootIndex`, or `allWorkspaceRoots: true`. |
137
- | `absolute_git_roots_preset_conflict` | `absoluteGitRoots` combined with a `preset` argument. |
138
- | `invalid_absolute_git_root` | An `absoluteGitRoots` entry is not a git-recognized directory. |
139
- | `absolute_git_roots_too_many` | More than 256 entries in `absoluteGitRoots`. |
140
- | `absolute_git_roots_empty` | `absoluteGitRoots` resolved to zero toplevels. |
141
- | `root_index_out_of_range` | `rootIndex` exceeds the number of MCP file roots. |
135
+ | `unsafe_ref_token` | A `compareRefs.left` / `compareRefs.right` token failed `isSafeGitAncestorRef` validation. |
136
+ | `root_list_nested_or_preset_conflict` | `root` array combined with `nestedRoots` or `preset`. |
137
+ | `invalid_root_path` | A `root` array entry is empty or not a git-recognized directory. |
138
+ | `root_list_too_many` | More than 256 entries in the `root` array. |
139
+ | `root_list_empty` | The `root` array resolved to zero git toplevels. |
142
140
  | `preset_not_found` | Named preset does not exist in the preset file. |
143
- | `invalid_json` | Preset file contains invalid JSON. |
144
- | `invalid_schema` | Preset file fails schema validation. |
141
+ | `preset_file_invalid` | Preset file failed to load. Discriminator `kind`: `"invalid_json"` (parse failure; may include `message`) or `"schema"` (Zod validation failure; may include `issues`). Always includes `presetFile`. |
145
142
 
146
143
  Skip entries (individual repos that could not be inventoried) appear inline in `entries[*]` with `skipReason` rather than as top-level errors.
147
144
 
@@ -154,10 +151,7 @@ Skip entries (individual repos that could not be inventoried) appear inline in `
154
151
  | `pairs` | `{left: string, right: string, label?: string}[]` | Path pairs to compare. `left` and `right` are relative to the workspace git toplevel. `label` is optional display name; defaults to `"left / right"`. At least one pair required (via inline `pairs` or `preset`). |
155
152
  | `preset` | string | Preset name from `.rethunk/git-mcp-presets.json`. Loads `parityPairs` from the preset's entry. |
156
153
  | `presetMerge` | boolean | Default `false`. When `true`, merge inline `pairs` with preset pairs instead of replacing. |
157
- | `workspaceRoot` | string | Explicit root; highest priority. |
158
- | `rootIndex` | int | Pick one of several MCP roots (0-based). |
159
- | `allWorkspaceRoots` | boolean | `false` | Fan out across all MCP roots. |
160
- | `absoluteGitRoots` | string[] | Explicit list of absolute paths; replaces normal workspace pick. Useful for checking parity across sibling clones. |
154
+ | `root` | string \| string[] \| `"*"` | Repo path (string), explicit list of repo paths (array, max 256), or `"*"` for every MCP root. Default: first MCP root / cwd. |
161
155
  | `format` | `"markdown"` \| `"json"` | Output format. Default: `"markdown"`. |
162
156
 
163
157
  ### `git_parity` — JSON shape (`format: "json"`)
@@ -165,7 +159,7 @@ Skip entries (individual repos that could not be inventoried) appear inline in `
165
159
  ```json
166
160
  {
167
161
  "parity": [{
168
- "workspace_root": "/abs/path",
162
+ "workspaceRoot": "/abs/path",
169
163
  "status": "MISMATCH",
170
164
  "pairs": [
171
165
  {
@@ -188,7 +182,7 @@ Skip entries (individual repos that could not be inventoried) appear inline in `
188
182
  }
189
183
  ```
190
184
 
191
- `status` is `"OK"` when every pair matches, `"MISMATCH"` when any pair differs or errors. On a match, `sha` carries the common HEAD SHA. On a mismatch, `leftSha` / `rightSha` carry the differing SHAs. On error (path escape or `git rev-parse HEAD` failure), `error` carries a description string and both SHA fields are absent. `presetSchemaVersion` is present when a preset was loaded.
185
+ `status` is `"OK"` when every pair matches, `"MISMATCH"` when any pair differs or errors. On a match, `sha` carries the common HEAD SHA. On a mismatch, `leftSha` / `rightSha` carry the differing SHAs. On error (path escape, non-git path, or `git rev-parse HEAD` failure), `error` carries a plain description string (e.g. `not a git repository: <path>`) and both SHA fields are absent — never a nested minified JSON blob. `presetSchemaVersion` is present when a preset was loaded.
192
186
 
193
187
  ### `git_parity` — error codes
194
188
 
@@ -196,15 +190,12 @@ Skip entries (individual repos that could not be inventoried) appear inline in `
196
190
  |------|---------|
197
191
  | `git_not_found` | `git` binary not on `PATH`. |
198
192
  | `no_pairs` | Neither inline `pairs` nor a preset with `parityPairs` was supplied. |
199
- | `absolute_git_roots_exclusive` | `absoluteGitRoots` combined with `workspaceRoot`, `rootIndex`, or `allWorkspaceRoots: true`. |
200
- | `absolute_git_roots_preset_conflict` | `absoluteGitRoots` combined with a `preset` argument. |
201
- | `invalid_absolute_git_root` | An `absoluteGitRoots` entry is not a git-recognized directory. |
202
- | `absolute_git_roots_too_many` | More than 256 entries in `absoluteGitRoots`. |
203
- | `absolute_git_roots_empty` | `absoluteGitRoots` resolved to zero toplevels. |
204
- | `root_index_out_of_range` | `rootIndex` exceeds the number of MCP file roots. |
193
+ | `root_list_preset_conflict` | `root` array combined with a `preset` argument. |
194
+ | `invalid_root_path` | A `root` array entry is empty or not a git-recognized directory. |
195
+ | `root_list_too_many` | More than 256 entries in the `root` array. |
196
+ | `root_list_empty` | The `root` array resolved to zero git toplevels. |
205
197
  | `preset_not_found` | Named preset does not exist in the preset file. |
206
- | `invalid_json` | Preset file contains invalid JSON. |
207
- | `invalid_schema` | Preset file fails schema validation. |
198
+ | `preset_file_invalid` | Preset file failed to load. Discriminator `kind`: `"invalid_json"` or `"schema"` (see `git_inventory`). |
208
199
 
209
200
  Path-escape and `rev-parse` failures are reported inline in `pairs[*].error`, not as top-level error codes.
210
201
 
@@ -214,10 +205,7 @@ Path-escape and `rev-parse` failures are reported inline in `pairs[*].error`, no
214
205
 
215
206
  | Parameter | Type | Notes |
216
207
  |-----------|------|-------|
217
- | `workspaceRoot` | string | Explicit root; highest priority. |
218
- | `rootIndex` | int | Pick one of several MCP roots (0-based). |
219
- | `allWorkspaceRoots` | boolean | Default `false`. Fan out across all MCP roots. |
220
- | `absoluteGitRoots` | string[] | Explicit list; replaces normal workspace pick. |
208
+ | `root` | string \| string[] \| `"*"` | Repo path (string), explicit list of repo paths (array, max 256), or `"*"` for every MCP root. Default: first MCP root / cwd. |
221
209
  | `format` | `"markdown"` \| `"json"` | Output format. Default: `"markdown"`. |
222
210
 
223
211
  ### `list_presets` — JSON shape (`format: "json"`)
@@ -250,40 +238,43 @@ Path-escape and `rev-parse` failures are reported inline in `pairs[*].error`, no
250
238
  |------|---------|
251
239
  | `git_not_found` | `git` binary not on `PATH`. |
252
240
  | `not_a_git_repository` | Root is not inside a git repository (reported inline in the `roots[*].error` field, not top-level). |
253
- | `invalid_json` | Preset file is not valid JSON (inline in `roots[*].error`). |
254
- | `invalid_schema` | Preset file fails schema validation (inline in `roots[*].error`). |
255
- | `absolute_git_roots_exclusive` | `absoluteGitRoots` combined with `workspaceRoot`, `rootIndex`, or `allWorkspaceRoots: true`. |
256
- | `invalid_absolute_git_root` | An `absoluteGitRoots` entry is not a git-recognized directory. |
257
- | `absolute_git_roots_too_many` | More than 256 entries in `absoluteGitRoots`. |
258
- | `absolute_git_roots_empty` | `absoluteGitRoots` resolved to zero toplevels. |
259
- | `root_index_out_of_range` | `rootIndex` exceeds the number of MCP file roots. |
241
+ | `preset_file_invalid` | Preset file failed to load (inline in `roots[*].error`). Discriminator `kind`: `"invalid_json"` or `"schema"`. |
242
+ | `invalid_root_path` | A `root` array entry is empty or not a git-recognized directory. |
243
+ | `root_list_too_many` | More than 256 entries in the `root` array. |
244
+ | `root_list_empty` | The `root` array resolved to zero git toplevels. |
260
245
 
261
246
  ---
262
247
 
263
248
  ## JSON responses
264
249
 
265
- Tool JSON bodies are minified and contain only the payload — no `rethunkGitMcp` envelope. Current `MCP_JSON_FORMAT_VERSION` is **`"3"`** (exported constant in `src/server.ts`); the version string is surfaced in the FastMCP `instructions` field and is therefore discoverable via the MCP `initialize` response. Payload keys (`groups`, `inventories`, `parity`, `roots`) are stable within a given format version. Preset-related responses may include **`presetSchemaVersion`**.
250
+ Tool JSON bodies are minified and contain only the payload — no `rethunkGitMcp` envelope. Current `MCP_JSON_FORMAT_VERSION` is **`"6"`** (exported constant in `src/server.ts`); the version string is surfaced in the FastMCP `instructions` field and is therefore discoverable via the MCP `initialize` response. Payload keys (`groups`, `inventories`, `parity`, `roots`) are stable within a given format version. Preset-related responses may include **`presetSchemaVersion`**.
251
+
252
+ v6 changes from v5: the tool surface shrank to **24 tools** — `git_fetch`, `git_remote`, `git_describe`, `git_stash_list`, `git_reflog`, `git_branch_list`, and `git_worktree_list` were removed (thin wrappers over tiny-output git commands; see [CONTRIBUTING.md — Tool inclusion criteria](../CONTRIBUTING.md#tool-inclusion-criteria)); `git_grep` dropped content mode — `pickaxe` is now required, `pattern`/`filesOnly` are gone, and `results[*]` always carries `commits[]` (never `matches`/`files`); the `pattern_or_pickaxe_required` error code was removed.
253
+
254
+ v5 changes from v4: `batch_commit` successful `results[*]` entries drop the echoed `message`/`files` (the caller already supplied both in the request); failing entries still carry `message`/`files` alongside `error`/`detail` for diagnosis.
255
+
256
+ v4 changes from v3: `git_blame` output is run-length grouped (`lines[]` with per-line commit metadata → `groups[]` with metadata once per contiguous same-commit run); `git_diff_summary` per-file `truncated` is omitted when `false`.
266
257
 
267
258
  The package also ships **`tool-parameters.schema.json`**, generated from the registered Zod parameter schemas via `bun run schema:tools`, plus the published **`schemas/`** directory (`schemas/index.json` + one JSON Schema per tool) via `bun run schema:individual`. Connected MCP clients should still prefer live schema discovery from `initialize` / tool listing; the shipped artifacts are for offline inspection, drift checks, and code generation.
268
259
 
269
- ### v2/v3 field omission (consumer contract)
260
+ ### Field omission (consumer contract, v2+)
270
261
 
271
- To keep responses compact, **optional fields are omitted when they would be empty, `null`, or `false`** — they are not emitted as `null`. Consumers must test for *presence*, not compare to `null`.
262
+ To keep responses compact, **optional fields are usually omitted when they would be empty, `null`, or `false`**. Consumers must test for *presence*, not compare to `null`, except where a tool documents an intentional `null` (documented exception: `list_presets` emits `gitTop: null` when the workspace is not inside a git repo).
272
263
 
273
264
  **`git_inventory` → `inventories[*]`**
274
265
 
275
- - Always present: `workspace_root`, `entries`.
266
+ - Always present: `workspaceRoot`, `entries`.
276
267
  - Omitted when not applicable: `presetSchemaVersion`, `nestedRootsTruncated`, `nestedRootsOmittedCount`, and the whole `upstream` object (emitted only when a fixed `remote`/`branch` pair was supplied; in `auto` mode it is absent).
277
268
 
278
269
  **`git_inventory` → `entries[*]` (`InventoryEntryJson`)**
279
270
 
280
271
  - Always present: `label`, `path`, `upstreamMode` (`"auto"` or `"fixed"`).
281
- - Optional (omitted when empty/absent): `branchStatus`, `headAbbrev`, `upstreamRef`, `ahead`, `behind`, `upstreamNote`, `detached` (only emitted as `true`), `skipReason` (only on skipped entries).
272
+ - Optional (omitted when empty/absent): `branchStatus`, `headAbbrev`, `upstreamRef`, `ahead`, `behind`, `upstreamNote`, `compareRefs` (when requested), `detached` (only emitted as `true`), `skipReason` (only on skipped entries).
282
273
  - **Removed in v2:** `shortStatus`. The porcelain entries now live inside `branchStatus` (the full `git status --short -b` body — branch header line followed by porcelain lines).
283
274
 
284
275
  **Errors** (any tool)
285
276
 
286
- - Error payloads carry an `error` code string and any structured context (e.g. `preset`, `presetFile`). The old free-text `message` field is **removed** for self-describing codes (`git_not_found`, `remote_branch_mismatch`, `invalid_remote_or_branch`, `no_pairs`, `preset_not_found` *missing* case). It is retained only where it carries parse output (the `invalid_json` preset branch).
277
+ - Error payloads carry an `error` code string and any structured context (e.g. `preset`, `presetFile`, `kind`). The old free-text `message` field is **removed** for self-describing codes (`git_not_found`, `remote_branch_mismatch`, `invalid_remote_or_branch`, `no_pairs`, `preset_not_found` *missing* case). For preset load failures the wire code is always `preset_file_invalid` with discriminator `kind` (`"invalid_json"` or `"schema"`); `message` is retained on the `invalid_json` kind (parse output) and `issues` on the `schema` kind.
287
278
 
288
279
  **When to bump `MCP_JSON_FORMAT_VERSION` or change payload shape:** [AGENTS.md](../AGENTS.md) — *Changing contracts*. The constant lives in `src/server.ts` and is surfaced via the server `instructions` field (discoverable from the MCP `initialize` response).
289
280
 
@@ -292,15 +283,14 @@ To keep responses compact, **optional fields are omitted when they would be empt
292
283
  | Parameter | Type | Default | Notes |
293
284
  |-----------|------|---------|-------|
294
285
  | `since` | string | `"7.days"` | Passed to `git log --since=`. Accepts ISO timestamps (`2026-04-01T00:00:00Z`) or git relative forms (`48.hours`, `2.weeks.ago`). |
295
- | `paths` | string[] | (all) | Restrict to commits touching these paths (appended after `--`). |
286
+ | `paths` | string[] | (all) | Restrict to commits touching these paths (appended after `--`). Each entry is confined with `resolvePathForRepo` / `assertRelativePathUnderTop` → `path_escapes_repo` on escape. |
287
+ | `follow` | boolean | `false` | Pass `--follow` for rename-aware history. Requires **exactly one** `paths` entry; otherwise top-level `invalid_paths` with `detail: "follow requires exactly one path"`. |
296
288
  | `grep` | string | — | Filter by commit message regex (git `--grep`, always case-insensitive). |
297
289
  | `author` | string | — | Filter by author name or email (`--author=`). |
298
290
  | `maxCommits` | int | `50` | Max commits per root. Hard cap: `500`. |
299
291
  | `branch` | string | `HEAD` | Ref/branch to log from. |
300
- | `workspaceRoot` | string | — | Explicit root; highest priority. |
301
- | `rootIndex` | int | | Pick one of several MCP roots (0-based). |
302
- | `allWorkspaceRoots` | boolean | `false` | Fan out across all MCP roots. |
303
- | `format` | `"markdown"` \| `"json"` \| `"oneline"` | `"markdown"` | Output format. `oneline` returns `<sha7> <subject>` per line with no headers (single root) or `### repo (branch)` separators per group (multi-root). Lowest-token option for post-commit verification. |
292
+ | `root` | string \| string[] \| `"*"` | — | Repo path (string), explicit list of repo paths (array, max 256), or `"*"` for every MCP root. Default: first MCP root / cwd. |
293
+ | `format` | `"markdown"` \| `"json"` \| `"oneline"` | `"markdown"` | Output format. `oneline` returns the first 7 characters of each commit SHA plus the subject (`<abbrev> <subject>`) per line with no headers (single root) or `### repo (branch)` separators per group (multi-root). Display shorthand only — the JSON field remains full `sha` (v3 removed the old `sha7` field). Lowest-token option for post-commit verification. |
304
294
 
305
295
  ### `git_log` — JSON shape (`format: "json"`)
306
296
 
@@ -337,25 +327,68 @@ v2 field-omission rules still apply: `filesChanged`, `insertions`, `deletions` o
337
327
  | `git_not_found` | `git` binary not on `PATH`. |
338
328
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
339
329
  | `invalid_since` | The `since` string contains shell metacharacters and was rejected. |
340
- | `invalid_paths` | One of the `paths` entries contains shell metacharacters and was rejected. |
330
+ | `invalid_paths` | A `paths` entry failed validation, or `follow: true` was set without exactly one `paths` entry (`detail: "follow requires exactly one path"`). |
331
+ | `path_escapes_repo` | A `paths` entry resolves outside that root's git toplevel. |
341
332
  | `unsafe_ref_token` | `branch` contains characters outside the argv-safe subset. |
342
333
  | `git_log_failed` | `git log` exited non-zero (e.g. unknown branch ref). |
343
- | `root_index_out_of_range` | `rootIndex` exceeds the number of MCP file roots. |
344
- | `absolute_git_roots_exclusive` | `absoluteGitRoots` was combined with `workspaceRoot`, `rootIndex`, or `allWorkspaceRoots: true`. |
345
- | `absolute_git_roots_preset_conflict` | `absoluteGitRoots` was combined with a `preset` argument (root resolution). |
346
- | `invalid_absolute_git_root` | An `absoluteGitRoots` entry is empty, not inside a git worktree, or not a directory git recognizes. |
347
- | `absolute_git_roots_too_many` | More than 256 entries in `absoluteGitRoots`. |
348
- | `absolute_git_roots_empty` | `absoluteGitRoots` produced zero git toplevels after resolution. |
349
- | `absolute_git_roots_single_repo_only` | A single-repo tool received `absoluteGitRoots` resolving to more than one distinct git toplevel. |
334
+ | `invalid_root_path` | A `root` array entry is empty, not inside a git worktree, or not a directory git recognizes. |
335
+ | `root_list_too_many` | More than 256 entries in the `root` array. |
336
+ | `root_list_empty` | The `root` array produced zero git toplevels after resolution. |
337
+
338
+ ### `git_grep` parameters
339
+
340
+ | Parameter | Type | Default | Notes |
341
+ |-----------|------|---------|-------|
342
+ | `pickaxe` | `{ mode: "S" \| "G", term: string }` | — | **Required.** Pickaxe history search (`git log -S` / `-G`): `S` finds commits that changed the occurrence count of `term`; `G` finds commits whose diff lines match the `term` regex. JSON results are `commits[]` of `{ sha, subject }` per root. |
343
+ | `ref` | string | — | Commit/branch/tag to use as the history tip. Validated as a safe ref token (`isSafeGitAncestorRef`); rejects `--`-prefixed or otherwise unsafe tokens. |
344
+ | `paths` | string[] | — | Limit history to these paths. Each must resolve within the repo root (`resolvePathForRepo` / `assertRelativePathUnderTop`); escaping paths are rejected per-root. |
345
+ | `ignoreCase` | boolean | `false` | Case-insensitive match (`-i`; affects `G` mode regexes). |
346
+ | `maxMatches` | integer | `200` | Cap on commits per root. Hard cap `1000`. |
347
+ | `root` | string \| string[] \| `"*"` | — | Repo path, array of paths (max 256), or `"*"` for every MCP root. Default: first MCP root / cwd. |
348
+ | `format` | `"markdown"` \| `"json"` | `"markdown"` | Output format. |
349
+
350
+ Content-mode working-tree search (`pattern`, `filesOnly`, ref-tree search) was **removed in v6** — it duplicated the native grep/rg tooling every MCP client already ships. Pickaxe history is the part with no native equivalent.
351
+
352
+ ### `git_grep` — JSON shape (`format: "json"`)
353
+
354
+ ```json
355
+ {
356
+ "results": [
357
+ {
358
+ "root": "/abs/workspace",
359
+ "repo": "workspace",
360
+ "commits": [
361
+ { "sha": "a1b2c3d4e5f6…", "subject": "feat: introduce needle" }
362
+ ],
363
+ "truncated": true
364
+ }
365
+ ]
366
+ }
367
+ ```
368
+
369
+ One `results` entry per resolved root, each carrying `commits[]` of `{ sha, subject }` hits. `truncated: true` is present only when the per-root commit count exceeded `maxMatches`. A root that fails to resolve or whose git invocation errors carries `error` (and optional `detail`) instead of `commits`. Zero pickaxe hits is **not** an error — `commits` is simply empty.
370
+
371
+ ### `git_grep` — error codes
372
+
373
+ | Code | Meaning |
374
+ |------|---------|
375
+ | `git_not_found` | `git` binary not on `PATH`. |
376
+ | `not_a_git_repository` | A resolved root is not inside a git repository. |
377
+ | `unsafe_ref_token` | `ref` failed `isSafeGitAncestorRef` validation. |
378
+ | `path_escapes_repo` | A `paths` entry resolves outside that root's git toplevel. |
379
+ | `git_grep_failed` | Pickaxe `git log` exited non-zero (bad regex, unknown ref, etc.). |
380
+ | `invalid_root_path` / `root_list_too_many` / `root_list_empty` | `root` array validation (shared fan-out behavior). |
381
+
382
+ ---
350
383
 
351
- ### `git_parity` — `absoluteGitRoots` example
384
+ ### `git_parity` — `root` array example
352
385
 
353
- Use **`absoluteGitRoots`** when the same parity pair should be checked across sibling clones that are not all MCP workspace roots:
386
+ Pass a `root` array when the same parity pair should be checked across sibling clones that are not all MCP workspace roots:
354
387
 
355
388
  ```json
356
389
  {
357
390
  "format": "json",
358
- "absoluteGitRoots": [
391
+ "root": [
359
392
  "/usr/local/src/com.github/Rethunk-AI/mcp-multi-root-git",
360
393
  "/usr/local/src/com.github/Rethunk-AI/rethunk-github-mcp"
361
394
  ],
@@ -363,19 +396,20 @@ Use **`absoluteGitRoots`** when the same parity pair should be checked across si
363
396
  }
364
397
  ```
365
398
 
366
- The response contains one **`parity[]`** entry per resolved git toplevel. `absoluteGitRoots` cannot be combined with `preset`; pass inline `pairs` for sibling-clone batches.
399
+ The response contains one **`parity[]`** entry per resolved git toplevel. An array `root` cannot be combined with `preset`; pass inline `pairs` for sibling-clone batches.
400
+
401
+ **Diff-family filtering divergence (intentional):** `git_diff_summary` filters client-side via a `fileFilter` glob applied to the summarized file list; `git_diff` and `git_show` instead take server-confined `paths[]` (each entry validated with `resolvePathForRepo`/`assertRelativePathUnderTop` and rejected with `path_escapes_repo` on escape). Don't expect glob syntax on `git_diff`/`git_show` `paths[]`, and don't expect path confinement guarantees on `git_diff_summary`'s `fileFilter`.
367
402
 
368
403
  ### `git_diff_summary` — parameters
369
404
 
370
405
  | Parameter | Type | Default | Notes |
371
406
  |-----------|------|---------|-------|
372
- | `range` | string | unstaged | Diff range. `"staged"` / `"cached"` for index; `"HEAD"` for last commit; `"A..B"` or `"A...B"` for revision ranges; single ref. Default: unstaged working-tree changes. |
407
+ | `range` | string | unstaged | Diff range. `"staged"` / `"cached"` for index; `"HEAD"` for last commit; `"A..B"` or `"A...B"` for revision ranges; single ref. Ancestor notation (`HEAD~3`, `main^2`) is accepted on any endpoint. Default: unstaged working-tree changes. |
373
408
  | `fileFilter` | string | — | Glob pattern to restrict output to matching files (e.g. `"*.ts"`, `"src/**"`). |
374
409
  | `maxLinesPerFile` | int | `50` | Max diff lines to include per file (1–2000). |
375
410
  | `maxFiles` | int | `30` | Max files to include in output (1–500). |
376
411
  | `excludePatterns` | string[] | lock files, dist, vendor | Glob patterns to exclude. Defaults to `*.lock`, `*.lockb`, `bun.lock`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `*.min.js`, `*.min.css`, `vendor/**`, `node_modules/**`, `dist/**`. Pass an empty array to disable. |
377
- | `workspaceRoot` | string | — | Explicit root; highest priority. |
378
- | `rootIndex` | int | — | Pick one of several MCP roots (0-based). |
412
+ | `workspaceRoot` | string | — | Repo path. Default: first MCP root / cwd. |
379
413
  | `format` | `"markdown"` \| `"json"` | `"markdown"` | Output format. |
380
414
 
381
415
  ### `git_diff_summary` — JSON shape (`format: "json"`)
@@ -391,7 +425,6 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
391
425
  "status": "modified",
392
426
  "additions": 8,
393
427
  "deletions": 3,
394
- "truncated": false,
395
428
  "diff": "@@ -1,3 +1,8 @@\n-const x = 1;\n+const x = 2;"
396
429
  }],
397
430
  "truncatedFiles": 1,
@@ -399,7 +432,7 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
399
432
  }
400
433
  ```
401
434
 
402
- `status` is one of `"modified"`, `"added"`, `"deleted"`, `"renamed"`. `oldPath` is present only for renamed files. `truncatedFiles` and `excludedFiles` are omitted when zero/empty (v2 field-omission contract).
435
+ `status` is one of `"modified"`, `"added"`, `"deleted"`, `"renamed"`. `oldPath` is present only for renamed files. Per-file `truncated` is present (`true`) only when the diff body was cut at `maxLinesPerFile` (v4). `totalFiles` / `totalAdditions` / `totalDeletions` count the post-`excludePatterns`+`fileFilter` set (before `maxFiles` display truncation). `excludedFiles` includes both exclude-pattern hits and `fileFilter` drops. `truncatedFiles` is the count omitted by `maxFiles`. `truncatedFiles` and `excludedFiles` are omitted when zero/empty (field-omission contract).
403
436
 
404
437
  ### `git_diff_summary` — error codes
405
438
 
@@ -416,27 +449,29 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
416
449
 
417
450
  | Parameter | Type | Default | Notes |
418
451
  |-----------|------|---------|-------|
419
- | `base` | string | — | Base ref for a revision diff. When omitted with no `staged`, the tool shows unstaged changes. |
420
- | `head` | string | `HEAD` | Head ref for a revision diff. Used only when `base` is provided. |
452
+ | `base` | string | — | Base ref for a revision diff. When omitted with no `staged`, the tool shows unstaged changes. Ancestor notation (`HEAD~3`, `main^2`) is accepted. |
453
+ | `head` | string | `HEAD` | Head ref for a revision diff. Used only when `base` is provided. Ancestor notation (`HEAD~3`, `main^2`) is accepted. |
421
454
  | `path` | string | — | Optional single file path to scope the diff. Confined to the repo (`path_escapes_repo` on escape). |
422
455
  | `paths` | string[] | — | Multiple file paths to scope the diff; unioned with `path` (deduped). Each confined to the repo. |
423
456
  | `unified` | integer | — | Context lines around each change (passed as `-U<n>`, 0–100). Omit for git's default (3). |
424
457
  | `staged` | boolean | `false` | When `true`, runs `git diff --staged`. Ignored when `base` is provided. |
425
- | `workspaceRoot` | string | | Explicit root; highest priority. |
426
- | `rootIndex` | int | — | Pick one of several MCP roots (0-based). |
458
+ | `maxBytes` | integer | `512000` | Cap on UTF-8 bytes of returned diff text (1024–10000000). Oversized output is truncated; JSON emits `truncated: true` (omitted when false). |
459
+ | `workspaceRoot` | string | — | Repo path. Default: first MCP root / cwd. |
427
460
  | `format` | `"markdown"` \| `"json"` | `"markdown"` | Output format. |
428
461
 
429
- `git_diff` is a single-repo tool. It does not accept `absoluteGitRoots` or `allWorkspaceRoots`; use `workspaceRoot` or `rootIndex` to select the target repo.
462
+ `git_diff` is a single-repo tool; use `workspaceRoot` to select the target repo. When `base` is set, `staged` is ignored and `head` is used; `head` alone (without `base`) still yields the unstaged working-tree diff.
430
463
 
431
464
  ### `git_diff` — JSON shape (`format: "json"`)
432
465
 
433
466
  ```json
434
467
  {
435
468
  "range": "HEAD~1..HEAD (src/server.ts)",
436
- "diff": "diff --git a/src/server.ts b/src/server.ts\n..."
469
+ "diff": "diff --git a/src/server.ts b/src/server.ts\n...",
470
+ "truncated": true
437
471
  }
438
472
  ```
439
473
 
474
+ `truncated: true` is present only when the `maxBytes` cap fired.
440
475
  ### `git_diff` — error codes
441
476
 
442
477
  | Code | Meaning |
@@ -452,11 +487,11 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
452
487
 
453
488
  | Parameter | Type | Notes |
454
489
  |-----------|------|-------|
455
- | `ref` | string | Commit, branch, tag, or other git rev-spec to inspect. |
456
- | `path` | string | Optional single path. When provided, the response shows that path's content at `ref` instead of the full commit diff. |
490
+ | `ref` | string | Commit, branch, tag, or other git rev-spec to inspect. Ancestor notation (`HEAD~3`, `main^2`) is accepted. |
491
+ | `path` | string | Optional single path. When provided, filters the shown patch/stat to that path (`git show <ref> -- <path>`), not a raw blob (`ref:path`) checkout. |
457
492
  | `stat` | boolean | When `true`, runs `git show --stat` — commit message plus per-file diffstat, no full patch (`statOutput` in JSON). |
458
493
  | `paths` | string[] | Filter the shown patch/stat to these repo-relative paths; unioned with `path`. Each confined to the repo. |
459
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
494
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
460
495
 
461
496
  ### `git_show` — JSON shape (`format: "json"`)
462
497
 
@@ -477,69 +512,50 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
477
512
  |------|---------|
478
513
  | `unsafe_ref_token` | `ref` contains characters outside the argv-safe subset. |
479
514
  | `path_escapes_repo` | `path` resolves outside the git toplevel. |
480
- | `git_show_failed` | `git show` exited non-zero (e.g. unknown ref). |
515
+ | `git_show_failed` | `git show` exited non-zero (e.g. unknown ref). Includes `detail` (stderr/stdout trim), matching peer tools. |
481
516
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
482
517
 
483
518
  ---
484
519
 
485
- ### `git_stash_list` — parameters
486
-
487
- | Parameter | Type | Notes |
488
- |-----------|------|-------|
489
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
490
-
491
- ### `git_stash_list` — JSON shape (`format: "json"`)
492
-
493
- ```json
494
- {
495
- "stashes": [
496
- { "index": 0, "message": "WIP on main: abc1234 feat: add tool", "sha": "abc1234" }
497
- ]
498
- }
499
- ```
500
-
501
- ### `git_stash_list` — error codes
502
-
503
- | Code | Meaning |
504
- |------|---------|
505
- | `stash_list_failed` | `git stash list` failed unexpectedly. |
506
- | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
507
-
508
- ---
509
-
510
- ### `git_fetch` — parameters
520
+ ### `git_conflicts` — parameters
511
521
 
512
522
  | Parameter | Type | Default | Notes |
513
523
  |-----------|------|---------|-------|
514
- | `remote` | string | `"origin"` | Remote to fetch from. |
515
- | `branch` | string | | Optional branch/ref to fetch from that remote. |
516
- | `prune` | boolean | `false` | Pass `--prune` to remove deleted remote-tracking refs. |
517
- | `tags` | boolean | `false` | Pass `--tags` to also fetch all tags. |
518
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
524
+ | `withHunks` | boolean | `true` | Parse conflict-marker hunks per file. Set `false` to return just the conflicted path list (skips reading file contents). |
525
+ | `maxLinesPerFile` | int | `200` | Cap on lines scanned per file (1–2000) before marking `truncated: true` on that file and dropping any hunk still open at the cutoff. |
526
+ | `workspaceRoot`, `format` | | | Standard single-repo pick + output format. |
519
527
 
520
- ### `git_fetch` — JSON shape (`format: "json"`)
528
+ ### `git_conflicts` — JSON shape (`format: "json"`)
521
529
 
522
530
  ```json
523
531
  {
524
- "ok": true,
525
- "remote": "origin",
526
- "updatedRefs": ["abc1234..def5678 main -> origin/main"],
527
- "newRefs": ["[new tag] v2.0.0 -> v2.0.0"],
528
- "updated": [{ "ref": "refs/remotes/origin/main", "oldSha": "abc1234…", "newSha": "def5678…", "flag": " " }],
529
- "created": [{ "ref": "refs/tags/v2.0.0", "newSha": "0a1b2c3…", "flag": "*" }],
530
- "pruned": [{ "ref": "refs/remotes/origin/old" }],
531
- "output": "From origin\n..."
532
+ "state": "merge",
533
+ "files": [
534
+ {
535
+ "path": "shared.txt",
536
+ "hunks": [
537
+ {
538
+ "startLine": 2,
539
+ "ours": "BETA",
540
+ "theirs": "ALPHA",
541
+ "oursLabel": "HEAD",
542
+ "theirsLabel": "feature"
543
+ }
544
+ ]
545
+ }
546
+ ]
532
547
  }
533
548
  ```
534
549
 
535
- `updated` / `created` / `pruned` are structured ref deltas parsed from `git fetch --porcelain` (git 2.41), each omitted when empty. On older git the `--porcelain` option is detected as unsupported and the tool falls back to a plain fetch, omitting the structured arrays; the `updatedRefs` / `newRefs` string fields are always present for back-compat. Fetch failures are reported as `ok: false` with the captured git output in `output`.
550
+ `state` is omitted when no merge/cherry-pick/revert/rebase is in progress (e.g. conflicts were left by some other means). A clean repo returns `{ "files": [] }`. Each file entry's `hunks` is omitted when empty (unreadable, binary, or no markers found file still listed by `path` alone); `base` appears only for diff3-style markers (`|||||||`); `oursLabel`/`theirsLabel` are omitted when git did not attach a label to that marker. Per-file optional `error: "path_escapes_repo"` when a conflict path fails confinement (rare; paths normally come from git). Incomplete conflict markers within the scan window set `truncated: true` (same as line-cap truncation).
551
+
552
+ ### `git_conflicts` — error codes
536
553
 
537
- ### `git_fetch`error codes
554
+ Only the shared single-repo prelude errors apply the tool itself never fails on conflict state:
538
555
 
539
556
  | Code | Meaning |
540
557
  |------|---------|
541
- | `unsafe_remote_token` | `remote` contains characters outside the argv-safe subset. |
542
- | `unsafe_ref_token` | `branch` contains characters outside the argv-safe subset. |
558
+ | `git_not_found` | `git` binary not on `PATH`. |
543
559
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
544
560
 
545
561
  ---
@@ -549,10 +565,11 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
549
565
  | Parameter | Type | Default | Notes |
550
566
  |-----------|------|---------|-------|
551
567
  | `path` | string | — | **Required.** Repo-relative file to annotate. Confined to the repo (`path_escapes_repo` on escape). |
552
- | `ref` | string | working tree | Commit-ish to blame at. Validated as a safe ref token. |
553
- | `startLine` | int | — | Start of a line range (`-L`). Requires `endLine`. |
554
- | `endLine` | int | — | End of the line range, inclusive. Requires `startLine`. |
555
- | `workspaceRoot`, `rootIndex`, `format` | | Standard single-repo workspace pick + output format. |
568
+ | `ref` | string | working tree | Commit-ish to blame at. Validated as a safe ref token. Ancestor notation (`HEAD~3`, `main^2`) is accepted. |
569
+ | `startLine` | int | — | Start of a line range (`-L`). Requires `endLine`. Max `1000000`. |
570
+ | `endLine` | int | — | End of the line range, inclusive. Requires `startLine`. Max `1000000`. |
571
+ | `maxLines` | int | `2000` | Max blamed lines to return (1–10000). Excess lines are dropped and signalled via `truncated`/`omittedLines`. |
572
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
556
573
 
557
574
  ### `git_blame` — JSON shape (`format: "json"`)
558
575
 
@@ -560,13 +577,26 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
560
577
  {
561
578
  "ref": "HEAD",
562
579
  "path": "src/server.ts",
563
- "lines": [
564
- { "line": 1, "sha": "a1b2c3d4…", "author": "Damon Blais", "date": "2026-04-12T18:32:01-07:00", "summary": "feat: add tool", "content": "import { FastMCP } from \"fastmcp\";" }
565
- ]
580
+ "groups": [
581
+ {
582
+ "sha": "a1b2c3d4…",
583
+ "author": "Damon Blais",
584
+ "date": "2026-04-12T18:32:01-07:00",
585
+ "summary": "feat: add tool",
586
+ "startLine": 1,
587
+ "endLine": 2,
588
+ "lines": [
589
+ { "line": 1, "content": "import { FastMCP } from \"fastmcp\";" },
590
+ { "line": 2, "content": "" }
591
+ ]
592
+ }
593
+ ],
594
+ "truncated": true,
595
+ "omittedLines": 120
566
596
  }
567
597
  ```
568
598
 
569
- `ref` is omitted when blaming the working tree.
599
+ One `groups` entry per **contiguous run of lines last touched by the same commit** (v4 run-length grouping — commit metadata is emitted once per run, not once per line). `ref` is omitted when blaming the working tree. `truncated`/`omittedLines` are omitted unless `maxLines` cut the output.
570
600
 
571
601
  ### `git_blame` — error codes
572
602
 
@@ -580,74 +610,17 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
580
610
 
581
611
  ---
582
612
 
583
- ### `git_branch_list` — parameters
584
-
585
- | Parameter | Type | Default | Notes |
586
- |-----------|------|---------|-------|
587
- | `includeRemotes` | boolean | `false` | Also list remote-tracking branches (`refs/remotes`); symbolic `origin/HEAD` is skipped. |
588
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
589
-
590
- ### `git_branch_list` — JSON shape (`format: "json"`)
591
-
592
- ```json
593
- {
594
- "branches": [
595
- { "name": "main", "sha": "a1b2c3d4…", "current": true, "upstream": "origin/main" },
596
- { "name": "feature/x", "sha": "b2c3d4e5…", "current": false }
597
- ],
598
- "remotes": [{ "name": "origin/main", "sha": "a1b2c3d4…" }]
599
- }
600
- ```
601
-
602
- `upstream` is omitted when a branch has no upstream. `remotes` is present only when `includeRemotes: true`.
603
-
604
- ### `git_branch_list` — error codes
605
-
606
- | Code | Meaning |
607
- |------|---------|
608
- | `branch_list_failed` | `git for-each-ref` exited non-zero. `detail` carries stderr. |
609
- | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
610
-
611
- ---
612
-
613
- ### `git_reflog` — parameters
614
-
615
- | Parameter | Type | Default | Notes |
616
- |-----------|------|---------|-------|
617
- | `ref` | string | `HEAD` | Ref whose reflog to show. Validated as a safe ref token. |
618
- | `maxEntries` | int | `30` | Max entries to return (1–200). |
619
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
620
-
621
- ### `git_reflog` — JSON shape (`format: "json"`)
622
-
623
- ```json
624
- {
625
- "ref": "HEAD",
626
- "entries": [
627
- { "sha": "a1b2c3d4…", "selector": "HEAD@{0}", "message": "commit: feat: add tool" }
628
- ]
629
- }
630
- ```
631
-
632
- ### `git_reflog` — error codes
633
-
634
- | Code | Meaning |
635
- |------|---------|
636
- | `unsafe_ref_token` | `ref` contains characters outside the argv-safe subset. |
637
- | `reflog_failed` | `git reflog show` exited non-zero. `detail` carries stderr. |
638
- | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
639
-
640
- ---
641
-
642
613
  ### `batch_commit` — atomic staging semantics
643
614
 
644
615
  **Critical for AI agents:** Each call to `batch_commit` is **self-contained and atomic per-commit entry**.
645
616
 
646
- - **All files in a single entry are staged together.** When you list `files: ["src/foo.ts", "src/bar.ts"]` in one commit entry, both are staged atomically as a unit with a single `git add` before the commit is created.
617
+ - **All files in a single entry are staged together.** When you list `files: ["src/foo.ts", "src/bar.ts"]` in one commit entry, both are staged as a unit before the commit is created.
618
+ - **Index isolation per commit.** Unrelated paths that were already staged before the call are temporarily unstaged around an **index-based** `git commit` so they are not included. Pathspec/`--only` mode is intentionally avoided — it commits from the worktree and would squash hunk-level (`{ path, lines }`) staging.
647
619
  - **Each commit entry is processed sequentially within the call.** The tool stages files, commits, then moves to the next entry. All entries within a single `batch_commit` call happen in one atomic MCP transaction.
648
620
  - **A single `batch_commit` call cannot be split across multiple MCP calls.** Do NOT attempt incremental staging like "call 1 with file A, then call 2 with file B hoping they stage together." Each call is independent — call 1's commit lands immediately; call 2's changes are a separate transaction.
649
- - **Failed entry stops the batch.** If staging or commit fails on entry N, the tool aborts and skips remaining entries. However, **entries that succeeded before the failure remain committed** — they are not rolled back.
621
+ - **Failed entry stops the batch.** If staging or commit fails on entry N, the tool aborts and skips remaining entries. On mid-entry `stage_failed`, paths already staged for that entry are unstaged (`git restore --staged`). **Entries that succeeded before the failure remain committed** — they are not rolled back.
650
622
  - **Include all files for a logical change in a single `batch_commit` call.** Group related files in each commit entry, list them all in the `files` array, and include all necessary entries in the `commits` array.
623
+ - **dryRun** uses path-scoped `diff --stat`, unstages between entries, and restores the full pre-call index via `write-tree`/`read-tree` (including overlapping pre-staged paths).
651
624
 
652
625
  Example: to commit two related changes atomically, pass both entries in one call:
653
626
  ```json
@@ -665,11 +638,10 @@ Do NOT do this: make two separate calls hoping to stage files incrementally. Tha
665
638
 
666
639
  | Parameter | Type | Notes |
667
640
  |-----------|------|-------|
668
- | `commits` | `{message: string, files: (string \| {path: string, lines: {from: number, to: number}})[]}[]` | Commits to create in order. 1–50 entries. Each `files` entry is either: (a) a path relative to the git root, staged with `git add`; (b) a `{path, lines: {from, to}}` object for hunk-level staging — only unified-diff hunks overlapping the given 1-indexed line range are staged; or (c) a path to a **deleted tracked file** (missing on disk but tracked in HEAD), which is staged as a removal via `git rm --cached` — combining `{path, lines}` with a deleted file is an error. All paths must stay within the git toplevel. |
641
+ | `commits` | `{message: string, files: (string \| {path: string, lines: {from: number, to: number}})[]}[]` | Commits to create in order. 1–50 entries. Each `files` entry is either: (a) a path relative to the git root, staged with `git add`; (b) a `{path, lines: {from, to}}` object for hunk-level staging — only unified-diff hunks overlapping the given 1-indexed line range are staged (`from`/`to` each max `1000000`; `from > to` → `invalid_line_range`); or (c) a path to a **deleted tracked file** (missing on disk but tracked in HEAD), which is staged as a removal via `git rm --cached` — combining `{path, lines}` with a deleted file is an error. All paths must stay within the git toplevel. Rejects `.`, repo-root, and directory pathspecs (`invalid_paths`). |
669
642
  | `push` | `"never"` \| `"after"` | Default `"never"`. `"after"` pushes the current branch to its upstream **once all commits succeed**. Never auto-sets upstream — branches without an upstream fail with `push_no_upstream`. Commits are **not** rolled back on push failure. |
670
643
  | `dryRun` | boolean | Default `false`. When `true`, stages each entry, reports what would be committed (`staged`, `diffStat`), then unstages everything without writing commits. |
671
- | `workspaceRoot` | string | Explicit root; highest priority. |
672
- | `rootIndex` | int | Pick one of several MCP roots (0-based). |
644
+ | `workspaceRoot` | string | Repo path. Default: first MCP root / cwd. |
673
645
  | `format` | `"markdown"` \| `"json"` | Output format. Default: `"markdown"`. |
674
646
 
675
647
  ### `batch_commit` — JSON shape (`format: "json"`)
@@ -682,15 +654,11 @@ Do NOT do this: make two separate calls hoping to stage files incrementally. Tha
682
654
  "results": [{
683
655
  "index": 0,
684
656
  "ok": true,
685
- "sha": "a1b2c3d",
686
- "message": "feat: add foo",
687
- "files": ["src/foo.ts"]
657
+ "sha": "a1b2c3d"
688
658
  }, {
689
659
  "index": 1,
690
660
  "ok": true,
691
- "sha": "b2c3d4e",
692
- "message": "chore: update config",
693
- "files": ["config.json"]
661
+ "sha": "b2c3d4e"
694
662
  }],
695
663
  "push": {
696
664
  "ok": true,
@@ -700,7 +668,9 @@ Do NOT do this: make two separate calls hoping to stage files incrementally. Tha
700
668
  }
701
669
  ```
702
670
 
703
- On first failure `ok` is `false`, `committed` reflects only the entries that succeeded before the error, and the failing entry includes `error` and `detail` fields. Remaining entries are skipped and not included in `results`.
671
+ Successful `results[*]` entries carry only what's needed to confirm the commit landed (`index`, `ok`, `sha`, plus `output` when git printed something) they omit `message`/`files` since the caller already supplied both in the request. Failing entries echo `message` and `files` back (alongside `error`/`detail`) so the caller can identify the failed commit without cross-referencing the request.
672
+
673
+ On first failure `ok` is `false`, `committed` reflects only the entries that succeeded before the error, and the failing entry includes `message`, `files`, `error`, and `detail` fields. Remaining entries are skipped and not included in `results`.
704
674
 
705
675
  When `dryRun: true`, the top-level response includes `dryRun: true`; successful `results[*]` entries omit `sha` and instead include `staged` and `diffStat`.
706
676
 
@@ -711,6 +681,8 @@ The `push` object is present only when `push: "after"` was requested **and** eve
711
681
  | Code | Meaning |
712
682
  |------|---------|
713
683
  | `path_escapes_repository` | One of the listed file paths resolves outside the git toplevel. |
684
+ | `invalid_paths` | A pathspec is `.`, the repo root, or a directory (file paths only). |
685
+ | `invalid_line_range` | A `{path, lines}` entry has `from > to`. |
714
686
  | `stage_failed` | Staging failed. `git add` error for modified/new files; `git rm --cached` error for deleted files (e.g. path never tracked in HEAD); `{path, lines}` on a deleted file. |
715
687
  | `commit_failed` | `git commit` failed (e.g. nothing staged, hooks rejected). |
716
688
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
@@ -731,11 +703,11 @@ The `push` object is present only when `push: "after"` was requested **and** eve
731
703
  |-----------|------|-------|
732
704
  | `sources` | `string[]` | Source branches to merge, in order. 1–20 entries. Each must be a valid git ref token. |
733
705
  | `into` | string | Destination branch. Defaults to the currently checked-out branch. Rejected when HEAD is detached. |
734
- | `strategy` | `"auto"` \| `"ff-only"` \| `"rebase"` \| `"merge"` | Default `"auto"`: cascade **fast-forward → rebase → merge-commit** per source. `"ff-only"` fails on divergence. `"rebase"` rebases source onto destination and fast-forwards; no merge-commit fallback. `"merge"` always creates a merge commit (`--no-ff`). |
706
+ | `strategy` | `"auto"` \| `"ff-only"` \| `"rebase"` \| `"merge"` | Default `"auto"`: cascade **fast-forward → rebase → merge-commit** per source. `"ff-only"` fails on divergence. `"rebase"` rebases source onto destination and fast-forwards; no merge-commit fallback. `"merge"` always creates a merge commit (`--no-ff`). **`auto`/`rebase` rewrite the source branch tip when rebasing** (history rewrite of the source ref), then fast-forward the destination — agents must not treat `auto` as destination-only. |
735
707
  | `message` | string | Merge commit message, used only when a merge commit is created. Defaults to `Merge branch '<source>' into <into>`. |
736
- | `deleteMergedBranches` | boolean | Default `false`. After **all** sources land cleanly, delete each source branch locally (`git branch -d`). **Protected names always skipped** (main, master, dev, develop, stable, trunk, prod, production, `release/*`, `release-*`, `hotfix/*`, `hotfix-*`). Never touches remote refs. |
708
+ | `deleteMergedBranches` | boolean | Default `false`. After **all** sources land cleanly, delete each source branch locally (`git branch -d`). **Protected names always skipped** (main, master, dev, develop, stable, trunk, prod, production, head, plus `release/*`/`release-*`/`hotfix/*`/`hotfix-*` — pattern requires a separator and non-empty suffix; bare `release`/`hotfix` are not protected). Never touches remote refs. |
737
709
  | `deleteMergedWorktrees` | boolean | Default `false`. After success, remove any local worktree currently checked out on a source branch (`git worktree remove`). Protected tails always skipped. |
738
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
710
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
739
711
 
740
712
  ### `git_merge` — JSON shape (`format: "json"`)
741
713
 
@@ -768,7 +740,7 @@ The `push` object is present only when `push: "after"` was requested **and** eve
768
740
 
769
741
  **`outcome`** (per source): `fast_forward`, `rebase_then_ff`, `merge_commit`, `up_to_date`, or `conflicts`. Cleanup fields (`branchDeleted`, `worktreeRemoved`) are only emitted when the corresponding flag was set and the operation actually ran — both are omitted for up-to-date sources and are never populated on partial-failure runs.
770
742
 
771
- On conflict: top-level `ok` is `false`, the conflicting entry has `ok: false` with `conflictStage` (`"rebase"` or `"merge"`), `conflictPaths` (array of paths with unresolved markers), and an `error` code. Remaining sources are not attempted.
743
+ On conflict: top-level `ok` is `false`, the conflicting entry has `ok: false` with `conflictStage` (`"rebase"` or `"merge"`), `conflictPaths` (array of paths with unresolved markers), and an `error` code. The tool attempts `git rebase --abort` / `git merge --abort`. When abort succeeds, the tree is clean. When abort itself fails, the per-source `error` is `rebase_abort_failed` / `merge_abort_failed` (tree may still be mid-rebase/merge; `detail` carries abort stderr). Remaining sources are not attempted.
772
744
 
773
745
  ### `git_merge` — error codes
774
746
 
@@ -782,8 +754,10 @@ On conflict: top-level `ok` is `false`, the conflicting entry has `ok: false` wi
782
754
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
783
755
  | `source_not_found` (per source) | A source branch name does not resolve. |
784
756
  | `cannot_fast_forward` (per source) | `strategy: "ff-only"` refused because branches have diverged. |
785
- | `rebase_conflicts` (per source) | Rebase encountered conflicts. Repo state is cleaned before returning. |
786
- | `merge_conflicts` (per source) | Merge commit encountered conflicts. Repo state is cleaned before returning. |
757
+ | `rebase_conflicts` (per source) | Rebase encountered conflicts. The tool attempts `git rebase --abort`; on abort success the tree is clean. On abort failure see `rebase_abort_failed`. |
758
+ | `merge_conflicts` (per source) | Merge commit encountered conflicts. The tool attempts `git merge --abort`; on abort success the tree is clean. On abort failure see `merge_abort_failed`. |
759
+ | `rebase_abort_failed` (per source) | `git rebase --abort` failed after a conflict; tree may still be mid-rebase. `detail` carries abort stderr. |
760
+ | `merge_abort_failed` (per source) | `git merge --abort` failed after a conflict; tree may still be mid-merge. `detail` carries abort stderr. |
787
761
  | `merge_failed` (per source) | `git merge --ff-only` failed unexpectedly. `detail` carries stderr. |
788
762
  | `merge_base_failed` (per source) | `git merge-base` failed (usually unrelated histories). |
789
763
 
@@ -793,12 +767,13 @@ On conflict: top-level `ok` is `false`, the conflicting entry has `ok: false` wi
793
767
 
794
768
  | Parameter | Type | Notes |
795
769
  |-----------|------|-------|
796
- | `sources` | `string[]` | Source specs. 1–50 entries. Each entry is one of: a full/short SHA, an `A..B` / `A...B` range, or a branch name. Branch names expand to `onto..<branch>` (oldest-first). |
770
+ | `sources` | `string[]` | Source specs. 1–50 entries. Each entry is one of: a full/short SHA, an `A..B` / `A...B` range, or a branch name. Branch names expand to `onto..<branch>` (oldest-first). After SHA-reachability filtering, at most **100** commits are fed to `git cherry-pick` per call; oversize returns `cherry_pick_too_many_commits` with `picked` + `max`. |
797
771
  | `onto` | string | Destination branch. Defaults to the currently checked-out branch. Rejected when HEAD is detached. |
798
772
  | `deleteMergedBranches` | boolean | Default `false`. After all commits apply, delete each **branch-kind** source locally. Deletion uses **patch-id equivalence** by default — correct for cherry-pick workflows where SHA differs but diff is identical. Protected names always skipped; never touches remote refs. |
799
773
  | `deleteMergedWorktrees` | boolean | Default `false`. After success, remove any local worktree attached to a branch-kind source (`git worktree remove`). Protected tails always skipped. |
800
774
  | `strictMergedRefEquality` | boolean | Default `false`. When `true`, branch deletion uses strict SHA-reachability (`git branch -d` ancestry semantics) instead of patch-id equivalence. Use when you need git's exact ancestry guarantee rather than content equivalence. |
801
- | `workspaceRoot`, `rootIndex`, `format` | | Standard workspace pick + output format. |
775
+ | `onConflict` | `"abort"` \| `"pause"` | Default `"abort"`: on conflict, run `cherry-pick --abort` and roll back the whole range (unchanged behavior). `"pause"`: on conflict, leave the conflict and native cherry-pick sequencer state in place — commits already applied stay applied — so it can be resolved and resumed via `git_cherry_pick_continue` (below). |
776
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
802
777
 
803
778
  ### `git_cherry_pick` — JSON shape (`format: "json"`)
804
779
 
@@ -838,7 +813,26 @@ On conflict, the response has `ok: false` and a top-level `conflict` object:
838
813
  }
839
814
  ```
840
815
 
841
- Repo state is cleaned (`git cherry-pick --abort`) before returning no partially-applied index.
816
+ With the default `onConflict: "abort"`, the tool attempts `git cherry-pick --abort`. On abort success the tree is clean. On abort failure: top-level `error: cherry_pick_abort_failed`, and `conflict.abortFailed: true` (+ optional `abortDetail`); `CHERRY_PICK_HEAD` may remain.
817
+
818
+ With `onConflict: "pause"`, the tool does **not** abort — `conflict.paused: true` is added and `CHERRY_PICK_HEAD`/the sequencer are left in place. `applied` reflects commits from the same range that landed before the conflicting one (cheaply derived via `rev-list --count`, not rolled back). Resolve the conflict and call `git_cherry_pick_continue` to resume, or abort it explicitly with the same tool (`action: "abort"`):
819
+
820
+ ```json
821
+ {
822
+ "ok": false,
823
+ "onto": "main",
824
+ "picked": 3,
825
+ "applied": 1,
826
+ "results": [ ... ],
827
+ "conflict": {
828
+ "stage": "cherry-pick",
829
+ "paused": true,
830
+ "commit": "abcdef1",
831
+ "paths": ["src/foo.ts"],
832
+ "detail": "…git stderr…"
833
+ }
834
+ }
835
+ ```
842
836
 
843
837
  ### `git_cherry_pick` — error codes
844
838
 
@@ -847,10 +841,67 @@ Repo state is cleaned (`git cherry-pick --abort`) before returning — no partia
847
841
  | `unsafe_ref_token` | A source or `onto` contains characters outside the argv-safe subset. |
848
842
  | `onto_detached_head` | HEAD is detached and no `onto` was given. |
849
843
  | `working_tree_dirty` | Uncommitted changes present. Commit, stash, or discard before cherry-picking. |
844
+ | `cherry_pick_in_progress` | A cherry-pick is already in progress (`CHERRY_PICK_HEAD` set, e.g. left paused by a prior call). Response includes `commit`. Resolve via `git_cherry_pick_continue` first. |
850
845
  | `checkout_failed` | Could not switch to `onto`. |
851
846
  | `destination_not_found` | `onto` does not resolve to a commit. |
852
847
  | `source_not_found` | A source spec resolves to neither a branch, a range, nor a commit. |
853
848
  | `range_resolution_failed` | `git rev-list` failed to expand a range spec. |
849
+ | `cherry_pick_too_many_commits` | Expanded/deduped pick list exceeds the hard cap of 100. Response includes `picked` + `max`. |
850
+ | `cherry_pick_abort_failed` | `git cherry-pick --abort` failed after a conflict (`onConflict: "abort"`, the default); tree may still be mid-cherry-pick. |
851
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
852
+
853
+ ---
854
+
855
+ ### `git_cherry_pick_continue` — parameters
856
+
857
+ | Parameter | Type | Notes |
858
+ |-----------|------|-------|
859
+ | `action` | `"continue"` \| `"abort"` | Default `"continue"`. `"continue"`: precheck no unmerged paths remain, then resume the native sequencer (`git -c core.editor=true cherry-pick --continue`). `"abort"`: roll back to the pre-cherry-pick HEAD (`git cherry-pick --abort`, same helper `git_cherry_pick` uses). |
860
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
861
+
862
+ This tool is **stateless** — it probes `CHERRY_PICK_HEAD` live off `.git` on every call rather than depending on the pausing `git_cherry_pick` call, so it works for a cherry-pick left in progress by any means.
863
+
864
+ ### `git_cherry_pick_continue` — JSON shape (`format: "json"`)
865
+
866
+ Success:
867
+
868
+ ```json
869
+ { "ok": true, "action": "continue", "applied": 2, "headSha": "a1b2c3d…" }
870
+ ```
871
+
872
+ `applied` counts commits added to HEAD since the call started (the resolved pick plus any further picks the sequencer completed on its own).
873
+
874
+ If resuming lands on another conflict later in the same range, the response mirrors a paused `git_cherry_pick` call so the loop is resumable — call this tool again after resolving:
875
+
876
+ ```json
877
+ {
878
+ "ok": false,
879
+ "action": "continue",
880
+ "applied": 1,
881
+ "conflict": {
882
+ "stage": "cherry-pick",
883
+ "paused": true,
884
+ "commit": "def4567",
885
+ "paths": ["src/bar.ts"],
886
+ "detail": "…git stderr…"
887
+ }
888
+ }
889
+ ```
890
+
891
+ `action: "abort"` success:
892
+
893
+ ```json
894
+ { "ok": true, "action": "abort", "headSha": "a1b2c3d…" }
895
+ ```
896
+
897
+ ### `git_cherry_pick_continue` — error codes
898
+
899
+ | Code | Meaning |
900
+ |------|---------|
901
+ | `no_cherry_pick_in_progress` | `CHERRY_PICK_HEAD` is not set — nothing to continue or abort. |
902
+ | `cherry_pick_unresolved_paths` | `action: "continue"` called while conflicted paths are still unmerged. Response includes `paths`. Stage resolutions first. |
903
+ | `cherry_pick_continue_failed` | `git cherry-pick --continue` failed for a reason other than a new conflict (e.g. the resolved pick would produce an empty commit). Response includes `detail`. |
904
+ | `cherry_pick_abort_failed` | `git cherry-pick --abort` failed (`action: "abort"`); tree may still be mid-cherry-pick. |
854
905
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
855
906
 
856
907
  ---
@@ -864,7 +915,7 @@ For already-committed work, call **`git_push`** directly instead of creating an
864
915
  | `remote` | string | Remote to push to. Defaults to the remote inferred from the upstream tracking ref, or `origin` when `setUpstream` is true. |
865
916
  | `branch` | string | Branch to push. Defaults to the currently checked-out branch. Rejected on detached HEAD. |
866
917
  | `setUpstream` | boolean | Default `false`. Pass `-u` to set the upstream tracking ref; remote defaults to `origin`. |
867
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
918
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
868
919
 
869
920
  ### `git_push` — JSON shape (`format: "json"`)
870
921
 
@@ -893,7 +944,7 @@ For already-committed work, call **`git_push`** directly instead of creating an
893
944
  | `message` | string | — | When provided, creates an annotated tag; otherwise creates a lightweight tag. |
894
945
  | `ref` | string | `HEAD` | Commit/ref to tag. Ignored when `delete: true`. |
895
946
  | `delete` | boolean | `false` | Delete the named tag instead of creating it. |
896
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
947
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
897
948
 
898
949
  ### `git_tag` — JSON shape (`format: "json"`)
899
950
 
@@ -918,12 +969,46 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
918
969
 
919
970
  ---
920
971
 
972
+ ### `git_branch` — parameters
973
+
974
+ | Parameter | Type | Default | Notes |
975
+ |-----------|------|---------|-------|
976
+ | `action` | `"create"` \| `"delete"` \| `"rename"` | — | Required. Which branch operation to perform. |
977
+ | `name` | string | — | Required. Branch to create/delete, or the existing branch name for rename. |
978
+ | `from` | string | `HEAD` | Commit-ish to base a new branch on. `create` only; ignored otherwise. |
979
+ | `newName` | string | — | New branch name. Required when `action: "rename"`. |
980
+ | `force` | boolean | `false` | Force-delete an unmerged branch (`git branch -D`). `delete` only; never overrides protected-branch rejection. |
981
+ | `workspaceRoot`, `format` | — | — | Standard single-repo pick + output format. |
982
+
983
+ ### `git_branch` — JSON shape (`format: "json"`)
984
+
985
+ ```json
986
+ { "action": "create", "branch": "feature/x", "sha": "a1b2c3d4e5f6..." }
987
+ ```
988
+
989
+ `action` echoes the requested operation; `branch` is the resulting branch name (the created/deleted name, or the new name after a rename); `sha` is the branch tip — for `delete`, the tip SHA the branch pointed to immediately before removal.
990
+
991
+ ### `git_branch` — error codes
992
+
993
+ | Code | Meaning |
994
+ |------|---------|
995
+ | `unsafe_ref_token` | `name`, `from`, or `newName` contains disallowed characters. |
996
+ | `protected_branch` | `name` (source) or `newName` (rename target) is on the protected names list. Checked for every action, regardless of `force`. |
997
+ | `missing_new_name` | `action: "rename"` without a non-empty `newName`. |
998
+ | `ref_not_found` | `from` (create) or `name` (delete) did not resolve to a commit. |
999
+ | `branch_create_failed` | `git branch <name> <from>` exited non-zero (e.g. name already exists). `detail` carries stderr. |
1000
+ | `branch_delete_failed` | `git branch -d`/`-D` exited non-zero (e.g. unmerged without `force`, or deleting the checked-out branch). `detail` carries stderr. |
1001
+ | `branch_rename_failed` | `git branch -m` exited non-zero (e.g. target name already exists), or the SHA could not be resolved after a successful rename. `detail` carries stderr/diagnostic text. |
1002
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
1003
+
1004
+ ---
1005
+
921
1006
  ### `git_reset_soft` — parameters
922
1007
 
923
1008
  | Parameter | Type | Notes |
924
1009
  |-----------|------|-------|
925
1010
  | `ref` | string | Target commit: `HEAD~N`, branch name, or full/short SHA. |
926
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
1011
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
927
1012
 
928
1013
  ### `git_reset_soft` — JSON shape (`format: "json"`)
929
1014
 
@@ -937,22 +1022,66 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
937
1022
  |------|---------|
938
1023
  | `unsafe_ref_token` | `ref` contains characters outside the ancestor-safe token set. |
939
1024
  | `working_tree_dirty` | Working tree has uncommitted/unstaged changes; clean up before resetting. |
940
- | `status_failed` | `git status` failed unexpectedly. |
941
1025
  | `reset_failed` | `git reset --soft` failed (e.g. ref does not exist). |
942
1026
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
943
1027
 
944
1028
  ---
945
1029
 
1030
+ ### `git_revert` — parameters
1031
+
1032
+ | Parameter | Type | Default | Notes |
1033
+ |-----------|------|---------|-------|
1034
+ | `sources` | string[] | — | Required, 1–20 entries. Commits to revert, applied in order: SHA, branch/tag name, or ancestor notation (`HEAD~1`). Each validated as an ancestor-safe ref token. |
1035
+ | `noCommit` | boolean | `false` | Pass `--no-commit`: stage the revert(s) in the index/working tree without committing. Working tree is intentionally left staged in this case (documented exception to the tree-clean guarantee). |
1036
+ | `mainline` | int ≥ 1 | — | Parent number (`-m N`) to diff against — required when reverting a merge commit. |
1037
+ | `workspaceRoot`, `format` | — | — | Standard single-repo pick + output format. |
1038
+
1039
+ ### `git_revert` — JSON shape (`format: "json"`)
1040
+
1041
+ Success (committed):
1042
+
1043
+ ```json
1044
+ { "ok": true, "reverted": [{ "source": "a1b2c3d", "sha": "f9e8d7c…" }] }
1045
+ ```
1046
+
1047
+ Success (`noCommit: true` — no commits made):
1048
+
1049
+ ```json
1050
+ { "ok": true, "staged": true, "sources": ["a1b2c3d"], "stagedCount": 1 }
1051
+ ```
1052
+
1053
+ Conflict (aborted, tree left clean):
1054
+
1055
+ ```json
1056
+ { "ok": false, "aborted": true, "commit": "a1b2c3d…", "conflicts": ["shared.txt"], "detail": "..." }
1057
+ ```
1058
+
1059
+ `commit`/`detail` are omitted when unavailable. `reverted[].sha` is the new commit created by reverting `source`, in the same order as `sources` (one new commit per source when `noCommit` is `false`).
1060
+
1061
+ ### `git_revert` — error codes
1062
+
1063
+ | Code | Meaning |
1064
+ |------|---------|
1065
+ | `unsafe_ref_token` | A `sources` entry contains characters outside the ancestor-safe token set. |
1066
+ | `working_tree_dirty` | Working tree has uncommitted/unstaged changes; clean up before reverting. |
1067
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
1068
+
1069
+ Conflict / other `git revert` failures do **not** use the `error` field — see the `aborted: true` shape above (mirrors `git_cherry_pick`'s conflict reporting).
1070
+
1071
+ ---
1072
+
946
1073
  ### `git_stash_apply` — parameters
947
1074
 
948
1075
  | Parameter | Type | Default | Notes |
949
1076
  |-----------|------|---------|-------|
950
- | `index` | int | `0` | Stash index to apply/pop (`stash@{index}`). |
1077
+ | `index` | int | `0` | Stash index to apply/pop (`stash@{index}`). Max `10000`. |
951
1078
  | `pop` | boolean | `false` | When `true`, runs `git stash pop` instead of `git stash apply`. |
952
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
1079
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
953
1080
 
954
1081
  ### `git_stash_apply` — JSON shape (`format: "json"`)
955
1082
 
1083
+ Success (`applied: true`, no `error`):
1084
+
956
1085
  ```json
957
1086
  {
958
1087
  "applied": true,
@@ -962,32 +1091,77 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
962
1091
  }
963
1092
  ```
964
1093
 
965
- `output` is omitted when git produced no stdout/stderr text.
1094
+ On failure (`applied: false`):
1095
+
1096
+ ```json
1097
+ {
1098
+ "error": "stash_apply_failed",
1099
+ "applied": false,
1100
+ "stashIndex": 0,
1101
+ "popped": false,
1102
+ "output": "…",
1103
+ "conflictPaths": ["conflict.txt"]
1104
+ }
1105
+ ```
1106
+
1107
+ `error: "stash_apply_failed"` on every failed apply/pop. `conflictPaths` is omitted when empty (omit-empty rule); populated via `git diff --name-only --diff-filter=U`. The tree is left as git left it (no auto-abort). With `pop: true`, a conflicted pop retains the stash entry (git does not drop it until the apply succeeds). Markdown failure output lists conflict paths under a `Conflicts:` block when present. `output` is omitted when git produced no stdout/stderr text. `popped` mirrors the requested `pop` flag (whether pop was attempted), not whether the stash entry was removed.
966
1108
 
967
1109
  ### `git_stash_apply` — error codes
968
1110
 
969
1111
  | Code | Meaning |
970
1112
  |------|---------|
971
1113
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
1114
+ | `stash_apply_failed` | Apply/pop failed (also on the failure payload above with `applied: false`). |
972
1115
 
973
1116
  ---
974
1117
 
975
- ### `git_worktree_list` — JSON shape (`format: "json"`)
1118
+ ### `git_stash_push` — parameters
1119
+
1120
+ | Parameter | Type | Default | Notes |
1121
+ |-----------|------|---------|-------|
1122
+ | `message` | string | — | Stash subject (`git stash push -m <message>`). |
1123
+ | `includeUntracked` | boolean | `false` | Also stash untracked files (`git stash push -u`). |
1124
+ | `keepIndex` | boolean | `false` | Keep staged changes in the index after stashing (`git stash push --keep-index`). |
1125
+ | `paths` | string[] | — | Scope the stash to specific paths, relative to git root. Each path must resolve within the repo root or the call fails with `path_escapes_repo`. |
1126
+ | `workspaceRoot`, `format` | — | — | Standard single-repo pick + output format. |
1127
+
1128
+ ### `git_stash_push` — JSON shape (`format: "json"`)
1129
+
1130
+ Success:
1131
+
1132
+ ```json
1133
+ {
1134
+ "stashed": true,
1135
+ "ref": "stash@{0}",
1136
+ "sha": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
1137
+ "message": "On main: wip: pushed"
1138
+ }
1139
+ ```
1140
+
1141
+ Nothing to stash (git exits 0 printing "No local changes to save"):
976
1142
 
977
1143
  ```json
978
- { "worktrees": [{ "path": "/abs/path", "branch": "feature/x", "head": "a1b2c3d…" }] }
1144
+ { "stashed": false, "reason": "no_local_changes" }
979
1145
  ```
980
1146
 
981
- `branch` is `null` for detached-HEAD worktrees.
1147
+ ### `git_stash_push` error codes
1148
+
1149
+ | Code | Meaning |
1150
+ |------|---------|
1151
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
1152
+ | `path_escapes_repo` | A `paths` entry resolves outside the repository root. |
1153
+ | `stash_push_failed` | `git stash push` exited non-zero for a reason other than "nothing to stash" (see `detail`). |
1154
+
1155
+ ---
982
1156
 
983
1157
  ### `git_worktree_add` — parameters
984
1158
 
985
1159
  | Parameter | Type | Notes |
986
1160
  |-----------|------|-------|
987
- | `path` | string | Filesystem path for the new worktree. Relative paths are resolved from the git toplevel. |
988
- | `branch` | string | Branch to check out. Created from `baseRef` if it does not already exist. |
989
- | `baseRef` | string | Commit-ish for branch creation. Default: `HEAD`. Ignored when `branch` already exists. |
990
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
1161
+ | `path` | string | Filesystem path for the new worktree. Relative paths are resolved from the git toplevel. Sibling worktrees **outside** the git toplevel remain allowed (absolute or relative). Leading `-` / option-like basenames and NUL bytes are rejected with `invalid_paths`. The path is passed to git after `--` so it cannot be parsed as an option. |
1162
+ | `branch` | string | Branch to check out. Created from `baseRef` if it does not already exist. Trimmed before validation and spawn. |
1163
+ | `baseRef` | string | Commit-ish for branch creation. Default: `HEAD`. Ignored when `branch` already exists. Trimmed before validation and spawn. |
1164
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
991
1165
 
992
1166
  ### `git_worktree_add` — JSON shape (`format: "json"`)
993
1167
 
@@ -1001,6 +1175,7 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
1001
1175
  |------|---------|
1002
1176
  | `unsafe_ref_token` | `branch` or `baseRef` contains disallowed characters. |
1003
1177
  | `protected_branch` | `branch` is on the protected names list. |
1178
+ | `invalid_paths` | `path` has a leading `-` / option-like basename, contains a NUL byte, or otherwise fails argv-safe path checks. |
1004
1179
  | `worktree_add_failed` | `git worktree add` exited non-zero. `detail` carries stderr. |
1005
1180
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
1006
1181
 
@@ -1008,9 +1183,15 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
1008
1183
 
1009
1184
  | Parameter | Type | Notes |
1010
1185
  |-----------|------|-------|
1011
- | `path` | string | Path of the worktree to remove. |
1186
+ | `path` | string | Path of the worktree to remove. Same argv rules as add (leading `-` / NUL → `invalid_paths`; path passed after `--`). Sibling paths outside the toplevel are allowed when registered. |
1012
1187
  | `force` | boolean | Default `false`. Pass `--force` to allow removal with uncommitted changes. |
1013
- | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
1188
+ | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
1189
+
1190
+ ### `git_worktree_remove` — JSON shape (`format: "json"`)
1191
+
1192
+ ```json
1193
+ { "ok": true, "path": "/abs/worktree" }
1194
+ ```
1014
1195
 
1015
1196
  ### `git_worktree_remove` — error codes
1016
1197
 
@@ -1018,6 +1199,7 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
1018
1199
  |------|---------|
1019
1200
  | `cannot_remove_main_worktree` | `path` resolves to the main (non-linked) worktree. |
1020
1201
  | `worktree_not_found` | `path` is not registered as a worktree in this repo. |
1202
+ | `invalid_paths` | `path` has a leading `-` / option-like basename, contains a NUL byte, or otherwise fails argv-safe path checks. |
1021
1203
  | `worktree_remove_failed` | `git worktree remove` failed. Pass `force: true` if there are uncommitted changes. |
1022
1204
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
1023
1205
 
@@ -1027,9 +1209,9 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
1027
1209
 
1028
1210
  | Variable | Default | Notes |
1029
1211
  |----------|---------|-------|
1030
- | `GIT_SUBPROCESS_PARALLELISM` | CPU-based | Max concurrent git subprocesses for multi-root fan-out (`git_inventory`, `git_parity`, multi-root `git_log`). |
1212
+ | `GIT_SUBPROCESS_PARALLELISM` | CPU-based | Max concurrent git subprocesses for multi-root fan-out (`git_inventory`, `git_parity`, multi-root `git_log`, multi-root `git_grep`). |
1031
1213
  | `GIT_SUBPROCESS_TIMEOUT_MS` | `120000` | Per-subprocess timeout in ms; on expiry the child is killed (SIGTERM) and the call resolves as failed. Set `0` (or negative) to disable (unbounded). |
1032
- | `RETHUNK_GIT_TOOLS` | _(unset — all)_ | Comma-separated allowlist of exact tool names. Unset or empty → all 23 tools registered. Non-empty → only the listed tools; unknown names warned to stderr. If every listed name is unknown, zero tools are registered (restriction honored literally). The presets resource is always available. Example: `RETHUNK_GIT_TOOLS=git_status,git_diff_summary,git_diff,git_log,batch_commit,git_push`. |
1214
+ | `RETHUNK_GIT_TOOLS` | _(unset — all)_ | Comma-separated allowlist of exact tool names. Unset or empty → all 24 tools registered. Non-empty → only the listed tools; unknown names warned to stderr. If every listed name is unknown, zero tools are registered (restriction honored literally). The presets resource is always available. Example: `RETHUNK_GIT_TOOLS=git_status,git_diff_summary,git_diff,git_log,batch_commit,git_push`. |
1033
1215
 
1034
1216
  ## Resource
1035
1217
 
@@ -1037,41 +1219,47 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
1037
1219
  |-----|---------|
1038
1220
  | `rethunk-git://presets` | JSON snapshot of `.rethunk/git-mcp-presets.json` at the resolved git toplevel (or structured errors). |
1039
1221
 
1040
- ## Workspace root resolution
1222
+ Error payloads from this resource may include:
1041
1223
 
1042
- ### `absoluteGitRoots` (sibling clones)
1224
+ | Code | Meaning |
1225
+ |------|---------|
1226
+ | `no_workspace_root` | No MCP workspace root could be resolved for the session. |
1227
+ | `not_a_git_repository` | The resolved path is not inside a git repository. |
1228
+ | `preset_file_invalid` | Preset file failed to load (`kind`: `"invalid_json"` or `"schema"`). |
1229
+
1230
+ When the file is missing, the resource returns `fileExists: false` with an empty `presets` object (not an error).
1231
+
1232
+ ## Root resolution
1043
1233
 
1044
- When **`absoluteGitRoots`** is a **non-empty** string array, it **replaces** the normal workspace pick for that tool call:
1234
+ Every tool carries exactly **one** routing parameter:
1045
1235
 
1046
- - Each entry is passed through `path.resolve`, then resolved to a **git toplevel** via the same logic as `workspaceRoot`. Duplicate toplevels are dropped (stable order, first wins).
1047
- - **Maximum** **256** paths (same cap as `git_inventory` `maxRoots` upper bound).
1048
- - **Mutually exclusive** with **`workspaceRoot`**, **`rootIndex`**, and **`allWorkspaceRoots: true`**. Combining them returns `{ "error": "absolute_git_roots_exclusive" }`.
1049
- - **Mutually exclusive** with a **`preset`** argument on root resolution (`absolute_git_roots_preset_conflict`).
1050
- - **`git_inventory` only:** also mutually exclusive with **`nestedRoots`** or **`preset`** on the same call (`absolute_git_roots_nested_or_preset_conflict`).
1051
- - **Mutating** tools (`batch_commit`, `git_push`, `git_merge`, …) **omit** this parameter from their schema; callers must use `workspaceRoot` / MCP roots for writes.
1052
- - **Read tools** that use **`requireSingleRepo`** (`git_diff_summary`, …) accept at most **one** distinct toplevel from `absoluteGitRoots`; more than one returns `absolute_git_roots_single_repo_only`.
1236
+ | Tools | Parameter | Accepts |
1237
+ |-------|-----------|---------|
1238
+ | Fan-out: `git_status`, `git_inventory`, `git_parity`, `list_presets`, `git_log`, `git_grep` | `root` | string (one repo path) \| string[] (explicit repo list) \| `"*"` (every MCP root) |
1239
+ | All other 18 tools | `workspaceRoot` | string (one repo path) |
1240
+
1241
+ ### `root` forms (fan-out tools)
1242
+
1243
+ - **string** — resolve that one path (same semantics as `workspaceRoot`).
1244
+ - **string[]** — explicit repo list (sibling clones). Each entry is passed through `path.resolve`, then resolved to a **git toplevel**; duplicate toplevels are dropped (stable order, first wins). Max **256** paths (`root_list_too_many`); an entry that is not inside a git repo returns `invalid_root_path`; zero resolved toplevels returns `root_list_empty`. Cannot be combined with a `preset` argument (`root_list_preset_conflict`; `git_inventory` also rejects arrays combined with `nestedRoots` — `root_list_nested_or_preset_conflict`).
1245
+ - **`"*"`** — every `file://` root reported by the MCP client (capped at **256** resolved toplevels — same `root_list_too_many` as an explicit array); markdown output emits one `# {tool}` header with per-root subsections (`git_inventory` uses `### {gitTop}`; `git_status` uses `### MCP root: ...`), or combined JSON.
1053
1246
 
1054
1247
  Example — two sibling repos in one `git_status` call:
1055
1248
 
1056
1249
  ```json
1057
1250
  {
1058
1251
  "format": "json",
1059
- "absoluteGitRoots": [
1252
+ "root": [
1060
1253
  "/usr/local/src/com.github/Rethunk-AI/mcp-multi-root-git",
1061
1254
  "/usr/local/src/com.github/Rethunk-AI/rethunk-github-mcp"
1062
1255
  ]
1063
1256
  }
1064
1257
  ```
1065
1258
 
1066
- ### Default order (when `absoluteGitRoots` is absent or empty)
1067
-
1068
- Order applied when resolving which directory(ies) tools run against:
1259
+ ### Default order (when `root` / `workspaceRoot` is omitted)
1069
1260
 
1070
- 1. Explicit **`workspaceRoot`** on the tool call (highest priority).
1071
- 2. **`rootIndex`** (0-based) one `file://` MCP root when several exist.
1072
- 3. **`allWorkspaceRoots`: true** every `file://` root; markdown output emits one `# {tool}` header with per-root subsections (`git_inventory` uses `### {gitTop}`; `git_status` uses `### MCP root: ...`), or combined JSON.
1073
- 4. **`preset`** set and multiple roots — first root whose git toplevel defines that preset (respecting **`workspaceRootHint`** on the preset entry when present).
1074
- 5. Otherwise the first `file://` root reported by the MCP client through **`roots/list`**.
1075
- 6. **`process.cwd()`** if no file roots (e.g. CI with explicit `workspaceRoot`).
1261
+ 1. **`preset`** set and multiple MCP roots (fan-out tools) — first root whose git toplevel defines that preset (respecting **`workspaceRootHint`** on the preset entry when present).
1262
+ 2. Otherwise the first `file://` root reported by the MCP client through **`roots/list`**.
1263
+ 3. **`process.cwd()`** if no file roots (e.g. CI with explicit `workspaceRoot`).
1076
1264
 
1077
1265
  Roots come from active MCP sessions (**`FastMCP` with `roots: { enabled: true }`** in code); there is no fixed `cwd` in server config. This is what allows one globally installed server to follow the workspace opened in VS Code, Claude Code, Cursor, or any other roots-capable MCP client.